From 70b854123fe211203f4a8d0e046cf84e8574b589 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Sat, 2 Aug 2025 22:48:05 +0900 Subject: [PATCH 01/44] TempSave --- .../main/res/drawable/newark_nj_1922_0.jpg | Bin 0 -> 143740 bytes .../main/res/drawable/newark_nj_1922_1.jpg | Bin 0 -> 137215 bytes .../com/mapconductor/core/OverlayProvider.kt | 27 ++++- .../core/controller/MapViewController.kt | 29 ++++- .../core/groundimage/GroundImage.kt | 66 +++++++++++ .../core/groundimage/GroundImageComponent.kt | 35 ++++++ .../core/groundimage/GroundImageEntity.kt | 14 +++ .../groundimage/GroundImageOverlayManager.kt | 111 ++++++++++++++++++ .../core/groundimage/GroundImageRenderer.kt | 38 ++++++ .../com/mapconductor/core/map/MapViewBase.kt | 4 +- .../com/mapconductor/core/map/MapViewState.kt | 2 +- .../core/polyline/PolylineComponent.kt | 2 +- .../arcgis/ArcGISMapViewController.kt | 4 +- .../ArcGISGroundOverlayRenderer.kt | 13 ++ .../com/mapconductor/googlemaps/GeoRect.kt | 23 ++++ .../googlemaps/GoogleMapViewController.kt | 29 ++++- .../GoogleMapGroundImageRenderer.kt | 91 ++++++++++++++ .../here/HereMapViewController.kt | 27 ++++- .../groundimage/HereGroundImageRenderer.kt | 13 ++ .../mapbox/MapboxMapViewController.kt | 4 +- .../groundImage/MapboxGroundImageRenderer.kt | 13 ++ 21 files changed, 527 insertions(+), 18 deletions(-) create mode 100644 example-app/src/main/res/drawable/newark_nj_1922_0.jpg create mode 100644 example-app/src/main/res/drawable/newark_nj_1922_1.jpg create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageRenderer.kt create mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt create mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt diff --git a/example-app/src/main/res/drawable/newark_nj_1922_0.jpg b/example-app/src/main/res/drawable/newark_nj_1922_0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1f4ae595ae1da7d13a7f436546a8bea35ffd7795 GIT binary patch literal 143740 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 literal 0 HcmV?d00001 diff --git a/example-app/src/main/res/drawable/newark_nj_1922_1.jpg b/example-app/src/main/res/drawable/newark_nj_1922_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c025cf693f6bbbb015e7b5b5203ee9a302c897a3 GIT binary patch literal 137215 zcmeFYWmFtZ*DgH6;O@a)2bT~aIKhJr?hZi)hhTw8f&~k1!QCx52~2`(a3?rHg9O)a zazD@eyze>dtn;n>IR8>Kv%0!>?W(@|>Z;m%S3S%=tN`$p<(1?CKp+s1jQ9gQtnxFx z@bq*Q<>7Jm;I^=Kv9#f~a)I*rSh(`=ar5#3#3X%OEvy`EJn1ZL?ChPz84lYz80hS+ z#ToP-tMaP4%G$iJSM+nY(ehK%w(@hd60v5Gl)x185%qz(LTx-P=zO3~&K{yZ;tc;N z7e$=^zRbfw_m7CDqd0@~?^@~fRW;~jUEFQx9&-zDS@H7m(FqB0^9eo{65`{e>r@bBk(`SK*dUwWR|cv!jHyL#HYIMe;sYhmf)w~#d!Yr0{`8D|JG&m%)-+~0)dba7q18x{}XLqK2ZUFQ2`;t|55&5LjPNF zRTpb}Ti^dvaY0c*k$)HeA42~wF2?g4l>Y(JKX2go^Zjqv|Np?Vw)zitu3qj=|GXk= zD;^ss8>o%5rw4+7d_4aPe%4l^wl3~a3r`7qsD+&kkAtg?ofyx5U;V!}@L$bFa0Ee5 zp8xRkzuNwv_xk_j_hO8-{!N8tX2>n~h?guovO|7F)-xc&%%KN9}SuD@{o z5dwcC{FhyS;rb&4{z&*QyZ*xUM+p3p@LzWQh3k(H_#@%J?D`AWA0hBZ!hhNI7p^}- z;E#m=vgb0Mf8qKg1pY|)FT4K2^+yQ& zk?>!3{e|m~5cnhEzwG)8*B>G9N5X&E^%t%`Lg0^t|FY{ZTz`bX9|`|u*I&5)2!THm z{>!evaQzViex9X=w1gpA0eBBNuVVjY#vRYOi@p-jh&>t}izD=kb8-FIJo&WJG3+rQR&)~%R&WR(T zCpKOMt2$kiYsX~r3Ly;}h>9qGHv#m&v;&BQi~>SML);W600MwWNXVGzD98wz-!1*! z3<4B-K7Ld}X-x~a*KxUH2>b*DWgxW-?q61q7%kuAX~oyc9zV5+(;05P8Jc85vyR=+FVbXel zs}&KJ^Sze63E*-JkRSlGLJBlT9F%~jV>sV!4IXTjy~)h6XQp8U={XXd^Z#^se7+~c zt-%7+;29TuYtP30D$J8JtAzb(;pW}H_54fvwI#01n~*QfzsUN$eit9`PA}F< zflz9ywM?EbjyE)_IIG%VDnc-|xp7)o7^9Qe_?GdRxDAq5jVjsa;@=pz@2&W)*YQ9l zMrpI?J~Fa(MhM5Hr_lNDV+O<9%*t&bNX}x>?SZc15+NyN82k*9T!!j6HUd9#?b$Aj z)ydchHY(7(D42yZ$@`i6_YBx&Rnw!szoeqa$uYTW;Cu}`VI2wF-!0hB7%#nwPR-y{qIW@V7-9}d$eXBgA7KEAm65tSGaoyxcG2_2g|gXM5t?M z;%z0C@e4e-q_F^IRY-+2!C<*vShmJ)hNu!ntNTh!uaTwRvyw`9825I5rUDdAg*2=Mi-K3DBF{-edVr`7nw55X^YU zVi15&Z4Zs1{pi1A@N|W_REt)&LMu|Os2$CUN0{qgCj+{*j>v(iT920}h`!wHKLAc@NQe@d3Z2q6DQMF(JG1TY4QGN$aSm1X>2eJ(@`Jk&Lu)ImpGwA6Si2)tt+MbX-q6(^vHf~`CbVYB&CxKl()$&I1I!Fxjg{t zqCiwF2F;9u>Yy%H`v<^%Kg`o+zK?1y%kTX!!Q%(OZYrMSKw6XF@bFw1_f(k&=+qje zW>KD=IR4;uA_Pn}+hlO12WVZ4y`m+Tk^4H<4`p|;?$ zT%Zq>;kHuRL!+ramZ4;Y1N^}lS4kN>r4tHCs{c$}E~ZwV`zQvTy{5g*3y$VqwyB&+ zw|P4Qe!jJ2DXbFJdFHHS2ni8h_JsO*mYemNV@_|+g^G(Bhl{+i+^p}6h?$=at}zBd z|@ z-Fuu;wy&vhz0|!6(ANUT;*||4)2B? zU7S;~f7tuJF*O|YdUx~@`=e$t=?Co6W@|KS;%&@3ZDV`Ws%9+ev~qr1<9zk>;@qRxmmL>?A?3E%YaWa9C_*(mkpyk9 zqILGQmEab7Nh$FLcG_h0)`)gTXR)G;iavKO^(JaO*R`X8uw|lMZ#^i3UA6*2zFXN_1qaC zJ|io$X@_l(SH&-LhW(U2f94Vlj?FE7(R4%RH*A7d|MQgaGfOdX$8$A`&!cP53AlaN zS^9hXrEU5hj*@FjH!$h7X47B_o^_0qQs zzwEQLR3v4egk&gdg?85X=g}SPA!Q_S4zbs3)>W5&-S!8ROVlID@5&o?6B>8yIjB`{ zxq^D}^efJzf5!a4h-M2KNg{+gro9j94y(}hm%mVOOF(>&N~PMwco}oF^Q5>mwFbX+ z^k}0cO&4miA1eeVSUms?0>mvp&Od2=`mR|$U#Coml5N?yPUy;G&9B?8-o>NWBbQK$ z=~^wn}ZuN4-Ll#HTe84 z&f{@O_Qj`?njNNh>+%6m*mZjLtGIgk`V(Vc zFIS`7XJ(zohQ{T8!(E z4;^>jti{zCXEzT3y_fc6k2NOut+)@-|M`N`WcxzAgqKgf_ z^W(rkS*T^Az(=31%@Jem2ID$`h~Edp^@)x8Dh>t6x~QNqKuC5t)(<*RPrD3z`+4$( zHO73=1lmVotoFVKzzDv0!TWWh8`S$Dh|Cw|kT0`uN29DI+#}~~Ug$Y^ z)CKfPzZZ9#Eo-tx?SM`aBPAY<=68Z+lmmM}7BUCPO(|PVatF8>%@DQWDRQ|v{p&&W zR_OVM+Zvi`R8NEBg(RF)#i;Ez7a^Hg1$Y!n6o`-zg9Yxa1h?C+EUOmx`f(A1l%Ch9 zAdl0~ch#=IGut#RjNz!Hqj@YZf4oSs>xdl9=_ByVXYtjr88*swrd)*aV?|fOL<6q6 z*Fy~zJJ5Q($80cOk{8++ISNL<5=kTs_*q-=*i2wJ`TIg~1%a>B00D*p0pIef`SOQ1AW>3CV0e&K;vLO2G!U*e(&)R2^oZ9}aR*gAXe5sukLeUR^B)0oS z;Jl7N3!16tv7NFWq1C;$Kbt~4zL4Vmi5egV_!=4M31Vi80}B8lrqVUEPlu};l07_W zMsw<|CLG+ei?*8C^zapu=l73VWn4-CCjotzYth`VlC;Vv7mEkt_otJ%8A=VD@wJ?^ zbDkC(E+uE4{{Xkvv#R$3EBC8BgXF}OpM@?3X6!XZQrpV9k$Fv(iK(5Sh7nH$v{Bch zE&yt1R8mW!n*PSc(;t)ndag|y%LXQ1V4eUD>)VLbbJKyGogCueQ& zh0Z(3*r1Jr(gdvAHlR!ZOYovpAS3V7`p;)^OmBt?bK{G~=RI@Y<|@u!7Jpv`R%cNV zQmqR2Q4pp_WX7)P1{d(OoxE)<%$!tg{&j?{t6Y9@-OU#fE_fYxs6(^LY@tqAyEKn2 zT4k3u@tstH^rRg$aYTL_+rX0CjMYJyKz}L3Fbz%0I=t%DvIFM8IRP0{qR+sB#S9;w zyP_X`yLY3vNDcz5bss&lKbn7wD<d$DcB#;D&(=fE1+&p^j)k3R@Q_(QRBuE9O zvBK_csh@xfB`+w-z5p=O=>tRoyo4+KQ{Dx-0%GXUK8z4WWjpP4;*euw*ThbxZTUCz zi&w!1FmaudT9smz5=9|PN^V7;_a)=XQ#YZY?z&F?UpTgEmLL3|>yg~!w}Nbf>(?#e zH=OQ;CSJjJ0h-O_t@VNn?s^5iG8E}XKe#GasNz?CDO350h7AHE_U~9U%BD%hZDcel zDxoi47_$XDZvJF%S!^uas$1Rt-1Bkjl)DPxAFC?>w1yJ#FSG4`{!#)iQD7ff%}(3R z9srS=4*=&1CyS;wYOY6zb|?2W%j>@15$2rTudbLwc|6z23tXs(bb-J8MCzO*l_h8M z)KgyG6e9yn8wVAayn{ZAvAWRPK8=g`+>Y=@&}(hKRQxF$dvqrH*6>aHuYPDmw}jtL z)w4s98stHf&qO5R3L7V0GC{s^;3eGZ9jldO<0QVyU5~&V-&rG4i%e?0Dfhj=snT6- zKfiughX%(+;an$es~j1fF;p_1K;B{YU|vvc+Vg;q;@ zD%y>_7JyrtCq32!x7git8%v%8X@+xy9#oHB;Usy~zbO|ft#_Oafw@vfg)OMk^3kkn zJB@KbD+D)`#NKIj=rn*A4)!zeeQ;F0z$L9qBu`ewgp-&f4@l2fmxtE`@0QH4fr<27 z128 zNo(S}4~HRSij zj0n|WDqg;Y;TDx8x+7~><*S0ciQ5KV1hF(^8%hj}RM-2NV@&M2IdC55wN%n2R_GTh z>F*}x>O!=|!r@9nPCl9$3H0y@92_;yOKen!&*T>`e(gcXwBilyK1l_u#SiGypW=p1 zv#W-1TuVHnFR_QO+w0e20^Z>C+$#WC{_!o40z-jEG(qY4dS-r(S23+4X8rq=LgCz` zg^KE76f)gof$0=WCcMPeM~)|$j69sRdh?9-~xoN#(>{B2-w+1SSBHmjRPo zZCF7|aHFc}*oA1#GBL%nlHBRlAse}FG{O}dEPZ82T#0DNd#pttbH+EPXyd|j{8X$ALer365%X@!s3*ZW6{K&vyDMG| z0rvqJ!(6kIy$RHEgqy^2;q_-Kb$B=&h>2Rc!6i1zwi{U(d1?i#EZ=ByxO*oIa}Q*G-mOVmwmg1(dREAJXFBH9t!tmvQ{IUWH|oRWEyiQ) z=fx9vZMUZNft@e!KJn8G!yL`|(+Ha5S!|Wt=#K5Q7wx-=6Dg%-EDN8$_9vYTq5`OW z=fHC0$_t^IhraqkjdU)?#D%iF5u>$K>j%H^Db#_)G;#xY{6_e9-B zIz*rI~ z#KqnA5dpreqhpXX^)3yj#uc4>p^&rHC9r9>XRgFfBR@p|0L*$Nlh0q{Z1|l-a`4T- zXUDf!y)M6$30sd{C&LphzGy{tE*PKus5?Y_tp=ONP3864hEl1QEI2VML#j)y-;1F} zsOk3K{o)_KIF1_C9>@-KoqhnkH`gBCC`P`sXNA>m*`Z^ zCDU)6E2HKQ8PzC)bPI0fK~pDL-nm z7(;25T!V#0r^dNA952DhJ-l(Dyh6D1C+WJHjB0nR@tp+*RjZ78g*s1G2Cyg-dOp6V zj0H7k2P-wxozktaeap*Jd85Tm_HmpuP4&?2j9Ni~=twf824v6ud!Rsbg)6-N{}`lJ=Z|K|h>0wRXL+K_vHcz#g+h3#3*d>%5c!*JSuu z;0$A{NU5-S(fAui7*aC+iHa)w(vxvzX@w`@@_@a!iw-q5oO^CP1KAeR)w(LI z>gwW0lw#8H2Qt|dx`u(WD~4JAeXh{~e7R1|2Ni+Q#k#e}WO*HSUgtKOJxuH~n8J-J z_R{)DmI*APPKil?c_wBWnS~XmvQ4MD{pMc|ZvG#mp139t+_XVuT>H-=oKVLxQz{KV zS9byw_@07O6w}#D9G+IYcY1!fZ+m=zJ{e z!%JEN5a9heQIf=ta>o#!}4!ZNTz9$1A=2L?Y{vY*Y#^7}ondtKrP2ujpe3 zHt@bk2)`xC_8<1=lN#XFZr}BB(LQBot{HzcRDg%9?`~EG5>A@xj{2@wpA4*fv32t)F-o?B+LqEgDyChHS~s*OkR z?Smqw$@}m|KmnVROvI4>&S;nS_=F-2x>TLQCae~wk3H?wzpqnr)^}v&z$IVv4d=?x z$%KtUg9hRo0UbPnTU%@1;ix|kDfJDkYs`pLDJCyms1R9vgT=+Y=~%!bHD{$WB65c3r#^(XS}r^P02_D zH=zh)N1$aKG_K^5#w;0BQN17?Eo^gpMlC3ML{ICZQ-G_+CyQDDYbBK+K@K2e zyu1?-J^9b*1lM%0NC12;%_W{ zz{Q#*p{!B!V%{EyPG}ZGMhZib6o8GKioK=zk+A&qG$*$?38i_sCsvEAURgIgjAVwF zLhOHprhKPgCQ0a8oN}qvyPumRVL11sQRRl2y!yCjXqHI#m$i2gBQFt|0%Kd54D5sF} zB*ya9`p*^dYvK^4sKG%S&FNjjyw2q5H*%-onMZ1LZ+Yb%bSQGk_gqb*emO4}B{fQS z)A-J+aCa5(;Yx{b^43pM2+Cn7k@UMU(uNh##($#*%~g@X1Tt4wc3+cuMTRf z!KY+2T{H!8g-{;e98o_atC+GE{|+gX2}qy&T+iG%r$VrR1cwzHuVdZx3F9~e%~5RA z9g{?JLJpICUZfJCjyQcT-&9JlXcu5Q5pUQN`x6$g zg`I@24>#s9JKBJdq?Ei`ske%W`8NRRR7J|623SLjoPp=kcSfJZvyY7R|2J|Erh$9ew@Z4oX1i?>-X=u9Aopq_Pnov^5!N zcIB}eogsAEn$GiGT`Q0WdffVx!DwDvu2z7^GU_})+C6H)a>hOPx#HA>UvL1hX)Uf;X7z9(m}>T|7N)0t zWm3&nO+umlB0HEGQvZRSb;V8I<~+Aeqjbxznw+@4iqk-KzYsNOM7v;q@a%0Uu%oug z7^N>QrVy1;-ImQhB1tIeGq(sg|C~7F1X|km3OaFKgEr?|yr5FaDLmgjS|2No))$3l zcs^xy(s-G}F=nd+Hne&2rLa>&OV!v^rN1t|!SEc4Hm44$yyZnM6KsAgL_!guTcn`# z&f=80i%`_Pf&fOXILI|ouA^My$~l=p&DE6G9lGAsi9@$M+wr*K2`eybDCW0KB4R4o zlnb#xe%88In{4E}cE$dloEz5~i>Xw1R|t+pZpF1lQTdYV+ov)yAW68IqL+%F*c?tp z>!bm066XmsCi|q^Eid^d_QmXQ^UEP$K9;SOFw4A~tL+DX72@NTWd3p6k&%0m{;Y~2 zUOd3&L~QQn#r<(3NCAc>f~&L3{s!$zh#XfN@3_iewf71qxnoXU#ufDfj#D+Mr}n1F zh)M~U_#;6Fio_233);~9mJ_(T${Y5h)F@J8&H`o^o_ROZUlBJdPcx88Cn<`)23o;N zISa~VUP9i#Ar#ZjuYqg4mBxf6P7;!cTwu3w=frB#2X(g`;8Hlvt51=kY44mC(mVjJ zhk)ZAR?|X~ze2|)tD--RXEh^LiY^=!PA5`9Eq4r6zo2yyz(CiNPRBnlz|jJIyX#+q zPL_xyDFYBaEgI9pI>INq@-LdA>F$J=!u>>JzZf9bwQ#<}Y20iv<|--H&CUW1Sq_xR zs_F^IJ+nX1c%DpyaA|#{KkI6Kh5x}tg?9n+TA35)))lKUU2vv!GG=r;I`zzt4SJ!G zJ7ig%HG)S&t~n`Dstd@+Ps(`FQE=s*H-+M*AJVVo6JT#~`X+w7P`FxmP)?|hP^`P= z(NiJkB+TcHCEvsuQrNj#Zx(;b5KZhZ|2)aoFj|DZXJbs6|2~U;<2v9?`FynoX^RevP)u}!c{W~Q+A@iKE0OF z)*o=$Yit@dqunL^1ROkc0t8I1MIpl-cN}6E2*tiqbx>Qag%UQrPX@3Alvd)( zwtZMM_Q>Hz-roZr&piw9Gv`@Bww+J6HwY{hQ494U$bwLTF@B7CItM6Mpdz6@H_lBW zC(K}ZOo2(b_LEx{C-`8(!x3Ba^&oNB^KNbMey$F=#gAp^XFNLIn@I0|{qu hdX; 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 I4~jqk+2w0Z2><{9 literal 0 HcmV?d00001 diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index 27aa3d43..4b7c3028 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.info.InfoBubbleEntry import com.mapconductor.core.map.MapOverlay import com.mapconductor.core.map.MapOverlayRegistry @@ -62,12 +63,14 @@ open class MapViewScope { val bubbleFlow = MutableStateFlow>(emptyList()) val polylineFlow = MutableStateFlow>(emptyList()) val circleFlow = MutableStateFlow>(emptyList()) + val groundImageFlow = MutableStateFlow>(emptyList()) fun buildRegistry(): MapOverlayRegistry { val registry = MapOverlayRegistry() registry.register(MarkerOverlay(markerFlow)) registry.register(CircleOverlay(circleFlow)) registry.register(PolylineOverlay(polylineFlow)) + registry.register(GroundImageOverlay(groundImageFlow)) return registry } } @@ -77,7 +80,7 @@ class MarkerOverlay( ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewController<*, *, *>, + controller: MapViewController<*, *, *, *>, ) { controller.addMarkers(data) } @@ -88,7 +91,7 @@ class CircleOverlay( ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewController<*, *, *>, + controller: MapViewController<*, *, *, *>, ) { controller.addCircles(data) } @@ -104,12 +107,23 @@ class PolylineOverlay( ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewController<*, *, *>, + controller: MapViewController<*, *, *, *>, ) { controller.addPolylines(data) } } +class GroundImageOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewController<*, *, *, *>, + ) { + controller.addGroundImages(data) + } +} + val LocalPolylineCollector = compositionLocalOf>> { error("Polyline must be under the ") @@ -120,10 +134,15 @@ val LocalCircleCollector = error("Circle must be under the ") } +val LocalGroundImageCollector = + compositionLocalOf>> { + error("GroundImage must be under the ") + } + @Composable fun CollectAndRenderOverlays( registry: MapOverlayRegistry, - controller: MapViewController<*, *, *>, + controller: MapViewController<*, *, *, *>, ) { registry.getAll().forEach { overlay -> @Suppress("UNCHECKED_CAST") diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index 6f1a0d73..2dfc0372 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -7,6 +7,9 @@ import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.geocell.HexCell import com.mapconductor.core.geocell.HexCoord import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.groundimage.GroundImageOverlayManager +import com.mapconductor.core.groundimage.GroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler @@ -19,13 +22,14 @@ import com.mapconductor.core.polyline.PolylineRenderer import com.mapconductor.core.polyline.PolylineState import kotlinx.coroutines.CoroutineScope -interface MapViewController { +interface MapViewController { val holder: MapViewHolder<*, *> val coroutine: CoroutineScope val markerOverlayManager: MarkerOverlayManager val hexGeocell: HexGeocell val circleManager: CircleManager val polylineOverlayManager: PolylineOverlayManager + val groundImageOverlayManager: GroundImageOverlayManager suspend fun addMarkers(data: List) @@ -35,6 +39,10 @@ interface MapViewController { suspend fun updatePolyline(state: PolylineState) + suspend fun addGroundImages(data: List) + + suspend fun updateGroundImage(state: GroundImageState) + suspend fun addCircles(data: List) suspend fun updateCircle(state: CircleState) @@ -60,8 +68,8 @@ data class SearchRangeAnalysis( val markersInRange: List, ) -abstract class BaseMapViewController : - MapViewController { +abstract class BaseMapViewController : + MapViewController { abstract val markerRenderer: MarkerRenderer override val markerOverlayManager: MarkerOverlayManager by lazy { @@ -80,6 +88,15 @@ abstract class BaseMapViewController + + override val groundImageOverlayManager: GroundImageOverlayManager by lazy { + createGroundImageOverlayManager().also { overlayManager -> + groundImageRenderer.init(overlayManager) + onGroundImageOverlayManagerInitialized(overlayManager) + } + } + protected open fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { // Stub } @@ -88,10 +105,16 @@ abstract class BaseMapViewController) { + // Stub + } + protected abstract fun createMarkerOverlayManager(): MarkerOverlayManager protected abstract fun createPolylineOverlayManager(): PolylineOverlayManager + protected abstract fun createGroundImageOverlayManager(): GroundImageOverlayManager + var cameraMoveListener: (OnCameraMoveHandler)? = null var mapClickListener: OnMapEventHandler? = null var mapLongClickListener: OnMapEventHandler? = null diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt new file mode 100644 index 00000000..9cea665d --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt @@ -0,0 +1,66 @@ +package com.mapconductor.core.groundimage + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import android.os.Parcelable +import com.mapconductor.core.StateFlowDelegate +import com.mapconductor.core.features.GeoRectBounds +import android.graphics.drawable.Drawable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +class GroundImageState( + bounds: GeoRectBounds, + image: Drawable, + alpha: Float = 1.0f, + id: String? = null, + extra: Parcelable? = null, +) { + val id = (id ?: generateId(bounds, image, alpha, extra)).toString() + + var bounds by StateFlowDelegate(bounds) + var image by mutableStateOf(image) + var alpha by mutableStateOf(alpha) + var extra by mutableStateOf(extra) + + fun fingerPrint(): GroundImageFingerPrint = GroundImageFingerPrint( + id = id.hashCode(), + bounds = bounds.hashCode(), + image = image.hashCode(), + alpha = alpha.hashCode(), + extra = extra?.hashCode() ?: 0, + ) + + fun asFlow(): Flow = + snapshotFlow { fingerPrint() }.distinctUntilChanged() + + private fun generateId( + bounds: GeoRectBounds, + image: Drawable, + alpha: Float, + extra: Parcelable? + ): Int { + var result = bounds.hashCode() + result = 31 * result + image.hashCode() + result = 31 * result + alpha.hashCode() + result = 31 * result + (extra?.hashCode() ?: 0) + return result + } + + override fun equals(other: Any?): Boolean = + (other as? GroundImageState)?.hashCode() == this.hashCode() + + override fun hashCode(): Int = fingerPrint().hashCode() +} + +data class GroundImageFingerPrint( + val id: Int, + val bounds: Int, + val image: Int, + val alpha: Int, + val extra: Int, +) + +typealias OnGroundImageEventHandler = (GroundImageState) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt new file mode 100644 index 00000000..5c65e824 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt @@ -0,0 +1,35 @@ +package com.mapconductor.core.groundimage + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember +import com.mapconductor.core.MapViewScope +import com.mapconductor.core.features.GeoRectBounds +import android.graphics.drawable.Drawable +import android.os.Parcelable + +@Composable +fun MapViewScope.GroundImage(state: GroundImageState) { + val rememberState = remember { state } + SideEffect { + groundImageFlow.value += rememberState + } +} + +@Composable +fun MapViewScope.GroundImage( + bounds: GeoRectBounds, + image: Drawable, + alpha: Float = 0.5f, + id: String? = null, + extra: Parcelable? = null, +) { + val state = GroundImageState( + bounds = bounds, + image = image, + alpha = alpha, + id = id, + extra = extra, + ) + GroundImage(state) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt new file mode 100644 index 00000000..ab2e4f13 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt @@ -0,0 +1,14 @@ +package com.mapconductor.core.groundimage + +interface GroundImageEntity { + val groundImage: ActualGroundImage + val state: GroundImageState + val stateHashCode: Int +} + +data class GroundImageEntityImpl( + override val groundImage: ActualGroundImage, + override val state: GroundImageState, +) : GroundImageEntity { + override val stateHashCode: Int = state.hashCode() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt new file mode 100644 index 00000000..08a03dd4 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt @@ -0,0 +1,111 @@ +package com.mapconductor.core.groundimage + +import kotlinx.coroutines.sync.Semaphore + +interface GroundImageOverlayManager { + suspend fun addGroundImages(overlays: List) + + suspend fun updateGroundImage(overlay: GroundImageState) + + suspend fun clearGroundImages() + + fun getGroundImageState(id: String): GroundImageState? + + fun getAllEntities(): List> +} + +class GroundImageOverlayManagerImpl( + val onAdd: suspend (List) -> List, + val onChange: suspend (List>) -> List, + + val onRemove: suspend (List>) -> Unit, + val onPostProcess: (suspend () -> Unit)? = null, +) : GroundImageOverlayManager { + +val groundImageEntities = mutableMapOf>() + val semaphore = Semaphore(1) + + override suspend fun addGroundImages(images: List) { + semaphore.acquire() + val previous = groundImageEntities.keys.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() + val removed = mutableListOf>() + + images.forEach { + if (previous.contains(it.id)) { + val prevEntity = groundImageEntities[it.id]!! + updated.add(object : GroundImageRenderer.UpdateParams { + override val entity = GroundImageEntityImpl( + groundImage = prevEntity.groundImage, + state = it + ) + override val prevEntity = prevEntity + }) + previous.remove(it.id) + } else { + added.add(it) + previous.remove(it.id) + } + } + + previous.forEach { remainId -> + groundImageEntities.remove(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + if (added.isNotEmpty()) { + val actualOverlays = onAdd(added) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val state = added[index] + val entity = GroundImageEntityImpl( + groundImage = it, + state = state + ) + groundImageEntities[state.id] = entity + } + } + } + + if (updated.isNotEmpty()) { + val actualOverlays = onChange(updated.toList()) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val state = updated[index].entity.state + val entity = GroundImageEntityImpl( + groundImage = it, + state = state + ) + groundImageEntities[state.id] = entity + } + } + } + + if (removed.isNotEmpty()) { + onRemove(removed) + } + + onPostProcess?.invoke() + semaphore.release() + } + + override suspend fun updateGroundImage(overlay: GroundImageState) { + semaphore.acquire() + // Implement fine-grained update logic if needed + semaphore.release() + } + + override suspend fun clearGroundImages() { + semaphore.acquire() + val entities = groundImageEntities.values.toList() + onRemove(entities) + groundImageEntities.clear() + semaphore.release() + } + + override fun getGroundImageState(id: String): GroundImageState? = groundImageEntities[id]?.state + + override fun getAllEntities(): List> = groundImageEntities.values.toList() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageRenderer.kt new file mode 100644 index 00000000..d123fe47 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageRenderer.kt @@ -0,0 +1,38 @@ +package com.mapconductor.core.groundimage + +import com.mapconductor.core.map.MapViewHolder +import kotlinx.coroutines.CoroutineScope + +interface GroundImageRendererFactory { + fun create( + onAdd: suspend (List) -> List, + onChange: suspend (List>) -> List, + onRemove: suspend (List>) -> Unit, + onPostProcess: (suspend () -> Unit)? = null, + ): GroundImageOverlayManager +} + +interface GroundImageRenderer { + interface UpdateParams { + val entity: GroundImageEntity + val prevEntity: GroundImageEntity + } + + fun init(groundImageOverlayManager: GroundImageOverlayManager) + + suspend fun addGroundImages(newGroundImages: List): List + + suspend fun removeGroundImages(removeEntities: List>) + + suspend fun changeGroundImages(changes: List>): List +} + +abstract class AbstractGroundImageRenderer : GroundImageRenderer { + protected lateinit var groundImageOverlayManager: GroundImageOverlayManager + abstract val holder: MapViewHolder<*, *> + abstract val coroutine: CoroutineScope + + override fun init(groundImageOverlayManager: GroundImageOverlayManager) { + this.groundImageOverlayManager = groundImageOverlayManager + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index d05185a3..4b8c1fa7 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -25,6 +25,7 @@ import com.mapconductor.core.CollectAndRenderOverlays import com.mapconductor.core.LocalCircleCollector import com.mapconductor.core.LocalMarkerCollector import com.mapconductor.core.LocalPolylineCollector +import com.mapconductor.core.LocalGroundImageCollector import com.mapconductor.core.MapViewScope import com.mapconductor.core.ResourceProvider import com.mapconductor.core.controller.MapViewController @@ -47,7 +48,7 @@ fun < SpecificState : MapViewState<*>, // Replace Any with a base MapViewController if you have one // Generic type for the actual Android Map View (e.g., com.google.android.gms.maps.MapView) - SpecificController : MapViewController<*, *, *>, + SpecificController : MapViewController<*, *, *, *>, ActualMapView : View, // Generic type for the actual Map SDK object (e.g., GoogleMap, HereMapSDK.MapController) ActualMap : Any, @@ -86,6 +87,7 @@ fun < LocalInfoBubbleCollector provides scope.bubbleFlow, LocalCircleCollector provides scope.circleFlow, LocalPolylineCollector provides scope.polylineFlow, + LocalGroundImageCollector provides scope.groundImageFlow, ) { with(scope) { content?.invoke(this) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt index a8d0af2b..05150d1e 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt @@ -87,7 +87,7 @@ interface MapOverlay { suspend fun render( data: List, - controller: MapViewController<*, *, *>, + controller: MapViewController<*, *, *, *>, ) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt index b1860d50..07b9f380 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt @@ -14,7 +14,7 @@ import android.os.Parcelable fun MapViewScope.Polyline(state: PolylineState) { val rememberState = remember { state } SideEffect { - polylineFlow.value = polylineFlow.value + rememberState + polylineFlow.value = polylineFlow.value + rememberState } } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt index 063cd3f4..4247ea67 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt @@ -46,7 +46,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IArcGISMapViewController : MapViewController { +interface IArcGISMapViewController : MapViewController { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -88,7 +88,7 @@ class ArcGISMapViewController( private val polylineRendererFactory: PolylineRendererFactory = DefaultArcGISPolylineRenderer(), override val circleManager: CircleManager = CircleManager(), -) : BaseMapViewController(), +) : BaseMapViewController(), IArcGISMapViewController { override val markerRenderer: MarkerRenderer = ArcGISMarkerRenderer( diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt new file mode 100644 index 00000000..a9152d4e --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt @@ -0,0 +1,13 @@ +package com.mapconductor.arcgis.groundoverlay + +import com.mapconductor.core.groundimage.GroundImageState + +class ArcGISGroundImageRenderer { + fun addOverlay(state: GroundImageState) { + // Not implemented + } + + fun updateOverlay(state: GroundImageState) { + // Not implemented + } +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt new file mode 100644 index 00000000..e212758f --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt @@ -0,0 +1,23 @@ +package com.mapconductor.googlemaps + +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoRectBounds + +fun GeoRectBounds.toLatLngBounds(): LatLngBounds? { + val sw = southWest ?: return null + val ne = northEast ?: return null + + return LatLngBounds( + LatLng(sw.latitude, sw.longitude), + LatLng(ne.latitude, ne.longitude) + ) +} + +fun LatLngBounds.toGeoRectBounds(): GeoRectBounds { + return GeoRectBounds( + southWest = GeoPoint(southwest.latitude, southwest.longitude), + northEast = GeoPoint(northeast.latitude, northeast.longitude) + ) +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt index 4bd2a67f..257c40d1 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -14,6 +14,7 @@ import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.CircleOptions +import com.google.android.gms.maps.model.GroundOverlay import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.Polyline @@ -24,6 +25,10 @@ import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.groundimage.GroundImageOverlayManager +import com.mapconductor.core.groundimage.GroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageRendererFactory +import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.MarkerOverlayManager @@ -35,6 +40,8 @@ import com.mapconductor.core.polyline.PolylineRenderer import com.mapconductor.core.polyline.PolylineRendererFactory import com.mapconductor.core.polyline.PolylineState import com.mapconductor.core.projection.WebMercator +import com.mapconductor.googlemaps.groundimage.DefaultGoogleMapGroundImageRenderer +import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer import com.mapconductor.googlemaps.marker.DefaultGoogleMapMarkerRenderer import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer import com.mapconductor.googlemaps.polyline.DefaultGoogleMapPolylineRenderer @@ -43,7 +50,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IGoogleMapViewController : MapViewController { +interface IGoogleMapViewController : MapViewController { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -67,7 +74,8 @@ class GoogleMapViewController( private val markerRendererFactory: MarkerRendererFactory = DefaultGoogleMapMarkerRenderer(), private val polylineRendererFactory: PolylineRendererFactory = DefaultGoogleMapPolylineRenderer(), override val circleManager: CircleManager = CircleManager(), -) : BaseMapViewController(), + private val groundImageRendererFactory: GroundImageRendererFactory = DefaultGoogleMapGroundImageRenderer(), +) : BaseMapViewController(), IGoogleMapViewController, OnCameraMoveStartedListener, OnCameraMoveCanceledListener, @@ -107,6 +115,19 @@ class GoogleMapViewController( onRemove = polylineRenderer::removeLines, ) + override val groundImageRenderer: GroundImageRenderer = + GoogleMapGroundImageRenderer( + holder = holder, + coroutine = coroutine, + ) + + override fun createGroundImageOverlayManager(): GroundImageOverlayManager = + groundImageRendererFactory.create( + onAdd = groundImageRenderer::addGroundImages, + onChange = groundImageRenderer::changeGroundImages, + onRemove = groundImageRenderer::removeGroundImages, + ) + init { setupListeners() markerRenderer.init(markerOverlayManager) @@ -192,6 +213,10 @@ class GoogleMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun addGroundImages(data: List) = groundImageOverlayManager.addGroundImages(data) + + override suspend fun updateGroundImage(state: GroundImageState) = groundImageOverlayManager.updateGroundImage(state) + override fun onCameraMove() { cameraMoveListener?.let { coroutine.launch { it(holder.map.cameraPosition) } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt new file mode 100644 index 00000000..471c92cb --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt @@ -0,0 +1,91 @@ +package com.mapconductor.googlemaps.groundimage + +import androidx.core.graphics.drawable.toBitmap +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.GroundOverlay +import com.google.android.gms.maps.model.GroundOverlayOptions +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.groundimage.AbstractGroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageEntity +import com.mapconductor.core.groundimage.GroundImageOverlayManager +import com.mapconductor.core.groundimage.GroundImageOverlayManagerImpl +import com.mapconductor.core.groundimage.GroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageRendererFactory +import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLngBounds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class DefaultGoogleMapGroundImageRenderer : GroundImageRendererFactory { + override fun create( + onAdd: suspend (List) -> List, + onChange: suspend (List>) -> List, + onRemove: suspend (List>) -> Unit, + onPostProcess: (suspend () -> Unit)? + ): GroundImageOverlayManager = + GroundImageOverlayManagerImpl( + onRemove = onRemove, + onAdd = onAdd, + onChange = onChange, + onPostProcess = onPostProcess, + ) +} + +class GoogleMapGroundImageRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope, +) : AbstractGroundImageRenderer() { + override suspend fun addGroundImages(newImages: List): List { + return withContext(coroutine.coroutineContext) { + return@withContext newImages.map { state -> + val bounds = state.bounds.toLatLngBounds() ?: return@withContext emptyList() + val image = BitmapDescriptorFactory.fromBitmap(state.image.toBitmap()) + val alpha = state.alpha + val options = + GroundOverlayOptions() + .image(image) + .positionFromBounds(bounds) + .transparency(alpha) + holder.map.addGroundOverlay(options)?.also { + it.tag = state.id + } + } + } + } + + override suspend fun removeGroundImages(removeEntities: List>) { + coroutine.launch { + removeEntities.forEach { params -> params.groundImage.remove() } + } + } + + override suspend fun changeGroundImages(changes: List>): List { + return withContext(coroutine.coroutineContext) { + return@withContext changes.map { params-> + val groundOverlay = params.entity.groundImage + val finger = params.entity.state.fingerPrint() + val prevFinger = params.prevEntity.fingerPrint + if (finger.bounds != prevFinger.bounds) { + val bounds = params.entity.state.bounds.map { GeoRectBounds.toLatLngBounds() } + groundOverlay.position = bounds + } + groundOverlay.transparency = params.prevEntity.state.alpha + groundOverlay.setImage(BitmapDescriptorFactory.fromBitmap(params.entity.state.image.toBitmap()) + return@map groundOverlay + } + } + } + + private fun GeoRectBounds.toLatLngBounds(): LatLngBounds? { + val sw = southWest ?: return null + val ne = northEast ?: return null + return LatLngBounds( + LatLng(sw.latitude, sw.longitude), + LatLng(ne.latitude, ne.longitude) + ) + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt index f03825c2..aba62fc1 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt @@ -13,6 +13,7 @@ import com.here.sdk.mapview.MapMarker import com.here.sdk.mapview.MapMeasure import com.here.sdk.mapview.MapPolygon import com.here.sdk.mapview.MapPolyline +import com.here.sdk.mapview.MapImage import com.here.sdk.mapview.MapScene import com.here.sdk.mapview.MapView import com.here.time.Duration @@ -23,6 +24,10 @@ import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.groundimage.GroundImageOverlayManager +import com.mapconductor.core.groundimage.GroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageRendererFactory +import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.MapViewState.MoveCameraCallback @@ -45,7 +50,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IHereMapViewController : MapViewController { +interface IHereMapViewController : MapViewController { fun moveCamera( dstPosition: MapCameraPosition, listener: MoveCameraCallback? = null, @@ -68,8 +73,9 @@ class HereMapViewController( ), private val markerRendererFactory: MarkerRendererFactory = DefaultHereMapMarkerRenderer(), private val polylineRendererFactory: PolylineRendererFactory = DefaultHereMapPolylineRenderer(), + private val groundImageRendererFactory: GroundImageRendererFactory = DefaultHereMapGroundImageRenderer(), override val circleManager: CircleManager = CircleManager(), -) : BaseMapViewController(), +) : BaseMapViewController(), IHereMapViewController, MapCameraListener, TapListener, @@ -104,6 +110,19 @@ class HereMapViewController( onRemove = polylineRenderer::removeLines, ) + override val groundImageRenderer: GroundImageRenderer = + HereMapGroundImageRenderer( + holder = holder, + coroutine = coroutine, + ) + + override fun createGroundImageOverlayManager(): GroundImageOverlayManager = + groundImageRenderer.create( + onAdd = groundImageRenderer::addLines, + onChange = groundImageRenderer::changeLine, + onRemove = groundImageRenderer::removeLines, + ) + override suspend fun clearOverlays() { markerOverlayManager.clearOverlays() polylineOverlayManager.clearOverlays() @@ -123,6 +142,10 @@ class HereMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun addGroundImages(data: List) = groundImageOverlayManager.addGroundImages(data) + + override suspend fun updateGroundImage(state: GroundImageState) = groundImageOverlayManager.updateGroundImage(state) + init { setupListeners() markerRenderer.init(markerOverlayManager) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt new file mode 100644 index 00000000..020d94fe --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt @@ -0,0 +1,13 @@ +package com.mapconductor.here.groundimage + +import com.mapconductor.core.groundimage.GroundImageState + +class HereGroundImageRenderer { + fun addOverlay(state: GroundImageState) { + // Not implemented + } + + fun updateOverlay(state: GroundImageState) { + // Not implemented + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index efdef546..008b00f9 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -57,7 +57,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore -interface IMapboxMapViewController : MapViewController { +interface IMapboxMapViewController : MapViewController { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -103,7 +103,7 @@ internal class MapboxMapViewController( layerId = "polyline-layer", ), override val circleManager: CircleManager = CircleManager(), -) : BaseMapViewController(), +) : BaseMapViewController(), IMapboxMapViewController, CameraChangedCallback, OnMapClickListener, diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt new file mode 100644 index 00000000..d5d564d9 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt @@ -0,0 +1,13 @@ +package com.mapconductor.mapbox.groundImage + +import com.mapconductor.core.groundimage.GroundImageState + +class MapboxGroundImageRenderer { + fun addOverlay(state: GroundImageState) { + // Not implemented + } + + fun updateOverlay(state: GroundImageState) { + // Not implemented + } +} From 19257b4abb8b763775da3a5551a425fd9f1bea30 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Mon, 4 Aug 2025 22:05:58 +0900 Subject: [PATCH 02/44] TempSave3 --- .../com/mapconductor/core/OverlayProvider.kt | 2 +- .../core/controller/MapViewController.kt | 3 +- .../core/groundimage/GroundImageEntity.kt | 4 +- .../arcgis/ArcGISMapViewController.kt | 25 ++++++++++ .../mapconductor/arcgis/ArcGISTypeAlias.kt | 1 + .../ArcGISGroundOverlayRenderer.kt | 22 +++++++-- .../GoogleMapGroundImageRenderer.kt | 7 +-- .../com/mapconductor/here/HereMapTypeAlias.kt | 2 + .../here/HereMapViewController.kt | 38 ++++++++++++-- .../groundimage/HereGroundImageRenderer.kt | 49 +++++++++++++++++-- .../mapbox/MapboxMapViewController.kt | 25 ++++++++++ .../mapconductor/mapbox/MapboxTypeAlias.kt | 1 + .../groundImage/MapboxGroundImageRenderer.kt | 22 +++++++-- 13 files changed, 175 insertions(+), 26 deletions(-) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index 7f160ac6..917b804b 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -121,7 +121,7 @@ class GroundImageOverlay( ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewController<*, *, *, *>, + controller: MapViewControllerAlias, ) { controller.addGroundImages(data) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index 15bcff5d..db206e64 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -100,6 +100,7 @@ abstract class BaseMapViewController override val groundImageOverlayManager: GroundImageOverlayManager by lazy { @@ -109,8 +110,6 @@ abstract class BaseMapViewController) - abstract val circleRenderer: CircleRenderer override val circleOverlayManager: CircleOverlayManager by lazy { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt index ab2e4f13..f8b6e8fd 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageEntity.kt @@ -3,12 +3,12 @@ package com.mapconductor.core.groundimage interface GroundImageEntity { val groundImage: ActualGroundImage val state: GroundImageState - val stateHashCode: Int + val fingerPrint: GroundImageFingerPrint } data class GroundImageEntityImpl( override val groundImage: ActualGroundImage, override val state: GroundImageState, ) : GroundImageEntity { - override val stateHashCode: Int = state.hashCode() + override val fingerPrint: GroundImageFingerPrint = state.fingerPrint() } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt index cdeac514..2bf72246 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt @@ -11,6 +11,7 @@ import com.arcgismaps.mapping.view.UpEvent import com.arcgismaps.mapping.view.extensions.motionEvent import com.mapconductor.arcgis.circle.ArcGISCircleRenderer import com.mapconductor.arcgis.circle.DefaultArcGISCircleRenderer +import com.mapconductor.arcgis.groundoverlay.ArcGISGroundImageRenderer import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer import com.mapconductor.arcgis.marker.DefaultArcGISMarkerRender import com.mapconductor.arcgis.polygon.ArcGISPolygonRenderer @@ -26,6 +27,9 @@ import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.groundimage.GroundImageOverlayManager +import com.mapconductor.core.groundimage.GroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.MarkerOverlayManager @@ -52,6 +56,7 @@ interface IArcGISMapViewController : ArcGISActualCircle, ArcGISActualPolyline, ArcGISActualPolygon, + ArcGISActualGroundImage, > { fun moveCamera( dstPosition: MapCameraPosition, @@ -107,6 +112,7 @@ class ArcGISMapViewController( ArcGISActualCircle, ArcGISActualPolyline, ArcGISActualPolygon, + ArcGISActualGroundImage, >(), IArcGISMapViewController { override val markerRenderer: MarkerRenderer = @@ -162,6 +168,10 @@ class ArcGISMapViewController( onRemove = circleRenderer::removeCircles, ) + override fun createGroundImageOverlayManager(): GroundImageOverlayManager { + TODO("Not yet implemented") + } + override val circleRenderer: CircleRenderer = ArcGISCircleRenderer( circleLayer = circleLayer, @@ -169,6 +179,14 @@ class ArcGISMapViewController( coroutine = coroutine, ) + override fun onGroundImageOverlayManagerInitialized(overlayManager: GroundImageOverlayManager) { + } + + override val groundImageRenderer: GroundImageRenderer = ArcGISGroundImageRenderer( + holder = holder, + coroutine = coroutine, + ) + override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } @@ -333,6 +351,13 @@ class ArcGISMapViewController( override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun addGroundImages(data: List) { + TODO("Not yet implemented") + } + + override suspend fun updateGroundImage(state: GroundImageState) { + TODO("Not yet implemented") + } override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISTypeAlias.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISTypeAlias.kt index a6f35e5d..4a216b4e 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISTypeAlias.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISTypeAlias.kt @@ -6,3 +6,4 @@ typealias ArcGISActualMarker = Graphic typealias ArcGISActualCircle = Graphic typealias ArcGISActualPolyline = Graphic typealias ArcGISActualPolygon = Graphic +typealias ArcGISActualGroundImage = Graphic diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt index a9152d4e..483d019d 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt @@ -1,13 +1,25 @@ package com.mapconductor.arcgis.groundoverlay +import com.arcgismaps.mapping.view.Graphic +import com.mapconductor.arcgis.ArcGISMapViewHolder +import com.mapconductor.core.groundimage.AbstractGroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageEntity +import com.mapconductor.core.groundimage.GroundImageRenderer import com.mapconductor.core.groundimage.GroundImageState +import kotlinx.coroutines.CoroutineScope -class ArcGISGroundImageRenderer { - fun addOverlay(state: GroundImageState) { - // Not implemented +class ArcGISGroundImageRenderer( + override val holder: ArcGISMapViewHolder, + override val coroutine: CoroutineScope + ) : AbstractGroundImageRenderer() { + override suspend fun addGroundImages(newGroundImages: List): List { + return emptyList() } - fun updateOverlay(state: GroundImageState) { - // Not implemented + override suspend fun changeGroundImages(changes: List>): List { + return emptyList() + } + + override suspend fun removeGroundImages(removeEntities: List>) { } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt index 471c92cb..b7eb65c6 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt @@ -70,11 +70,12 @@ class GoogleMapGroundImageRenderer( val finger = params.entity.state.fingerPrint() val prevFinger = params.prevEntity.fingerPrint if (finger.bounds != prevFinger.bounds) { - val bounds = params.entity.state.bounds.map { GeoRectBounds.toLatLngBounds() } - groundOverlay.position = bounds + params.entity.state.bounds.toLatLngBounds()?.let{ + groundOverlay.setPositionFromBounds(it) + } } groundOverlay.transparency = params.prevEntity.state.alpha - groundOverlay.setImage(BitmapDescriptorFactory.fromBitmap(params.entity.state.image.toBitmap()) + groundOverlay.setImage(BitmapDescriptorFactory.fromBitmap(params.entity.state.image.toBitmap())) return@map groundOverlay } } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt index dc757271..7151aecb 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt @@ -1,5 +1,6 @@ package com.mapconductor.here +import com.here.sdk.mapview.MapImage import com.here.sdk.mapview.MapMarker import com.here.sdk.mapview.MapPolygon import com.here.sdk.mapview.MapPolyline @@ -8,3 +9,4 @@ typealias HereMapActualMarker = MapMarker typealias HereMapActualCircle = MapPolygon typealias HereMapActualPolyline = MapPolyline typealias HereMapActualPolygon = MapPolygon +typealias HereMapActualGroundImage = MapImage diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt index e7472eee..0368f990 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt @@ -10,6 +10,7 @@ import com.here.sdk.mapview.MapCamera import com.here.sdk.mapview.MapCameraAnimationFactory import com.here.sdk.mapview.MapCameraListener import com.here.sdk.mapview.MapCameraUpdateFactory +import com.here.sdk.mapview.MapImage import com.here.sdk.mapview.MapMarker import com.here.sdk.mapview.MapMeasure import com.here.sdk.mapview.MapPolygon @@ -25,6 +26,10 @@ import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.groundimage.GroundImageOverlayManager +import com.mapconductor.core.groundimage.GroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageRendererFactory +import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.MapViewState.MoveCameraCallback @@ -42,6 +47,8 @@ import com.mapconductor.core.polyline.PolylineState import com.mapconductor.core.projection.WebMercator import com.mapconductor.here.circle.DefaultHereMapCircleRenderer import com.mapconductor.here.circle.HereMapCircleRenderer +import com.mapconductor.here.groundimage.DefaultHereMapGroundImageRenderer +import com.mapconductor.here.groundimage.HereMapGroundImageRenderer import com.mapconductor.here.marker.DefaultHereMapMarkerRenderer import com.mapconductor.here.marker.HereMapMarkerRenderer import com.mapconductor.here.polygon.DefaultHereMapPolygonRenderer @@ -53,7 +60,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IHereMapViewController : MapViewController { +interface IHereMapViewController : MapViewController { fun moveCamera( dstPosition: MapCameraPosition, listener: MoveCameraCallback? = null, @@ -75,16 +82,17 @@ class HereMapViewController( baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 ), private val markerRendererFactory: MarkerRendererFactory = DefaultHereMapMarkerRenderer(), - private val polylineRendererFactory: PolylineRendererFactory = - DefaultHereMapPolylineRenderer(), + private val polylineRendererFactory: PolylineRendererFactory = DefaultHereMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultHereMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), + private val groundImageRendererFactory: GroundImageRendererFactory = DefaultHereMapGroundImageRenderer(), ) : BaseMapViewController< MapCamera.State, HereMapActualMarker, HereMapActualCircle, HereMapActualPolyline, HereMapActualPolygon, + HereMapActualGroundImage, >(), IHereMapViewController, MapCameraListener, @@ -129,6 +137,10 @@ class HereMapViewController( coroutine = coroutine, ) + override val groundImageRenderer: GroundImageRenderer = HereMapGroundImageRenderer( + holder = holder, + coroutine = coroutine, + ) override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } @@ -141,6 +153,9 @@ class HereMapViewController( override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { } + override fun onGroundImageOverlayManagerInitialized(overlayManager: GroundImageOverlayManager) { + } + override fun createPolylineOverlayManager(): PolylineOverlayManager = polylineRendererFactory.create( onAdd = polylineRenderer::addLines, @@ -162,6 +177,15 @@ class HereMapViewController( onRemove = circleRenderer::removeCircles, ) + override fun createGroundImageOverlayManager(): GroundImageOverlayManager { + groundImageRendererFactory.create( + onAdd = groundImageRenderer::addGroundImages, + onChange = groundImageRenderer::changeGroundImages, + onRemove = groundImageRenderer::removeGroundImages, + ) + TODO("Not yet implemented") + } + override suspend fun clearOverlays() { markerOverlayManager.clearOverlays() polylineOverlayManager.clearOverlays() @@ -181,6 +205,14 @@ class HereMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun addGroundImages(data: List) { + TODO("Not yet implemented") + } + + override suspend fun updateGroundImage(state: GroundImageState) { + TODO("Not yet implemented") + } + init { setupListeners() markerRenderer.init(markerOverlayManager) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt index 020d94fe..3fd90e60 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt @@ -1,13 +1,52 @@ package com.mapconductor.here.groundimage +import com.here.sdk.mapview.MapImage +import com.mapconductor.core.groundimage.AbstractGroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageEntity +import com.mapconductor.core.groundimage.GroundImageOverlayManager +import com.mapconductor.core.groundimage.GroundImageOverlayManagerImpl +import com.mapconductor.core.groundimage.GroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageRendererFactory import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.here.HereMapViewHolder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.newCoroutineContext +import kotlinx.coroutines.withContext -class HereGroundImageRenderer { - fun addOverlay(state: GroundImageState) { - // Not implemented +class DefaultHereMapGroundImageRenderer : GroundImageRendererFactory { + override fun create( + onAdd: suspend (List) -> List, + onChange: suspend (List>) -> List, + onRemove: suspend (List>) -> Unit, + onPostProcess: (suspend () -> Unit)? + ): GroundImageOverlayManager = + GroundImageOverlayManagerImpl( + onRemove = onRemove, + onAdd = onAdd, + onChange = onChange, + onPostProcess = onPostProcess, + ) +} + +class HereMapGroundImageRenderer( + override val holder: HereMapViewHolder, + override val coroutine: CoroutineScope +) : AbstractGroundImageRenderer() { + override suspend fun addGroundImages(newGroundImages: List): List { + return withContext(coroutine.newCoroutineContext){ + return@withContext newGroundImages.map { state -> + val bounds = state.bounds ?: return@withContext emptyList() + val image = + val alpha = + val options = + } + } + } + + override suspend fun changeGroundImages(changes: List>): List { + return emptyList() } - fun updateOverlay(state: GroundImageState) { - // Not implemented + override suspend fun removeGroundImages(removeEntities: List>) { } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index ac4156e1..90dd2265 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -30,6 +30,9 @@ import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.groundimage.GroundImageOverlayManager +import com.mapconductor.core.groundimage.GroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.MarkerOverlayManager @@ -44,6 +47,7 @@ import com.mapconductor.core.projection.WebMercator import com.mapconductor.mapbox.circle.DefaultMapboxCircleRenderer import com.mapconductor.mapbox.circle.MapboxCircleLayer import com.mapconductor.mapbox.circle.MapboxCircleRenderer +import com.mapconductor.mapbox.groundImage.MapboxGroundImageRenderer import com.mapconductor.mapbox.marker.DefaultMapboxMarkerRenderer import com.mapconductor.mapbox.marker.MapboxMarkerRenderer import com.mapconductor.mapbox.marker.MarkerDragLayer @@ -65,6 +69,7 @@ interface IMapboxMapViewController : MapboxActualCircle, MapboxActualPolyline, MapboxActualPolygon, + MapboxActualGroundImage, > { fun moveCamera( dstPosition: MapCameraPosition, @@ -123,6 +128,7 @@ internal class MapboxMapViewController( MapboxActualCircle, MapboxActualPolyline, MapboxActualPolygon, + MapboxActualGroundImage, >(), IMapboxMapViewController, CameraChangedCallback, @@ -171,6 +177,10 @@ internal class MapboxMapViewController( onPostProcess = circleRenderer::redraw, ) + override fun createGroundImageOverlayManager(): GroundImageOverlayManager { + TODO("Not yet implemented") + } + override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { holder.map.getStyle { style -> style.addSource(circleLayer.source) @@ -204,6 +214,14 @@ internal class MapboxMapViewController( layer = circleLayer, ) + override fun onGroundImageOverlayManagerInitialized(overlayManager: GroundImageOverlayManager) { + } + + override val groundImageRenderer: GroundImageRenderer = MapboxGroundImageRenderer( + holder = holder, + coroutine = coroutine, + ) + override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } @@ -241,6 +259,13 @@ internal class MapboxMapViewController( override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun addGroundImages(data: List) { + TODO("Not yet implemented") + } + + override suspend fun updateGroundImage(state: GroundImageState) { + TODO("Not yet implemented") + } override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt index 0175622a..0138c3cd 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt @@ -6,3 +6,4 @@ typealias MapboxActualMarker = Feature typealias MapboxActualCircle = Feature typealias MapboxActualPolyline = Feature typealias MapboxActualPolygon = Feature +typealias MapboxActualGroundImage = Feature diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt index d5d564d9..208b2793 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt @@ -1,13 +1,25 @@ package com.mapconductor.mapbox.groundImage +import com.mapbox.geojson.Feature +import com.mapconductor.core.groundimage.AbstractGroundImageRenderer +import com.mapconductor.core.groundimage.GroundImageEntity +import com.mapconductor.core.groundimage.GroundImageRenderer import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.mapbox.MapboxMapViewHolder +import kotlinx.coroutines.CoroutineScope -class MapboxGroundImageRenderer { - fun addOverlay(state: GroundImageState) { - // Not implemented +class MapboxGroundImageRenderer( + override val holder: MapboxMapViewHolder, + override val coroutine: CoroutineScope + ) : AbstractGroundImageRenderer() { + override suspend fun addGroundImages(newGroundImages: List): List { + return emptyList() } - fun updateOverlay(state: GroundImageState) { - // Not implemented + override suspend fun changeGroundImages(changes: List>): List { + return emptyList() + } + + override suspend fun removeGroundImages(removeEntities: List>) { } } From 5a87a980808121d02eb425950328fffd1917c587 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Mon, 4 Aug 2025 23:11:49 +0900 Subject: [PATCH 03/44] Resolve merge conflicts. --- .../mapconductor/here/groundimage/HereGroundImageRenderer.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt index f20ead06..c5d6e3c2 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt @@ -34,8 +34,6 @@ class HereMapGroundImageRenderer( ) : AbstractGroundImageRenderer() { override suspend fun addGroundImages(newGroundImages: List): List { return emptyList() - } -// override suspend fun addGroundImages(newGroundImages: List): List { // return withContext(coroutine.coroutineContext){ // return@withContext newGroundImages.map { state -> // val bounds = state.bounds ?: return@withContext emptyList() @@ -44,7 +42,7 @@ class HereMapGroundImageRenderer( // val options = state.extra // } // } -// } + } override suspend fun changeGroundImages(changes: List>): List { return emptyList() From 81d864529101351633d0184c97f53d16cb7ad2d9 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Wed, 6 Aug 2025 00:47:37 +0900 Subject: [PATCH 04/44] WIP: GroundImage bounds change detection in progress. --- .../com/mapconductor/example/DemoAppScreen.kt | 13 ++ .../mapconductor/example/MapViewContainer.kt | 11 ++ .../groundimage/GroundImageMapComponent.kt | 40 +++++ .../pages/groundimage/GroundImageMapPage.kt | 65 +++++++ .../GroundImageMapPageViewModel.kt | 160 ++++++++++++++++++ 5 files changed, 289 insertions(+) create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index bf7d3965..47bf0d05 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Home import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -13,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.navigation.NavigationViewModel import com.mapconductor.example.pages.circle.CircleMapPage +import com.mapconductor.example.pages.groundimage.GroundImageMapPage import com.mapconductor.example.pages.stores.StoreMapPage import com.mapconductor.example.ui.sidebar.Sidebar import com.mapconductor.example.ui.sidebar.SidebarItem @@ -38,6 +40,12 @@ fun DemoAppScreen() { icon = Icons.Default.CheckCircle, route = "circle", ), + SidebarItem( + id = "groundImage", + title = "GroundImage Demo", + icon = Icons.Default.Favorite, + route = "groundImage", + ), // SidebarItem( // id = "examples", // title = "Map Examples", @@ -67,6 +75,11 @@ fun DemoAppScreen() { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "groundImage" -> { + GroundImageMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } "settings" -> { // Placeholder for settings page Box( diff --git a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt index 652617f1..6df047fc 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -6,6 +6,7 @@ import com.mapconductor.arcgis.ArcGISMapView import com.mapconductor.arcgis.ArcGISMapViewState import com.mapconductor.core.MapViewScope import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler @@ -28,6 +29,8 @@ fun MapViewContainer( onMarkerAnimateStart: OnMarkerEventHandler? = null, onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, + onGroundImageClick: OnGroundImageEventHandler? = null, + onGroundImageChange: OnGroundImageEventHandler? = null, content: @Composable MapViewScope.() -> Unit, ) { when (state) { @@ -43,6 +46,8 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, + onGroundImageClick = onGroundImageClick, + onGroundImageChange = onGroundImageChange, content = content, ) @@ -57,6 +62,8 @@ fun MapViewContainer( onMarkerDragEnd = onMarkerDragEnd, onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, + onGroundImageClick = onGroundImageClick, + onGroundImageChange = onGroundImageChange, content = content, ) @@ -72,6 +79,8 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, + onGroundImageClick = onGroundImageClick, + onGroundImageChange = onGroundImageChange, content = content, ) @@ -87,6 +96,8 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, + onGroundImageClick = onGroundImageClick, + onGroundImageChange = onGroundImageChange, content = content, ) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt new file mode 100644 index 00000000..e11f6db6 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -0,0 +1,40 @@ +package com.mapconductor.example.pages.groundimage + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import com.mapconductor.core.circle.Circle +import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.groundimage.GroundImage +import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.OnMapEventHandler +import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.example.MapViewContainer +import com.mapconductor.example.pages.circle.CirclePageViewModel + +@Composable +fun GroundImageMapComponent( + mapViewState: MapViewState<*>?, + viewModel: GroundImageMapPageViewModel, + modifier: Modifier = Modifier, + onMapClick: OnMapEventHandler = {}, + onGroundImageClick: OnGroundImageEventHandler = {}, + onGroundImageChange: OnGroundImageEventHandler = {}, +) { + mapViewState?.let { mapViewState -> + MapViewContainer( + modifier = modifier, + state = mapViewState, + onMapClick = onMapClick, + onGroundImageClick = onGroundImageClick, + onGroundImageChange = onGroundImageChange, + ) { + // GroundImage + key(viewModel.groundImageState.id) { + GroundImage(viewModel.groundImageState) + } + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt new file mode 100644 index 00000000..093eacaa --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -0,0 +1,65 @@ +package com.mapconductor.example.pages.groundimage + +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.mapconductor.example.pages.circle.CircleMapComponent +import com.mapconductor.example.pages.circle.CirclePageViewModel +import com.mapconductor.example.pages.circle.CirclePageViewModelImpl +import com.mapconductor.example.toast.ToastHost +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.MapViewStatePanel +import com.mapconductor.example.ui.MessageCard + +@Composable +fun GroundImageMapPage( + viewModel: GroundImageMapPageViewModel = GroundImageMapPageViewModelImpl(), + onToggleSidebar: () -> Unit = {}, +) { + DemoMapPageScaffold( + initCameraPosition = viewModel.initCameraPosition, + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = viewModel::onMapViewChanged, + ) { paddingValues -> + val mapViewState = viewModel.mapViewState.collectAsState() + + GroundImageMapComponent( + mapViewState = mapViewState.value, + viewModel = viewModel, + onMapClick = viewModel::onMapClick, + onGroundImageClick = viewModel::onGroundImageClick, + onGroundImageChange = viewModel::onGroundImageChange, + ) + + // Message Card + MessageCard( + modifier = + Modifier + .align(Alignment.BottomStart) + .padding( + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, + ), + title = "Messages", + ) { + MapViewStatePanel( + mapViewState.value + ?.mapCameraPosition + ?.collectAsState() + ?.value, + ) + } + + ToastHost( + messages = viewModel.messages.collectAsState().value, + onDismiss = { viewModel.removeToast(it) }, + ) + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt new file mode 100644 index 00000000..1e246bad --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -0,0 +1,160 @@ +package com.mapconductor.example.pages.groundimage + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import com.mapconductor.core.circle.CircleClickEvent +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.spherical.calculatePositionAtDistance +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.example.toast.ToastMessage +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface GroundImageMapPageViewModel { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + val messages: StateFlow> + + val circleCenter: GeoPoint + val radiusMeters: Double + val centerMarker: MarkerState + val edgeMarker: MarkerState + val groundImageState: GroundImageState + + fun onMapViewChanged(state: MapViewState<*>) + + fun cameraReset(listener: MapViewState.MoveCameraCallback? = null) + + fun onMapClick(clicked: GeoPoint) + + fun onGroundImageClick(event: GroundImageClickEvent) + + fun onGroundImageChange(event: GroundImageClickEvent) + + fun showToast(text: String) + + fun removeToast(toastMessage: ToastMessage) +} + +class GroundImageMapPageViewModelImpl : + ViewModel(), + GroundImageMapPageViewModel { + private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) + override val messages: StateFlow> = _messages.asStateFlow() + + override val initCameraPosition = + MapCameraPosition( + position = + GeoPoint.fromLatLong( + latitude = 21.382314, + longitude = -157.933097, + ), + zoom = 12.0, + bearing = 0.0, + tilt = 0.0, + paddings = null, + ) + +// override val circleCenter = GeoPoint.fromLatLong(21.382314, -157.933097) +// +// override val centerMarker = +// MarkerState( +// id = "center_marker", +// position = circleCenter, +// icon = +// DefaultIcon( +// fillColor = Color.Red, +// strokeColor = Color.White, +// label = "C", +// ), +// draggable = false, +// ) +// + private val _edgeMarker: MutableState = + mutableStateOf( + MarkerState( + id = "edge_marker", + position = + calculatePositionAtDistance( + center = circleCenter, + distanceMeters = 1000.0, + bearingDegrees = 90.0, // East + ), + icon = + DefaultIcon( + fillColor = Color.Green, + strokeColor = Color.White, + label = "E", + ), + draggable = true, + ), + ) + override val edgeMarker: MarkerState + get() = _edgeMarker.value +// +// override val radiusMeters by derivedStateOf { +// haversineDistance(circleCenter, _edgeMarker.value.position) +// } +// + private val _groundImageState: MutableState = + mutableStateOf( + GroundImageState( + id = "groundImage", + ), + ) + override val groundImageState: GroundImageState + get() = _groundImageState.value +// +// private val _mapViewState = MutableStateFlow?>(null) +// override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() +// +// override fun onMapViewChanged(state: MapViewState<*>) { +// this._mapViewState.value = state +// } +// +// override fun cameraReset(listener: MapViewState.MoveCameraCallback?) { +// this.mapViewState.value?.moveCameraTo( +// cameraPosition = initCameraPosition, +// durationMs = 3000, +// listener = listener, +// ) +// } + + override fun onMapClick(clicked: GeoPoint) { + showToast("Map clicked at: ${clicked.toUrlValue()}") + } + + override fun onGroundImageClick(event: GroundImageClickEvent) { + event.state.fillColor = Color.Blue.copy(alpha = 0.5f) + showToast("GroundImage clicked - Radius: ${radiusMeters.toInt()}m") + } + + override fun onGroundImageChange(event: GroundImageClickEvent) { + event.state.fillColor = Color.Blue.copy(alpha = 0.5f) + showToast("GroundImage clicked - Radius: ${radiusMeters.toInt()}m") + } + + override fun showToast(text: String) { + this._messages.value = this._messages.value + ToastMessage(text = text) + } + + override fun removeToast(toastMessage: ToastMessage) { + this._messages.value = this._messages.value.filter { it != toastMessage } + } + + override fun onCleared() { + super.onCleared() + } +} From 1acf8a38a5609a809e2c33e6e1b342ac4118a144 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Fri, 8 Aug 2025 01:09:29 +0900 Subject: [PATCH 05/44] WIP: GroundImage rendered; modification handling not yet done. --- .../groundimage/GroundImageMapComponent.kt | 5 - .../pages/groundimage/GroundImageMapPage.kt | 10 +- .../GroundImageMapPageViewModel.kt | 101 ++++++------------ .../core/groundimage/GroundImage.kt | 7 +- .../com/mapconductor/arcgis/ArcGISMapView.kt | 3 + .../mapconductor/googlemaps/GoogleMapView.kt | 3 + .../GoogleMapGroundImageRenderer.kt | 2 +- .../java/com/mapconductor/here/HereMapView.kt | 3 + .../com/mapconductor/mapbox/MapboxMapView.kt | 3 + 9 files changed, 63 insertions(+), 74 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt index e11f6db6..e0d9bc71 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -3,16 +3,11 @@ package com.mapconductor.example.pages.groundimage import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier -import com.mapconductor.core.circle.Circle -import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.groundimage.GroundImage import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler -import com.mapconductor.core.marker.Marker -import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.example.MapViewContainer -import com.mapconductor.example.pages.circle.CirclePageViewModel @Composable fun GroundImageMapComponent( diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index 093eacaa..60717b96 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -7,8 +7,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import com.mapconductor.core.groundimage.GroundImageClickEvent +import com.mapconductor.example.R import com.mapconductor.example.pages.circle.CircleMapComponent import com.mapconductor.example.pages.circle.CirclePageViewModel import com.mapconductor.example.pages.circle.CirclePageViewModelImpl @@ -16,10 +20,14 @@ import com.mapconductor.example.toast.ToastHost import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MapViewStatePanel import com.mapconductor.example.ui.MessageCard +import android.content.Context +import android.graphics.drawable.Drawable @Composable fun GroundImageMapPage( - viewModel: GroundImageMapPageViewModel = GroundImageMapPageViewModelImpl(), + context: Context = LocalContext.current, + image: Drawable = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_0)!!, + viewModel: GroundImageMapPageViewModel = GroundImageMapPageViewModelImpl(image), onToggleSidebar: () -> Unit = {}, ) { DemoMapPageScaffold( diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index 1e246bad..044c156e 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -7,9 +7,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel +import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.mapconductor.core.circle.CircleClickEvent import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.groundimage.GroundImageClickEvent import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState @@ -17,7 +20,9 @@ import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.spherical.calculatePositionAtDistance import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.example.R import com.mapconductor.example.toast.ToastMessage +import android.graphics.drawable.Drawable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -27,10 +32,9 @@ interface GroundImageMapPageViewModel { val mapViewState: StateFlow?> val messages: StateFlow> - val circleCenter: GeoPoint - val radiusMeters: Double - val centerMarker: MarkerState - val edgeMarker: MarkerState + val bounds: GeoRectBounds + val image: Drawable + val alpha: Float val groundImageState: GroundImageState fun onMapViewChanged(state: MapViewState<*>) @@ -48,11 +52,9 @@ interface GroundImageMapPageViewModel { fun removeToast(toastMessage: ToastMessage) } -class GroundImageMapPageViewModelImpl : +class GroundImageMapPageViewModelImpl(override val image: Drawable) : ViewModel(), GroundImageMapPageViewModel { - private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) - override val messages: StateFlow> = _messages.asStateFlow() override val initCameraPosition = MapCameraPosition( @@ -67,83 +69,50 @@ class GroundImageMapPageViewModelImpl : paddings = null, ) -// override val circleCenter = GeoPoint.fromLatLong(21.382314, -157.933097) -// -// override val centerMarker = -// MarkerState( -// id = "center_marker", -// position = circleCenter, -// icon = -// DefaultIcon( -// fillColor = Color.Red, -// strokeColor = Color.White, -// label = "C", -// ), -// draggable = false, -// ) -// - private val _edgeMarker: MutableState = - mutableStateOf( - MarkerState( - id = "edge_marker", - position = - calculatePositionAtDistance( - center = circleCenter, - distanceMeters = 1000.0, - bearingDegrees = 90.0, // East - ), - icon = - DefaultIcon( - fillColor = Color.Green, - strokeColor = Color.White, - label = "E", - ), - draggable = true, - ), + private val _mapViewState = MutableStateFlow?>(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + override fun onMapViewChanged(state: MapViewState<*>) { + this._mapViewState.value = state + } + + override fun cameraReset(listener: MapViewState.MoveCameraCallback?) { + this.mapViewState.value?.moveCameraTo( + cameraPosition = initCameraPosition, + durationMs = 3000, + listener = listener, ) - override val edgeMarker: MarkerState - get() = _edgeMarker.value -// -// override val radiusMeters by derivedStateOf { -// haversineDistance(circleCenter, _edgeMarker.value.position) -// } -// + } + + private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) + override val messages: StateFlow> = _messages.asStateFlow() + + override val bounds = GeoRectBounds( + southWest = GeoPoint.fromLatLong(20.842314, -158.458097), + northEast = GeoPoint.fromLatLong(21.922314, -157.408097) + ) + + override val alpha = 1.0f + private val _groundImageState: MutableState = mutableStateOf( GroundImageState( id = "groundImage", + bounds = bounds, + image = image, ), ) override val groundImageState: GroundImageState get() = _groundImageState.value -// -// private val _mapViewState = MutableStateFlow?>(null) -// override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() -// -// override fun onMapViewChanged(state: MapViewState<*>) { -// this._mapViewState.value = state -// } -// -// override fun cameraReset(listener: MapViewState.MoveCameraCallback?) { -// this.mapViewState.value?.moveCameraTo( -// cameraPosition = initCameraPosition, -// durationMs = 3000, -// listener = listener, -// ) -// } override fun onMapClick(clicked: GeoPoint) { showToast("Map clicked at: ${clicked.toUrlValue()}") } override fun onGroundImageClick(event: GroundImageClickEvent) { - event.state.fillColor = Color.Blue.copy(alpha = 0.5f) - showToast("GroundImage clicked - Radius: ${radiusMeters.toInt()}m") } override fun onGroundImageChange(event: GroundImageClickEvent) { - event.state.fillColor = Color.Blue.copy(alpha = 0.5f) - showToast("GroundImage clicked - Radius: ${radiusMeters.toInt()}m") } override fun showToast(text: String) { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt index 9cea665d..2351b803 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt @@ -63,4 +63,9 @@ data class GroundImageFingerPrint( val extra: Int, ) -typealias OnGroundImageEventHandler = (GroundImageState) -> Unit +data class GroundImageClickEvent( + val state: GroundImageState, + val bounds: GeoRectBounds, +) + +typealias OnGroundImageEventHandler = (GroundImageClickEvent) -> Unit diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index 4c33a367..20cf41b9 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler @@ -24,6 +25,8 @@ fun ArcGISMapView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, + onGroundImageClick: OnGroundImageEventHandler? = null, + onGroundImageChange: OnGroundImageEventHandler? = null, content: (@Composable ArcGISMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index 78807155..fc7d502b 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -13,6 +13,7 @@ import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.model.CameraPosition import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler @@ -30,6 +31,8 @@ fun GoogleMapsView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, + onGroundImageClick: OnGroundImageEventHandler? = null, + onGroundImageChange: OnGroundImageEventHandler? = null, content: (@Composable GoogleMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt index b7eb65c6..f3ba381f 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt @@ -49,7 +49,7 @@ class GoogleMapGroundImageRenderer( GroundOverlayOptions() .image(image) .positionFromBounds(bounds) - .transparency(alpha) +// .transparency(alpha) holder.map.addGroundOverlay(options)?.also { it.tag = state.id } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index 1fba4577..3fe4a0a5 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner +import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler @@ -30,6 +31,8 @@ fun HereMapView( onMarkerDragEnd: OnMarkerEventHandler? = {}, onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, + onGroundImageClick: OnGroundImageEventHandler? = null, + onGroundImageChange: OnGroundImageEventHandler? = null, content: (@Composable HereMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index f4d60b41..190e9739 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import com.mapbox.maps.MapInitOptions import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler @@ -26,6 +27,8 @@ fun MapboxMapView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, + onGroundImageClick: OnGroundImageEventHandler? = null, + onGroundImageChange: OnGroundImageEventHandler? = null, content: (@Composable MapboxMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } From 4d515a41c7b12a3673ce473250dc5cffd5ede930 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Mon, 11 Aug 2025 08:04:58 +0900 Subject: [PATCH 06/44] Add support for swapping GroundImage (Google Maps API GroundOverlay) image on touch event --- .../com/mapconductor/example/DemoAppScreen.kt | 13 +++++ .../mapconductor/example/MapViewContainer.kt | 9 +--- .../groundimage/GroundImageMapComponent.kt | 6 +-- .../pages/groundimage/GroundImageMapPage.kt | 10 +--- .../GroundImageMapPageViewModel.kt | 47 +++++++++---------- .../pages/groundimage/GroundImageResources.kt | 8 ++++ .../core/controller/MapViewController.kt | 2 + .../core/groundimage/GroundImage.kt | 19 ++++---- .../core/groundimage/GroundImageComponent.kt | 4 +- .../groundimage/GroundImageOverlayManager.kt | 34 +++++++++++++- .../com/mapconductor/core/map/MapViewBase.kt | 8 ++++ .../com/mapconductor/arcgis/ArcGISMapView.kt | 5 +- .../mapconductor/googlemaps/GoogleMapView.kt | 6 +-- .../googlemaps/GoogleMapViewController.kt | 11 +++++ .../GoogleMapGroundImageRenderer.kt | 18 ++----- .../java/com/mapconductor/here/HereMapView.kt | 5 +- .../com/mapconductor/mapbox/MapboxMapView.kt | 5 +- 17 files changed, 125 insertions(+), 85 deletions(-) create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index 47bf0d05..3afc33fd 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -9,12 +9,16 @@ import androidx.compose.material.icons.filled.Home import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.navigation.NavigationViewModel import com.mapconductor.example.pages.circle.CircleMapPage import com.mapconductor.example.pages.groundimage.GroundImageMapPage +import com.mapconductor.example.pages.groundimage.GroundImageResources import com.mapconductor.example.pages.stores.StoreMapPage import com.mapconductor.example.ui.sidebar.Sidebar import com.mapconductor.example.ui.sidebar.SidebarItem @@ -26,6 +30,14 @@ fun DemoAppScreen() { val currentPage by navigationViewModel.currentPage val isSidebarExpanded by navigationViewModel.isSidebarExpanded + val context = LocalContext.current + val groundImageResources = remember { + GroundImageResources( + imageToggle0 = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_0)!!, + imageToggle1 = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_1)!!, + ) + } + val sidebarItems = listOf( SidebarItem( @@ -77,6 +89,7 @@ fun DemoAppScreen() { } "groundImage" -> { GroundImageMapPage( + groundImageResources = groundImageResources, onToggleSidebar = navigationViewModel::toggleSidebar, ) } diff --git a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt index 6df047fc..aa0369c1 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -6,7 +6,7 @@ import com.mapconductor.arcgis.ArcGISMapView import com.mapconductor.arcgis.ArcGISMapViewState import com.mapconductor.core.MapViewScope import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.groundimage.OnGroundImageClickEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler @@ -29,8 +29,7 @@ fun MapViewContainer( onMarkerAnimateStart: OnMarkerEventHandler? = null, onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, - onGroundImageClick: OnGroundImageEventHandler? = null, - onGroundImageChange: OnGroundImageEventHandler? = null, + onGroundImageClick: OnGroundImageClickEventHandler? = null, content: @Composable MapViewScope.() -> Unit, ) { when (state) { @@ -47,7 +46,6 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onGroundImageClick = onGroundImageClick, - onGroundImageChange = onGroundImageChange, content = content, ) @@ -63,7 +61,6 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onGroundImageClick = onGroundImageClick, - onGroundImageChange = onGroundImageChange, content = content, ) @@ -80,7 +77,6 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onGroundImageClick = onGroundImageClick, - onGroundImageChange = onGroundImageChange, content = content, ) @@ -97,7 +93,6 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onGroundImageClick = onGroundImageClick, - onGroundImageChange = onGroundImageChange, content = content, ) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt index e0d9bc71..2b67495b 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier import com.mapconductor.core.groundimage.GroundImage -import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.groundimage.OnGroundImageClickEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.example.MapViewContainer @@ -15,8 +15,7 @@ fun GroundImageMapComponent( viewModel: GroundImageMapPageViewModel, modifier: Modifier = Modifier, onMapClick: OnMapEventHandler = {}, - onGroundImageClick: OnGroundImageEventHandler = {}, - onGroundImageChange: OnGroundImageEventHandler = {}, + onGroundImageClick: OnGroundImageClickEventHandler = {}, ) { mapViewState?.let { mapViewState -> MapViewContainer( @@ -24,7 +23,6 @@ fun GroundImageMapComponent( state = mapViewState, onMapClick = onMapClick, onGroundImageClick = onGroundImageClick, - onGroundImageChange = onGroundImageChange, ) { // GroundImage key(viewModel.groundImageState.id) { diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index 60717b96..bc8c0435 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -11,11 +11,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import com.mapconductor.core.groundimage.GroundImageClickEvent import com.mapconductor.example.R -import com.mapconductor.example.pages.circle.CircleMapComponent -import com.mapconductor.example.pages.circle.CirclePageViewModel -import com.mapconductor.example.pages.circle.CirclePageViewModelImpl import com.mapconductor.example.toast.ToastHost import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MapViewStatePanel @@ -25,9 +21,8 @@ import android.graphics.drawable.Drawable @Composable fun GroundImageMapPage( - context: Context = LocalContext.current, - image: Drawable = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_0)!!, - viewModel: GroundImageMapPageViewModel = GroundImageMapPageViewModelImpl(image), + groundImageResources: GroundImageResources, + viewModel: GroundImageMapPageViewModel = GroundImageMapPageViewModelImpl(groundImageResources), onToggleSidebar: () -> Unit = {}, ) { DemoMapPageScaffold( @@ -42,7 +37,6 @@ fun GroundImageMapPage( viewModel = viewModel, onMapClick = viewModel::onMapClick, onGroundImageClick = viewModel::onGroundImageClick, - onGroundImageChange = viewModel::onGroundImageChange, ) // Message Card diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index 044c156e..4609272a 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -1,26 +1,14 @@ package com.mapconductor.example.pages.groundimage import androidx.compose.runtime.MutableState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel -import com.google.android.gms.maps.model.BitmapDescriptorFactory -import com.mapconductor.core.circle.CircleClickEvent -import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.groundimage.GroundImageClickEvent import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.marker.DefaultIcon -import com.mapconductor.core.marker.MarkerState -import com.mapconductor.core.spherical.calculatePositionAtDistance -import com.mapconductor.core.spherical.haversineDistance -import com.mapconductor.example.R import com.mapconductor.example.toast.ToastMessage import android.graphics.drawable.Drawable import kotlinx.coroutines.flow.MutableStateFlow @@ -33,8 +21,9 @@ interface GroundImageMapPageViewModel { val messages: StateFlow> val bounds: GeoRectBounds + val imageResources: GroundImageResources val image: Drawable - val alpha: Float + val opacity: Float val groundImageState: GroundImageState fun onMapViewChanged(state: MapViewState<*>) @@ -43,16 +32,14 @@ interface GroundImageMapPageViewModel { fun onMapClick(clicked: GeoPoint) - fun onGroundImageClick(event: GroundImageClickEvent) - - fun onGroundImageChange(event: GroundImageClickEvent) + fun onGroundImageClick(clicked: GroundImageClickEvent) fun showToast(text: String) fun removeToast(toastMessage: ToastMessage) } -class GroundImageMapPageViewModelImpl(override val image: Drawable) : +class GroundImageMapPageViewModelImpl(override val imageResources: GroundImageResources) : ViewModel(), GroundImageMapPageViewModel { @@ -60,8 +47,8 @@ class GroundImageMapPageViewModelImpl(override val image: Drawable) : MapCameraPosition( position = GeoPoint.fromLatLong( - latitude = 21.382314, - longitude = -157.933097, + latitude = 40.7430785, + longitude = -74.175995, ), zoom = 12.0, bearing = 0.0, @@ -87,12 +74,14 @@ class GroundImageMapPageViewModelImpl(override val image: Drawable) : private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) override val messages: StateFlow> = _messages.asStateFlow() - override val bounds = GeoRectBounds( - southWest = GeoPoint.fromLatLong(20.842314, -158.458097), - northEast = GeoPoint.fromLatLong(21.922314, -157.408097) + override val bounds = GeoRectBounds( // ココが固定だと変化できないので、後日処理を書き換え。 + southWest = GeoPoint.fromLatLong(40.712216, -74.22655), + northEast = GeoPoint.fromLatLong(40.773941, -74.12544) ) - override val alpha = 1.0f + override val image = imageResources.imageToggle0 + + override val opacity = 1.0f private val _groundImageState: MutableState = mutableStateOf( @@ -102,6 +91,7 @@ class GroundImageMapPageViewModelImpl(override val image: Drawable) : image = image, ), ) + override val groundImageState: GroundImageState get() = _groundImageState.value @@ -109,10 +99,15 @@ class GroundImageMapPageViewModelImpl(override val image: Drawable) : showToast("Map clicked at: ${clicked.toUrlValue()}") } - override fun onGroundImageClick(event: GroundImageClickEvent) { - } + override fun onGroundImageClick(clicked: GroundImageClickEvent) { + if (clicked.state.image == imageResources.imageToggle0) { + _groundImageState.value.image = imageResources.imageToggle1 + } + else { + _groundImageState.value.image = imageResources.imageToggle0 + } - override fun onGroundImageChange(event: GroundImageClickEvent) { + showToast("Ground Image clicked.") } override fun showToast(text: String) { diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt new file mode 100644 index 00000000..ca3d2f3f --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt @@ -0,0 +1,8 @@ +package com.mapconductor.example.pages.groundimage + +import android.graphics.drawable.Drawable + +data class GroundImageResources ( + val imageToggle0: Drawable, + val imageToggle1: Drawable, +) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index db206e64..cac3cb9d 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -11,6 +11,7 @@ import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.groundimage.GroundImageOverlayManager import com.mapconductor.core.groundimage.GroundImageRenderer import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.core.groundimage.OnGroundImageClickEventHandler import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler @@ -147,6 +148,7 @@ abstract class BaseMapViewController Unit +typealias OnGroundImageClickEventHandler = (GroundImageClickEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt index 5c65e824..9068cf0e 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt @@ -20,14 +20,14 @@ fun MapViewScope.GroundImage(state: GroundImageState) { fun MapViewScope.GroundImage( bounds: GeoRectBounds, image: Drawable, - alpha: Float = 0.5f, + opacity: Float = 0.5f, id: String? = null, extra: Parcelable? = null, ) { val state = GroundImageState( bounds = bounds, image = image, - alpha = alpha, + opacity = opacity, id = id, extra = extra, ) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt index 08a03dd4..c3bccc44 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt @@ -1,5 +1,6 @@ package com.mapconductor.core.groundimage +import com.mapconductor.core.features.IGeoPoint import kotlinx.coroutines.sync.Semaphore interface GroundImageOverlayManager { @@ -12,6 +13,8 @@ interface GroundImageOverlayManager { fun getGroundImageState(id: String): GroundImageState? fun getAllEntities(): List> + + fun find(position: IGeoPoint): GroundImageEntity? } class GroundImageOverlayManagerImpl( @@ -91,9 +94,30 @@ val groundImageEntities = mutableMapOf>() + semaphore.acquire() - // Implement fine-grained update logic if needed + val prevEntity = groundImageEntities[state.id]!! + updated.add(object : GroundImageRenderer.UpdateParams { + override val entity: GroundImageEntity = + GroundImageEntityImpl( + groundImage = prevEntity.groundImage, + state = state + ) + override val prevEntity: GroundImageEntity = prevEntity + }) + + val actualOverlays: List = onChange(updated) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val entity = GroundImageEntityImpl( + groundImage = it, + state = state + ) + groundImageEntities[state.id] = entity + } + } semaphore.release() } @@ -108,4 +132,10 @@ val groundImageEntities = mutableMapOf> = groundImageEntities.values.toList() + + override fun find(position: IGeoPoint): GroundImageEntity? { + return groundImageEntities.values.find { entity -> + entity.state.bounds.contains(position) + } + } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index 960b5077..8920e323 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -117,6 +117,14 @@ fun < } } } + val groundImage = scope.groundImageFlow.collectAsState() + groundImage.value.forEach { groundImageState -> + LaunchedEffect(groundImageState.id) { + groundImageState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + controller.updateGroundImage(groundImageState) + } + } + } } Box( diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index 20cf41b9..141fee0c 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.groundimage.OnGroundImageClickEventHandler import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler @@ -25,8 +25,7 @@ fun ArcGISMapView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, - onGroundImageChange: OnGroundImageEventHandler? = null, + onGroundImageClick: OnGroundImageClickEventHandler? = null, content: (@Composable ArcGISMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index fc7d502b..9615197b 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -13,7 +13,7 @@ import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.model.CameraPosition import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.groundimage.OnGroundImageClickEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler @@ -31,8 +31,7 @@ fun GoogleMapsView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, - onGroundImageChange: OnGroundImageEventHandler? = null, + onGroundImageClick: OnGroundImageClickEventHandler? = null, content: (@Composable GoogleMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -87,6 +86,7 @@ fun GoogleMapsView( controller.setOnMarkerAnimationStart(onMarkerAnimateStart) controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) controller.circleClickListener = onCircleClick + controller.groundImageClickListener = onGroundImageClick holderRef.value = controller.holder controllerRef.value = controller diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt index d8849159..03bde306 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -25,6 +25,7 @@ import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.groundimage.GroundImageClickEvent import com.mapconductor.core.groundimage.GroundImageOverlayManager import com.mapconductor.core.groundimage.GroundImageRenderer import com.mapconductor.core.groundimage.GroundImageRendererFactory @@ -295,6 +296,16 @@ class GoogleMapViewController( return } + groundImageOverlayManager.find(touchPosition)?.let { entity -> + val event = + GroundImageClickEvent( + state = entity.state, + position = touchPosition, + ) + groundImageClickListener?.invoke(event) + return + } + mapClickListener?.let { coroutine.launch { it(position.toGeoPoint()) } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt index f3ba381f..8ae26675 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt @@ -4,9 +4,6 @@ import androidx.core.graphics.drawable.toBitmap import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.GroundOverlay import com.google.android.gms.maps.model.GroundOverlayOptions -import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.LatLngBounds -import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.groundimage.AbstractGroundImageRenderer import com.mapconductor.core.groundimage.GroundImageEntity import com.mapconductor.core.groundimage.GroundImageOverlayManager @@ -44,12 +41,12 @@ class GoogleMapGroundImageRenderer( return@withContext newImages.map { state -> val bounds = state.bounds.toLatLngBounds() ?: return@withContext emptyList() val image = BitmapDescriptorFactory.fromBitmap(state.image.toBitmap()) - val alpha = state.alpha + val opacity = state.opacity val options = GroundOverlayOptions() .image(image) .positionFromBounds(bounds) -// .transparency(alpha) + .transparency(1.0f - opacity) holder.map.addGroundOverlay(options)?.also { it.tag = state.id } @@ -74,19 +71,10 @@ class GoogleMapGroundImageRenderer( groundOverlay.setPositionFromBounds(it) } } - groundOverlay.transparency = params.prevEntity.state.alpha + groundOverlay.transparency = 1.0f - params.entity.state.opacity groundOverlay.setImage(BitmapDescriptorFactory.fromBitmap(params.entity.state.image.toBitmap())) return@map groundOverlay } } } - - private fun GeoRectBounds.toLatLngBounds(): LatLngBounds? { - val sw = southWest ?: return null - val ne = northEast ?: return null - return LatLngBounds( - LatLng(sw.latitude, sw.longitude), - LatLng(ne.latitude, ne.longitude) - ) - } } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index 3fe4a0a5..c3e6c29b 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner -import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.groundimage.OnGroundImageClickEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler @@ -31,8 +31,7 @@ fun HereMapView( onMarkerDragEnd: OnMarkerEventHandler? = {}, onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, - onGroundImageChange: OnGroundImageEventHandler? = null, + onGroundImageClick: OnGroundImageClickEventHandler? = null, content: (@Composable HereMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index 190e9739..e5fb8ec4 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import com.mapbox.maps.MapInitOptions import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.groundimage.OnGroundImageClickEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler @@ -27,8 +27,7 @@ fun MapboxMapView( onMarkerAnimateStart: OnMarkerEventHandler? = {}, onMarkerAnimateEnd: OnMarkerEventHandler? = {}, onCircleClick: OnCircleEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, - onGroundImageChange: OnGroundImageEventHandler? = null, + onGroundImageClick: OnGroundImageClickEventHandler? = null, content: (@Composable MapboxMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } From e255ab8df2a0c49b7240a44845f4197e3b145334 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Tue, 12 Aug 2025 01:53:40 +0900 Subject: [PATCH 07/44] Impliment edit opacity slider'. --- example-app/build.gradle.kts | 1 + .../pages/groundimage/GroundImageMapPage.kt | 29 +++++++++++++++---- .../GroundImageMapPageViewModel.kt | 16 ++++++++-- gradle/libs.versions.toml | 2 ++ 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/example-app/build.gradle.kts b/example-app/build.gradle.kts index bc1fe2b5..3ef09501 100644 --- a/example-app/build.gradle.kts +++ b/example-app/build.gradle.kts @@ -104,6 +104,7 @@ dependencies { implementation(project(":mapconductor-for-mapbox")) implementation(project(":mapconductor-for-arcgis")) implementation(libs.androidx.vectordrawable) + implementation(libs.androidx.room.runtime.android) testImplementation(libs.junit) testImplementation(libs.androidx.core) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index bc8c0435..987bed92 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -1,12 +1,17 @@ package com.mapconductor.example.pages.groundimage +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -51,12 +56,24 @@ fun GroundImageMapPage( ), title = "Messages", ) { - MapViewStatePanel( - mapViewState.value - ?.mapCameraPosition - ?.collectAsState() - ?.value, - ) + Column { + Text("opacity: ${"%.2f".format(viewModel.opacity)}", color = Color.Black) + Slider( + value = viewModel.opacity, + onValueChange = { newValue -> + viewModel.setOpacity(newValue) + }, + valueRange = 0.0f..1.0f, // スライダー範囲 + steps = 0, // 中間ステップ数(範囲内を1000分割) + colors = SliderDefaults.colors( + thumbColor = Color.Black, // つまみの色 + activeTrackColor = Color.DarkGray, // 値までのトラック + inactiveTrackColor = Color.LightGray, // 残りのトラック + activeTickColor = Color.Black, // 有効ステップの目盛り + inactiveTickColor = Color.White // 無効ステップの目盛り + ) + ) + } } ToastHost( diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index 4609272a..dfb8f3f1 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -1,8 +1,11 @@ package com.mapconductor.example.pages.groundimage import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.room.util.copy import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.groundimage.GroundImageClickEvent @@ -34,6 +37,8 @@ interface GroundImageMapPageViewModel { fun onGroundImageClick(clicked: GroundImageClickEvent) + fun setOpacity(value: Float) + fun showToast(text: String) fun removeToast(toastMessage: ToastMessage) @@ -79,9 +84,16 @@ class GroundImageMapPageViewModelImpl(override val imageResources: GroundImageRe northEast = GeoPoint.fromLatLong(40.773941, -74.12544) ) - override val image = imageResources.imageToggle0 + override fun setOpacity(value: Float) { + val normalizedValue = value.coerceIn(0f, 1f) + _opacity = normalizedValue + _groundImageState.value.opacity = normalizedValue + } + private var _opacity by mutableStateOf(1.0f) + override val opacity + get() = _opacity - override val opacity = 1.0f + override val image = imageResources.imageToggle0 private val _groundImageState: MutableState = mutableStateOf( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b82ae1aa..ecb653b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ secretsGradlePlugin = "2.0.1" arcgisMapsKotlin = "200.7.0" uiTooling = "1.8.1" vectordrawable = "1.2.0" +roomRuntimeAndroid = "2.7.2" [libraries] # Android 基本 @@ -78,6 +79,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" } androidx-vectordrawable = { group = "androidx.vectordrawable", name = "vectordrawable", version.ref = "vectordrawable" } +androidx-room-runtime-android = { group = "androidx.room", name = "room-runtime-android", version.ref = "roomRuntimeAndroid" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 1b1e051e45c2299195580611514e0a5ea7599418 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Thu, 14 Aug 2025 14:57:32 +0900 Subject: [PATCH 08/44] update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bc78d6ca..e0f4433a 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ KtLintに従います。ローカルで実行する場合は、下記コマン | Map | ☑ | ☑ | ☑ | ☑ | | Marker | ☑ | ☑ | ☑ | ☑ | | Circle | ☑ | ☑ | ☐ | ☐ (wip) | -| Polyline | ☐ | ☐ | ☐ | ☐ | -| Polygon | ☐ | ☐ | ☐ | ☐ | -| GroundOverlay | ☐ | ☐ | ☐ | ☐ | -| RasterTileLayer | ☐ | ☐ | ☐ | ☐ | -| VectorTileLayer | ☐ | ☐ | ☐ | ☐ | +| Polyline | ☑ | ☑ | ☑ | ☑ | +| Polygon | ☐ | ☐ | ☐ | ☐ | +| GroundOverlay | ☐ | ☐ | ☐ | ☐ | +| RasterTileLayer | ☐ | ☐ | ☐ | ☐ | +| VectorTileLayer | ☐ | ☐ | ☐ | ☐ | From 69c97578dcd203937e79947eef93e6dd24d65fb1 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Thu, 14 Aug 2025 19:35:28 +0900 Subject: [PATCH 09/44] Changes groundImageState.opacity following the slider value --- .../com/mapconductor/example/DemoAppScreen.kt | 14 ++-- .../example/navigation/NavigationViewModel.kt | 2 +- .../groundimage/GroundImageMapComponent.kt | 5 +- .../pages/groundimage/GroundImageMapPage.kt | 33 +++++---- .../GroundImageMapPageViewModel.kt | 57 +++++---------- .../pages/groundimage/GroundImageResources.kt | 6 +- .../core/controller/MapViewController.kt | 14 +++- .../core/groundimage/GroundImage.kt | 25 ++++--- .../core/groundimage/GroundImageComponent.kt | 20 +++--- .../groundimage/GroundImageOverlayManager.kt | 71 ++++++++++--------- .../com/mapconductor/core/map/MapViewBase.kt | 2 +- .../mapconductor/core/marker/MarkerCompose.kt | 2 +- .../arcgis/ArcGISMapViewController.kt | 16 +++-- .../ArcGISGroundImageRenderer.kt} | 22 +++--- .../com/mapconductor/googlemaps/GeoRect.kt | 9 ++- .../googlemaps/GoogleMapViewController.kt | 3 +- .../GoogleMapGroundImageRenderer.kt | 20 ++++-- .../java/com/mapconductor/here/HereMapView.kt | 2 +- .../here/HereMapViewController.kt | 20 ++++-- .../groundimage/HereGroundImageRenderer.kt | 12 ++-- .../mapbox/MapboxMapViewController.kt | 14 ++-- .../groundImage/MapboxGroundImageRenderer.kt | 15 ++-- 22 files changed, 201 insertions(+), 183 deletions(-) rename mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/{groundoverlay/ArcGISGroundOverlayRenderer.kt => groundimage/ArcGISGroundImageRenderer.kt} (51%) diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index 57940b79..75a445bd 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -45,13 +45,13 @@ fun DemoAppScreen() { ) } - val context = LocalContext.current - val groundImageResources = remember { - GroundImageResources( - imageToggle0 = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_0)!!, - imageToggle1 = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_1)!!, - ) - } + val groundImageResources = + remember { + GroundImageResources( + image = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_0)!!, + clickedImage = ContextCompat.getDrawable(context, R.drawable.newark_nj_1922_1)!!, + ) + } val sidebarItems = listOf( diff --git a/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt b/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt index e771adcf..0ed71062 100644 --- a/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel class NavigationViewModel : ViewModel() { - private val _currentPage = mutableStateOf("flyTo") + private val _currentPage = mutableStateOf("groundImage") val currentPage: State = _currentPage private val _isSidebarExpanded = mutableStateOf(false) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt index 07fd812d..5eccef76 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.Modifier import com.mapconductor.core.groundimage.GroundImage import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.example.MapViewContainer @Composable @@ -14,18 +13,16 @@ fun GroundImageMapComponent( mapViewState: MapViewState<*>?, viewModel: GroundImageMapPageViewModel, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler = {}, onGroundImageClick: OnGroundImageEventHandler = {}, ) { mapViewState?.let { mapViewState -> MapViewContainer( modifier = modifier, state = mapViewState, - onMapClick = onMapClick, onGroundImageClick = onGroundImageClick, ) { // GroundImage - key(viewModel.groundImageState.id) { + key(viewModel.groundImageState.fingerPrint()) { GroundImage(viewModel.groundImageState) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index 987bed92..64f044e1 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -12,24 +12,34 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import com.mapconductor.example.R +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.toast.ToastHost import com.mapconductor.example.ui.DemoMapPageScaffold -import com.mapconductor.example.ui.MapViewStatePanel import com.mapconductor.example.ui.MessageCard -import android.content.Context -import android.graphics.drawable.Drawable @Composable fun GroundImageMapPage( groundImageResources: GroundImageResources, - viewModel: GroundImageMapPageViewModel = GroundImageMapPageViewModelImpl(groundImageResources), onToggleSidebar: () -> Unit = {}, ) { + val viewModel: GroundImageMapPageViewModel = + viewModel( + factory = + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(GroundImageMapPageViewModelImpl::class.java)) { + @Suppress("UNCHECKED_CAST") + return GroundImageMapPageViewModelImpl(groundImageResources) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } + }, + ) + DemoMapPageScaffold( initCameraPosition = viewModel.initCameraPosition, onToggleSidebar = onToggleSidebar, @@ -40,7 +50,6 @@ fun GroundImageMapPage( GroundImageMapComponent( mapViewState = mapViewState.value, viewModel = viewModel, - onMapClick = viewModel::onMapClick, onGroundImageClick = viewModel::onGroundImageClick, ) @@ -61,16 +70,16 @@ fun GroundImageMapPage( Slider( value = viewModel.opacity, onValueChange = { newValue -> - viewModel.setOpacity(newValue) + viewModel.opacity = newValue }, - valueRange = 0.0f..1.0f, // スライダー範囲 - steps = 0, // 中間ステップ数(範囲内を1000分割) + valueRange = 0.0f..1.0f, // スライダー範囲 + steps = 0, // 中間ステップ数(範囲内を1000分割) colors = SliderDefaults.colors( thumbColor = Color.Black, // つまみの色 activeTrackColor = Color.DarkGray, // 値までのトラック inactiveTrackColor = Color.LightGray, // 残りのトラック activeTickColor = Color.Black, // 有効ステップの目盛り - inactiveTickColor = Color.White // 無効ステップの目盛り + inactiveTickColor = Color.White, // 無効ステップの目盛り ) ) } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index 662dfee2..2ae65db6 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -1,6 +1,5 @@ package com.mapconductor.example.pages.groundimage -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -25,28 +24,24 @@ interface GroundImageMapPageViewModel { val bounds: GeoRectBounds val imageResources: GroundImageResources val image: Drawable - val opacity: Float + var opacity: Float val groundImageState: GroundImageState fun onMapViewChanged(state: MapViewState<*>) fun cameraReset(listener: MapViewState.MoveCameraCallback? = null) - fun onMapClick(clicked: GeoPoint) - fun onGroundImageClick(clicked: GroundImageEvent) - fun setOpacity(value: Float) - fun showToast(text: String) fun removeToast(toastMessage: ToastMessage) } -class GroundImageMapPageViewModelImpl(override val imageResources: GroundImageResources) : - ViewModel(), +class GroundImageMapPageViewModelImpl( + override val imageResources: GroundImageResources, +) : ViewModel(), GroundImageMapPageViewModel { - override val initCameraPosition = MapCameraPosition( position = @@ -78,44 +73,30 @@ class GroundImageMapPageViewModelImpl(override val imageResources: GroundImageRe private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) override val messages: StateFlow> = _messages.asStateFlow() - override val bounds = GeoRectBounds( // ココが固定だと変化できないので、後日処理を書き換え。 - southWest = GeoPoint.fromLatLong(40.712216, -74.22655), - northEast = GeoPoint.fromLatLong(40.773941, -74.12544) - ) + override val bounds = + GeoRectBounds( // ココが固定だと変化できないので、後日処理を書き換え。 + southWest = GeoPoint.fromLatLong(40.712216, -74.22655), + northEast = GeoPoint.fromLatLong(40.773941, -74.12544), + ) - override fun setOpacity(value: Float) { - val normalizedValue = value.coerceIn(0f, 1f) - _opacity = normalizedValue - _groundImageState.value.opacity = normalizedValue - } - private var _opacity by mutableStateOf(1.0f) - override val opacity - get() = _opacity + override var opacity by mutableStateOf(0.5f) - override val image = imageResources.imageToggle0 + override var image by mutableStateOf(imageResources.image) - private val _groundImageState: MutableState = - mutableStateOf( + override val groundImageState + get() = GroundImageState( id = "groundImage", bounds = bounds, image = image, - ), - ) - - override val groundImageState: GroundImageState - get() = _groundImageState.value - - override fun onMapClick(clicked: GeoPoint) { - showToast("Map clicked at: ${clicked.toUrlValue()}") - } + opacity = opacity, + ) override fun onGroundImageClick(clicked: GroundImageEvent) { - if (clicked.state.image == imageResources.imageToggle0) { - _groundImageState.value.image = imageResources.imageToggle1 - } - else { - _groundImageState.value.image = imageResources.imageToggle0 + if (clicked.state.image == imageResources.image) { + image = imageResources.clickedImage + } else { + image = imageResources.image } showToast("Ground Image clicked.") diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt index ca3d2f3f..6d356daf 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageResources.kt @@ -2,7 +2,7 @@ package com.mapconductor.example.pages.groundimage import android.graphics.drawable.Drawable -data class GroundImageResources ( - val imageToggle0: Drawable, - val imageToggle1: Drawable, +data class GroundImageResources( + val image: Drawable, + val clickedImage: Drawable, ) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index 1c6f8d09..e27a39f4 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -75,8 +75,14 @@ data class SearchRangeAnalysis( val markersInRange: List, ) -abstract class BaseMapViewController : - MapViewController { +abstract class BaseMapViewController< + ActualCamera, + ActualMarker, + ActualCircle, + ActualPolyline, + ActualPolygon, + ActualGroundImage, +> : MapViewController { abstract val markerRenderer: MarkerRenderer override val markerOverlayManager: MarkerOverlayManager by lazy { @@ -129,7 +135,9 @@ abstract class BaseMapViewController) - protected abstract fun onGroundImageOverlayManagerInitialized(overlayManager: GroundImageOverlayManager) + protected abstract fun onGroundImageOverlayManagerInitialized( + overlayManager: GroundImageOverlayManager, + ) protected abstract fun createMarkerOverlayManager(): MarkerOverlayManager diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt index 1ff605a1..6d3efc7e 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt @@ -4,11 +4,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow -import android.os.Parcelable import com.mapconductor.core.StateFlowDelegate import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.GeoRectBounds import android.graphics.drawable.Drawable +import android.os.Parcelable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -26,22 +26,22 @@ class GroundImageState( var opacity by mutableStateOf(opacity) var extra by mutableStateOf(extra) - fun fingerPrint(): GroundImageFingerPrint = GroundImageFingerPrint( - id = id.hashCode(), - bounds = bounds.hashCode(), - image = image.hashCode(), - opacity = opacity.hashCode(), - extra = extra?.hashCode() ?: 0, - ) + fun fingerPrint(): GroundImageFingerPrint = + GroundImageFingerPrint( + id = id.hashCode(), + bounds = bounds.hashCode(), + image = image.hashCode(), + opacity = opacity.hashCode(), + extra = extra?.hashCode() ?: 0, + ) - fun asFlow(): Flow = - snapshotFlow { fingerPrint() }.distinctUntilChanged() + fun asFlow(): Flow = snapshotFlow { fingerPrint() }.distinctUntilChanged() private fun generateId( bounds: GeoRectBounds, image: Drawable, opacity: Float, - extra: Parcelable? + extra: Parcelable?, ): Int { var result = bounds.hashCode() result = 31 * result + image.hashCode() @@ -50,8 +50,7 @@ class GroundImageState( return result } - override fun equals(other: Any?): Boolean = - (other as? GroundImageState)?.hashCode() == this.hashCode() + override fun equals(other: Any?): Boolean = (other as? GroundImageState)?.hashCode() == this.hashCode() override fun hashCode(): Int = fingerPrint().hashCode() } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt index 9068cf0e..bf96a026 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt @@ -7,12 +7,13 @@ import com.mapconductor.core.MapViewScope import com.mapconductor.core.features.GeoRectBounds import android.graphics.drawable.Drawable import android.os.Parcelable +import kotlinx.coroutines.flow.filter @Composable fun MapViewScope.GroundImage(state: GroundImageState) { - val rememberState = remember { state } + val rememberState = remember(state.fingerPrint()) { state } SideEffect { - groundImageFlow.value += rememberState + groundImageFlow.value = groundImageFlow.value.filter { it.id != state.id } + rememberState } } @@ -24,12 +25,13 @@ fun MapViewScope.GroundImage( id: String? = null, extra: Parcelable? = null, ) { - val state = GroundImageState( - bounds = bounds, - image = image, - opacity = opacity, - id = id, - extra = extra, - ) + val state = + GroundImageState( + bounds = bounds, + image = image, + opacity = opacity, + id = id, + extra = extra, + ) GroundImage(state) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt index c3bccc44..2704a9d5 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt @@ -20,12 +20,10 @@ interface GroundImageOverlayManager { class GroundImageOverlayManagerImpl( val onAdd: suspend (List) -> List, val onChange: suspend (List>) -> List, - val onRemove: suspend (List>) -> Unit, val onPostProcess: (suspend () -> Unit)? = null, ) : GroundImageOverlayManager { - -val groundImageEntities = mutableMapOf>() + val groundImageEntities = mutableMapOf>() val semaphore = Semaphore(1) override suspend fun addGroundImages(images: List) { @@ -38,13 +36,16 @@ val groundImageEntities = mutableMapOf { - override val entity = GroundImageEntityImpl( - groundImage = prevEntity.groundImage, - state = it - ) - override val prevEntity = prevEntity - }) + updated.add( + object : GroundImageRenderer.UpdateParams { + override val entity = + GroundImageEntityImpl( + groundImage = prevEntity.groundImage, + state = it, + ) + override val prevEntity = prevEntity + }, + ) previous.remove(it.id) } else { added.add(it) @@ -63,10 +64,11 @@ val groundImageEntities = mutableMapOf actualOverlay?.let { val state = added[index] - val entity = GroundImageEntityImpl( - groundImage = it, - state = state - ) + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) groundImageEntities[state.id] = entity } } @@ -77,10 +79,11 @@ val groundImageEntities = mutableMapOf actualOverlay?.let { val state = updated[index].entity.state - val entity = GroundImageEntityImpl( - groundImage = it, - state = state - ) + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) groundImageEntities[state.id] = entity } } @@ -99,22 +102,25 @@ val groundImageEntities = mutableMapOf { - override val entity: GroundImageEntity = - GroundImageEntityImpl( - groundImage = prevEntity.groundImage, - state = state - ) - override val prevEntity: GroundImageEntity = prevEntity - }) + updated.add( + object : GroundImageRenderer.UpdateParams { + override val entity: GroundImageEntity = + GroundImageEntityImpl( + groundImage = prevEntity.groundImage, + state = state, + ) + override val prevEntity: GroundImageEntity = prevEntity + }, + ) val actualOverlays: List = onChange(updated) actualOverlays.forEachIndexed { index, actualOverlay -> actualOverlay?.let { - val entity = GroundImageEntityImpl( - groundImage = it, - state = state - ) + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) groundImageEntities[state.id] = entity } } @@ -133,9 +139,8 @@ val groundImageEntities = mutableMapOf> = groundImageEntities.values.toList() - override fun find(position: IGeoPoint): GroundImageEntity? { - return groundImageEntities.values.find { entity -> + override fun find(position: IGeoPoint): GroundImageEntity? = + groundImageEntities.values.find { entity -> entity.state.bounds.contains(position) } - } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index 8920e323..7abb34dc 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -23,9 +23,9 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import com.mapconductor.core.CollectAndRenderOverlays import com.mapconductor.core.LocalCircleCollector +import com.mapconductor.core.LocalGroundImageCollector import com.mapconductor.core.LocalMarkerCollector import com.mapconductor.core.LocalPolylineCollector -import com.mapconductor.core.LocalGroundImageCollector import com.mapconductor.core.MapViewScope import com.mapconductor.core.ResourceProvider import com.mapconductor.core.controller.MapViewControllerAlias diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt index 948ff660..893dc601 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt @@ -9,7 +9,7 @@ import android.os.Parcelable @Composable fun MapViewScope.Marker(state: MarkerState) { - val rememberMarker = remember(state.fingerPrint()) { state } + val rememberMarker = remember(state.id) { state } SideEffect { markerFlow.value = markerFlow.value.filter { it.id != rememberMarker.id } + rememberMarker } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt index fbeadf0a..9b6b6977 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt @@ -11,7 +11,7 @@ import com.arcgismaps.mapping.view.UpEvent import com.arcgismaps.mapping.view.extensions.motionEvent import com.mapconductor.arcgis.circle.ArcGISCircleRenderer import com.mapconductor.arcgis.circle.DefaultArcGISCircleRenderer -import com.mapconductor.arcgis.groundoverlay.ArcGISGroundImageRenderer +import com.mapconductor.arcgis.groundimage.ArcGISGroundImageRenderer import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer import com.mapconductor.arcgis.marker.DefaultArcGISMarkerRender import com.mapconductor.arcgis.polygon.ArcGISPolygonRenderer @@ -179,13 +179,16 @@ class ArcGISMapViewController( coroutine = coroutine, ) - override fun onGroundImageOverlayManagerInitialized(overlayManager: GroundImageOverlayManager) { + override fun onGroundImageOverlayManagerInitialized( + overlayManager: GroundImageOverlayManager, + ) { } - override val groundImageRenderer: GroundImageRenderer = ArcGISGroundImageRenderer( - holder = holder, - coroutine = coroutine, - ) + override val groundImageRenderer: GroundImageRenderer = + ArcGISGroundImageRenderer( + holder = holder, + coroutine = coroutine, + ) override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } @@ -351,6 +354,7 @@ class ArcGISMapViewController( override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun addGroundImages(data: List) { TODO("Not yet implemented") } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundimage/ArcGISGroundImageRenderer.kt similarity index 51% rename from mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt rename to mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundimage/ArcGISGroundImageRenderer.kt index 483d019d..2d92546c 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundoverlay/ArcGISGroundOverlayRenderer.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundimage/ArcGISGroundImageRenderer.kt @@ -1,6 +1,7 @@ -package com.mapconductor.arcgis.groundoverlay +package com.mapconductor.arcgis.groundimage import com.arcgismaps.mapping.view.Graphic +import com.mapconductor.arcgis.ArcGISActualGroundImage import com.mapconductor.arcgis.ArcGISMapViewHolder import com.mapconductor.core.groundimage.AbstractGroundImageRenderer import com.mapconductor.core.groundimage.GroundImageEntity @@ -9,17 +10,16 @@ import com.mapconductor.core.groundimage.GroundImageState import kotlinx.coroutines.CoroutineScope class ArcGISGroundImageRenderer( - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope - ) : AbstractGroundImageRenderer() { - override suspend fun addGroundImages(newGroundImages: List): List { - return emptyList() - } + override val holder: ArcGISMapViewHolder, + override val coroutine: CoroutineScope, +) : AbstractGroundImageRenderer() { + override suspend fun addGroundImages(newGroundImages: List): List = + emptyList() - override suspend fun changeGroundImages(changes: List>): List { - return emptyList() - } + override suspend fun changeGroundImages( + changes: List>, + ): List = emptyList() - override suspend fun removeGroundImages(removeEntities: List>) { + override suspend fun removeGroundImages(removeEntities: List>) { } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt index e212758f..408e7492 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GeoRect.kt @@ -11,13 +11,12 @@ fun GeoRectBounds.toLatLngBounds(): LatLngBounds? { return LatLngBounds( LatLng(sw.latitude, sw.longitude), - LatLng(ne.latitude, ne.longitude) + LatLng(ne.latitude, ne.longitude), ) } -fun LatLngBounds.toGeoRectBounds(): GeoRectBounds { - return GeoRectBounds( +fun LatLngBounds.toGeoRectBounds(): GeoRectBounds = + GeoRectBounds( southWest = GeoPoint(southwest.latitude, southwest.longitude), - northEast = GeoPoint(northeast.latitude, northeast.longitude) + northEast = GeoPoint(northeast.latitude, northeast.longitude), ) -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt index 655c04ce..fd2872fa 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -82,7 +82,8 @@ class GoogleMapViewController( private val polylineRendererFactory: PolylineRendererFactory = DefaultGoogleMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultGoogleMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultGoogleMapCircleRenderer(), - private val groundImageRendererFactory: GroundImageRendererFactory = DefaultGoogleMapGroundImageRenderer(), + private val groundImageRendererFactory: GroundImageRendererFactory = + DefaultGoogleMapGroundImageRenderer(), ) : BaseMapViewController(), IGoogleMapViewController, OnCameraMoveStartedListener, diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt index 8ae26675..6f22b9b0 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt @@ -22,7 +22,7 @@ class DefaultGoogleMapGroundImageRenderer : GroundImageRendererFactory) -> List, onChange: suspend (List>) -> List, onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)? + onPostProcess: (suspend () -> Unit)?, ): GroundImageOverlayManager = GroundImageOverlayManagerImpl( onRemove = onRemove, @@ -60,19 +60,27 @@ class GoogleMapGroundImageRenderer( } } - override suspend fun changeGroundImages(changes: List>): List { + override suspend fun changeGroundImages( + changes: List>, + ): List { return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params-> + return@withContext changes.map { params -> val groundOverlay = params.entity.groundImage - val finger = params.entity.state.fingerPrint() + val finger = params.entity.fingerPrint val prevFinger = params.prevEntity.fingerPrint if (finger.bounds != prevFinger.bounds) { - params.entity.state.bounds.toLatLngBounds()?.let{ + params.entity.state.bounds.toLatLngBounds()?.let { groundOverlay.setPositionFromBounds(it) } } groundOverlay.transparency = 1.0f - params.entity.state.opacity - groundOverlay.setImage(BitmapDescriptorFactory.fromBitmap(params.entity.state.image.toBitmap())) + if (finger.image != prevFinger.image) { + val bitmap = + params.entity.state.image + .toBitmap() + val bitmapDesc = BitmapDescriptorFactory.fromBitmap(bitmap) + groundOverlay.setImage(bitmapDesc) + } return@map groundOverlay } } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index b4bc157a..fe3b4887 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -9,8 +9,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner -import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt index 27f4b526..e9dd2f33 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt @@ -83,10 +83,12 @@ class HereMapViewController( baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 ), private val markerRendererFactory: MarkerRendererFactory = DefaultHereMapMarkerRenderer(), - private val polylineRendererFactory: PolylineRendererFactory = DefaultHereMapPolylineRenderer(), + private val polylineRendererFactory: PolylineRendererFactory = + DefaultHereMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultHereMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), - private val groundImageRendererFactory: GroundImageRendererFactory = DefaultHereMapGroundImageRenderer(), + private val groundImageRendererFactory: GroundImageRendererFactory = + DefaultHereMapGroundImageRenderer(), ) : BaseMapViewController< MapCamera.State, HereMapActualMarker, @@ -138,10 +140,12 @@ class HereMapViewController( coroutine = coroutine, ) - override val groundImageRenderer: GroundImageRenderer = HereMapGroundImageRenderer( - holder = holder, - coroutine = coroutine, - ) + override val groundImageRenderer: GroundImageRenderer = + HereMapGroundImageRenderer( + holder = holder, + coroutine = coroutine, + ) + override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } @@ -154,7 +158,9 @@ class HereMapViewController( override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { } - override fun onGroundImageOverlayManagerInitialized(overlayManager: GroundImageOverlayManager) { + override fun onGroundImageOverlayManagerInitialized( + overlayManager: GroundImageOverlayManager, + ) { } override fun createPolylineOverlayManager(): PolylineOverlayManager = diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt index c5d6e3c2..b5da8cca 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt @@ -10,15 +10,13 @@ import com.mapconductor.core.groundimage.GroundImageRendererFactory import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.here.HereMapViewHolder import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.newCoroutineContext -import kotlinx.coroutines.withContext class DefaultHereMapGroundImageRenderer : GroundImageRendererFactory { override fun create( onAdd: suspend (List) -> List, onChange: suspend (List>) -> List, onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)? + onPostProcess: (suspend () -> Unit)?, ): GroundImageOverlayManager = GroundImageOverlayManagerImpl( onRemove = onRemove, @@ -30,7 +28,7 @@ class DefaultHereMapGroundImageRenderer : GroundImageRendererFactory { class HereMapGroundImageRenderer( override val holder: HereMapViewHolder, - override val coroutine: CoroutineScope + override val coroutine: CoroutineScope, ) : AbstractGroundImageRenderer() { override suspend fun addGroundImages(newGroundImages: List): List { return emptyList() @@ -44,9 +42,9 @@ class HereMapGroundImageRenderer( // } } - override suspend fun changeGroundImages(changes: List>): List { - return emptyList() - } + override suspend fun changeGroundImages( + changes: List>, + ): List = emptyList() override suspend fun removeGroundImages(removeEntities: List>) { } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index c84a17a5..748cf1bf 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -214,13 +214,16 @@ internal class MapboxMapViewController( layer = circleLayer, ) - override fun onGroundImageOverlayManagerInitialized(overlayManager: GroundImageOverlayManager) { + override fun onGroundImageOverlayManagerInitialized( + overlayManager: GroundImageOverlayManager, + ) { } - override val groundImageRenderer: GroundImageRenderer = MapboxGroundImageRenderer( - holder = holder, - coroutine = coroutine, - ) + override val groundImageRenderer: GroundImageRenderer = + MapboxGroundImageRenderer( + holder = holder, + coroutine = coroutine, + ) override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } @@ -259,6 +262,7 @@ internal class MapboxMapViewController( override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun addGroundImages(data: List) { TODO("Not yet implemented") } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt index 208b2793..1a5b43d9 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt @@ -9,16 +9,13 @@ import com.mapconductor.mapbox.MapboxMapViewHolder import kotlinx.coroutines.CoroutineScope class MapboxGroundImageRenderer( - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope - ) : AbstractGroundImageRenderer() { - override suspend fun addGroundImages(newGroundImages: List): List { - return emptyList() - } + override val holder: MapboxMapViewHolder, + override val coroutine: CoroutineScope, +) : AbstractGroundImageRenderer() { + override suspend fun addGroundImages(newGroundImages: List): List = emptyList() - override suspend fun changeGroundImages(changes: List>): List { - return emptyList() - } + override suspend fun changeGroundImages(changes: List>): List = + emptyList() override suspend fun removeGroundImages(removeEntities: List>) { } From 24a522680b516c690220d8594a36513f2fe7f6d1 Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Sun, 17 Aug 2025 22:41:57 +0900 Subject: [PATCH 10/44] Initial implementation of GroundImage (GroundOverlay). NE and SW markers are not yet linked with bounds, but committing as instructed. --- .../groundimage/GroundImageMapComponent.kt | 13 +++++ .../pages/groundimage/GroundImageMapPage.kt | 3 +- .../GroundImageMapPageViewModel.kt | 53 +++++++++++++++++-- .../core/groundimage/GroundImage.kt | 4 +- .../core/groundimage/GroundImageComponent.kt | 23 ++++++++ 5 files changed, 90 insertions(+), 6 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt index 5eccef76..4146ebd7 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -4,8 +4,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier import com.mapconductor.core.groundimage.GroundImage +import com.mapconductor.core.groundimage.NorthEast import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.groundimage.SouthWest import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.example.MapViewContainer @Composable @@ -14,17 +17,27 @@ fun GroundImageMapComponent( viewModel: GroundImageMapPageViewModel, modifier: Modifier = Modifier, onGroundImageClick: OnGroundImageEventHandler = {}, + onMarkerDrag: OnMarkerEventHandler = {}, ) { mapViewState?.let { mapViewState -> MapViewContainer( modifier = modifier, state = mapViewState, onGroundImageClick = onGroundImageClick, + onMarkerDrag = onMarkerDrag, ) { // GroundImage key(viewModel.groundImageState.fingerPrint()) { GroundImage(viewModel.groundImageState) } + // SouthWest + key(viewModel.southWestMarkerState.fingerPrint()) { + SouthWest(viewModel.southWestMarkerState) + } + // NorthEast + key(viewModel.northEastMarkerState.fingerPrint()) { + NorthEast(viewModel.northEastMarkerState) + } } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index 64f044e1..1bfb0f40 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -51,6 +51,7 @@ fun GroundImageMapPage( mapViewState = mapViewState.value, viewModel = viewModel, onGroundImageClick = viewModel::onGroundImageClick, + onMarkerDrag = viewModel::onMarkerDrag, ) // Message Card @@ -73,7 +74,7 @@ fun GroundImageMapPage( viewModel.opacity = newValue }, valueRange = 0.0f..1.0f, // スライダー範囲 - steps = 0, // 中間ステップ数(範囲内を1000分割) + steps = 0, colors = SliderDefaults.colors( thumbColor = Color.Black, // つまみの色 activeTrackColor = Color.DarkGray, // 値までのトラック diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index 2ae65db6..a2181360 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -3,6 +3,7 @@ package com.mapconductor.example.pages.groundimage import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.GeoRectBounds @@ -10,6 +11,8 @@ import com.mapconductor.core.groundimage.GroundImageEvent import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.MarkerState import com.mapconductor.example.toast.ToastMessage import android.graphics.drawable.Drawable import kotlinx.coroutines.flow.MutableStateFlow @@ -22,6 +25,10 @@ interface GroundImageMapPageViewModel { val messages: StateFlow> val bounds: GeoRectBounds + var southWest: GeoPoint + val southWestMarkerState: MarkerState + var northEast: GeoPoint + val northEastMarkerState: MarkerState val imageResources: GroundImageResources val image: Drawable var opacity: Float @@ -33,6 +40,8 @@ interface GroundImageMapPageViewModel { fun onGroundImageClick(clicked: GroundImageEvent) + fun onMarkerDrag(dragged: MarkerState) + fun showToast(text: String) fun removeToast(toastMessage: ToastMessage) @@ -73,10 +82,37 @@ class GroundImageMapPageViewModelImpl( private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) override val messages: StateFlow> = _messages.asStateFlow() - override val bounds = - GeoRectBounds( // ココが固定だと変化できないので、後日処理を書き換え。 - southWest = GeoPoint.fromLatLong(40.712216, -74.22655), - northEast = GeoPoint.fromLatLong(40.773941, -74.12544), + override var southWest by mutableStateOf(GeoPoint.fromLatLong(40.712216, -74.22655)) + override val southWestMarkerState = + MarkerState( + id = "south_west", + position = southWest, + icon = + DefaultIcon( + fillColor = Color.Blue, + strokeColor = Color.White, + label = "SW", + ), + draggable = true, + ) + + override var northEast by mutableStateOf(GeoPoint.fromLatLong(40.773941,-74.12544)) + override val northEastMarkerState = + MarkerState( + id = "north_east", + position = northEast, + icon = + DefaultIcon( + fillColor = Color.Red, + strokeColor = Color.White, + label = "NE", + ), + draggable = true, + ) + + override val bounds: GeoRectBounds = GeoRectBounds( + southWest = southWest, + northEast = northEast, ) override var opacity by mutableStateOf(0.5f) @@ -102,6 +138,15 @@ class GroundImageMapPageViewModelImpl( showToast("Ground Image clicked.") } + override fun onMarkerDrag(dragged: MarkerState) { + if (dragged.id == "south_west") { + southWest = GeoPoint.fromLatLong(dragged.position.latitude, dragged.position.longitude) + } + else { + northEast = GeoPoint.fromLatLong(dragged.position.latitude, dragged.position.longitude) + } + } + override fun showToast(text: String) { this._messages.value = this._messages.value + ToastMessage(text = text) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt index 6d3efc7e..aa22b4a0 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.snapshotFlow import com.mapconductor.core.StateFlowDelegate import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.marker.MarkerState import android.graphics.drawable.Drawable import android.os.Parcelable import kotlinx.coroutines.flow.Flow @@ -21,7 +22,8 @@ class GroundImageState( ) { val id = (id ?: generateId(bounds, image, opacity, extra)).toString() - var bounds by StateFlowDelegate(bounds) +// var bounds by StateFlowDelegate(bounds) + var bounds by mutableStateOf(bounds) var image by mutableStateOf(image) var opacity by mutableStateOf(opacity) var extra by mutableStateOf(extra) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt index bf96a026..3520faba 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt @@ -3,8 +3,15 @@ package com.mapconductor.core.groundimage import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.mapconductor.core.MapViewScope +import com.mapconductor.core.circle.Circle +import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoRectBounds +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.MarkerState import android.graphics.drawable.Drawable import android.os.Parcelable import kotlinx.coroutines.flow.filter @@ -17,6 +24,22 @@ fun MapViewScope.GroundImage(state: GroundImageState) { } } +@Composable +fun MapViewScope.SouthWest(state: MarkerState) { + val rememberState = remember(state.fingerPrint()) { state } + SideEffect { + markerFlow.value = markerFlow.value.filter { it.id != state.id } + rememberState + } +} + +@Composable +fun MapViewScope.NorthEast(state: MarkerState) { + val rememberState = remember(state.fingerPrint()) { state } + SideEffect { + markerFlow.value = markerFlow.value.filter { it.id != state.id } + rememberState + } +} + @Composable fun MapViewScope.GroundImage( bounds: GeoRectBounds, From d705245ac907c3d49498f8a5eaff02c2a51b23ff Mon Sep 17 00:00:00 2001 From: KouTsukigase Date: Mon, 18 Aug 2025 00:57:45 +0900 Subject: [PATCH 11/44] WIP: GroundImage feature implementation. Known issue: recomposition does not occur, committing current state. --- .../mapconductor/example/MapViewContainer.kt | 1 + .../groundimage/GroundImageMapComponent.kt | 18 ++++----- .../pages/groundimage/GroundImageMapPage.kt | 4 ++ .../GroundImageMapPageViewModel.kt | 37 ++++++++++++------- .../core/groundimage/GroundImage.kt | 4 +- .../core/groundimage/GroundImageComponent.kt | 16 -------- 6 files changed, 39 insertions(+), 41 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt index 36b09250..e7bf5de9 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -9,6 +9,7 @@ import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler +import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.googlemaps.GoogleMapViewState diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt index 4146ebd7..17106ab8 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -4,10 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier import com.mapconductor.core.groundimage.GroundImage -import com.mapconductor.core.groundimage.NorthEast import com.mapconductor.core.groundimage.OnGroundImageEventHandler -import com.mapconductor.core.groundimage.SouthWest import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.example.MapViewContainer @@ -15,6 +15,8 @@ import com.mapconductor.example.MapViewContainer fun GroundImageMapComponent( mapViewState: MapViewState<*>?, viewModel: GroundImageMapPageViewModel, + southWestMarker: MarkerState, + northEastMarker: MarkerState, modifier: Modifier = Modifier, onGroundImageClick: OnGroundImageEventHandler = {}, onMarkerDrag: OnMarkerEventHandler = {}, @@ -30,14 +32,10 @@ fun GroundImageMapComponent( key(viewModel.groundImageState.fingerPrint()) { GroundImage(viewModel.groundImageState) } - // SouthWest - key(viewModel.southWestMarkerState.fingerPrint()) { - SouthWest(viewModel.southWestMarkerState) - } - // NorthEast - key(viewModel.northEastMarkerState.fingerPrint()) { - NorthEast(viewModel.northEastMarkerState) - } + + // BoundsMarkers + Marker(southWestMarker) + Marker(northEastMarker) } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index a6fce2c2..c3c744b5 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -46,8 +46,12 @@ fun GroundImageMapPage( onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> val mapViewState = viewModel.mapViewState.collectAsState() + val southWestMarker = viewModel.southWest + val northEastMarker = viewModel.northEast GroundImageMapComponent( + southWestMarker = southWestMarker, + northEastMarker = northEastMarker, mapViewState = mapViewState.value, viewModel = viewModel, onGroundImageClick = viewModel::onGroundImageClick, diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index a2181360..8774d745 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -1,5 +1,6 @@ package com.mapconductor.example.pages.groundimage +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -25,10 +26,8 @@ interface GroundImageMapPageViewModel { val messages: StateFlow> val bounds: GeoRectBounds - var southWest: GeoPoint - val southWestMarkerState: MarkerState - var northEast: GeoPoint - val northEastMarkerState: MarkerState + val southWest: MarkerState + val northEast: MarkerState val imageResources: GroundImageResources val image: Drawable var opacity: Float @@ -82,11 +81,11 @@ class GroundImageMapPageViewModelImpl( private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) override val messages: StateFlow> = _messages.asStateFlow() - override var southWest by mutableStateOf(GeoPoint.fromLatLong(40.712216, -74.22655)) - override val southWestMarkerState = + private val _southWest: MutableState = + mutableStateOf( MarkerState( id = "south_west", - position = southWest, + position = GeoPoint.fromLatLong(40.712216, -74.22655), icon = DefaultIcon( fillColor = Color.Blue, @@ -95,12 +94,15 @@ class GroundImageMapPageViewModelImpl( ), draggable = true, ) + ) + override val southWest: MarkerState + get() = _southWest.value - override var northEast by mutableStateOf(GeoPoint.fromLatLong(40.773941,-74.12544)) - override val northEastMarkerState = + private val _northEast: MutableState = + mutableStateOf( MarkerState( id = "north_east", - position = northEast, + position = GeoPoint.fromLatLong(40.773941,-74.12544), icon = DefaultIcon( fillColor = Color.Red, @@ -109,10 +111,13 @@ class GroundImageMapPageViewModelImpl( ), draggable = true, ) + ) + override val northEast: MarkerState + get() = _northEast.value override val bounds: GeoRectBounds = GeoRectBounds( - southWest = southWest, - northEast = northEast, + southWest = GeoPoint.from(southWest.position), + northEast = GeoPoint.from(northEast.position), ) override var opacity by mutableStateOf(0.5f) @@ -140,10 +145,14 @@ class GroundImageMapPageViewModelImpl( override fun onMarkerDrag(dragged: MarkerState) { if (dragged.id == "south_west") { - southWest = GeoPoint.fromLatLong(dragged.position.latitude, dragged.position.longitude) + val southWest = GeoPoint.from(dragged.position) + val northEast = groundImageState.bounds.northEast + groundImageState.bounds = GeoRectBounds(southWest, northEast) } else { - northEast = GeoPoint.fromLatLong(dragged.position.latitude, dragged.position.longitude) + val southWest = groundImageState.bounds.southWest + val northEast = GeoPoint.from(dragged.position) + groundImageState.bounds = GeoRectBounds(southWest, northEast) } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt index aa22b4a0..767a6de7 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt @@ -37,7 +37,9 @@ class GroundImageState( extra = extra?.hashCode() ?: 0, ) - fun asFlow(): Flow = snapshotFlow { fingerPrint() }.distinctUntilChanged() + fun asFlow(): Flow = snapshotFlow { + fingerPrint() + }.distinctUntilChanged() private fun generateId( bounds: GeoRectBounds, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt index 3520faba..ff026d47 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt @@ -24,22 +24,6 @@ fun MapViewScope.GroundImage(state: GroundImageState) { } } -@Composable -fun MapViewScope.SouthWest(state: MarkerState) { - val rememberState = remember(state.fingerPrint()) { state } - SideEffect { - markerFlow.value = markerFlow.value.filter { it.id != state.id } + rememberState - } -} - -@Composable -fun MapViewScope.NorthEast(state: MarkerState) { - val rememberState = remember(state.fingerPrint()) { state } - SideEffect { - markerFlow.value = markerFlow.value.filter { it.id != state.id } + rememberState - } -} - @Composable fun MapViewScope.GroundImage( bounds: GeoRectBounds, From 8b878646859b4cf48783059af9a1831677900349 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Mon, 18 Aug 2025 02:33:56 +0900 Subject: [PATCH 12/44] Fix: GroundImage does not update by following markers --- .../mapconductor/example/MapViewContainer.kt | 1 - .../groundimage/GroundImageMapComponent.kt | 10 +- .../GroundImageMapPageViewModel.kt | 91 ++++++++----------- .../core/groundimage/GroundImage.kt | 9 +- .../core/groundimage/GroundImageComponent.kt | 16 +--- 5 files changed, 54 insertions(+), 73 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt index e7bf5de9..36b09250 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -9,7 +9,6 @@ import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler -import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.googlemaps.GoogleMapViewState diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt index 17106ab8..42be06dc 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -29,13 +29,17 @@ fun GroundImageMapComponent( onMarkerDrag = onMarkerDrag, ) { // GroundImage - key(viewModel.groundImageState.fingerPrint()) { + key(viewModel.groundImageState.id) { GroundImage(viewModel.groundImageState) } // BoundsMarkers - Marker(southWestMarker) - Marker(northEastMarker) + key(southWestMarker.id) { + Marker(southWestMarker) + } + key(northEastMarker.id) { + Marker(northEastMarker) + } } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index 8774d745..b4aacf0a 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -1,6 +1,5 @@ package com.mapconductor.example.pages.groundimage -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -25,7 +24,6 @@ interface GroundImageMapPageViewModel { val mapViewState: StateFlow?> val messages: StateFlow> - val bounds: GeoRectBounds val southWest: MarkerState val northEast: MarkerState val imageResources: GroundImageResources @@ -35,8 +33,6 @@ interface GroundImageMapPageViewModel { fun onMapViewChanged(state: MapViewState<*>) - fun cameraReset(listener: MapViewState.MoveCameraCallback? = null) - fun onGroundImageClick(clicked: GroundImageEvent) fun onMarkerDrag(dragged: MarkerState) @@ -70,60 +66,54 @@ class GroundImageMapPageViewModelImpl( this._mapViewState.value = state } - override fun cameraReset(listener: MapViewState.MoveCameraCallback?) { - this.mapViewState.value?.moveCameraTo( - cameraPosition = initCameraPosition, - durationMs = 3000, - listener = listener, - ) - } - private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) override val messages: StateFlow> = _messages.asStateFlow() - private val _southWest: MutableState = - mutableStateOf( - MarkerState( - id = "south_west", - position = GeoPoint.fromLatLong(40.712216, -74.22655), - icon = - DefaultIcon( - fillColor = Color.Blue, - strokeColor = Color.White, - label = "SW", - ), - draggable = true, - ) - ) - override val southWest: MarkerState - get() = _southWest.value - - private val _northEast: MutableState = - mutableStateOf( - MarkerState( - id = "north_east", - position = GeoPoint.fromLatLong(40.773941,-74.12544), - icon = - DefaultIcon( - fillColor = Color.Red, - strokeColor = Color.White, - label = "NE", - ), - draggable = true, - ) + override val southWest: MarkerState = + MarkerState( + id = "south_west", + position = + GeoPoint( + latitude = 40.712216, + longitude = -74.22655, + ), + icon = + DefaultIcon( + fillColor = Color.Blue, + strokeColor = Color.White, + label = "SW", + ), + draggable = true, ) - override val northEast: MarkerState - get() = _northEast.value - override val bounds: GeoRectBounds = GeoRectBounds( - southWest = GeoPoint.from(southWest.position), - northEast = GeoPoint.from(northEast.position), + override val northEast: MarkerState = + MarkerState( + id = "north_east", + position = + GeoPoint( + latitude = 40.773941, + longitude = -74.12544, + ), + icon = + DefaultIcon( + fillColor = Color.Red, + strokeColor = Color.White, + label = "NE", + ), + draggable = true, ) override var opacity by mutableStateOf(0.5f) override var image by mutableStateOf(imageResources.image) + private var bounds by mutableStateOf( + GeoRectBounds( + southWest = GeoPoint.from(southWest.position), + northEast = GeoPoint.from(northEast.position), + ), + ) + override val groundImageState get() = GroundImageState( @@ -147,12 +137,11 @@ class GroundImageMapPageViewModelImpl( if (dragged.id == "south_west") { val southWest = GeoPoint.from(dragged.position) val northEast = groundImageState.bounds.northEast - groundImageState.bounds = GeoRectBounds(southWest, northEast) - } - else { + bounds = GeoRectBounds(southWest, northEast) + } else { val southWest = groundImageState.bounds.southWest val northEast = GeoPoint.from(dragged.position) - groundImageState.bounds = GeoRectBounds(southWest, northEast) + bounds = GeoRectBounds(southWest, northEast) } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt index 767a6de7..0f7068f3 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt @@ -4,10 +4,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow -import com.mapconductor.core.StateFlowDelegate import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.GeoRectBounds -import com.mapconductor.core.marker.MarkerState import android.graphics.drawable.Drawable import android.os.Parcelable import kotlinx.coroutines.flow.Flow @@ -37,9 +35,10 @@ class GroundImageState( extra = extra?.hashCode() ?: 0, ) - fun asFlow(): Flow = snapshotFlow { - fingerPrint() - }.distinctUntilChanged() + fun asFlow(): Flow = + snapshotFlow { + fingerPrint() + }.distinctUntilChanged() private fun generateId( bounds: GeoRectBounds, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt index ff026d47..06c84bb6 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageComponent.kt @@ -1,26 +1,16 @@ package com.mapconductor.core.groundimage import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp +import androidx.compose.runtime.LaunchedEffect import com.mapconductor.core.MapViewScope -import com.mapconductor.core.circle.Circle -import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoRectBounds -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.marker.MarkerState import android.graphics.drawable.Drawable import android.os.Parcelable -import kotlinx.coroutines.flow.filter @Composable fun MapViewScope.GroundImage(state: GroundImageState) { - val rememberState = remember(state.fingerPrint()) { state } - SideEffect { - groundImageFlow.value = groundImageFlow.value.filter { it.id != state.id } + rememberState + LaunchedEffect(state.fingerPrint()) { + groundImageFlow.value = groundImageFlow.value.filter { it.id != state.id } + state } } From 5ae84721c77053b4cb36c7374c59d66fdd5ef76b Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Mon, 18 Aug 2025 03:01:10 +0900 Subject: [PATCH 13/44] Fix: GeoRectBounds.extend is incorrect --- .../GroundImageMapPageViewModel.kt | 11 +-- .../core/features/GeoRectBounds.kt | 84 +++++++++++++------ 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index b4aacf0a..aaa47173 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -134,14 +134,9 @@ class GroundImageMapPageViewModelImpl( } override fun onMarkerDrag(dragged: MarkerState) { - if (dragged.id == "south_west") { - val southWest = GeoPoint.from(dragged.position) - val northEast = groundImageState.bounds.northEast - bounds = GeoRectBounds(southWest, northEast) - } else { - val southWest = groundImageState.bounds.southWest - val northEast = GeoPoint.from(dragged.position) - bounds = GeoRectBounds(southWest, northEast) + bounds = GeoRectBounds().also { + it.extend(southWest.position) + it.extend(northEast.position) } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt index 4be64fab..2acf3d87 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt @@ -11,38 +11,68 @@ data class GeoRectBounds( get() = southWest == null || northEast == null fun extend(point: IGeoPoint) { - val target = GeoPoint.from(point) - - if (isEmpty) { - southWest = target - northEast = target - return - } - - val sw = southWest!! - val ne = northEast!! - - val south = min(sw.latitude, target.latitude) - val north = max(ne.latitude, target.latitude) - - var west = sw.longitude - var east = ne.longitude + val position = GeoPoint.from(point) + + when { + // まだ何もない:両方に同じ点を入れて初期化 + southWest == null && northEast == null -> { + southWest = position + northEast = position + return + } - val lon = target.longitude + // southWest だけある:既存点と position の2点から SW/NE を決める + southWest != null && northEast == null -> { + val sw = southWest!! + val south = minOf(sw.latitude, position.latitude) + val north = maxOf(sw.latitude, position.latitude) + // 1点ずつなので子午線跨ぎの判定は不要。単純に min/max でOK + val west = minOf(sw.longitude, position.longitude) + val east = maxOf(sw.longitude, position.longitude) + + southWest = GeoPoint(south, west) + northEast = GeoPoint(north, east) + return + } - if (!containsLongitude(lon, west, east)) { - val distToEast = distanceEast(lon, east) - val distToWest = distanceWest(west, lon) + // northEast だけある:既存点と position の2点から SW/NE を決める + southWest == null && northEast != null -> { + val ne = northEast!! + val south = minOf(ne.latitude, position.latitude) + val north = maxOf(ne.latitude, position.latitude) + val west = minOf(ne.longitude, position.longitude) + val east = maxOf(ne.longitude, position.longitude) + + southWest = GeoPoint(south, west) + northEast = GeoPoint(north, east) + return + } - if (distToEast < distToWest) { - east = lon - } else { - west = lon + else -> { + // どちらもある:従来ロジック(子午線跨ぎ考慮)+ 緯度/経度の参照を修正 + val south = minOf(position.latitude, southWest!!.latitude) + val north = maxOf(position.latitude, northEast!!.latitude) + + var west = southWest!!.longitude + var east = northEast!!.longitude + + if (west > 0 && east < 0) { + // すでに経度が + と - に分かれている=日付変更線跨ぎの矩形 + if (position.longitude > 0) { + west = minOf(position.longitude, west) + } else { + east = maxOf(position.longitude, east) + } + } else { + // 通常ケース:単純に min/max + west = minOf(position.longitude, southWest!!.longitude) + east = maxOf(position.longitude, northEast!!.longitude) + } + + southWest = GeoPoint(south, west) + northEast = GeoPoint(north, east) } } - - southWest = GeoPoint(south, west) - northEast = GeoPoint(north, east) } private fun distanceEast( From 82daee10644168ec48d88eece7498ba769a5ee14 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Mon, 18 Aug 2025 03:27:42 +0900 Subject: [PATCH 14/44] Update: Marker labels based on the relative positions --- .../groundimage/GroundImageMapComponent.kt | 12 +- .../pages/groundimage/GroundImageMapPage.kt | 4 - .../GroundImageMapPageViewModel.kt | 119 ++++++++++++------ .../core/features/GeoRectBounds.kt | 11 +- 4 files changed, 90 insertions(+), 56 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt index 42be06dc..d85e6182 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -7,7 +7,6 @@ import com.mapconductor.core.groundimage.GroundImage import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.Marker -import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.example.MapViewContainer @@ -15,8 +14,6 @@ import com.mapconductor.example.MapViewContainer fun GroundImageMapComponent( mapViewState: MapViewState<*>?, viewModel: GroundImageMapPageViewModel, - southWestMarker: MarkerState, - northEastMarker: MarkerState, modifier: Modifier = Modifier, onGroundImageClick: OnGroundImageEventHandler = {}, onMarkerDrag: OnMarkerEventHandler = {}, @@ -34,11 +31,10 @@ fun GroundImageMapComponent( } // BoundsMarkers - key(southWestMarker.id) { - Marker(southWestMarker) - } - key(northEastMarker.id) { - Marker(northEastMarker) + viewModel.markers.forEach { marker -> + key(marker.fingerPrint()) { + Marker(marker) + } } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index c3c744b5..a6fce2c2 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -46,12 +46,8 @@ fun GroundImageMapPage( onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> val mapViewState = viewModel.mapViewState.collectAsState() - val southWestMarker = viewModel.southWest - val northEastMarker = viewModel.northEast GroundImageMapComponent( - southWestMarker = southWestMarker, - northEastMarker = northEastMarker, mapViewState = mapViewState.value, viewModel = viewModel, onGroundImageClick = viewModel::onGroundImageClick, diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index aaa47173..0be2a546 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -15,6 +15,7 @@ import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerState import com.mapconductor.example.toast.ToastMessage import android.graphics.drawable.Drawable +import android.util.Log import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -24,8 +25,7 @@ interface GroundImageMapPageViewModel { val mapViewState: StateFlow?> val messages: StateFlow> - val southWest: MarkerState - val northEast: MarkerState + val markers: List val imageResources: GroundImageResources val image: Drawable var opacity: Float @@ -69,39 +69,76 @@ class GroundImageMapPageViewModelImpl( private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) override val messages: StateFlow> = _messages.asStateFlow() - override val southWest: MarkerState = - MarkerState( - id = "south_west", - position = - GeoPoint( - latitude = 40.712216, - longitude = -74.22655, - ), - icon = - DefaultIcon( - fillColor = Color.Blue, - strokeColor = Color.White, - label = "SW", - ), - draggable = true, - ) + private var _southWestPosition by mutableStateOf( + GeoPoint( + latitude = 40.712216, + longitude = -74.22655, + ), + ) - override val northEast: MarkerState = - MarkerState( - id = "north_east", - position = - GeoPoint( - latitude = 40.773941, - longitude = -74.12544, + private var _northEastPosition by mutableStateOf( + GeoPoint( + latitude = 40.773941, + longitude = -74.12544, + ), + ) + + private fun calculateMarkerLabels(): Pair { + val swLat = _southWestPosition.latitude + val swLng = _southWestPosition.longitude + val neLat = _northEastPosition.latitude + val neLng = _northEastPosition.longitude + + val southWestLabel = + when { + swLat <= neLat && swLng <= neLng -> "SW" // Normal + swLat <= neLat && swLng > neLng -> "SE" // East-West flipped + swLat > neLat && swLng <= neLng -> "NW" // North-South flipped + else -> "NE" // Both flipped + } + + val northEastLabel = + when { + neLat >= swLat && neLng >= swLng -> "NE" // Normal + neLat >= swLat && neLng < swLng -> "NW" // East-West flipped + neLat < swLat && neLng >= swLng -> "SE" // North-South flipped + else -> "SW" // Both flipped + } + + return Pair(southWestLabel, northEastLabel) + } + + override val markers: List + get() { + val (swLabel, neLabel) = calculateMarkerLabels() + Log.d("debug", "$swLabel, $neLabel") + return listOf( + MarkerState( + id = "south_west", + position = _southWestPosition, + icon = + DefaultIcon( + fillColor = Color.Blue, + strokeColor = Color.White, + label = swLabel, + labelTextColor = Color.White, + ), + draggable = true, ), - icon = - DefaultIcon( - fillColor = Color.Red, - strokeColor = Color.White, - label = "NE", + MarkerState( + id = "north_east", + position = _northEastPosition, + icon = + DefaultIcon( + fillColor = Color.Red, + strokeColor = Color.White, + label = neLabel, + labelTextColor = Color.White, + ), + draggable = true, ), - draggable = true, - ) + ) + } override var opacity by mutableStateOf(0.5f) @@ -109,8 +146,8 @@ class GroundImageMapPageViewModelImpl( private var bounds by mutableStateOf( GeoRectBounds( - southWest = GeoPoint.from(southWest.position), - northEast = GeoPoint.from(northEast.position), + southWest = GeoPoint.from(_southWestPosition), + northEast = GeoPoint.from(_northEastPosition), ), ) @@ -134,10 +171,18 @@ class GroundImageMapPageViewModelImpl( } override fun onMarkerDrag(dragged: MarkerState) { - bounds = GeoRectBounds().also { - it.extend(southWest.position) - it.extend(northEast.position) + // Update the internal position based on which marker was dragged + when (dragged.id) { + "south_west" -> _southWestPosition = GeoPoint.from(dragged.position) + "north_east" -> _northEastPosition = GeoPoint.from(dragged.position) } + + // Update bounds using the new positions + bounds = + GeoRectBounds().also { + it.extend(markers[0].position) + it.extend(markers[1].position) + } } override fun showToast(text: String) { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt index 2acf3d87..74b61ca6 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt @@ -1,8 +1,5 @@ package com.mapconductor.core.features -import kotlin.math.max -import kotlin.math.min - data class GeoRectBounds( var southWest: GeoPoint? = null, var northEast: GeoPoint? = null, @@ -27,8 +24,8 @@ data class GeoRectBounds( val south = minOf(sw.latitude, position.latitude) val north = maxOf(sw.latitude, position.latitude) // 1点ずつなので子午線跨ぎの判定は不要。単純に min/max でOK - val west = minOf(sw.longitude, position.longitude) - val east = maxOf(sw.longitude, position.longitude) + val west = minOf(sw.longitude, position.longitude) + val east = maxOf(sw.longitude, position.longitude) southWest = GeoPoint(south, west) northEast = GeoPoint(north, east) @@ -40,8 +37,8 @@ data class GeoRectBounds( val ne = northEast!! val south = minOf(ne.latitude, position.latitude) val north = maxOf(ne.latitude, position.latitude) - val west = minOf(ne.longitude, position.longitude) - val east = maxOf(ne.longitude, position.longitude) + val west = minOf(ne.longitude, position.longitude) + val east = maxOf(ne.longitude, position.longitude) southWest = GeoPoint(south, west) northEast = GeoPoint(north, east) From 22fb300e1dc38e990b0b308baa7672374388cb1e Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Tue, 19 Aug 2025 22:26:22 +0900 Subject: [PATCH 15/44] Fix: ktLint errors --- .../GroundImageMapPageViewModel.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index 0be2a546..59c74501 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -69,14 +69,14 @@ class GroundImageMapPageViewModelImpl( private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) override val messages: StateFlow> = _messages.asStateFlow() - private var _southWestPosition by mutableStateOf( + private var southWestPosition by mutableStateOf( GeoPoint( latitude = 40.712216, longitude = -74.22655, ), ) - private var _northEastPosition by mutableStateOf( + private var northEastPosition by mutableStateOf( GeoPoint( latitude = 40.773941, longitude = -74.12544, @@ -84,10 +84,10 @@ class GroundImageMapPageViewModelImpl( ) private fun calculateMarkerLabels(): Pair { - val swLat = _southWestPosition.latitude - val swLng = _southWestPosition.longitude - val neLat = _northEastPosition.latitude - val neLng = _northEastPosition.longitude + val swLat = southWestPosition.latitude + val swLng = southWestPosition.longitude + val neLat = northEastPosition.latitude + val neLng = northEastPosition.longitude val southWestLabel = when { @@ -115,7 +115,7 @@ class GroundImageMapPageViewModelImpl( return listOf( MarkerState( id = "south_west", - position = _southWestPosition, + position = southWestPosition, icon = DefaultIcon( fillColor = Color.Blue, @@ -127,7 +127,7 @@ class GroundImageMapPageViewModelImpl( ), MarkerState( id = "north_east", - position = _northEastPosition, + position = northEastPosition, icon = DefaultIcon( fillColor = Color.Red, @@ -146,8 +146,8 @@ class GroundImageMapPageViewModelImpl( private var bounds by mutableStateOf( GeoRectBounds( - southWest = GeoPoint.from(_southWestPosition), - northEast = GeoPoint.from(_northEastPosition), + southWest = GeoPoint.from(southWestPosition), + northEast = GeoPoint.from(northEastPosition), ), ) @@ -173,8 +173,8 @@ class GroundImageMapPageViewModelImpl( override fun onMarkerDrag(dragged: MarkerState) { // Update the internal position based on which marker was dragged when (dragged.id) { - "south_west" -> _southWestPosition = GeoPoint.from(dragged.position) - "north_east" -> _northEastPosition = GeoPoint.from(dragged.position) + "south_west" -> southWestPosition = GeoPoint.from(dragged.position) + "north_east" -> northEastPosition = GeoPoint.from(dragged.position) } // Update bounds using the new positions From 49dca646c825c6879493414bef177d1b932b52d9 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Tue, 19 Aug 2025 23:07:45 +0900 Subject: [PATCH 16/44] Use LaunchedEffect instead of SideEffect --- .../main/java/com/mapconductor/example/DemoAppScreen.kt | 2 +- .../example/pages/circle/CircleMapComponent.kt | 4 +--- .../example/pages/groundimage/GroundImageMapComponent.kt | 8 ++------ .../pages/groundimage/GroundImageMapPageViewModel.kt | 2 -- .../java/com/mapconductor/core/circle/CircleCompose.kt | 6 +++--- .../java/com/mapconductor/core/marker/MarkerCompose.kt | 6 +++--- .../com/mapconductor/core/polyline/PolylineComponent.kt | 6 +++--- 7 files changed, 13 insertions(+), 21 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index 75a445bd..fb1fc614 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -75,7 +75,7 @@ fun DemoAppScreen() { ), SidebarItem( id = "groundImage", - title = "GroundImage Demo", + title = "GroundImage", icon = Icons.Default.Favorite, route = "groundImage", ), diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt index 56cb301a..7d983ae6 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt @@ -31,9 +31,7 @@ fun CircleMapComponent( onMarkerDrag = onMarkerDrag, ) { // Circle - key(viewModel.circleState.id) { - Circle(viewModel.circleState) - } + Circle(viewModel.circleState) // Center marker (not draggable) key(viewModel.centerMarker.id) { diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt index d85e6182..47e811d2 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -26,15 +26,11 @@ fun GroundImageMapComponent( onMarkerDrag = onMarkerDrag, ) { // GroundImage - key(viewModel.groundImageState.id) { - GroundImage(viewModel.groundImageState) - } + GroundImage(viewModel.groundImageState) // BoundsMarkers viewModel.markers.forEach { marker -> - key(marker.fingerPrint()) { - Marker(marker) - } + Marker(marker) } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt index 59c74501..4047338c 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPageViewModel.kt @@ -15,7 +15,6 @@ import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerState import com.mapconductor.example.toast.ToastMessage import android.graphics.drawable.Drawable -import android.util.Log import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -111,7 +110,6 @@ class GroundImageMapPageViewModelImpl( override val markers: List get() { val (swLabel, neLabel) = calculateMarkerLabels() - Log.d("debug", "$swLabel, $neLabel") return listOf( MarkerState( id = "south_west", diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt index 30ed1ce4..639c2f37 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt @@ -1,6 +1,7 @@ package com.mapconductor.core.circle import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color @@ -12,9 +13,8 @@ import android.os.Parcelable @Composable fun MapViewScope.Circle(state: CircleState) { - val rememberState = remember(state.fingerPrint()) { state } - SideEffect { - circleFlow.value = circleFlow.value.filter { it.id != rememberState.id } + rememberState + LaunchedEffect(state.fingerPrint()) { + circleFlow.value = circleFlow.value.filter { it.id != state.id } + state } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt index 893dc601..2a1694d8 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt @@ -1,6 +1,7 @@ package com.mapconductor.core.marker import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember import com.mapconductor.core.MapViewScope @@ -9,9 +10,8 @@ import android.os.Parcelable @Composable fun MapViewScope.Marker(state: MarkerState) { - val rememberMarker = remember(state.id) { state } - SideEffect { - markerFlow.value = markerFlow.value.filter { it.id != rememberMarker.id } + rememberMarker + LaunchedEffect(state.fingerPrint()) { + markerFlow.value = markerFlow.value.filter { it.id != state.id } + state } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt index 9fcecf80..25a15e24 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt @@ -1,6 +1,7 @@ package com.mapconductor.core.polyline import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color @@ -12,9 +13,8 @@ import android.os.Parcelable @Composable fun MapViewScope.Polyline(state: PolylineState) { - val rememberState = remember(state.fingerPrint()) { state } - SideEffect { - polylineFlow.value = polylineFlow.value.filter { it.id != rememberState.id } + rememberState + LaunchedEffect(state.fingerPrint()) { + polylineFlow.value = polylineFlow.value.filter { it.id != state.id } + state } } From 18e6d7f2ebfda0953960565fbdeb538b41a0d5ec Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 20 Aug 2025 15:08:51 +0900 Subject: [PATCH 17/44] Fix: crash the app on displaying maps (except Google Maps) --- .../src/main/java/com/mapconductor/core/OverlayProvider.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index 917b804b..ccced255 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -170,7 +170,9 @@ fun CollectAndRenderOverlays( LaunchedEffect(Unit) { typedOverlay.flow.collect { - typedOverlay.render(it, controller) + if (it.isNotEmpty()) { + typedOverlay.render(it, controller) + } } } From 41443ce3954ca047794f43a51f3c45ee0c93dd84 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 20 Aug 2025 19:16:39 +0900 Subject: [PATCH 18/44] Update: GroundImage is now selectable by controller --- .../groundimage/GroundImageMapComponent.kt | 1 - .../com/mapconductor/core/OverlayProvider.kt | 12 +- .../mapconductor/core/circle/CircleCompose.kt | 2 - .../core/controller/MapViewController.kt | 83 ++++++---- .../core/groundimage/GroundImageController.kt | 141 +++++++++++++++++ .../core/groundimage/GroundImageOverlay.kt | 16 ++ .../groundimage/GroundImageOverlayManager.kt | 146 ------------------ .../core/groundimage/GroundImageRenderer.kt | 38 ----- .../com/mapconductor/core/map/MapViewBase.kt | 13 +- .../mapconductor/core/marker/MarkerCompose.kt | 2 - .../core/polyline/PolylineComponent.kt | 2 - .../arcgis/ArcGISMapViewController.kt | 29 ---- .../mapconductor/arcgis/ArcGISTypeAlias.kt | 1 - .../groundimage/ArcGISGroundImageRenderer.kt | 25 --- .../googlemaps/GoogleMapTypeAlias.kt | 13 ++ .../googlemaps/GoogleMapViewController.kt | 59 +++---- .../GoogleMapViewControllerStore.kt | 13 ++ .../GoogleMapGroundImageRenderer.kt | 59 +++---- .../com/mapconductor/here/HereMapTypeAlias.kt | 2 - .../here/HereMapViewController.kt | 40 +---- .../groundimage/HereGroundImageRenderer.kt | 51 ------ .../mapbox/MapboxMapViewController.kt | 29 ---- .../mapconductor/mapbox/MapboxTypeAlias.kt | 1 - .../groundImage/MapboxGroundImageRenderer.kt | 22 --- 24 files changed, 290 insertions(+), 510 deletions(-) create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageRenderer.kt delete mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundimage/ArcGISGroundImageRenderer.kt create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt delete mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt delete mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt index 47e811d2..7d1e2620 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapComponent.kt @@ -1,7 +1,6 @@ package com.mapconductor.example.pages.groundimage import androidx.compose.runtime.Composable -import androidx.compose.runtime.key import androidx.compose.ui.Modifier import com.mapconductor.core.groundimage.GroundImage import com.mapconductor.core.groundimage.OnGroundImageEventHandler diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index ccced255..dc351e37 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.groundimage.GroundImageOverlay import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.info.InfoBubbleEntry import com.mapconductor.core.map.MapOverlay @@ -116,17 +117,6 @@ class PolylineOverlay( } } -class GroundImageOverlay( - override val flow: StateFlow>, -) : MapOverlay { - override suspend fun render( - data: List, - controller: MapViewControllerAlias, - ) { - controller.addGroundImages(data) - } -} - val LocalPolylineCollector = compositionLocalOf>> { error("Polyline must be under the ") diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt index 639c2f37..ec5c710d 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCompose.kt @@ -2,8 +2,6 @@ package com.mapconductor.core.circle import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index 06c5f73a..6fccbc27 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -4,11 +4,8 @@ import com.mapconductor.core.circle.CircleOverlayManager import com.mapconductor.core.circle.CircleRenderer import com.mapconductor.core.circle.CircleState import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.groundimage.GroundImageOverlayManager -import com.mapconductor.core.groundimage.GroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageState -import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler @@ -24,7 +21,7 @@ import com.mapconductor.core.polyline.PolylineRenderer import com.mapconductor.core.polyline.PolylineState import kotlinx.coroutines.CoroutineScope -interface MapViewController { +interface MapViewController { val holder: MapViewHolder<*, *> val coroutine: CoroutineScope val markerOverlayManager: MarkerOverlayManager @@ -32,7 +29,6 @@ interface MapViewController val circleOverlayManager: CircleOverlayManager val polygonOverlayManager: PolygonOverlayManager - val groundImageOverlayManager: GroundImageOverlayManager suspend fun addMarkers(data: List) @@ -42,10 +38,6 @@ interface MapViewController) - - suspend fun updateGroundImage(state: GroundImageState) - suspend fun addCircles(data: List) suspend fun updateCircle(state: CircleState) @@ -73,13 +65,57 @@ interface MapViewController + +interface OverlayRenderer { + interface Changes { + val current: EntityType + val prev: EntityType + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List) + + suspend fun onPostProcess() +} + +interface OverlayController { + val zIndex: Int + + // val overlayManager: OverlayManager + val renderer: OverlayRenderer + + suspend fun add(data: List) + + suspend fun update(state: StateType) + + suspend fun clear() - fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) + var clickListener: ((EventType) -> Unit)? + + fun find(position: IGeoPoint): EntityType? +} + +interface OverlayManager { + suspend fun add(states: List) + + suspend fun update(state: StateType) + + suspend fun clear() + + fun getById(id: String): StateType? + + fun allEntities(): List + + fun find(position: IGeoPoint): EntityType? } -typealias MapViewControllerAlias = MapViewController<*, *, *, *, *> -abstract class BaseMapViewController : - MapViewController { +abstract class BaseMapViewController : + MapViewController { abstract val markerRenderer: MarkerRenderer override val markerOverlayManager: MarkerOverlayManager by lazy { @@ -106,14 +142,6 @@ abstract class BaseMapViewController - - override val groundImageOverlayManager: GroundImageOverlayManager by lazy { - createGroundImageOverlayManager().also { overlayManager -> - groundImageRenderer.init(overlayManager) - onGroundImageOverlayManagerInitialized(overlayManager) - } - } abstract val circleRenderer: CircleRenderer @@ -132,10 +160,6 @@ abstract class BaseMapViewController) - protected abstract fun onGroundImageOverlayManagerInitialized( - overlayManager: GroundImageOverlayManager, - ) - protected abstract fun createMarkerOverlayManager(): MarkerOverlayManager protected abstract fun createPolylineOverlayManager(): PolylineOverlayManager @@ -144,8 +168,6 @@ abstract class BaseMapViewController - protected abstract fun createGroundImageOverlayManager(): GroundImageOverlayManager - protected var cameraMoveCallback: OnCameraMoveHandler? = null protected var mapClickCallback: OnMapEventHandler? = null protected var mapLongClickCallback: OnMapEventHandler? = null @@ -155,7 +177,6 @@ abstract class BaseMapViewController { + suspend fun compositionGroundImages(data: List) + + suspend fun updateGroundImage(state: GroundImageState) +} + +class GroundImageController( + override val renderer: OverlayRenderer>, + override var clickListener: OnGroundImageEventHandler? = null, +) : OverlayController< + ActualGroundImage, + GroundImageState, + GroundImageEntity, + GroundImageEvent, + > { + override val zIndex: Int = 2 + val entities = mutableMapOf>() + val semaphore = Semaphore(1) + + override suspend fun add(data: List) { + semaphore.withPermit { + val previous = entities.keys.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>>() + val removed = mutableListOf>() + + data.forEach { + if (previous.contains(it.id)) { + val prevEntity = entities[it.id]!! + updated.add( + object : OverlayRenderer.Changes> { + override val current = + GroundImageEntityImpl( + groundImage = prevEntity.groundImage, + state = it, + ) + override val prev = prevEntity + }, + ) + previous.remove(it.id) + } else { + added.add(it) + previous.remove(it.id) + } + } + + previous.forEach { remainId -> + entities.remove(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + if (added.isNotEmpty()) { + val actualOverlays = renderer.onAdd(added) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val state = added[index] + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) + entities[state.id] = entity + } + } + } + + if (updated.isNotEmpty()) { + val actualOverlays = renderer.onChange(updated.toList()) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val state = updated[index].current.state + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) + entities[state.id] = entity + } + } + } + + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + renderer.onPostProcess() + } + } + + override suspend fun update(state: GroundImageState) { + semaphore.withPermit { + val updated = mutableListOf>>() + val prevEntity = entities[state.id]!! + updated.add( + object : OverlayRenderer.Changes> { + override val current: GroundImageEntity = + GroundImageEntityImpl( + groundImage = prevEntity.groundImage, + state = state, + ) + override val prev: GroundImageEntity = prevEntity + }, + ) + + val actualOverlays: List = renderer.onChange(updated) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) + entities[state.id] = entity + } + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + renderer.onRemove(entities.values.toList()) + entities.clear() + } + } + + override fun find(position: IGeoPoint): GroundImageEntity? { + // TODO: Improve this implementation later + return entities.values.find { entity -> + entity.state.bounds.contains(position) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt new file mode 100644 index 00000000..5ed59d24 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt @@ -0,0 +1,16 @@ +package com.mapconductor.core.groundimage + +import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.StateFlow + +class GroundImageOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewControllerAlias, + ) { + (controller as? GroundImageCapable<*>)?.compositionGroundImages(data) + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt deleted file mode 100644 index 2704a9d5..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayManager.kt +++ /dev/null @@ -1,146 +0,0 @@ -package com.mapconductor.core.groundimage - -import com.mapconductor.core.features.IGeoPoint -import kotlinx.coroutines.sync.Semaphore - -interface GroundImageOverlayManager { - suspend fun addGroundImages(overlays: List) - - suspend fun updateGroundImage(overlay: GroundImageState) - - suspend fun clearGroundImages() - - fun getGroundImageState(id: String): GroundImageState? - - fun getAllEntities(): List> - - fun find(position: IGeoPoint): GroundImageEntity? -} - -class GroundImageOverlayManagerImpl( - val onAdd: suspend (List) -> List, - val onChange: suspend (List>) -> List, - val onRemove: suspend (List>) -> Unit, - val onPostProcess: (suspend () -> Unit)? = null, -) : GroundImageOverlayManager { - val groundImageEntities = mutableMapOf>() - val semaphore = Semaphore(1) - - override suspend fun addGroundImages(images: List) { - semaphore.acquire() - val previous = groundImageEntities.keys.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>() - val removed = mutableListOf>() - - images.forEach { - if (previous.contains(it.id)) { - val prevEntity = groundImageEntities[it.id]!! - updated.add( - object : GroundImageRenderer.UpdateParams { - override val entity = - GroundImageEntityImpl( - groundImage = prevEntity.groundImage, - state = it, - ) - override val prevEntity = prevEntity - }, - ) - previous.remove(it.id) - } else { - added.add(it) - previous.remove(it.id) - } - } - - previous.forEach { remainId -> - groundImageEntities.remove(remainId)?.let { removedEntity -> - removed.add(removedEntity) - } - } - - if (added.isNotEmpty()) { - val actualOverlays = onAdd(added) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val state = added[index] - val entity = - GroundImageEntityImpl( - groundImage = it, - state = state, - ) - groundImageEntities[state.id] = entity - } - } - } - - if (updated.isNotEmpty()) { - val actualOverlays = onChange(updated.toList()) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val state = updated[index].entity.state - val entity = - GroundImageEntityImpl( - groundImage = it, - state = state, - ) - groundImageEntities[state.id] = entity - } - } - } - - if (removed.isNotEmpty()) { - onRemove(removed) - } - - onPostProcess?.invoke() - semaphore.release() - } - - override suspend fun updateGroundImage(state: GroundImageState) { - val updated = mutableListOf>() - - semaphore.acquire() - val prevEntity = groundImageEntities[state.id]!! - updated.add( - object : GroundImageRenderer.UpdateParams { - override val entity: GroundImageEntity = - GroundImageEntityImpl( - groundImage = prevEntity.groundImage, - state = state, - ) - override val prevEntity: GroundImageEntity = prevEntity - }, - ) - - val actualOverlays: List = onChange(updated) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val entity = - GroundImageEntityImpl( - groundImage = it, - state = state, - ) - groundImageEntities[state.id] = entity - } - } - semaphore.release() - } - - override suspend fun clearGroundImages() { - semaphore.acquire() - val entities = groundImageEntities.values.toList() - onRemove(entities) - groundImageEntities.clear() - semaphore.release() - } - - override fun getGroundImageState(id: String): GroundImageState? = groundImageEntities[id]?.state - - override fun getAllEntities(): List> = groundImageEntities.values.toList() - - override fun find(position: IGeoPoint): GroundImageEntity? = - groundImageEntities.values.find { entity -> - entity.state.bounds.contains(position) - } -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageRenderer.kt deleted file mode 100644 index d123fe47..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageRenderer.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.mapconductor.core.groundimage - -import com.mapconductor.core.map.MapViewHolder -import kotlinx.coroutines.CoroutineScope - -interface GroundImageRendererFactory { - fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)? = null, - ): GroundImageOverlayManager -} - -interface GroundImageRenderer { - interface UpdateParams { - val entity: GroundImageEntity - val prevEntity: GroundImageEntity - } - - fun init(groundImageOverlayManager: GroundImageOverlayManager) - - suspend fun addGroundImages(newGroundImages: List): List - - suspend fun removeGroundImages(removeEntities: List>) - - suspend fun changeGroundImages(changes: List>): List -} - -abstract class AbstractGroundImageRenderer : GroundImageRenderer { - protected lateinit var groundImageOverlayManager: GroundImageOverlayManager - abstract val holder: MapViewHolder<*, *> - abstract val coroutine: CoroutineScope - - override fun init(groundImageOverlayManager: GroundImageOverlayManager) { - this.groundImageOverlayManager = groundImageOverlayManager - } -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index 8251735b..ee89b4b4 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -30,6 +30,7 @@ import com.mapconductor.core.MapViewScope import com.mapconductor.core.ResourceProvider import com.mapconductor.core.controller.MapViewControllerAlias import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.groundimage.GroundImageCapable import com.mapconductor.core.info.InfoBubbleOverlay import com.mapconductor.core.info.LocalInfoBubbleCollector import com.mapconductor.core.marker.DefaultIcon @@ -117,11 +118,13 @@ fun < } } } - val groundImage = scope.groundImageFlow.collectAsState() - groundImage.value.forEach { groundImageState -> - LaunchedEffect(groundImageState.id) { - groundImageState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updateGroundImage(groundImageState) + (controller as? GroundImageCapable<*>)?.let { + val groundImage = scope.groundImageFlow.collectAsState() + groundImage.value.forEach { groundImageState -> + LaunchedEffect(groundImageState.id) { + groundImageState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + controller.updateGroundImage(groundImageState) + } } } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt index 2a1694d8..ddc22005 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt @@ -2,8 +2,6 @@ package com.mapconductor.core.marker import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember import com.mapconductor.core.MapViewScope import com.mapconductor.core.features.IGeoPoint import android.os.Parcelable diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt index 25a15e24..d3e8bef5 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineComponent.kt @@ -2,8 +2,6 @@ package com.mapconductor.core.polyline import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt index da0a7ab3..ec34c923 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt @@ -10,7 +10,6 @@ import com.arcgismaps.mapping.view.UpEvent import com.arcgismaps.mapping.view.extensions.motionEvent import com.mapconductor.arcgis.circle.ArcGISCircleRenderer import com.mapconductor.arcgis.circle.DefaultArcGISCircleRenderer -import com.mapconductor.arcgis.groundimage.ArcGISGroundImageRenderer import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer import com.mapconductor.arcgis.marker.DefaultArcGISMarkerRender import com.mapconductor.arcgis.polygon.ArcGISPolygonRenderer @@ -26,9 +25,6 @@ import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.groundimage.GroundImageOverlayManager -import com.mapconductor.core.groundimage.GroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.MarkerOverlayManager @@ -55,7 +51,6 @@ interface IArcGISMapViewController : ArcGISActualCircle, ArcGISActualPolyline, ArcGISActualPolygon, - ArcGISActualGroundImage, > { fun moveCamera( dstPosition: MapCameraPosition, @@ -110,7 +105,6 @@ class ArcGISMapViewController( ArcGISActualCircle, ArcGISActualPolyline, ArcGISActualPolygon, - ArcGISActualGroundImage, >(), IArcGISMapViewController { override val markerRenderer: MarkerRenderer = @@ -166,10 +160,6 @@ class ArcGISMapViewController( onRemove = circleRenderer::removeCircles, ) - override fun createGroundImageOverlayManager(): GroundImageOverlayManager { - TODO("Not yet implemented") - } - override val circleRenderer: CircleRenderer = ArcGISCircleRenderer( circleLayer = circleLayer, @@ -177,17 +167,6 @@ class ArcGISMapViewController( coroutine = coroutine, ) - override fun onGroundImageOverlayManagerInitialized( - overlayManager: GroundImageOverlayManager, - ) { - } - - override val groundImageRenderer: GroundImageRenderer = - ArcGISGroundImageRenderer( - holder = holder, - coroutine = coroutine, - ) - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } @@ -356,14 +335,6 @@ class ArcGISMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) - override suspend fun addGroundImages(data: List) { - TODO("Not yet implemented") - } - - override suspend fun updateGroundImage(state: GroundImageState) { - TODO("Not yet implemented") - } - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISTypeAlias.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISTypeAlias.kt index 4a216b4e..a6f35e5d 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISTypeAlias.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISTypeAlias.kt @@ -6,4 +6,3 @@ typealias ArcGISActualMarker = Graphic typealias ArcGISActualCircle = Graphic typealias ArcGISActualPolyline = Graphic typealias ArcGISActualPolygon = Graphic -typealias ArcGISActualGroundImage = Graphic diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundimage/ArcGISGroundImageRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundimage/ArcGISGroundImageRenderer.kt deleted file mode 100644 index 2d92546c..00000000 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/groundimage/ArcGISGroundImageRenderer.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.mapconductor.arcgis.groundimage - -import com.arcgismaps.mapping.view.Graphic -import com.mapconductor.arcgis.ArcGISActualGroundImage -import com.mapconductor.arcgis.ArcGISMapViewHolder -import com.mapconductor.core.groundimage.AbstractGroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageEntity -import com.mapconductor.core.groundimage.GroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageState -import kotlinx.coroutines.CoroutineScope - -class ArcGISGroundImageRenderer( - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractGroundImageRenderer() { - override suspend fun addGroundImages(newGroundImages: List): List = - emptyList() - - override suspend fun changeGroundImages( - changes: List>, - ): List = emptyList() - - override suspend fun removeGroundImages(removeEntities: List>) { - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt new file mode 100644 index 00000000..68769060 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt @@ -0,0 +1,13 @@ +package com.mapconductor.googlemaps + +import com.google.android.gms.maps.model.Circle +import com.google.android.gms.maps.model.GroundOverlay +import com.google.android.gms.maps.model.Marker +import com.google.android.gms.maps.model.Polygon +import com.google.android.gms.maps.model.Polyline + +typealias ActualGoogleMapMarker = Marker +typealias ActualGoogleMapCircle = Circle +typealias ActualGoogleMapPolyline = Polyline +typealias ActualGoogleMapPolygon = Polygon +typealias ActualGoogleMapGroundImage = GroundOverlay diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt index 24be6362..5426faa8 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -10,7 +10,6 @@ import com.google.android.gms.maps.GoogleMap.OnMapClickListener import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener import com.google.android.gms.maps.model.Circle -import com.google.android.gms.maps.model.GroundOverlay import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.Polygon @@ -23,11 +22,11 @@ import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.geocell.HexGeocell +import com.mapconductor.core.groundimage.GroundImageCapable +import com.mapconductor.core.groundimage.GroundImageController import com.mapconductor.core.groundimage.GroundImageEvent -import com.mapconductor.core.groundimage.GroundImageOverlayManager -import com.mapconductor.core.groundimage.GroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageRendererFactory import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.MarkerOverlayManager @@ -44,8 +43,6 @@ import com.mapconductor.core.polyline.PolylineState import com.mapconductor.core.projection.WebMercator import com.mapconductor.googlemaps.circle.DefaultGoogleMapCircleRenderer import com.mapconductor.googlemaps.circle.GoogleMapCircleRenderer -import com.mapconductor.googlemaps.groundimage.DefaultGoogleMapGroundImageRenderer -import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer import com.mapconductor.googlemaps.marker.DefaultGoogleMapMarkerRenderer import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer import com.mapconductor.googlemaps.polygon.DefaultGoogleMapPolygonRenderer @@ -56,7 +53,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IGoogleMapViewController : MapViewController { +interface IGoogleMapViewController : + MapViewController, + GroundImageCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -81,9 +80,13 @@ class GoogleMapViewController( private val polylineRendererFactory: PolylineRendererFactory = DefaultGoogleMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultGoogleMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultGoogleMapCircleRenderer(), - private val groundImageRendererFactory: GroundImageRendererFactory = - DefaultGoogleMapGroundImageRenderer(), -) : BaseMapViewController(), + private val groundImageController: GroundImageController, +) : BaseMapViewController< + ActualGoogleMapMarker, + ActualGoogleMapCircle, + ActualGoogleMapPolyline, + ActualGoogleMapPolygon, + >(), IGoogleMapViewController, OnCameraMoveStartedListener, OnCameraMoveCanceledListener, @@ -151,9 +154,6 @@ class GoogleMapViewController( coroutine = coroutine, ) - override fun onGroundImageOverlayManagerInitialized(overlayManager: GroundImageOverlayManager) { - } - override fun createCircleOverlayManager(): CircleOverlayManager = circleRendererFactory.create( onAdd = circleRenderer::addCircles, @@ -161,19 +161,6 @@ class GoogleMapViewController( onRemove = circleRenderer::removeCircles, ) - override val groundImageRenderer: GroundImageRenderer = - GoogleMapGroundImageRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createGroundImageOverlayManager(): GroundImageOverlayManager = - groundImageRendererFactory.create( - onAdd = groundImageRenderer::addGroundImages, - onChange = groundImageRenderer::changeGroundImages, - onRemove = groundImageRenderer::removeGroundImages, - ) - init { setupListeners() markerRenderer.init(markerOverlayManager) @@ -242,10 +229,6 @@ class GoogleMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) - override suspend fun addGroundImages(data: List) = groundImageOverlayManager.addGroundImages(data) - - override suspend fun updateGroundImage(state: GroundImageState) = groundImageOverlayManager.updateGroundImage(state) - override fun onCameraMove() { cameraMoveCallback?.let { val mapCameraPosition = holder.map.cameraPosition.toMapCameraPosition() @@ -299,16 +282,14 @@ class GoogleMapViewController( return } - groundImageOverlayManager.find(touchPosition)?.let { entity -> + groundImageController.find(touchPosition)?.let { entity -> val event = GroundImageEvent( state = entity.state, position = touchPosition, ) - groundImageClickCallback?.let { - coroutine.launch { - it.invoke(event) - } + coroutine.launch { + groundImageController.clickListener?.invoke(event) } return } @@ -351,4 +332,12 @@ class GoogleMapViewController( markerDragStartCallback?.invoke(state) } } + + override suspend fun compositionGroundImages(data: List) = groundImageController.add(data) + + override suspend fun updateGroundImage(state: GroundImageState) = groundImageController.update(state) + + fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) { + this.groundImageController.clickListener = listener + } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt index 6253853f..ced55963 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt @@ -3,8 +3,10 @@ package com.mapconductor.googlemaps import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.MapView +import com.mapconductor.core.groundimage.GroundImageController import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder +import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer import android.app.Activity import android.content.Context import android.content.ContextWrapper @@ -28,8 +30,19 @@ object GoogleMapViewControllerStore : StaticHolder() { options = options, ) + val groundImageRenderer = + GoogleMapGroundImageRenderer( + holder = holder, + ) + + val groundImageController = + GroundImageController( + renderer = groundImageRenderer, + ) + val controller = GoogleMapViewController( + groundImageController = groundImageController, holder = holder, ) this.set(id, controller) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt index 6f22b9b0..4d831a7b 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt @@ -4,41 +4,24 @@ import androidx.core.graphics.drawable.toBitmap import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.GroundOverlay import com.google.android.gms.maps.model.GroundOverlayOptions -import com.mapconductor.core.groundimage.AbstractGroundImageRenderer +import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.groundimage.GroundImageEntity -import com.mapconductor.core.groundimage.GroundImageOverlayManager -import com.mapconductor.core.groundimage.GroundImageOverlayManagerImpl -import com.mapconductor.core.groundimage.GroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageRendererFactory import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.googlemaps.ActualGoogleMapGroundImage import com.mapconductor.googlemaps.GoogleMapViewHolder import com.mapconductor.googlemaps.toLatLngBounds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultGoogleMapGroundImageRenderer : GroundImageRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): GroundImageOverlayManager = - GroundImageOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - class GoogleMapGroundImageRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractGroundImageRenderer() { - override suspend fun addGroundImages(newImages: List): List { + val holder: GoogleMapViewHolder, + val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : OverlayRenderer> { + override suspend fun onAdd(data: List): List { return withContext(coroutine.coroutineContext) { - return@withContext newImages.map { state -> + return@withContext data.map { state -> val bounds = state.bounds.toLatLngBounds() ?: return@withContext emptyList() val image = BitmapDescriptorFactory.fromBitmap(state.image.toBitmap()) val opacity = state.opacity @@ -54,29 +37,33 @@ class GoogleMapGroundImageRenderer( } } - override suspend fun removeGroundImages(removeEntities: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { - removeEntities.forEach { params -> params.groundImage.remove() } + data.forEach { params -> params.groundImage.remove() } } } - override suspend fun changeGroundImages( - changes: List>, + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + data: List>>, ): List { return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val groundOverlay = params.entity.groundImage - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint + return@withContext data.map { params -> + val groundOverlay = params.current.groundImage + val finger = params.current.fingerPrint + val prevFinger = params.prev.fingerPrint if (finger.bounds != prevFinger.bounds) { - params.entity.state.bounds.toLatLngBounds()?.let { + params.current.state.bounds.toLatLngBounds()?.let { groundOverlay.setPositionFromBounds(it) } } - groundOverlay.transparency = 1.0f - params.entity.state.opacity + groundOverlay.transparency = 1.0f - params.current.state.opacity if (finger.image != prevFinger.image) { val bitmap = - params.entity.state.image + params.current.state.image .toBitmap() val bitmapDesc = BitmapDescriptorFactory.fromBitmap(bitmap) groundOverlay.setImage(bitmapDesc) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt index 7151aecb..dc757271 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt @@ -1,6 +1,5 @@ package com.mapconductor.here -import com.here.sdk.mapview.MapImage import com.here.sdk.mapview.MapMarker import com.here.sdk.mapview.MapPolygon import com.here.sdk.mapview.MapPolyline @@ -9,4 +8,3 @@ typealias HereMapActualMarker = MapMarker typealias HereMapActualCircle = MapPolygon typealias HereMapActualPolyline = MapPolyline typealias HereMapActualPolygon = MapPolygon -typealias HereMapActualGroundImage = MapImage diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt index 3555651b..62e434ca 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt @@ -10,7 +10,6 @@ import com.here.sdk.mapview.MapCamera import com.here.sdk.mapview.MapCameraAnimationFactory import com.here.sdk.mapview.MapCameraListener import com.here.sdk.mapview.MapCameraUpdateFactory -import com.here.sdk.mapview.MapImage import com.here.sdk.mapview.MapMarker import com.here.sdk.mapview.MapMeasure import com.here.sdk.mapview.MapPolygon @@ -27,10 +26,6 @@ import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.groundimage.GroundImageOverlayManager -import com.mapconductor.core.groundimage.GroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageRendererFactory -import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.MapViewState.MoveCameraCallback @@ -48,8 +43,6 @@ import com.mapconductor.core.polyline.PolylineState import com.mapconductor.core.projection.WebMercator import com.mapconductor.here.circle.DefaultHereMapCircleRenderer import com.mapconductor.here.circle.HereMapCircleRenderer -import com.mapconductor.here.groundimage.DefaultHereMapGroundImageRenderer -import com.mapconductor.here.groundimage.HereMapGroundImageRenderer import com.mapconductor.here.marker.DefaultHereMapMarkerRenderer import com.mapconductor.here.marker.HereMapMarkerRenderer import com.mapconductor.here.polygon.DefaultHereMapPolygonRenderer @@ -61,7 +54,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IHereMapViewController : MapViewController { +interface IHereMapViewController : MapViewController { fun moveCamera( dstPosition: MapCameraPosition, listener: MoveCameraCallback? = null, @@ -87,14 +80,11 @@ class HereMapViewController( DefaultHereMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultHereMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), - private val groundImageRendererFactory: GroundImageRendererFactory = - DefaultHereMapGroundImageRenderer(), ) : BaseMapViewController< HereMapActualMarker, HereMapActualCircle, HereMapActualPolyline, HereMapActualPolygon, - HereMapActualGroundImage, >(), IHereMapViewController, MapCameraListener, @@ -139,12 +129,6 @@ class HereMapViewController( coroutine = coroutine, ) - override val groundImageRenderer: GroundImageRenderer = - HereMapGroundImageRenderer( - holder = holder, - coroutine = coroutine, - ) - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } @@ -157,11 +141,6 @@ class HereMapViewController( override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { } - override fun onGroundImageOverlayManagerInitialized( - overlayManager: GroundImageOverlayManager, - ) { - } - override fun createPolylineOverlayManager(): PolylineOverlayManager = polylineRendererFactory.create( onAdd = polylineRenderer::addPolylines, @@ -183,15 +162,6 @@ class HereMapViewController( onRemove = circleRenderer::removeCircles, ) - override fun createGroundImageOverlayManager(): GroundImageOverlayManager { - groundImageRendererFactory.create( - onAdd = groundImageRenderer::addGroundImages, - onChange = groundImageRenderer::changeGroundImages, - onRemove = groundImageRenderer::removeGroundImages, - ) - TODO("Not yet implemented") - } - override suspend fun clearOverlays() { markerOverlayManager.clearOverlays() polylineOverlayManager.clearOverlays() @@ -211,14 +181,6 @@ class HereMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) - override suspend fun addGroundImages(data: List) { - TODO("Not yet implemented") - } - - override suspend fun updateGroundImage(state: GroundImageState) { - TODO("Not yet implemented") - } - init { setupListeners() markerRenderer.init(markerOverlayManager) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt deleted file mode 100644 index b5da8cca..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/groundimage/HereGroundImageRenderer.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.mapconductor.here.groundimage - -import com.here.sdk.mapview.MapImage -import com.mapconductor.core.groundimage.AbstractGroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageEntity -import com.mapconductor.core.groundimage.GroundImageOverlayManager -import com.mapconductor.core.groundimage.GroundImageOverlayManagerImpl -import com.mapconductor.core.groundimage.GroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageRendererFactory -import com.mapconductor.core.groundimage.GroundImageState -import com.mapconductor.here.HereMapViewHolder -import kotlinx.coroutines.CoroutineScope - -class DefaultHereMapGroundImageRenderer : GroundImageRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): GroundImageOverlayManager = - GroundImageOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class HereMapGroundImageRenderer( - override val holder: HereMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractGroundImageRenderer() { - override suspend fun addGroundImages(newGroundImages: List): List { - return emptyList() -// return withContext(coroutine.coroutineContext){ -// return@withContext newGroundImages.map { state -> -// val bounds = state.bounds ?: return@withContext emptyList() -// val image = state.image -// val alpha = state.alpha -// val options = state.extra -// } -// } - } - - override suspend fun changeGroundImages( - changes: List>, - ): List = emptyList() - - override suspend fun removeGroundImages(removeEntities: List>) { - } -} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index d4fb175f..bae037c1 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -30,9 +30,6 @@ import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.groundimage.GroundImageOverlayManager -import com.mapconductor.core.groundimage.GroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.MarkerOverlayManager @@ -47,7 +44,6 @@ import com.mapconductor.core.projection.WebMercator import com.mapconductor.mapbox.circle.DefaultMapboxCircleRenderer import com.mapconductor.mapbox.circle.MapboxCircleLayer import com.mapconductor.mapbox.circle.MapboxCircleRenderer -import com.mapconductor.mapbox.groundImage.MapboxGroundImageRenderer import com.mapconductor.mapbox.marker.DefaultMapboxMarkerRenderer import com.mapconductor.mapbox.marker.MapboxMarkerRenderer import com.mapconductor.mapbox.marker.MarkerDragLayer @@ -69,7 +65,6 @@ interface IMapboxMapViewController : MapboxActualCircle, MapboxActualPolyline, MapboxActualPolygon, - MapboxActualGroundImage, > { fun moveCamera( dstPosition: MapCameraPosition, @@ -127,7 +122,6 @@ internal class MapboxMapViewController( MapboxActualCircle, MapboxActualPolyline, MapboxActualPolygon, - MapboxActualGroundImage, >(), IMapboxMapViewController, CameraChangedCallback, @@ -176,10 +170,6 @@ internal class MapboxMapViewController( onPostProcess = circleRenderer::redraw, ) - override fun createGroundImageOverlayManager(): GroundImageOverlayManager { - TODO("Not yet implemented") - } - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { holder.map.getStyle { style -> style.addSource(circleLayer.source) @@ -213,17 +203,6 @@ internal class MapboxMapViewController( layer = circleLayer, ) - override fun onGroundImageOverlayManagerInitialized( - overlayManager: GroundImageOverlayManager, - ) { - } - - override val groundImageRenderer: GroundImageRenderer = - MapboxGroundImageRenderer( - holder = holder, - coroutine = coroutine, - ) - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } @@ -262,14 +241,6 @@ internal class MapboxMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) - override suspend fun addGroundImages(data: List) { - TODO("Not yet implemented") - } - - override suspend fun updateGroundImage(state: GroundImageState) { - TODO("Not yet implemented") - } - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt index b4065d74..a8199ba7 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt @@ -6,4 +6,3 @@ typealias MapboxActualMarker = Feature typealias MapboxActualCircle = Feature typealias MapboxActualPolyline = List typealias MapboxActualPolygon = Feature -typealias MapboxActualGroundImage = Feature diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt deleted file mode 100644 index 1a5b43d9..00000000 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/groundImage/MapboxGroundImageRenderer.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.mapconductor.mapbox.groundImage - -import com.mapbox.geojson.Feature -import com.mapconductor.core.groundimage.AbstractGroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageEntity -import com.mapconductor.core.groundimage.GroundImageRenderer -import com.mapconductor.core.groundimage.GroundImageState -import com.mapconductor.mapbox.MapboxMapViewHolder -import kotlinx.coroutines.CoroutineScope - -class MapboxGroundImageRenderer( - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractGroundImageRenderer() { - override suspend fun addGroundImages(newGroundImages: List): List = emptyList() - - override suspend fun changeGroundImages(changes: List>): List = - emptyList() - - override suspend fun removeGroundImages(removeEntities: List>) { - } -} From f53b03f732be59c4c9b259f69f248ded24269966 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 20 Aug 2025 19:24:19 +0900 Subject: [PATCH 19/44] Rename: position property to clicked of GroundImageEvent class --- .../main/java/com/mapconductor/core/groundimage/GroundImage.kt | 2 +- .../java/com/mapconductor/googlemaps/GoogleMapViewController.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt index 0f7068f3..a2b72dd5 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImage.kt @@ -68,7 +68,7 @@ data class GroundImageFingerPrint( data class GroundImageEvent( val state: GroundImageState, - val position: GeoPoint?, + val clicked: GeoPoint?, ) typealias OnGroundImageEventHandler = (GroundImageEvent) -> Unit diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt index 5426faa8..3c0b2f8a 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -286,7 +286,7 @@ class GoogleMapViewController( val event = GroundImageEvent( state = entity.state, - position = touchPosition, + clicked = touchPosition, ) coroutine.launch { groundImageController.clickListener?.invoke(event) From dfed49148b572bc73ec66247eb18751c4d27459f Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 20 Aug 2025 19:37:09 +0900 Subject: [PATCH 20/44] Remove generic type from GroundImageCapable --- .../com/mapconductor/core/groundimage/GroundImageController.kt | 2 +- .../java/com/mapconductor/googlemaps/GoogleMapViewController.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt index 0a9d34b1..1bdf9d15 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt @@ -6,7 +6,7 @@ import com.mapconductor.core.features.IGeoPoint import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -interface GroundImageCapable { +interface GroundImageCapable { suspend fun compositionGroundImages(data: List) suspend fun updateGroundImage(state: GroundImageState) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt index 3c0b2f8a..6aa0d496 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -55,7 +55,7 @@ import kotlinx.coroutines.launch interface IGoogleMapViewController : MapViewController, - GroundImageCapable { + GroundImageCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, From 997a08cd757b16876aa293e587f856eb73dd891b Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 20 Aug 2025 19:37:59 +0900 Subject: [PATCH 21/44] Remove unused interface --- .../core/controller/MapViewController.kt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index 6fccbc27..dc277600 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -100,20 +100,6 @@ interface OverlayController { fun find(position: IGeoPoint): EntityType? } -interface OverlayManager { - suspend fun add(states: List) - - suspend fun update(state: StateType) - - suspend fun clear() - - fun getById(id: String): StateType? - - fun allEntities(): List - - fun find(position: IGeoPoint): EntityType? -} - abstract class BaseMapViewController : MapViewController { abstract val markerRenderer: MarkerRenderer From 2b0bd0e011f1f34d6a7f3816465d719597dfe322 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 20 Aug 2025 20:27:08 +0900 Subject: [PATCH 22/44] Small refactoring --- .../core/controller/BaseMapViewController.kt | 126 +++++++++++++ .../core/controller/MapViewController.kt | 144 --------------- .../core/controller/OverlayController.kt | 21 +++ .../core/controller/OverlayRenderer.kt | 16 ++ .../core/groundimage/GroundImageOverlay.kt | 2 +- .../core/marker/MarkerController.kt | 167 ++++++++++++++++++ .../mapconductor/core/marker/MarkerOverlay.kt | 16 ++ 7 files changed, 347 insertions(+), 145 deletions(-) create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerController.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt new file mode 100644 index 00000000..23a59543 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt @@ -0,0 +1,126 @@ +package com.mapconductor.core.controller + +import com.mapconductor.core.circle.CircleOverlayManager +import com.mapconductor.core.circle.CircleRenderer +import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.map.OnCameraMoveHandler +import com.mapconductor.core.map.OnMapEventHandler +import com.mapconductor.core.marker.MarkerOverlayManager +import com.mapconductor.core.marker.MarkerRenderer +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.PolygonOverlayManager +import com.mapconductor.core.polygon.PolygonRenderer +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineOverlayManager +import com.mapconductor.core.polyline.PolylineRenderer + +abstract class BaseMapViewController : + MapViewController { + abstract val markerRenderer: MarkerRenderer + + override val markerOverlayManager: MarkerOverlayManager by lazy { + createMarkerOverlayManager().also { overlayManager -> + markerRenderer.init(overlayManager) + onMarkerOverlayManagerInitialized(overlayManager) + } + } + + abstract val polylineRenderer: PolylineRenderer + + override val polylineOverlayManager: PolylineOverlayManager by lazy { + createPolylineOverlayManager().also { overlayManager -> + polylineRenderer.init(overlayManager) + onPolylineOverlayManagerInitialized(overlayManager) + } + } + + abstract val polygonRenderer: PolygonRenderer + + override val polygonOverlayManager: PolygonOverlayManager by lazy { + createPolygonOverlayManager().also { overlayManager -> + polygonRenderer.init(overlayManager) + onPolygonOverlayManagerInitialized(overlayManager) + } + } + + abstract val circleRenderer: CircleRenderer + + override val circleOverlayManager: CircleOverlayManager by lazy { + createCircleOverlayManager().also { overlayManager -> + circleRenderer.init(overlayManager) + onCircleOverlayManagerInitialized(overlayManager) + } + } + + protected abstract fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) + + protected abstract fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) + + protected abstract fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) + + protected abstract fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) + + protected abstract fun createMarkerOverlayManager(): MarkerOverlayManager + + protected abstract fun createPolylineOverlayManager(): PolylineOverlayManager + + protected abstract fun createPolygonOverlayManager(): PolygonOverlayManager + + protected abstract fun createCircleOverlayManager(): CircleOverlayManager + + protected var cameraMoveCallback: OnCameraMoveHandler? = null + protected var mapClickCallback: OnMapEventHandler? = null + protected var mapLongClickCallback: OnMapEventHandler? = null + protected var markerClickCallback: OnMarkerEventHandler? = null + protected var markerDragStartCallback: OnMarkerEventHandler? = null + protected var markerDragCallback: OnMarkerEventHandler? = null + protected var markerDragEndCallback: OnMarkerEventHandler? = null + protected var circleClickCallback: OnCircleEventHandler? = null + protected var polylineClickCallback: OnPolylineEventHandler? = null + + abstract fun setupListeners() + + override fun setCameraMoveListener(listener: OnCameraMoveHandler?) { + this.cameraMoveCallback = listener + } + + override fun setMapClickListener(listener: OnMapEventHandler?) { + this.mapClickCallback = listener + } + + override fun setMapLongClickListener(listener: OnMapEventHandler?) { + this.mapClickCallback = listener + } + + override fun setMarkerClickListener(listener: OnMarkerEventHandler?) { + this.markerClickCallback = listener + } + + override fun setMarkerDragStartListener(listener: OnMarkerEventHandler?) { + this.markerDragStartCallback = listener + } + + override fun setMarkerDragListener(listener: OnMarkerEventHandler?) { + this.markerDragCallback = listener + } + + override fun setMarkerDragEndListener(listener: OnMarkerEventHandler?) { + this.markerDragEndCallback = listener + } + + override fun setCircleClickListener(listener: OnCircleEventHandler?) { + this.circleClickCallback = listener + } + + override fun setPolylineClickListener(listener: OnPolylineEventHandler?) { + this.polylineClickCallback = listener + } + + override fun setOnMarkerAnimationStart(listener: OnMarkerEventHandler?) = + markerRenderer + .setOnMarkerAnimationStart(listener) + + override fun setOnMarkerAnimationEnd(listener: OnMarkerEventHandler?) = + markerRenderer + .setOnMarkerAnimationEnd(listener) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index dc277600..79c1d58a 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -4,7 +4,6 @@ import com.mapconductor.core.circle.CircleOverlayManager import com.mapconductor.core.circle.CircleRenderer import com.mapconductor.core.circle.CircleState import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.OnCameraMoveHandler @@ -67,146 +66,3 @@ interface MapViewController - -interface OverlayRenderer { - interface Changes { - val current: EntityType - val prev: EntityType - } - - suspend fun onAdd(data: List): List - - suspend fun onChange(data: List>): List - - suspend fun onRemove(data: List) - - suspend fun onPostProcess() -} - -interface OverlayController { - val zIndex: Int - - // val overlayManager: OverlayManager - val renderer: OverlayRenderer - - suspend fun add(data: List) - - suspend fun update(state: StateType) - - suspend fun clear() - - var clickListener: ((EventType) -> Unit)? - - fun find(position: IGeoPoint): EntityType? -} - -abstract class BaseMapViewController : - MapViewController { - abstract val markerRenderer: MarkerRenderer - - override val markerOverlayManager: MarkerOverlayManager by lazy { - createMarkerOverlayManager().also { overlayManager -> - markerRenderer.init(overlayManager) - onMarkerOverlayManagerInitialized(overlayManager) - } - } - - abstract val polylineRenderer: PolylineRenderer - - override val polylineOverlayManager: PolylineOverlayManager by lazy { - createPolylineOverlayManager().also { overlayManager -> - polylineRenderer.init(overlayManager) - onPolylineOverlayManagerInitialized(overlayManager) - } - } - - abstract val polygonRenderer: PolygonRenderer - - override val polygonOverlayManager: PolygonOverlayManager by lazy { - createPolygonOverlayManager().also { overlayManager -> - polygonRenderer.init(overlayManager) - onPolygonOverlayManagerInitialized(overlayManager) - } - } - - abstract val circleRenderer: CircleRenderer - - override val circleOverlayManager: CircleOverlayManager by lazy { - createCircleOverlayManager().also { overlayManager -> - circleRenderer.init(overlayManager) - onCircleOverlayManagerInitialized(overlayManager) - } - } - - protected abstract fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) - - protected abstract fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) - - protected abstract fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) - - protected abstract fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) - - protected abstract fun createMarkerOverlayManager(): MarkerOverlayManager - - protected abstract fun createPolylineOverlayManager(): PolylineOverlayManager - - protected abstract fun createPolygonOverlayManager(): PolygonOverlayManager - - protected abstract fun createCircleOverlayManager(): CircleOverlayManager - - protected var cameraMoveCallback: OnCameraMoveHandler? = null - protected var mapClickCallback: OnMapEventHandler? = null - protected var mapLongClickCallback: OnMapEventHandler? = null - protected var markerClickCallback: OnMarkerEventHandler? = null - protected var markerDragStartCallback: OnMarkerEventHandler? = null - protected var markerDragCallback: OnMarkerEventHandler? = null - protected var markerDragEndCallback: OnMarkerEventHandler? = null - protected var circleClickCallback: OnCircleEventHandler? = null - protected var polylineClickCallback: OnPolylineEventHandler? = null - - abstract fun setupListeners() - - override fun setCameraMoveListener(listener: OnCameraMoveHandler?) { - this.cameraMoveCallback = listener - } - - override fun setMapClickListener(listener: OnMapEventHandler?) { - this.mapClickCallback = listener - } - - override fun setMapLongClickListener(listener: OnMapEventHandler?) { - this.mapClickCallback = listener - } - - override fun setMarkerClickListener(listener: OnMarkerEventHandler?) { - this.markerClickCallback = listener - } - - override fun setMarkerDragStartListener(listener: OnMarkerEventHandler?) { - this.markerDragStartCallback = listener - } - - override fun setMarkerDragListener(listener: OnMarkerEventHandler?) { - this.markerDragCallback = listener - } - - override fun setMarkerDragEndListener(listener: OnMarkerEventHandler?) { - this.markerDragEndCallback = listener - } - - override fun setCircleClickListener(listener: OnCircleEventHandler?) { - this.circleClickCallback = listener - } - - override fun setPolylineClickListener(listener: OnPolylineEventHandler?) { - this.polylineClickCallback = listener - } - - override fun setOnMarkerAnimationStart(listener: OnMarkerEventHandler?) = - markerRenderer - .setOnMarkerAnimationStart(listener) - - override fun setOnMarkerAnimationEnd(listener: OnMarkerEventHandler?) = - markerRenderer - .setOnMarkerAnimationEnd(listener) -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt new file mode 100644 index 00000000..a0867a65 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt @@ -0,0 +1,21 @@ +package com.mapconductor.core.controller + +import com.mapconductor.core.features.IGeoPoint + + +interface OverlayController { + val zIndex: Int + + // val overlayManager: OverlayManager + val renderer: OverlayRenderer + + suspend fun add(data: List) + + suspend fun update(state: StateType) + + suspend fun clear() + + var clickListener: ((EventType) -> Unit)? + + fun find(position: IGeoPoint): EntityType? +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt new file mode 100644 index 00000000..92ae79da --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt @@ -0,0 +1,16 @@ +package com.mapconductor.core.controller + +interface OverlayRenderer { + interface Changes { + val current: EntityType + val prev: EntityType + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List) + + suspend fun onPostProcess() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt index 5ed59d24..31b66d84 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt @@ -11,6 +11,6 @@ class GroundImageOverlay( data: List, controller: MapViewControllerAlias, ) { - (controller as? GroundImageCapable<*>)?.compositionGroundImages(data) + (controller as? GroundImageCapable)?.compositionGroundImages(data) } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerController.kt new file mode 100644 index 00000000..35c5bc9e --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerController.kt @@ -0,0 +1,167 @@ +package com.mapconductor.core.marker + +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.controller.OverlayRenderer +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.MarkerRenderer.UpdateParams +import kotlin.collections.forEach +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +interface MarkerCapable { + suspend fun compositionMarkers(data: List) + + suspend fun updateMarker(state: MarkerState) +} + + +class MarkerController( + val markerManager: MarkerManager, + override val renderer: OverlayRenderer>, + override var clickListener: OnMarkerEventHandler? = null, +) : OverlayController< + ActualMarker, + MarkerState, + MarkerEntity, + MarkerState, +> { + override val zIndex: Int = 10 + val entities = mutableMapOf>() + val semaphore = Semaphore(1) + + override suspend fun add(data: List) { + semaphore.withPermit { + val defaultIcon = DefaultIcon() + val defaultIconBitmapIcon = defaultIcon.toBitmapIcon() + val modifiedEntities = mutableListOf>() + val previous = markerManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() + val removed = mutableListOf>() + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = markerManager.getEntity(state.id)!! + val markerIcon = state.icon ?: defaultIcon + updated.add( + object : UpdateParams { + override val entity: MarkerEntity = + MarkerEntityImpl( + state = state, + marker = prevEntity.marker, + ) + override val bitmapIcon: BitmapIcon + get() { + return markerIcon.toBitmapIcon() + } + override val prevEntity: MarkerEntity = prevEntity + }, + ) + previous.remove(state.id) + return@forEach + } + added.add(state) + previous.remove(state.id) + } + previous.forEach { remainId -> + markerManager.removeEntity(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + // Remove markers + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + // Add new markers + if (added.isNotEmpty()) { + val addedList = added.toList() + + addedList + .map { state -> + val markerIcon = state.icon?.toBitmapIcon() ?: defaultIconBitmapIcon + Pair(state, markerIcon) + }.also { + val actualMarkers: List = renderer.onAdd(it) + actualMarkers.forEachIndexed { index, actualMarker -> + actualMarker?.let { + val entity = + MarkerEntityImpl( + marker = actualMarker, + state = addedList[index], + ) + markerManager.registerEntity(entity) + modifiedEntities.add(entity) + } + } + } + } + + // Update changed markers + if (updated.isNotEmpty()) { + val actualMarkers: List = renderer.onChange(updated) + + actualMarkers.forEachIndexed { index, actualMarker -> + actualMarker?.let { + val params = updated[index] + val entity = + MarkerEntityImpl( + state = params.entity.state, + marker = actualMarker, + ) + markerManager.registerEntity(entity) + } + } + } + modifiedEntities.forEach { entity -> + entity.state.animation?.let { + renderer.onAnimate(entity) + } + } + renderer.onPostProcess() + } + } + + override suspend fun update(state: MarkerState) { + semaphore.withPermit { + val updated = mutableListOf>>() + val prevEntity = entities[state.id]!! + updated.add( + object : OverlayRenderer.Changes> { + override val current: MarkerEntity = + MarkerEntityImpl( + marker = prevEntity.marker, + state = state, + ) + override val prev: MarkerEntity = prevEntity + }, + ) + + val actualOverlays: List = renderer.onChange(updated) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val entity = + MarkerEntityImpl( + marker = it, + state = state, + ) + entities[state.id] = entity + } + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + renderer.onRemove(entities.values.toList()) + entities.clear() + } + } + + override fun find(position: IGeoPoint): MarkerEntity? { + // TODO: Improve this implementation later + return entities.values.find { entity -> + entity.state.bounds.contains(position) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt new file mode 100644 index 00000000..27248993 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt @@ -0,0 +1,16 @@ +package com.mapconductor.core.marker + +import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.StateFlow + +class MarkerOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewControllerAlias, + ) { + controller.addMarkers(data) + } +} From 22bb16d6a4ae0f809b108dfc0fc8a7233242c0d7 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 22 Aug 2025 02:38:27 +0900 Subject: [PATCH 23/44] Refactor marker architecture to abstract pattern - Extract abstract marker controller and render interfaces - Replace concrete MarkerController with AbstractMarkerController - Add provider-specific marker controller implementations for all map SDKs - Rename MapViewState files to *Impl.kt for consistency - Remove deprecated marker implementation files --- .../com/mapconductor/example/DemoAppScreen.kt | 19 +- .../com/mapconductor/example/MainActivity.kt | 4 +- .../mapconductor/example/MapViewContainer.kt | 19 +- .../example/navigation/NavigationViewModel.kt | 6 +- .../pages/circle/CircleMapComponent.kt | 6 +- .../example/pages/circle/CircleMapPage.kt | 5 +- .../pages/circle/CirclePageViewModel.kt | 4 +- .../pages/groundimage/GroundImageMapPage.kt | 3 +- .../example/pages/map/flyto/FlyToMapPage.kt | 3 +- .../example/pages/polyline/PolylineMapPage.kt | 3 +- .../example/pages/stores/StoreMapPage.kt | 3 +- .../example/ui/DemoMapPageScaffold.kt | 110 +++++--- .../com/mapconductor/example/ui/HeaderCard.kt | 18 +- .../com/mapconductor/core/OverlayProvider.kt | 12 +- .../core/controller/BaseMapViewController.kt | 48 +--- .../core/controller/MapViewController.kt | 31 +-- .../core/controller/OverlayController.kt | 4 - .../core/controller/OverlayRenderer.kt | 4 +- .../core/groundimage/GroundImageController.kt | 12 +- .../com/mapconductor/core/map/MapViewBase.kt | 7 +- .../mapconductor/core/map/MapViewHolder.kt | 6 +- .../com/mapconductor/core/map/MapViewState.kt | 2 +- .../core/marker/AbstractMarkerController.kt | 216 ++++++++++++++++ .../marker/AbstractMarkerOverlayRenderer.kt | 135 ++++++++++ .../com/mapconductor/core/marker/Marker.kt | 13 +- .../core/marker/MarkerController.kt | 167 ------------ .../mapconductor/core/marker/MarkerOverlay.kt | 2 +- .../core/marker/MarkerOverlayManagerImpl.kt | 176 ------------- .../core/marker/MarkerRenderer.kt | 242 ------------------ .../com/mapconductor/arcgis/ArcGISMapView.kt | 34 ++- .../arcgis/ArcGISMapViewController.kt | 157 +++++------- .../arcgis/ArcGISViewControllerStore.kt | 35 +++ .../mapconductor/arcgis/ArcGisMapViewState.kt | 22 +- .../arcgis/marker/ArcGISMarkerController.kt | 63 +++++ .../arcgis/marker/ArcGISMarkerRenderer.kt | 89 +++---- .../googlemaps/GoogleMapTypeAlias.kt | 10 +- .../mapconductor/googlemaps/GoogleMapView.kt | 46 ++-- .../googlemaps/GoogleMapViewController.kt | 126 +++------ .../GoogleMapViewControllerStore.kt | 50 +++- ...ViewState.kt => GoogleMapViewStateImpl.kt} | 39 +-- .../GoogleMapGroundImageRenderer.kt | 10 +- .../marker/GoogleMapMarkerController.kt | 88 +++++++ .../marker/GoogleMapMarkerRenderer.kt | 85 +++--- .../java/com/mapconductor/here/HereMapView.kt | 40 ++- .../here/HereMapViewController.kt | 138 ++++------ .../here/HereMapViewControllerStore.kt | 28 ++ ...reMapViewState.kt => HereViewStateImpl.kt} | 22 +- .../here/marker/HereMapMarkerRenderer.kt | 88 +++---- .../here/marker/HereMarkerController.kt | 56 ++++ .../com/mapconductor/mapbox/MapboxMapView.kt | 65 +++-- .../mapbox/MapboxMapViewController.kt | 168 +++++------- ...boxViewState.kt => MapboxViewStateImpl.kt} | 22 +- .../mapbox/marker/MapboxMarkerController.kt | 60 +++++ .../mapbox/marker/MapboxMarkerRenderer.kt | 114 +++++---- .../mapbox/marker/MarkerDragLayer.kt | 7 +- 55 files changed, 1438 insertions(+), 1504 deletions(-) create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerOverlayRenderer.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerController.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlayManagerImpl.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerRenderer.kt create mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt rename mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/{GoogleMapViewState.kt => GoogleMapViewStateImpl.kt} (82%) create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerController.kt rename mapconductor-for-here/src/main/java/com/mapconductor/here/{HereMapViewState.kt => HereViewStateImpl.kt} (89%) create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt rename mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/{MapboxViewState.kt => MapboxViewStateImpl.kt} (88%) create mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index fb1fc614..940bf897 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.navigation.NavigationViewModel import com.mapconductor.example.pages.circle.CircleMapPage @@ -29,8 +31,21 @@ import com.mapconductor.example.ui.sidebar.SidebarItem import com.mapconductor.example.ui.theme.AppTheme @Composable -fun DemoAppScreen() { - val navigationViewModel: NavigationViewModel = viewModel() +fun DemoAppScreen(initPage: String = "map") { + val navigationViewModel: NavigationViewModel = + viewModel( + factory = + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(NavigationViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return NavigationViewModel(initPage) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } + }, + ) + val currentPage by navigationViewModel.currentPage val isSidebarExpanded by navigationViewModel.isSidebarExpanded val context = LocalContext.current diff --git a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt index b59feaab..71aca370 100644 --- a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt +++ b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt @@ -11,7 +11,9 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { - DemoAppScreen() + DemoAppScreen( + initPage = "circle", + ) } } } diff --git a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt index 36b09250..5cd25ba4 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -3,7 +3,7 @@ package com.mapconductor.example import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.mapconductor.arcgis.ArcGISMapView -import com.mapconductor.arcgis.ArcGISMapViewState +import com.mapconductor.arcgis.ArcGISMapViewStateImpl import com.mapconductor.core.MapViewScope import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.groundimage.OnGroundImageEventHandler @@ -11,12 +11,12 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler -import com.mapconductor.googlemaps.GoogleMapViewState +import com.mapconductor.googlemaps.GoogleMapViewStateImpl import com.mapconductor.googlemaps.GoogleMapsView import com.mapconductor.here.HereMapView -import com.mapconductor.here.HereMapViewState +import com.mapconductor.here.HereViewStateImpl import com.mapconductor.mapbox.MapboxMapView -import com.mapconductor.mapbox.MapboxMapViewState +import com.mapconductor.mapbox.MapboxViewStateImpl @Composable fun MapViewContainer( @@ -35,7 +35,7 @@ fun MapViewContainer( content: @Composable MapViewScope.() -> Unit, ) { when (state) { - is GoogleMapViewState -> + is GoogleMapViewStateImpl -> GoogleMapsView( modifier = modifier, state = state, @@ -52,7 +52,7 @@ fun MapViewContainer( content = content, ) - is HereMapViewState -> + is HereViewStateImpl -> HereMapView( modifier = modifier, state = state, @@ -63,13 +63,12 @@ fun MapViewContainer( onMarkerDragEnd = onMarkerDragEnd, onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, - onGroundImageClick = onGroundImageClick, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, content = content, ) - is MapboxMapViewState -> + is MapboxViewStateImpl -> MapboxMapView( modifier = modifier, state = state, @@ -82,11 +81,10 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, - onGroundImageClick = onGroundImageClick, content = content, ) - is ArcGISMapViewState -> + is ArcGISMapViewStateImpl -> ArcGISMapView( modifier = modifier, state = state, @@ -98,7 +96,6 @@ fun MapViewContainer( onMarkerAnimateStart = onMarkerAnimateStart, onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, - onGroundImageClick = onGroundImageClick, onPolylineClick = onPolylineClick, content = content, ) diff --git a/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt b/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt index 0ed71062..36047dcb 100644 --- a/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/navigation/NavigationViewModel.kt @@ -4,8 +4,10 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -class NavigationViewModel : ViewModel() { - private val _currentPage = mutableStateOf("groundImage") +class NavigationViewModel( + initPage: String, +) : ViewModel() { + private val _currentPage = mutableStateOf(initPage) val currentPage: State = _currentPage private val _isSidebarExpanded = mutableStateOf(false) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt index 7d983ae6..ff0bc995 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt @@ -19,7 +19,7 @@ fun CircleMapComponent( onMapClick: OnMapEventHandler = {}, onMarkerClick: OnMarkerEventHandler = {}, onCircleClick: OnCircleEventHandler = {}, - onMarkerDrag: OnMarkerEventHandler = {}, + onMarkerMove: OnMarkerEventHandler = {}, ) { mapViewState?.let { it -> MapViewContainer( @@ -28,7 +28,9 @@ fun CircleMapComponent( onMapClick = onMapClick, onMarkerClick = onMarkerClick, onCircleClick = onCircleClick, - onMarkerDrag = onMarkerDrag, + onMarkerDragStart = onMarkerMove, + onMarkerDrag = onMarkerMove, + onMarkerDragEnd = onMarkerMove, ) { // Circle Circle(viewModel.circleState) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt index f541b893..2d6ffd67 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.mapconductor.example.R import com.mapconductor.example.toast.ToastHost +import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MessageCard @@ -23,7 +24,7 @@ fun CircleMapPage( onToggleSidebar: () -> Unit = {}, ) { DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> @@ -35,7 +36,7 @@ fun CircleMapPage( onMapClick = viewModel::onMapClick, onMarkerClick = viewModel::onMarkerClick, onCircleClick = viewModel::onCircleClick, - onMarkerDrag = viewModel::onMarkerDrag, + onMarkerMove = viewModel::onMarkerMove, ) // Message Card diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt index 8ad89b81..28d4e670 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt @@ -40,7 +40,7 @@ interface CirclePageViewModel { fun onCircleClick(event: CircleClickEvent) - fun onMarkerDrag(dragged: MarkerState) + fun onMarkerMove(dragged: MarkerState) fun showToast(text: String) @@ -152,7 +152,7 @@ class CirclePageViewModelImpl : showToast("Circle clicked - Radius: ${radiusMeters.toInt()}m") } - override fun onMarkerDrag(dragged: MarkerState) { + override fun onMarkerMove(dragged: MarkerState) { _edgeMarker.value.position = dragged.position // Update circle radius diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index a6fce2c2..219132c4 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -19,6 +19,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.toast.ToastHost import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.GroundImageCapableMapViewItems import com.mapconductor.example.ui.MessageCard @Composable @@ -41,7 +42,7 @@ fun GroundImageMapPage( ) DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = GroundImageCapableMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt index 80c0303a..914f404f 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel +import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MessageCard @@ -48,7 +49,7 @@ fun FlyToMapPage( ) DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt index de95aaa9..b3f7aa75 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt @@ -2,6 +2,7 @@ package com.mapconductor.example.pages.polyline import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold @Composable @@ -10,7 +11,7 @@ fun PolylineMapPage( onToggleSidebar: () -> Unit = {}, ) { DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddingValues -> diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt index 28c5deaa..e60a9b14 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt @@ -3,6 +3,7 @@ package com.mapconductor.example.pages.stores import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.platform.LocalContext +import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold @Composable @@ -13,7 +14,7 @@ fun StoreMapPage( val context = LocalContext.current DemoMapPageScaffold( - initCameraPosition = viewModel.initCameraPosition, + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, ) { paddings -> diff --git a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt index 0e48e220..678813d5 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt @@ -27,41 +27,72 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.mapconductor.arcgis.ArcGISDesign +import com.mapconductor.arcgis.ArcGISMapViewStateImpl import com.mapconductor.arcgis.rememberArcGISMapViewState import com.mapconductor.core.map.IMapCameraPosition import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.MapViewStateImpl import com.mapconductor.example.R import com.mapconductor.googlemaps.GoogleMapDesign +import com.mapconductor.googlemaps.GoogleMapViewStateImpl import com.mapconductor.googlemaps.rememberGoogleMapViewState import com.mapconductor.here.HereMapDesign +import com.mapconductor.here.HereViewStateImpl import com.mapconductor.here.rememberHereMapViewState import com.mapconductor.mapbox.MapboxMapDesign +import com.mapconductor.mapbox.MapboxViewStateImpl import com.mapconductor.mapbox.rememberMapboxMapViewState @Composable -fun DemoMapPageScaffold( - initCameraPosition: IMapCameraPosition, - initSelect: Int = 0, - onToggleSidebar: () -> Unit, - onMapViewStateChanged: (MapViewState<*>) -> Unit = {}, - content: @Composable BoxScope.(PaddingValues) -> Unit = {}, -) { - // ---------- Map States --------------- +fun GetGoogleMapViewItem(initCameraPosition: IMapCameraPosition): IconItem { val googleMapState = rememberGoogleMapViewState( mapDesign = GoogleMapDesign.Normal, cameraPosition = initCameraPosition, ) + return IconItem( + key = "googlemap", + label = "Google Map", + lightIconResId = R.drawable.google_maps_logo, + darkIconResId = R.drawable.google_maps_logo, + value = googleMapState, + ) +} + +@Composable +fun GetMapboxViewItem(initCameraPosition: IMapCameraPosition): IconItem { val mapboxMapState = rememberMapboxMapViewState( mapDesign = MapboxMapDesign.Standard, cameraPosition = initCameraPosition, ) + return IconItem( + key = "mapbox", + label = "Mapbox", + lightIconResId = R.drawable.mapbox_logo_black, + darkIconResId = R.drawable.mapbox_logo_white, + value = mapboxMapState, + ) +} + +@Composable +fun GetHereViewItem(initCameraPosition: IMapCameraPosition): IconItem { val hereMapState = rememberHereMapViewState( mapDesign = HereMapDesign.NormalDay, cameraPosition = initCameraPosition, ) + return IconItem( + key = "heremap", + label = "Here", + lightIconResId = R.drawable.here_logo_black, + darkIconResId = R.drawable.here_logo_white, + value = hereMapState, + ) +} + +@Composable +fun GetArcGISViewItem(initCameraPosition: IMapCameraPosition): IconItem { val elevationSources = listOf( "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer", @@ -71,39 +102,40 @@ fun DemoMapPageScaffold( mapDesign = ArcGISDesign.Streets.withElevationSources(elevationSources), cameraPosition = initCameraPosition, ) + return IconItem( + key = "arcgis", + label = "ArcGIS", + lightIconResId = R.drawable.arcgis_logo_black, + darkIconResId = R.drawable.arcgis_logo_white, + value = arcGISMapState, + ) +} - val menuItems = - listOf( - IconItem( - key = "googlemap", - label = "Google Map", - lightIconResId = R.drawable.google_maps_logo, - darkIconResId = R.drawable.google_maps_logo, - value = googleMapState, - ), - IconItem( - key = "mapbox", - label = "Mapbox", - lightIconResId = R.drawable.mapbox_logo_black, - darkIconResId = R.drawable.mapbox_logo_white, - value = mapboxMapState, - ), - IconItem( - key = "heremap", - label = "Here", - lightIconResId = R.drawable.here_logo_black, - darkIconResId = R.drawable.here_logo_white, - value = hereMapState, - ), - IconItem( - key = "arcgis", - label = "ArcGIS", - lightIconResId = R.drawable.arcgis_logo_black, - darkIconResId = R.drawable.arcgis_logo_white, - value = arcGISMapState, - ), - ) +@Composable +fun DefaultMapViewItems(initCameraPosition: IMapCameraPosition): List>> = + listOf( + GetGoogleMapViewItem(initCameraPosition), + GetMapboxViewItem(initCameraPosition), + GetHereViewItem(initCameraPosition), + GetArcGISViewItem(initCameraPosition), + ) + +@Composable +fun GroundImageCapableMapViewItems( + initCameraPosition: IMapCameraPosition, +): List>> = + listOf( + GetGoogleMapViewItem(initCameraPosition), + ) +@Composable +fun DemoMapPageScaffold( + menuItems: List>>, + initSelect: Int = 0, + onToggleSidebar: () -> Unit, + onMapViewStateChanged: (MapViewState<*>) -> Unit = {}, + content: @Composable (BoxScope.(PaddingValues) -> Unit) = {}, +) { var selectedIndex by rememberSaveable { mutableIntStateOf(initSelect) } LaunchedEffect(selectedIndex) { onMapViewStateChanged(menuItems.elementAt(selectedIndex).value) diff --git a/example-app/src/main/java/com/mapconductor/example/ui/HeaderCard.kt b/example-app/src/main/java/com/mapconductor/example/ui/HeaderCard.kt index 2320dc1d..188b6d70 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/HeaderCard.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/HeaderCard.kt @@ -1,21 +1,19 @@ package com.mapconductor.example.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import com.mapconductor.arcgis.ArcGISMapViewState +import com.mapconductor.arcgis.ArcGISMapViewStateImpl import com.mapconductor.core.map.MapViewState -import com.mapconductor.googlemaps.GoogleMapViewState -import com.mapconductor.here.HereMapViewState -import com.mapconductor.mapbox.MapboxMapViewState +import com.mapconductor.googlemaps.GoogleMapViewStateImpl +import com.mapconductor.here.HereViewStateImpl +import com.mapconductor.mapbox.MapboxViewStateImpl @Composable fun HeaderCard( - googleMapState: GoogleMapViewState, - mapboxMapState: MapboxMapViewState, - hereMapState: HereMapViewState, - arcGISMapState: ArcGISMapViewState, + googleMapState: GoogleMapViewStateImpl, + mapboxMapState: MapboxViewStateImpl, + hereMapState: HereViewStateImpl, + arcGISMapState: ArcGISMapViewStateImpl, modifier: Modifier = Modifier, onToggleSidebar: () -> Unit, onSdkSelectChange: (selected: MapViewState<*>) -> Unit, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index dc351e37..1bb9ebb3 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -10,6 +10,7 @@ import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.info.InfoBubbleEntry import com.mapconductor.core.map.MapOverlay import com.mapconductor.core.map.MapOverlayRegistry +import com.mapconductor.core.marker.MarkerOverlay import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.PolylineState @@ -79,17 +80,6 @@ open class MapViewScope { } } -class MarkerOverlay( - override val flow: StateFlow>, -) : MapOverlay { - override suspend fun render( - data: List, - controller: MapViewControllerAlias, - ) { - controller.addMarkers(data) - } -} - class CircleOverlay( override val flow: StateFlow>, ) : MapOverlay { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt index 23a59543..e03488ae 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt @@ -5,26 +5,14 @@ import com.mapconductor.core.circle.CircleRenderer import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerRenderer -import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRenderer import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineOverlayManager import com.mapconductor.core.polyline.PolylineRenderer -abstract class BaseMapViewController : - MapViewController { - abstract val markerRenderer: MarkerRenderer - - override val markerOverlayManager: MarkerOverlayManager by lazy { - createMarkerOverlayManager().also { overlayManager -> - markerRenderer.init(overlayManager) - onMarkerOverlayManagerInitialized(overlayManager) - } - } - +abstract class BaseMapViewController : + MapViewController { abstract val polylineRenderer: PolylineRenderer override val polylineOverlayManager: PolylineOverlayManager by lazy { @@ -52,16 +40,12 @@ abstract class BaseMapViewController) - protected abstract fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) protected abstract fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) protected abstract fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) - protected abstract fun createMarkerOverlayManager(): MarkerOverlayManager - protected abstract fun createPolylineOverlayManager(): PolylineOverlayManager protected abstract fun createPolygonOverlayManager(): PolygonOverlayManager @@ -71,10 +55,6 @@ abstract class BaseMapViewController { +interface MapViewController { val holder: MapViewHolder<*, *> val coroutine: CoroutineScope - val markerOverlayManager: MarkerOverlayManager - val hexGeocell: HexGeocell val polylineOverlayManager: PolylineOverlayManager val circleOverlayManager: CircleOverlayManager val polygonOverlayManager: PolygonOverlayManager - suspend fun addMarkers(data: List) - - suspend fun updateMarker(state: MarkerState) - suspend fun addPolylines(data: List) suspend fun updatePolyline(state: PolylineState) @@ -49,20 +35,9 @@ interface MapViewController + +typealias MapViewControllerAlias = MapViewController<*, *, *> diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt index a0867a65..25f053de 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt @@ -2,13 +2,9 @@ package com.mapconductor.core.controller import com.mapconductor.core.features.IGeoPoint - interface OverlayController { val zIndex: Int - // val overlayManager: OverlayManager - val renderer: OverlayRenderer - suspend fun add(data: List) suspend fun update(state: StateType) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt index 92ae79da..8e9db140 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayRenderer.kt @@ -1,14 +1,14 @@ package com.mapconductor.core.controller interface OverlayRenderer { - interface Changes { + interface ChangeParams { val current: EntityType val prev: EntityType } suspend fun onAdd(data: List): List - suspend fun onChange(data: List>): List + suspend fun onChange(data: List>): List suspend fun onRemove(data: List) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt index 1bdf9d15..78199fe3 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt @@ -10,10 +10,12 @@ interface GroundImageCapable { suspend fun compositionGroundImages(data: List) suspend fun updateGroundImage(state: GroundImageState) + + fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) } class GroundImageController( - override val renderer: OverlayRenderer>, + val renderer: OverlayRenderer>, override var clickListener: OnGroundImageEventHandler? = null, ) : OverlayController< ActualGroundImage, @@ -29,14 +31,14 @@ class GroundImageController( semaphore.withPermit { val previous = entities.keys.toMutableSet() val added = mutableListOf() - val updated = mutableListOf>>() + val updated = mutableListOf>>() val removed = mutableListOf>() data.forEach { if (previous.contains(it.id)) { val prevEntity = entities[it.id]!! updated.add( - object : OverlayRenderer.Changes> { + object : OverlayRenderer.ChangeParams> { override val current = GroundImageEntityImpl( groundImage = prevEntity.groundImage, @@ -98,10 +100,10 @@ class GroundImageController( override suspend fun update(state: GroundImageState) { semaphore.withPermit { - val updated = mutableListOf>>() + val updated = mutableListOf>>() val prevEntity = entities[state.id]!! updated.add( - object : OverlayRenderer.Changes> { + object : OverlayRenderer.ChangeParams> { override val current: GroundImageEntity = GroundImageEntityImpl( groundImage = prevEntity.groundImage, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index ee89b4b4..395c74b2 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -34,6 +34,7 @@ import com.mapconductor.core.groundimage.GroundImageCapable import com.mapconductor.core.info.InfoBubbleOverlay import com.mapconductor.core.info.LocalInfoBubbleCollector import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.settings.Settings import android.view.View import android.view.ViewGroup @@ -98,7 +99,7 @@ fun < markers.value.forEach { markerState -> LaunchedEffect(markerState.id) { markerState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updateMarker(markerState) + (controller as? MarkerCapable<*>)?.updateMarker(markerState) } } } @@ -118,7 +119,7 @@ fun < } } } - (controller as? GroundImageCapable<*>)?.let { + (controller as? GroundImageCapable)?.let { val groundImage = scope.groundImageFlow.collectAsState() groundImage.value.forEach { groundImageState -> LaunchedEffect(groundImageState.id) { @@ -185,7 +186,7 @@ fun < } } - if (controller != null && bubbles.isNotEmpty()) { + if (controller != null && bubbles.isNotEmpty() && cameraPosition != null) { Box( modifier = Modifier diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt index c651e923..8fc18530 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt @@ -4,9 +4,9 @@ import androidx.compose.ui.geometry.Offset import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.IGeoPoint -interface MapViewHolder { - val mapView: TMapView - val map: TMap +interface MapViewHolder { + val mapView: ActualMapViewType + val map: ActualMapType fun toScreenOffset(position: IGeoPoint): Offset? diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt index 0bcf093b..59e0eca4 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt @@ -25,7 +25,7 @@ interface MapViewState { val id: String val initCameraPosition: MapCameraPosition val isInitialized: StateFlow - val cameraPosition: StateFlow + val cameraPosition: StateFlow val mapDesignType: MapDesignType fun initAsync(init: suspend () -> Boolean) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt new file mode 100644 index 00000000..696fd159 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt @@ -0,0 +1,216 @@ +package com.mapconductor.core.marker + +import com.mapconductor.core.controller.OverlayController +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +interface MarkerCapable { + suspend fun compositionMarkers(data: List) + + suspend fun updateMarker(state: MarkerState) + + fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) + + fun setOnMarkerDrag(listener: OnMarkerEventHandler?) + + fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) + + fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) + + fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) + + fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) +} + +interface MarkerOverlayRenderer { + var animateStartListener: OnMarkerEventHandler? + var animateEndListener: OnMarkerEventHandler? + + interface AddParams { + val state: MarkerState + val bitmapIcon: BitmapIcon + } + + interface ChangeParams { + val current: MarkerEntity + val bitmapIcon: BitmapIcon + val prev: MarkerEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onAnimate(entity: MarkerEntity) + + suspend fun onPostProcess() +} + +abstract class AbstractMarkerController( + val markerManager: MarkerManager, + open val renderer: MarkerOverlayRenderer, + override var clickListener: OnMarkerEventHandler? = null, +) : OverlayController< + ActualMarker, + MarkerState, + MarkerEntity, + MarkerState, + > { + override val zIndex: Int = 10 + val semaphore = Semaphore(1) + + var dragStartListener: ((MarkerState) -> Unit)? = null + var dragListener: ((MarkerState) -> Unit)? = null + var dragEndListener: ((MarkerState) -> Unit)? = null + + protected fun setDraggingState( + markerState: MarkerState, + dragging: Boolean, + ) { + // Since this "isDragging" property is internal accessor, + // childViewControllers must call this method instead of "isDragging = true/false". + markerState.isDragging = dragging + } + + override suspend fun add(data: List) { + semaphore.withPermit { + val defaultIcon = DefaultIcon() + val defaultIconBitmapIcon = defaultIcon.toBitmapIcon() + val modifiedEntities = mutableListOf>() + val previous = markerManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() + val removed = mutableListOf>() + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = markerManager.getEntity(state.id)!! + val markerIcon = state.icon ?: defaultIcon + updated.add( + object : MarkerOverlayRenderer.ChangeParams { + override val current: MarkerEntity = + MarkerEntityImpl( + state = state, + marker = prevEntity.marker, + ) + override val bitmapIcon: BitmapIcon = markerIcon.toBitmapIcon() + override val prev: MarkerEntity = prevEntity + }, + ) + previous.remove(state.id) + } else { + added.add( + object : MarkerOverlayRenderer.AddParams { + override val state: MarkerState = state + override val bitmapIcon: BitmapIcon = state.icon?.toBitmapIcon() ?: defaultIconBitmapIcon + }, + ) + previous.remove(state.id) + } + } + + previous.forEach { remainId -> + markerManager.removeEntity(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + // Remove markers + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + // Add new markers + if (added.isNotEmpty()) { + val actualMarkers: List = renderer.onAdd(added) + actualMarkers.forEachIndexed { index, actualMarker -> + actualMarker?.let { + val entity = + MarkerEntityImpl( + marker = actualMarker, + state = added[index].state, + ) + markerManager.registerEntity(entity) + modifiedEntities.add(entity) + } + } + } + + // Update changed markers + if (updated.isNotEmpty()) { + val actualMarkers: List = renderer.onChange(updated) + + actualMarkers.forEachIndexed { index, actualMarker -> + actualMarker?.let { + val params = updated[index] + val entity = + MarkerEntityImpl( + state = params.current.state, + marker = actualMarker, + ) + markerManager.registerEntity(entity) + } + } + } + modifiedEntities.forEach { entity -> + entity.state.getAnimation()?.let { + renderer.onAnimate(entity) + } + } + renderer.onPostProcess() + } + } + + override suspend fun update(state: MarkerState) { + semaphore.withPermit { + val prevEntity = markerManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinger = prevEntity.fingerPrint + if (currentFinger == prevFinger) { + return + } + + val marker = prevEntity.marker + val defaultIcon = DefaultIcon() + val markerIcon = state.icon ?: defaultIcon + + val entity = + MarkerEntityImpl( + marker = marker, + state = state, + ) + val markerParams = + object : MarkerOverlayRenderer.ChangeParams { + override val current: MarkerEntity = entity + override val bitmapIcon: BitmapIcon = markerIcon.toBitmapIcon() + override val prev: MarkerEntity = prevEntity + } + val markers = renderer.onChange(listOf(markerParams)) + + markers[0]?.let { + val entity = + MarkerEntityImpl( + marker = it, + state = state, + ) + markerManager.registerEntity(entity) + + // Execute the animation property + if (prevFinger.animation != currentFinger.animation) { + state.getAnimation()?.let { + renderer.onAnimate(entity) + } + } + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + val entities: List> = markerManager.allEntities() + renderer.onRemove(entities) + markerManager.clear() + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerOverlayRenderer.kt new file mode 100644 index 00000000..652da368 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerOverlayRenderer.kt @@ -0,0 +1,135 @@ +package com.mapconductor.core.marker + +import androidx.compose.ui.geometry.Offset +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapViewHolder +import com.mapconductor.settings.Settings +import kotlin.math.min +import kotlin.math.pow +import android.os.SystemClock +import android.view.animation.BounceInterpolator +import android.view.animation.LinearInterpolator +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach + +abstract class AbstractMarkerOverlayRenderer< + MapViewHolderType : MapViewHolder<*, *>, + ActualMarker, +>( + val holder: MapViewHolderType, + val coroutine: CoroutineScope, + val tileSize: Int = 256, + val dropAnimateDuration: Int = Settings.Default.markerDropAnimateDuration, + val bounceAnimateDuration: Int = Settings.Default.markerBounceAnimateDuration, +) : MarkerOverlayRenderer { + override var animateStartListener: OnMarkerEventHandler? = null + override var animateEndListener: OnMarkerEventHandler? = null + + abstract fun setMarkerPosition( + markerEntity: MarkerEntity, + position: GeoPoint, + ) + + override suspend fun onAnimate(entity: MarkerEntity) { + val animation = entity.state.getAnimation() + when (animation) { + MarkerAnimation.Drop -> + animateMarkerDrop( + entity = entity, + duration = dropAnimateDuration, + ) + MarkerAnimation.Bounce -> + animateMarkerBounce( + entity = entity, + duration = bounceAnimateDuration, + ) + else -> throw IllegalArgumentException("No animation is available: $animation") + } + } + + fun zoomToMetersPerPixel(zoom: Double): Double { + val earthCircumference = 40075016.686 + return earthCircumference / (tileSize * 2.0.pow(zoom)) + } + + fun animateMarkerDrop( + entity: MarkerEntity, + duration: Int, + ) { + // アニメーションの最終的な目標地点(地理座標) + val target = entity.state.position + + // 線形補間 + val interpolator = LinearInterpolator() + + // 開始地点:x座標はMarkerと同じ、y座標は画面上端。なければreturn + val startPoint = holder.toScreenOffset(target)?.let { Offset(it.x, 0f) } ?: return + + animateStartListener?.invoke(entity.state) + + flow { + val startTime = SystemClock.uptimeMillis() + var t = 0f + while (t < 1f) { + val elapsed = SystemClock.uptimeMillis() - startTime + t = min(1f, elapsed.toFloat() / duration) + emit(interpolator.getInterpolation(t)) + delay(16L) + } + }.onEach { t: Float -> + // 開始時の画面座標から緯度経度に戻す(垂直方向アニメーション起点) + val startLatLng = holder.fromScreenOffset(startPoint)!! + + // 緯度・経度を線形補間 + val lat = t * target.latitude + (1f - t) * startLatLng.latitude + val lng = t * target.longitude + (1f - t) * startLatLng.longitude + + // 現在の座標をマーカーに適用 + val newPosition = GeoPoint.fromLatLong(lat, lng) + setMarkerPosition(entity, newPosition) + }.onCompletion { + entity.state.position = target + entity.state.setAnimation(null) + animateEndListener?.invoke(entity.state) + }.launchIn(coroutine) + } + + fun animateMarkerBounce( + entity: MarkerEntity, + duration: Int, + ) { + val startTime = SystemClock.uptimeMillis() + + val target = entity.state.position + val interpolator = BounceInterpolator() + val startPoint = holder.toScreenOffset(target)?.let { Offset(it.x, 0f) } ?: return + + animateStartListener?.invoke(entity.state) + flow { + var t = 0f + while (t < 1f) { + val elapsed = SystemClock.uptimeMillis() - startTime + t = interpolator.getInterpolation(min(1f, elapsed.toFloat() / duration)) + emit(t) + delay(16L) + } + }.onEach { t -> + val startLatLng = holder.fromScreenOffset(startPoint) ?: return@onEach + val lng = t * target.longitude + (1f - t) * startLatLng.longitude + val lat = t * target.latitude + (1f - t) * startLatLng.latitude + + // 現在の座標をマーカーに適用 + val newPosition = GeoPoint.fromLatLong(lat, lng) + setMarkerPosition(entity, newPosition) + }.onCompletion { + // 最終的にマーカー位置を正確な着地点に戻す(補間誤差などを吸収) + entity.state.position = target + entity.state.setAnimation(null) + animateEndListener?.invoke(entity.state) + }.launchIn(coroutine) + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt index 39f24c18..861cb189 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt @@ -35,6 +35,7 @@ class MarkerState( icon?.hashCode() ?: 0, clickable.hashCode(), draggable.hashCode(), + animation?.hashCode() ?: 0, ), ) ).toString() @@ -60,7 +61,13 @@ class MarkerState( } } - var animation by mutableStateOf(animation) + private var internalAnimation by mutableStateOf(animation) + + fun setAnimation(animation: MarkerAnimation?) { + internalAnimation = animation + } + + internal fun getAnimation(): MarkerAnimation? = internalAnimation var position by mutableStateOf(position) @@ -96,7 +103,7 @@ class MarkerState( result = 31 * result + draggable.hashCode() result = 31 * result + position.hashCode() result = 31 * result + (icon?.hashCode() ?: 0) - result = 31 * result + (ResourceProvider.spToPx(1.0).hashCode() ?: 0) + result = 31 * result + ResourceProvider.spToPx(1.0).hashCode() return result } @@ -108,7 +115,7 @@ class MarkerState( clickable.hashCode(), draggable.hashCode(), internalPosition.hashCode(), - animation.hashCode(), + internalAnimation.hashCode(), ) fun asFlow(): Flow = snapshotFlow { fingerPrint() }.distinctUntilChanged() diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerController.kt deleted file mode 100644 index 35c5bc9e..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerController.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.mapconductor.core.marker - -import com.mapconductor.core.controller.OverlayController -import com.mapconductor.core.controller.OverlayRenderer -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import kotlin.collections.forEach -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit - -interface MarkerCapable { - suspend fun compositionMarkers(data: List) - - suspend fun updateMarker(state: MarkerState) -} - - -class MarkerController( - val markerManager: MarkerManager, - override val renderer: OverlayRenderer>, - override var clickListener: OnMarkerEventHandler? = null, -) : OverlayController< - ActualMarker, - MarkerState, - MarkerEntity, - MarkerState, -> { - override val zIndex: Int = 10 - val entities = mutableMapOf>() - val semaphore = Semaphore(1) - - override suspend fun add(data: List) { - semaphore.withPermit { - val defaultIcon = DefaultIcon() - val defaultIconBitmapIcon = defaultIcon.toBitmapIcon() - val modifiedEntities = mutableListOf>() - val previous = markerManager.allEntities().map { it.state.id }.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>() - val removed = mutableListOf>() - data.forEach { state -> - if (previous.contains(state.id)) { - val prevEntity = markerManager.getEntity(state.id)!! - val markerIcon = state.icon ?: defaultIcon - updated.add( - object : UpdateParams { - override val entity: MarkerEntity = - MarkerEntityImpl( - state = state, - marker = prevEntity.marker, - ) - override val bitmapIcon: BitmapIcon - get() { - return markerIcon.toBitmapIcon() - } - override val prevEntity: MarkerEntity = prevEntity - }, - ) - previous.remove(state.id) - return@forEach - } - added.add(state) - previous.remove(state.id) - } - previous.forEach { remainId -> - markerManager.removeEntity(remainId)?.let { removedEntity -> - removed.add(removedEntity) - } - } - - // Remove markers - if (removed.isNotEmpty()) { - renderer.onRemove(removed) - } - - // Add new markers - if (added.isNotEmpty()) { - val addedList = added.toList() - - addedList - .map { state -> - val markerIcon = state.icon?.toBitmapIcon() ?: defaultIconBitmapIcon - Pair(state, markerIcon) - }.also { - val actualMarkers: List = renderer.onAdd(it) - actualMarkers.forEachIndexed { index, actualMarker -> - actualMarker?.let { - val entity = - MarkerEntityImpl( - marker = actualMarker, - state = addedList[index], - ) - markerManager.registerEntity(entity) - modifiedEntities.add(entity) - } - } - } - } - - // Update changed markers - if (updated.isNotEmpty()) { - val actualMarkers: List = renderer.onChange(updated) - - actualMarkers.forEachIndexed { index, actualMarker -> - actualMarker?.let { - val params = updated[index] - val entity = - MarkerEntityImpl( - state = params.entity.state, - marker = actualMarker, - ) - markerManager.registerEntity(entity) - } - } - } - modifiedEntities.forEach { entity -> - entity.state.animation?.let { - renderer.onAnimate(entity) - } - } - renderer.onPostProcess() - } - } - - override suspend fun update(state: MarkerState) { - semaphore.withPermit { - val updated = mutableListOf>>() - val prevEntity = entities[state.id]!! - updated.add( - object : OverlayRenderer.Changes> { - override val current: MarkerEntity = - MarkerEntityImpl( - marker = prevEntity.marker, - state = state, - ) - override val prev: MarkerEntity = prevEntity - }, - ) - - val actualOverlays: List = renderer.onChange(updated) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val entity = - MarkerEntityImpl( - marker = it, - state = state, - ) - entities[state.id] = entity - } - } - } - } - - override suspend fun clear() { - semaphore.withPermit { - renderer.onRemove(entities.values.toList()) - entities.clear() - } - } - - override fun find(position: IGeoPoint): MarkerEntity? { - // TODO: Improve this implementation later - return entities.values.find { entity -> - entity.state.bounds.contains(position) - } - } -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt index 27248993..5abdfc71 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt @@ -11,6 +11,6 @@ class MarkerOverlay( data: List, controller: MapViewControllerAlias, ) { - controller.addMarkers(data) + (controller as? MarkerCapable<*>)?.compositionMarkers(data) } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlayManagerImpl.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlayManagerImpl.kt deleted file mode 100644 index 77c034ae..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlayManagerImpl.kt +++ /dev/null @@ -1,176 +0,0 @@ -package com.mapconductor.core.marker - -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit - -interface MarkerOverlayManager { - val markerManager: MarkerManager - - suspend fun addMarkers(markerList: List) - - suspend fun updateMarker(marker: MarkerState) - - suspend fun clearOverlays() - - fun getMarkerState(id: String): MarkerState? -} - -class MarkerOverlayManagerImpl( - override val markerManager: MarkerManager, - val onRemove: suspend (List>) -> Unit, - val onAdd: suspend (List>) -> List, - val onChange: suspend (List>) -> List, - val onPostProcess: (suspend () -> Unit)? = null, - val onAnimate: suspend (entity: MarkerEntity) -> Unit, -) : MarkerOverlayManager { - val semaphore = Semaphore(1) - - override suspend fun addMarkers(markerList: List) { - semaphore.withPermit { - val defaultIcon = DefaultIcon() - val defaultIconBitmapIcon = defaultIcon.toBitmapIcon() - val modifiedEntities = mutableListOf>() - val previous = markerManager.allEntities().map { it.state.id }.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>() - val removed = mutableListOf>() - markerList.forEach { state -> - if (previous.contains(state.id)) { - val prevEntity = markerManager.getEntity(state.id)!! - val markerIcon = state.icon ?: defaultIcon - updated.add( - object : UpdateParams { - override val entity: MarkerEntity = - MarkerEntityImpl( - state = state, - marker = prevEntity.marker, - ) - override val bitmapIcon: BitmapIcon - get() { - return markerIcon.toBitmapIcon() - } - override val prevEntity: MarkerEntity = prevEntity - }, - ) - previous.remove(state.id) - return@forEach - } - added.add(state) - previous.remove(state.id) - } - previous.forEach { remainId -> - markerManager.removeEntity(remainId)?.let { removedEntity -> - removed.add(removedEntity) - } - } - - // Remove markers - if (removed.isNotEmpty()) { - onRemove(removed) - } - - // Add new markers - if (added.isNotEmpty()) { - val addedList = added.toList() - - addedList - .map { state -> - val markerIcon = state.icon?.toBitmapIcon() ?: defaultIconBitmapIcon - Pair(state, markerIcon) - }.also { - val actualMarkers: List = onAdd(it) - actualMarkers.forEachIndexed { index, actualMarker -> - actualMarker?.let { - val entity = - MarkerEntityImpl( - marker = actualMarker, - state = addedList[index], - ) - markerManager.registerEntity(entity) - modifiedEntities.add(entity) - } - } - } - } - - // Update changed markers - if (updated.isNotEmpty()) { - val actualMarkers: List = onChange(updated) - - actualMarkers.forEachIndexed { index, actualMarker -> - actualMarker?.let { - val params = updated[index] - val entity = - MarkerEntityImpl( - state = params.entity.state, - marker = actualMarker, - ) - markerManager.registerEntity(entity) - } - } - } - modifiedEntities.forEach { entity -> - entity.state.animation?.let { - onAnimate(entity) - } - } - onPostProcess?.invoke() - } - } - - override suspend fun updateMarker(state: MarkerState) { - val prevEntity = markerManager.getEntity(state.id) ?: return - val currentFinger = state.fingerPrint() - val prevFinger = prevEntity.fingerPrint - if (currentFinger == prevFinger) { - return - } - - semaphore.withPermit { - val marker = prevEntity.marker - val defaultIcon = DefaultIcon() - val markerIcon = state.icon ?: defaultIcon - - val entity = - MarkerEntityImpl( - marker = marker, - state = state, - ) - val markerParams = - object : UpdateParams { - override val entity: MarkerEntity = entity - override val bitmapIcon: BitmapIcon = markerIcon.toBitmapIcon() - override val prevEntity: MarkerEntity = prevEntity - } - val markers = onChange(listOf(markerParams)) - - markers[0]?.let { - val entity = - MarkerEntityImpl( - marker = it, - state = state, - ) - markerManager.registerEntity(entity) - - // Execute the animation property - if (prevFinger.animation != currentFinger.animation) { - state.animation?.let { - onAnimate(entity) - } - } - } - } - } - - override suspend fun clearOverlays() { - semaphore.withPermit { - val entities: List> = markerManager.allEntities() - markerManager.clear() - onRemove(entities) - markerManager.clear() - } - } - - override fun getMarkerState(id: String): MarkerState? = markerManager.getEntity(id)?.state -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerRenderer.kt deleted file mode 100644 index 2bc17899..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerRenderer.kt +++ /dev/null @@ -1,242 +0,0 @@ -package com.mapconductor.core.marker - -import androidx.compose.ui.geometry.Offset -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.map.MapViewHolder -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.spherical.haversineDistance -import com.mapconductor.settings.Settings -import kotlin.math.min -import kotlin.math.pow -import android.os.SystemClock -import android.view.animation.BounceInterpolator -import android.view.animation.LinearInterpolator -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach - -interface MarkerRendererFactory { - fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)? = null, - ): MarkerOverlayManager -} - -interface MarkerRenderer { - interface UpdateParams { - val entity: MarkerEntity - val bitmapIcon: BitmapIcon - val prevEntity: MarkerEntity - } - - fun init(markerOverlayManager: MarkerOverlayManager) - - suspend fun addIcons(newMarkers: List>): List - - suspend fun removeIcons(removeEntities: List>) - - suspend fun changeIcons(changes: List>): List - - fun animate(entity: MarkerEntity) - - fun setOnMarkerAnimationStart(listener: OnMarkerEventHandler?) - - fun setOnMarkerAnimationEnd(listener: OnMarkerEventHandler?) - - fun setDraggingState( - markerState: MarkerState, - dragging: Boolean, - ) - - fun findNearestMarker( - position: IGeoPoint, - tolerance: Double, - zoom: Double, - ): MarkerEntity? -} - -abstract class AbstractMarkerRenderer : MarkerRenderer { - protected val defaultIcon: BitmapIcon - protected var markerAnimationStartHandler: ((state: MarkerState) -> Unit)? = null - protected var markerAnimationEndHandler: ((state: MarkerState) -> Unit)? = null - - protected lateinit var markerOverlayManager: MarkerOverlayManager - - abstract val holder: MapViewHolder<*, *> - abstract val coroutine: CoroutineScope - - override fun setOnMarkerAnimationStart(listener: OnMarkerEventHandler?) { - this.markerAnimationStartHandler = listener - } - - override fun setOnMarkerAnimationEnd(listener: OnMarkerEventHandler?) { - this.markerAnimationEndHandler = listener - } - - init { - defaultIcon = DefaultIcon().toBitmapIcon() - } - - override fun findNearestMarker( - position: IGeoPoint, - tolerance: Double, - zoom: Double, - ): MarkerEntity? { -// val acceptDPI = tolerance.value * ResourceProvider.density - -// clearPolyline() -// -// // 検索範囲の詳細分析 -// val searchAnalysis = analyzeSearchRange(position, zoom, acceptDPI.toDouble()) -// -// // 可視化レイヤーを選択 -// drawSearchOutline(searchAnalysis) - - return findMarkerFromPoint( - position = position, - zoom = zoom, - tolerance = tolerance, - ) - } - - protected abstract fun setMarkerPosition( - markerEntity: MarkerEntity, - position: GeoPoint, - ) - - override fun init(markerManager: MarkerOverlayManager) { - this.markerOverlayManager = markerManager - } - - override fun animate(entity: MarkerEntity) { - when (entity.state.animation) { - MarkerAnimation.Drop -> animateMarkerDrop(entity) - MarkerAnimation.Bounce -> animateMarkerBounce(entity) - else -> throw IllegalArgumentException("No animation is available: ${entity.state.animation}") - } - } - - protected fun zoomToMetersPerPixel(zoom: Double): Double { - val earthCircumference = 40075016.686 - val tileSize = 256 - return earthCircumference / (tileSize * 2.0.pow(zoom)) - } - - override fun setDraggingState( - markerState: MarkerState, - dragging: Boolean, - ) { - // Since this "isDragging" property is internal accessor, - // childViewControllers must call this method instead of "isDragging = true/false". - markerState.isDragging = dragging - } - - protected fun findMarkerFromPoint( - position: IGeoPoint, - zoom: Double, - tolerance: Double, - ): MarkerEntity? { - val meterInMapPixel = zoomToMetersPerPixel(zoom) - val radius = tolerance * meterInMapPixel - val entity = markerOverlayManager.markerManager.findNearest(position) ?: return null - val distance = haversineDistance(position, entity.state.position) - return if (distance <= radius) { - entity - } else { - null - } - } - - protected fun animateMarkerDrop( - markerEntity: MarkerEntity, // ラップしたMarkerオブジェクト - duration: Int = Settings.Default.markerDropAnimateDuration, // アニメションする時間(ms) - ) { - // アニメーションの最終的な目標地点(地理座標) - val target = markerEntity.state.position - - // 線形補間 - val interpolator = LinearInterpolator() - - // 開始地点:x座標はMarkerと同じ、y座標は画面上端。なければreturn - val startPoint = holder.toScreenOffset(target)?.let { Offset(it.x, 0f) } ?: return - - markerAnimationStartHandler?.invoke(markerEntity.state) - - // ここからアニメ本体 - flow { - val startTime = SystemClock.uptimeMillis() - var t = 0f - while (t < 1f) { - val elapsed = SystemClock.uptimeMillis() - startTime - t = min(1f, elapsed.toFloat() / duration) - emit(interpolator.getInterpolation(t)) - delay(16L) - } - }.onEach { t: Float -> - // 開始時の画面座標から緯度経度に戻す(垂直方向アニメーション起点) - val startLatLng = holder.fromScreenOffset(startPoint)!! - - // 緯度・経度を線形補間 - val lat = t * target.latitude + (1f - t) * startLatLng.latitude - val lng = t * target.longitude + (1f - t) * startLatLng.longitude - - // 現在の座標をマーカーに適用 - val newPosition = GeoPoint.fromLatLong(lat, lng) - setMarkerPosition(markerEntity, newPosition) - }.onCompletion { - // 最終的にマーカー位置を正確な着地点に戻す(補間誤差などを吸収) - markerEntity.state.position = target - markerEntity.state.animation = null - markerAnimationEndHandler?.invoke(markerEntity.state) - }.launchIn(coroutine) - } - - protected fun animateMarkerBounce( - markerEntity: MarkerEntity, - duration: Int = Settings.Default.markerBounceAnimateDuration, // アニメションする時間(ms) - ) { - val startTime = SystemClock.uptimeMillis() - - // アニメーションの最終的な目標地点(地理座標) - val target = markerEntity.state.position - - // 線形補間 - val interpolator = BounceInterpolator() - - // 開始地点:x座標はMarkerと同じ、y座標は画面上端。なければreturn - val startPoint = holder.toScreenOffset(target)?.let { Offset(it.x, 0f) } ?: return - - markerAnimationStartHandler?.invoke(markerEntity.state) - flow { - var t = 0f - while (t < 1f) { - val elapsed = SystemClock.uptimeMillis() - startTime - t = interpolator.getInterpolation(min(1f, elapsed.toFloat() / duration)) - emit(t) - delay(16L) - } - }.onEach { t -> - val startLatLng = holder.fromScreenOffset(startPoint) ?: return@onEach - val lng = target.longitude - val lat = t * target.latitude + (1f - t) * startLatLng.latitude - - // 現在の座標をマーカーに適用 - val newPosition = GeoPoint.fromLatLong(lat, lng) - setMarkerPosition(markerEntity, newPosition) - }.onCompletion { - // 最終的にマーカー位置を正確な着地点に戻す(補間誤差などを吸収) - markerEntity.state.position = target - markerEntity.state.animation = null - markerAnimationEndHandler?.invoke(markerEntity.state) - }.launchIn(coroutine) - } -} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index 9add681d..7724f066 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler @@ -16,18 +15,17 @@ import com.mapconductor.core.polyline.OnPolylineEventHandler @Composable fun ArcGISMapView( - state: ArcGISMapViewState, + state: ArcGISMapViewStateImpl, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler? = {}, - onMarkerClick: OnMarkerEventHandler? = {}, - onMarkerDragStart: OnMarkerEventHandler? = {}, - onMarkerDrag: OnMarkerEventHandler? = {}, - onMarkerDragEnd: OnMarkerEventHandler? = {}, - onMarkerAnimateStart: OnMarkerEventHandler? = {}, - onMarkerAnimateEnd: OnMarkerEventHandler? = {}, - onCircleClick: OnCircleEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, - onPolylineClick: OnPolylineEventHandler? = {}, + onMapClick: OnMapEventHandler? = null, + onMarkerClick: OnMarkerEventHandler? = null, + onMarkerDragStart: OnMarkerEventHandler? = null, + onMarkerDrag: OnMarkerEventHandler? = null, + onMarkerDragEnd: OnMarkerEventHandler? = null, + onMarkerAnimateStart: OnMarkerEventHandler? = null, + onMarkerAnimateEnd: OnMarkerEventHandler? = null, + onCircleClick: OnCircleEventHandler? = null, + onPolylineClick: OnPolylineEventHandler? = null, content: (@Composable ArcGISMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -65,14 +63,14 @@ fun ArcGISMapView( controller.holder.mapView.onResume(owner) controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) - controller.setMarkerClickListener(onMarkerClick) - controller.setMarkerDragStartListener(onMarkerDragStart) - controller.setMarkerDragListener(onMarkerDrag) - controller.setMarkerDragEndListener(onMarkerDragEnd) controller.setCircleClickListener(onCircleClick) controller.setPolylineClickListener(onPolylineClick) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) + controller.setOnMarkerClickListener(onMarkerClick) + controller.setOnMarkerDragStart(onMarkerDragStart) + controller.setOnMarkerDrag(onMarkerDrag) + controller.setOnMarkerDragEnd(onMarkerDragEnd) + controller.setOnMarkerAnimateStart(onMarkerAnimateStart) + controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) state.controller = controller diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt index ec34c923..ee4789d2 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt @@ -1,6 +1,5 @@ package com.mapconductor.arcgis -import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.mapping.view.LongPressEvent import com.arcgismaps.mapping.view.PanChangeEvent @@ -10,13 +9,12 @@ import com.arcgismaps.mapping.view.UpEvent import com.arcgismaps.mapping.view.extensions.motionEvent import com.mapconductor.arcgis.circle.ArcGISCircleRenderer import com.mapconductor.arcgis.circle.DefaultArcGISCircleRenderer -import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer -import com.mapconductor.arcgis.marker.DefaultArcGISMarkerRender +import com.mapconductor.arcgis.marker.ArcGISMarkerController +import com.mapconductor.arcgis.marker.SelectedMarker import com.mapconductor.arcgis.polygon.ArcGISPolygonRenderer import com.mapconductor.arcgis.polygon.DefaultArcGISPolygonRenderer import com.mapconductor.arcgis.polyline.ArcGISPolylineRenderer import com.mapconductor.arcgis.polyline.DefaultArcGISPolylineRenderer -import com.mapconductor.core.ResourceProvider import com.mapconductor.core.circle.CircleClickEvent import com.mapconductor.core.circle.CircleOverlayManager import com.mapconductor.core.circle.CircleRenderer @@ -24,13 +22,11 @@ import com.mapconductor.core.circle.CircleRendererFactory import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController -import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerRenderer -import com.mapconductor.core.marker.MarkerRendererFactory +import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRenderer import com.mapconductor.core.polygon.PolygonRendererFactory @@ -38,7 +34,6 @@ import com.mapconductor.core.polyline.PolylineOverlayManager import com.mapconductor.core.polyline.PolylineRenderer import com.mapconductor.core.polyline.PolylineRendererFactory import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.projection.WebMercator import com.mapconductor.settings.Settings import android.view.MotionEvent import kotlinx.coroutines.CoroutineScope @@ -47,11 +42,11 @@ import kotlinx.coroutines.launch interface IArcGISMapViewController : MapViewController< - ArcGISActualMarker, ArcGISActualCircle, ArcGISActualPolyline, ArcGISActualPolygon, - > { + >, + MarkerCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -64,23 +59,9 @@ interface IArcGISMapViewController : ) } -internal data class SelectedMarker( - val state: MarkerState, - val graphic: Graphic, -) - class ArcGISMapViewController( override val holder: ArcGISMapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - override val hexGeocell: HexGeocell = - HexGeocell( - projection = WebMercator, - baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 - ), - private val markerLayer: GraphicsOverlay = - GraphicsOverlay().apply { - sceneProperties.surfacePlacement = SurfacePlacement.Relative - }, private val circleLayer: GraphicsOverlay = GraphicsOverlay().apply { sceneProperties.surfacePlacement = SurfacePlacement.DrapedFlat @@ -93,38 +74,19 @@ class ArcGISMapViewController( GraphicsOverlay().apply { sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded }, - private val markerRendererFactory: MarkerRendererFactory = DefaultArcGISMarkerRender(), private val polylineRendererFactory: PolylineRendererFactory = DefaultArcGISPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultArcGISPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultArcGISCircleRenderer(), + private val markerController: ArcGISMarkerController, ) : BaseMapViewController< - ArcGISActualMarker, ArcGISActualCircle, ArcGISActualPolyline, ArcGISActualPolygon, >(), IArcGISMapViewController { - override val markerRenderer: MarkerRenderer = - ArcGISMarkerRenderer( - markerLayer = markerLayer, - holder = holder, - coroutine = coroutine, - ) - - private var selectedMarker: SelectedMarker? = null - - override fun createMarkerOverlayManager(): MarkerOverlayManager = - markerRendererFactory.create( - hexGeocell = hexGeocell, - onIconAdd = markerRenderer::addIcons, - onIconRemove = markerRenderer::removeIcons, - onIconChange = markerRenderer::changeIcons, - onAnimate = markerRenderer::animate, - ) - override fun createPolylineOverlayManager(): PolylineOverlayManager = polylineRendererFactory.create( onAdd = polylineRenderer::addPolylines, @@ -176,15 +138,11 @@ class ArcGISMapViewController( override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { } - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { - } - init { - markerRenderer.init(markerOverlayManager) holder.map.graphicsOverlays.clear() holder.map.graphicsOverlays.add(circleLayer) holder.map.graphicsOverlays.add(polylineLayer) - holder.map.graphicsOverlays.add(markerLayer) + holder.map.graphicsOverlays.add(markerController.renderer.markerLayer) setupListeners() } @@ -214,35 +172,33 @@ class ArcGISMapViewController( } private suspend fun onMapPan(event: PanChangeEvent) { - selectedMarker?.also { + markerController.selectedMarker?.also { val screenPoint = event.screenCoordinate val point = holder.map.screenToLocation(screenPoint).getOrNull() ?: return val position = point.toGeoPoint() it.graphic.geometry = point it.state.position = position - markerDragCallback?.invoke(it.state) + markerController.dragListener?.invoke(it.state) } } private suspend fun onMapUp(event: UpEvent) { - selectedMarker?.also { + markerController.selectedMarker?.also { val screenPoint = event.screenCoordinate val point = holder.map.screenToLocation(screenPoint).getOrNull() ?: return val position = point.toGeoPoint() it.graphic.geometry = point it.state.position = position - // Restore the recomposition for the position property - markerRenderer.setDraggingState(it.state, false) + markerController.selectedMarker = null + markerController.dragEndListener?.invoke(it.state) - markerDragEndCallback?.invoke(it.state) with(holder.map) { interactionOptions.isPanEnabled = true interactionOptions.isRotateEnabled = true interactionOptions.isZoomEnabled = true } } - selectedMarker = null } private suspend fun onMapLongPress(event: LongPressEvent) { @@ -253,7 +209,7 @@ class ArcGISMapViewController( val position = point.toGeoPoint() val identifyResult = holder.map.identifyGraphicsOverlay( - graphicsOverlay = markerLayer, + graphicsOverlay = markerController.renderer.markerLayer, screenCoordinate = screenPoint, tolerance = Settings.Default.tapTolerance.value @@ -261,29 +217,28 @@ class ArcGISMapViewController( returnPopupsOnly = false, ) val graphics = identifyResult.getOrNull()?.graphics - val graphic = graphics?.firstOrNull() - if (graphic == null) { - mapLongClickCallback?.invoke(position) - return - } - val markerId = (graphic.attributes.get("id") as? String) ?: return - val state = markerOverlayManager.getMarkerState(markerId) ?: return - selectedMarker = - SelectedMarker( - state = state, - graphic = graphic, - ) - // 3Dナビゲーションを無効化 - with(holder.map) { - interactionOptions.isPanEnabled = false - interactionOptions.isRotateEnabled = false - interactionOptions.isZoomEnabled = false + graphics?.firstOrNull()?.let { graphic -> + (graphic.attributes.get("id") as? String)?.let { markerId -> + markerController.markerManager.getEntity(markerId)?.let { entity -> + if (entity.state.draggable) { + markerController.selectedMarker = + SelectedMarker( + state = entity.state, + graphic = graphic, + ) + // 3Dナビゲーションを無効化 + with(holder.map) { + interactionOptions.isPanEnabled = false + interactionOptions.isRotateEnabled = false + interactionOptions.isZoomEnabled = false + } + markerController.dragStartListener?.invoke(entity.state) + return + } + } + } } - - // Suppress the recomposition for the position property - markerRenderer.setDraggingState(state, true) - - markerDragStartCallback?.invoke(state) + mapLongClickCallback?.invoke(position) } private suspend fun onMapTap(event: SingleTapConfirmedEvent) { @@ -294,16 +249,8 @@ class ArcGISMapViewController( .getOrNull() ?.toGeoPoint() ?: return - val markerEntity = - markerRenderer.findNearestMarker( - position = touchPosition, - tolerance = - Settings.Default.tapTolerance.value - .toDouble() * ResourceProvider.getDensity(), - zoom = holder.map.getCurrentViewpointCamera().getZoomLevel(), - ) - if (markerEntity != null) { - markerClickCallback?.invoke(markerEntity.state) + markerController.find(touchPosition)?.let { markerEntity -> + markerController.clickListener?.invoke(markerEntity.state) return } @@ -323,13 +270,13 @@ class ArcGISMapViewController( } override suspend fun clearOverlays() { - markerOverlayManager.clearOverlays() + markerController.clear() polylineOverlayManager.clearOverlays() } - override suspend fun addMarkers(markerList: List) = markerOverlayManager.addMarkers(markerList) + override suspend fun compositionMarkers(data: List) = markerController.add(data) - override suspend fun updateMarker(state: MarkerState) = markerOverlayManager.updateMarker(state) + override suspend fun updateMarker(state: MarkerState) = markerController.update(state) override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) @@ -367,4 +314,28 @@ class ArcGISMapViewController( listener?.onComplete(result.isSuccess) } } + + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { + this.markerController.dragStartListener = listener + } + + override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { + this.markerController.dragListener = listener + } + + override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { + this.markerController.dragEndListener = listener + } + + override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { + this.markerController.renderer.animateStartListener = listener + } + + override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { + this.markerController.renderer.animateEndListener = listener + } + + override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { + this.markerController.clickListener = listener + } } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt index 185a9c8e..3d178729 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt @@ -1,8 +1,15 @@ package com.mapconductor.arcgis +import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.mapping.view.SceneView +import com.arcgismaps.mapping.view.SurfacePlacement +import com.mapconductor.arcgis.marker.ArcGISMarkerController +import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer +import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.projection.WebMercator import android.content.Context typealias ArcGISMapViewHolder = MapViewHolder @@ -28,8 +35,36 @@ object ArcGISViewControllerStore : val controller = ArcGISMapViewController( holder = holder, + markerController = getMarkerController(holder), ) this.set(id, controller) return controller } + + private fun getMarkerController(holder: ArcGISMapViewHolder): ArcGISMarkerController { + val hexGeocell = + HexGeocell( + projection = WebMercator, + baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 + ) + val manager = MarkerManager(hexGeocell) + + val markerLayer: GraphicsOverlay = + GraphicsOverlay().apply { + sceneProperties.surfacePlacement = SurfacePlacement.Relative + } + + val renderer = + ArcGISMarkerRenderer( + markerLayer = markerLayer, + holder = holder, + ) + + val controller = + ArcGISMarkerController( + markerManager = manager, + renderer = renderer, + ) + return controller + } } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt index 92633b09..373ed8bc 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt @@ -22,14 +22,14 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IArcGISMapViewState : MapViewState +interface ArcGISMapViewState : MapViewState -class ArcGISMapViewState( +class ArcGISMapViewStateImpl( override val id: String, override val initCameraPosition: MapCameraPosition, override val mapDesignType: ArcGISDesign, ) : MapViewStateImpl(), - IArcGISMapViewState { + ArcGISMapViewState { // Map padding private val _padding = MutableStateFlow(MapPaddingsImpl.Zeros) val padding: StateFlow = _padding.asStateFlow() @@ -74,11 +74,11 @@ class ArcGISMapViewState( } } -class ArcGISMapViewSaver : BaseMapViewSaver() { - override fun extractCameraPosition(state: ArcGISMapViewState): MapCameraPosition? = state.cameraPosition.value +class ArcGISMapViewSaver : BaseMapViewSaver() { + override fun extractCameraPosition(state: ArcGISMapViewStateImpl): MapCameraPosition? = state.cameraPosition.value override fun saveMapDesign( - state: ArcGISMapViewState, + state: ArcGISMapViewStateImpl, bundle: Bundle, ) { bundle.putString("id", state.mapDesignType.id) @@ -88,8 +88,8 @@ class ArcGISMapViewSaver : BaseMapViewSaver() { stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): ArcGISMapViewState = - ArcGISMapViewState( + ): ArcGISMapViewStateImpl = + ArcGISMapViewStateImpl( id = stateId, mapDesignType = ArcGISDesign.Create( @@ -98,14 +98,14 @@ class ArcGISMapViewSaver : BaseMapViewSaver() { initCameraPosition = cameraPosition, ) - override fun getStateId(state: ArcGISMapViewState): String = state.id + override fun getStateId(state: ArcGISMapViewStateImpl): String = state.id } @Composable fun rememberArcGISMapViewState( mapDesign: ArcGISDesign = ArcGISDesign.Streets, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, -): ArcGISMapViewState { +): ArcGISMapViewStateImpl { val stateId by rememberSaveable { val uuid = UUID.randomUUID().toString() mutableStateOf(uuid) @@ -115,7 +115,7 @@ fun rememberArcGISMapViewState( stateSaver = ArcGISMapViewSaver().createSaver(), ) { mutableStateOf( - ArcGISMapViewState( + ArcGISMapViewStateImpl( id = stateId, mapDesignType = mapDesign, initCameraPosition = MapCameraPosition.from(cameraPosition), diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt new file mode 100644 index 00000000..462d6906 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerController.kt @@ -0,0 +1,63 @@ +package com.mapconductor.arcgis.marker + +import com.arcgismaps.mapping.view.Graphic +import com.mapconductor.arcgis.ArcGISActualMarker +import com.mapconductor.arcgis.getZoomLevel +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.AbstractMarkerController +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.settings.Settings + +internal data class SelectedMarker( + val state: MarkerState, + val graphic: Graphic, +) + +class ArcGISMarkerController( + markerManager: MarkerManager, + override val renderer: ArcGISMarkerRenderer, +) : AbstractMarkerController( + markerManager = markerManager, + renderer = renderer, + ) { + private var internalSelectedMarker: SelectedMarker? = null + + internal var selectedMarker: SelectedMarker? + set(value) { + if (value == null) { + internalSelectedMarker?.let { + // Restore the recomposition for the position property + setDraggingState(it.state, false) + } + return + } + internalSelectedMarker = value + // Suppress the recomposition for the position property + setDraggingState(value.state, true) + } + get() = internalSelectedMarker + + override fun find(position: IGeoPoint): MarkerEntity? { + return markerManager.findNearest(position)?.let { nearest -> + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + val zoom = + renderer.holder.map + .getCurrentViewpointCamera() + .getZoomLevel() + val meterInMapPixel = renderer.zoomToMetersPerPixel(zoom) + val radius = tolerance * meterInMapPixel + val distance = haversineDistance(position, nearest.state.position) + return if (distance <= radius) { + nearest + } else { + null + } + } + } +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt index 8fb2203c..5c384523 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt @@ -4,48 +4,27 @@ import androidx.core.graphics.drawable.toDrawable import com.arcgismaps.mapping.symbology.PictureMarkerSymbol import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay +import com.mapconductor.arcgis.ArcGISActualMarker import com.mapconductor.arcgis.ArcGISMapViewHolder import com.mapconductor.arcgis.toPoint import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.marker.AbstractMarkerRenderer -import com.mapconductor.core.marker.BitmapIcon +import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerOverlayManagerImpl -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.MarkerOverlayRenderer import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultArcGISMarkerRender : MarkerRendererFactory { - override fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): MarkerOverlayManager = - MarkerOverlayManagerImpl( - markerManager = MarkerManager(hexGeocell), - onAdd = onIconAdd, - onChange = onIconChange, - onRemove = onIconRemove, - onPostProcess = onPostProcess, - onAnimate = onAnimate, - ) -} - class ArcGISMarkerRenderer( - private val markerLayer: GraphicsOverlay, - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractMarkerRenderer() { + val markerLayer: GraphicsOverlay, + holder: ArcGISMapViewHolder, + coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractMarkerOverlayRenderer( + holder = holder, + coroutine = coroutine, + ) { override fun setMarkerPosition( markerEntity: MarkerEntity, position: GeoPoint, @@ -53,16 +32,16 @@ class ArcGISMarkerRenderer( markerEntity.marker.geometry = position.toPoint(holder.map.scene?.spatialReference) } - override suspend fun addIcons(newMarkers: List>): List { + override suspend fun onAdd(data: List): List { return withContext(coroutine.coroutineContext) { - newMarkers + data .map { params -> - val bitmapDrawable = params.second.bitmap.toDrawable(holder.mapView.context.resources) + val bitmapDrawable = params.bitmapIcon.bitmap.toDrawable(holder.mapView.context.resources) val density = ResourceProvider.getDensity() - val width = ((params.second.size.width * (params.first.icon?.scale ?: 1.0f)) / density) - val height = ((params.second.size.height * (params.first.icon?.scale ?: 1.0f)) / density) - val anchorX = (0.5 - params.second.anchor.x) * width - val anchorY = (params.second.anchor.y - 0.5) * height + val width = ((params.bitmapIcon.size.width * (params.state.icon?.scale ?: 1.0f)) / density) + val height = ((params.bitmapIcon.size.height * (params.state.icon?.scale ?: 1.0f)) / density) + val anchorX = (0.5 - params.bitmapIcon.anchor.x) * width + val anchorY = (params.bitmapIcon.anchor.y - 0.5) * height val pictureSymbolFuture = PictureMarkerSymbol.createWithImage(bitmapDrawable).also { @@ -74,10 +53,10 @@ class ArcGISMarkerRenderer( val marker = Graphic( - geometry = GeoPoint.from(params.first.position).toPoint(holder.map.scene?.spatialReference), + geometry = GeoPoint.from(params.state.position).toPoint(holder.map.scene?.spatialReference), symbol = pictureSymbolFuture, ).also { - it.attributes.set("id", params.first.id) + it.attributes.set("id", params.state.id) } return@map marker }.also { @@ -86,18 +65,24 @@ class ArcGISMarkerRenderer( } } - override suspend fun removeIcons(removeEntities: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { - val elements = removeEntities.map { params -> params.marker } + val elements = data.map { params -> params.marker } markerLayer.graphics.removeAll(elements) } } - override suspend fun changeIcons(changes: List>): List = + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + data: List>, + ): List = withContext(coroutine.coroutineContext) { - changes.map { params -> - val prevFinger = params.prevEntity.fingerPrint - val currFinger = params.entity.fingerPrint + data.map { params -> + val prevFinger = params.prev.fingerPrint + val currFinger = params.current.fingerPrint if (currFinger.icon != prevFinger.icon) { val bitmapDrawable = params.bitmapIcon.bitmap.toDrawable(holder.mapView.context.resources) val density = ResourceProvider.getDensity() @@ -113,16 +98,16 @@ class ArcGISMarkerRenderer( it.offsetX = anchorX.toFloat() it.offsetY = anchorY.toFloat() } - params.entity.marker.symbol = pictureSymbolFuture + params.current.marker.symbol = pictureSymbolFuture } - if (params.entity.state.position != params.prevEntity.state.position) { - params.entity.marker.geometry = - GeoPoint.from(params.entity.state.position).toPoint() + if (params.current.state.position != params.prev.state.position) { + params.current.marker.geometry = + GeoPoint.from(params.current.state.position).toPoint() } // ArcGISはマーカーを再作成しなくてよいので、同じマーカーのインスタンスを返す - params.entity.marker + params.current.marker } } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt index 68769060..7b68fdb6 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapTypeAlias.kt @@ -6,8 +6,8 @@ import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.Polyline -typealias ActualGoogleMapMarker = Marker -typealias ActualGoogleMapCircle = Circle -typealias ActualGoogleMapPolyline = Polyline -typealias ActualGoogleMapPolygon = Polygon -typealias ActualGoogleMapGroundImage = GroundOverlay +typealias GoogleMapActualMarker = Marker +typealias GoogleMapActualCircle = Circle +typealias GoogleMapActualPolyline = Polyline +typealias GoogleMapActualPolygon = Polygon +typealias GoogleMapActualGroundImage = GroundOverlay diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index 3dc97f44..aa90a07f 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -22,18 +22,18 @@ import android.view.ViewGroup @Composable fun GoogleMapsView( - state: IGoogleMapViewState, + state: GoogleMapViewState, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler? = {}, - onMarkerClick: OnMarkerEventHandler? = {}, - onMarkerDragStart: OnMarkerEventHandler? = {}, - onMarkerDrag: OnMarkerEventHandler? = {}, - onMarkerDragEnd: OnMarkerEventHandler? = {}, - onMarkerAnimateStart: OnMarkerEventHandler? = {}, - onMarkerAnimateEnd: OnMarkerEventHandler? = {}, - onCircleClick: OnCircleEventHandler? = {}, - onPolylineClick: OnPolylineEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = {}, + onMapClick: OnMapEventHandler? = null, + onMarkerClick: OnMarkerEventHandler? = null, + onMarkerDragStart: OnMarkerEventHandler? = null, + onMarkerDrag: OnMarkerEventHandler? = null, + onMarkerDragEnd: OnMarkerEventHandler? = null, + onMarkerAnimateStart: OnMarkerEventHandler? = null, + onMarkerAnimateEnd: OnMarkerEventHandler? = null, + onCircleClick: OnCircleEventHandler? = null, + onPolylineClick: OnPolylineEventHandler? = null, + onGroundImageClick: OnGroundImageEventHandler? = null, content: (@Composable GoogleMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -54,14 +54,14 @@ fun GoogleMapsView( // Specific Google Maps initialization logic // This lambda will be executed within state.initAsync by MapViewBase val cameraPosition = - state.cameraPosition.value.let { + state.cameraPosition.value?.let { camera -> CameraPosition .Builder() .apply { - target(GeoPoint.from(it.position).toLatLng()) - zoom(it.zoom.toFloat()) - bearing(it.bearing.toFloat()) - tilt(it.tilt.toFloat()) + target(GeoPoint.from(camera.position).toLatLng()) + zoom(camera.zoom.toFloat()) + bearing(camera.bearing.toFloat()) + tilt(camera.tilt.toFloat()) }.build() } @@ -76,19 +76,19 @@ fun GoogleMapsView( id = state.id, options = mapInitOptions, ) - (state as? GoogleMapViewState)?.let { mapViewState -> + (state as? GoogleMapViewStateImpl)?.let { mapViewState -> mapViewState.controller = controller controller.setCameraMoveListener(mapViewState::onCameraChange) } controller.setMapClickListener(onMapClick) - controller.setMarkerClickListener(onMarkerClick) - controller.setMarkerDragStartListener(onMarkerDragStart) - controller.setMarkerDragListener(onMarkerDrag) - controller.setMarkerDragEndListener(onMarkerDragEnd) + controller.setOnMarkerClickListener(onMarkerClick) + controller.setOnMarkerDragStart(onMarkerDragStart) + controller.setOnMarkerDrag(onMarkerDrag) + controller.setOnMarkerDragEnd(onMarkerDragEnd) controller.setCircleClickListener(onCircleClick) controller.setPolylineClickListener(onPolylineClick) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) + controller.setOnMarkerAnimateStart(onMarkerAnimateStart) + controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) controller.setOnGroundImageClickListener(onGroundImageClick) holderRef.value = controller.holder diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt index 6aa0d496..476e8fc2 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -7,11 +7,8 @@ import com.google.android.gms.maps.GoogleMap.OnCameraMoveCanceledListener import com.google.android.gms.maps.GoogleMap.OnCameraMoveListener import com.google.android.gms.maps.GoogleMap.OnCameraMoveStartedListener import com.google.android.gms.maps.GoogleMap.OnMapClickListener -import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener -import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.Polyline import com.mapconductor.core.circle.CircleClickEvent @@ -21,7 +18,6 @@ import com.mapconductor.core.circle.CircleRendererFactory import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController -import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.groundimage.GroundImageCapable import com.mapconductor.core.groundimage.GroundImageController import com.mapconductor.core.groundimage.GroundImageEvent @@ -29,10 +25,9 @@ import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerRenderer -import com.mapconductor.core.marker.MarkerRendererFactory +import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRenderer import com.mapconductor.core.polygon.PolygonRendererFactory @@ -40,11 +35,9 @@ import com.mapconductor.core.polyline.PolylineOverlayManager import com.mapconductor.core.polyline.PolylineRenderer import com.mapconductor.core.polyline.PolylineRendererFactory import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.projection.WebMercator import com.mapconductor.googlemaps.circle.DefaultGoogleMapCircleRenderer import com.mapconductor.googlemaps.circle.GoogleMapCircleRenderer -import com.mapconductor.googlemaps.marker.DefaultGoogleMapMarkerRenderer -import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer +import com.mapconductor.googlemaps.marker.GoogleMapMarkerController import com.mapconductor.googlemaps.polygon.DefaultGoogleMapPolygonRenderer import com.mapconductor.googlemaps.polygon.GoogleMapPolygonRenderer import com.mapconductor.googlemaps.polyline.DefaultGoogleMapPolylineRenderer @@ -54,8 +47,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch interface IGoogleMapViewController : - MapViewController, - GroundImageCapable { + MapViewController, + GroundImageCapable, + MarkerCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -70,46 +64,23 @@ interface IGoogleMapViewController : class GoogleMapViewController( override val holder: GoogleMapViewHolder, + private val markerController: GoogleMapMarkerController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - override val hexGeocell: HexGeocell = - HexGeocell( - projection = WebMercator, - baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 - ), - private val markerRendererFactory: MarkerRendererFactory = DefaultGoogleMapMarkerRenderer(), private val polylineRendererFactory: PolylineRendererFactory = DefaultGoogleMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultGoogleMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultGoogleMapCircleRenderer(), - private val groundImageController: GroundImageController, + private val groundImageController: GroundImageController, ) : BaseMapViewController< - ActualGoogleMapMarker, - ActualGoogleMapCircle, - ActualGoogleMapPolyline, - ActualGoogleMapPolygon, + GoogleMapActualCircle, + GoogleMapActualPolyline, + GoogleMapActualPolygon, >(), IGoogleMapViewController, OnCameraMoveStartedListener, OnCameraMoveCanceledListener, OnCameraMoveListener, OnCameraIdleListener, - OnMarkerClickListener, - OnMapClickListener, - OnMarkerDragListener { - override val markerRenderer: MarkerRenderer = - GoogleMapMarkerRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createMarkerOverlayManager(): MarkerOverlayManager = - markerRendererFactory.create( - hexGeocell = hexGeocell, - onIconAdd = markerRenderer::addIcons, - onIconRemove = markerRenderer::removeIcons, - onIconChange = markerRenderer::changeIcons, - onAnimate = markerRenderer::animate, - ) - + OnMapClickListener { override val polylineRenderer: PolylineRenderer = GoogleMapPolylineRenderer( holder = holder, @@ -145,9 +116,6 @@ class GoogleMapViewController( override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { } - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { - } - override val circleRenderer: CircleRenderer = GoogleMapCircleRenderer( holder = holder, @@ -163,7 +131,6 @@ class GoogleMapViewController( init { setupListeners() - markerRenderer.init(markerOverlayManager) } override fun setupListeners() { @@ -171,9 +138,7 @@ class GoogleMapViewController( holder.map.setOnCameraMoveCanceledListener(this) holder.map.setOnCameraMoveListener(this) holder.map.setOnCameraIdleListener(this) - holder.map.setOnMarkerClickListener(this) holder.map.setOnMapClickListener(this) - holder.map.setOnMarkerDragListener(this) } override fun moveCamera( @@ -213,13 +178,16 @@ class GoogleMapViewController( } override suspend fun clearOverlays() { - markerOverlayManager.clearOverlays() + markerController.clear() + groundImageController.clear() + circleOverlayManager.clearOverlays() polylineOverlayManager.clearOverlays() + polygonOverlayManager.clearOverlays() } - override suspend fun addMarkers(markerList: List) = markerOverlayManager.addMarkers(markerList) + override suspend fun compositionMarkers(data: List) = markerController.add(data) - override suspend fun updateMarker(state: MarkerState) = markerOverlayManager.updateMarker(state) + override suspend fun updateMarker(state: MarkerState) = markerController.update(state) override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) @@ -257,18 +225,6 @@ class GoogleMapViewController( } } - override fun onMarkerClick(marker: Marker): Boolean { - val key = marker.tag?.toString() ?: return true - val state = markerOverlayManager.getMarkerState(key) ?: return true - if (!state.clickable) return true - markerClickCallback?.let { - coroutine.launch { - it(state) - } - } - return true - } - override fun onMapClick(position: LatLng) { val touchPosition = position.toGeoPoint() @@ -299,45 +255,35 @@ class GoogleMapViewController( } } - private fun getMarkerStateFrom(marker: Marker): MarkerState? { - val markerId = marker.tag as? String ?: return null - return markerOverlayManager.getMarkerState(markerId) - } - - override fun onMarkerDrag(marker: Marker) { - this.getMarkerStateFrom(marker)?.also { state -> + override suspend fun compositionGroundImages(data: List) = groundImageController.add(data) - // Suppress the recomposition for the position property - markerRenderer.setDraggingState(state, true) + override suspend fun updateGroundImage(state: GroundImageState) = groundImageController.update(state) - state.position = marker.position.toGeoPoint() - markerDragCallback?.invoke(state) - } + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { + this.markerController.dragStartListener = listener } - override fun onMarkerDragEnd(marker: Marker) { - this.getMarkerStateFrom(marker)?.also { state -> - state.position = marker.position.toGeoPoint() - markerDragEndCallback?.invoke(state) - } + override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { + this.markerController.dragListener = listener } - override fun onMarkerDragStart(marker: Marker) { - this.getMarkerStateFrom(marker)?.also { state -> - state.position = marker.position.toGeoPoint() - - // Restore the recomposition for the position property - markerRenderer.setDraggingState(state, false) + override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { + this.markerController.dragEndListener = listener + } - markerDragStartCallback?.invoke(state) - } + override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { + this.markerController.renderer.animateStartListener = listener } - override suspend fun compositionGroundImages(data: List) = groundImageController.add(data) + override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { + this.markerController.renderer.animateEndListener = listener + } - override suspend fun updateGroundImage(state: GroundImageState) = groundImageController.update(state) + override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { + this.markerController.clickListener = listener + } - fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) { + override fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) { this.groundImageController.clickListener = listener } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt index ced55963..b6f5efb3 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt @@ -3,10 +3,15 @@ package com.mapconductor.googlemaps import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.MapView +import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.groundimage.GroundImageController import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.projection.WebMercator import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer +import com.mapconductor.googlemaps.marker.GoogleMapMarkerController +import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer import android.app.Activity import android.content.Context import android.content.ContextWrapper @@ -30,6 +35,42 @@ object GoogleMapViewControllerStore : StaticHolder() { options = options, ) + val controller = + GoogleMapViewController( + markerController = getMarkerController(holder), + groundImageController = getGroundImageController(holder), + holder = holder, + ) + this.set(id, controller) + + return controller + } + + private fun getMarkerController(holder: GoogleMapViewHolder): GoogleMapMarkerController { + val hexGeocell = + HexGeocell( + projection = WebMercator, + baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 + ) + val manager = MarkerManager(hexGeocell) + + val renderer = + GoogleMapMarkerRenderer( + holder = holder, + ) + + val markerController = + GoogleMapMarkerController( + markerManager = manager, + renderer = renderer, + ) + + return markerController + } + + private fun getGroundImageController( + holder: GoogleMapViewHolder, + ): GroundImageController { val groundImageRenderer = GoogleMapGroundImageRenderer( holder = holder, @@ -40,14 +81,7 @@ object GoogleMapViewControllerStore : StaticHolder() { renderer = groundImageRenderer, ) - val controller = - GoogleMapViewController( - groundImageController = groundImageController, - holder = holder, - ) - this.set(id, controller) - - return controller + return groundImageController } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt similarity index 82% rename from mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt rename to mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt index 5b94238c..cf57ee74 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewState.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt @@ -18,21 +18,21 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IGoogleMapViewState : MapViewState +interface GoogleMapViewState : MapViewState -class GoogleMapViewState( +class GoogleMapViewStateImpl( override val id: String, override val mapDesignType: GoogleMapDesignType, override val initCameraPosition: MapCameraPosition, ) : MapViewStateImpl(), - IGoogleMapViewState { + GoogleMapViewState { // Map padding private val _padding = MutableStateFlow(MapPaddingsImpl.Zeros) val padding: StateFlow = _padding.asStateFlow() // Camera position - private val _cameraPosition = MutableStateFlow(initCameraPosition) - override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() + private val _cameraPosition = MutableStateFlow(initCameraPosition) + override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() internal var controller: IGoogleMapViewController? = null @@ -46,12 +46,13 @@ class GoogleMapViewState( listener?.onComplete(false) return } - val currCameraPosition = this.cameraPosition.value - val newPosition = - currCameraPosition.copy( - position = position, - ) - this.moveCameraTo(newPosition, durationMs, listener) + this.cameraPosition.value?.let { current -> + val newPosition = + current.copy( + position = position, + ) + this.moveCameraTo(newPosition, durationMs, listener) + } } override fun moveCameraTo( @@ -101,11 +102,11 @@ class GoogleMapViewState( } // GoogleMapViewSaver implementation -class GoogleMapViewSaver : BaseMapViewSaver() { - override fun extractCameraPosition(state: GoogleMapViewState): MapCameraPosition? = state.cameraPosition.value +class GoogleMapViewSaver : BaseMapViewSaver() { + override fun extractCameraPosition(state: GoogleMapViewStateImpl): MapCameraPosition? = state.cameraPosition.value override fun saveMapDesign( - state: GoogleMapViewState, + state: GoogleMapViewStateImpl, bundle: Bundle, ) { bundle.putInt("id", state.mapDesignType.id) @@ -115,8 +116,8 @@ class GoogleMapViewSaver : BaseMapViewSaver() { stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): GoogleMapViewState = - GoogleMapViewState( + ): GoogleMapViewStateImpl = + GoogleMapViewStateImpl( id = stateId, mapDesignType = GoogleMapDesign.Create( @@ -125,14 +126,14 @@ class GoogleMapViewSaver : BaseMapViewSaver() { initCameraPosition = cameraPosition, ) - override fun getStateId(state: GoogleMapViewState): String = state.id + override fun getStateId(state: GoogleMapViewStateImpl): String = state.id } @Composable fun rememberGoogleMapViewState( mapDesign: GoogleMapDesignType = GoogleMapDesign.Normal, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, -): GoogleMapViewState { +): GoogleMapViewStateImpl { val stateId by rememberSaveable { val uuid = UUID.randomUUID().toString() mutableStateOf(uuid) @@ -142,7 +143,7 @@ fun rememberGoogleMapViewState( stateSaver = GoogleMapViewSaver().createSaver(), ) { mutableStateOf( - GoogleMapViewState( + GoogleMapViewStateImpl( id = stateId, mapDesignType = mapDesign, initCameraPosition = MapCameraPosition.from(cameraPosition), diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt index 4d831a7b..d77e82a3 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt @@ -7,7 +7,7 @@ import com.google.android.gms.maps.model.GroundOverlayOptions import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.groundimage.GroundImageEntity import com.mapconductor.core.groundimage.GroundImageState -import com.mapconductor.googlemaps.ActualGoogleMapGroundImage +import com.mapconductor.googlemaps.GoogleMapActualGroundImage import com.mapconductor.googlemaps.GoogleMapViewHolder import com.mapconductor.googlemaps.toLatLngBounds import kotlinx.coroutines.CoroutineScope @@ -18,8 +18,8 @@ import kotlinx.coroutines.withContext class GoogleMapGroundImageRenderer( val holder: GoogleMapViewHolder, val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), -) : OverlayRenderer> { - override suspend fun onAdd(data: List): List { +) : OverlayRenderer> { + override suspend fun onAdd(data: List): List { return withContext(coroutine.coroutineContext) { return@withContext data.map { state -> val bounds = state.bounds.toLatLngBounds() ?: return@withContext emptyList() @@ -37,7 +37,7 @@ class GoogleMapGroundImageRenderer( } } - override suspend fun onRemove(data: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { data.forEach { params -> params.groundImage.remove() } } @@ -48,7 +48,7 @@ class GoogleMapGroundImageRenderer( } override suspend fun onChange( - data: List>>, + data: List>>, ): List { return withContext(coroutine.coroutineContext) { return@withContext data.map { params -> diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerController.kt new file mode 100644 index 00000000..7ff3b3bc --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerController.kt @@ -0,0 +1,88 @@ +package com.mapconductor.googlemaps.marker + +import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener +import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.AbstractMarkerController +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.googlemaps.GoogleMapActualMarker +import com.mapconductor.googlemaps.toGeoPoint +import com.mapconductor.settings.Settings + +class GoogleMapMarkerController( + markerManager: MarkerManager, + override val renderer: GoogleMapMarkerRenderer, +) : AbstractMarkerController( + markerManager = markerManager, + renderer = renderer, + ), + OnMarkerClickListener, + OnMarkerDragListener { + init { + renderer.holder.map.apply { + setOnMarkerClickListener(this@GoogleMapMarkerController) + setOnMarkerDragListener(this@GoogleMapMarkerController) + } + } + + override fun onMarkerClick(marker: GoogleMapActualMarker): Boolean { + val stateId = (marker.tag as? String) ?: return false + markerManager.getEntity(stateId)?.also { entity -> + if (!entity.state.clickable) return true + clickListener?.invoke(entity.state) + } + return true + } + + override fun onMarkerDrag(marker: GoogleMapActualMarker) { + val stateId = (marker.tag as? String) ?: return + markerManager.getEntity(stateId)?.also { entity -> + + // Suppress the recomposition for the position property + setDraggingState(entity.state, true) + + entity.state.position = marker.position.toGeoPoint() + dragListener?.invoke(entity.state) + } + } + + override fun onMarkerDragEnd(marker: GoogleMapActualMarker) { + val stateId = (marker.tag as? String) ?: return + markerManager.getEntity(stateId)?.also { entity -> + entity.state.position = marker.position.toGeoPoint() + dragEndListener?.invoke(entity.state) + } + } + + override fun onMarkerDragStart(marker: GoogleMapActualMarker) { + val stateId = (marker.tag as? String) ?: return + markerManager.getEntity(stateId)?.also { entity -> + entity.state.position = marker.position.toGeoPoint() + // Restore the recomposition for the position property + setDraggingState(entity.state, false) + dragStartListener?.invoke(entity.state) + } + } + + override fun find(position: IGeoPoint): MarkerEntity? { + return markerManager.findNearest(position)?.let { nearest -> + val zoom = + renderer.holder.map.cameraPosition.zoom + .toDouble() + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + val meterInMapPixel = renderer.zoomToMetersPerPixel(zoom) + val radius = tolerance * meterInMapPixel + val distance = haversineDistance(position, nearest.state.position) + return if (distance <= radius) { + nearest + } else { + null + } + } + } +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerRenderer.kt index 36e950d7..47dc7350 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/marker/GoogleMapMarkerRenderer.kt @@ -4,95 +4,80 @@ import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.marker.AbstractMarkerRenderer -import com.mapconductor.core.marker.BitmapIcon +import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerOverlayManagerImpl -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.MarkerOverlayRenderer +import com.mapconductor.googlemaps.GoogleMapActualMarker import com.mapconductor.googlemaps.GoogleMapViewHolder import com.mapconductor.googlemaps.toLatLng import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultGoogleMapMarkerRenderer : MarkerRendererFactory { - override fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): MarkerOverlayManager = - MarkerOverlayManagerImpl( - markerManager = MarkerManager(hexGeocell), - onRemove = onIconRemove, - onAdd = onIconAdd, - onChange = onIconChange, - onPostProcess = onPostProcess, - onAnimate = onAnimate, - ) -} - class GoogleMapMarkerRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractMarkerRenderer() { + holder: GoogleMapViewHolder, + coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractMarkerOverlayRenderer( + holder = holder, + coroutine = coroutine, + ) { override fun setMarkerPosition( - markerEntity: MarkerEntity, + markerEntity: MarkerEntity, position: GeoPoint, ) { markerEntity.marker.position = position.toLatLng() } - override suspend fun addIcons(newMarkers: List>): List { + override suspend fun onAdd(data: List): List { return withContext(coroutine.coroutineContext) { - newMarkers.map { params -> - val bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(params.second.bitmap) + data.map { params -> + val bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(params.bitmapIcon.bitmap) val options = MarkerOptions() - .position(GeoPoint.from(params.first.position).toLatLng()) + .position(GeoPoint.from(params.state.position).toLatLng()) .anchor( - params.second.anchor.x, - params.second.anchor.y, + params.bitmapIcon.anchor.x, + params.bitmapIcon.anchor.y, ).icon(bitmapDescriptor) - .draggable(params.first.draggable) + .draggable(params.state.draggable) val marker = holder.map.addMarker(options)?.also { - it.tag = params.first.id + it.tag = params.state.id } return@map marker } } } - override suspend fun removeIcons(removeEntities: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { - removeEntities.forEach { params -> params.marker.remove() } + data.forEach { params -> params.marker.remove() } } } - override suspend fun changeIcons(changes: List>): List = + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + changes: List>, + ): List = withContext(coroutine.coroutineContext) { changes.map { params -> - val prevFinger = params.prevEntity.fingerPrint - val currentFinger = params.entity.fingerPrint + val prevFinger = params.prev.fingerPrint + val currentFinger = params.current.fingerPrint if (prevFinger.icon != currentFinger.icon) { val bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(params.bitmapIcon.bitmap) - params.entity.marker.setIcon(bitmapDescriptor) + params.current.marker.setIcon(bitmapDescriptor) } - if (params.entity.state.position != params.prevEntity.state.position) { - params.entity.marker.position = - GeoPoint.from(params.entity.state.position).toLatLng() + if (params.current.state.position != params.prev.state.position) { + params.current.marker.position = + GeoPoint.from(params.current.state.position).toLatLng() } // Google Mapsはマーカーを再作成しなくてよいので、同じマーカーのインスタンスを返す - params.entity.marker + params.current.marker } } } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index 9ca73d3e..2e3d9a81 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -10,7 +10,6 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler @@ -24,18 +23,17 @@ import kotlinx.coroutines.suspendCancellableCoroutine @OptIn(ExperimentalCoroutinesApi::class) @Composable fun HereMapView( - state: IHereMapViewState, + state: HereViewState, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler? = {}, - onMarkerClick: OnMarkerEventHandler? = {}, - onMarkerDragStart: OnMarkerEventHandler? = {}, - onMarkerDrag: OnMarkerEventHandler? = {}, - onMarkerDragEnd: OnMarkerEventHandler? = {}, - onMarkerAnimateStart: OnMarkerEventHandler? = {}, - onMarkerAnimateEnd: OnMarkerEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, - onCircleClick: OnCircleEventHandler? = {}, - onPolylineClick: OnPolylineEventHandler? = {}, + onMapClick: OnMapEventHandler? = null, + onMarkerClick: OnMarkerEventHandler? = null, + onMarkerDragStart: OnMarkerEventHandler? = null, + onMarkerDrag: OnMarkerEventHandler? = null, + onMarkerDragEnd: OnMarkerEventHandler? = null, + onMarkerAnimateStart: OnMarkerEventHandler? = null, + onMarkerAnimateEnd: OnMarkerEventHandler? = null, + onCircleClick: OnCircleEventHandler? = null, + onPolylineClick: OnPolylineEventHandler? = null, content: (@Composable HereMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -68,23 +66,21 @@ fun HereMapView( options = mapInitOptions, ) - (state as? HereMapViewState)?.let { mapViewState -> + (state as? HereViewStateImpl)?.let { mapViewState -> mapViewState.controller = controller controller.setCameraMoveListener(mapViewState::onCameraChange) } controller.setMapClickListener(onMapClick) - controller.setMarkerClickListener(onMarkerClick) - controller.setMarkerDragStartListener(onMarkerDragStart) - controller.setMarkerDragListener(onMarkerDrag) - controller.setMarkerDragEndListener(onMarkerDragEnd) + controller.setOnMarkerClickListener(onMarkerClick) + controller.setOnMarkerDragStart(onMarkerDragStart) + controller.setOnMarkerDrag(onMarkerDrag) + controller.setOnMarkerDragEnd(onMarkerDragEnd) + controller.setOnMarkerAnimateStart(onMarkerAnimateStart) + controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) controller.setCircleClickListener(onCircleClick) controller.setPolylineClickListener(onPolylineClick) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) - (state as? HereMapViewState)?.controller = controller + (state as? HereViewStateImpl)?.controller = controller controller.holder.mapView.mapScene.loadScene(state.mapDesignType.id) { mapError -> if (mapError != null) { diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt index 62e434ca..e53857c6 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt @@ -10,14 +10,12 @@ import com.here.sdk.mapview.MapCamera import com.here.sdk.mapview.MapCameraAnimationFactory import com.here.sdk.mapview.MapCameraListener import com.here.sdk.mapview.MapCameraUpdateFactory -import com.here.sdk.mapview.MapMarker import com.here.sdk.mapview.MapMeasure import com.here.sdk.mapview.MapPolygon import com.here.sdk.mapview.MapPolyline import com.here.sdk.mapview.MapScene import com.here.sdk.mapview.MapView import com.here.time.Duration -import com.mapconductor.core.ResourceProvider import com.mapconductor.core.circle.CircleClickEvent import com.mapconductor.core.circle.CircleOverlayManager import com.mapconductor.core.circle.CircleRendererFactory @@ -25,36 +23,32 @@ import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.MapViewState.MoveCameraCallback -import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerRenderer -import com.mapconductor.core.marker.MarkerRendererFactory +import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRendererFactory import com.mapconductor.core.polyline.PolylineOverlayManager import com.mapconductor.core.polyline.PolylineRenderer import com.mapconductor.core.polyline.PolylineRendererFactory import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.projection.WebMercator import com.mapconductor.here.circle.DefaultHereMapCircleRenderer import com.mapconductor.here.circle.HereMapCircleRenderer -import com.mapconductor.here.marker.DefaultHereMapMarkerRenderer -import com.mapconductor.here.marker.HereMapMarkerRenderer +import com.mapconductor.here.marker.HereMarkerController import com.mapconductor.here.polygon.DefaultHereMapPolygonRenderer import com.mapconductor.here.polygon.HereMapPolygonRenderer import com.mapconductor.here.polyline.DefaultHereMapPolylineRenderer import com.mapconductor.here.polyline.HereMapPolylineRenderer -import com.mapconductor.settings.Settings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IHereMapViewController : MapViewController { +interface IHereMapViewController : + MapViewController, + MarkerCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MoveCameraCallback? = null, @@ -68,20 +62,14 @@ interface IHereMapViewController : MapViewController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - override val hexGeocell: HexGeocell = - HexGeocell( - projection = WebMercator, - baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 - ), - private val markerRendererFactory: MarkerRendererFactory = DefaultHereMapMarkerRenderer(), private val polylineRendererFactory: PolylineRendererFactory = DefaultHereMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultHereMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), ) : BaseMapViewController< - HereMapActualMarker, HereMapActualCircle, HereMapActualPolyline, HereMapActualPolygon, @@ -94,23 +82,6 @@ class HereMapViewController( private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 } - private var selectedMarker: MarkerEntity? = null - - override val markerRenderer: MarkerRenderer = - HereMapMarkerRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createMarkerOverlayManager(): MarkerOverlayManager = - markerRendererFactory.create( - hexGeocell = hexGeocell, - onIconAdd = markerRenderer::addIcons, - onIconRemove = markerRenderer::removeIcons, - onIconChange = markerRenderer::changeIcons, - onAnimate = markerRenderer::animate, - ) - override val polylineRenderer: PolylineRenderer = HereMapPolylineRenderer( holder = holder, @@ -138,9 +109,6 @@ class HereMapViewController( override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { } - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { - } - override fun createPolylineOverlayManager(): PolylineOverlayManager = polylineRendererFactory.create( onAdd = polylineRenderer::addPolylines, @@ -163,15 +131,39 @@ class HereMapViewController( ) override suspend fun clearOverlays() { - markerOverlayManager.clearOverlays() + markerController.clear() polylineOverlayManager.clearOverlays() polygonOverlayManager.clearOverlays() circleOverlayManager.clearOverlays() } - override suspend fun addMarkers(markerList: List) = markerOverlayManager.addMarkers(markerList) + override suspend fun compositionMarkers(data: List) = markerController.add(data) + + override suspend fun updateMarker(state: MarkerState) = markerController.update(state) + + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { + markerController.dragStartListener = listener + } + + override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { + markerController.dragListener = listener + } - override suspend fun updateMarker(state: MarkerState) = markerOverlayManager.updateMarker(state) + override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { + markerController.dragEndListener = listener + } + + override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { + markerController.renderer.animateStartListener = listener + } + + override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { + markerController.renderer.animateEndListener = listener + } + + override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { + markerController.clickListener = listener + } override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) @@ -183,7 +175,6 @@ class HereMapViewController( init { setupListeners() - markerRenderer.init(markerOverlayManager) polygonRenderer.init(polygonOverlayManager) circleRenderer.init(circleOverlayManager) } @@ -259,19 +250,9 @@ class HereMapViewController( override fun onTap(point: Point2D) { val touchPosition = this.getGeoPointFromPoint(point) ?: return - val zoom = holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE - val tolerance = - Settings.Default.tapTolerance.value - .toDouble() * ResourceProvider.getDensity() - - val entity = - markerRenderer.findNearestMarker( - position = touchPosition, - tolerance = tolerance, - zoom = zoom, - ) - if (entity != null) { - markerClickCallback?.invoke(entity.state) + + markerController.find(touchPosition)?.let { entity -> + markerController.clickListener?.invoke(entity.state) return } @@ -297,46 +278,33 @@ class HereMapViewController( when (gesture.value) { GestureState.BEGIN.value -> { - val zoom = holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE - val tolerance = - Settings.Default.tapTolerance.value - .toDouble() * ResourceProvider.getDensity() - - val entity = - markerRenderer.findNearestMarker( - position = position, - tolerance = tolerance, - zoom = zoom, - ) ?: return - - entity.state.position = position - selectedMarker = entity - - // Suppress the recomposition for the position property - markerRenderer.setDraggingState(entity.state, true) - - markerDragStartCallback?.invoke(entity.state) + markerController.find(position)?.let { entity -> + if (entity.state.draggable) { + entity.state.position = position + markerController.selectedMarker = entity + markerController.dragStartListener?.invoke(entity.state) + return + } + } + mapLongClickCallback?.invoke(position) } GestureState.UPDATE.value -> { - selectedMarker?.also { selected -> + markerController.selectedMarker?.also { selected -> holder.mapView.viewToGeoCoordinates(point)?.also { coordinates -> selected.marker.coordinates = coordinates selected.state.position = coordinates.toGeoPoint() } - markerDragCallback?.invoke(selected.state) + markerController.dragListener?.invoke(selected.state) } } GestureState.END.value, GestureState.CANCEL.value -> { - selectedMarker?.also { selected -> - markerOverlayManager.markerManager.updateEntity(selected) - - // Restore the recomposition for the position property - markerRenderer.setDraggingState(selected.state, false) - - markerDragEndCallback?.invoke(selected.state) - selectedMarker = null + markerController.selectedMarker?.also { selected -> + markerController.markerManager.updateEntity(selected) + markerController.dragEndListener?.invoke(selected.state) + markerController.selectedMarker = null + markerController.selectedMarker = null } } } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt index a1c7e8c3..e6d29c52 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt @@ -5,8 +5,13 @@ import com.here.sdk.core.engine.SDKNativeEngine import com.here.sdk.core.engine.SDKOptions import com.here.sdk.mapview.MapScene import com.here.sdk.mapview.MapView +import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.projection.WebMercator +import com.mapconductor.here.marker.HereMapMarkerRenderer +import com.mapconductor.here.marker.HereMarkerController import android.content.Context import android.content.pm.PackageManager @@ -77,15 +82,38 @@ object HereMapViewControllerStore : StaticHolder() { // } // } // } + val markerController = getMarkerController(holder) val controller = HereMapViewController( holder = holder, + markerController = markerController, ) this.set(id, controller) return controller } + private fun getMarkerController(holder: HereMapViewHolder): HereMarkerController { + val hexGeocell = + HexGeocell( + projection = WebMercator, + baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 + ) + val manager = MarkerManager(hexGeocell) + + val renderer = + HereMapMarkerRenderer( + holder = holder, + ) + + val controller = + HereMarkerController( + markerManager = manager, + renderer = renderer, + ) + return controller + } + // fun release() { // mapCount-- // if (mapCount > 0) return diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt similarity index 89% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt index 1600d14e..e40fb317 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewState.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt @@ -24,14 +24,14 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IHereMapViewState : MapViewState +interface HereViewState : MapViewState -class HereMapViewState( +class HereViewStateImpl( override val id: String, override val mapDesignType: HereMapDesignType, override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, ) : MapViewStateImpl(), - IHereMapViewState { + HereViewState { internal var controller: IHereMapViewController? = null // Camera center position @@ -84,11 +84,11 @@ class HereMapViewState( } } -class HereMapViewSaver : BaseMapViewSaver() { - override fun extractCameraPosition(state: HereMapViewState): MapCameraPosition? = state.cameraPosition.value +class HereMapViewSaver : BaseMapViewSaver() { + override fun extractCameraPosition(state: HereViewStateImpl): MapCameraPosition? = state.cameraPosition.value override fun saveMapDesign( - state: HereMapViewState, + state: HereViewStateImpl, bundle: Bundle, ) { bundle.putInt("id", state.mapDesignType.getValue().value) @@ -98,8 +98,8 @@ class HereMapViewSaver : BaseMapViewSaver() { stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): HereMapViewState = - HereMapViewState( + ): HereViewStateImpl = + HereViewStateImpl( id = stateId, mapDesignType = HereMapDesign.CreateById( @@ -110,14 +110,14 @@ class HereMapViewSaver : BaseMapViewSaver() { override fun getCameraPaddings(): MapPaddings? = MapPaddingsImpl.Zeros - override fun getStateId(state: HereMapViewState): String = state.id + override fun getStateId(state: HereViewStateImpl): String = state.id } @Composable fun rememberHereMapViewState( mapDesign: HereMapDesign = HereMapDesign.NormalDay, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, -): HereMapViewState { +): HereViewStateImpl { val stateId by rememberSaveable { val uuid = UUID.randomUUID().toString() mutableStateOf(uuid) @@ -127,7 +127,7 @@ fun rememberHereMapViewState( stateSaver = HereMapViewSaver().createSaver(), ) { mutableStateOf( - HereMapViewState( + HereViewStateImpl( id = stateId, mapDesignType = mapDesign, initCameraPosition = MapCameraPosition.from(cameraPosition), diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt index 74af6fe5..10689d75 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt @@ -4,68 +4,50 @@ import com.here.sdk.core.Metadata import com.here.sdk.mapview.MapMarker import com.mapconductor.core.calculateZIndex import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.marker.AbstractMarkerRenderer -import com.mapconductor.core.marker.BitmapIcon +import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer import com.mapconductor.core.marker.MarkerEntity -import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerOverlayManagerImpl -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.MarkerOverlayRenderer +import com.mapconductor.here.HereMapActualMarker import com.mapconductor.here.HereMapViewHolder import com.mapconductor.here.toAnchor2D import com.mapconductor.here.toGeoCoordinates import com.mapconductor.here.toMapImage import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultHereMapMarkerRenderer : MarkerRendererFactory { - override fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): MarkerOverlayManager = - MarkerOverlayManagerImpl( - markerManager = MarkerManager(hexGeocell), - onAdd = onIconAdd, - onChange = onIconChange, - onRemove = onIconRemove, - onPostProcess = onPostProcess, - onAnimate = onAnimate, - ) -} - class HereMapMarkerRenderer( - override val holder: HereMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractMarkerRenderer() { + holder: HereMapViewHolder, + coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractMarkerOverlayRenderer< + HereMapViewHolder, + HereMapActualMarker, + >( + holder = holder, + coroutine = coroutine, + ) { override fun setMarkerPosition( - markerEntity: MarkerEntity, + markerEntity: MarkerEntity, position: GeoPoint, ) { markerEntity.marker.coordinates = position.toGeoCoordinates() } - override suspend fun addIcons(newMarkers: List>): List { + override suspend fun onAdd(data: List): List { val markers = withContext(coroutine.coroutineContext) { - newMarkers.map { params -> + data.map { params -> val marker = MapMarker( - GeoPoint.from(params.first.position).toGeoCoordinates(), - params.second.toMapImage(), - params.second.toAnchor2D(), + GeoPoint.from(params.state.position).toGeoCoordinates(), + params.bitmapIcon.toMapImage(), + params.bitmapIcon.toAnchor2D(), ).apply { - drawOrder = calculateZIndex(params.first.position).toInt() + drawOrder = calculateZIndex(params.state.position).toInt() metadata = Metadata().apply { - setString("id", params.first.id) + setString("id", params.state.id) } } return@map marker @@ -78,27 +60,33 @@ class HereMapMarkerRenderer( return markers } - override suspend fun removeIcons(removeEntities: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { - val markers: List = removeEntities.map { params -> params.marker } + val markers: List = data.map { params -> params.marker } holder.map.removeMapMarkers(markers) } } - override suspend fun changeIcons(changes: List>): List = + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + changes: List>, + ): List = changes.map { params -> - val prevFinger = params.prevEntity.fingerPrint - val currFinger = params.entity.fingerPrint + val prevFinger = params.prev.fingerPrint + val currFinger = params.current.fingerPrint if (currFinger.icon != prevFinger.icon) { - params.entity.marker.image = params.bitmapIcon.toMapImage() - params.entity.marker.anchor = params.bitmapIcon.toAnchor2D() + params.current.marker.image = params.bitmapIcon.toMapImage() + params.current.marker.anchor = params.bitmapIcon.toAnchor2D() } - if (params.entity.state.position != params.prevEntity.state.position) { - params.entity.marker.coordinates = - GeoPoint.from(params.entity.state.position).toGeoCoordinates() + if (params.current.state.position != params.prev.state.position) { + params.current.marker.coordinates = + GeoPoint.from(params.current.state.position).toGeoCoordinates() } // Hereはマーカーを再作成しなくてよいので、同じマーカーのインスタンスを返す - params.entity.marker + params.current.marker } } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt new file mode 100644 index 00000000..acb1b663 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt @@ -0,0 +1,56 @@ +package com.mapconductor.here.marker + +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.AbstractMarkerController +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.here.HereMapActualMarker +import com.mapconductor.settings.Settings + +class HereMarkerController( + markerManager: MarkerManager, + override val renderer: HereMapMarkerRenderer, +) : AbstractMarkerController( + markerManager = markerManager, + renderer = renderer, + ) { + private var internalSelectedMarker: MarkerEntity? = null + + internal var selectedMarker: MarkerEntity? + set(value) { + if (value == null) { + internalSelectedMarker?.let { + // Restore the recomposition for the position property + setDraggingState(it.state, false) + } + return + } + internalSelectedMarker = value + // Suppress the recomposition for the position property + setDraggingState(value.state, true) + } + get() = internalSelectedMarker + + companion object { + private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 + } + + override fun find(position: IGeoPoint): MarkerEntity? { + return markerManager.findNearest(position)?.let { nearest -> + val zoom = renderer.holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + val meterInMapPixel = renderer.zoomToMetersPerPixel(zoom) + val radius = tolerance * meterInMapPixel + val distance = haversineDistance(position, nearest.state.position) + return if (distance <= radius) { + nearest + } else { + null + } + } + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index 43721569..79f6aed2 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -7,29 +7,32 @@ import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import com.mapbox.maps.MapInitOptions import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.groundimage.OnGroundImageEventHandler +import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler +import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.projection.WebMercator +import com.mapconductor.mapbox.marker.MapboxMarkerController +import com.mapconductor.mapbox.marker.MapboxMarkerRenderer import android.app.Activity import android.content.Context import android.content.ContextWrapper @Composable fun MapboxMapView( - state: IMapboxMapViewState, + state: MapboxViewState, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler? = {}, - onMarkerClick: OnMarkerEventHandler? = {}, - onMarkerDragStart: OnMarkerEventHandler? = {}, - onMarkerDrag: OnMarkerEventHandler? = {}, - onMarkerDragEnd: OnMarkerEventHandler? = {}, - onMarkerAnimateStart: OnMarkerEventHandler? = {}, - onMarkerAnimateEnd: OnMarkerEventHandler? = {}, - onCircleClick: OnCircleEventHandler? = {}, - onPolylineClick: OnPolylineEventHandler? = {}, - onGroundImageClick: OnGroundImageEventHandler? = null, + onMapClick: OnMapEventHandler? = null, + onMarkerClick: OnMarkerEventHandler? = null, + onMarkerDragStart: OnMarkerEventHandler? = null, + onMarkerDrag: OnMarkerEventHandler? = null, + onMarkerDragEnd: OnMarkerEventHandler? = null, + onMarkerAnimateStart: OnMarkerEventHandler? = null, + onMarkerAnimateEnd: OnMarkerEventHandler? = null, + onCircleClick: OnCircleEventHandler? = null, + onPolylineClick: OnPolylineEventHandler? = null, content: (@Composable MapboxMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -62,26 +65,26 @@ fun MapboxMapView( ) val holder = MapboxMapViewHolderImpl.create(context, mapInitOptions) + val markerController = getMarkerController(holder) val controller = MapboxMapViewController( holder = holder, + markerController = markerController, ) - (state as? MapboxMapViewState)?.let { mapViewState -> + (state as? MapboxViewStateImpl)?.let { mapViewState -> mapViewState.controller = controller controller.setCameraMoveListener(mapViewState::onCameraChange) } controller.setMapClickListener(onMapClick) - controller.setMarkerClickListener(onMarkerClick) - controller.setMarkerDragStartListener(onMarkerDragStart) - controller.setMarkerDragListener(onMarkerDrag) - controller.setMarkerDragEndListener(onMarkerDragEnd) controller.setCircleClickListener(onCircleClick) controller.setPolylineClickListener(onPolylineClick) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) - controller.setOnMarkerAnimationStart(onMarkerAnimateStart) - controller.setOnMarkerAnimationEnd(onMarkerAnimateEnd) + controller.setOnMarkerClickListener(onMarkerClick) + controller.setOnMarkerDragStart(onMarkerDragStart) + controller.setOnMarkerDrag(onMarkerDrag) + controller.setOnMarkerDragEnd(onMarkerDragEnd) + controller.setOnMarkerAnimateStart(onMarkerAnimateStart) + controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) holderRef.value = holder controllerRef.value = controller @@ -94,6 +97,26 @@ fun MapboxMapView( ) } +internal fun getMarkerController(holder: MapboxMapViewHolder): MapboxMarkerController { + val hexGeocell = + HexGeocell( + projection = WebMercator, + baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 + ) + val manager = MarkerManager(hexGeocell) + + val renderer = + MapboxMarkerRenderer( + holder = holder, + markerManager = manager, + ) + val controller = + MapboxMarkerController( + renderer = renderer, + ) + return controller +} + internal fun Context.findActivity(): Activity? = when (this) { is Activity -> this diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index bae037c1..06a10aa5 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -2,7 +2,6 @@ package com.mapconductor.mapbox import androidx.compose.ui.geometry.Offset import com.mapbox.android.gestures.MoveGestureDetector -import com.mapbox.geojson.Feature import com.mapbox.geojson.Point import com.mapbox.maps.CameraChanged import com.mapbox.maps.CameraChangedCallback @@ -22,38 +21,31 @@ import com.mapbox.maps.plugin.gestures.addOnMoveListener import com.mapbox.maps.plugin.gestures.removeOnMapClickListener import com.mapbox.maps.plugin.gestures.removeOnMapLongClickListener import com.mapbox.maps.plugin.gestures.removeOnMoveListener -import com.mapconductor.core.ResourceProvider import com.mapconductor.core.circle.CircleClickEvent import com.mapconductor.core.circle.CircleOverlayManager import com.mapconductor.core.circle.CircleRendererFactory import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController -import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerRendererFactory +import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRenderer import com.mapconductor.core.polyline.PolylineOverlayManager import com.mapconductor.core.polyline.PolylineRendererFactory import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.projection.WebMercator import com.mapconductor.mapbox.circle.DefaultMapboxCircleRenderer import com.mapconductor.mapbox.circle.MapboxCircleLayer import com.mapconductor.mapbox.circle.MapboxCircleRenderer -import com.mapconductor.mapbox.marker.DefaultMapboxMarkerRenderer -import com.mapconductor.mapbox.marker.MapboxMarkerRenderer -import com.mapconductor.mapbox.marker.MarkerDragLayer -import com.mapconductor.mapbox.marker.MarkerLayer +import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.polygon.MapboxPolygonLayer import com.mapconductor.mapbox.polygon.MapboxPolygonRenderer import com.mapconductor.mapbox.polyline.DefaultMapboxPolylineRenderer import com.mapconductor.mapbox.polyline.MapboxPolylineLayer import com.mapconductor.mapbox.polyline.MapboxPolylineRenderer -import com.mapconductor.settings.Settings import android.animation.Animator import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -61,11 +53,11 @@ import kotlinx.coroutines.launch interface IMapboxMapViewController : MapViewController< - MapboxActualMarker, MapboxActualCircle, MapboxActualPolyline, MapboxActualPolygon, - > { + >, + MarkerCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -80,26 +72,10 @@ interface IMapboxMapViewController : internal class MapboxMapViewController( override val holder: MapboxMapViewHolder, + private val markerController: MapboxMarkerController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - // Web Mercator投影法に適したbaseHexSideLengthを使用 - override val hexGeocell: HexGeocell = - HexGeocell( - projection = WebMercator, - baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 - ), - private val markerRendererFactory: MarkerRendererFactory = DefaultMapboxMarkerRenderer(), private val polylineRendererFactory: PolylineRendererFactory = DefaultMapboxPolylineRenderer(), - private val markerLayer: MarkerLayer = - MarkerLayer( - sourceId = "markers-source", - layerId = "markers-layer", - ), - private val dragLayer: MarkerDragLayer = - MarkerDragLayer( - sourceId = "marker-drag-source", - layerId = "marker-drag-layer", - ), private val circleLayer: MapboxCircleLayer = MapboxCircleLayer( sourceId = "circle-source", @@ -118,7 +94,6 @@ internal class MapboxMapViewController( private val circleRendererFactory: CircleRendererFactory = DefaultMapboxCircleRenderer(), ) : BaseMapViewController< - MapboxActualMarker, MapboxActualCircle, MapboxActualPolyline, MapboxActualPolygon, @@ -132,24 +107,6 @@ internal class MapboxMapViewController( private const val ZOOM_ADJUST_VALUE = 1.0 } - override val markerRenderer: MapboxMarkerRenderer = - MapboxMarkerRenderer( - holder = holder, - coroutine = coroutine, - markerLayer = markerLayer, - dragLayer = dragLayer, - ) - - override fun createMarkerOverlayManager(): MarkerOverlayManager = - markerRendererFactory.create( - hexGeocell = hexGeocell, - onIconAdd = markerRenderer::addIcons, - onIconRemove = markerRenderer::removeIcons, - onIconChange = markerRenderer::changeIcons, - onPostProcess = markerRenderer::redraw, - onAnimate = markerRenderer::animate, - ) - override fun createPolylineOverlayManager(): PolylineOverlayManager = polylineRendererFactory.create( onAdd = polylineRenderer::addPolylines, @@ -170,19 +127,6 @@ internal class MapboxMapViewController( onPostProcess = circleRenderer::redraw, ) - override fun onMarkerOverlayManagerInitialized(overlayManager: MarkerOverlayManager) { - holder.map.getStyle { style -> - style.addSource(circleLayer.source) - style.addLayer(circleLayer.layer) - style.addSource(polylineLayer.source) - style.addLayer(polylineLayer.layer) - style.addSource(markerLayer.source) - style.addLayer(markerLayer.layer) - style.addSource(dragLayer.source) - style.addLayer(dragLayer.layer) - } - } - override val polylineRenderer: MapboxPolylineRenderer = MapboxPolylineRenderer( holder = holder, @@ -213,6 +157,16 @@ internal class MapboxMapViewController( } init { + holder.map.getStyle { style -> + style.addSource(circleLayer.source) + style.addLayer(circleLayer.layer) + style.addSource(polylineLayer.source) + style.addLayer(polylineLayer.layer) + style.addSource(markerController.renderer.markerLayer.source) + style.addLayer(markerController.renderer.markerLayer.layer) + style.addSource(markerController.renderer.dragLayer.source) + style.addLayer(markerController.renderer.dragLayer.layer) + } setupListeners() } @@ -229,13 +183,13 @@ internal class MapboxMapViewController( } override suspend fun clearOverlays() { - markerOverlayManager.clearOverlays() + markerController.clear() polylineOverlayManager.clearOverlays() } - override suspend fun addMarkers(markerList: List) = markerOverlayManager.addMarkers(markerList) + override suspend fun compositionMarkers(data: List) = markerController.add(data) - override suspend fun updateMarker(state: MarkerState) = markerOverlayManager.updateMarker(state) + override suspend fun updateMarker(state: MarkerState) = markerController.update(state) override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) @@ -329,41 +283,27 @@ internal class MapboxMapViewController( } override fun onMapLongClick(point: Point): Boolean { - val geoPoint = point.toGeoPoint() - val entity = - this.markerRenderer.findNearestMarker( - position = geoPoint, - tolerance = ResourceProvider.dpToPx(Settings.Default.tapTolerance), - zoom = holder.map.cameraState.zoom, - ) - if (entity != null) { - markerRenderer.setDraggingState(entity.state, true) // Suppress the recomposition for the position property - markerOverlayManager.markerManager.removeEntity(entity.state.id) - dragLayer.selected = entity - dragLayer.updatePosition(geoPoint) - markerRenderer.redraw() - markerRenderer.drawDragLayer() - - markerDragStartCallback?.invoke(entity.state) - return true + val touchPosition = point.toGeoPoint() + markerController.find(touchPosition)?.let { entity -> + if (entity.state.draggable) { + markerController.selectedMarker = entity + markerController.markerManager.removeEntity(entity.state.id) + markerController.dragStartListener?.invoke(entity.state) + return true + } } - mapLongClickCallback?.invoke(geoPoint) + mapLongClickCallback?.invoke(touchPosition) return true } override fun onMapClick(point: Point): Boolean { val touchPosition = point.toGeoPoint() - this.markerRenderer - .findNearestMarker( - position = touchPosition, - tolerance = ResourceProvider.dpToPx(Settings.Default.tapTolerance), - zoom = holder.map.cameraState.zoom, - )?.let { - markerClickCallback?.invoke(it.state) - return true - } + markerController.find(touchPosition)?.let { entity -> + markerController.clickListener?.invoke(entity.state) + return true + } val circleEntity = this.circleOverlayManager.find(touchPosition) circleEntity?.let { @@ -389,7 +329,7 @@ internal class MapboxMapViewController( // } override fun onMove(detector: MoveGestureDetector): Boolean { - dragLayer.selected?.let { entity -> + markerController.renderer.dragLayer.selected?.let { entity -> val screenCoordinate = Offset( @@ -399,11 +339,11 @@ internal class MapboxMapViewController( holder.fromScreenOffsetSync(screenCoordinate)?.let { entity.state.position = it - dragLayer.updatePosition(it) - markerRenderer.drawDragLayer() + markerController.renderer.dragLayer.updatePosition(it) + markerController.renderer.drawDragLayer() } - markerDragCallback?.invoke(entity.state) + markerController.dragListener?.invoke(entity.state) return true } return false @@ -414,20 +354,40 @@ internal class MapboxMapViewController( } override fun onMoveEnd(detector: MoveGestureDetector) { - dragLayer.selected?.let { entity -> + markerController.selectedMarker?.let { entity -> val screenCoordinate = ScreenCoordinate( detector.focalPoint.x.toDouble(), detector.focalPoint.y.toDouble(), ) val point = holder.map.coordinateForPixel(screenCoordinate) - dragLayer.updatePosition(point.toGeoPoint()) - dragLayer.selected = null - markerRenderer.drawDragLayer() - markerRenderer.setDraggingState(entity.state, false) // Restore the recomposition for the position property - markerOverlayManager.markerManager.registerEntity(entity) - markerRenderer.redraw() - markerDragEndCallback?.invoke(entity.state) + markerController.renderer.dragLayer.updatePosition(point.toGeoPoint()) + markerController.selectedMarker = null + markerController.dragEndListener?.invoke(entity.state) } } + + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { + markerController.dragStartListener = listener + } + + override fun setOnMarkerDrag(listener: OnMarkerEventHandler?) { + markerController.dragListener = listener + } + + override fun setOnMarkerDragEnd(listener: OnMarkerEventHandler?) { + markerController.dragEndListener = listener + } + + override fun setOnMarkerAnimateStart(listener: OnMarkerEventHandler?) { + markerController.renderer.animateStartListener = listener + } + + override fun setOnMarkerAnimateEnd(listener: OnMarkerEventHandler?) { + markerController.renderer.animateEndListener = listener + } + + override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { + markerController.clickListener = listener + } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt similarity index 88% rename from mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt rename to mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt index 4a893721..cda6bb27 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewState.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt @@ -18,14 +18,14 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface IMapboxMapViewState : MapViewState +interface MapboxViewState : MapViewState -class MapboxMapViewState( +class MapboxViewStateImpl( override val id: String, override val mapDesignType: MapboxDesignType, override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, ) : MapViewStateImpl(), - IMapboxMapViewState { + MapboxViewState { internal var controller: IMapboxMapViewController? = null // Camera center position @@ -77,11 +77,11 @@ class MapboxMapViewState( } } -class MapboxMapViewSaver : BaseMapViewSaver() { - override fun extractCameraPosition(state: MapboxMapViewState): MapCameraPosition? = state.cameraPosition.value +class MapboxMapViewSaver : BaseMapViewSaver() { + override fun extractCameraPosition(state: MapboxViewStateImpl): MapCameraPosition? = state.cameraPosition.value override fun saveMapDesign( - state: MapboxMapViewState, + state: MapboxViewStateImpl, bundle: Bundle, ) { bundle.putString("id", state.mapDesignType.id) @@ -91,8 +91,8 @@ class MapboxMapViewSaver : BaseMapViewSaver() { stateId: String, mapDesignBundle: Bundle?, cameraPosition: MapCameraPosition, - ): MapboxMapViewState = - MapboxMapViewState( + ): MapboxViewStateImpl = + MapboxViewStateImpl( id = stateId, mapDesignType = MapboxMapDesign.Create( @@ -101,14 +101,14 @@ class MapboxMapViewSaver : BaseMapViewSaver() { initCameraPosition = cameraPosition, ) - override fun getStateId(state: MapboxMapViewState): String = state.id + override fun getStateId(state: MapboxViewStateImpl): String = state.id } @Composable fun rememberMapboxMapViewState( mapDesign: MapboxDesignType = Standard, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, -): MapboxMapViewState { +): MapboxViewStateImpl { val stateId by rememberSaveable { val uuid = UUID.randomUUID().toString() mutableStateOf(uuid) @@ -118,7 +118,7 @@ fun rememberMapboxMapViewState( stateSaver = MapboxMapViewSaver().createSaver(), ) { mutableStateOf( - MapboxMapViewState( + MapboxViewStateImpl( id = stateId, mapDesignType = mapDesign, initCameraPosition = MapCameraPosition.from(cameraPosition), diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt new file mode 100644 index 00000000..e8ba5ebe --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt @@ -0,0 +1,60 @@ +package com.mapconductor.mapbox.marker + +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.marker.AbstractMarkerController +import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.core.spherical.haversineDistance +import com.mapconductor.mapbox.MapboxActualMarker +import com.mapconductor.settings.Settings + +class MapboxMarkerController( + override val renderer: MapboxMarkerRenderer, +) : AbstractMarkerController( + markerManager = renderer.markerManager, + renderer = renderer, + ) { + private var internalSelectedMarker: MarkerEntity? = null + + internal var selectedMarker: MarkerEntity? + set(value) { + if (value == null) { + internalSelectedMarker?.let { + renderer.dragLayer.updatePosition(GeoPoint.from(it.state.position)) + // Restore the recomposition for the position property + setDraggingState(it.state, false) + renderer.drawDragLayer() + markerManager.registerEntity(it) + renderer.redraw() + } + return + } + internalSelectedMarker = value + markerManager.removeEntity(value.state.id) + // Suppress the recomposition for the position property + setDraggingState(value.state, true) + renderer.dragLayer.selected = value + renderer.dragLayer.updatePosition(GeoPoint.from(value.state.position)) + renderer.redraw() + renderer.drawDragLayer() + } + get() = internalSelectedMarker + + override fun find(position: IGeoPoint): MarkerEntity? { + return markerManager.findNearest(position)?.let { nearest -> + val zoom = renderer.holder.map.cameraState.zoom + val tolerance = + Settings.Default.tapTolerance.value + .toDouble() * ResourceProvider.getDensity() + val meterInMapPixel = renderer.zoomToMetersPerPixel(zoom) + val radius = tolerance * meterInMapPixel + val distance = haversineDistance(position, nearest.state.position) + return if (distance <= radius) { + nearest + } else { + null + } + } + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt index 5ac85b38..f71cc1ee 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt @@ -6,50 +6,44 @@ import com.mapbox.geojson.Feature import com.mapbox.geojson.FeatureCollection import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.marker.AbstractMarkerRenderer +import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer import com.mapconductor.core.marker.BitmapIcon +import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerEntity import com.mapconductor.core.marker.MarkerIcon import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.marker.MarkerOverlayManager -import com.mapconductor.core.marker.MarkerOverlayManagerImpl -import com.mapconductor.core.marker.MarkerRenderer.UpdateParams -import com.mapconductor.core.marker.MarkerRendererFactory -import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.marker.MarkerOverlayRenderer import com.mapconductor.mapbox.MapboxActualMarker import com.mapconductor.mapbox.MapboxMapViewHolder import com.mapconductor.mapbox.toPoint import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class DefaultMapboxMarkerRenderer : MarkerRendererFactory { - override fun create( - hexGeocell: HexGeocell, - onIconAdd: suspend (List>) -> List, - onIconRemove: suspend (List>) -> Unit, - onIconChange: suspend (List>) -> List, - onAnimate: suspend (MarkerEntity) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): MarkerOverlayManager = - MarkerOverlayManagerImpl( - markerManager = MarkerManager(hexGeocell), - onRemove = onIconRemove, - onAdd = onIconAdd, - onChange = onIconChange, - onPostProcess = onPostProcess, - onAnimate = onAnimate, - ) -} - class MapboxMarkerRenderer( - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope, - private val markerLayer: MarkerLayer, - private val dragLayer: MarkerDragLayer, -) : AbstractMarkerRenderer() { + holder: MapboxMapViewHolder, + coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), + val markerManager: MarkerManager, + val markerLayer: MarkerLayer = + MarkerLayer( + sourceId = "markers-source", + layerId = "markers-layer", + ), + val dragLayer: MarkerDragLayer = + MarkerDragLayer( + sourceId = "marker-drag-source", + layerId = "marker-drag-layer", + ), +) : AbstractMarkerOverlayRenderer< + MapboxMapViewHolder, + MapboxActualMarker, + >( + holder = holder, + coroutine = coroutine, + ) { private val iconRefCounter: MutableMap = mutableMapOf() + private val defaultIcon: BitmapIcon = DefaultIcon().toBitmapIcon() object Prop { const val ICON_ID = "icon_id" @@ -58,15 +52,14 @@ class MapboxMarkerRenderer( const val ICON_ANCHOR = "icon-offset" } - override fun init(markerOverlayManager: MarkerOverlayManager) { - super.init(markerOverlayManager) + init { holder.map.getStyle { style -> style.addImage(Prop.DEFAULT_MARKER_ID, defaultIcon.bitmap) } } fun redraw() { - val entities = markerOverlayManager.markerManager.allEntities() + val entities = markerManager.allEntities() coroutine.launch { markerLayer.draw(entities) } @@ -82,7 +75,7 @@ class MapboxMarkerRenderer( markerEntity: MarkerEntity, position: GeoPoint, ) { - val entities = markerOverlayManager.markerManager.allEntities() + val entities = markerManager.allEntities() val feature = Feature.fromGeometry( position.toPoint(), @@ -105,7 +98,7 @@ class MapboxMarkerRenderer( } } - override suspend fun addIcons(newMarkers: List>): List { + override suspend fun onAdd(data: List): List { val style = suspendCoroutine { continuation -> holder.map.getStyle { style -> @@ -113,21 +106,24 @@ class MapboxMarkerRenderer( } } - newMarkers.forEach { (state, bitmapIcon) -> - val iconKey = state.icon.hashCode().toString() + data.forEach { + val iconKey = + it.state.icon + .hashCode() + .toString() if (!iconRefCounter.contains(iconKey)) { - style.addImage(iconKey, bitmapIcon.bitmap) + style.addImage(iconKey, it.bitmapIcon.bitmap) iconRefCounter[iconKey] = 0 } } - return newMarkers.map { (state, _) -> - val featureId = "marker-${state.id}" - val position = GeoPoint.from(state.position).toPoint() + return data.map { + val featureId = "marker-${it.state.id}" + val position = GeoPoint.from(it.state.position).toPoint() val properties = JsonObject().apply { - if (state.icon != null) { - state.icon?.let { icon -> + if (it.state.icon != null) { + it.state.icon?.let { icon -> val iconKey = icon.hashCode().toString() iconRefCounter[iconKey] = iconRefCounter.getOrDefault(iconKey, 0) + 1 addProperty(Prop.ICON_ID, iconKey) @@ -138,14 +134,14 @@ class MapboxMarkerRenderer( addProperty(Prop.ICON_ID, Prop.DEFAULT_MARKER_ID) add(Prop.ICON_ANCHOR, getDefaultIconOffsetProperty()) } - addProperty(Prop.SCALE, state.icon?.scale ?: 1.0) + addProperty(Prop.SCALE, it.state.icon?.scale ?: 1.0) } Feature.fromGeometry(position, properties, featureId) } } - override suspend fun removeIcons(removeEntities: List>) { - removeEntities.forEach { entity -> + override suspend fun onRemove(data: List>) { + data.forEach { entity -> entity.state.icon?.let { icon -> val iconKey = icon.hashCode().toString() val cnt = iconRefCounter.getOrDefault(iconKey, 1) - 1 @@ -159,17 +155,23 @@ class MapboxMarkerRenderer( } } - override suspend fun changeIcons(changes: List>): List = - changes.map { params -> - val prevFinger = params.prevEntity.fingerPrint - val currFinger = params.entity.fingerPrint - val prevProperties = params.prevEntity.marker.properties() + override suspend fun onPostProcess() { + redraw() + } + + override suspend fun onChange( + data: List>, + ): List = + data.map { params -> + val prevFinger = params.prev.fingerPrint + val currFinger = params.current.fingerPrint + val prevProperties = params.prev.marker.properties() val properties = JsonObject().apply { addProperty( Prop.SCALE, - params.entity.state.icon + params.current.state.icon ?.scale ?: 1.0f, ) if (currFinger.icon == prevFinger.icon) { @@ -196,7 +198,7 @@ class MapboxMarkerRenderer( addProperty(Prop.ICON_ID, Prop.DEFAULT_MARKER_ID) add(Prop.ICON_ANCHOR, getDefaultIconOffsetProperty()) } else { - params.entity.state.icon?.let { icon -> + params.current.state.icon?.let { icon -> // icon id val iconKey = icon.hashCode().toString() if (iconRefCounter.contains(iconKey)) { @@ -213,8 +215,8 @@ class MapboxMarkerRenderer( } val position = - GeoPoint.from(params.entity.state.position).toPoint() - val featureId = "marker-${params.entity.state.id}" + GeoPoint.from(params.current.state.position).toPoint() + val featureId = "marker-${params.current.state.id}" Feature.fromGeometry(position, properties, featureId) } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerDragLayer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerDragLayer.kt index 7b09cd89..5badc84b 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerDragLayer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerDragLayer.kt @@ -4,13 +4,14 @@ import com.mapbox.geojson.Feature import com.mapbox.geojson.FeatureCollection import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.marker.MarkerEntity +import com.mapconductor.mapbox.MapboxActualMarker import com.mapconductor.mapbox.toPoint class MarkerDragLayer( sourceId: String, layerId: String, ) : MarkerLayer(sourceId, layerId) { - var selected: MarkerEntity? = null + var selected: MarkerEntity? = null fun updatePosition(geoPoint: GeoPoint) { selected?.let { @@ -28,8 +29,8 @@ class MarkerDragLayer( it.state.id, ) it.marker = feature - listOf(feature) - } ?: emptyList() + listOf(feature) + } ?: emptyList() source.featureCollection( FeatureCollection.fromFeatures(features), From 3857074b0ed4511818484cf1f793decb8f03efde Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 22 Aug 2025 03:51:14 +0900 Subject: [PATCH 24/44] Refactor polyline architecture to abstract controller pattern - Create AbstractPolylineController.kt with unified controller logic - Create AbstractPolylineOverlayRenderer.kt as base renderer class - Implement provider-specific controllers for Google Maps, Mapbox, HERE, and ArcGIS - Replace overlay manager pattern with controller architecture - Remove deprecated polyline renderer implementations - Update all MapViewController classes to use new controller pattern --- .../com/mapconductor/example/MainActivity.kt | 2 +- .../com/mapconductor/core/OverlayProvider.kt | 12 +- .../core/controller/BaseMapViewController.kt | 24 +- .../core/controller/MapViewController.kt | 14 +- .../com/mapconductor/core/map/MapViewBase.kt | 3 +- .../polyline/AbstractPolylineController.kt | 206 ++++++++++++++++++ .../AbstractPolylineOverlayRenderer.kt | 45 ++++ .../core/polyline/PolylineOverlay.kt | 16 ++ .../com/mapconductor/arcgis/ArcGISMapView.kt | 2 +- .../arcgis/ArcGISMapViewController.kt | 49 ++--- .../arcgis/ArcGISViewControllerStore.kt | 22 ++ .../polyline/ArcGISPolylineController.kt | 115 ++++++++++ .../arcgis/polyline/ArcGISPolylineRenderer.kt | 130 ----------- .../mapconductor/googlemaps/GoogleMapView.kt | 2 +- .../googlemaps/GoogleMapViewController.kt | 43 ++-- .../GoogleMapViewControllerStore.kt | 16 ++ .../polyline/GoogleMapPolylineController.kt | 80 +++++++ .../polyline/GoogleMapPolylineRenderer.kt | 85 -------- .../java/com/mapconductor/here/HereMapView.kt | 2 +- .../here/HereMapViewController.kt | 44 ++-- .../here/HereMapViewControllerStore.kt | 19 +- .../here/polyline/HereMapPolylineRenderer.kt | 115 ---------- .../here/polyline/HerePolylineController.kt | 107 +++++++++ .../com/mapconductor/mapbox/MapboxMapView.kt | 32 ++- .../mapbox/MapboxMapViewController.kt | 53 ++--- ...enderer.kt => MapboxPolylineController.kt} | 184 ++++++++-------- 26 files changed, 812 insertions(+), 610 deletions(-) create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineOverlayRenderer.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt create mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt delete mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineRenderer.kt create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt delete mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineRenderer.kt delete mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HereMapPolylineRenderer.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt rename mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/{MapboxPolylineRenderer.kt => MapboxPolylineController.kt} (50%) diff --git a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt index 71aca370..d1271a7e 100644 --- a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt +++ b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt @@ -12,7 +12,7 @@ class MainActivity : ComponentActivity() { setContent { DemoAppScreen( - initPage = "circle", + initPage = "polyline", ) } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index 1bb9ebb3..57b04ae6 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -13,6 +13,7 @@ import com.mapconductor.core.map.MapOverlayRegistry import com.mapconductor.core.marker.MarkerOverlay import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.PolylineOverlay import com.mapconductor.core.polyline.PolylineState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -96,17 +97,6 @@ val LocalMarkerCollector = error("Marker must be under the ") } -class PolylineOverlay( - override val flow: StateFlow>, -) : MapOverlay { - override suspend fun render( - data: List, - controller: MapViewControllerAlias, - ) { - controller.addPolylines(data) - } -} - val LocalPolylineCollector = compositionLocalOf>> { error("Polyline must be under the ") diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt index e03488ae..d67e9b5e 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt @@ -7,21 +7,8 @@ import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRenderer -import com.mapconductor.core.polyline.OnPolylineEventHandler -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineRenderer - -abstract class BaseMapViewController : - MapViewController { - abstract val polylineRenderer: PolylineRenderer - - override val polylineOverlayManager: PolylineOverlayManager by lazy { - createPolylineOverlayManager().also { overlayManager -> - polylineRenderer.init(overlayManager) - onPolylineOverlayManagerInitialized(overlayManager) - } - } +abstract class BaseMapViewController : MapViewController { abstract val polygonRenderer: PolygonRenderer override val polygonOverlayManager: PolygonOverlayManager by lazy { @@ -40,14 +27,10 @@ abstract class BaseMapViewController) - protected abstract fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) protected abstract fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) - protected abstract fun createPolylineOverlayManager(): PolylineOverlayManager - protected abstract fun createPolygonOverlayManager(): PolygonOverlayManager protected abstract fun createCircleOverlayManager(): CircleOverlayManager @@ -56,7 +39,6 @@ abstract class BaseMapViewController { +interface MapViewController { val holder: MapViewHolder<*, *> val coroutine: CoroutineScope - val polylineOverlayManager: PolylineOverlayManager val circleOverlayManager: CircleOverlayManager val polygonOverlayManager: PolygonOverlayManager - suspend fun addPolylines(data: List) - - suspend fun updatePolyline(state: PolylineState) - suspend fun addCircles(data: List) suspend fun updateCircle(state: CircleState) @@ -36,8 +28,6 @@ interface MapViewController { fun setMapLongClickListener(listener: OnMapEventHandler?) fun setCircleClickListener(listener: OnCircleEventHandler?) - - fun setPolylineClickListener(listener: OnPolylineEventHandler?) } -typealias MapViewControllerAlias = MapViewController<*, *, *> +typealias MapViewControllerAlias = MapViewController<*, *> diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index 395c74b2..e630d3fb 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -35,6 +35,7 @@ import com.mapconductor.core.info.InfoBubbleOverlay import com.mapconductor.core.info.LocalInfoBubbleCollector import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.settings.Settings import android.view.View import android.view.ViewGroup @@ -115,7 +116,7 @@ fun < polylines.value.forEach { polylineState -> LaunchedEffect(polylineState.id) { polylineState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updatePolyline(polylineState) + (controller as? PolylineCapable)?.updatePolyline(polylineState) } } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt new file mode 100644 index 00000000..e96ff3ee --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt @@ -0,0 +1,206 @@ +package com.mapconductor.core.polyline + +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.features.IGeoPoint +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +interface PolylineCapable { + suspend fun compositionPolylines(data: List) + + suspend fun updatePolyline(state: PolylineState) + + fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) +} + +interface PolylineOverlayRenderer { + interface AddParams { + val state: PolylineState + } + + interface ChangeParams { + val current: PolylineEntity + val prev: PolylineEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onPostProcess() +} + +abstract class PolylineController( + val polylineManager: PolylineManager, + open val renderer: PolylineOverlayRenderer, + override var clickListener: OnPolylineEventHandler? = null, +) : OverlayController< + ActualPolyline, + PolylineState, + PolylineEntity, + PolylineState, + > { + override val zIndex: Int = 5 + val semaphore = Semaphore(1) + + override suspend fun add(data: List) { + semaphore.withPermit { + val modifiedEntities = mutableListOf>() + val previous = polylineManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() + val removed = mutableListOf>() + + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = polylineManager.getEntity(state.id)!! + updated.add( + object : PolylineOverlayRenderer.ChangeParams { + override val current: PolylineEntity = + PolylineEntityImpl( + state = state, + polyline = prevEntity.polyline, + ) + override val prev: PolylineEntity = prevEntity + }, + ) + previous.remove(state.id) + } else { + added.add( + object : PolylineOverlayRenderer.AddParams { + override val state: PolylineState = state + }, + ) + previous.remove(state.id) + } + } + + previous.forEach { remainId -> + polylineManager.removeEntity(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + // Remove polylines + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + // Add new polylines + if (added.isNotEmpty()) { + val actualPolylines: List = renderer.onAdd(added) + actualPolylines.forEachIndexed { index, actualPolyline -> + actualPolyline?.let { + val entity = + PolylineEntityImpl( + polyline = actualPolyline, + state = added[index].state, + ) + polylineManager.registerEntity(entity) + modifiedEntities.add(entity) + } + } + } + + // Update changed polylines + if (updated.isNotEmpty()) { + val actualPolylines: List = renderer.onChange(updated) + + actualPolylines.forEachIndexed { index, actualPolyline -> + actualPolyline?.let { + val params = updated[index] + val entity = + PolylineEntityImpl( + state = params.current.state, + polyline = actualPolyline, + ) + polylineManager.registerEntity(entity) + } + } + } + + renderer.onPostProcess() + } + } + + override suspend fun update(state: PolylineState) { + semaphore.withPermit { + val prevEntity = polylineManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinger = prevEntity.fingerPrint + if (currentFinger == prevFinger) { + return + } + + val polyline = prevEntity.polyline + val entity = + PolylineEntityImpl( + polyline = polyline, + state = state, + ) + val polylineParams = + object : PolylineOverlayRenderer.ChangeParams { + override val current: PolylineEntity = entity + override val prev: PolylineEntity = prevEntity + } + val polylines = renderer.onChange(listOf(polylineParams)) + + polylines[0]?.let { + val entity = + PolylineEntityImpl( + polyline = it, + state = state, + ) + polylineManager.registerEntity(entity) + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + val entities: List> = polylineManager.allEntities() + renderer.onRemove(entities) + polylineManager.clear() + } + } + + override fun find(position: IGeoPoint): PolylineEntity? = polylineManager.findNearest(position) +} + +interface PolylineManager { + fun registerEntity(entity: PolylineEntity) + + fun removeEntity(id: String): PolylineEntity? + + fun getEntity(id: String): PolylineEntity? + + fun allEntities(): List> + + fun clear() + + fun findNearest(position: IGeoPoint): PolylineEntity? +} + +class PolylineManagerImpl : PolylineManager { + private val entities = mutableMapOf>() + + override fun registerEntity(entity: PolylineEntity) { + entities[entity.state.id] = entity + } + + override fun removeEntity(id: String): PolylineEntity? = entities.remove(id) + + override fun getEntity(id: String): PolylineEntity? = entities[id] + + override fun allEntities(): List> = entities.values.toList() + + override fun clear() { + entities.clear() + } + + override fun findNearest(position: IGeoPoint): PolylineEntity? { + return entities.values.firstOrNull() + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineOverlayRenderer.kt new file mode 100644 index 00000000..a55188af --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineOverlayRenderer.kt @@ -0,0 +1,45 @@ +package com.mapconductor.core.polyline + +import com.mapconductor.core.map.MapViewHolder +import kotlinx.coroutines.CoroutineScope + +abstract class AbstractPolylineOverlayRenderer : PolylineOverlayRenderer { + abstract val holder: MapViewHolder<*, *> + abstract val coroutine: CoroutineScope + + override suspend fun onPostProcess() { + // Default implementation - can be overridden by subclasses + } + + abstract suspend fun createPolyline(state: PolylineState): ActualPolyline? + + abstract suspend fun updatePolylineProperties( + polyline: ActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): ActualPolyline? + + abstract suspend fun removePolyline(entity: PolylineEntity) + + override suspend fun onAdd(data: List): List = + data.map { params -> + createPolyline(params.state) + } + + override suspend fun onChange( + data: List>, + ): List = + data.map { params -> + updatePolylineProperties( + polyline = params.prev.polyline, + current = params.current, + prev = params.prev, + ) + } + + override suspend fun onRemove(data: List>) { + data.forEach { entity -> + removePolyline(entity) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt new file mode 100644 index 00000000..d73b7bd4 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt @@ -0,0 +1,16 @@ +package com.mapconductor.core.polyline + +import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.StateFlow + +class PolylineOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewControllerAlias, + ) { + (controller as? PolylineCapable)?.compositionPolylines(data) + } +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index 7724f066..21b3e420 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -64,7 +64,7 @@ fun ArcGISMapView( controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) controller.setCircleClickListener(onCircleClick) - controller.setPolylineClickListener(onPolylineClick) + controller.setOnPolylineClickListener(onPolylineClick) controller.setOnMarkerClickListener(onMarkerClick) controller.setOnMarkerDragStart(onMarkerDragStart) controller.setOnMarkerDrag(onMarkerDrag) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt index ee4789d2..ef710e42 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt @@ -13,8 +13,7 @@ import com.mapconductor.arcgis.marker.ArcGISMarkerController import com.mapconductor.arcgis.marker.SelectedMarker import com.mapconductor.arcgis.polygon.ArcGISPolygonRenderer import com.mapconductor.arcgis.polygon.DefaultArcGISPolygonRenderer -import com.mapconductor.arcgis.polyline.ArcGISPolylineRenderer -import com.mapconductor.arcgis.polyline.DefaultArcGISPolylineRenderer +import com.mapconductor.arcgis.polyline.ArcGISPolylineController import com.mapconductor.core.circle.CircleClickEvent import com.mapconductor.core.circle.CircleOverlayManager import com.mapconductor.core.circle.CircleRenderer @@ -30,9 +29,8 @@ import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRenderer import com.mapconductor.core.polygon.PolygonRendererFactory -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineRenderer -import com.mapconductor.core.polyline.PolylineRendererFactory +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState import com.mapconductor.settings.Settings import android.view.MotionEvent @@ -43,10 +41,10 @@ import kotlinx.coroutines.launch interface IArcGISMapViewController : MapViewController< ArcGISActualCircle, - ArcGISActualPolyline, ArcGISActualPolygon, >, - MarkerCapable { + MarkerCapable, + PolylineCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -61,21 +59,16 @@ interface IArcGISMapViewController : class ArcGISMapViewController( override val holder: ArcGISMapViewHolder, + private val polylineController: ArcGISPolylineController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), private val circleLayer: GraphicsOverlay = GraphicsOverlay().apply { sceneProperties.surfacePlacement = SurfacePlacement.DrapedFlat }, - private val polylineLayer: GraphicsOverlay = - GraphicsOverlay().apply { - sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded - }, private val polygonLayer: GraphicsOverlay = GraphicsOverlay().apply { sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded }, - private val polylineRendererFactory: PolylineRendererFactory = - DefaultArcGISPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultArcGISPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = @@ -83,24 +76,9 @@ class ArcGISMapViewController( private val markerController: ArcGISMarkerController, ) : BaseMapViewController< ArcGISActualCircle, - ArcGISActualPolyline, ArcGISActualPolygon, >(), IArcGISMapViewController { - override fun createPolylineOverlayManager(): PolylineOverlayManager = - polylineRendererFactory.create( - onAdd = polylineRenderer::addPolylines, - onChange = polylineRenderer::changePolylines, - onRemove = polylineRenderer::removePolylines, - ) - - override val polylineRenderer: PolylineRenderer = - ArcGISPolylineRenderer( - polylineLayer = polylineLayer, - holder = holder, - coroutine = coroutine, - ) - override fun createPolygonOverlayManager(): PolygonOverlayManager = polygonRendererFactory.create( onAdd = polygonRenderer::addPolygons, @@ -135,13 +113,10 @@ class ArcGISMapViewController( override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { } - override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { - } - init { holder.map.graphicsOverlays.clear() holder.map.graphicsOverlays.add(circleLayer) - holder.map.graphicsOverlays.add(polylineLayer) + holder.map.graphicsOverlays.add(polylineController.renderer.polylineLayer) holder.map.graphicsOverlays.add(markerController.renderer.markerLayer) setupListeners() } @@ -271,16 +246,16 @@ class ArcGISMapViewController( override suspend fun clearOverlays() { markerController.clear() - polylineOverlayManager.clearOverlays() + polylineController.clear() } override suspend fun compositionMarkers(data: List) = markerController.add(data) override suspend fun updateMarker(state: MarkerState) = markerController.update(state) - override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) + override suspend fun compositionPolylines(data: List) = polylineController.add(data) - override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) @@ -338,4 +313,8 @@ class ArcGISMapViewController( override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { this.markerController.clickListener = listener } + + override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { + this.polylineController.clickListener = listener + } } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt index 3d178729..36762790 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt @@ -5,6 +5,8 @@ import com.arcgismaps.mapping.view.SceneView import com.arcgismaps.mapping.view.SurfacePlacement import com.mapconductor.arcgis.marker.ArcGISMarkerController import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer +import com.mapconductor.arcgis.polyline.ArcGISPolylineController +import com.mapconductor.arcgis.polyline.ArcGISPolylineOverlayRenderer import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder @@ -36,11 +38,31 @@ object ArcGISViewControllerStore : ArcGISMapViewController( holder = holder, markerController = getMarkerController(holder), + polylineController = getPolylineController(holder), ) this.set(id, controller) return controller } + private fun getPolylineController(holder: ArcGISMapViewHolder): ArcGISPolylineController { + val polylineLayer: GraphicsOverlay = + GraphicsOverlay().apply { + sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded + } + + val renderer = + ArcGISPolylineOverlayRenderer( + polylineLayer = polylineLayer, + holder = holder, + ) + + val controller = + ArcGISPolylineController( + renderer = renderer, + ) + return controller + } + private fun getMarkerController(holder: ArcGISMapViewHolder): ArcGISMarkerController { val hexGeocell = HexGeocell( diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt new file mode 100644 index 00000000..81e00780 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt @@ -0,0 +1,115 @@ +package com.mapconductor.arcgis.polyline + +import com.arcgismaps.geometry.Geometry +import com.arcgismaps.geometry.PolylineBuilder +import com.arcgismaps.mapping.symbology.SimpleLineSymbol +import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.mapconductor.arcgis.ArcGISMapViewHolder +import com.mapconductor.arcgis.toArcGISColor +import com.mapconductor.arcgis.toPoint +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineManagerImpl +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.core.spherical.Spherical +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ArcGISPolylineController( + polylineManager: PolylineManager = PolylineManagerImpl(), + override val renderer: ArcGISPolylineOverlayRenderer, +) : PolylineController(polylineManager, renderer) + +class ArcGISPolylineOverlayRenderer( + val polylineLayer: GraphicsOverlay, + override val holder: ArcGISMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): Graphic? = + withContext(coroutine.coroutineContext) { + val geometry = createGeometry(state) + + val lineSymbol = + SimpleLineSymbol().apply { + style = SimpleLineSymbolStyle.Solid + color = state.strokeColor.toArcGISColor() + width = state.strokeWidth.value.toFloat() + } + + val graphic = + Graphic(geometry, lineSymbol).also { + it.attributes.set("id", state.id) + } + + polylineLayer.graphics.add(graphic) + graphic + } + + override suspend fun updatePolylineProperties( + polyline: Graphic, + current: PolylineEntity, + prev: PolylineEntity, + ): Graphic? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { + polyline.geometry = createGeometry(current.state) + } + + (polyline.symbol as SimpleLineSymbol).let { symbol -> + if (finger.strokeColor != prevFinger.strokeColor) { + symbol.color = current.state.strokeColor.toArcGISColor() + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + symbol.width = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + } + } + + polyline + } + + override suspend fun removePolyline(entity: PolylineEntity) { + coroutine.launch { + polylineLayer.graphics.remove(entity.polyline) + } + } + + private fun createGeometry(state: PolylineState): Geometry { + val polylineBuilder = + PolylineBuilder().also { builder -> + if (state.geodesic) { + state.points.forEach { + builder.addPoint(GeoPoint.from(it).toPoint()) + } + return@also + } + + builder.addPoint(GeoPoint.from(state.points[0]).toPoint()) + for (i in 1 until state.points.size) { + var fraction = 0.0 + while (fraction <= 1.0) { + val point = + Spherical.linearInterpolate( + from = state.points[i - 1], + to = state.points[i], + fraction = fraction, + ) + builder.addPoint(point.toPoint()) + fraction += 0.01 + } + builder.addPoint(GeoPoint.from(state.points[i]).toPoint()) + } + } + return polylineBuilder.toGeometry() + } +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineRenderer.kt deleted file mode 100644 index 9091c7eb..00000000 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineRenderer.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.mapconductor.arcgis.polyline - -import com.arcgismaps.geometry.Geometry -import com.arcgismaps.geometry.PolylineBuilder -import com.arcgismaps.mapping.symbology.SimpleLineSymbol -import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle -import com.arcgismaps.mapping.view.Graphic -import com.arcgismaps.mapping.view.GraphicsOverlay -import com.mapconductor.arcgis.ArcGISMapViewHolder -import com.mapconductor.arcgis.toArcGISColor -import com.mapconductor.arcgis.toPoint -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polyline.AbstractPolylineRenderer -import com.mapconductor.core.polyline.PolylineEntity -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineOverlayManagerImpl -import com.mapconductor.core.polyline.PolylineRenderer.UpdateParams -import com.mapconductor.core.polyline.PolylineRendererFactory -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.spherical.Spherical -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultArcGISPolylineRenderer : PolylineRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolylineOverlayManager = - PolylineOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class ArcGISPolylineRenderer( - val polylineLayer: GraphicsOverlay, - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolylineRenderer() { - override suspend fun addPolylines(newLines: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newLines.map { state -> - - val geometry = createGeometry(state) - - val lineSymbol = - SimpleLineSymbol().apply { - style = SimpleLineSymbolStyle.Solid - color = state.strokeColor.toArcGISColor() - width = state.strokeWidth.value.toFloat() // 線の太さ - } - - val graphic = - Graphic(geometry, lineSymbol).also { - it.attributes.set("id", state.id) - } - - polylineLayer.graphics.add(graphic) - - return@map graphic - } - } - } - - override suspend fun removePolylines(removeEntities: List>) { - val polylines = removeEntities.map { it.polyline } - coroutine.launch { - polylineLayer.graphics.removeAll(polylines) - } - } - - override suspend fun changePolylines(changes: List>): List { - return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { - params.entity.polyline.geometry = createGeometry(params.entity.state) - } - - (params.entity.polyline.symbol as SimpleLineSymbol).let { symbol -> - if (finger.strokeColor != prevFinger.strokeColor) { - symbol.color = - params.entity.state.strokeColor - .toArcGISColor() - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - symbol.width = ResourceProvider.dpToPx(params.prevEntity.state.strokeWidth).toFloat() - } - } - return@map params.prevEntity.polyline - } - } - } - - private fun createGeometry(state: PolylineState): Geometry { - val polylineBuilder = - PolylineBuilder().also { builder -> - if (state.geodesic) { - state.points.forEach { - builder.addPoint(GeoPoint.from(it).toPoint()) - } - return@also - } - - builder.addPoint(GeoPoint.from(state.points[0]).toPoint()) - for (i in 1 until state.points.size) { - var fraction = 0.0 - while (fraction <= 1.0) { - val point = - Spherical.linearInterpolate( - from = state.points[i - 1], - to = state.points[i], - fraction = fraction, - ) - builder.addPoint(point.toPoint()) - fraction += 0.01 - } - builder.addPoint(GeoPoint.from(state.points[i]).toPoint()) - } - } - return polylineBuilder.toGeometry() - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index aa90a07f..01ac0c48 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -86,7 +86,7 @@ fun GoogleMapsView( controller.setOnMarkerDrag(onMarkerDrag) controller.setOnMarkerDragEnd(onMarkerDragEnd) controller.setCircleClickListener(onCircleClick) - controller.setPolylineClickListener(onPolylineClick) + controller.setOnPolylineClickListener(onPolylineClick) controller.setOnMarkerAnimateStart(onMarkerAnimateStart) controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) controller.setOnGroundImageClickListener(onGroundImageClick) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt index 476e8fc2..147e3013 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -10,7 +10,6 @@ import com.google.android.gms.maps.GoogleMap.OnMapClickListener import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.Polygon -import com.google.android.gms.maps.model.Polyline import com.mapconductor.core.circle.CircleClickEvent import com.mapconductor.core.circle.CircleOverlayManager import com.mapconductor.core.circle.CircleRenderer @@ -31,25 +30,24 @@ import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRenderer import com.mapconductor.core.polygon.PolygonRendererFactory -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineRenderer -import com.mapconductor.core.polyline.PolylineRendererFactory +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState import com.mapconductor.googlemaps.circle.DefaultGoogleMapCircleRenderer import com.mapconductor.googlemaps.circle.GoogleMapCircleRenderer import com.mapconductor.googlemaps.marker.GoogleMapMarkerController import com.mapconductor.googlemaps.polygon.DefaultGoogleMapPolygonRenderer import com.mapconductor.googlemaps.polygon.GoogleMapPolygonRenderer -import com.mapconductor.googlemaps.polyline.DefaultGoogleMapPolylineRenderer -import com.mapconductor.googlemaps.polyline.GoogleMapPolylineRenderer +import com.mapconductor.googlemaps.polyline.GoogleMapPolylineController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch interface IGoogleMapViewController : - MapViewController, + MapViewController, GroundImageCapable, - MarkerCapable { + MarkerCapable, + PolylineCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -65,14 +63,13 @@ interface IGoogleMapViewController : class GoogleMapViewController( override val holder: GoogleMapViewHolder, private val markerController: GoogleMapMarkerController, + private val polylineController: GoogleMapPolylineController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - private val polylineRendererFactory: PolylineRendererFactory = DefaultGoogleMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultGoogleMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultGoogleMapCircleRenderer(), private val groundImageController: GroundImageController, ) : BaseMapViewController< GoogleMapActualCircle, - GoogleMapActualPolyline, GoogleMapActualPolygon, >(), IGoogleMapViewController, @@ -81,19 +78,6 @@ class GoogleMapViewController( OnCameraMoveListener, OnCameraIdleListener, OnMapClickListener { - override val polylineRenderer: PolylineRenderer = - GoogleMapPolylineRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createPolylineOverlayManager(): PolylineOverlayManager = - polylineRendererFactory.create( - onAdd = polylineRenderer::addPolylines, - onChange = polylineRenderer::changePolylines, - onRemove = polylineRenderer::removePolylines, - ) - override val polygonRenderer: PolygonRenderer = GoogleMapPolygonRenderer( holder = holder, @@ -113,9 +97,6 @@ class GoogleMapViewController( override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { } - override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { - } - override val circleRenderer: CircleRenderer = GoogleMapCircleRenderer( holder = holder, @@ -180,8 +161,8 @@ class GoogleMapViewController( override suspend fun clearOverlays() { markerController.clear() groundImageController.clear() + polylineController.clear() circleOverlayManager.clearOverlays() - polylineOverlayManager.clearOverlays() polygonOverlayManager.clearOverlays() } @@ -193,9 +174,9 @@ class GoogleMapViewController( override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) - override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) + override suspend fun compositionPolylines(data: List) = polylineController.add(data) - override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) override fun onCameraMove() { cameraMoveCallback?.let { @@ -286,4 +267,8 @@ class GoogleMapViewController( override fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) { this.groundImageController.clickListener = listener } + + override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { + this.polylineController.clickListener = listener + } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt index b6f5efb3..55368ffb 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt @@ -12,6 +12,8 @@ import com.mapconductor.core.projection.WebMercator import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer import com.mapconductor.googlemaps.marker.GoogleMapMarkerController import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer +import com.mapconductor.googlemaps.polyline.GoogleMapPolylineController +import com.mapconductor.googlemaps.polyline.GoogleMapPolylineOverlayRenderer import android.app.Activity import android.content.Context import android.content.ContextWrapper @@ -39,6 +41,7 @@ object GoogleMapViewControllerStore : StaticHolder() { GoogleMapViewController( markerController = getMarkerController(holder), groundImageController = getGroundImageController(holder), + polylineController = getPolylineController(holder), holder = holder, ) this.set(id, controller) @@ -46,6 +49,19 @@ object GoogleMapViewControllerStore : StaticHolder() { return controller } + private fun getPolylineController(holder: GoogleMapViewHolder): GoogleMapPolylineController { + val renderer = + GoogleMapPolylineOverlayRenderer( + holder = holder, + ) + + val controller = + GoogleMapPolylineController( + renderer = renderer, + ) + return controller + } + private fun getMarkerController(holder: GoogleMapViewHolder): GoogleMapMarkerController { val hexGeocell = HexGeocell( diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt new file mode 100644 index 00000000..abde102f --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt @@ -0,0 +1,80 @@ +package com.mapconductor.googlemaps.polyline + +import androidx.compose.ui.graphics.toArgb +import com.google.android.gms.maps.model.Polyline +import com.google.android.gms.maps.model.PolylineOptions +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineManagerImpl +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLng +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class GoogleMapPolylineController( + polylineManager: PolylineManager = PolylineManagerImpl(), + renderer: GoogleMapPolylineOverlayRenderer, +) : PolylineController(polylineManager, renderer) + +class GoogleMapPolylineOverlayRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): Polyline? = + withContext(coroutine.coroutineContext) { + val points = state.points.map { GeoPoint.from(it).toLatLng() } + val options = + PolylineOptions() + .addAll(points) + .color(state.strokeColor.toArgb()) + .width(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) + .geodesic(state.geodesic) + .clickable(true) + + holder.map.addPolyline(options).also { + it.tag = state.id + } + } + + override suspend fun updatePolylineProperties( + polyline: Polyline, + current: PolylineEntity, + prev: PolylineEntity, + ): Polyline? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.points != prevFinger.points) { + val points = current.state.points.map { GeoPoint.from(it).toLatLng() } + polyline.points = points + } + + if (finger.geodesic != prevFinger.geodesic) { + polyline.isGeodesic = current.state.geodesic + } + + if (finger.strokeWidth != prevFinger.strokeWidth) { + polyline.width = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + } + + if (finger.strokeColor != prevFinger.strokeColor) { + polyline.color = current.state.strokeColor.toArgb() + } + + polyline + } + + override suspend fun removePolyline(entity: PolylineEntity) { + coroutine.launch { + entity.polyline.remove() + } + } +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineRenderer.kt deleted file mode 100644 index 43c9cb3f..00000000 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineRenderer.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.mapconductor.googlemaps.polyline - -import androidx.compose.ui.graphics.toArgb -import com.google.android.gms.maps.model.Polyline -import com.google.android.gms.maps.model.PolylineOptions -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polyline.AbstractPolylineRenderer -import com.mapconductor.core.polyline.PolylineEntity -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineOverlayManagerImpl -import com.mapconductor.core.polyline.PolylineRenderer.UpdateParams -import com.mapconductor.core.polyline.PolylineRendererFactory -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.googlemaps.GoogleMapViewHolder -import com.mapconductor.googlemaps.toLatLng -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultGoogleMapPolylineRenderer : PolylineRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolylineOverlayManager = - PolylineOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class GoogleMapPolylineRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolylineRenderer() { - override suspend fun addPolylines(newLines: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newLines.map { state -> - val points = state.points.map { GeoPoint.from(it).toLatLng() } - val options = - PolylineOptions() - .addAll(points) - .color(state.strokeColor.toArgb()) - .width(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) - .geodesic(state.geodesic) - .clickable(false) - holder.map.addPolyline(options).also { - it.tag = state.id - } - } - } - } - - override suspend fun removePolylines(removeEntities: List>) { - coroutine.launch { - removeEntities.forEach { params -> params.polyline.remove() } - } - } - - override suspend fun changePolylines(changes: List>): List { - return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val polyline = params.entity.polyline - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - if (finger.points != prevFinger.points) { - val points = - params.entity.state.points - .map { GeoPoint.from(it).toLatLng() } - polyline.points = points - } - polyline.isGeodesic = params.entity.state.geodesic - polyline.width = ResourceProvider.dpToPx(params.entity.state.strokeWidth).toFloat() - polyline.color = - params.entity.state.strokeColor - .toArgb() - return@map polyline - } - } - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index 2e3d9a81..92ca9c9d 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -78,7 +78,7 @@ fun HereMapView( controller.setOnMarkerAnimateStart(onMarkerAnimateStart) controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) controller.setCircleClickListener(onCircleClick) - controller.setPolylineClickListener(onPolylineClick) + controller.setOnPolylineClickListener(onPolylineClick) (state as? HereViewStateImpl)?.controller = controller diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt index e53857c6..f353b1d0 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt @@ -12,7 +12,6 @@ import com.here.sdk.mapview.MapCameraListener import com.here.sdk.mapview.MapCameraUpdateFactory import com.here.sdk.mapview.MapMeasure import com.here.sdk.mapview.MapPolygon -import com.here.sdk.mapview.MapPolyline import com.here.sdk.mapview.MapScene import com.here.sdk.mapview.MapView import com.here.time.Duration @@ -31,24 +30,23 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRendererFactory -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineRenderer -import com.mapconductor.core.polyline.PolylineRendererFactory +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState import com.mapconductor.here.circle.DefaultHereMapCircleRenderer import com.mapconductor.here.circle.HereMapCircleRenderer import com.mapconductor.here.marker.HereMarkerController import com.mapconductor.here.polygon.DefaultHereMapPolygonRenderer import com.mapconductor.here.polygon.HereMapPolygonRenderer -import com.mapconductor.here.polyline.DefaultHereMapPolylineRenderer -import com.mapconductor.here.polyline.HereMapPolylineRenderer +import com.mapconductor.here.polyline.HerePolylineController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch interface IHereMapViewController : - MapViewController, - MarkerCapable { + MapViewController, + MarkerCapable, + PolylineCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MoveCameraCallback? = null, @@ -63,15 +61,13 @@ interface IHereMapViewController : class HereMapViewController( private val markerController: HereMarkerController, + private val polylineController: HerePolylineController, override val holder: MapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - private val polylineRendererFactory: PolylineRendererFactory = - DefaultHereMapPolylineRenderer(), private val polygonRendererFactory: PolygonRendererFactory = DefaultHereMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), ) : BaseMapViewController< HereMapActualCircle, - HereMapActualPolyline, HereMapActualPolygon, >(), IHereMapViewController, @@ -82,12 +78,6 @@ class HereMapViewController( private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 } - override val polylineRenderer: PolylineRenderer = - HereMapPolylineRenderer( - holder = holder, - coroutine = coroutine, - ) - override val polygonRenderer = HereMapPolygonRenderer( holder = holder, @@ -106,16 +96,6 @@ class HereMapViewController( override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { } - override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { - } - - override fun createPolylineOverlayManager(): PolylineOverlayManager = - polylineRendererFactory.create( - onAdd = polylineRenderer::addPolylines, - onChange = polylineRenderer::changePolylines, - onRemove = polylineRenderer::removePolylines, - ) - override fun createPolygonOverlayManager(): PolygonOverlayManager = polygonRendererFactory.create( onAdd = polygonRenderer::addPolygons, @@ -132,7 +112,7 @@ class HereMapViewController( override suspend fun clearOverlays() { markerController.clear() - polylineOverlayManager.clearOverlays() + polylineController.clear() polygonOverlayManager.clearOverlays() circleOverlayManager.clearOverlays() } @@ -169,9 +149,9 @@ class HereMapViewController( override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) - override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) + override suspend fun compositionPolylines(data: List) = polylineController.add(data) - override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) init { setupListeners() @@ -314,4 +294,8 @@ class HereMapViewController( holder.mapView .viewToGeoCoordinates(point) ?.toGeoPoint() + + override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { + polylineController.clickListener = listener + } } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt index e6d29c52..98443fe0 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt @@ -12,6 +12,8 @@ import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.projection.WebMercator import com.mapconductor.here.marker.HereMapMarkerRenderer import com.mapconductor.here.marker.HereMarkerController +import com.mapconductor.here.polyline.HerePolylineController +import com.mapconductor.here.polyline.HerePolylineOverlayRenderer import android.content.Context import android.content.pm.PackageManager @@ -82,17 +84,30 @@ object HereMapViewControllerStore : StaticHolder() { // } // } // } - val markerController = getMarkerController(holder) val controller = HereMapViewController( holder = holder, - markerController = markerController, + markerController = getMarkerController(holder), + polylineController = getPolylineController(holder), ) this.set(id, controller) return controller } + private fun getPolylineController(holder: HereMapViewHolder): HerePolylineController { + val renderer = + HerePolylineOverlayRenderer( + holder = holder, + ) + + val controller = + HerePolylineController( + renderer = renderer, + ) + return controller + } + private fun getMarkerController(holder: HereMapViewHolder): HereMarkerController { val hexGeocell = HexGeocell( diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HereMapPolylineRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HereMapPolylineRenderer.kt deleted file mode 100644 index a7f56acd..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HereMapPolylineRenderer.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.mapconductor.here.polyline - -import androidx.compose.ui.graphics.toArgb -import com.here.sdk.core.Color -import com.here.sdk.core.GeoPolyline -import com.here.sdk.mapview.LineCap -import com.here.sdk.mapview.MapMeasureDependentRenderSize -import com.here.sdk.mapview.MapPolyline -import com.here.sdk.mapview.RenderSize -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.createInterpolatePoints -import com.mapconductor.core.createLinearInterpolatePoints -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.polyline.AbstractPolylineRenderer -import com.mapconductor.core.polyline.PolylineEntity -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineOverlayManagerImpl -import com.mapconductor.core.polyline.PolylineRenderer.UpdateParams -import com.mapconductor.core.polyline.PolylineRendererFactory -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.here.HereMapActualPolyline -import com.mapconductor.here.HereMapViewHolder -import com.mapconductor.here.toGeoCoordinates -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class DefaultHereMapPolylineRenderer : PolylineRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolylineOverlayManager = - PolylineOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class HereMapPolylineRenderer( - override val holder: HereMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolylineRenderer() { - override suspend fun addPolylines(newLines: List): List { - val polylines = - newLines.map { state -> - val geoPolyline = createGeoPolyline(state) - val representation = createRepresentation(state) - MapPolyline(geoPolyline, representation) - } - coroutine.launch { - holder.map.addMapPolylines(polylines) - } - return polylines - } - - override suspend fun removePolylines(removeEntities: List>) { - val polylines = removeEntities.map { it.polyline } - coroutine.launch { - holder.map.removeMapPolylines(polylines) - } - } - - override suspend fun changePolylines( - changes: List>, - ): List { - val removed = mutableListOf() - val polylines = - changes.map { params -> - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { - removed.add(params.prevEntity.polyline) - val geoPolyline = createGeoPolyline(params.entity.state) - params.entity.polyline.geometry = geoPolyline - } - if (finger.strokeColor != prevFinger.strokeColor || finger.strokeWidth != prevFinger.strokeColor) { - val representation = createRepresentation(params.entity.state) - params.entity.polyline.setRepresentation(representation) - } - return@map params.entity.polyline - } - - coroutine.launch { - holder.map.removeMapPolylines(removed) - holder.map.addMapPolylines(polylines) - } - return polylines - } - - private fun createGeoPolyline(state: PolylineState): GeoPolyline { - val geoPoints: List = - when (state.geodesic) { - true -> createInterpolatePoints(state.points) - false -> createLinearInterpolatePoints(state.points) - } - val points = geoPoints.map { GeoPoint.from(it).toGeoCoordinates() } - val geoPolyline = GeoPolyline(points) - return geoPolyline - } - - private fun createRepresentation(state: PolylineState): MapPolyline.Representation { - val lineWidth = - MapMeasureDependentRenderSize( - RenderSize.Unit.PIXELS, - ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()), - ) - val lineColor = Color.valueOf(state.strokeColor.toArgb()) - val lineCap = LineCap.SQUARE - return MapPolyline.SolidRepresentation(lineWidth, lineColor, lineCap) - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt new file mode 100644 index 00000000..754ca61f --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt @@ -0,0 +1,107 @@ +package com.mapconductor.here.polyline + +import androidx.compose.ui.graphics.toArgb +import com.here.sdk.core.Color +import com.here.sdk.core.GeoPolyline +import com.here.sdk.mapview.LineCap +import com.here.sdk.mapview.MapMeasureDependentRenderSize +import com.here.sdk.mapview.MapPolyline +import com.here.sdk.mapview.RenderSize +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.createInterpolatePoints +import com.mapconductor.core.createLinearInterpolatePoints +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineManagerImpl +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.here.HereMapActualPolyline +import com.mapconductor.here.HereMapViewHolder +import com.mapconductor.here.toGeoCoordinates +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class HerePolylineController( + polylineManager: PolylineManager = PolylineManagerImpl(), + renderer: HerePolylineOverlayRenderer, +) : PolylineController(polylineManager, renderer) + +class HerePolylineOverlayRenderer( + override val holder: HereMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): HereMapActualPolyline? { + val geoPolyline = createGeoPolyline(state) + val representation = createRepresentation(state) + val mapPolyline = MapPolyline(geoPolyline, representation) + + coroutine.launch { + holder.map.addMapPolylines(listOf(mapPolyline)) + } + + return mapPolyline + } + + override suspend fun updatePolylineProperties( + polyline: HereMapActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): HereMapActualPolyline? { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + var needsReAdd = false + + if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { + val geoPolyline = createGeoPolyline(current.state) + polyline.geometry = geoPolyline + needsReAdd = true + } + + if (finger.strokeColor != prevFinger.strokeColor || finger.strokeWidth != prevFinger.strokeWidth) { + val representation = createRepresentation(current.state) + polyline.setRepresentation(representation) + needsReAdd = true + } + + if (needsReAdd) { + coroutine.launch { + holder.map.removeMapPolylines(listOf(polyline)) + holder.map.addMapPolylines(listOf(polyline)) + } + } + + return polyline + } + + override suspend fun removePolyline(entity: PolylineEntity) { + coroutine.launch { + holder.map.removeMapPolylines(listOf(entity.polyline)) + } + } + + private fun createGeoPolyline(state: PolylineState): GeoPolyline { + val geoPoints: List = + when (state.geodesic) { + true -> createInterpolatePoints(state.points) + false -> createLinearInterpolatePoints(state.points) + } + val points = geoPoints.map { GeoPoint.from(it).toGeoCoordinates() } + return GeoPolyline(points) + } + + private fun createRepresentation(state: PolylineState): MapPolyline.Representation { + val lineWidth = + MapMeasureDependentRenderSize( + RenderSize.Unit.PIXELS, + ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()), + ) + val lineColor = Color.valueOf(state.strokeColor.toArgb()) + val lineCap = LineCap.SQUARE + return MapPolyline.SolidRepresentation(lineWidth, lineColor, lineCap) + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index 79f6aed2..cc339308 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -13,9 +13,13 @@ import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineManagerImpl import com.mapconductor.core.projection.WebMercator import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.marker.MapboxMarkerRenderer +import com.mapconductor.mapbox.polyline.MapboxPolylineController +import com.mapconductor.mapbox.polyline.MapboxPolylineLayer +import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer import android.app.Activity import android.content.Context import android.content.ContextWrapper @@ -65,12 +69,12 @@ fun MapboxMapView( ) val holder = MapboxMapViewHolderImpl.create(context, mapInitOptions) - val markerController = getMarkerController(holder) val controller = MapboxMapViewController( holder = holder, - markerController = markerController, + markerController = getMarkerController(holder), + polylineController = getPolylineController(holder), ) (state as? MapboxViewStateImpl)?.let { mapViewState -> mapViewState.controller = controller @@ -78,7 +82,7 @@ fun MapboxMapView( } controller.setMapClickListener(onMapClick) controller.setCircleClickListener(onCircleClick) - controller.setPolylineClickListener(onPolylineClick) + controller.setOnPolylineClickListener(onPolylineClick) controller.setOnMarkerClickListener(onMarkerClick) controller.setOnMarkerDragStart(onMarkerDragStart) controller.setOnMarkerDrag(onMarkerDrag) @@ -97,6 +101,28 @@ fun MapboxMapView( ) } +internal fun getPolylineController(holder: MapboxMapViewHolder): MapboxPolylineController { + val polylineLayer: MapboxPolylineLayer = + MapboxPolylineLayer( + sourceId = "polyline-source", + layerId = "polyline-layer", + ) + val polylineManager = PolylineManagerImpl() + + val renderer = + MapboxPolylineOverlayRenderer( + layer = polylineLayer, + polylineManager = polylineManager, + holder = holder, + ) + + val controller = + MapboxPolylineController( + renderer = renderer, + ) + return controller +} + internal fun getMarkerController(holder: MapboxMapViewHolder): MapboxMarkerController { val hexGeocell = HexGeocell( diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt index 06a10aa5..3c849525 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -34,8 +34,8 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.PolygonOverlayManager import com.mapconductor.core.polygon.PolygonRenderer -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineRendererFactory +import com.mapconductor.core.polyline.OnPolylineEventHandler +import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState import com.mapconductor.mapbox.circle.DefaultMapboxCircleRenderer import com.mapconductor.mapbox.circle.MapboxCircleLayer @@ -43,9 +43,7 @@ import com.mapconductor.mapbox.circle.MapboxCircleRenderer import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.polygon.MapboxPolygonLayer import com.mapconductor.mapbox.polygon.MapboxPolygonRenderer -import com.mapconductor.mapbox.polyline.DefaultMapboxPolylineRenderer -import com.mapconductor.mapbox.polyline.MapboxPolylineLayer -import com.mapconductor.mapbox.polyline.MapboxPolylineRenderer +import com.mapconductor.mapbox.polyline.MapboxPolylineController import android.animation.Animator import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -54,10 +52,10 @@ import kotlinx.coroutines.launch interface IMapboxMapViewController : MapViewController< MapboxActualCircle, - MapboxActualPolyline, MapboxActualPolygon, >, - MarkerCapable { + MarkerCapable, + PolylineCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -73,19 +71,13 @@ interface IMapboxMapViewController : internal class MapboxMapViewController( override val holder: MapboxMapViewHolder, private val markerController: MapboxMarkerController, + private val polylineController: MapboxPolylineController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - private val polylineRendererFactory: PolylineRendererFactory = - DefaultMapboxPolylineRenderer(), private val circleLayer: MapboxCircleLayer = MapboxCircleLayer( sourceId = "circle-source", layerId = "circle-layer", ), - private val polylineLayer: MapboxPolylineLayer = - MapboxPolylineLayer( - sourceId = "polyline-source", - layerId = "polyline-layer", - ), private val polygonLayer: MapboxPolygonLayer = MapboxPolygonLayer( sourceId = "polygon-source", @@ -95,7 +87,6 @@ internal class MapboxMapViewController( DefaultMapboxCircleRenderer(), ) : BaseMapViewController< MapboxActualCircle, - MapboxActualPolyline, MapboxActualPolygon, >(), IMapboxMapViewController, @@ -107,14 +98,6 @@ internal class MapboxMapViewController( private const val ZOOM_ADJUST_VALUE = 1.0 } - override fun createPolylineOverlayManager(): PolylineOverlayManager = - polylineRendererFactory.create( - onAdd = polylineRenderer::addPolylines, - onChange = polylineRenderer::changePolylines, - onRemove = polylineRenderer::removePolylines, - onPostProcess = polylineRenderer::redraw, - ) - override fun createPolygonOverlayManager(): PolygonOverlayManager { TODO("Not yet implemented") } @@ -127,13 +110,6 @@ internal class MapboxMapViewController( onPostProcess = circleRenderer::redraw, ) - override val polylineRenderer: MapboxPolylineRenderer = - MapboxPolylineRenderer( - holder = holder, - coroutine = coroutine, - layer = polylineLayer, - ) - override val polygonRenderer: PolygonRenderer = MapboxPolygonRenderer( holder = holder, @@ -153,15 +129,12 @@ internal class MapboxMapViewController( override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { } - override fun onPolylineOverlayManagerInitialized(overlayManager: PolylineOverlayManager) { - } - init { holder.map.getStyle { style -> style.addSource(circleLayer.source) style.addLayer(circleLayer.layer) - style.addSource(polylineLayer.source) - style.addLayer(polylineLayer.layer) + style.addSource(polylineController.renderer.layer.source) + style.addLayer(polylineController.renderer.layer.layer) style.addSource(markerController.renderer.markerLayer.source) style.addLayer(markerController.renderer.markerLayer.layer) style.addSource(markerController.renderer.dragLayer.source) @@ -184,16 +157,16 @@ internal class MapboxMapViewController( override suspend fun clearOverlays() { markerController.clear() - polylineOverlayManager.clearOverlays() + polylineController.clear() } override suspend fun compositionMarkers(data: List) = markerController.add(data) override suspend fun updateMarker(state: MarkerState) = markerController.update(state) - override suspend fun addPolylines(data: List) = polylineOverlayManager.addPolylines(data) + override suspend fun compositionPolylines(data: List) = polylineController.add(data) - override suspend fun updatePolyline(state: PolylineState) = polylineOverlayManager.updatePolyline(state) + override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) @@ -390,4 +363,8 @@ internal class MapboxMapViewController( override fun setOnMarkerClickListener(listener: OnMarkerEventHandler?) { markerController.clickListener = listener } + + override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { + polylineController.clickListener = listener + } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt similarity index 50% rename from mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineRenderer.kt rename to mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt index ada810d1..12a025c2 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt @@ -1,92 +1,92 @@ -package com.mapconductor.mapbox.polyline - -import com.google.gson.JsonObject -import com.mapbox.geojson.Feature -import com.mapbox.geojson.LineString -import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures -import com.mapconductor.core.createInterpolatePoints -import com.mapconductor.core.createLinearInterpolatePoints -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.features.normalize -import com.mapconductor.core.polyline.AbstractPolylineRenderer -import com.mapconductor.core.polyline.PolylineEntity -import com.mapconductor.core.polyline.PolylineOverlayManager -import com.mapconductor.core.polyline.PolylineOverlayManagerImpl -import com.mapconductor.core.polyline.PolylineRenderer.UpdateParams -import com.mapconductor.core.polyline.PolylineRendererFactory -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.splitByMeridian -import com.mapconductor.mapbox.MapboxActualPolyline -import com.mapconductor.mapbox.MapboxMapViewHolder -import com.mapconductor.mapbox.toMapboxColorString -import com.mapconductor.mapbox.toPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class DefaultMapboxPolylineRenderer : PolylineRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolylineOverlayManager = - PolylineOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class MapboxPolylineRenderer( - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope, - private val layer: MapboxPolylineLayer, -) : AbstractPolylineRenderer() { - override suspend fun addPolylines(newLines: List): List = - newLines.map { state -> createMapboxLines(state) } - - private fun createMapboxLines(state: PolylineState): List { - val geoPoints: List = - when (state.geodesic) { - true -> createInterpolatePoints(state.points) - false -> createLinearInterpolatePoints(state.points) - }.map { it.normalize() } - return splitByMeridian(geoPoints, state.geodesic).mapIndexed { index, linePoints -> - val points = linePoints.map { GeoPoint.from(it).toPoint() } - val id = "polyline-${state.id}-$index" - - return@mapIndexed Feature.fromGeometry( - LineString.fromLngLats(points), - JsonObject().apply { - addProperty(MapboxPolylineLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) - addProperty("id", id) - }, - id, - ) - } - } - - override suspend fun removePolylines(removeEntities: List>) { - val featureIds = - removeEntities.map { entity -> - entity.polyline.map { feature -> - feature.getStringProperty("id") - } - } - layer.source.removeGeoJSONSourceFeatures(featureIds.flatten()) - } - - override suspend fun changePolylines( - changes: List>, - ): List = changes.map { params -> createMapboxLines(params.entity.state) } - - fun redraw() { - val polylines = polylineOverlayManager.getAllEntities() - coroutine.launch { - layer.draw(polylines) - } - } -} +package com.mapconductor.mapbox.polyline + +import com.google.gson.JsonObject +import com.mapbox.geojson.Feature +import com.mapbox.geojson.LineString +import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures +import com.mapconductor.core.createInterpolatePoints +import com.mapconductor.core.createLinearInterpolatePoints +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.features.normalize +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.core.splitByMeridian +import com.mapconductor.mapbox.MapboxActualPolyline +import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.toMapboxColorString +import com.mapconductor.mapbox.toPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class MapboxPolylineController( + override val renderer: MapboxPolylineOverlayRenderer, + polylineManager: PolylineManager = renderer.polylineManager, +) : PolylineController(polylineManager, renderer) + +class MapboxPolylineOverlayRenderer( + val layer: MapboxPolylineLayer, + val polylineManager: PolylineManager, + override val holder: MapboxMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): MapboxActualPolyline? = createMapboxLines(state) + + override suspend fun updatePolylineProperties( + polyline: MapboxActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): MapboxActualPolyline? { + // For Mapbox, we need to recreate the features when properties change + return createMapboxLines(current.state) + } + + override suspend fun removePolyline(entity: PolylineEntity) { + val featureIds = + entity.polyline.map { feature -> + feature.getStringProperty("id") + } + layer.source.removeGeoJSONSourceFeatures(featureIds) + } + + override suspend fun onPostProcess() { + // Redraw all polylines on the layer + val polylines = getAllPolylineEntities() + coroutine.launch { + layer.draw(polylines) + } + } + + private fun createMapboxLines(state: PolylineState): List { + val geoPoints: List = + when (state.geodesic) { + true -> createInterpolatePoints(state.points) + false -> createLinearInterpolatePoints(state.points) + }.map { it.normalize() } + + return splitByMeridian(geoPoints, state.geodesic).mapIndexed { index, linePoints -> + val points = linePoints.map { GeoPoint.from(it).toPoint() } + val id = "polyline-${state.id}-$index" + + return@mapIndexed Feature.fromGeometry( + LineString.fromLngLats(points), + JsonObject().apply { + addProperty(MapboxPolylineLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) + addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) + addProperty("id", id) + }, + id, + ) + } + } + + private fun getAllPolylineEntities(): List> { + // This would need access to the polyline manager + // For now, we'll implement a simple workaround + return polylineManager.allEntities() + } +} From 806808e51dd3ceae0dc9d6741fd0f5e05f5d7e38 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 22 Aug 2025 12:08:31 +0900 Subject: [PATCH 25/44] Small refactoring --- .../mapconductor/core/polyline/AbstractPolylineController.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt index e96ff3ee..0cd94797 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt @@ -200,7 +200,5 @@ class PolylineManagerImpl : PolylineManager { entities.clear() } - override fun findNearest(position: IGeoPoint): PolylineEntity? { - return entities.values.firstOrNull() - } + override fun findNearest(position: IGeoPoint): PolylineEntity? = entities.values.firstOrNull() } From 598f021d119d483b869960ec928a770a429d5fd1 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 23 Aug 2025 00:24:39 +0900 Subject: [PATCH 26/44] wip: refactoring for polygon --- .claude/settings.local.json | 3 +- build.gradle.kts | 25 ++++--- .../com/mapconductor/example/DemoAppScreen.kt | 13 ++++ .../com/mapconductor/example/MainActivity.kt | 2 +- .../mapconductor/example/MapViewContainer.kt | 2 + .../pages/polygon/PolygonMapComponent.kt | 37 ++++++---- .../example/ui/DemoMapPageScaffold.kt | 9 +++ .../com/mapconductor/core/OverlayProvider.kt | 25 ++----- .../core/controller/BaseMapViewController.kt | 19 +---- .../core/controller/MapViewController.kt | 6 +- .../com/mapconductor/core/map/MapViewBase.kt | 45 +++++++----- .../core/polygon/AbstractPolygonController.kt | 4 ++ .../com/mapconductor/core/polygon/Polygon.kt | 7 +- .../core/polygon/PolygonOverlayManager.kt | 8 +++ .../com/mapconductor/arcgis/ArcGISMapView.kt | 2 +- ...ller.kt => ArcGISMapViewControllerImpl.kt} | 70 +++++++++++-------- ...sMapViewState.kt => ArcGISMapViewState.kt} | 2 +- .../arcgis/ArcGISViewControllerStore.kt | 6 +- .../arcgis/polygon/ArcGISPolygonRenderer.kt | 61 +++++++--------- .../mapconductor/googlemaps/GoogleMapView.kt | 2 +- ...ller.kt => GoogleMapViewControllerImpl.kt} | 65 ++++++++--------- .../GoogleMapViewControllerStore.kt | 23 +++++- .../googlemaps/GoogleMapViewStateImpl.kt | 2 +- .../polygon/GoogleMapPolygonRenderer.kt | 64 +++++++---------- .../java/com/mapconductor/here/HereMapView.kt | 2 +- ...roller.kt => HereMapViewControllerImpl.kt} | 64 +++++++++-------- .../here/HereMapViewControllerStore.kt | 25 ++++++- .../mapconductor/here/HereViewStateImpl.kt | 2 +- .../here/polygon/HereMapPolygonRenderer.kt | 67 +++++++----------- .../com/mapconductor/mapbox/MapboxMapView.kt | 7 +- ...ller.kt => MapboxMapViewControllerImpl.kt} | 62 ++++++++++------ .../mapbox/MapboxViewStateImpl.kt | 2 +- .../mapbox/polygon/MapboxPolygonRenderer.kt | 52 ++++++-------- 33 files changed, 422 insertions(+), 363 deletions(-) create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonController.kt rename mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/{ArcGISMapViewController.kt => ArcGISMapViewControllerImpl.kt} (88%) rename mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/{ArcGisMapViewState.kt => ArcGISMapViewState.kt} (98%) rename mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/{GoogleMapViewController.kt => GoogleMapViewControllerImpl.kt} (87%) rename mapconductor-for-here/src/main/java/com/mapconductor/here/{HereMapViewController.kt => HereMapViewControllerImpl.kt} (89%) rename mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/{MapboxMapViewController.kt => MapboxMapViewControllerImpl.kt} (89%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f50d2426..df15c18c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(./gradlew:*)" + "Bash(./gradlew:*)", + "Bash(grep:*)" ], "deny": [] } diff --git a/build.gradle.kts b/build.gradle.kts index ab1d3a7d..2dccd789 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,22 +17,25 @@ buildscript { } } -val modules: List = rootDir.resolve("projects.properties").readLines() - .firstOrNull { it.startsWith("modules=") } - ?.removePrefix("modules=") - ?.split(",") - ?.map { it.trim() } - ?.filter { it.isNotEmpty() } - ?: emptyList() +val modules: List = + rootDir + .resolve("projects.properties") + .readLines() + .firstOrNull { it.startsWith("modules=") } + ?.removePrefix("modules=") + ?.split(",") + ?.map { it.trim() } + ?.filter { it.isNotEmpty() } + ?: emptyList() tasks.register("allLintChecks") { group = "verification" description = "Run ktlintFormat and lint for all modules" - val lintTasks = modules.map { module -> - listOf(":$module:ktlintFormat", ":$module:lint") - } + val lintTasks = + modules.map { module -> + listOf(":$module:ktlintFormat", ":$module:lint") + } dependsOn(lintTasks) } - diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index 940bf897..3c2c1c40 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -24,6 +24,7 @@ import com.mapconductor.example.pages.groundimage.GroundImageMapPage import com.mapconductor.example.pages.groundimage.GroundImageResources import com.mapconductor.example.pages.map.flyto.FlyToMapIcons import com.mapconductor.example.pages.map.flyto.FlyToMapPage +import com.mapconductor.example.pages.polygon.PolygonMapPage import com.mapconductor.example.pages.polyline.PolylineMapPage import com.mapconductor.example.pages.stores.StoreMapPage import com.mapconductor.example.ui.sidebar.Sidebar @@ -100,6 +101,12 @@ fun DemoAppScreen(initPage: String = "map") { icon = Icons.Default.PlayArrow, route = "polyline", ), + SidebarItem( + id = "polygon", + title = "polygon ", + icon = Icons.Default.PlayArrow, + route = "polygon", + ), // SidebarItem( // id = "examples", // title = "Map Examples", @@ -114,6 +121,7 @@ fun DemoAppScreen(initPage: String = "map") { // ), ) + AppTheme { Box(modifier = Modifier.fillMaxSize()) { // Main content @@ -134,6 +142,11 @@ fun DemoAppScreen(initPage: String = "map") { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "polygon" -> { + PolygonMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } "flyTo" -> { FlyToMapPage( icons = flyToMapPageIcons, diff --git a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt index d1271a7e..e70cdbdd 100644 --- a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt +++ b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt @@ -12,7 +12,7 @@ class MainActivity : ComponentActivity() { setContent { DemoAppScreen( - initPage = "polyline", + initPage = "polygon", ) } } diff --git a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt index 5cd25ba4..00365270 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -10,6 +10,7 @@ import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.googlemaps.GoogleMapViewStateImpl import com.mapconductor.googlemaps.GoogleMapsView @@ -31,6 +32,7 @@ fun MapViewContainer( onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, onGroundImageClick: OnGroundImageEventHandler? = null, content: @Composable MapViewScope.() -> Unit, ) { diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt index bae34c77..edb8cf0f 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt @@ -3,27 +3,34 @@ package com.mapconductor.example.pages.polygon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.Marker import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.Polygon +import com.mapconductor.example.MapViewContainer @Composable fun PolygonMapComponent( mapViewState: MapViewState<*>?, + viewModel: PolygonMapPageViewModel, modifier: Modifier = Modifier, + onPolygonClick: OnPolygonEventHandler = {}, onMarkerDrag: OnMarkerEventHandler = {}, ) { -// mapViewState?.let { it -> -// MapViewContainer( -// modifier = modifier, -// state = it, -// onMarkerDrag = onMarkerDrag, -// ) { -// // Polyline -// Polyline(viewModel.polylineState) -// -// // Waypoint markers -// viewModel.wayPointMarkers.forEach { marker -> -// Marker(marker) -// } -// } -// } + mapViewState?.let { mapViewState -> + MapViewContainer( + modifier = modifier, + state = mapViewState, + onPolygonClick = onPolygonClick, + onMarkerDrag = onMarkerDrag, + ) { + // Polygon + Polygon(viewModel.polygonState) + + // Vertex markers (draggable) + viewModel.polygonVertexMarkers.forEach { marker -> + Marker(marker) + } + } + } } diff --git a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt index 678813d5..14348109 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt @@ -128,6 +128,15 @@ fun GroundImageCapableMapViewItems( GetGoogleMapViewItem(initCameraPosition), ) +@Composable +fun PolygonCapableMapViewItems(initCameraPosition: IMapCameraPosition): List>> = + listOf( + GetGoogleMapViewItem(initCameraPosition), + GetMapboxViewItem(initCameraPosition), + GetHereViewItem(initCameraPosition), + GetArcGISViewItem(initCameraPosition), + ) + @Composable fun DemoMapPageScaffold( menuItems: List>>, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index 57b04ae6..124ee827 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -12,6 +12,7 @@ import com.mapconductor.core.map.MapOverlay import com.mapconductor.core.map.MapOverlayRegistry import com.mapconductor.core.marker.MarkerOverlay import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.polygon.PolygonOverlay import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.PolylineOverlay import com.mapconductor.core.polyline.PolylineState @@ -75,7 +76,7 @@ open class MapViewScope { registry.register(MarkerOverlay(markerFlow)) registry.register(CircleOverlay(circleFlow)) registry.register(PolylineOverlay(polylineFlow)) - // registry.register(PolygonOverlay(polygonFlow)) // TODO: Implement addPolygons in MapViewController + registry.register(PolygonOverlay(polygonFlow)) registry.register(GroundImageOverlay(groundImageFlow)) return registry } @@ -88,7 +89,7 @@ class CircleOverlay( data: List, controller: MapViewControllerAlias, ) { - controller.addCircles(data) +// controller.addCircles(data) } } @@ -107,22 +108,10 @@ val LocalCircleCollector = error("Circle must be under the ") } -// TODO: Implement addPolygons in MapViewController first -// class PolygonOverlay( -// override val flow: StateFlow>, -// ) : MapOverlay { -// override suspend fun render( -// data: List, -// controller: MapViewController<*, *, *>, -// ) { -// controller.addPolygons(data) -// } -// } -// -// val LocalPolygonCollector = -// compositionLocalOf>> { -// error("Polygon must be under the ") -// } +val LocalPolygonCollector = + compositionLocalOf>> { + error("Polygon must be under the ") + } val LocalGroundImageCollector = compositionLocalOf>> { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt index d67e9b5e..6c8b90fb 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt @@ -5,19 +5,9 @@ import com.mapconductor.core.circle.CircleRenderer import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonRenderer - -abstract class BaseMapViewController : MapViewController { - abstract val polygonRenderer: PolygonRenderer - - override val polygonOverlayManager: PolygonOverlayManager by lazy { - createPolygonOverlayManager().also { overlayManager -> - polygonRenderer.init(overlayManager) - onPolygonOverlayManagerInitialized(overlayManager) - } - } +import com.mapconductor.core.polygon.OnPolygonEventHandler +abstract class BaseMapViewController : MapViewController { abstract val circleRenderer: CircleRenderer override val circleOverlayManager: CircleOverlayManager by lazy { @@ -27,18 +17,15 @@ abstract class BaseMapViewController : MapViewContr } } - protected abstract fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) - protected abstract fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) - protected abstract fun createPolygonOverlayManager(): PolygonOverlayManager - protected abstract fun createCircleOverlayManager(): CircleOverlayManager protected var cameraMoveCallback: OnCameraMoveHandler? = null protected var mapClickCallback: OnMapEventHandler? = null protected var mapLongClickCallback: OnMapEventHandler? = null protected var circleClickCallback: OnCircleEventHandler? = null + protected var polygonClickCallback: OnPolygonEventHandler? = null abstract fun setupListeners() diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index c019a52b..d32579f8 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -6,14 +6,12 @@ import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler -import com.mapconductor.core.polygon.PolygonOverlayManager import kotlinx.coroutines.CoroutineScope -interface MapViewController { +interface MapViewController { val holder: MapViewHolder<*, *> val coroutine: CoroutineScope val circleOverlayManager: CircleOverlayManager - val polygonOverlayManager: PolygonOverlayManager suspend fun addCircles(data: List) @@ -30,4 +28,4 @@ interface MapViewController { fun setCircleClickListener(listener: OnCircleEventHandler?) } -typealias MapViewControllerAlias = MapViewController<*, *> +typealias MapViewControllerAlias = MapViewController<*> diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index e630d3fb..e7a77d04 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -25,6 +25,7 @@ import com.mapconductor.core.CollectAndRenderOverlays import com.mapconductor.core.LocalCircleCollector import com.mapconductor.core.LocalGroundImageCollector import com.mapconductor.core.LocalMarkerCollector +import com.mapconductor.core.LocalPolygonCollector import com.mapconductor.core.LocalPolylineCollector import com.mapconductor.core.MapViewScope import com.mapconductor.core.ResourceProvider @@ -35,6 +36,7 @@ import com.mapconductor.core.info.InfoBubbleOverlay import com.mapconductor.core.info.LocalInfoBubbleCollector import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.PolygonCapable import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.settings.Settings import android.view.View @@ -90,25 +92,28 @@ fun < LocalInfoBubbleCollector provides scope.bubbleFlow, LocalCircleCollector provides scope.circleFlow, LocalPolylineCollector provides scope.polylineFlow, + LocalPolygonCollector provides scope.polygonFlow, LocalGroundImageCollector provides scope.groundImageFlow, ) { with(scope) { content?.invoke(this) } } - val markers = scope.markerFlow.collectAsState() - markers.value.forEach { markerState -> - LaunchedEffect(markerState.id) { - markerState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - (controller as? MarkerCapable<*>)?.updateMarker(markerState) + (controller as? GroundImageCapable)?.let { + val groundImage = scope.groundImageFlow.collectAsState() + groundImage.value.forEach { groundImageState -> + LaunchedEffect(groundImageState.id) { + groundImageState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + controller.updateGroundImage(groundImageState) + } } } } - val circles = scope.circleFlow.collectAsState() - circles.value.forEach { circleState -> - LaunchedEffect(circleState.id) { - circleState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updateCircle(circleState) + val polygons = scope.polygonFlow.collectAsState() + polygons.value.forEach { polygonState -> + LaunchedEffect(polygonState.id) { + polygonState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + (controller as? PolygonCapable)?.updatePolygon(polygonState) } } } @@ -120,13 +125,19 @@ fun < } } } - (controller as? GroundImageCapable)?.let { - val groundImage = scope.groundImageFlow.collectAsState() - groundImage.value.forEach { groundImageState -> - LaunchedEffect(groundImageState.id) { - groundImageState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updateGroundImage(groundImageState) - } + val circles = scope.circleFlow.collectAsState() + circles.value.forEach { circleState -> + LaunchedEffect(circleState.id) { + circleState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + controller.updateCircle(circleState) + } + } + } + val markers = scope.markerFlow.collectAsState() + markers.value.forEach { markerState -> + LaunchedEffect(markerState.id) { + markerState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { + (controller as? MarkerCapable<*>)?.updateMarker(markerState) } } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonController.kt new file mode 100644 index 00000000..d03d3296 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonController.kt @@ -0,0 +1,4 @@ +package com.mapconductor.core.polygon + +abstract class AbstractPolygonController { +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt index 22de0030..2ba99219 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt @@ -95,4 +95,9 @@ data class PolygonFingerPrint( val extra: Int, ) -typealias OnPolygonEventHandler = (PolygonState) -> Unit +data class PolygonEvent( + val state: PolygonState, + val clicked: IGeoPoint?, +) + +typealias OnPolygonEventHandler = (PolygonEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt index 41a992be..c2a0b35e 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt @@ -4,6 +4,14 @@ import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +interface PolygonCapable { + suspend fun compositionPolygons(data: List) + + suspend fun updatePolygon(state: PolygonState) + + fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) +} + interface PolygonOverlayManager { suspend fun addPolygons(polygons: List) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index 21b3e420..1a308c23 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -29,7 +29,7 @@ fun ArcGISMapView( content: (@Composable ArcGISMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } - val controllerRef = remember { Ref() } + val controllerRef = remember { Ref() } val scope = remember { ArcGISMapViewScope() } // Use specific scope val context = LocalContext.current // Context will be available from MapViewBase too if needed val registry = remember { scope.buildRegistry() } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt similarity index 88% rename from mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt rename to mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt index ef710e42..03f02a6c 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt @@ -12,7 +12,6 @@ import com.mapconductor.arcgis.circle.DefaultArcGISCircleRenderer import com.mapconductor.arcgis.marker.ArcGISMarkerController import com.mapconductor.arcgis.marker.SelectedMarker import com.mapconductor.arcgis.polygon.ArcGISPolygonRenderer -import com.mapconductor.arcgis.polygon.DefaultArcGISPolygonRenderer import com.mapconductor.arcgis.polyline.ArcGISPolylineController import com.mapconductor.core.circle.CircleClickEvent import com.mapconductor.core.circle.CircleOverlayManager @@ -26,9 +25,11 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonRenderer -import com.mapconductor.core.polygon.PolygonRendererFactory +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState @@ -38,13 +39,13 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IArcGISMapViewController : +interface ArcGISMapViewController : MapViewController< ArcGISActualCircle, - ArcGISActualPolygon, >, MarkerCapable, - PolylineCapable { + PolylineCapable, + PolygonCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -57,7 +58,7 @@ interface IArcGISMapViewController : ) } -class ArcGISMapViewController( +class ArcGISMapViewControllerImpl( override val holder: ArcGISMapViewHolder, private val polylineController: ArcGISPolylineController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), @@ -69,30 +70,13 @@ class ArcGISMapViewController( GraphicsOverlay().apply { sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded }, - private val polygonRendererFactory: PolygonRendererFactory = - DefaultArcGISPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultArcGISCircleRenderer(), private val markerController: ArcGISMarkerController, ) : BaseMapViewController< ArcGISActualCircle, - ArcGISActualPolygon, >(), - IArcGISMapViewController { - override fun createPolygonOverlayManager(): PolygonOverlayManager = - polygonRendererFactory.create( - onAdd = polygonRenderer::addPolygons, - onChange = polygonRenderer::changePolygon, - onRemove = polygonRenderer::removePolygons, - ) - - override val polygonRenderer: PolygonRenderer = - ArcGISPolygonRenderer( - polygonLayer = polygonLayer, - holder = holder, - coroutine = coroutine, - ) - + ArcGISMapViewController { override fun createCircleOverlayManager(): CircleOverlayManager = circleRendererFactory.create( onAdd = circleRenderer::addCircles, @@ -107,15 +91,23 @@ class ArcGISMapViewController( coroutine = coroutine, ) - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } + private val polygonController: PolygonController = + PolygonController( + renderer = + ArcGISPolygonRenderer( + polygonLayer = polygonLayer, + holder = holder, + coroutine = coroutine, + ), + ) - override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { + override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } init { holder.map.graphicsOverlays.clear() holder.map.graphicsOverlays.add(circleLayer) + holder.map.graphicsOverlays.add(polygonLayer) holder.map.graphicsOverlays.add(polylineController.renderer.polylineLayer) holder.map.graphicsOverlays.add(markerController.renderer.markerLayer) setupListeners() @@ -237,6 +229,17 @@ class ArcGISMapViewController( position = touchPosition, ) circleClickCallback?.invoke(event) + return + } + + polygonController.find(touchPosition)?.let { polygonEntity -> + val event = + PolygonEvent( + state = polygonEntity.state, + clicked = touchPosition, + ) + polygonClickCallback?.invoke(event) + return } holder.map.screenToLocation(screenPoint).getOrNull()?.also { @@ -247,6 +250,7 @@ class ArcGISMapViewController( override suspend fun clearOverlays() { markerController.clear() polylineController.clear() + polygonController.clear() } override suspend fun compositionMarkers(data: List) = markerController.add(data) @@ -257,6 +261,10 @@ class ArcGISMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) + override suspend fun compositionPolygons(data: List) = polygonController.add(data) + + override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) + override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) @@ -317,4 +325,8 @@ class ArcGISMapViewController( override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { this.polylineController.clickListener = listener } + + override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { + this.polygonClickCallback = listener + } } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewState.kt similarity index 98% rename from mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt rename to mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewState.kt index 373ed8bc..a0695e43 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGisMapViewState.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewState.kt @@ -34,7 +34,7 @@ class ArcGISMapViewStateImpl( private val _padding = MutableStateFlow(MapPaddingsImpl.Zeros) val padding: StateFlow = _padding.asStateFlow() - internal var controller: IArcGISMapViewController? = null + internal var controller: ArcGISMapViewController? = null // Camera position private val _cameraPosition = MutableStateFlow(initCameraPosition) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt index 36762790..19fabf61 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt @@ -17,14 +17,14 @@ import android.content.Context typealias ArcGISMapViewHolder = MapViewHolder object ArcGISViewControllerStore : - StaticHolder() { + StaticHolder() { fun hasCache(id: String): Boolean = this.has(id) fun getOrCreate( context: Context, id: String, options: ArcGISMapViewInitOptions, - ): ArcGISMapViewController { + ): ArcGISMapViewControllerImpl { val existing = this.get(id) if (existing != null) return existing @@ -35,7 +35,7 @@ object ArcGISViewControllerStore : ) val controller = - ArcGISMapViewController( + ArcGISMapViewControllerImpl( holder = holder, markerController = getMarkerController(holder), polylineController = getPolylineController(holder), diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt index 7a610610..82aacbc6 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt @@ -13,41 +13,22 @@ import com.mapconductor.arcgis.ArcGISMapViewHolder import com.mapconductor.arcgis.toArcGISColor import com.mapconductor.arcgis.toPoint import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.AbstractPolygonRenderer import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonOverlayManagerImpl -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import com.mapconductor.core.polygon.PolygonRendererFactory import com.mapconductor.core.polygon.PolygonState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultArcGISPolygonRenderer : PolygonRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolygonOverlayManager = - PolygonOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - class ArcGISPolygonRenderer( val polygonLayer: GraphicsOverlay, - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolygonRenderer() { - override suspend fun addPolygons(newPolygons: List): List { + val holder: ArcGISMapViewHolder, + val coroutine: CoroutineScope, +) : OverlayRenderer> { + override suspend fun onAdd(data: List): List { return withContext(coroutine.coroutineContext) { - return@withContext newPolygons.map { state -> + return@withContext data.map { state -> val geometry = createGeometry(state) @@ -77,40 +58,46 @@ class ArcGISPolygonRenderer( } } - override suspend fun removePolygons(removeEntities: List>) { - val polygons = removeEntities.map { it.polygon } + override suspend fun onRemove(data: List>) { + val polygons = data.map { it.polygon } coroutine.launch { polygonLayer.graphics.removeAll(polygons) } } - override suspend fun changePolygon(changes: List>): List { + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + data: List>>, + ): List { return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val finger = params.entity.state.fingerPrint() - val prevFinger = params.prevEntity.state.fingerPrint() + return@withContext data.map { params -> + val finger = params.current.fingerPrint + val prevFinger = params.prev.fingerPrint if (finger.points != prevFinger.points) { - params.entity.polygon.geometry = createGeometry(params.entity.state) + params.current.polygon.geometry = createGeometry(params.current.state) } - (params.entity.polygon.symbol as SimpleFillSymbol).let { symbol -> + (params.current.polygon.symbol as SimpleFillSymbol).let { symbol -> if (finger.fillColor != prevFinger.fillColor) { symbol.color = - params.entity.state.fillColor + params.current.state.fillColor .toArcGISColor() } symbol.outline?.let { outline -> if (finger.strokeColor != prevFinger.strokeColor) { outline.color = - params.entity.state.strokeColor + params.current.state.strokeColor .toArcGISColor() } if (finger.strokeWidth != prevFinger.strokeWidth) { - outline.width = ResourceProvider.dpToPx(params.entity.state.strokeWidth).toFloat() + outline.width = ResourceProvider.dpToPx(params.current.state.strokeWidth).toFloat() } } } - return@map params.entity.polygon + return@map params.current.polygon } } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index 01ac0c48..8d2ed6d6 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -37,7 +37,7 @@ fun GoogleMapsView( content: (@Composable GoogleMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } - val controllerRef = remember { Ref() } + val controllerRef = remember { Ref() } val scope = remember { GoogleMapViewScope() } // Use specific scope val context = LocalContext.current // Context will be available from MapViewBase too if needed val registry = remember { scope.buildRegistry() } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt similarity index 87% rename from mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt rename to mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt index 147e3013..992c775b 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt @@ -9,7 +9,6 @@ import com.google.android.gms.maps.GoogleMap.OnCameraMoveStartedListener import com.google.android.gms.maps.GoogleMap.OnMapClickListener import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.Polygon import com.mapconductor.core.circle.CircleClickEvent import com.mapconductor.core.circle.CircleOverlayManager import com.mapconductor.core.circle.CircleRenderer @@ -27,25 +26,26 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonRenderer -import com.mapconductor.core.polygon.PolygonRendererFactory +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState import com.mapconductor.googlemaps.circle.DefaultGoogleMapCircleRenderer import com.mapconductor.googlemaps.circle.GoogleMapCircleRenderer import com.mapconductor.googlemaps.marker.GoogleMapMarkerController -import com.mapconductor.googlemaps.polygon.DefaultGoogleMapPolygonRenderer -import com.mapconductor.googlemaps.polygon.GoogleMapPolygonRenderer import com.mapconductor.googlemaps.polyline.GoogleMapPolylineController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IGoogleMapViewController : - MapViewController, +interface GoogleMapViewController : + MapViewController, GroundImageCapable, + PolygonCapable, MarkerCapable, PolylineCapable { fun moveCamera( @@ -60,43 +60,24 @@ interface IGoogleMapViewController : ) } -class GoogleMapViewController( +class GoogleMapViewControllerImpl( override val holder: GoogleMapViewHolder, private val markerController: GoogleMapMarkerController, private val polylineController: GoogleMapPolylineController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - private val polygonRendererFactory: PolygonRendererFactory = DefaultGoogleMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultGoogleMapCircleRenderer(), private val groundImageController: GroundImageController, -) : BaseMapViewController< - GoogleMapActualCircle, - GoogleMapActualPolygon, - >(), - IGoogleMapViewController, + private val polygonController: PolygonController, +) : BaseMapViewController(), + GoogleMapViewController, OnCameraMoveStartedListener, OnCameraMoveCanceledListener, OnCameraMoveListener, OnCameraIdleListener, OnMapClickListener { - override val polygonRenderer: PolygonRenderer = - GoogleMapPolygonRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createPolygonOverlayManager(): PolygonOverlayManager = - polygonRendererFactory.create( - onAdd = polygonRenderer::addPolygons, - onChange = polygonRenderer::changePolygon, - onRemove = polygonRenderer::removePolygons, - ) - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } - override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { - } - override val circleRenderer: CircleRenderer = GoogleMapCircleRenderer( holder = holder, @@ -162,8 +143,8 @@ class GoogleMapViewController( markerController.clear() groundImageController.clear() polylineController.clear() + polygonController.clear() circleOverlayManager.clearOverlays() - polygonOverlayManager.clearOverlays() } override suspend fun compositionMarkers(data: List) = markerController.add(data) @@ -231,6 +212,18 @@ class GoogleMapViewController( return } + polygonController.find(touchPosition)?.let { entity -> + val event = + PolygonEvent( + state = entity.state, + clicked = touchPosition, + ) + coroutine.launch { + polygonController.clickListener?.invoke(event) + } + return + } + mapClickCallback?.let { coroutine.launch { it(position.toGeoPoint()) } } @@ -240,6 +233,10 @@ class GoogleMapViewController( override suspend fun updateGroundImage(state: GroundImageState) = groundImageController.update(state) + override suspend fun compositionPolygons(data: List) = polygonController.add(data) + + override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) + override fun setOnMarkerDragStart(listener: OnMarkerEventHandler?) { this.markerController.dragStartListener = listener } @@ -271,4 +268,8 @@ class GoogleMapViewController( override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { this.polylineController.clickListener = listener } + + override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { + this.polygonController.clickListener = listener + } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt index 55368ffb..906d58c8 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt @@ -8,10 +8,12 @@ import com.mapconductor.core.groundimage.GroundImageController import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder import com.mapconductor.core.marker.MarkerManager +import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.projection.WebMercator import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer import com.mapconductor.googlemaps.marker.GoogleMapMarkerController import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer +import com.mapconductor.googlemaps.polygon.GoogleMapPolygonRenderer import com.mapconductor.googlemaps.polyline.GoogleMapPolylineController import com.mapconductor.googlemaps.polyline.GoogleMapPolylineOverlayRenderer import android.app.Activity @@ -20,12 +22,12 @@ import android.content.ContextWrapper typealias GoogleMapViewHolder = MapViewHolder -object GoogleMapViewControllerStore : StaticHolder() { +object GoogleMapViewControllerStore : StaticHolder() { suspend fun getOrCreate( context: Context, id: String, options: GoogleMapOptions, - ): GoogleMapViewController { + ): GoogleMapViewControllerImpl { val existing = this.get(id) if (existing != null) { return existing @@ -38,10 +40,11 @@ object GoogleMapViewControllerStore : StaticHolder() { ) val controller = - GoogleMapViewController( + GoogleMapViewControllerImpl( markerController = getMarkerController(holder), groundImageController = getGroundImageController(holder), polylineController = getPolylineController(holder), + polygonController = getPolygonController(holder), holder = holder, ) this.set(id, controller) @@ -99,6 +102,20 @@ object GoogleMapViewControllerStore : StaticHolder() { return groundImageController } + + private fun getPolygonController(holder: GoogleMapViewHolder): PolygonController { + val polygonRenderer = + GoogleMapPolygonRenderer( + holder = holder, + ) + + val polygonController = + PolygonController( + renderer = polygonRenderer, + ) + + return polygonController + } } internal fun Context.findActivity(): Activity? = diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt index cf57ee74..88879caa 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt @@ -34,7 +34,7 @@ class GoogleMapViewStateImpl( private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() - internal var controller: IGoogleMapViewController? = null + internal var controller: GoogleMapViewController? = null override fun moveCameraTo( position: GeoPoint, diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt index 8fedaffa..101b0825 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt @@ -1,45 +1,27 @@ package com.mapconductor.googlemaps.polygon import androidx.compose.ui.graphics.toArgb -import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.PolygonOptions import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.AbstractPolygonRenderer import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonOverlayManagerImpl -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import com.mapconductor.core.polygon.PolygonRendererFactory import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.googlemaps.GoogleMapActualPolygon import com.mapconductor.googlemaps.GoogleMapViewHolder import com.mapconductor.googlemaps.toLatLng import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultGoogleMapPolygonRenderer : PolygonRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolygonOverlayManager = - PolygonOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - class GoogleMapPolygonRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolygonRenderer() { - override suspend fun addPolygons(newPolygons: List): List { + val holder: GoogleMapViewHolder, + val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : OverlayRenderer> { + override suspend fun onAdd(data: List): List { return withContext(coroutine.coroutineContext) { - return@withContext newPolygons.map { state -> + return@withContext data.map { state -> val points = state.points.map { GeoPoint.from(it).toLatLng() } val options = PolygonOptions() @@ -48,37 +30,43 @@ class GoogleMapPolygonRenderer( .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) .fillColor(state.fillColor.toArgb()) .clickable(true) - holder.map.addPolygon(options).also { + holder.map.addPolygon(options)?.also { it.tag = state.id } } } } - override suspend fun removePolygons(removeEntities: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { - removeEntities.forEach { params -> params.polygon.remove() } + data.forEach { entity -> entity.polygon.remove() } } } - override suspend fun changePolygon(changes: List>): List { + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + data: List>>, + ): List { return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val polygon = params.entity.polygon - val finger = params.entity.state.fingerPrint() - val prevFinger = params.prevEntity.state.fingerPrint() + return@withContext data.map { params -> + val polygon = params.current.polygon + val finger = params.current.fingerPrint + val prevFinger = params.prev.fingerPrint if (finger.points != prevFinger.points) { val points = - params.entity.state.points + params.current.state.points .map { GeoPoint.from(it).toLatLng() } polygon.points = points } - polygon.strokeWidth = ResourceProvider.dpToPx(params.entity.state.strokeWidth).toFloat() + polygon.strokeWidth = ResourceProvider.dpToPx(params.current.state.strokeWidth).toFloat() polygon.strokeColor = - params.entity.state.strokeColor + params.current.state.strokeColor .toArgb() polygon.fillColor = - params.entity.state.fillColor + params.current.state.fillColor .toArgb() return@map polygon } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index 92ca9c9d..d368baf1 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -38,7 +38,7 @@ fun HereMapView( ) { val holderRef = remember { Ref() } val scope = remember { HereMapViewScope() } - val controllerRef = remember { Ref() } + val controllerRef = remember { Ref() } val context = LocalContext.current val lifecycle = LocalLifecycleOwner.current.lifecycle val registry = remember { scope.buildRegistry() } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt similarity index 89% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt index f353b1d0..20fb2de2 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt @@ -28,24 +28,26 @@ import com.mapconductor.core.map.MapViewState.MoveCameraCallback import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonRendererFactory +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState import com.mapconductor.here.circle.DefaultHereMapCircleRenderer import com.mapconductor.here.circle.HereMapCircleRenderer import com.mapconductor.here.marker.HereMarkerController -import com.mapconductor.here.polygon.DefaultHereMapPolygonRenderer -import com.mapconductor.here.polygon.HereMapPolygonRenderer +import com.mapconductor.here.polygon.HerePolygonController import com.mapconductor.here.polyline.HerePolylineController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IHereMapViewController : - MapViewController, +interface HereMapViewController : + MapViewController, MarkerCapable, + PolygonCapable, PolylineCapable { fun moveCamera( dstPosition: MapCameraPosition, @@ -59,18 +61,15 @@ interface IHereMapViewController : ) } -class HereMapViewController( +class HereMapViewControllerImpl( private val markerController: HereMarkerController, private val polylineController: HerePolylineController, + private val polygonController: HerePolygonController, override val holder: MapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - private val polygonRendererFactory: PolygonRendererFactory = DefaultHereMapPolygonRenderer(), private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), -) : BaseMapViewController< - HereMapActualCircle, - HereMapActualPolygon, - >(), - IHereMapViewController, +) : BaseMapViewController(), + HereMapViewController, MapCameraListener, TapListener, LongPressListener { @@ -78,12 +77,6 @@ class HereMapViewController( private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 } - override val polygonRenderer = - HereMapPolygonRenderer( - holder = holder, - coroutine = coroutine, - ) - override val circleRenderer = HereMapCircleRenderer( holder = holder, @@ -93,16 +86,6 @@ class HereMapViewController( override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } - override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { - } - - override fun createPolygonOverlayManager(): PolygonOverlayManager = - polygonRendererFactory.create( - onAdd = polygonRenderer::addPolygons, - onChange = polygonRenderer::changePolygon, - onRemove = polygonRenderer::removePolygons, - ) - override fun createCircleOverlayManager(): CircleOverlayManager = circleRendererFactory.create( onAdd = circleRenderer::addCircles, @@ -113,7 +96,7 @@ class HereMapViewController( override suspend fun clearOverlays() { markerController.clear() polylineController.clear() - polygonOverlayManager.clearOverlays() + polygonController.clear() circleOverlayManager.clearOverlays() } @@ -153,9 +136,12 @@ class HereMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) + override suspend fun compositionPolygons(data: List) = polygonController.add(data) + + override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) + init { setupListeners() - polygonRenderer.init(polygonOverlayManager) circleRenderer.init(circleOverlayManager) } @@ -246,6 +232,18 @@ class HereMapViewController( return } + polygonController.find(touchPosition)?.let { entity -> + val event = + PolygonEvent( + state = entity.state, + clicked = touchPosition, + ) + coroutine.launch { + polygonController.clickListener?.invoke(event) + } + return + } + // If no overlay is processed, process the tap as onMapClick mapClickCallback?.invoke(touchPosition) } @@ -298,4 +296,8 @@ class HereMapViewController( override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { polylineController.clickListener = listener } + + override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { + polygonController.clickListener = listener + } } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt index 98443fe0..2664cacc 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt @@ -12,14 +12,18 @@ import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.projection.WebMercator import com.mapconductor.here.marker.HereMapMarkerRenderer import com.mapconductor.here.marker.HereMarkerController +import com.mapconductor.here.polygon.HereMapPolygonRenderer +import com.mapconductor.here.polygon.HerePolygonController import com.mapconductor.here.polyline.HerePolylineController import com.mapconductor.here.polyline.HerePolylineOverlayRenderer import android.content.Context import android.content.pm.PackageManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers typealias HereMapViewHolder = MapViewHolder -object HereMapViewControllerStore : StaticHolder() { +object HereMapViewControllerStore : StaticHolder() { private var mapCount: Int = 0 fun initSDK(context: Context) { @@ -55,7 +59,7 @@ object HereMapViewControllerStore : StaticHolder() { context: Context, id: String, options: HereMapViewInitOptions, - ): HereMapViewController { + ): HereMapViewControllerImpl { val existing = this.get(id) if (existing != null) { return existing @@ -86,10 +90,11 @@ object HereMapViewControllerStore : StaticHolder() { // } val controller = - HereMapViewController( + HereMapViewControllerImpl( holder = holder, markerController = getMarkerController(holder), polylineController = getPolylineController(holder), + polygonController = getPolygonController(holder), ) this.set(id, controller) return controller @@ -129,6 +134,20 @@ object HereMapViewControllerStore : StaticHolder() { return controller } + private fun getPolygonController(holder: HereMapViewHolder): HerePolygonController { + val renderer = + HereMapPolygonRenderer( + holder = holder, + coroutine = CoroutineScope(Dispatchers.Default), + ) + + val controller = + HerePolygonController( + renderer = renderer, + ) + return controller + } + // fun release() { // mapCount-- // if (mapCount > 0) return diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt index e40fb317..300eb401 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt @@ -32,7 +32,7 @@ class HereViewStateImpl( override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, ) : MapViewStateImpl(), HereViewState { - internal var controller: IHereMapViewController? = null + internal var controller: HereMapViewController? = null // Camera center position private val _cameraPosition = MutableStateFlow(initCameraPosition) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt index 15b35c8c..98191c5d 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt @@ -5,41 +5,22 @@ import com.here.sdk.core.Color import com.here.sdk.core.GeoPolygon import com.here.sdk.mapview.MapPolygon import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.AbstractPolygonRenderer import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonOverlayManagerImpl -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import com.mapconductor.core.polygon.PolygonRendererFactory import com.mapconductor.core.polygon.PolygonState import com.mapconductor.here.HereMapViewHolder import com.mapconductor.here.toGeoCoordinates import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class DefaultHereMapPolygonRenderer : PolygonRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolygonOverlayManager = - PolygonOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - class HereMapPolygonRenderer( - override val holder: HereMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractPolygonRenderer() { - override suspend fun addPolygons(newPolygons: List): List { + val holder: HereMapViewHolder, + val coroutine: CoroutineScope, +) : OverlayRenderer> { + override suspend fun onAdd(data: List): List { val polygons = - newPolygons.map { state -> + data.map { state -> val geoPolygon = createGeoPolygon(state) val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) MapPolygon( @@ -55,43 +36,49 @@ class HereMapPolygonRenderer( return polygons } - override suspend fun removePolygons(removeEntities: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { - removeEntities.forEach { holder.map.removeMapPolygon(it.polygon) } + data.forEach { holder.map.removeMapPolygon(it.polygon) } } } - override suspend fun changePolygon(changes: List>): List { - return changes.map { params -> - val finger = params.entity.state.fingerPrint() - val prevFinger = params.prevEntity.state.fingerPrint() + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + data: List>>, + ): List { + return data.map { params -> + val finger = params.current.fingerPrint + val prevFinger = params.prev.fingerPrint if (finger.points != prevFinger.points) { - val geoPolygon = createGeoPolygon(params.entity.state) - params.entity.polygon.geometry = geoPolygon + val geoPolygon = createGeoPolygon(params.current.state) + params.current.polygon.geometry = geoPolygon } if (finger.strokeColor != prevFinger.strokeColor) { - params.entity.polygon.outlineColor = + params.current.polygon.outlineColor = Color.valueOf( - params.entity.state.strokeColor + params.current.state.strokeColor .toArgb(), ) } if (finger.strokeWidth != prevFinger.strokeWidth) { val lineWidth = ResourceProvider.dpToPx( - params.entity.state.strokeWidth.value + params.current.state.strokeWidth.value .toDouble(), ) - params.entity.polygon.outlineWidth = lineWidth + params.current.polygon.outlineWidth = lineWidth } if (finger.fillColor != prevFinger.fillColor) { - params.entity.polygon.fillColor = + params.current.polygon.fillColor = Color.valueOf( - params.entity.state.fillColor + params.current.state.fillColor .toArgb(), ) } - return@map params.entity.polygon + return@map params.current.polygon } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index cc339308..737c56eb 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -12,6 +12,7 @@ import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineManagerImpl import com.mapconductor.core.projection.WebMercator @@ -37,11 +38,12 @@ fun MapboxMapView( onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, content: (@Composable MapboxMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } val context = LocalContext.current - val controllerRef = remember { Ref() } + val controllerRef = remember { Ref() } val scope = remember { MapboxMapViewScope() } val registry = remember { scope.buildRegistry() } @@ -71,7 +73,7 @@ fun MapboxMapView( val holder = MapboxMapViewHolderImpl.create(context, mapInitOptions) val controller = - MapboxMapViewController( + MapboxMapViewControllerImpl( holder = holder, markerController = getMarkerController(holder), polylineController = getPolylineController(holder), @@ -83,6 +85,7 @@ fun MapboxMapView( controller.setMapClickListener(onMapClick) controller.setCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) + controller.setOnPolygonClickListener(onPolygonClick) controller.setOnMarkerClickListener(onMarkerClick) controller.setOnMarkerDragStart(onMarkerDragStart) controller.setOnMarkerDrag(onMarkerDrag) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt similarity index 89% rename from mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt rename to mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt index 3c849525..5d805151 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt @@ -32,8 +32,11 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonRenderer +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState @@ -42,20 +45,20 @@ import com.mapconductor.mapbox.circle.MapboxCircleLayer import com.mapconductor.mapbox.circle.MapboxCircleRenderer import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.polygon.MapboxPolygonLayer -import com.mapconductor.mapbox.polygon.MapboxPolygonRenderer +import com.mapconductor.mapbox.polygon.createMapboxPolygonController import com.mapconductor.mapbox.polyline.MapboxPolylineController import android.animation.Animator import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface IMapboxMapViewController : +interface MapboxMapViewController : MapViewController< MapboxActualCircle, - MapboxActualPolygon, >, MarkerCapable, - PolylineCapable { + PolylineCapable, + PolygonCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -68,10 +71,11 @@ interface IMapboxMapViewController : ) } -internal class MapboxMapViewController( +internal class MapboxMapViewControllerImpl( override val holder: MapboxMapViewHolder, private val markerController: MapboxMarkerController, private val polylineController: MapboxPolylineController, + private val polygonController: MapboxPolygonController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), private val circleLayer: MapboxCircleLayer = MapboxCircleLayer( @@ -87,9 +91,8 @@ internal class MapboxMapViewController( DefaultMapboxCircleRenderer(), ) : BaseMapViewController< MapboxActualCircle, - MapboxActualPolygon, >(), - IMapboxMapViewController, + MapboxMapViewController, CameraChangedCallback, OnMapClickListener, OnMapLongClickListener, @@ -98,10 +101,6 @@ internal class MapboxMapViewController( private const val ZOOM_ADJUST_VALUE = 1.0 } - override fun createPolygonOverlayManager(): PolygonOverlayManager { - TODO("Not yet implemented") - } - override fun createCircleOverlayManager(): CircleOverlayManager = circleRendererFactory.create( onAdd = circleRenderer::addCircles, @@ -110,12 +109,6 @@ internal class MapboxMapViewController( onPostProcess = circleRenderer::redraw, ) - override val polygonRenderer: PolygonRenderer = - MapboxPolygonRenderer( - holder = holder, - coroutine = coroutine, - layer = polygonLayer, - ) override val circleRenderer: MapboxCircleRenderer = MapboxCircleRenderer( holder = holder, @@ -123,16 +116,22 @@ internal class MapboxMapViewController( layer = circleLayer, ) - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } + private val polygonController: PolygonController = + createMapboxPolygonController( + holder = holder, + coroutine = coroutine, + layer = polygonLayer, + ) - override fun onPolygonOverlayManagerInitialized(overlayManager: PolygonOverlayManager) { + override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } init { holder.map.getStyle { style -> style.addSource(circleLayer.source) style.addLayer(circleLayer.layer) + style.addSource(polygonLayer.source) + style.addLayer(polygonLayer.layer) style.addSource(polylineController.renderer.layer.source) style.addLayer(polylineController.renderer.layer.layer) style.addSource(markerController.renderer.markerLayer.source) @@ -158,6 +157,7 @@ internal class MapboxMapViewController( override suspend fun clearOverlays() { markerController.clear() polylineController.clear() + polygonController.clear() } override suspend fun compositionMarkers(data: List) = markerController.add(data) @@ -168,6 +168,10 @@ internal class MapboxMapViewController( override suspend fun updatePolyline(state: PolylineState) = polylineController.update(state) + override suspend fun compositionPolygons(data: List) = polygonController.add(data) + + override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) + override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) @@ -289,6 +293,16 @@ internal class MapboxMapViewController( return true } + polygonController.find(touchPosition)?.let { polygonEntity -> + val event = + PolygonEvent( + state = polygonEntity.state, + clicked = touchPosition, + ) + polygonClickCallback?.invoke(event) + return true + } + mapClickCallback?.invoke(touchPosition) return true } @@ -367,4 +381,8 @@ internal class MapboxMapViewController( override fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) { polylineController.clickListener = listener } + + override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { + this.polygonClickCallback = listener + } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt index cda6bb27..35c1f7e6 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt @@ -26,7 +26,7 @@ class MapboxViewStateImpl( override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, ) : MapViewStateImpl(), MapboxViewState { - internal var controller: IMapboxMapViewController? = null + internal var controller: MapboxMapViewController? = null // Camera center position private val _cameraPosition = MutableStateFlow(initCameraPosition) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt index b39e00d4..f2656b3a 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt @@ -5,13 +5,9 @@ import com.mapbox.geojson.Feature import com.mapbox.geojson.Polygon import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures import com.mapbox.maps.extension.style.sources.updateGeoJSONSourceFeatures +import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.AbstractPolygonRenderer import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonOverlayManager -import com.mapconductor.core.polygon.PolygonOverlayManagerImpl -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import com.mapconductor.core.polygon.PolygonRendererFactory import com.mapconductor.core.polygon.PolygonState import com.mapconductor.mapbox.MapboxActualPolygon import com.mapconductor.mapbox.MapboxMapViewHolder @@ -20,29 +16,14 @@ import com.mapconductor.mapbox.toPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class DefaultMapboxPolygonRenderer : PolygonRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): PolygonOverlayManager = - PolygonOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - class MapboxPolygonRenderer( - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope, + val holder: MapboxMapViewHolder, + val coroutine: CoroutineScope, private val layer: MapboxPolygonLayer, -) : AbstractPolygonRenderer() { - override suspend fun addPolygons(newPolygons: List): List { +) : OverlayRenderer> { + override suspend fun onAdd(data: List): List { val polygons = - newPolygons.map { state -> + data.map { state -> val points = state.points.map { GeoPoint.from(it).toPoint() } // Close the polygon by adding the first point at the end if not already closed val closedPoints = @@ -64,15 +45,21 @@ class MapboxPolygonRenderer( return polygons } - override suspend fun removePolygons(removeEntities: List>) { - val featureIds = removeEntities.map { "polygon-${it.state.id}" } + override suspend fun onRemove(data: List>) { + val featureIds = data.map { "polygon-${it.state.id}" } layer.source.removeGeoJSONSourceFeatures(featureIds) } - override suspend fun changePolygon(changes: List>): List { + override suspend fun onPostProcess() { + // Do nothing here + } + + override suspend fun onChange( + data: List>>, + ): List { val features = - changes.map { params -> - val state = params.entity.state + data.map { params -> + val state = params.current.state val points = state.points.map { GeoPoint.from(it).toPoint() } // Close the polygon by adding the first point at the end if not already closed val closedPoints = @@ -96,9 +83,10 @@ class MapboxPolygonRenderer( } fun redraw() { - val polygons = polygonOverlayManager.getAllEntities() + // Note: This method needs to be updated to work with the new architecture + // when a polygon overlay manager is available coroutine.launch { - layer.draw(polygons) + // TODO: Update this method when MapViewController integration is complete } } } From 39e4875d02522547a1b36c292ad7a78ab6d76ed1 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 23 Aug 2025 00:24:53 +0900 Subject: [PATCH 27/44] wip: refactoring for polygon --- .../example/pages/polygon/PolygonMapPage.kt | 124 ++++++++++++++++ .../pages/polygon/PolygonMapPageViewModel.kt | 118 +++++++++++++++ .../core/polygon/PolygonController.kt | 137 +++++++++++++++++ .../core/polygon/PolygonOverlay.kt | 16 ++ .../here/polygon/HerePolygonController.kt | 140 ++++++++++++++++++ .../mapbox/polygon/MapboxPolygonController.kt | 24 +++ 6 files changed, 559 insertions(+) create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt create mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt new file mode 100644 index 00000000..b5e7bdd8 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt @@ -0,0 +1,124 @@ +package com.mapconductor.example.pages.polygon + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import com.mapconductor.example.toast.ToastHost +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.MessageCard +import com.mapconductor.example.ui.PolygonCapableMapViewItems + +@Composable +fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { + val viewModel: PolygonMapPageViewModel = + viewModel( + factory = + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(PolygonMapPageViewModelImpl::class.java)) { + @Suppress("UNCHECKED_CAST") + return PolygonMapPageViewModelImpl() as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } + }, + ) + + DemoMapPageScaffold( + menuItems = PolygonCapableMapViewItems(viewModel.initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = viewModel::onMapViewChanged, + ) { paddingValues -> + val mapViewState = viewModel.mapViewState.collectAsState() + + Column { + // Polygon Controls + Column( + modifier = + Modifier.padding( + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr), + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr), + top = paddingValues.calculateTopPadding(), + ), + ) { + MessageCard( + title = "Polygon Example", + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Text( + "This example demonstrates polygon rendering with draggable vertices. " + + "Drag the markers to change the polygon shape.", + ) + } + + // Fill Opacity Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Fill Opacity: ${String.format("%.1f", viewModel.fillOpacity)}") + Slider( + value = viewModel.fillOpacity, + onValueChange = { viewModel.fillOpacity = it }, + valueRange = 0f..1f, + colors = + SliderDefaults.colors( + thumbColor = Color.Blue, + activeTrackColor = Color.Blue, + ), + ) + } + + // Stroke Width Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Stroke Width: ${String.format("%.1f", viewModel.strokeWidth)}dp") + Slider( + value = viewModel.strokeWidth, + onValueChange = { viewModel.strokeWidth = it }, + valueRange = 1f..10f, + colors = + SliderDefaults.colors( + thumbColor = Color.Blue, + activeTrackColor = Color.Blue, + ), + ) + } + } + + // Map Component + PolygonMapComponent( + mapViewState = mapViewState.value, + viewModel = viewModel, + modifier = + Modifier.padding( + bottom = paddingValues.calculateBottomPadding(), + ), + onPolygonClick = viewModel::onPolygonClick, + onMarkerDrag = viewModel::onMarkerDrag, + ) + } + + // Toast messages + ToastHost( + messages = viewModel.messages.collectAsState().value, + onDismiss = viewModel::removeToast, + ) + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt new file mode 100644 index 00000000..30d598ba --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt @@ -0,0 +1,118 @@ +package com.mapconductor.example.pages.polygon + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.example.toast.ToastMessage +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface PolygonMapPageViewModel { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + val messages: StateFlow> + + val polygonVertexMarkers: List + var fillOpacity: Float + var strokeWidth: Float + val polygonState: PolygonState + + fun onMapViewChanged(state: MapViewState<*>) + + fun onPolygonClick(clicked: PolygonEvent) + + fun onMarkerDrag(dragged: MarkerState) + + fun showToast(text: String) + + fun removeToast(toastMessage: ToastMessage) +} + +class PolygonMapPageViewModelImpl : + ViewModel(), + PolygonMapPageViewModel { + override val initCameraPosition = + MapCameraPosition( + position = GeoPoint(35.6762, 139.6503), // Tokyo + zoom = 12.0, + ) + + private val _mapViewState = MutableStateFlow?>(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + private val _messages = MutableStateFlow>(emptyList()) + override val messages: StateFlow> = _messages.asStateFlow() + + // Polygon vertices (triangle around Tokyo landmarks) + private val polygonVertices = + mutableListOf( + GeoPoint(35.6794, 139.6515), // Near Tokyo Station + GeoPoint(35.6682, 139.6594), // Near Tokyo Bay + GeoPoint(35.6856, 139.6539), // Near Imperial Palace + ) + + override var fillOpacity by mutableStateOf(0.3f) + override var strokeWidth by mutableStateOf(3.0f) + + override val polygonVertexMarkers: List = + polygonVertices.mapIndexed { index, point -> + MarkerState( + position = point, + icon = DefaultIcon(), + id = "vertex_$index", + draggable = true, + ) + } + + override val polygonState: PolygonState + get() = + PolygonState( + points = polygonVertices, + id = "example_polygon", + strokeColor = Color.Blue, + strokeWidth = strokeWidth.dp, + fillColor = Color.Blue.copy(alpha = fillOpacity), + geodesic = false, + ) + + override fun onMapViewChanged(state: MapViewState<*>) { + _mapViewState.value = state + } + + override fun onPolygonClick(clicked: PolygonEvent) { + val clickedPoint = clicked.clicked + if (clickedPoint != null) { + showToast("Polygon clicked at: ${clickedPoint.latitude}, ${clickedPoint.longitude}") + } else { + showToast("Polygon clicked") + } + } + + override fun onMarkerDrag(dragged: MarkerState) { + when (dragged.id) { + "vertex_0" -> polygonVertices[0] = GeoPoint.from(dragged.position) + "vertex_1" -> polygonVertices[1] = GeoPoint.from(dragged.position) + "vertex_2" -> polygonVertices[2] = GeoPoint.from(dragged.position) + } + } + + override fun showToast(text: String) { + val newMessage = ToastMessage(text = text) + _messages.value = _messages.value + newMessage + } + + override fun removeToast(toastMessage: ToastMessage) { + _messages.value = _messages.value - toastMessage + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt new file mode 100644 index 00000000..d19be09c --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt @@ -0,0 +1,137 @@ +package com.mapconductor.core.polygon + +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.controller.OverlayRenderer +import com.mapconductor.core.features.IGeoPoint +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +class PolygonController( + val renderer: OverlayRenderer>, + override var clickListener: OnPolygonEventHandler? = null, +) : OverlayController< + ActualPolygon, + PolygonState, + PolygonEntity, + PolygonEvent, + > { + override val zIndex: Int = 3 + val entities = mutableMapOf>() + val semaphore = Semaphore(1) + + override suspend fun add(data: List) { + semaphore.withPermit { + val previous = entities.keys.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>>() + val removed = mutableListOf>() + + data.forEach { + if (previous.contains(it.id)) { + val prevEntity = entities[it.id]!! + updated.add( + object : OverlayRenderer.ChangeParams> { + override val current = + PolygonEntityImpl( + polygon = prevEntity.polygon, + state = it, + ) + override val prev = prevEntity + }, + ) + previous.remove(it.id) + } else { + added.add(it) + previous.remove(it.id) + } + } + + previous.forEach { remainId -> + entities.remove(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + if (added.isNotEmpty()) { + val actualOverlays = renderer.onAdd(added) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val state = added[index] + val entity = + PolygonEntityImpl( + polygon = it, + state = state, + ) + entities[state.id] = entity + } + } + } + + if (updated.isNotEmpty()) { + val actualOverlays = renderer.onChange(updated.toList()) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val state = updated[index].current.state + val entity = + PolygonEntityImpl( + polygon = it, + state = state, + ) + entities[state.id] = entity + } + } + } + + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + renderer.onPostProcess() + } + } + + override suspend fun update(state: PolygonState) { + semaphore.withPermit { + val updated = mutableListOf>>() + val prevEntity = entities[state.id]!! + updated.add( + object : OverlayRenderer.ChangeParams> { + override val current: PolygonEntity = + PolygonEntityImpl( + polygon = prevEntity.polygon, + state = state, + ) + override val prev: PolygonEntity = prevEntity + }, + ) + + val actualOverlays: List = renderer.onChange(updated) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val entity = + PolygonEntityImpl( + polygon = it, + state = state, + ) + entities[state.id] = entity + } + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + renderer.onRemove(entities.values.toList()) + entities.clear() + } + } + + override fun find(position: IGeoPoint): PolygonEntity? { + // TODO: Improve this implementation later + return entities.values.find { entity -> + // For polygons, we need a point-in-polygon check + // This is a simplified version - would need proper implementation + entity.state.points.isNotEmpty() + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt new file mode 100644 index 00000000..9498957f --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt @@ -0,0 +1,16 @@ +package com.mapconductor.core.polygon + +import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.StateFlow + +class PolygonOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewControllerAlias, + ) { + (controller as? PolygonCapable)?.compositionPolygons(data) + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt new file mode 100644 index 00000000..3d9bd5e3 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt @@ -0,0 +1,140 @@ +package com.mapconductor.here.polygon +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.controller.OverlayRenderer +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonEntityImpl +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.here.HereMapActualPolygon +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +class HerePolygonController( + val renderer: OverlayRenderer>, + override var clickListener: OnPolygonEventHandler? = null, +) : OverlayController< + HereMapActualPolygon, + PolygonState, + PolygonEntity, + PolygonEvent, + > { + override val zIndex: Int = 3 + val entities = mutableMapOf>() + val semaphore = Semaphore(1) + + override suspend fun add(data: List) { + semaphore.withPermit { + val previous = entities.keys.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>>() + val removed = mutableListOf>() + + data.forEach { + if (previous.contains(it.id)) { + val prevEntity = entities[it.id]!! + updated.add( + object : OverlayRenderer.ChangeParams> { + override val current = + PolygonEntityImpl( + polygon = prevEntity.polygon, + state = it, + ) + override val prev = prevEntity + }, + ) + previous.remove(it.id) + } else { + added.add(it) + previous.remove(it.id) + } + } + + previous.forEach { remainId -> + entities.remove(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + if (added.isNotEmpty()) { + val actualOverlays = renderer.onAdd(added) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val state = added[index] + val entity = + PolygonEntityImpl( + polygon = it, + state = state, + ) + entities[state.id] = entity + } + } + } + + if (updated.isNotEmpty()) { + val actualOverlays = renderer.onChange(updated.toList()) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val state = updated[index].current.state + val entity = + PolygonEntityImpl( + polygon = it, + state = state, + ) + entities[state.id] = entity + } + } + } + + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + renderer.onPostProcess() + } + } + + override suspend fun update(state: PolygonState) { + semaphore.withPermit { + val updated = mutableListOf>>() + val prevEntity = entities[state.id]!! + updated.add( + object : OverlayRenderer.ChangeParams> { + override val current: PolygonEntity = + PolygonEntityImpl( + polygon = prevEntity.polygon, + state = state, + ) + override val prev: PolygonEntity = prevEntity + }, + ) + + val actualOverlays: List = renderer.onChange(updated) + actualOverlays.forEachIndexed { index, actualOverlay -> + actualOverlay?.let { + val entity = + PolygonEntityImpl( + polygon = it, + state = state, + ) + entities[state.id] = entity + } + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + renderer.onRemove(entities.values.toList()) + entities.clear() + } + } + + override fun find(position: IGeoPoint): PolygonEntity? { + // TODO: Improve this implementation later with proper point-in-polygon check + return entities.values.find { entity -> + entity.state.points.isNotEmpty() + } + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt new file mode 100644 index 00000000..a661c532 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt @@ -0,0 +1,24 @@ +package com.mapconductor.mapbox.polygon + +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.mapbox.MapboxActualPolygon +import com.mapconductor.mapbox.MapboxMapViewHolder +import kotlinx.coroutines.CoroutineScope + +class MapboxPolygonController( + override val renderer: MapboxPolygonRenderer, + polygonManager: PolygonManager<> +) +fun createMapboxPolygonController( + holder: MapboxMapViewHolder, + coroutine: CoroutineScope, + layer: MapboxPolygonLayer, +): PolygonController = + PolygonController( + renderer = + MapboxPolygonRenderer( + holder = holder, + coroutine = coroutine, + layer = layer, + ), + ) From 62b7bbfa9191afd6bde8e09ba4d46b88cefc35d7 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Mon, 25 Aug 2025 22:35:39 +0900 Subject: [PATCH 28/44] wip: implementing polygon --- .../core/groundimage/GroundImageCapable.kt | 9 ++ .../core/groundimage/GroundImageController.kt | 8 -- .../core/polygon/AbstractPolygonController.kt | 4 - .../polygon/AbstractPolygonOverlayRenderer.kt | 4 + .../core/polygon/PolygonCapable.kt | 9 ++ .../core/polygon/PolygonController.kt | 4 +- .../core/polygon/PolygonManager.kt | 33 +++++ .../core/polygon/PolygonOverlayManager.kt | 119 ------------------ .../core/polygon/PolygonOverlayRenderer.kt | 16 +++ .../core/polygon/PolygonRenderer.kt | 38 ------ .../core/polyline/PolylineCapable.kt | 9 ++ ...ineController.kt => PolylineController.kt} | 70 +---------- .../core/polyline/PolylineManager.kt | 38 ++++++ .../core/polyline/PolylineOverlayRenderer.kt | 20 +++ 14 files changed, 145 insertions(+), 236 deletions(-) create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageCapable.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonController.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonCapable.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonRenderer.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineCapable.kt rename mapconductor-core/src/main/java/com/mapconductor/core/polyline/{AbstractPolylineController.kt => PolylineController.kt} (72%) create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayRenderer.kt diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageCapable.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageCapable.kt new file mode 100644 index 00000000..cab5cba3 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageCapable.kt @@ -0,0 +1,9 @@ +package com.mapconductor.core.groundimage + +interface GroundImageCapable { + suspend fun compositionGroundImages(data: List) + + suspend fun updateGroundImage(state: GroundImageState) + + fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt index 78199fe3..29a2e5d0 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt @@ -6,14 +6,6 @@ import com.mapconductor.core.features.IGeoPoint import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -interface GroundImageCapable { - suspend fun compositionGroundImages(data: List) - - suspend fun updateGroundImage(state: GroundImageState) - - fun setOnGroundImageClickListener(listener: OnGroundImageEventHandler?) -} - class GroundImageController( val renderer: OverlayRenderer>, override var clickListener: OnGroundImageEventHandler? = null, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonController.kt deleted file mode 100644 index d03d3296..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonController.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.mapconductor.core.polygon - -abstract class AbstractPolygonController { -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt new file mode 100644 index 00000000..f75a88e5 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt @@ -0,0 +1,4 @@ +package com.mapconductor.core.polygon + +abstract class AbstractPolygonOverlayRenderer : PolygonOverlay { +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonCapable.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonCapable.kt new file mode 100644 index 00000000..6bef04bf --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonCapable.kt @@ -0,0 +1,9 @@ +package com.mapconductor.core.polygon + +interface PolygonCapable { + suspend fun compositionPolygons(data: List) + + suspend fun updatePolygon(state: PolygonState) + + fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt index d19be09c..2dc34440 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit class PolygonController( + val polygonManager: PolygonManager, val renderer: OverlayRenderer>, override var clickListener: OnPolygonEventHandler? = null, ) : OverlayController< @@ -16,12 +17,11 @@ class PolygonController( PolygonEvent, > { override val zIndex: Int = 3 - val entities = mutableMapOf>() val semaphore = Semaphore(1) override suspend fun add(data: List) { semaphore.withPermit { - val previous = entities.keys.toMutableSet() + val previous = polygonManager.allEntities().map { it.state.id } val added = mutableListOf() val updated = mutableListOf>>() val removed = mutableListOf>() diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt new file mode 100644 index 00000000..f4b2739a --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt @@ -0,0 +1,33 @@ +package com.mapconductor.core.polygon + +import com.mapconductor.core.features.IGeoPoint + +interface PolygonManager { + fun registerEntity(entity: PolygonEntity) + fun removeEntity(id: String): PolygonEntity? + fun getEntity(id: String): PolygonEntity? + fun allEntities(): List> + fun clear() + fun find(position: IGeoPoint): PolygonEntity? +} + +class PolygonManagerImpl : PolygonManager { + private val entities = mutableMapOf>() + + override fun registerEntity(entity: PolygonEntity) { + entities[entity.state.id] = entity + } + + override fun removeEntity(id: String): PolygonEntity? = entities.remove(id) + + override fun getEntity(id: String): PolygonEntity? = entities[id] + + override fun allEntities(): List> = entities.values.toList() + + override fun clear() { + entities.clear() + } + + override fun find(position: IGeoPoint): PolygonEntity? = entities.values.firstOrNull() + +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt deleted file mode 100644 index c2a0b35e..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayManager.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.mapconductor.core.polygon - -import com.mapconductor.core.polygon.PolygonRenderer.UpdateParams -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit - -interface PolygonCapable { - suspend fun compositionPolygons(data: List) - - suspend fun updatePolygon(state: PolygonState) - - fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) -} - -interface PolygonOverlayManager { - suspend fun addPolygons(polygons: List) - - suspend fun updatePolygon(polygon: PolygonState) - - suspend fun clearOverlays() - - fun getPolygonState(id: String): PolygonState? - - fun getAllEntities(): List> -} - -class PolygonOverlayManagerImpl( - val onAdd: suspend (List) -> List, - val onChange: suspend (List>) -> List, - val onRemove: suspend (List>) -> Unit, - val onPostProcess: (suspend () -> Unit)? = null, -) : PolygonOverlayManager { - val polygonEntities = mutableMapOf>() - - val semaphore = Semaphore(1) - - override suspend fun addPolygons(polygons: List) { - semaphore.withPermit { - val previous = polygonEntities.keys.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>() - val removed = mutableListOf>() - polygons.forEach { - if (previous.contains(it.id)) { - val prevEntity = polygonEntities.get(it.id)!! - updated.add( - object : UpdateParams { - override val entity: PolygonEntity = - PolygonEntityImpl( - state = it, - polygon = prevEntity.polygon, - ) - override val prevEntity: PolygonEntity = prevEntity - }, - ) - previous.remove(it.id) - return@forEach - } - added.add(it) - previous.remove(it.id) - } - previous.forEach { remainId -> - polygonEntities.remove(remainId)?.let { removedEntity -> - removed.add(removedEntity) - } - } - - if (added.isNotEmpty()) { - val actualPolygons = onAdd(added) - actualPolygons.forEachIndexed { index, actualPolygon -> - actualPolygon?.let { - val state = added[index] - val entity = - PolygonEntityImpl( - polygon = it, - state = state, - ) - polygonEntities[state.id] = entity - } - } - } - - if (updated.isNotEmpty()) { - val actualPolygons: List = onChange(updated) - actualPolygons.forEachIndexed { index, actualPolygon -> - actualPolygon?.let { - val state = updated[index].entity.state - val entity = - PolygonEntityImpl( - polygon = it, - state = state, - ) - polygonEntities[state.id] = entity - } - } - } - - if (removed.isNotEmpty()) { - onRemove(removed) - } - onPostProcess?.invoke() - } - } - - override suspend fun updatePolygon(polygon: PolygonState) { - } - - override suspend fun clearOverlays() { - semaphore.withPermit { - val entities = polygonEntities.values.toList() - onRemove(entities) - polygonEntities.clear() - } - } - - override fun getPolygonState(id: String): PolygonState? = polygonEntities.get(id)?.state - - override fun getAllEntities(): List> = polygonEntities.values.toList() -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt new file mode 100644 index 00000000..493083a6 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt @@ -0,0 +1,16 @@ +package com.mapconductor.core.polygon + +interface PolygonOverlayRenderer { + interface AddParams { + val state: PolygonState + } + interface ChangeParams { + val current: PolygonEntity + val prev: PolygonEntity + } + + suspend fun onAdd(data: List): List + suspend fun onChange(data: List>): List + suspend fun onRemove(data: PolygonEntity) + suspend fun onPostProcess() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonRenderer.kt deleted file mode 100644 index b9fc8a23..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonRenderer.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.mapconductor.core.polygon - -import com.mapconductor.core.map.MapViewHolder -import kotlinx.coroutines.CoroutineScope - -interface PolygonRendererFactory { - fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)? = null, - ): PolygonOverlayManager -} - -interface PolygonRenderer { - interface UpdateParams { - val entity: PolygonEntity - val prevEntity: PolygonEntity - } - - fun init(polygonOverlayManager: PolygonOverlayManager) - - suspend fun addPolygons(newPolygons: List): List - - suspend fun removePolygons(removeEntities: List>) - - suspend fun changePolygon(changes: List>): List -} - -abstract class AbstractPolygonRenderer : PolygonRenderer { - protected lateinit var polygonOverlayManager: PolygonOverlayManager - abstract val holder: MapViewHolder<*, *> - abstract val coroutine: CoroutineScope - - override fun init(polygonOverlayManager: PolygonOverlayManager) { - this.polygonOverlayManager = polygonOverlayManager - } -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineCapable.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineCapable.kt new file mode 100644 index 00000000..de2a9406 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineCapable.kt @@ -0,0 +1,9 @@ +package com.mapconductor.core.polyline + +interface PolylineCapable { + suspend fun compositionPolylines(data: List) + + suspend fun updatePolyline(state: PolylineState) + + fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt similarity index 72% rename from mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt rename to mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt index 0cd94797..61423b16 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/AbstractPolylineController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt @@ -5,42 +5,16 @@ import com.mapconductor.core.features.IGeoPoint import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -interface PolylineCapable { - suspend fun compositionPolylines(data: List) - - suspend fun updatePolyline(state: PolylineState) - - fun setOnPolylineClickListener(listener: OnPolylineEventHandler?) -} - -interface PolylineOverlayRenderer { - interface AddParams { - val state: PolylineState - } - - interface ChangeParams { - val current: PolylineEntity - val prev: PolylineEntity - } - - suspend fun onAdd(data: List): List - - suspend fun onChange(data: List>): List - - suspend fun onRemove(data: List>) - - suspend fun onPostProcess() -} abstract class PolylineController( val polylineManager: PolylineManager, open val renderer: PolylineOverlayRenderer, override var clickListener: OnPolylineEventHandler? = null, ) : OverlayController< - ActualPolyline, - PolylineState, - PolylineEntity, - PolylineState, + ActualPolyline, + PolylineState, + PolylineEntity, + PolylineState, > { override val zIndex: Int = 5 val semaphore = Semaphore(1) @@ -166,39 +140,5 @@ abstract class PolylineController( } } - override fun find(position: IGeoPoint): PolylineEntity? = polylineManager.findNearest(position) -} - -interface PolylineManager { - fun registerEntity(entity: PolylineEntity) - - fun removeEntity(id: String): PolylineEntity? - - fun getEntity(id: String): PolylineEntity? - - fun allEntities(): List> - - fun clear() - - fun findNearest(position: IGeoPoint): PolylineEntity? -} - -class PolylineManagerImpl : PolylineManager { - private val entities = mutableMapOf>() - - override fun registerEntity(entity: PolylineEntity) { - entities[entity.state.id] = entity - } - - override fun removeEntity(id: String): PolylineEntity? = entities.remove(id) - - override fun getEntity(id: String): PolylineEntity? = entities[id] - - override fun allEntities(): List> = entities.values.toList() - - override fun clear() { - entities.clear() - } - - override fun findNearest(position: IGeoPoint): PolylineEntity? = entities.values.firstOrNull() + override fun find(position: IGeoPoint): PolylineEntity? = polylineManager.find(position) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt new file mode 100644 index 00000000..8c4d0817 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt @@ -0,0 +1,38 @@ +package com.mapconductor.core.polyline + +import com.mapconductor.core.features.IGeoPoint + + +interface PolylineManager { + fun registerEntity(entity: PolylineEntity) + + fun removeEntity(id: String): PolylineEntity? + + fun getEntity(id: String): PolylineEntity? + + fun allEntities(): List> + + fun clear() + + fun find(position: IGeoPoint): PolylineEntity? +} + +class PolylineManagerImpl : PolylineManager { + private val entities = mutableMapOf>() + + override fun registerEntity(entity: PolylineEntity) { + entities[entity.state.id] = entity + } + + override fun removeEntity(id: String): PolylineEntity? = entities.remove(id) + + override fun getEntity(id: String): PolylineEntity? = entities[id] + + override fun allEntities(): List> = entities.values.toList() + + override fun clear() { + entities.clear() + } + + override fun find(position: IGeoPoint): PolylineEntity? = entities.values.firstOrNull() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayRenderer.kt new file mode 100644 index 00000000..7b2ecad7 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayRenderer.kt @@ -0,0 +1,20 @@ +package com.mapconductor.core.polyline + +interface PolylineOverlayRenderer { + interface AddParams { + val state: PolylineState + } + + interface ChangeParams { + val current: PolylineEntity + val prev: PolylineEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onPostProcess() +} From 8b2932b875abf41fae9e7e01159802ff86c22596 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Thu, 28 Aug 2025 09:35:51 +0900 Subject: [PATCH 29/44] wip: PolygonController --- .../polygon/AbstractPolygonOverlayRenderer.kt | 43 +++++- .../core/polygon/PolygonController.kt | 137 +++++++++--------- .../core/polygon/PolygonOverlayRenderer.kt | 4 +- .../core/polyline/PolylineController.kt | 12 +- .../arcgis/ArcGISMapViewControllerImpl.kt | 10 -- .../arcgis/polygon/ArcGISPolygonController.kt | 11 ++ .../polyline/ArcGISPolylineController.kt | 19 +-- .../mapbox/MapboxMapViewControllerImpl.kt | 8 +- .../mapbox/polygon/MapboxPolygonController.kt | 52 +++++-- 9 files changed, 179 insertions(+), 117 deletions(-) create mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt index f75a88e5..45c7376d 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt @@ -1,4 +1,45 @@ package com.mapconductor.core.polygon -abstract class AbstractPolygonOverlayRenderer : PolygonOverlay { +import com.mapconductor.core.map.MapViewHolder +import com.mapconductor.core.polyline.PolylineState +import android.icu.util.Currency +import kotlinx.coroutines.CoroutineScope + +abstract class AbstractPolygonOverlayRenderer : PolygonOverlayRenderer { + abstract val holder: MapViewHolder<*, *> + abstract val coroutine: CoroutineScope + + override suspend fun onPostProcess() { + // Default implementation - can be overridden by subclasses + } + + abstract suspend fun removePolygon(entity: PolygonEntity) + + abstract suspend fun createPolygon(state: PolygonState): ActualPolygon? + + abstract suspend fun updatePolygonProperties( + polygon: ActualPolygon, + current: PolygonEntity, + prev: PolygonEntity, + ) : ActualPolygon? + + override suspend fun onAdd(data: List): List = + data.map { params -> createPolygon(params.state) } + + override suspend fun onChange( + data: List>, + ): List = + data.map { params -> + updatePolygonProperties( + polygon = params.prev.polygon, + current = params.current, + prev = params.prev, + ) + } + + override suspend fun onRemove(data: List>) { + data.forEach { entity -> + removePolygon(entity) + } + } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt index 2dc34440..0a39fbbd 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt @@ -1,14 +1,13 @@ package com.mapconductor.core.polygon import com.mapconductor.core.controller.OverlayController -import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.IGeoPoint import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -class PolygonController( +abstract class PolygonController( val polygonManager: PolygonManager, - val renderer: OverlayRenderer>, + open val renderer: PolygonOverlayRenderer, override var clickListener: OnPolygonEventHandler? = null, ) : OverlayController< ActualPolygon, @@ -21,117 +20,123 @@ class PolygonController( override suspend fun add(data: List) { semaphore.withPermit { - val previous = polygonManager.allEntities().map { it.state.id } - val added = mutableListOf() - val updated = mutableListOf>>() + val modifiedEntities = mutableListOf>() + val previous = polygonManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() val removed = mutableListOf>() - data.forEach { - if (previous.contains(it.id)) { - val prevEntity = entities[it.id]!! + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = polygonManager.getEntity(state.id)!! updated.add( - object : OverlayRenderer.ChangeParams> { - override val current = + object : PolygonOverlayRenderer.ChangeParams { + override val current: PolygonEntity = PolygonEntityImpl( + state = state, polygon = prevEntity.polygon, - state = it, ) - override val prev = prevEntity + override val prev: PolygonEntity = prevEntity }, ) - previous.remove(it.id) + previous.remove(state.id) } else { - added.add(it) - previous.remove(it.id) + added.add( + object : PolygonOverlayRenderer.AddParams { + override val state: PolygonState = state + } + ) + previous.remove(state.id) } } previous.forEach { remainId -> - entities.remove(remainId)?.let { removedEntity -> + polygonManager.removeEntity(remainId)?.let { removedEntity -> removed.add(removedEntity) } } + // Remove polygon + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + // Add new polygons if (added.isNotEmpty()) { - val actualOverlays = renderer.onAdd(added) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val state = added[index] + val actualPolygons: List = renderer.onAdd(added) + actualPolygons.forEachIndexed { index, polygon -> + polygon?.let { val entity = PolygonEntityImpl( - polygon = it, - state = state, + polygon = polygon, + state = added[index].state, ) - entities[state.id] = entity + polygonManager.registerEntity(entity) + modifiedEntities.add(entity) } } } + // Update changed polygons if (updated.isNotEmpty()) { - val actualOverlays = renderer.onChange(updated.toList()) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val state = updated[index].current.state + val actualPolygons: List = renderer.onChange(updated) + actualPolygons.forEachIndexed { index, polygon -> + polygon?.let { + val params = updated[index] val entity = PolygonEntityImpl( - polygon = it, - state = state, + state = params.current.state, + polygon = polygon, ) - entities[state.id] = entity + polygonManager.registerEntity(entity) } } } - if (removed.isNotEmpty()) { - renderer.onRemove(removed) - } - renderer.onPostProcess() } } override suspend fun update(state: PolygonState) { semaphore.withPermit { - val updated = mutableListOf>>() - val prevEntity = entities[state.id]!! - updated.add( - object : OverlayRenderer.ChangeParams> { - override val current: PolygonEntity = - PolygonEntityImpl( - polygon = prevEntity.polygon, - state = state, - ) - override val prev: PolygonEntity = prevEntity - }, - ) + val prevEntity = polygonManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinger = prevEntity.fingerPrint + if (currentFinger == prevFinger) { + return + } - val actualOverlays: List = renderer.onChange(updated) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val entity = - PolygonEntityImpl( - polygon = it, - state = state, - ) - entities[state.id] = entity + val polygon = prevEntity.polygon + val entity = + PolygonEntityImpl( + polygon = polygon, + state = state, + ) + val polygonParams = + object : PolygonOverlayRenderer.ChangeParams { + override val current: PolygonEntity = entity + override val prev: PolygonEntity = prevEntity } + val polygons = renderer.onChange(listOf(polygonParams)) + + polygons[0]?.let { + val entity = + PolygonEntityImpl( + polygon = it, + state = state, + ) + polygonManager.registerEntity(entity) } } } override suspend fun clear() { semaphore.withPermit { - renderer.onRemove(entities.values.toList()) - entities.clear() + val entities: List> = polygonManager.allEntities() + renderer.onRemove(entities) + polygonManager.clear() } } - override fun find(position: IGeoPoint): PolygonEntity? { - // TODO: Improve this implementation later - return entities.values.find { entity -> - // For polygons, we need a point-in-polygon check - // This is a simplified version - would need proper implementation - entity.state.points.isNotEmpty() - } - } + override fun find(position: IGeoPoint): PolygonEntity? = polygonManager.find(position) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt index 493083a6..b1dbac53 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt @@ -9,8 +9,8 @@ interface PolygonOverlayRenderer { val prev: PolygonEntity } - suspend fun onAdd(data: List): List + suspend fun onAdd(data: List): List suspend fun onChange(data: List>): List - suspend fun onRemove(data: PolygonEntity) + suspend fun onRemove(data: List>) suspend fun onPostProcess() } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt index 61423b16..f1f08007 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt @@ -65,11 +65,11 @@ abstract class PolylineController( // Add new polylines if (added.isNotEmpty()) { val actualPolylines: List = renderer.onAdd(added) - actualPolylines.forEachIndexed { index, actualPolyline -> - actualPolyline?.let { + actualPolylines.forEachIndexed { index, polyline -> + polyline?.let { val entity = PolylineEntityImpl( - polyline = actualPolyline, + polyline = polyline, state = added[index].state, ) polylineManager.registerEntity(entity) @@ -82,13 +82,13 @@ abstract class PolylineController( if (updated.isNotEmpty()) { val actualPolylines: List = renderer.onChange(updated) - actualPolylines.forEachIndexed { index, actualPolyline -> - actualPolyline?.let { + actualPolylines.forEachIndexed { index, polyline -> + polyline?.let { val params = updated[index] val entity = PolylineEntityImpl( state = params.current.state, - polyline = actualPolyline, + polyline = polyline, ) polylineManager.registerEntity(entity) } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt index 03f02a6c..5ef4a291 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt @@ -91,16 +91,6 @@ class ArcGISMapViewControllerImpl( coroutine = coroutine, ) - private val polygonController: PolygonController = - PolygonController( - renderer = - ArcGISPolygonRenderer( - polygonLayer = polygonLayer, - holder = holder, - coroutine = coroutine, - ), - ) - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt new file mode 100644 index 00000000..1fcd495e --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.arcgis.polygon + +import com.mapconductor.arcgis.ArcGISActualPolygon +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonManagerImpl + +class ArcGISPolygonController( + polygonManager: PolygonManager = PolygonManagerImpl(), + override val renderer: ArcGISPolygonRenderer, +) : PolygonController(polygonManager, renderer) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt index 81e00780..1b1c17a8 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt @@ -6,6 +6,7 @@ import com.arcgismaps.mapping.symbology.SimpleLineSymbol import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay +import com.mapconductor.arcgis.ArcGISActualPolyline import com.mapconductor.arcgis.ArcGISMapViewHolder import com.mapconductor.arcgis.toArcGISColor import com.mapconductor.arcgis.toPoint @@ -24,16 +25,16 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ArcGISPolylineController( - polylineManager: PolylineManager = PolylineManagerImpl(), + polylineManager: PolylineManager = PolylineManagerImpl(), override val renderer: ArcGISPolylineOverlayRenderer, -) : PolylineController(polylineManager, renderer) +) : PolylineController(polylineManager, renderer) class ArcGISPolylineOverlayRenderer( val polylineLayer: GraphicsOverlay, override val holder: ArcGISMapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), -) : AbstractPolylineOverlayRenderer() { - override suspend fun createPolyline(state: PolylineState): Graphic? = +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): ArcGISActualPolyline? = withContext(coroutine.coroutineContext) { val geometry = createGeometry(state) @@ -54,10 +55,10 @@ class ArcGISPolylineOverlayRenderer( } override suspend fun updatePolylineProperties( - polyline: Graphic, - current: PolylineEntity, - prev: PolylineEntity, - ): Graphic? = + polyline: ArcGISActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): ArcGISActualPolyline? = withContext(coroutine.coroutineContext) { val finger = current.fingerPrint val prevFinger = prev.fingerPrint @@ -78,7 +79,7 @@ class ArcGISPolylineOverlayRenderer( polyline } - override suspend fun removePolyline(entity: PolylineEntity) { + override suspend fun removePolyline(entity: PolylineEntity) { coroutine.launch { polylineLayer.graphics.remove(entity.polyline) } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt index 5d805151..bb560f39 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt @@ -44,6 +44,7 @@ import com.mapconductor.mapbox.circle.DefaultMapboxCircleRenderer import com.mapconductor.mapbox.circle.MapboxCircleLayer import com.mapconductor.mapbox.circle.MapboxCircleRenderer import com.mapconductor.mapbox.marker.MapboxMarkerController +import com.mapconductor.mapbox.polygon.MapboxPolygonController import com.mapconductor.mapbox.polygon.MapboxPolygonLayer import com.mapconductor.mapbox.polygon.createMapboxPolygonController import com.mapconductor.mapbox.polyline.MapboxPolylineController @@ -116,13 +117,6 @@ internal class MapboxMapViewControllerImpl( layer = circleLayer, ) - private val polygonController: PolygonController = - createMapboxPolygonController( - holder = holder, - coroutine = coroutine, - layer = polygonLayer, - ) - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt index a661c532..df0ad85c 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt @@ -1,24 +1,44 @@ package com.mapconductor.mapbox.polygon +import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonState import com.mapconductor.mapbox.MapboxActualPolygon import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers class MapboxPolygonController( - override val renderer: MapboxPolygonRenderer, - polygonManager: PolygonManager<> -) -fun createMapboxPolygonController( - holder: MapboxMapViewHolder, - coroutine: CoroutineScope, - layer: MapboxPolygonLayer, -): PolygonController = - PolygonController( - renderer = - MapboxPolygonRenderer( - holder = holder, - coroutine = coroutine, - layer = layer, - ), - ) + override val renderer: MapboxPolygonOverlayRenderer, + polygonManager: PolygonManager = renderer.polygonManager, +) : PolygonController(polygonManager, renderer) + +class MapboxPolygonOverlayRenderer( + val layer: MapboxPolygonLayer, + val polygonManager: PolygonManager, + override val holder: MapboxMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolygonOverlayRenderer() { + override suspend fun createPolygon(state: PolygonState): MapboxActualPolygon? = createMapboxPolygons(state) + + override suspend fun updatePolygonProperties( + polygon: MapboxActualPolygon, + current: PolygonEntity, + prev: PolygonEntity + ): MapboxActualPolygon? { + return createMapboxPolygons(current.state) + } + + override suspend fun removePolygon(entity: PolygonEntity) { + val featureIds = + entity.polygon.map { feature -> + feature.getStringProperty("id") + } + layer.source.removeGeoJSONSourceFeatures(featureIds) + } +} From 4244ae1f2e85d1d7d90efa68f0e64079e223d5e8 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Thu, 28 Aug 2025 23:43:53 +0900 Subject: [PATCH 30/44] wip: implementing PolygonOverlayManager for Mapbox --- .../arcgis/ArcGISMapViewControllerImpl.kt | 10 +- .../arcgis/ArcGISViewControllerStore.kt | 22 ++++ .../arcgis/polygon/ArcGISPolygonController.kt | 104 +++++++++++++++- .../arcgis/polygon/ArcGISPolygonRenderer.kt | 114 ------------------ .../GoogleMapViewControllerStore.kt | 30 +++-- .../polygon/GoogleMapPolygonRenderer.kt | 101 ++++++++-------- .../polyline/GoogleMapPolylineController.kt | 17 +-- .../com/mapconductor/here/HereMapTypeAlias.kt | 10 -- .../java/com/mapconductor/here/HereMapView.kt | 8 +- .../com/mapconductor/here/HereTypeAlias.kt | 10 ++ ...ollerImpl.kt => HereViewControllerImpl.kt} | 10 +- ...lerStore.kt => HereViewControllerStore.kt} | 25 ++-- ...iewHolderImpl.kt => HereViewHolderImpl.kt} | 4 +- ...wInitOptions.kt => HereViewInitOptions.kt} | 2 +- .../{HereMapViewScope.kt => HereViewScope.kt} | 2 +- ...ircleRenderer.kt => HereCircleRenderer.kt} | 4 +- .../here/marker/HereMarkerController.kt | 14 +-- ...arkerRenderer.kt => HereMarkerRenderer.kt} | 24 ++-- .../here/polygon/HereMapPolygonRenderer.kt | 96 --------------- .../here/polygon/HerePolygonController.kt | 34 +++--- .../here/polygon/HerePolygonRenderer.kt | 105 ++++++++++++++++ .../here/polyline/HerePolylineController.kt | 66 +++++----- .../mapbox/polygon/MapboxPolygonController.kt | 25 ++++ 23 files changed, 439 insertions(+), 398 deletions(-) delete mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt delete mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/HereTypeAlias.kt rename mapconductor-for-here/src/main/java/com/mapconductor/here/{HereMapViewControllerImpl.kt => HereViewControllerImpl.kt} (97%) rename mapconductor-for-here/src/main/java/com/mapconductor/here/{HereMapViewControllerStore.kt => HereViewControllerStore.kt} (85%) rename mapconductor-for-here/src/main/java/com/mapconductor/here/{HereMapViewHolderImpl.kt => HereViewHolderImpl.kt} (93%) rename mapconductor-for-here/src/main/java/com/mapconductor/here/{HereMapViewInitOptions.kt => HereViewInitOptions.kt} (77%) rename mapconductor-for-here/src/main/java/com/mapconductor/here/{HereMapViewScope.kt => HereViewScope.kt} (64%) rename mapconductor-for-here/src/main/java/com/mapconductor/here/circle/{HereMapCircleRenderer.kt => HereCircleRenderer.kt} (98%) rename mapconductor-for-here/src/main/java/com/mapconductor/here/marker/{HereMapMarkerRenderer.kt => HereMarkerRenderer.kt} (82%) delete mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt index 5ef4a291..d2f85d2e 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt @@ -11,7 +11,7 @@ import com.mapconductor.arcgis.circle.ArcGISCircleRenderer import com.mapconductor.arcgis.circle.DefaultArcGISCircleRenderer import com.mapconductor.arcgis.marker.ArcGISMarkerController import com.mapconductor.arcgis.marker.SelectedMarker -import com.mapconductor.arcgis.polygon.ArcGISPolygonRenderer +import com.mapconductor.arcgis.polygon.ArcGISPolygonController import com.mapconductor.arcgis.polyline.ArcGISPolylineController import com.mapconductor.core.circle.CircleClickEvent import com.mapconductor.core.circle.CircleOverlayManager @@ -27,7 +27,6 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polygon.PolygonCapable -import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler @@ -61,15 +60,12 @@ interface ArcGISMapViewController : class ArcGISMapViewControllerImpl( override val holder: ArcGISMapViewHolder, private val polylineController: ArcGISPolylineController, + private val polygonController: ArcGISPolygonController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), private val circleLayer: GraphicsOverlay = GraphicsOverlay().apply { sceneProperties.surfacePlacement = SurfacePlacement.DrapedFlat }, - private val polygonLayer: GraphicsOverlay = - GraphicsOverlay().apply { - sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded - }, private val circleRendererFactory: CircleRendererFactory = DefaultArcGISCircleRenderer(), private val markerController: ArcGISMarkerController, @@ -97,7 +93,7 @@ class ArcGISMapViewControllerImpl( init { holder.map.graphicsOverlays.clear() holder.map.graphicsOverlays.add(circleLayer) - holder.map.graphicsOverlays.add(polygonLayer) + holder.map.graphicsOverlays.add(polygonController.renderer.polygonLayer) holder.map.graphicsOverlays.add(polylineController.renderer.polylineLayer) holder.map.graphicsOverlays.add(markerController.renderer.markerLayer) setupListeners() diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt index 19fabf61..ac92557b 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt @@ -5,6 +5,8 @@ import com.arcgismaps.mapping.view.SceneView import com.arcgismaps.mapping.view.SurfacePlacement import com.mapconductor.arcgis.marker.ArcGISMarkerController import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer +import com.mapconductor.arcgis.polygon.ArcGISPolygonController +import com.mapconductor.arcgis.polygon.ArcGISPolygonOverlayRenderer import com.mapconductor.arcgis.polyline.ArcGISPolylineController import com.mapconductor.arcgis.polyline.ArcGISPolylineOverlayRenderer import com.mapconductor.core.geocell.HexGeocell @@ -39,6 +41,7 @@ object ArcGISViewControllerStore : holder = holder, markerController = getMarkerController(holder), polylineController = getPolylineController(holder), + polygonController = getPolygonController(holder), ) this.set(id, controller) return controller @@ -63,6 +66,25 @@ object ArcGISViewControllerStore : return controller } + private fun getPolygonController(holder: ArcGISMapViewHolder): ArcGISPolygonController { + val polygonLayer: GraphicsOverlay = + GraphicsOverlay().apply { + sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded + } + + val renderer = + ArcGISPolygonOverlayRenderer( + polygonLayer = polygonLayer, + holder = holder, + ) + + val controller = + ArcGISPolygonController( + renderer = renderer, + ) + return controller + } + private fun getMarkerController(holder: ArcGISMapViewHolder): ArcGISMarkerController { val hexGeocell = HexGeocell( diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt index 1fcd495e..0c0bd008 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt @@ -1,11 +1,113 @@ package com.mapconductor.arcgis.polygon +import com.arcgismaps.geometry.Geometry +import com.arcgismaps.geometry.PolygonBuilder +import com.arcgismaps.mapping.symbology.SimpleFillSymbol +import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle +import com.arcgismaps.mapping.symbology.SimpleLineSymbol +import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay import com.mapconductor.arcgis.ArcGISActualPolygon +import com.mapconductor.arcgis.ArcGISMapViewHolder +import com.mapconductor.arcgis.toArcGISColor +import com.mapconductor.arcgis.toPoint +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonEntity import com.mapconductor.core.polygon.PolygonManager import com.mapconductor.core.polygon.PolygonManagerImpl +import com.mapconductor.core.polygon.PolygonState +import kotlin.collections.set +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class ArcGISPolygonController( polygonManager: PolygonManager = PolygonManagerImpl(), - override val renderer: ArcGISPolygonRenderer, + override val renderer: ArcGISPolygonOverlayRenderer, ) : PolygonController(polygonManager, renderer) + + +class ArcGISPolygonOverlayRenderer( + val polygonLayer: GraphicsOverlay, + override val holder: ArcGISMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolygonOverlayRenderer() { + override suspend fun createPolygon(state: PolygonState): ArcGISActualPolygon? = + withContext(coroutine.coroutineContext) { + val geometry = createGeometry(state) + val outlineSymbol = + SimpleLineSymbol().apply { + style = SimpleLineSymbolStyle.Solid + color = state.strokeColor.toArcGISColor() + width = state.strokeWidth.value.toFloat() + } + + val fillSymbol = + SimpleFillSymbol().apply { + style = SimpleFillSymbolStyle.Solid + color = state.fillColor.toArcGISColor() + outline = outlineSymbol + } + + val graphic = + Graphic(geometry, fillSymbol).also { + it.attributes.set("id", state.id) + } + + polygonLayer.graphics.add(graphic) + graphic + } + + override suspend fun updatePolygonProperties( + polygon: ArcGISActualPolygon, + current: PolygonEntity, + prev: PolygonEntity + ): ArcGISActualPolygon? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + if (finger.points != prevFinger.points) { + current.polygon.geometry = createGeometry(current.state) + } + + (current.polygon.symbol as SimpleFillSymbol).let { symbol -> + if (finger.fillColor != prevFinger.fillColor) { + symbol.color = + current.state.fillColor + .toArcGISColor() + } + symbol.outline?.let { outline -> + if (finger.strokeColor != prevFinger.strokeColor) { + outline.color = + current.state.strokeColor + .toArcGISColor() + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + outline.width = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + } + } + } + polygon + } + + override suspend fun removePolygon(entity: PolygonEntity) { + coroutine.launch { + polygonLayer.graphics.remove(entity.polygon) + } + } + + private fun createGeometry(state: PolygonState): Geometry { + val polygonBuilder = + PolygonBuilder().also { builder -> + state.points.forEach { + builder.addPoint(GeoPoint.from(it).toPoint()) + } + } + return polygonBuilder.toGeometry() + } +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt deleted file mode 100644 index 82aacbc6..00000000 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonRenderer.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.mapconductor.arcgis.polygon - -import com.arcgismaps.geometry.Geometry -import com.arcgismaps.geometry.PolygonBuilder -import com.arcgismaps.mapping.symbology.SimpleFillSymbol -import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle -import com.arcgismaps.mapping.symbology.SimpleLineSymbol -import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle -import com.arcgismaps.mapping.view.Graphic -import com.arcgismaps.mapping.view.GraphicsOverlay -import com.mapconductor.arcgis.ArcGISActualPolygon -import com.mapconductor.arcgis.ArcGISMapViewHolder -import com.mapconductor.arcgis.toArcGISColor -import com.mapconductor.arcgis.toPoint -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.controller.OverlayRenderer -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class ArcGISPolygonRenderer( - val polygonLayer: GraphicsOverlay, - val holder: ArcGISMapViewHolder, - val coroutine: CoroutineScope, -) : OverlayRenderer> { - override suspend fun onAdd(data: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext data.map { state -> - - val geometry = createGeometry(state) - - val outlineSymbol = - SimpleLineSymbol().apply { - style = SimpleLineSymbolStyle.Solid - color = state.strokeColor.toArcGISColor() - width = state.strokeWidth.value.toFloat() - } - - val fillSymbol = - SimpleFillSymbol().apply { - style = SimpleFillSymbolStyle.Solid - color = state.fillColor.toArcGISColor() - outline = outlineSymbol - } - - val graphic = - Graphic(geometry, fillSymbol).also { - it.attributes.set("id", state.id) - } - - polygonLayer.graphics.add(graphic) - - return@map graphic - } - } - } - - override suspend fun onRemove(data: List>) { - val polygons = data.map { it.polygon } - coroutine.launch { - polygonLayer.graphics.removeAll(polygons) - } - } - - override suspend fun onPostProcess() { - // Do nothing here - } - - override suspend fun onChange( - data: List>>, - ): List { - return withContext(coroutine.coroutineContext) { - return@withContext data.map { params -> - val finger = params.current.fingerPrint - val prevFinger = params.prev.fingerPrint - if (finger.points != prevFinger.points) { - params.current.polygon.geometry = createGeometry(params.current.state) - } - - (params.current.polygon.symbol as SimpleFillSymbol).let { symbol -> - if (finger.fillColor != prevFinger.fillColor) { - symbol.color = - params.current.state.fillColor - .toArcGISColor() - } - symbol.outline?.let { outline -> - if (finger.strokeColor != prevFinger.strokeColor) { - outline.color = - params.current.state.strokeColor - .toArcGISColor() - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - outline.width = ResourceProvider.dpToPx(params.current.state.strokeWidth).toFloat() - } - } - } - return@map params.current.polygon - } - } - } - - private fun createGeometry(state: PolygonState): Geometry { - val polygonBuilder = - PolygonBuilder().also { builder -> - state.points.forEach { - builder.addPoint(GeoPoint.from(it).toPoint()) - } - } - return polygonBuilder.toGeometry() - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt index 906d58c8..fe7ac90b 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt @@ -8,12 +8,12 @@ import com.mapconductor.core.groundimage.GroundImageController import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder import com.mapconductor.core.marker.MarkerManager -import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.projection.WebMercator import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer import com.mapconductor.googlemaps.marker.GoogleMapMarkerController import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer -import com.mapconductor.googlemaps.polygon.GoogleMapPolygonRenderer +import com.mapconductor.googlemaps.polygon.GoogleMapPolygonController +import com.mapconductor.googlemaps.polygon.GoogleMapPolygonOverlayRenderer import com.mapconductor.googlemaps.polyline.GoogleMapPolylineController import com.mapconductor.googlemaps.polyline.GoogleMapPolylineOverlayRenderer import android.app.Activity @@ -52,6 +52,18 @@ object GoogleMapViewControllerStore : StaticHolder( return controller } + private fun getPolygonController(holder: GoogleMapViewHolder): GoogleMapPolygonController { + val renderer = + GoogleMapPolygonOverlayRenderer( + holder = holder, + ) + + val controller = + GoogleMapPolygonController( + renderer = renderer, + ) + return controller + } private fun getPolylineController(holder: GoogleMapViewHolder): GoogleMapPolylineController { val renderer = GoogleMapPolylineOverlayRenderer( @@ -102,20 +114,6 @@ object GoogleMapViewControllerStore : StaticHolder( return groundImageController } - - private fun getPolygonController(holder: GoogleMapViewHolder): PolygonController { - val polygonRenderer = - GoogleMapPolygonRenderer( - holder = holder, - ) - - val polygonController = - PolygonController( - renderer = polygonRenderer, - ) - - return polygonController - } } internal fun Context.findActivity(): Activity? = diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt index 101b0825..a46d4b27 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt @@ -5,71 +5,74 @@ import com.google.android.gms.maps.model.PolygonOptions import com.mapconductor.core.ResourceProvider import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer +import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonManagerImpl import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.PolylineController import com.mapconductor.googlemaps.GoogleMapActualPolygon import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.polyline.GoogleMapPolylineOverlayRenderer import com.mapconductor.googlemaps.toLatLng +import kotlin.coroutines.coroutineContext +import kotlinx.coroutines.AbstractCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class GoogleMapPolygonRenderer( - val holder: GoogleMapViewHolder, - val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), -) : OverlayRenderer> { - override suspend fun onAdd(data: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext data.map { state -> - val points = state.points.map { GeoPoint.from(it).toLatLng() } - val options = - PolygonOptions() - .addAll(points) - .strokeColor(state.strokeColor.toArgb()) - .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) - .fillColor(state.fillColor.toArgb()) - .clickable(true) - holder.map.addPolygon(options)?.also { - it.tag = state.id - } - } - } - } +class GoogleMapPolygonController( + polygonManager: PolygonManager = PolygonManagerImpl(), + renderer: GoogleMapPolygonOverlayRenderer, +) : PolygonController(polygonManager, renderer) - override suspend fun onRemove(data: List>) { +class GoogleMapPolygonOverlayRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolygonOverlayRenderer() { + override suspend fun removePolygon(entity: PolygonEntity) { coroutine.launch { - data.forEach { entity -> entity.polygon.remove() } + entity.polygon.remove() } } - override suspend fun onPostProcess() { - // Do nothing here - } + override suspend fun createPolygon(state: PolygonState) = + withContext(coroutine.coroutineContext) { + val points = state.points.map { GeoPoint.from(it).toLatLng() } + val options = + PolygonOptions() + .addAll(points) + .strokeColor(state.strokeColor.toArgb()) + .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) + .fillColor(state.fillColor.toArgb()) + .clickable(true) + holder.map.addPolygon(options)?.also { + it.tag = state.id + } + } - override suspend fun onChange( - data: List>>, - ): List { - return withContext(coroutine.coroutineContext) { - return@withContext data.map { params -> - val polygon = params.current.polygon - val finger = params.current.fingerPrint - val prevFinger = params.prev.fingerPrint - if (finger.points != prevFinger.points) { - val points = - params.current.state.points - .map { GeoPoint.from(it).toLatLng() } - polygon.points = points - } - polygon.strokeWidth = ResourceProvider.dpToPx(params.current.state.strokeWidth).toFloat() - polygon.strokeColor = - params.current.state.strokeColor - .toArgb() - polygon.fillColor = - params.current.state.fillColor - .toArgb() - return@map polygon + override suspend fun updatePolygonProperties( + polygon: GoogleMapActualPolygon, + current: PolygonEntity, + prev: PolygonEntity + ): GoogleMapActualPolygon? = + withContext(coroutine.coroutineContext) { + val polygon = current.polygon + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + if (finger.points != prevFinger.points) { + val points = + current.state.points + .map { GeoPoint.from(it).toLatLng() } + polygon.points = points } + polygon.strokeWidth = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + polygon.strokeColor = + current.state.strokeColor.toArgb() + polygon.fillColor = + current.state.fillColor.toArgb() + polygon } - } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt index abde102f..a5b7eaa3 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt @@ -11,6 +11,7 @@ import com.mapconductor.core.polyline.PolylineEntity import com.mapconductor.core.polyline.PolylineManager import com.mapconductor.core.polyline.PolylineManagerImpl import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.googlemaps.GoogleMapActualPolyline import com.mapconductor.googlemaps.GoogleMapViewHolder import com.mapconductor.googlemaps.toLatLng import kotlinx.coroutines.CoroutineScope @@ -19,15 +20,15 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class GoogleMapPolylineController( - polylineManager: PolylineManager = PolylineManagerImpl(), + polylineManager: PolylineManager = PolylineManagerImpl(), renderer: GoogleMapPolylineOverlayRenderer, -) : PolylineController(polylineManager, renderer) +) : PolylineController(polylineManager, renderer) class GoogleMapPolylineOverlayRenderer( override val holder: GoogleMapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), -) : AbstractPolylineOverlayRenderer() { - override suspend fun createPolyline(state: PolylineState): Polyline? = +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): GoogleMapActualPolyline? = withContext(coroutine.coroutineContext) { val points = state.points.map { GeoPoint.from(it).toLatLng() } val options = @@ -44,9 +45,9 @@ class GoogleMapPolylineOverlayRenderer( } override suspend fun updatePolylineProperties( - polyline: Polyline, - current: PolylineEntity, - prev: PolylineEntity, + polyline: GoogleMapActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, ): Polyline? = withContext(coroutine.coroutineContext) { val finger = current.fingerPrint @@ -72,7 +73,7 @@ class GoogleMapPolylineOverlayRenderer( polyline } - override suspend fun removePolyline(entity: PolylineEntity) { + override suspend fun removePolyline(entity: PolylineEntity) { coroutine.launch { entity.polyline.remove() } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt deleted file mode 100644 index dc757271..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapTypeAlias.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.mapconductor.here - -import com.here.sdk.mapview.MapMarker -import com.here.sdk.mapview.MapPolygon -import com.here.sdk.mapview.MapPolyline - -typealias HereMapActualMarker = MapMarker -typealias HereMapActualCircle = MapPolygon -typealias HereMapActualPolyline = MapPolyline -typealias HereMapActualPolygon = MapPolygon diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index d368baf1..268b6081 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -34,10 +34,10 @@ fun HereMapView( onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, onPolylineClick: OnPolylineEventHandler? = null, - content: (@Composable HereMapViewScope.() -> Unit)? = null, + content: (@Composable HereViewScope.() -> Unit)? = null, ) { - val holderRef = remember { Ref() } - val scope = remember { HereMapViewScope() } + val holderRef = remember { Ref() } + val scope = remember { HereViewScope() } val controllerRef = remember { Ref() } val context = LocalContext.current val lifecycle = LocalLifecycleOwner.current.lifecycle @@ -55,7 +55,7 @@ fun HereMapView( HereMapViewControllerStore.initSDK(context) val mapInitOptions = - HereMapViewInitOptions( + HereViewInitOptions( scheme = state.mapDesignType.id, ) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereTypeAlias.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereTypeAlias.kt new file mode 100644 index 00000000..5184eadf --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereTypeAlias.kt @@ -0,0 +1,10 @@ +package com.mapconductor.here + +import com.here.sdk.mapview.MapMarker +import com.here.sdk.mapview.MapPolygon +import com.here.sdk.mapview.MapPolyline + +typealias HereActualMarker = MapMarker +typealias HereActualCircle = MapPolygon +typealias HereActualPolyline = MapPolyline +typealias HereActualPolygon = MapPolygon diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt similarity index 97% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt index 20fb2de2..057d4a1d 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt @@ -45,8 +45,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch interface HereMapViewController : - MapViewController, - MarkerCapable, + MapViewController, + MarkerCapable, PolygonCapable, PolylineCapable { fun moveCamera( @@ -67,8 +67,8 @@ class HereMapViewControllerImpl( private val polygonController: HerePolygonController, override val holder: MapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), -) : BaseMapViewController(), + private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), +) : BaseMapViewController(), HereMapViewController, MapCameraListener, TapListener, @@ -83,7 +83,7 @@ class HereMapViewControllerImpl( coroutine = coroutine, ) - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { + override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { } override fun createCircleOverlayManager(): CircleOverlayManager = diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt similarity index 85% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt index 2664cacc..b9c33495 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerStore.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt @@ -10,18 +10,16 @@ import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.projection.WebMercator -import com.mapconductor.here.marker.HereMapMarkerRenderer import com.mapconductor.here.marker.HereMarkerController -import com.mapconductor.here.polygon.HereMapPolygonRenderer +import com.mapconductor.here.marker.HereMarkerRenderer import com.mapconductor.here.polygon.HerePolygonController +import com.mapconductor.here.polygon.HerePolygonOverlayRenderer import com.mapconductor.here.polyline.HerePolylineController import com.mapconductor.here.polyline.HerePolylineOverlayRenderer import android.content.Context import android.content.pm.PackageManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -typealias HereMapViewHolder = MapViewHolder +typealias HereViewHolder = MapViewHolder object HereMapViewControllerStore : StaticHolder() { private var mapCount: Int = 0 @@ -58,7 +56,7 @@ object HereMapViewControllerStore : StaticHolder() { fun getOrCreate( context: Context, id: String, - options: HereMapViewInitOptions, + options: HereViewInitOptions, ): HereMapViewControllerImpl { val existing = this.get(id) if (existing != null) { @@ -67,7 +65,7 @@ object HereMapViewControllerStore : StaticHolder() { initSDK(context.applicationContext) val holder = - HereMapViewHolderImpl.create( + HereViewHolderImpl.create( context.applicationContext, ) @@ -100,7 +98,7 @@ object HereMapViewControllerStore : StaticHolder() { return controller } - private fun getPolylineController(holder: HereMapViewHolder): HerePolylineController { + private fun getPolylineController(holder: HereViewHolder): HerePolylineController { val renderer = HerePolylineOverlayRenderer( holder = holder, @@ -113,16 +111,16 @@ object HereMapViewControllerStore : StaticHolder() { return controller } - private fun getMarkerController(holder: HereMapViewHolder): HereMarkerController { + private fun getMarkerController(holder: HereViewHolder): HereMarkerController { val hexGeocell = HexGeocell( projection = WebMercator, baseHexSideLength = 100000, // 100km - 中ズームレベルに適した値 ) - val manager = MarkerManager(hexGeocell) + val manager = MarkerManager(hexGeocell) val renderer = - HereMapMarkerRenderer( + HereMarkerRenderer( holder = holder, ) @@ -134,11 +132,10 @@ object HereMapViewControllerStore : StaticHolder() { return controller } - private fun getPolygonController(holder: HereMapViewHolder): HerePolygonController { + private fun getPolygonController(holder: HereViewHolder): HerePolygonController { val renderer = - HereMapPolygonRenderer( + HerePolygonOverlayRenderer( holder = holder, - coroutine = CoroutineScope(Dispatchers.Default), ) val controller = diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewHolderImpl.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewHolderImpl.kt similarity index 93% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewHolderImpl.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewHolderImpl.kt index 8f28d72f..0a21d727 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewHolderImpl.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewHolderImpl.kt @@ -11,7 +11,7 @@ import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.map.MapViewHolder import android.content.Context -internal class HereMapViewHolderImpl private constructor( +internal class HereViewHolderImpl private constructor( override val mapView: MapView, ) : MapViewHolder { override lateinit var map: MapScene @@ -48,7 +48,7 @@ internal class HereMapViewHolderImpl private constructor( onResume() } - val holder = HereMapViewHolderImpl(mapView) + val holder = HereViewHolderImpl(mapView) holder.map = mapView.mapScene return holder } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewInitOptions.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewInitOptions.kt similarity index 77% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewInitOptions.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewInitOptions.kt index 5671a4a3..1ac85769 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewInitOptions.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewInitOptions.kt @@ -2,6 +2,6 @@ package com.mapconductor.here import com.here.sdk.mapview.MapScheme -data class HereMapViewInitOptions( +data class HereViewInitOptions( val scheme: MapScheme = MapScheme.NORMAL_DAY, ) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewScope.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewScope.kt similarity index 64% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewScope.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewScope.kt index 22a4984d..9fa4ab94 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewScope.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewScope.kt @@ -2,4 +2,4 @@ package com.mapconductor.here import com.mapconductor.core.MapViewScope -class HereMapViewScope : MapViewScope() +class HereViewScope : MapViewScope() diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereMapCircleRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleRenderer.kt similarity index 98% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereMapCircleRenderer.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleRenderer.kt index b30319c9..d407b62b 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereMapCircleRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleRenderer.kt @@ -14,7 +14,7 @@ import com.mapconductor.core.circle.CircleRenderer.UpdateParams import com.mapconductor.core.circle.CircleRendererFactory import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoPoint -import com.mapconductor.here.HereMapViewHolder +import com.mapconductor.here.HereViewHolder import com.mapconductor.here.toGeoCoordinates import kotlin.math.PI import kotlin.math.cos @@ -38,7 +38,7 @@ class DefaultHereMapCircleRenderer : CircleRendererFactory { } class HereMapCircleRenderer( - override val holder: HereMapViewHolder, + override val holder: HereViewHolder, override val coroutine: CoroutineScope, ) : AbstractCircleRenderer() { companion object { diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt index acb1b663..aec8f5c9 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerController.kt @@ -6,19 +6,19 @@ import com.mapconductor.core.marker.AbstractMarkerController import com.mapconductor.core.marker.MarkerEntity import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.spherical.haversineDistance -import com.mapconductor.here.HereMapActualMarker +import com.mapconductor.here.HereActualMarker import com.mapconductor.settings.Settings class HereMarkerController( - markerManager: MarkerManager, - override val renderer: HereMapMarkerRenderer, -) : AbstractMarkerController( + markerManager: MarkerManager, + override val renderer: HereMarkerRenderer, +) : AbstractMarkerController( markerManager = markerManager, renderer = renderer, ) { - private var internalSelectedMarker: MarkerEntity? = null + private var internalSelectedMarker: MarkerEntity? = null - internal var selectedMarker: MarkerEntity? + internal var selectedMarker: MarkerEntity? set(value) { if (value == null) { internalSelectedMarker?.let { @@ -37,7 +37,7 @@ class HereMarkerController( private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 } - override fun find(position: IGeoPoint): MarkerEntity? { + override fun find(position: IGeoPoint): MarkerEntity? { return markerManager.findNearest(position)?.let { nearest -> val zoom = renderer.holder.mapView.camera.state.zoomLevel - ZOOM_ADJUST_VALUE val tolerance = diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerRenderer.kt similarity index 82% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerRenderer.kt index 10689d75..bb5682c0 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMapMarkerRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/marker/HereMarkerRenderer.kt @@ -7,8 +7,8 @@ import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.marker.AbstractMarkerOverlayRenderer import com.mapconductor.core.marker.MarkerEntity import com.mapconductor.core.marker.MarkerOverlayRenderer -import com.mapconductor.here.HereMapActualMarker -import com.mapconductor.here.HereMapViewHolder +import com.mapconductor.here.HereActualMarker +import com.mapconductor.here.HereViewHolder import com.mapconductor.here.toAnchor2D import com.mapconductor.here.toGeoCoordinates import com.mapconductor.here.toMapImage @@ -17,24 +17,24 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class HereMapMarkerRenderer( - holder: HereMapViewHolder, +class HereMarkerRenderer( + holder: HereViewHolder, coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), ) : AbstractMarkerOverlayRenderer< - HereMapViewHolder, - HereMapActualMarker, + HereViewHolder, + HereActualMarker, >( holder = holder, coroutine = coroutine, ) { override fun setMarkerPosition( - markerEntity: MarkerEntity, + markerEntity: MarkerEntity, position: GeoPoint, ) { markerEntity.marker.coordinates = position.toGeoCoordinates() } - override suspend fun onAdd(data: List): List { + override suspend fun onAdd(data: List): List { val markers = withContext(coroutine.coroutineContext) { data.map { params -> @@ -60,9 +60,9 @@ class HereMapMarkerRenderer( return markers } - override suspend fun onRemove(data: List>) { + override suspend fun onRemove(data: List>) { coroutine.launch { - val markers: List = data.map { params -> params.marker } + val markers: List = data.map { params -> params.marker } holder.map.removeMapMarkers(markers) } } @@ -72,8 +72,8 @@ class HereMapMarkerRenderer( } override suspend fun onChange( - changes: List>, - ): List = + changes: List>, + ): List = changes.map { params -> val prevFinger = params.prev.fingerPrint val currFinger = params.current.fingerPrint diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt deleted file mode 100644 index 98191c5d..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HereMapPolygonRenderer.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.mapconductor.here.polygon - -import androidx.compose.ui.graphics.toArgb -import com.here.sdk.core.Color -import com.here.sdk.core.GeoPolygon -import com.here.sdk.mapview.MapPolygon -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.controller.OverlayRenderer -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.here.HereMapViewHolder -import com.mapconductor.here.toGeoCoordinates -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class HereMapPolygonRenderer( - val holder: HereMapViewHolder, - val coroutine: CoroutineScope, -) : OverlayRenderer> { - override suspend fun onAdd(data: List): List { - val polygons = - data.map { state -> - val geoPolygon = createGeoPolygon(state) - val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) - MapPolygon( - geoPolygon, - Color.valueOf(state.fillColor.toArgb()), - Color.valueOf(state.strokeColor.toArgb()), - lineWidth, - ) - } - coroutine.launch { - polygons.forEach { holder.map.addMapPolygon(it) } - } - return polygons - } - - override suspend fun onRemove(data: List>) { - coroutine.launch { - data.forEach { holder.map.removeMapPolygon(it.polygon) } - } - } - - override suspend fun onPostProcess() { - // Do nothing here - } - - override suspend fun onChange( - data: List>>, - ): List { - return data.map { params -> - val finger = params.current.fingerPrint - val prevFinger = params.prev.fingerPrint - if (finger.points != prevFinger.points) { - val geoPolygon = createGeoPolygon(params.current.state) - params.current.polygon.geometry = geoPolygon - } - if (finger.strokeColor != prevFinger.strokeColor) { - params.current.polygon.outlineColor = - Color.valueOf( - params.current.state.strokeColor - .toArgb(), - ) - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - val lineWidth = - ResourceProvider.dpToPx( - params.current.state.strokeWidth.value - .toDouble(), - ) - params.current.polygon.outlineWidth = lineWidth - } - if (finger.fillColor != prevFinger.fillColor) { - params.current.polygon.fillColor = - Color.valueOf( - params.current.state.fillColor - .toArgb(), - ) - } - return@map params.current.polygon - } - } - - private fun createGeoPolygon(state: PolygonState): GeoPolygon { - val points = state.points.map { GeoPoint.from(it).toGeoCoordinates() } - // Ensure the polygon is closed by adding the first point at the end if not already closed - val closedPoints = - if (points.first() != points.last()) { - points + points.first() - } else { - points - } - return GeoPolygon(closedPoints) - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt index 3d9bd5e3..a77a2598 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt @@ -7,35 +7,35 @@ import com.mapconductor.core.polygon.PolygonEntity import com.mapconductor.core.polygon.PolygonEntityImpl import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.here.HereMapActualPolygon +import com.mapconductor.here.HereActualPolygon import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit class HerePolygonController( - val renderer: OverlayRenderer>, + val renderer: OverlayRenderer>, override var clickListener: OnPolygonEventHandler? = null, ) : OverlayController< - HereMapActualPolygon, + HereActualPolygon, PolygonState, - PolygonEntity, + PolygonEntity, PolygonEvent, > { override val zIndex: Int = 3 - val entities = mutableMapOf>() + val entities = mutableMapOf>() val semaphore = Semaphore(1) override suspend fun add(data: List) { semaphore.withPermit { val previous = entities.keys.toMutableSet() val added = mutableListOf() - val updated = mutableListOf>>() - val removed = mutableListOf>() + val updated = mutableListOf>>() + val removed = mutableListOf>() data.forEach { if (previous.contains(it.id)) { val prevEntity = entities[it.id]!! updated.add( - object : OverlayRenderer.ChangeParams> { + object : OverlayRenderer.ChangeParams> { override val current = PolygonEntityImpl( polygon = prevEntity.polygon, @@ -63,7 +63,7 @@ class HerePolygonController( actualOverlay?.let { val state = added[index] val entity = - PolygonEntityImpl( + PolygonEntityImpl( polygon = it, state = state, ) @@ -78,7 +78,7 @@ class HerePolygonController( actualOverlay?.let { val state = updated[index].current.state val entity = - PolygonEntityImpl( + PolygonEntityImpl( polygon = it, state = state, ) @@ -97,24 +97,24 @@ class HerePolygonController( override suspend fun update(state: PolygonState) { semaphore.withPermit { - val updated = mutableListOf>>() + val updated = mutableListOf>>() val prevEntity = entities[state.id]!! updated.add( - object : OverlayRenderer.ChangeParams> { - override val current: PolygonEntity = + object : OverlayRenderer.ChangeParams> { + override val current: PolygonEntity = PolygonEntityImpl( polygon = prevEntity.polygon, state = state, ) - override val prev: PolygonEntity = prevEntity + override val prev: PolygonEntity = prevEntity }, ) - val actualOverlays: List = renderer.onChange(updated) + val actualOverlays: List = renderer.onChange(updated) actualOverlays.forEachIndexed { index, actualOverlay -> actualOverlay?.let { val entity = - PolygonEntityImpl( + PolygonEntityImpl( polygon = it, state = state, ) @@ -131,7 +131,7 @@ class HerePolygonController( } } - override fun find(position: IGeoPoint): PolygonEntity? { + override fun find(position: IGeoPoint): PolygonEntity? { // TODO: Improve this implementation later with proper point-in-polygon check return entities.values.find { entity -> entity.state.points.isNotEmpty() diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt new file mode 100644 index 00000000..08c341a2 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt @@ -0,0 +1,105 @@ +package com.mapconductor.here.polygon + +import androidx.compose.ui.graphics.toArgb +import com.here.sdk.core.Color +import com.here.sdk.core.GeoPolygon +import com.here.sdk.mapview.LineCap +import com.here.sdk.mapview.MapMeasureDependentRenderSize +import com.here.sdk.mapview.MapPolygon +import com.here.sdk.mapview.MapPolyline +import com.here.sdk.mapview.RenderSize +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.controller.OverlayRenderer +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonManagerImpl +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.here.HereActualPolygon +import com.mapconductor.here.HereViewHolder +import com.mapconductor.here.toGeoCoordinates +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class HerePolygonController( + polygonManager: PolygonManager = PolygonManagerImpl(), + renderer: HerePolygonOverlayRenderer, +) : PolygonController(polygonManager, renderer) + +class HerePolygonOverlayRenderer( + override val holder: HereViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolygonOverlayRenderer() { + override suspend fun removePolygon(entity: PolygonEntity) { + coroutine.launch { + holder.map.removeMapPolygon(entity.polygon) + } + } + + override suspend fun createPolygon(state: PolygonState): HereActualPolygon? { + val geoPolygon = createGeoPolygon(state) + val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) + val mapPolygon = MapPolygon( + geoPolygon, + Color.valueOf(state.fillColor.toArgb()), + Color.valueOf(state.strokeColor.toArgb()), + lineWidth, + ) + coroutine.launch { + holder.map.addMapPolygon(mapPolygon) + } + return mapPolygon + } + + override suspend fun updatePolygonProperties( + polygon: HereActualPolygon, + current: PolygonEntity, + prev: PolygonEntity + ): HereActualPolygon? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.points != prevFinger.points) { + val geoPolygon = createGeoPolygon(current.state) + current.polygon.geometry = geoPolygon + } + if (finger.strokeColor != prevFinger.strokeColor) { + current.polygon.outlineColor = + Color.valueOf( + current.state.strokeColor.toArgb(), + ) + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + val lineWidth = + ResourceProvider.dpToPx( + current.state.strokeWidth.value.toDouble(), + ) + current.polygon.outlineWidth = lineWidth + } + if (finger.fillColor != prevFinger.fillColor) { + current.polygon.fillColor = + Color.valueOf( + current.state.fillColor.toArgb(), + ) + } + polygon + } + + private fun createGeoPolygon(state: PolygonState): GeoPolygon { + val points = state.points.map { GeoPoint.from(it).toGeoCoordinates() } + // Ensure the polygon is closed by adding the first point at the end if not already closed + val closedPoints = + if (points.first() != points.last()) { + points + points.first() + } else { + points + } + return GeoPolygon(closedPoints) + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt index 754ca61f..1c255e21 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt @@ -18,23 +18,24 @@ import com.mapconductor.core.polyline.PolylineEntity import com.mapconductor.core.polyline.PolylineManager import com.mapconductor.core.polyline.PolylineManagerImpl import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.here.HereMapActualPolyline -import com.mapconductor.here.HereMapViewHolder +import com.mapconductor.here.HereActualPolyline +import com.mapconductor.here.HereViewHolder import com.mapconductor.here.toGeoCoordinates import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class HerePolylineController( - polylineManager: PolylineManager = PolylineManagerImpl(), + polylineManager: PolylineManager = PolylineManagerImpl(), renderer: HerePolylineOverlayRenderer, -) : PolylineController(polylineManager, renderer) +) : PolylineController(polylineManager, renderer) class HerePolylineOverlayRenderer( - override val holder: HereMapViewHolder, + override val holder: HereViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), -) : AbstractPolylineOverlayRenderer() { - override suspend fun createPolyline(state: PolylineState): HereMapActualPolyline? { +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): HereActualPolyline? { val geoPolyline = createGeoPolyline(state) val representation = createRepresentation(state) val mapPolyline = MapPolyline(geoPolyline, representation) @@ -47,38 +48,39 @@ class HerePolylineOverlayRenderer( } override suspend fun updatePolylineProperties( - polyline: HereMapActualPolyline, - current: PolylineEntity, - prev: PolylineEntity, - ): HereMapActualPolyline? { - val finger = current.fingerPrint - val prevFinger = prev.fingerPrint + polyline: HereActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): HereActualPolyline? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint - var needsReAdd = false + var needsReAdd = false - if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { - val geoPolyline = createGeoPolyline(current.state) - polyline.geometry = geoPolyline - needsReAdd = true - } + if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { + val geoPolyline = createGeoPolyline(current.state) + polyline.geometry = geoPolyline + needsReAdd = true + } - if (finger.strokeColor != prevFinger.strokeColor || finger.strokeWidth != prevFinger.strokeWidth) { - val representation = createRepresentation(current.state) - polyline.setRepresentation(representation) - needsReAdd = true - } + if (finger.strokeColor != prevFinger.strokeColor || finger.strokeWidth != prevFinger.strokeWidth) { + val representation = createRepresentation(current.state) + polyline.setRepresentation(representation) + needsReAdd = true + } - if (needsReAdd) { - coroutine.launch { - holder.map.removeMapPolylines(listOf(polyline)) - holder.map.addMapPolylines(listOf(polyline)) + if (needsReAdd) { + coroutine.launch { + holder.map.removeMapPolylines(listOf(polyline)) + holder.map.addMapPolylines(listOf(polyline)) + } } - } - return polyline - } + polyline + } - override suspend fun removePolyline(entity: PolylineEntity) { + override suspend fun removePolyline(entity: PolylineEntity) { coroutine.launch { holder.map.removeMapPolylines(listOf(entity.polyline)) } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt index df0ad85c..2a81104e 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt @@ -1,7 +1,11 @@ package com.mapconductor.mapbox.polygon +import com.mapbox.geojson.Feature +import com.mapbox.geojson.Polygon import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.polygon.PolygonEntity @@ -41,4 +45,25 @@ class MapboxPolygonOverlayRenderer( } layer.source.removeGeoJSONSourceFeatures(featureIds) } + + private fun createMapboxPolygons(state: PolygonState): List { + val geoPoints: List = state.points.map { GeoPoint.from(it).toPoint() } + // Close the polygon by adding the first point at the end if not already closed + val closedPoints = + if (geoPoints.first() != geoPoints.last()) { + geoPoints + geoPoints.first() + } else { + geoPoints + } + + Feature.fromGeometry( + Polygon.fromLngLats(listOf(closedPoints)), + JsonObject().apply { + addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) + addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) + addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) + }, + "polygon-${state.id}", + ) + } } From 1e1afeb44ca09f7b9d123ae601c4108fd8c388cc Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 29 Aug 2025 01:56:32 +0900 Subject: [PATCH 31/44] wip: refactoring polygon --- .../pages/groundimage/GroundImageMapPage.kt | 2 +- .../example/pages/polygon/PolygonMapPage.kt | 119 +++++++-------- .../pages/polygon/PolygonMapPageViewModel.kt | 2 +- .../here/polygon/HerePolygonController.kt | 140 ------------------ .../com/mapconductor/mapbox/MapboxMapView.kt | 27 ++++ .../mapbox/MapboxMapViewControllerImpl.kt | 10 +- .../mapbox/polygon/MapboxPolygonController.kt | 95 ++++++++---- .../mapbox/polygon/MapboxPolygonRenderer.kt | 92 ------------ .../polyline/MapboxPolylineController.kt | 3 +- 9 files changed, 155 insertions(+), 335 deletions(-) delete mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt delete mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index 219132c4..ea5ada4a 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -65,7 +65,7 @@ fun GroundImageMapPage( start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, ), - title = "Messages", + title = "GroundImage Example", ) { Column { Text("opacity: ${"%.2f".format(viewModel.opacity)}", color = Color.Black) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt index b5e7bdd8..43223040 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt @@ -45,74 +45,65 @@ fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { ) { paddingValues -> val mapViewState = viewModel.mapViewState.collectAsState() - Column { - // Polygon Controls - Column( - modifier = - Modifier.padding( - start = paddingValues.calculateStartPadding(LayoutDirection.Ltr), - end = paddingValues.calculateEndPadding(LayoutDirection.Ltr), - top = paddingValues.calculateTopPadding(), + // Map Component + PolygonMapComponent( + mapViewState = mapViewState.value, + viewModel = viewModel, + modifier = + Modifier.padding( + bottom = paddingValues.calculateBottomPadding(), + ), + onPolygonClick = viewModel::onPolygonClick, + onMarkerDrag = viewModel::onMarkerDrag, + ) + MessageCard( + title = "Polygon Example", + maxHeight = 250.dp, + modifier = + Modifier + .align(Alignment.BottomStart) + .padding( + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, ), - ) { - MessageCard( - title = "Polygon Example", - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - "This example demonstrates polygon rendering with draggable vertices. " + - "Drag the markers to change the polygon shape.", - ) - } + ) { - // Fill Opacity Control - Column( - modifier = Modifier.padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text("Fill Opacity: ${String.format("%.1f", viewModel.fillOpacity)}") - Slider( - value = viewModel.fillOpacity, - onValueChange = { viewModel.fillOpacity = it }, - valueRange = 0f..1f, - colors = - SliderDefaults.colors( - thumbColor = Color.Blue, - activeTrackColor = Color.Blue, - ), - ) - } - - // Stroke Width Control - Column( - modifier = Modifier.padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text("Stroke Width: ${String.format("%.1f", viewModel.strokeWidth)}dp") - Slider( - value = viewModel.strokeWidth, - onValueChange = { viewModel.strokeWidth = it }, - valueRange = 1f..10f, - colors = - SliderDefaults.colors( - thumbColor = Color.Blue, - activeTrackColor = Color.Blue, - ), - ) - } + // Fill Opacity Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Fill Opacity: ${String.format("%.1f", viewModel.fillOpacity)}") + Slider( + value = viewModel.fillOpacity, + onValueChange = { viewModel.fillOpacity = it }, + valueRange = 0f..1f, + colors = + SliderDefaults.colors( + thumbColor = Color.Blue, + activeTrackColor = Color.Blue, + ), + ) } - // Map Component - PolygonMapComponent( - mapViewState = mapViewState.value, - viewModel = viewModel, - modifier = - Modifier.padding( - bottom = paddingValues.calculateBottomPadding(), - ), - onPolygonClick = viewModel::onPolygonClick, - onMarkerDrag = viewModel::onMarkerDrag, - ) + // Stroke Width Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Stroke Width: ${String.format("%.1f", viewModel.strokeWidth)}dp") + Slider( + value = viewModel.strokeWidth, + onValueChange = { viewModel.strokeWidth = it }, + valueRange = 1f..10f, + colors = + SliderDefaults.colors( + thumbColor = Color.Blue, + activeTrackColor = Color.Blue, + ), + ) + } } // Toast messages diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt index 30d598ba..c8479a46 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt @@ -45,7 +45,7 @@ class PolygonMapPageViewModelImpl : override val initCameraPosition = MapCameraPosition( position = GeoPoint(35.6762, 139.6503), // Tokyo - zoom = 12.0, + zoom = 14.0, ) private val _mapViewState = MutableStateFlow?>(null) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt deleted file mode 100644 index a77a2598..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.mapconductor.here.polygon -import com.mapconductor.core.controller.OverlayController -import com.mapconductor.core.controller.OverlayRenderer -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.polygon.OnPolygonEventHandler -import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonEntityImpl -import com.mapconductor.core.polygon.PolygonEvent -import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.here.HereActualPolygon -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit - -class HerePolygonController( - val renderer: OverlayRenderer>, - override var clickListener: OnPolygonEventHandler? = null, -) : OverlayController< - HereActualPolygon, - PolygonState, - PolygonEntity, - PolygonEvent, - > { - override val zIndex: Int = 3 - val entities = mutableMapOf>() - val semaphore = Semaphore(1) - - override suspend fun add(data: List) { - semaphore.withPermit { - val previous = entities.keys.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>>() - val removed = mutableListOf>() - - data.forEach { - if (previous.contains(it.id)) { - val prevEntity = entities[it.id]!! - updated.add( - object : OverlayRenderer.ChangeParams> { - override val current = - PolygonEntityImpl( - polygon = prevEntity.polygon, - state = it, - ) - override val prev = prevEntity - }, - ) - previous.remove(it.id) - } else { - added.add(it) - previous.remove(it.id) - } - } - - previous.forEach { remainId -> - entities.remove(remainId)?.let { removedEntity -> - removed.add(removedEntity) - } - } - - if (added.isNotEmpty()) { - val actualOverlays = renderer.onAdd(added) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val state = added[index] - val entity = - PolygonEntityImpl( - polygon = it, - state = state, - ) - entities[state.id] = entity - } - } - } - - if (updated.isNotEmpty()) { - val actualOverlays = renderer.onChange(updated.toList()) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val state = updated[index].current.state - val entity = - PolygonEntityImpl( - polygon = it, - state = state, - ) - entities[state.id] = entity - } - } - } - - if (removed.isNotEmpty()) { - renderer.onRemove(removed) - } - - renderer.onPostProcess() - } - } - - override suspend fun update(state: PolygonState) { - semaphore.withPermit { - val updated = mutableListOf>>() - val prevEntity = entities[state.id]!! - updated.add( - object : OverlayRenderer.ChangeParams> { - override val current: PolygonEntity = - PolygonEntityImpl( - polygon = prevEntity.polygon, - state = state, - ) - override val prev: PolygonEntity = prevEntity - }, - ) - - val actualOverlays: List = renderer.onChange(updated) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val entity = - PolygonEntityImpl( - polygon = it, - state = state, - ) - entities[state.id] = entity - } - } - } - } - - override suspend fun clear() { - semaphore.withPermit { - renderer.onRemove(entities.values.toList()) - entities.clear() - } - } - - override fun find(position: IGeoPoint): PolygonEntity? { - // TODO: Improve this implementation later with proper point-in-polygon check - return entities.values.find { entity -> - entity.state.points.isNotEmpty() - } - } -} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index 737c56eb..b74b30ee 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -13,11 +13,15 @@ import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.OnPolygonEventHandler +import com.mapconductor.core.polygon.PolygonManagerImpl import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineManagerImpl import com.mapconductor.core.projection.WebMercator import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.marker.MapboxMarkerRenderer +import com.mapconductor.mapbox.polygon.MapboxPolygonController +import com.mapconductor.mapbox.polygon.MapboxPolygonLayer +import com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer import com.mapconductor.mapbox.polyline.MapboxPolylineController import com.mapconductor.mapbox.polyline.MapboxPolylineLayer import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer @@ -77,6 +81,7 @@ fun MapboxMapView( holder = holder, markerController = getMarkerController(holder), polylineController = getPolylineController(holder), + polygonController = getPolygonController(holder), ) (state as? MapboxViewStateImpl)?.let { mapViewState -> mapViewState.controller = controller @@ -104,6 +109,28 @@ fun MapboxMapView( ) } +internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonController { + val polygonLayer: MapboxPolygonLayer = + MapboxPolygonLayer( + sourceId = "polygon-source", + layerId = "polygon-layer", + ) + val polygonManager = PolygonManagerImpl() + + val renderer = + MapboxPolygonOverlayRenderer( + layer = polygonLayer, + polygonManager = polygonManager, + holder = holder, + ) + + val controller = + MapboxPolygonController( + renderer = renderer, + ) + return controller +} + internal fun getPolylineController(holder: MapboxMapViewHolder): MapboxPolylineController { val polylineLayer: MapboxPolylineLayer = MapboxPolylineLayer( diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt index bb560f39..4c015dc7 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt @@ -46,7 +46,6 @@ import com.mapconductor.mapbox.circle.MapboxCircleRenderer import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.polygon.MapboxPolygonController import com.mapconductor.mapbox.polygon.MapboxPolygonLayer -import com.mapconductor.mapbox.polygon.createMapboxPolygonController import com.mapconductor.mapbox.polyline.MapboxPolylineController import android.animation.Animator import kotlinx.coroutines.CoroutineScope @@ -83,11 +82,6 @@ internal class MapboxMapViewControllerImpl( sourceId = "circle-source", layerId = "circle-layer", ), - private val polygonLayer: MapboxPolygonLayer = - MapboxPolygonLayer( - sourceId = "polygon-source", - layerId = "polygon-layer", - ), private val circleRendererFactory: CircleRendererFactory = DefaultMapboxCircleRenderer(), ) : BaseMapViewController< @@ -124,8 +118,8 @@ internal class MapboxMapViewControllerImpl( holder.map.getStyle { style -> style.addSource(circleLayer.source) style.addLayer(circleLayer.layer) - style.addSource(polygonLayer.source) - style.addLayer(polygonLayer.layer) + style.addSource(polygonController.renderer.layer.source) + style.addLayer(polygonController.renderer.layer.layer) style.addSource(polylineController.renderer.layer.source) style.addLayer(polylineController.renderer.layer.layer) style.addSource(markerController.renderer.markerLayer.source) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt index 2a81104e..6149847e 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt @@ -1,11 +1,12 @@ package com.mapconductor.mapbox.polygon +import com.google.gson.JsonObject import com.mapbox.geojson.Feature import com.mapbox.geojson.Polygon import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures -import com.mapconductor.core.controller.OverlayController +import com.mapbox.maps.extension.style.sources.updateGeoJSONSourceFeatures +import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.polygon.PolygonEntity @@ -13,57 +14,95 @@ import com.mapconductor.core.polygon.PolygonManager import com.mapconductor.core.polygon.PolygonState import com.mapconductor.mapbox.MapboxActualPolygon import com.mapconductor.mapbox.MapboxMapViewHolder -import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer +import com.mapconductor.mapbox.toMapboxColorString +import com.mapconductor.mapbox.toPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class MapboxPolygonController( override val renderer: MapboxPolygonOverlayRenderer, polygonManager: PolygonManager = renderer.polygonManager, ) : PolygonController(polygonManager, renderer) + class MapboxPolygonOverlayRenderer( val layer: MapboxPolygonLayer, val polygonManager: PolygonManager, override val holder: MapboxMapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), ) : AbstractPolygonOverlayRenderer() { - override suspend fun createPolygon(state: PolygonState): MapboxActualPolygon? = createMapboxPolygons(state) - override suspend fun updatePolygonProperties( - polygon: MapboxActualPolygon, - current: PolygonEntity, - prev: PolygonEntity - ): MapboxActualPolygon? { - return createMapboxPolygons(current.state) + override suspend fun onRemove(data: List>) { + val featureIds = data.map { entity -> + entity.polygon.getStringProperty("id") + } + layer.source.removeGeoJSONSourceFeatures(featureIds) + } + + override suspend fun onPostProcess() { + val polygons = getAllPolygonEntities() + coroutine.launch { + layer.draw(polygons) + } } override suspend fun removePolygon(entity: PolygonEntity) { val featureIds = - entity.polygon.map { feature -> - feature.getStringProperty("id") - } + listOf(entity.polygon.getStringProperty("id")) layer.source.removeGeoJSONSourceFeatures(featureIds) } - private fun createMapboxPolygons(state: PolygonState): List { - val geoPoints: List = state.points.map { GeoPoint.from(it).toPoint() } + override suspend fun createPolygon(state: PolygonState): MapboxActualPolygon? { + val points = state.points.map { GeoPoint.from(it).toPoint() } // Close the polygon by adding the first point at the end if not already closed val closedPoints = - if (geoPoints.first() != geoPoints.last()) { - geoPoints + geoPoints.first() + if (points.first() != points.last()) { + points + points.first() } else { - geoPoints + points } - - Feature.fromGeometry( - Polygon.fromLngLats(listOf(closedPoints)), - JsonObject().apply { - addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) - addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) - }, - "polygon-${state.id}", - ) + return Feature.fromGeometry( + Polygon.fromLngLats(listOf(closedPoints)), + JsonObject().apply { + addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) + addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) + addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) + }, + "polygon-${state.id}", + ) + } + + override suspend fun updatePolygonProperties( + polygon: MapboxActualPolygon, + current: PolygonEntity, + prev: PolygonEntity + ): MapboxActualPolygon? { + val state = current.state + val points = state.points.map { GeoPoint.from(it).toPoint() } + // Close the polygon by adding the first point at the end if not already closed + val closedPoints = + if (points.first() != points.last()) { + points + points.first() + } else { + points + } + val feature = Feature.fromGeometry( + Polygon.fromLngLats(listOf(closedPoints)), + JsonObject().apply { + addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) + addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, ResourceProvider.dpToPx(state.strokeWidth.value)) + addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) + }, + "polygon-${state.id}", + ) + layer.source.updateGeoJSONSourceFeatures(listOf(feature)) + return feature + } + + private fun getAllPolygonEntities(): List> { + // This would need access to the polygon manager + // For now, we'll implement a simple workaround + return polygonManager.allEntities() } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt deleted file mode 100644 index f2656b3a..00000000 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonRenderer.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.mapconductor.mapbox.polygon - -import com.google.gson.JsonObject -import com.mapbox.geojson.Feature -import com.mapbox.geojson.Polygon -import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures -import com.mapbox.maps.extension.style.sources.updateGeoJSONSourceFeatures -import com.mapconductor.core.controller.OverlayRenderer -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.mapbox.MapboxActualPolygon -import com.mapconductor.mapbox.MapboxMapViewHolder -import com.mapconductor.mapbox.toMapboxColorString -import com.mapconductor.mapbox.toPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class MapboxPolygonRenderer( - val holder: MapboxMapViewHolder, - val coroutine: CoroutineScope, - private val layer: MapboxPolygonLayer, -) : OverlayRenderer> { - override suspend fun onAdd(data: List): List { - val polygons = - data.map { state -> - val points = state.points.map { GeoPoint.from(it).toPoint() } - // Close the polygon by adding the first point at the end if not already closed - val closedPoints = - if (points.first() != points.last()) { - points + points.first() - } else { - points - } - Feature.fromGeometry( - Polygon.fromLngLats(listOf(closedPoints)), - JsonObject().apply { - addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) - addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) - }, - "polygon-${state.id}", - ) - } - return polygons - } - - override suspend fun onRemove(data: List>) { - val featureIds = data.map { "polygon-${it.state.id}" } - layer.source.removeGeoJSONSourceFeatures(featureIds) - } - - override suspend fun onPostProcess() { - // Do nothing here - } - - override suspend fun onChange( - data: List>>, - ): List { - val features = - data.map { params -> - val state = params.current.state - val points = state.points.map { GeoPoint.from(it).toPoint() } - // Close the polygon by adding the first point at the end if not already closed - val closedPoints = - if (points.first() != points.last()) { - points + points.first() - } else { - points - } - Feature.fromGeometry( - Polygon.fromLngLats(listOf(closedPoints)), - JsonObject().apply { - addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) - addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) - }, - "polygon-${state.id}", - ) - } - layer.source.updateGeoJSONSourceFeatures(features) - return features - } - - fun redraw() { - // Note: This method needs to be updated to work with the new architecture - // when a polygon overlay manager is available - coroutine.launch { - // TODO: Update this method when MapViewController integration is complete - } - } -} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt index 12a025c2..ddac9dfa 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt @@ -4,6 +4,7 @@ import com.google.gson.JsonObject import com.mapbox.geojson.Feature import com.mapbox.geojson.LineString import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures +import com.mapconductor.core.ResourceProvider import com.mapconductor.core.createInterpolatePoints import com.mapconductor.core.createLinearInterpolatePoints import com.mapconductor.core.features.GeoPoint @@ -76,7 +77,7 @@ class MapboxPolylineOverlayRenderer( LineString.fromLngLats(points), JsonObject().apply { addProperty(MapboxPolylineLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) + addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, ResourceProvider.dpToPx(state.strokeWidth.value)) addProperty("id", id) }, id, From 940486341be707304e006d6255c7bb6088e193e4 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 30 Aug 2025 18:28:47 +0900 Subject: [PATCH 32/44] wip:reimplement Polygon for Mapbox --- .../mapconductor/example/MapViewContainer.kt | 4 + .../example/pages/polygon/PolygonMapPage.kt | 11 +-- .../pages/polygon/PolygonMapPageViewModel.kt | 29 +++++-- .../core/features/GeoRectBounds.kt | 33 +++---- .../com/mapconductor/arcgis/ArcGISMapView.kt | 3 + .../mapconductor/googlemaps/GoogleMapView.kt | 3 + .../java/com/mapconductor/here/HereMapView.kt | 3 + .../com/mapconductor/mapbox/MapboxMapView.kt | 36 +++++--- .../mapbox/MapboxMapViewControllerImpl.kt | 19 +++-- .../mapconductor/mapbox/MapboxPolyUtils.kt | 44 ++++++++++ .../mapconductor/mapbox/MapboxTypeAlias.kt | 7 +- .../mapbox/polygon/MapboxPolygonConductor.kt | 85 +++++++++++++++++++ .../mapbox/polygon/MapboxPolygonController.kt | 73 +++++++--------- ...ygonLayer.kt => MapboxPolygonFillLayer.kt} | 9 +- .../polyline/MapboxPolylineController.kt | 41 ++++----- 15 files changed, 275 insertions(+), 125 deletions(-) create mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt create mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt rename mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/{MapboxPolygonLayer.kt => MapboxPolygonFillLayer.kt} (77%) diff --git a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt index 00365270..43bbaa9c 100644 --- a/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt +++ b/example-app/src/main/java/com/mapconductor/example/MapViewContainer.kt @@ -50,6 +50,7 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, + onPolygonClick = onPolygonClick, onGroundImageClick = onGroundImageClick, content = content, ) @@ -67,6 +68,7 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, + onPolygonClick = onPolygonClick, content = content, ) @@ -83,6 +85,7 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, + onPolygonClick = onPolygonClick, content = content, ) @@ -99,6 +102,7 @@ fun MapViewContainer( onMarkerAnimateEnd = onMarkerAnimateEnd, onCircleClick = onCircleClick, onPolylineClick = onPolylineClick, + onPolygonClick = onPolygonClick, content = content, ) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt index 43223040..b3a7dd72 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt @@ -39,6 +39,7 @@ fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { ) DemoMapPageScaffold( + initSelect = 1, menuItems = PolygonCapableMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, @@ -80,10 +81,7 @@ fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { onValueChange = { viewModel.fillOpacity = it }, valueRange = 0f..1f, colors = - SliderDefaults.colors( - thumbColor = Color.Blue, - activeTrackColor = Color.Blue, - ), + SliderDefaults.colors(), ) } @@ -98,10 +96,7 @@ fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { onValueChange = { viewModel.strokeWidth = it }, valueRange = 1f..10f, colors = - SliderDefaults.colors( - thumbColor = Color.Blue, - activeTrackColor = Color.Blue, - ), + SliderDefaults.colors(), ) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt index c8479a46..34f95a53 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.DefaultIcon @@ -14,6 +15,7 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.example.toast.ToastMessage +import android.util.Log import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -42,11 +44,7 @@ interface PolygonMapPageViewModel { class PolygonMapPageViewModelImpl : ViewModel(), PolygonMapPageViewModel { - override val initCameraPosition = - MapCameraPosition( - position = GeoPoint(35.6762, 139.6503), // Tokyo - zoom = 14.0, - ) + private val _mapViewState = MutableStateFlow?>(null) override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() @@ -57,9 +55,24 @@ class PolygonMapPageViewModelImpl : // Polygon vertices (triangle around Tokyo landmarks) private val polygonVertices = mutableListOf( - GeoPoint(35.6794, 139.6515), // Near Tokyo Station - GeoPoint(35.6682, 139.6594), // Near Tokyo Bay - GeoPoint(35.6856, 139.6539), // Near Imperial Palace + GeoPoint(41.79883, 140.75675), + GeoPoint(41.799240000000005, 140.75875000000002), + GeoPoint(41.797650000000004, 140.75905), + GeoPoint(41.79637, 140.76018000000002), + GeoPoint(41.79567, 140.75845), + GeoPoint(41.794470000000004, 140.75714000000002), + GeoPoint(41.795010000000005, 140.75611), + GeoPoint(41.79477000000001, 140.75484), + GeoPoint(41.79576, 140.75475), + GeoPoint(41.796150000000004, 140.75364000000002), + GeoPoint(41.79744, 140.75454000000002), + GeoPoint(41.79909000000001, 140.75465) + ) + + override val initCameraPosition = + MapCameraPosition( + position = GeoPoint(41.796855,140.756910), + zoom = 16.0, ) override var fillOpacity by mutableStateOf(0.3f) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt index 74b61ca6..7c1e6a65 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoRectBounds.kt @@ -112,26 +112,27 @@ data class GeoRectBounds( return withinLat && withinLng } - fun getCenter(): GeoPoint? { - if (isEmpty) return null + val center: GeoPoint? + get() { + if (isEmpty) return null - val sw = southWest!! - val ne = northEast!! + val sw = southWest!! + val ne = northEast!! - val centerLat = (sw.latitude + ne.latitude) / 2.0 + val centerLat = (sw.latitude + ne.latitude) / 2.0 - val lng1 = sw.longitude - val lng2 = ne.longitude - val centerLng = - if (lng1 <= lng2) { - (lng1 + lng2) / 2.0 - } else { - val mid = (lng1 + (lng2 + 360)) / 2.0 - if (mid > 180) mid - 360 else mid - } + val lng1 = sw.longitude + val lng2 = ne.longitude + val centerLng = + if (lng1 <= lng2) { + (lng1 + lng2) / 2.0 + } else { + val mid = (lng1 + (lng2 + 360)) / 2.0 + if (mid > 180) mid - 360 else mid + } - return GeoPoint(centerLat, centerLng) - } + return GeoPoint(centerLat, centerLng) + } fun union(other: GeoRectBounds): GeoRectBounds { if (other.isEmpty) return this diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index 1a308c23..bd799847 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -11,6 +11,7 @@ import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler @Composable @@ -26,6 +27,7 @@ fun ArcGISMapView( onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, content: (@Composable ArcGISMapViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -65,6 +67,7 @@ fun ArcGISMapView( controller.setMapClickListener(onMapClick) controller.setCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) + controller.setOnPolygonClickListener(onPolygonClick) controller.setOnMarkerClickListener(onMarkerClick) controller.setOnMarkerDragStart(onMarkerDragStart) controller.setOnMarkerDrag(onMarkerDrag) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index 8d2ed6d6..36fde1b3 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -17,6 +17,7 @@ import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler import android.view.ViewGroup @@ -33,6 +34,7 @@ fun GoogleMapsView( onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, onGroundImageClick: OnGroundImageEventHandler? = null, content: (@Composable GoogleMapViewScope.() -> Unit)? = null, ) { @@ -87,6 +89,7 @@ fun GoogleMapsView( controller.setOnMarkerDragEnd(onMarkerDragEnd) controller.setCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) + controller.setOnPolygonClickListener(onPolygonClick) controller.setOnMarkerAnimateStart(onMarkerAnimateStart) controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) controller.setOnGroundImageClickListener(onGroundImageClick) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index 268b6081..ed8364c3 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -14,6 +14,7 @@ import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polyline.OnPolylineEventHandler import android.util.Log import android.view.ViewGroup @@ -34,6 +35,7 @@ fun HereMapView( onMarkerAnimateEnd: OnMarkerEventHandler? = null, onCircleClick: OnCircleEventHandler? = null, onPolylineClick: OnPolylineEventHandler? = null, + onPolygonClick: OnPolygonEventHandler? = null, content: (@Composable HereViewScope.() -> Unit)? = null, ) { val holderRef = remember { Ref() } @@ -79,6 +81,7 @@ fun HereMapView( controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) controller.setCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) + controller.setOnPolygonClickListener(onPolygonClick) (state as? HereViewStateImpl)?.controller = controller diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index b74b30ee..e0b8b2a2 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -19,7 +19,7 @@ import com.mapconductor.core.polyline.PolylineManagerImpl import com.mapconductor.core.projection.WebMercator import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.marker.MapboxMarkerRenderer -import com.mapconductor.mapbox.polygon.MapboxPolygonController +import com.mapconductor.mapbox.polygon.MapboxPolygonConductor import com.mapconductor.mapbox.polygon.MapboxPolygonLayer import com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer import com.mapconductor.mapbox.polyline.MapboxPolylineController @@ -109,26 +109,38 @@ fun MapboxMapView( ) } -internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonController { +internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonConductor { + val polylineLayer: MapboxPolylineLayer = + MapboxPolylineLayer( + sourceId = "polygon-outline-source", + layerId = "polygon-outline-layer", + ) + val polylineManager = PolylineManagerImpl() + val polylineOverlayRenderer = + MapboxPolylineOverlayRenderer( + layer = polylineLayer, + polylineManager = polylineManager, + holder = holder, + ) + + val polygonManager = PolygonManagerImpl() val polygonLayer: MapboxPolygonLayer = MapboxPolygonLayer( - sourceId = "polygon-source", - layerId = "polygon-layer", + sourceId = "polygon-fill-source", + layerId = "polygon-fill-layer", ) - val polygonManager = PolygonManagerImpl() - - val renderer = + val polygonOverlayRenderer = MapboxPolygonOverlayRenderer( layer = polygonLayer, polygonManager = polygonManager, holder = holder, ) - val controller = - MapboxPolygonController( - renderer = renderer, - ) - return controller + val conductor = MapboxPolygonConductor( + polygonOverlay = polygonOverlayRenderer, + polylineOverlay = polylineOverlayRenderer, + ) + return conductor } internal fun getPolylineController(holder: MapboxMapViewHolder): MapboxPolylineController { diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt index 4c015dc7..eefe403c 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt @@ -34,7 +34,6 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polygon.PolygonCapable -import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler @@ -44,8 +43,7 @@ import com.mapconductor.mapbox.circle.DefaultMapboxCircleRenderer import com.mapconductor.mapbox.circle.MapboxCircleLayer import com.mapconductor.mapbox.circle.MapboxCircleRenderer import com.mapconductor.mapbox.marker.MapboxMarkerController -import com.mapconductor.mapbox.polygon.MapboxPolygonController -import com.mapconductor.mapbox.polygon.MapboxPolygonLayer +import com.mapconductor.mapbox.polygon.MapboxPolygonConductor import com.mapconductor.mapbox.polyline.MapboxPolylineController import android.animation.Animator import kotlinx.coroutines.CoroutineScope @@ -75,7 +73,7 @@ internal class MapboxMapViewControllerImpl( override val holder: MapboxMapViewHolder, private val markerController: MapboxMarkerController, private val polylineController: MapboxPolylineController, - private val polygonController: MapboxPolygonController, + private val polygonController: MapboxPolygonConductor, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), private val circleLayer: MapboxCircleLayer = MapboxCircleLayer( @@ -116,12 +114,21 @@ internal class MapboxMapViewControllerImpl( init { holder.map.getStyle { style -> + // Circle style.addSource(circleLayer.source) style.addLayer(circleLayer.layer) - style.addSource(polygonController.renderer.layer.source) - style.addLayer(polygonController.renderer.layer.layer) + + // Polygon + style.addSource(polygonController.polygonOverlay.layer.source) + style.addLayer(polygonController.polygonOverlay.layer.layer) + style.addSource(polygonController.polylineOverlay.layer.source) + style.addLayer(polygonController.polylineOverlay.layer.layer) + + // Polyline style.addSource(polylineController.renderer.layer.source) style.addLayer(polylineController.renderer.layer.layer) + + // Marker style.addSource(markerController.renderer.markerLayer.source) style.addLayer(markerController.renderer.markerLayer.layer) style.addSource(markerController.renderer.dragLayer.source) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt new file mode 100644 index 00000000..92e65b5c --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt @@ -0,0 +1,44 @@ +package com.mapconductor.mapbox + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import com.google.gson.JsonObject +import com.mapbox.geojson.Feature +import com.mapbox.geojson.LineString +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.createInterpolatePoints +import com.mapconductor.core.createLinearInterpolatePoints +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.features.normalize +import com.mapconductor.core.splitByMeridian +import com.mapconductor.mapbox.polyline.MapboxPolylineLayer + +internal fun createMapboxLines( + id: String, + points: List, + geodesic: Boolean, + strokeColor: Color, + strokeWidth: Dp, +): List { + val geoPoints: List = + when (geodesic) { + true -> createInterpolatePoints(points) + false -> createLinearInterpolatePoints(points) + }.map { it.normalize() } + + return splitByMeridian(geoPoints, geodesic).mapIndexed { index, linePoints -> + val points = linePoints.map { GeoPoint.from(it).toPoint() } + val id = "polyline-${id}-$index" + + return@mapIndexed Feature.fromGeometry( + LineString.fromLngLats(points), + JsonObject().apply { + addProperty(MapboxPolylineLayer.Prop.STROKE_COLOR, strokeColor.toMapboxColorString()) + addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, ResourceProvider.dpToPx(strokeWidth.value)) + addProperty("id", id) + }, + id, + ) + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt index a8199ba7..a011c7c0 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxTypeAlias.kt @@ -5,4 +5,9 @@ import com.mapbox.geojson.Feature typealias MapboxActualMarker = Feature typealias MapboxActualCircle = Feature typealias MapboxActualPolyline = List -typealias MapboxActualPolygon = Feature +typealias MapboxActualPolygon = List + +data class MapboxOutlineAndFill( + val outline: MapboxActualPolyline?, + val fill: MapboxActualPolygon?, +) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt new file mode 100644 index 00000000..0e830ef0 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt @@ -0,0 +1,85 @@ +package com.mapconductor.mapbox.polygon + +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonEntityImpl +import com.mapconductor.core.polygon.PolygonEvent +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineEntityImpl +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.mapbox.MapboxOutlineAndFill +import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer + +class MapboxPolygonConductor( + val polygonOverlay: MapboxPolygonOverlayRenderer, + val polylineOverlay: MapboxPolylineOverlayRenderer, +) : OverlayController< + PolygonState, + PolygonState, + PolygonEntity, + PolygonEvent, +> { + override val zIndex: Int = 2 + override suspend fun add(data: List) { + data.forEach { polygonState -> + + polygonOverlay.createPolygon(polygonState)?.let { polygon -> + val polygonEntity = PolygonEntityImpl( + polygon = polygon, + state = polygonState, + ) + polygonOverlay.polygonManager.registerEntity(polygonEntity) + } + + val polylineState = polygonState.toPolylineState() + polylineOverlay.createPolyline(polylineState)?.let { polyline -> + val polylineEntity = PolylineEntityImpl( + polyline = polyline, + state = polylineState, + ) + polylineOverlay.polylineManager.registerEntity(polylineEntity) + } + } + polygonOverlay.onPostProcess() + polylineOverlay.onPostProcess() + } + + override suspend fun update(state: PolygonState) { + } + + override var clickListener: ((PolygonEvent) -> Unit)? = null + override fun find(position: IGeoPoint): PolygonEntity? { + return null + } + + suspend fun updatePolygonProperties( + polygon: MapboxOutlineAndFill, + current: PolygonEntity, + prev: PolygonEntity + ): MapboxOutlineAndFill? { + return null + } + + override suspend fun clear() { + } +} + +private fun PolygonState.toPolylineState(): PolylineState { + val closedPoints = + if (points.first() != points.last()) { + points + points.first() + } else { + points + } + return PolylineState( + points = closedPoints, + id = "outline-${this.id}", + strokeColor = this.strokeColor, + strokeWidth = this.strokeWidth, + geodesic = this.geodesic, + extra = null, + ) +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt index 6149847e..88cfa0c7 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt @@ -3,9 +3,6 @@ package com.mapconductor.mapbox.polygon import com.google.gson.JsonObject import com.mapbox.geojson.Feature import com.mapbox.geojson.Polygon -import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures -import com.mapbox.maps.extension.style.sources.updateGeoJSONSourceFeatures -import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer import com.mapconductor.core.polygon.PolygonController @@ -20,12 +17,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class MapboxPolygonController( - override val renderer: MapboxPolygonOverlayRenderer, - polygonManager: PolygonManager = renderer.polygonManager, -) : PolygonController(polygonManager, renderer) - - class MapboxPolygonOverlayRenderer( val layer: MapboxPolygonLayer, val polygonManager: PolygonManager, @@ -34,23 +25,23 @@ class MapboxPolygonOverlayRenderer( ) : AbstractPolygonOverlayRenderer() { override suspend fun onRemove(data: List>) { - val featureIds = data.map { entity -> - entity.polygon.getStringProperty("id") - } - layer.source.removeGeoJSONSourceFeatures(featureIds) +// val featureIds = data.map { entity -> +// entity.polygon.getStringProperty("id") +// } +// layer.source.removeGeoJSONSourceFeatures(featureIds) } override suspend fun onPostProcess() { val polygons = getAllPolygonEntities() coroutine.launch { - layer.draw(polygons) + this@MapboxPolygonOverlayRenderer.layer.draw(polygons) } } override suspend fun removePolygon(entity: PolygonEntity) { - val featureIds = - listOf(entity.polygon.getStringProperty("id")) - layer.source.removeGeoJSONSourceFeatures(featureIds) +// val featureIds = +// listOf(entity.polygon.getStringProperty("id")) +// layer.source.removeGeoJSONSourceFeatures(featureIds) } override suspend fun createPolygon(state: PolygonState): MapboxActualPolygon? { @@ -62,15 +53,13 @@ class MapboxPolygonOverlayRenderer( } else { points } - return Feature.fromGeometry( + return listOf(Feature.fromGeometry( Polygon.fromLngLats(listOf(closedPoints)), JsonObject().apply { - addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) }, "polygon-${state.id}", - ) + )) } override suspend fun updatePolygonProperties( @@ -78,26 +67,26 @@ class MapboxPolygonOverlayRenderer( current: PolygonEntity, prev: PolygonEntity ): MapboxActualPolygon? { - val state = current.state - val points = state.points.map { GeoPoint.from(it).toPoint() } - // Close the polygon by adding the first point at the end if not already closed - val closedPoints = - if (points.first() != points.last()) { - points + points.first() - } else { - points - } - val feature = Feature.fromGeometry( - Polygon.fromLngLats(listOf(closedPoints)), - JsonObject().apply { - addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, ResourceProvider.dpToPx(state.strokeWidth.value)) - addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) - }, - "polygon-${state.id}", - ) - layer.source.updateGeoJSONSourceFeatures(listOf(feature)) - return feature +// val state = current.state +// val points = state.points.map { GeoPoint.from(it).toPoint() } +// // Close the polygon by adding the first point at the end if not already closed +// val closedPoints = +// if (points.first() != points.last()) { +// points + points.first() +// } else { +// points +// } +// val feature = Feature.fromGeometry( +// Polygon.fromLngLats(listOf(closedPoints)), +// JsonObject().apply { +// addProperty(MapboxPolygonLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) +// addProperty(MapboxPolygonLayer.Prop.STROKE_WIDTH, ResourceProvider.dpToPx(state.strokeWidth.value)) +// addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) +// }, +// "polygon-${state.id}", +// ) +// layer.source.updateGeoJSONSourceFeatures(listOf(feature)) + return prev.polygon } private fun getAllPolygonEntities(): List> { @@ -105,4 +94,6 @@ class MapboxPolygonOverlayRenderer( // For now, we'll implement a simple workaround return polygonManager.allEntities() } + + } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonLayer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonFillLayer.kt similarity index 77% rename from mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonLayer.kt rename to mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonFillLayer.kt index b0362976..f1a5829d 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonLayer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonFillLayer.kt @@ -12,8 +12,6 @@ class MapboxPolygonLayer( val layerId: String, ) { object Prop { - const val STROKE_COLOR = "strokeColor" - const val STROKE_WIDTH = "strokeWidth" const val FILL_COLOR = "fillColor" } @@ -25,17 +23,12 @@ class MapboxPolygonLayer( literal(Prop.FILL_COLOR) }, ) - fillOutlineColor( - get { - literal(Prop.STROKE_COLOR) - }, - ) } fun draw(entities: List>) { val features = entities.map { it.polygon } source.featureCollection( - FeatureCollection.fromFeatures(features), + FeatureCollection.fromFeatures(features.flatten()), ) } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt index ddac9dfa..b4824847 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt @@ -18,6 +18,7 @@ import com.mapconductor.core.polyline.PolylineState import com.mapconductor.core.splitByMeridian import com.mapconductor.mapbox.MapboxActualPolyline import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.createMapboxLines import com.mapconductor.mapbox.toMapboxColorString import com.mapconductor.mapbox.toPoint import kotlinx.coroutines.CoroutineScope @@ -35,7 +36,14 @@ class MapboxPolylineOverlayRenderer( override val holder: MapboxMapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), ) : AbstractPolylineOverlayRenderer() { - override suspend fun createPolyline(state: PolylineState): MapboxActualPolyline? = createMapboxLines(state) + override suspend fun createPolyline(state: PolylineState): MapboxActualPolyline? = + createMapboxLines( + id = state.id, + points = state.points, + geodesic = state.geodesic, + strokeColor = state.strokeColor, + strokeWidth = state.strokeWidth, + ) override suspend fun updatePolylineProperties( polyline: MapboxActualPolyline, @@ -43,7 +51,13 @@ class MapboxPolylineOverlayRenderer( prev: PolylineEntity, ): MapboxActualPolyline? { // For Mapbox, we need to recreate the features when properties change - return createMapboxLines(current.state) + return createMapboxLines( + id = current.state.id, + points = current.state.points, + geodesic = current.state.geodesic, + strokeColor = current.state.strokeColor, + strokeWidth = current.state.strokeWidth, + ) } override suspend fun removePolyline(entity: PolylineEntity) { @@ -62,29 +76,6 @@ class MapboxPolylineOverlayRenderer( } } - private fun createMapboxLines(state: PolylineState): List { - val geoPoints: List = - when (state.geodesic) { - true -> createInterpolatePoints(state.points) - false -> createLinearInterpolatePoints(state.points) - }.map { it.normalize() } - - return splitByMeridian(geoPoints, state.geodesic).mapIndexed { index, linePoints -> - val points = linePoints.map { GeoPoint.from(it).toPoint() } - val id = "polyline-${state.id}-$index" - - return@mapIndexed Feature.fromGeometry( - LineString.fromLngLats(points), - JsonObject().apply { - addProperty(MapboxPolylineLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) - addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, ResourceProvider.dpToPx(state.strokeWidth.value)) - addProperty("id", id) - }, - id, - ) - } - } - private fun getAllPolylineEntities(): List> { // This would need access to the polyline manager // For now, we'll implement a simple workaround From 72b30763375d1cd142ef2f01d5c33fa343f9e172 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sun, 31 Aug 2025 01:39:49 +0900 Subject: [PATCH 33/44] wip: enhance polygon and circle UI with interactive controls Add fill opacity and stroke width sliders to polygon and circle examples, refactor ViewModels to use remember pattern, and improve component state management --- .../com/mapconductor/example/DemoAppScreen.kt | 1 + .../com/mapconductor/example/MainActivity.kt | 2 +- .../pages/circle/CircleMapComponent.kt | 20 ++-- .../example/pages/circle/CircleMapPage.kt | 50 ++++++++-- .../pages/circle/CirclePageViewModel.kt | 41 ++++---- .../pages/groundimage/GroundImageMapPage.kt | 18 +--- .../example/pages/map/flyto/FlyToMapPage.kt | 18 +--- .../pages/polygon/PolygonMapComponent.kt | 17 ++-- .../example/pages/polygon/PolygonMapPage.kt | 32 +----- .../pages/polygon/PolygonMapPageViewModel.kt | 54 ++++------- .../pages/polyline/PolylineMapComponent.kt | 2 - .../example/pages/polyline/PolylineMapPage.kt | 28 +----- .../pages/polyline/PolylinePageViewModel.kt | 97 ++++++++----------- .../example/pages/stores/StoreMapPage.kt | 3 +- .../com/mapconductor/core/OverlayProvider.kt | 2 +- .../mapconductor/core/features/GeoPoint.kt | 87 +++++++++++++---- .../circle/GoogleMapCircleRenderer.kt | 3 - .../polygon/GoogleMapPolygonRenderer.kt | 2 + .../mapbox/polygon/MapboxPolygonConductor.kt | 27 ++++-- 19 files changed, 241 insertions(+), 263 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index 3c2c1c40..ce8fe918 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -26,6 +26,7 @@ import com.mapconductor.example.pages.map.flyto.FlyToMapIcons import com.mapconductor.example.pages.map.flyto.FlyToMapPage import com.mapconductor.example.pages.polygon.PolygonMapPage import com.mapconductor.example.pages.polyline.PolylineMapPage +import com.mapconductor.example.pages.polyline.PolylinePageViewModelImpl import com.mapconductor.example.pages.stores.StoreMapPage import com.mapconductor.example.ui.sidebar.Sidebar import com.mapconductor.example.ui.sidebar.SidebarItem diff --git a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt index e70cdbdd..71aca370 100644 --- a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt +++ b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt @@ -12,7 +12,7 @@ class MainActivity : ComponentActivity() { setContent { DemoAppScreen( - initPage = "polygon", + initPage = "circle", ) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt index ff0bc995..5a17a8b8 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt @@ -4,20 +4,22 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier import com.mapconductor.core.circle.Circle +import com.mapconductor.core.circle.CircleState import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.example.MapViewContainer @Composable fun CircleMapComponent( mapViewState: MapViewState<*>?, - viewModel: CirclePageViewModel, + circleState: CircleState, + centerMarker: MarkerState, + edgeMarker: MarkerState, modifier: Modifier = Modifier, - onMapClick: OnMapEventHandler = {}, - onMarkerClick: OnMarkerEventHandler = {}, onCircleClick: OnCircleEventHandler = {}, onMarkerMove: OnMarkerEventHandler = {}, ) { @@ -25,25 +27,19 @@ fun CircleMapComponent( MapViewContainer( modifier = modifier, state = it, - onMapClick = onMapClick, - onMarkerClick = onMarkerClick, onCircleClick = onCircleClick, onMarkerDragStart = onMarkerMove, onMarkerDrag = onMarkerMove, onMarkerDragEnd = onMarkerMove, ) { // Circle - Circle(viewModel.circleState) + Circle(circleState) // Center marker (not draggable) - key(viewModel.centerMarker.id) { - Marker(viewModel.centerMarker) - } + Marker(centerMarker) // Edge marker (draggable) - key(viewModel.edgeMarker.id) { - Marker(viewModel.edgeMarker) - } + Marker(edgeMarker) } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt index 2d6ffd67..9b12e90d 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt @@ -1,12 +1,16 @@ package com.mapconductor.example.pages.circle +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -20,9 +24,9 @@ import com.mapconductor.example.ui.MessageCard @Composable fun CircleMapPage( - viewModel: CirclePageViewModel = CirclePageViewModelImpl(), onToggleSidebar: () -> Unit = {}, ) { + val viewModel = remember { CirclePageViewModelImpl() } DemoMapPageScaffold( menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, @@ -32,15 +36,16 @@ fun CircleMapPage( CircleMapComponent( mapViewState = mapViewState.value, - viewModel = viewModel, - onMapClick = viewModel::onMapClick, - onMarkerClick = viewModel::onMarkerClick, + circleState = viewModel.circleState, + centerMarker = viewModel.centerMarker, + edgeMarker = viewModel.edgeMarker, onCircleClick = viewModel::onCircleClick, onMarkerMove = viewModel::onMarkerMove, ) - // Message Card MessageCard( + title = "Circle Example", + maxHeight = 250.dp, modifier = Modifier .align(Alignment.BottomStart) @@ -49,12 +54,37 @@ fun CircleMapPage( start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, ), - title = "Messages", ) { - Text( - text = stringResource(R.string.circle_example_description), - modifier = Modifier.fillMaxSize(), - ) + + // Fill Opacity Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Fill Opacity: ${String.format("%.1f", viewModel.fillOpacity)}") + Slider( + value = viewModel.fillOpacity, + onValueChange = { viewModel.fillOpacity = it }, + valueRange = 0f..1f, + colors = + SliderDefaults.colors(), + ) + } + + // Stroke Width Control + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Stroke Width: ${String.format("%.1f", viewModel.strokeWidth)}dp") + Slider( + value = viewModel.strokeWidth, + onValueChange = { viewModel.strokeWidth = it }, + valueRange = 0f..10f, + colors = + SliderDefaults.colors(), + ) + } } ToastHost( diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt index 28d4e670..d7938806 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel @@ -31,6 +32,8 @@ interface CirclePageViewModel { val centerMarker: MarkerState val edgeMarker: MarkerState val circleState: CircleState + var fillOpacity: Float + var strokeWidth: Float fun onMapViewChanged(state: MapViewState<*>) @@ -54,14 +57,16 @@ class CirclePageViewModelImpl : override val messages: StateFlow> = _messages.asStateFlow() private val colors: List = listOf( - Color.Blue.copy(0.2f), - Color.Red.copy(alpha = 0.2f), - Color.Green.copy(alpha = 0.2f), - Color.Cyan.copy(alpha = 0.2f), - Color.LightGray.copy(alpha = 0.2f), - Color.Magenta.copy(alpha = 0.2f), + Color.Blue, + Color.Red, + Color.Green, + Color.Cyan, + Color.LightGray, + Color.Magenta, ) private var tapIdx = 0 + override var fillOpacity by mutableStateOf(0.3f) + override var strokeWidth by mutableStateOf(3.0f) override val initCameraPosition = MapCameraPosition( @@ -117,19 +122,15 @@ class CirclePageViewModelImpl : haversineDistance(circleCenter, _edgeMarker.value.position) } - private val _circleState: MutableState = - mutableStateOf( - CircleState( - id = "circle", - center = circleCenter, - radiusMeters = 1000.0, // Initial radius - strokeColor = Color.Blue.copy(alpha = 0.5f), - strokeWidth = 2.dp, - fillColor = this.colors[0], - ), - ) override val circleState: CircleState - get() = _circleState.value + get() = CircleState( + id = "circle", + center = circleCenter, + radiusMeters = radiusMeters, // Initial radius + strokeColor = Color.Blue.copy(alpha = 0.5f), + strokeWidth = strokeWidth.dp, + fillColor = this.colors[0].copy(alpha = fillOpacity), + ) private val _mapViewState = MutableStateFlow?>(null) override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() @@ -148,7 +149,7 @@ class CirclePageViewModelImpl : override fun onCircleClick(event: CircleClickEvent) { this.tapIdx = (this.tapIdx + 1) % this.colors.size - event.state.fillColor = this.colors[this.tapIdx] + event.state.fillColor = this.colors[this.tapIdx].copy(alpha = fillOpacity) showToast("Circle clicked - Radius: ${radiusMeters.toInt()}m") } @@ -156,7 +157,7 @@ class CirclePageViewModelImpl : _edgeMarker.value.position = dragged.position // Update circle radius - _circleState.value.radiusMeters = radiusMeters // haversineDistance(circleCenter, _edgeMarker.value.position) + circleState.radiusMeters = radiusMeters // haversineDistance(circleCenter, _edgeMarker.value.position) // showToast("Radius updated: ${radiusMeters.toInt()}m") } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index ea5ada4a..4a384449 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -9,14 +9,12 @@ import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.toast.ToastHost import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.GroundImageCapableMapViewItems @@ -27,19 +25,7 @@ fun GroundImageMapPage( groundImageResources: GroundImageResources, onToggleSidebar: () -> Unit = {}, ) { - val viewModel: GroundImageMapPageViewModel = - viewModel( - factory = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(GroundImageMapPageViewModelImpl::class.java)) { - @Suppress("UNCHECKED_CAST") - return GroundImageMapPageViewModelImpl(groundImageResources) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } - }, - ) + val viewModel = remember { GroundImageMapPageViewModelImpl(groundImageResources) } DemoMapPageScaffold( menuItems = GroundImageCapableMapViewItems(viewModel.initCameraPosition), diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt index 914f404f..5566c9ca 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapPage.kt @@ -18,13 +18,11 @@ import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MessageCard @@ -34,19 +32,7 @@ fun FlyToMapPage( icons: FlyToMapIcons, onToggleSidebar: () -> Unit = {}, ) { - val viewModel: FlyToPageViewModel = - viewModel( - factory = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(FlyToPageViewModelImpl::class.java)) { - @Suppress("UNCHECKED_CAST") - return FlyToPageViewModelImpl(icons) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } - }, - ) + val viewModel: FlyToPageViewModel = remember { FlyToPageViewModelImpl(icons) } DemoMapPageScaffold( menuItems = DefaultMapViewItems(viewModel.initCameraPosition), diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt index edb8cf0f..e5f4d281 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt @@ -4,31 +4,32 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polygon.Polygon +import com.mapconductor.core.polygon.PolygonState import com.mapconductor.example.MapViewContainer @Composable fun PolygonMapComponent( mapViewState: MapViewState<*>?, - viewModel: PolygonMapPageViewModel, + polygonVertexMarkers: List, + polygonState: PolygonState, modifier: Modifier = Modifier, - onPolygonClick: OnPolygonEventHandler = {}, - onMarkerDrag: OnMarkerEventHandler = {}, + onMarkerDrag: OnMarkerEventHandler, ) { - mapViewState?.let { mapViewState -> + mapViewState?.let { it -> MapViewContainer( modifier = modifier, - state = mapViewState, - onPolygonClick = onPolygonClick, + state = it, onMarkerDrag = onMarkerDrag, ) { // Polygon - Polygon(viewModel.polygonState) + Polygon(polygonState) // Vertex markers (draggable) - viewModel.polygonVertexMarkers.forEach { marker -> + polygonVertexMarkers.forEach { marker -> Marker(marker) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt index b3a7dd72..76a3acbd 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt @@ -9,34 +9,18 @@ import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel -import com.mapconductor.example.toast.ToastHost import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MessageCard import com.mapconductor.example.ui.PolygonCapableMapViewItems @Composable fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { - val viewModel: PolygonMapPageViewModel = - viewModel( - factory = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(PolygonMapPageViewModelImpl::class.java)) { - @Suppress("UNCHECKED_CAST") - return PolygonMapPageViewModelImpl() as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } - }, - ) + val viewModel = remember { PolygonMapPageViewModelImpl() } DemoMapPageScaffold( initSelect = 1, @@ -49,12 +33,12 @@ fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { // Map Component PolygonMapComponent( mapViewState = mapViewState.value, - viewModel = viewModel, + polygonState = viewModel.polygonState, + polygonVertexMarkers = viewModel.polygonVertexMarkers, modifier = Modifier.padding( bottom = paddingValues.calculateBottomPadding(), ), - onPolygonClick = viewModel::onPolygonClick, onMarkerDrag = viewModel::onMarkerDrag, ) MessageCard( @@ -94,17 +78,11 @@ fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { Slider( value = viewModel.strokeWidth, onValueChange = { viewModel.strokeWidth = it }, - valueRange = 1f..10f, + valueRange = 0f..10f, colors = SliderDefaults.colors(), ) } } - - // Toast messages - ToastHost( - messages = viewModel.messages.collectAsState().value, - onDismiss = viewModel::removeToast, - ) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt index 34f95a53..4ee8bac1 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt @@ -1,6 +1,7 @@ package com.mapconductor.example.pages.polygon import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color @@ -15,6 +16,7 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.example.toast.ToastMessage +import android.os.Bundle import android.util.Log import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -23,7 +25,6 @@ import kotlinx.coroutines.flow.asStateFlow interface PolygonMapPageViewModel { val initCameraPosition: MapCameraPosition val mapViewState: StateFlow?> - val messages: StateFlow> val polygonVertexMarkers: List var fillOpacity: Float @@ -32,29 +33,19 @@ interface PolygonMapPageViewModel { fun onMapViewChanged(state: MapViewState<*>) - fun onPolygonClick(clicked: PolygonEvent) - fun onMarkerDrag(dragged: MarkerState) - - fun showToast(text: String) - - fun removeToast(toastMessage: ToastMessage) } class PolygonMapPageViewModelImpl : ViewModel(), PolygonMapPageViewModel { - private val _mapViewState = MutableStateFlow?>(null) override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() - private val _messages = MutableStateFlow>(emptyList()) - override val messages: StateFlow> = _messages.asStateFlow() - - // Polygon vertices (triangle around Tokyo landmarks) + // Polygon vertices private val polygonVertices = - mutableListOf( + mutableStateListOf( GeoPoint(41.79883, 140.75675), GeoPoint(41.799240000000005, 140.75875000000002), GeoPoint(41.797650000000004, 140.75905), @@ -82,9 +73,16 @@ class PolygonMapPageViewModelImpl : polygonVertices.mapIndexed { index, point -> MarkerState( position = point, - icon = DefaultIcon(), + icon = DefaultIcon( + scale = 0.7f, + fillColor = Color.Yellow, + strokeColor = Color.Black, + ), id = "vertex_$index", draggable = true, + extra = Bundle().apply { + putInt("index", index) + } ) } @@ -93,7 +91,7 @@ class PolygonMapPageViewModelImpl : PolygonState( points = polygonVertices, id = "example_polygon", - strokeColor = Color.Blue, + strokeColor = Color.Red, strokeWidth = strokeWidth.dp, fillColor = Color.Blue.copy(alpha = fillOpacity), geodesic = false, @@ -103,29 +101,11 @@ class PolygonMapPageViewModelImpl : _mapViewState.value = state } - override fun onPolygonClick(clicked: PolygonEvent) { - val clickedPoint = clicked.clicked - if (clickedPoint != null) { - showToast("Polygon clicked at: ${clickedPoint.latitude}, ${clickedPoint.longitude}") - } else { - showToast("Polygon clicked") - } - } - override fun onMarkerDrag(dragged: MarkerState) { - when (dragged.id) { - "vertex_0" -> polygonVertices[0] = GeoPoint.from(dragged.position) - "vertex_1" -> polygonVertices[1] = GeoPoint.from(dragged.position) - "vertex_2" -> polygonVertices[2] = GeoPoint.from(dragged.position) + (dragged.extra as? Bundle)?.getInt("index")?.let { index -> + if (index >= 0 && index < polygonVertices.size) { + polygonVertices[index] = GeoPoint.from(dragged.position) + } } } - - override fun showToast(text: String) { - val newMessage = ToastMessage(text = text) - _messages.value = _messages.value + newMessage - } - - override fun removeToast(toastMessage: ToastMessage) { - _messages.value = _messages.value - toastMessage - } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt index 538ae3f5..3d8e28a6 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt @@ -17,14 +17,12 @@ fun PolylineMapComponent( polylineState: PolylineState, wayPointMarkers: List, modifier: Modifier = Modifier, - onPolylineClick: OnPolylineEventHandler = {}, onMarkerDrag: OnMarkerEventHandler = {}, ) { mapViewState?.let { it -> MapViewContainer( modifier = modifier, state = it, - onPolylineClick = onPolylineClick, onMarkerDrag = onMarkerDrag, ) { // Polyline diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt index b3f7aa75..9faf3d81 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt @@ -2,14 +2,15 @@ package com.mapconductor.example.pages.polyline import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold @Composable fun PolylineMapPage( - viewModel: PolylinePageViewModel = PolylinePageViewModelImpl(), onToggleSidebar: () -> Unit = {}, ) { + val viewModel = remember { PolylinePageViewModelImpl() } DemoMapPageScaffold( menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, @@ -21,32 +22,7 @@ fun PolylineMapPage( polylineState = viewModel.polylineState, wayPointMarkers = viewModel.wayPointMarkers, mapViewState = mapViewState.value, - onPolylineClick = viewModel::onPolylineClick, onMarkerDrag = viewModel::onMarkerDrag, ) - -// MessageCard( -// modifier = -// Modifier -// .align(Alignment.BottomStart) -// .padding( -// bottom = paddingValues.calculateBottomPadding() + 16.dp, -// start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, -// end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, -// ), -// title = "Messages", -// ) { -// MapViewStatePanel( -// mapViewState.value -// ?.mapCameraPosition -// ?.collectAsState() -// ?.value, -// ) -// } -// -// ToastHost( -// messages = viewModel.messages.collectAsState().value, -// onDismiss = { viewModel.removeToast(it) }, -// ) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt index 652209b9..ecc0d454 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt @@ -12,6 +12,7 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.polyline.PolylineState +import android.os.Bundle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -25,8 +26,6 @@ interface PolylinePageViewModel { fun onMapViewChanged(state: MapViewState<*>) - fun onPolylineClick(state: PolylineState) - fun onMarkerDrag(dragged: MarkerState) } @@ -56,51 +55,44 @@ class PolylinePageViewModelImpl : GeoPoint.fromLatLong(21.382314, -157.933097), // Back to center ) - private val _wayPointMarkers: MutableState> = - mutableStateOf( - polylinePoints.mapIndexed { index, point -> - val markerColor = - when { - index == 0 -> Color.Green - index == polylinePoints.size - 1 -> Color.Green - else -> Color.Yellow - } - val label = - when { - index == 0 -> "S" - index == polylinePoints.size - 1 -> "E" - else -> "$index" - } - MarkerState( - id = "waypoint_$index", - position = point, - icon = - DefaultIcon( - fillColor = markerColor, - strokeColor = Color.Black, - label = label, - ), - draggable = true, - ) - }, - ) - - override val wayPointMarkers: List - get() = _wayPointMarkers.value - - private val _polylineState: MutableState = - mutableStateOf( - PolylineState( - id = "example_polyline", - points = polylinePoints, - strokeColor = Color.Red, - strokeWidth = 4.dp, - geodesic = true, - ), - ) + override val wayPointMarkers: List = + polylinePoints.mapIndexed { index, point -> + val markerColor = + when { + index == 0 -> Color.Green + index == polylinePoints.size - 1 -> Color.Green + else -> Color.Yellow + } + val label = + when { + index == 0 -> "S" + index == polylinePoints.size - 1 -> "E" + else -> "$index" + } + MarkerState( + id = "waypoint_$index", + position = point, + icon = + DefaultIcon( + fillColor = markerColor, + strokeColor = Color.Black, + label = label, + ), + draggable = true, + extra = Bundle().apply { + putInt("index", index) + } + ) + } override val polylineState: PolylineState - get() = _polylineState.value + get() = PolylineState( + id = "example_polyline", + points = polylinePoints, + strokeColor = Color.Red, + strokeWidth = 4.dp, + geodesic = true, + ) private val _mapViewState = MutableStateFlow?>(null) override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() @@ -109,17 +101,12 @@ class PolylinePageViewModelImpl : this._mapViewState.value = state } - override fun onPolylineClick(state: PolylineState) { - _polylineState.value.strokeColor = Color.Magenta - } - override fun onMarkerDrag(dragged: MarkerState) { - val markerIndex = _wayPointMarkers.value.indexOfFirst { it.id == dragged.id } - if (markerIndex < 0) return - - // 1. pointsを更新 - polylinePoints[markerIndex].latitude = dragged.position.latitude - polylinePoints[markerIndex].longitude = dragged.position.longitude + (dragged.extra as? Bundle)?.getInt("index")?.let { index -> + if (index >= 0 && index < polylinePoints.size) { + polylinePoints[index] = GeoPoint.from(dragged.position) + } + } } override fun onCleared() { diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt index e60a9b14..7473b95f 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt @@ -2,15 +2,16 @@ package com.mapconductor.example.pages.stores import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold @Composable fun StoreMapPage( - viewModel: StoreMapPageViewModel = StoreMapPageViewModelImpl(), onToggleSidebar: () -> Unit = {}, ) { + val viewModel = remember { StoreMapPageViewModelImpl() } val context = LocalContext.current DemoMapPageScaffold( diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index 124ee827..c4980a71 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -89,7 +89,7 @@ class CircleOverlay( data: List, controller: MapViewControllerAlias, ) { -// controller.addCircles(data) + controller.addCircles(data) } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt index 825991c6..03316243 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt @@ -13,15 +13,11 @@ interface IGeoPoint { val altitude: Double? } -class GeoPoint( - latitude: Double, - longitude: Double, - altitude: Double = 0.0, -) : IGeoPoint { - override var latitude by mutableStateOf(latitude) - override var longitude by mutableStateOf(longitude) - override var altitude by mutableStateOf(altitude) - +data class GeoPoint( + override val latitude: Double, + override val longitude: Double, + override val altitude: Double = 0.0, +): IGeoPoint { fun toUrlValue(precision: Int = 6): String = "${latitude.toFixed(precision)},${longitude.toFixed(precision)}" override fun equals(other: Any?): Boolean { @@ -46,17 +42,6 @@ class GeoPoint( return result } - fun copy( - latitude: Double? = null, - longitude: Double? = null, - altitude: Double? = null, - ): GeoPoint = - GeoPoint( - latitude = latitude ?: this.latitude, - longitude = longitude ?: this.longitude, - altitude = altitude ?: this.altitude, - ) - companion object { fun fromLatLong( latitude: Double, @@ -80,6 +65,68 @@ class GeoPoint( } } } +//class GeoPoint( +// latitude: Double, +// longitude: Double, +// altitude: Double = 0.0, +//) : IGeoPoint { +// override var latitude by mutableStateOf(latitude) +// override var longitude by mutableStateOf(longitude) +// override var altitude by mutableStateOf(altitude) +// +// fun toUrlValue(precision: Int = 6): String = "${latitude.toFixed(precision)},${longitude.toFixed(precision)}" +// +// override fun equals(other: Any?): Boolean { +// if (this === other) return true +// if (other !is GeoPoint) return false +// +// val tolerance = 1e-7 +// return abs(latitude - other.latitude) < tolerance && +// abs(longitude - other.longitude) < tolerance && +// abs(altitude - other.altitude) < tolerance +// } +// +// override fun hashCode(): Int { +// // 誤差許容しているため、丸めた値を使って hash を安定させる +// val latHash = (latitude * 1e7).toLong() +// val lngHash = (longitude * 1e7).toLong() +// val altHash = (altitude * 1e7).toLong() +// +// var result = latHash.hashCode() +// result = 31 * result + lngHash.hashCode() +// result = 31 * result + altHash.hashCode() +// return result +// } +// +// fun copy( +// latitude: Double? = null, +// longitude: Double? = null, +// altitude: Double? = null, +// ): GeoPoint = +// GeoPoint( +// latitude = latitude ?: this.latitude, +// longitude = longitude ?: this.longitude, +// altitude = altitude ?: this.altitude, +// ) +// +// companion object { +// fun fromLatLong( +// latitude: Double, +// longitude: Double, +// ) = GeoPoint(latitude, longitude) +// +// fun fromLongLat( +// longitude: Double, +// latitude: Double, +// ) = GeoPoint(latitude, longitude) +// +// fun from(position: IGeoPoint) = GeoPoint( +// latitude = position.latitude, +// longitude = position.longitude, +// altitude = position.altitude ?: 0.0, +// ) +// } +//} /** * Extension function to create a normalized GeoPoint with clamped/normalized coordinates diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt index 657fa7f6..806e1a31 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt @@ -14,7 +14,6 @@ import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoPoint import com.mapconductor.googlemaps.GoogleMapViewHolder import com.mapconductor.googlemaps.toLatLng -import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -64,13 +63,11 @@ class GoogleMapCircleRenderer( } override suspend fun changeCircle(changes: List>): List { - Log.d("debug", "--->changeCircle") return withContext(coroutine.coroutineContext) { return@withContext changes.map { params -> val circle = params.entity.circle val finger = params.entity.fingerPrint val prevFinger = params.prevEntity.fingerPrint - Log.d("debug", " ---> ${finger.hashCode()} = ${prevFinger.hashCode()}") if (finger.center != prevFinger.center) { circle.center = GeoPoint.from(params.entity.state.center).toLatLng() diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt index a46d4b27..4f1d75e8 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt @@ -17,6 +17,7 @@ import com.mapconductor.googlemaps.GoogleMapViewHolder import com.mapconductor.googlemaps.polyline.GoogleMapPolylineOverlayRenderer import com.mapconductor.googlemaps.toLatLng import kotlin.coroutines.coroutineContext +import android.util.Log import kotlinx.coroutines.AbstractCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -62,6 +63,7 @@ class GoogleMapPolygonOverlayRenderer( val polygon = current.polygon val finger = current.fingerPrint val prevFinger = prev.fingerPrint + Log.d("GoogleMaps", "----->$finger, $prevFinger") if (finger.points != prevFinger.points) { val points = current.state.points diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt index 0e830ef0..c33b71ff 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt @@ -48,6 +48,25 @@ class MapboxPolygonConductor( } override suspend fun update(state: PolygonState) { + + polygonOverlay.createPolygon(state)?.let { polygon -> + val polygonEntity = PolygonEntityImpl( + polygon = polygon, + state = state, + ) + polygonOverlay.polygonManager.registerEntity(polygonEntity) + } + + val polylineState = state.toPolylineState() + polylineOverlay.createPolyline(polylineState)?.let { polyline -> + val polylineEntity = PolylineEntityImpl( + polyline = polyline, + state = polylineState, + ) + polylineOverlay.polylineManager.registerEntity(polylineEntity) + } + polygonOverlay.onPostProcess() + polylineOverlay.onPostProcess() } override var clickListener: ((PolygonEvent) -> Unit)? = null @@ -55,14 +74,6 @@ class MapboxPolygonConductor( return null } - suspend fun updatePolygonProperties( - polygon: MapboxOutlineAndFill, - current: PolygonEntity, - prev: PolygonEntity - ): MapboxOutlineAndFill? { - return null - } - override suspend fun clear() { } } From 73449717b845897b8f1fe8f3b1e7dedb53ce1f97 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sun, 31 Aug 2025 17:01:22 +0900 Subject: [PATCH 34/44] Fix: marker.icon.scale property does not work on ArcGIS and Google Maps SDKs --- .../com/mapconductor/example/DemoAppScreen.kt | 14 ++++- .../pages/marker/MarkerBasicMapComponent.kt | 62 +++++++++++++++++++ .../example/pages/marker/MarkerBasicPage.kt | 42 +++++++++++++ .../mapconductor/core/marker/DefaultIcon.kt | 2 +- .../arcgis/marker/ArcGISMarkerRenderer.kt | 6 +- .../mapconductor/mapbox/marker/MarkerLayer.kt | 2 +- 6 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index ce8fe918..bc6319d3 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -24,9 +24,9 @@ import com.mapconductor.example.pages.groundimage.GroundImageMapPage import com.mapconductor.example.pages.groundimage.GroundImageResources import com.mapconductor.example.pages.map.flyto.FlyToMapIcons import com.mapconductor.example.pages.map.flyto.FlyToMapPage +import com.mapconductor.example.pages.marker.MarkerBasicPage import com.mapconductor.example.pages.polygon.PolygonMapPage import com.mapconductor.example.pages.polyline.PolylineMapPage -import com.mapconductor.example.pages.polyline.PolylinePageViewModelImpl import com.mapconductor.example.pages.stores.StoreMapPage import com.mapconductor.example.ui.sidebar.Sidebar import com.mapconductor.example.ui.sidebar.SidebarItem @@ -78,6 +78,12 @@ fun DemoAppScreen(initPage: String = "map") { icon = Icons.Default.Home, route = "map", ), + SidebarItem( + id = "marker-basic", + title = "Marker", + icon = Icons.Default.Home, + route = "marker-basic", + ), SidebarItem( id = "flyTo", title = "Move camera", @@ -122,7 +128,6 @@ fun DemoAppScreen(initPage: String = "map") { // ), ) - AppTheme { Box(modifier = Modifier.fillMaxSize()) { // Main content @@ -133,6 +138,11 @@ fun DemoAppScreen(initPage: String = "map") { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "marker-basic" -> { + MarkerBasicPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } "circle" -> { CircleMapPage( onToggleSidebar = navigationViewModel::toggleSidebar, diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt new file mode 100644 index 00000000..083b8065 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt @@ -0,0 +1,62 @@ +package com.mapconductor.example.pages.marker + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.Marker +import com.mapconductor.example.MapViewContainer + +@Composable +fun MarkerBasicMapComponent( + mapViewState: MapViewState<*>, + modifier: Modifier = Modifier, +) { + MapViewContainer( + state = mapViewState, + modifier = modifier, + ) { +// Marker( +// position = GeoPoint.fromLatLong(0.0, 0.004), +// ) +// Marker( +// position = GeoPoint.fromLatLong(0.0, 0.008), +// icon = DefaultIcon( +// fillColor = Color.Yellow, +// strokeColor = Color.Black, +// strokeWidth = 2.dp, +// ), +// ) +// Marker( +// position = GeoPoint.fromLatLong(0.0, 0.012), +// icon = DefaultIcon( +// label = "AB", +// labelTextColor = Color.White, +// labelStrokeColor = Color.Black, +// ), +// ) + + Marker( + position = GeoPoint.fromLatLong(-0.004, 0.004), + icon = + DefaultIcon( + scale = 0.7f, + ), + ) + +// Marker( +// position = GeoPoint.fromLatLong(-0.004, 0.008), +// icon = DefaultIcon( +// scale = 1.4f, +// ), +// ) +// +// Marker( +// position = GeoPoint.fromLatLong(-0.004, 0.012), +// icon = DefaultIcon( +// scale = 2.1f, +// ), +// ) + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt new file mode 100644 index 00000000..4733b674 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt @@ -0,0 +1,42 @@ +package com.mapconductor.example.pages.marker + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.example.ui.DefaultMapViewItems +import com.mapconductor.example.ui.DemoMapPageScaffold + +@Composable +fun MarkerBasicPage(onToggleSidebar: () -> Unit = {}) { + val initCameraPosition = + remember { + MapCameraPosition( + position = GeoPoint(0.0, 0.0), + zoom = 15.0, + ) + } + + var mapViewState by remember { mutableStateOf?>(null) } + + DemoMapPageScaffold( + menuItems = DefaultMapViewItems(initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = { newMapViewState -> + mapViewState = newMapViewState + }, + ) { paddingValues -> + mapViewState?.let { + MarkerBasicMapComponent( + mapViewState = it, + modifier = Modifier.fillMaxSize(), + ) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt index e4e6f0de..9426e167 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt @@ -137,7 +137,7 @@ abstract class AbstractDefaultIcon( iconScale: Float, ): Path { val originalSize = Size(23.5f, 25.6f) - val markerScale = minOf(canvasSize / originalSize.width, canvasSize / originalSize.height) + val markerScale = minOf(canvasSize / originalSize.width, canvasSize / originalSize.height) * iconScale val scaledWidth = originalSize.width * markerScale val scaledHeight = originalSize.height * markerScale val offsetX = (canvasSize - scaledWidth) / 2f diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt index 5c384523..90a7425b 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt @@ -38,8 +38,10 @@ class ArcGISMarkerRenderer( .map { params -> val bitmapDrawable = params.bitmapIcon.bitmap.toDrawable(holder.mapView.context.resources) val density = ResourceProvider.getDensity() - val width = ((params.bitmapIcon.size.width * (params.state.icon?.scale ?: 1.0f)) / density) - val height = ((params.bitmapIcon.size.height * (params.state.icon?.scale ?: 1.0f)) / density) +// val width = ((params.bitmapIcon.size.width * (params.state.icon?.scale ?: 1.0f)) / density) +// val height = ((params.bitmapIcon.size.height * (params.state.icon?.scale ?: 1.0f)) / density) + val width = params.bitmapIcon.size.width / density + val height = params.bitmapIcon.size.height / density val anchorX = (0.5 - params.bitmapIcon.anchor.x) * width val anchorY = (params.bitmapIcon.anchor.y - 0.5) * height diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt index 289a3e98..9a948396 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt @@ -17,7 +17,7 @@ open class MarkerLayer( ) { val layer = symbolLayer(layerId, sourceId) { - iconSize(Expression.get(MapboxMarkerRenderer.Prop.SCALE)) +// iconSize(Expression.get(MapboxMarkerRenderer.Prop.SCALE)) iconImage(Expression.get(MapboxMarkerRenderer.Prop.ICON_ID)) iconAllowOverlap(true) iconIgnorePlacement(true) From cce97b7fe9eac987840011b0fdf6e1e5574b3970 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sun, 31 Aug 2025 17:04:19 +0900 Subject: [PATCH 35/44] Fix: KtLint errors --- .../com/mapconductor/example/MainActivity.kt | 2 +- .../pages/circle/CircleMapComponent.kt | 2 - .../example/pages/circle/CircleMapPage.kt | 8 +-- .../pages/circle/CirclePageViewModel.kt | 17 +++--- .../pages/groundimage/GroundImageMapPage.kt | 2 +- .../pages/polygon/PolygonMapComponent.kt | 1 - .../example/pages/polygon/PolygonMapPage.kt | 1 - .../pages/polygon/PolygonMapPageViewModel.kt | 27 ++++----- .../pages/polyline/PolylineMapComponent.kt | 1 - .../example/pages/polyline/PolylineMapPage.kt | 4 +- .../pages/polyline/PolylinePageViewModel.kt | 24 ++++---- .../example/pages/stores/StoreMapPage.kt | 6 +- .../mapconductor/core/features/GeoPoint.kt | 9 ++- .../polygon/AbstractPolygonOverlayRenderer.kt | 4 +- .../core/polygon/PolygonController.kt | 2 +- .../core/polygon/PolygonManager.kt | 6 +- .../core/polygon/PolygonOverlayRenderer.kt | 4 ++ .../core/polyline/PolylineController.kt | 9 ++- .../core/polyline/PolylineManager.kt | 1 - .../arcgis/polygon/ArcGISPolygonController.kt | 3 +- .../GoogleMapViewControllerStore.kt | 1 + .../polygon/GoogleMapPolygonRenderer.kt | 7 +-- .../here/polygon/HerePolygonRenderer.kt | 24 ++++---- .../com/mapconductor/mapbox/MapboxMapView.kt | 9 +-- .../mapconductor/mapbox/MapboxPolyUtils.kt | 2 +- .../mapbox/polygon/MapboxPolygonConductor.kt | 56 +++++++++---------- .../mapbox/polygon/MapboxPolygonController.kt | 22 ++++---- .../polyline/MapboxPolylineController.kt | 12 ---- 28 files changed, 115 insertions(+), 151 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt index 71aca370..6eb8b3a0 100644 --- a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt +++ b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt @@ -12,7 +12,7 @@ class MainActivity : ComponentActivity() { setContent { DemoAppScreen( - initPage = "circle", + initPage = "marker-basic", ) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt index 5a17a8b8..81552777 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapComponent.kt @@ -1,13 +1,11 @@ package com.mapconductor.example.pages.circle import androidx.compose.runtime.Composable -import androidx.compose.runtime.key import androidx.compose.ui.Modifier import com.mapconductor.core.circle.Circle import com.mapconductor.core.circle.CircleState import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.Marker import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt index 9b12e90d..619113bd 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CircleMapPage.kt @@ -3,7 +3,6 @@ package com.mapconductor.example.pages.circle import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults @@ -13,19 +12,15 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import com.mapconductor.example.R import com.mapconductor.example.toast.ToastHost import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold import com.mapconductor.example.ui.MessageCard @Composable -fun CircleMapPage( - onToggleSidebar: () -> Unit = {}, -) { +fun CircleMapPage(onToggleSidebar: () -> Unit = {}) { val viewModel = remember { CirclePageViewModelImpl() } DemoMapPageScaffold( menuItems = DefaultMapViewItems(viewModel.initCameraPosition), @@ -55,7 +50,6 @@ fun CircleMapPage( end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, ), ) { - // Fill Opacity Control Column( modifier = Modifier.padding(horizontal = 16.dp), diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt index d7938806..e0fa5626 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt @@ -123,14 +123,15 @@ class CirclePageViewModelImpl : } override val circleState: CircleState - get() = CircleState( - id = "circle", - center = circleCenter, - radiusMeters = radiusMeters, // Initial radius - strokeColor = Color.Blue.copy(alpha = 0.5f), - strokeWidth = strokeWidth.dp, - fillColor = this.colors[0].copy(alpha = fillOpacity), - ) + get() = + CircleState( + id = "circle", + center = circleCenter, + radiusMeters = radiusMeters, // Initial radius + strokeColor = Color.Blue.copy(alpha = 0.5f), + strokeWidth = strokeWidth.dp, + fillColor = this.colors[0].copy(alpha = fillOpacity), + ) private val _mapViewState = MutableStateFlow?>(null) override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index 4a384449..9a3a7224 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -25,7 +25,7 @@ fun GroundImageMapPage( groundImageResources: GroundImageResources, onToggleSidebar: () -> Unit = {}, ) { - val viewModel = remember { GroundImageMapPageViewModelImpl(groundImageResources) } + val viewModel = remember { GroundImageMapPageViewModelImpl(groundImageResources) } DemoMapPageScaffold( menuItems = GroundImageCapableMapViewItems(viewModel.initCameraPosition), diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt index e5f4d281..a71f1574 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapComponent.kt @@ -6,7 +6,6 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.Marker import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler -import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polygon.Polygon import com.mapconductor.core.polygon.PolygonState import com.mapconductor.example.MapViewContainer diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt index 76a3acbd..4d6adc99 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPage.kt @@ -53,7 +53,6 @@ fun PolygonMapPage(onToggleSidebar: () -> Unit = {}) { end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, ), ) { - // Fill Opacity Control Column( modifier = Modifier.padding(horizontal = 16.dp), diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt index 4ee8bac1..f672b2c9 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt @@ -8,16 +8,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.GeoRectBounds import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerState -import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.example.toast.ToastMessage import android.os.Bundle -import android.util.Log import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -39,7 +35,6 @@ interface PolygonMapPageViewModel { class PolygonMapPageViewModelImpl : ViewModel(), PolygonMapPageViewModel { - private val _mapViewState = MutableStateFlow?>(null) override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() @@ -57,12 +52,12 @@ class PolygonMapPageViewModelImpl : GeoPoint(41.79576, 140.75475), GeoPoint(41.796150000000004, 140.75364000000002), GeoPoint(41.79744, 140.75454000000002), - GeoPoint(41.79909000000001, 140.75465) + GeoPoint(41.79909000000001, 140.75465), ) override val initCameraPosition = MapCameraPosition( - position = GeoPoint(41.796855,140.756910), + position = GeoPoint(41.796855, 140.756910), zoom = 16.0, ) @@ -73,16 +68,18 @@ class PolygonMapPageViewModelImpl : polygonVertices.mapIndexed { index, point -> MarkerState( position = point, - icon = DefaultIcon( - scale = 0.7f, - fillColor = Color.Yellow, - strokeColor = Color.Black, - ), + icon = + DefaultIcon( + scale = 0.7f, + fillColor = Color.Yellow, + strokeColor = Color.Black, + ), id = "vertex_$index", draggable = true, - extra = Bundle().apply { - putInt("index", index) - } + extra = + Bundle().apply { + putInt("index", index) + }, ) } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt index 3d8e28a6..a33adb92 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt @@ -6,7 +6,6 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.Marker import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler -import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.Polyline import com.mapconductor.core.polyline.PolylineState import com.mapconductor.example.MapViewContainer diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt index 9faf3d81..e3afbe31 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt @@ -7,9 +7,7 @@ import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold @Composable -fun PolylineMapPage( - onToggleSidebar: () -> Unit = {}, -) { +fun PolylineMapPage(onToggleSidebar: () -> Unit = {}) { val viewModel = remember { PolylinePageViewModelImpl() } DemoMapPageScaffold( menuItems = DefaultMapViewItems(viewModel.initCameraPosition), diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt index ecc0d454..aece0396 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt @@ -1,8 +1,6 @@ package com.mapconductor.example.pages.polyline -import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel @@ -79,20 +77,22 @@ class PolylinePageViewModelImpl : label = label, ), draggable = true, - extra = Bundle().apply { - putInt("index", index) - } + extra = + Bundle().apply { + putInt("index", index) + }, ) } override val polylineState: PolylineState - get() = PolylineState( - id = "example_polyline", - points = polylinePoints, - strokeColor = Color.Red, - strokeWidth = 4.dp, - geodesic = true, - ) + get() = + PolylineState( + id = "example_polyline", + points = polylinePoints, + strokeColor = Color.Red, + strokeWidth = 4.dp, + geodesic = true, + ) private val _mapViewState = MutableStateFlow?>(null) override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt index 7473b95f..f426256c 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapPage.kt @@ -8,10 +8,8 @@ import com.mapconductor.example.ui.DefaultMapViewItems import com.mapconductor.example.ui.DemoMapPageScaffold @Composable -fun StoreMapPage( - onToggleSidebar: () -> Unit = {}, -) { - val viewModel = remember { StoreMapPageViewModelImpl() } +fun StoreMapPage(onToggleSidebar: () -> Unit = {}) { + val viewModel = remember { StoreMapPageViewModelImpl() } val context = LocalContext.current DemoMapPageScaffold( diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt index 03316243..c48df6b6 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/features/GeoPoint.kt @@ -1,7 +1,6 @@ package com.mapconductor.core.features import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.mapconductor.core.spherical.Spherical import com.mapconductor.core.toFixed @@ -17,7 +16,7 @@ data class GeoPoint( override val latitude: Double, override val longitude: Double, override val altitude: Double = 0.0, -): IGeoPoint { +) : IGeoPoint { fun toUrlValue(precision: Int = 6): String = "${latitude.toFixed(precision)},${longitude.toFixed(precision)}" override fun equals(other: Any?): Boolean { @@ -65,11 +64,11 @@ data class GeoPoint( } } } -//class GeoPoint( +// class GeoPoint( // latitude: Double, // longitude: Double, // altitude: Double = 0.0, -//) : IGeoPoint { +// ) : IGeoPoint { // override var latitude by mutableStateOf(latitude) // override var longitude by mutableStateOf(longitude) // override var altitude by mutableStateOf(altitude) @@ -126,7 +125,7 @@ data class GeoPoint( // altitude = position.altitude ?: 0.0, // ) // } -//} +// } /** * Extension function to create a normalized GeoPoint with clamped/normalized coordinates diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt index 45c7376d..eeb888d6 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/AbstractPolygonOverlayRenderer.kt @@ -1,8 +1,6 @@ package com.mapconductor.core.polygon import com.mapconductor.core.map.MapViewHolder -import com.mapconductor.core.polyline.PolylineState -import android.icu.util.Currency import kotlinx.coroutines.CoroutineScope abstract class AbstractPolygonOverlayRenderer : PolygonOverlayRenderer { @@ -21,7 +19,7 @@ abstract class AbstractPolygonOverlayRenderer : PolygonOverlayRen polygon: ActualPolygon, current: PolygonEntity, prev: PolygonEntity, - ) : ActualPolygon? + ): ActualPolygon? override suspend fun onAdd(data: List): List = data.map { params -> createPolygon(params.state) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt index 0a39fbbd..4c3c9c06 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt @@ -44,7 +44,7 @@ abstract class PolygonController( added.add( object : PolygonOverlayRenderer.AddParams { override val state: PolygonState = state - } + }, ) previous.remove(state.id) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt index f4b2739a..a61f29b4 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonManager.kt @@ -4,10 +4,15 @@ import com.mapconductor.core.features.IGeoPoint interface PolygonManager { fun registerEntity(entity: PolygonEntity) + fun removeEntity(id: String): PolygonEntity? + fun getEntity(id: String): PolygonEntity? + fun allEntities(): List> + fun clear() + fun find(position: IGeoPoint): PolygonEntity? } @@ -29,5 +34,4 @@ class PolygonManagerImpl : PolygonManager { } override fun find(position: IGeoPoint): PolygonEntity? = entities.values.firstOrNull() - } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt index b1dbac53..25681647 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlayRenderer.kt @@ -4,13 +4,17 @@ interface PolygonOverlayRenderer { interface AddParams { val state: PolygonState } + interface ChangeParams { val current: PolygonEntity val prev: PolygonEntity } suspend fun onAdd(data: List): List + suspend fun onChange(data: List>): List + suspend fun onRemove(data: List>) + suspend fun onPostProcess() } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt index f1f08007..38c4de02 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt @@ -5,16 +5,15 @@ import com.mapconductor.core.features.IGeoPoint import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit - abstract class PolylineController( val polylineManager: PolylineManager, open val renderer: PolylineOverlayRenderer, override var clickListener: OnPolylineEventHandler? = null, ) : OverlayController< - ActualPolyline, - PolylineState, - PolylineEntity, - PolylineState, + ActualPolyline, + PolylineState, + PolylineEntity, + PolylineState, > { override val zIndex: Int = 5 val semaphore = Semaphore(1) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt index 8c4d0817..e2eaf223 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineManager.kt @@ -2,7 +2,6 @@ package com.mapconductor.core.polyline import com.mapconductor.core.features.IGeoPoint - interface PolylineManager { fun registerEntity(entity: PolylineEntity) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt index 0c0bd008..542c92b2 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt @@ -31,7 +31,6 @@ class ArcGISPolygonController( override val renderer: ArcGISPolygonOverlayRenderer, ) : PolygonController(polygonManager, renderer) - class ArcGISPolygonOverlayRenderer( val polygonLayer: GraphicsOverlay, override val holder: ArcGISMapViewHolder, @@ -66,7 +65,7 @@ class ArcGISPolygonOverlayRenderer( override suspend fun updatePolygonProperties( polygon: ArcGISActualPolygon, current: PolygonEntity, - prev: PolygonEntity + prev: PolygonEntity, ): ArcGISActualPolygon? = withContext(coroutine.coroutineContext) { val finger = current.fingerPrint diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt index fe7ac90b..a0b3cd87 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt @@ -64,6 +64,7 @@ object GoogleMapViewControllerStore : StaticHolder( ) return controller } + private fun getPolylineController(holder: GoogleMapViewHolder): GoogleMapPolylineController { val renderer = GoogleMapPolylineOverlayRenderer( diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt index 4f1d75e8..f0a691d1 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt @@ -3,7 +3,6 @@ package com.mapconductor.googlemaps.polygon import androidx.compose.ui.graphics.toArgb import com.google.android.gms.maps.model.PolygonOptions import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer import com.mapconductor.core.polygon.PolygonController @@ -11,14 +10,10 @@ import com.mapconductor.core.polygon.PolygonEntity import com.mapconductor.core.polygon.PolygonManager import com.mapconductor.core.polygon.PolygonManagerImpl import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.core.polyline.PolylineController import com.mapconductor.googlemaps.GoogleMapActualPolygon import com.mapconductor.googlemaps.GoogleMapViewHolder -import com.mapconductor.googlemaps.polyline.GoogleMapPolylineOverlayRenderer import com.mapconductor.googlemaps.toLatLng -import kotlin.coroutines.coroutineContext import android.util.Log -import kotlinx.coroutines.AbstractCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -57,7 +52,7 @@ class GoogleMapPolygonOverlayRenderer( override suspend fun updatePolygonProperties( polygon: GoogleMapActualPolygon, current: PolygonEntity, - prev: PolygonEntity + prev: PolygonEntity, ): GoogleMapActualPolygon? = withContext(coroutine.coroutineContext) { val polygon = current.polygon diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt index 08c341a2..48589818 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt @@ -3,13 +3,8 @@ package com.mapconductor.here.polygon import androidx.compose.ui.graphics.toArgb import com.here.sdk.core.Color import com.here.sdk.core.GeoPolygon -import com.here.sdk.mapview.LineCap -import com.here.sdk.mapview.MapMeasureDependentRenderSize import com.here.sdk.mapview.MapPolygon -import com.here.sdk.mapview.MapPolyline -import com.here.sdk.mapview.RenderSize import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer import com.mapconductor.core.polygon.PolygonController @@ -17,7 +12,6 @@ import com.mapconductor.core.polygon.PolygonEntity import com.mapconductor.core.polygon.PolygonManager import com.mapconductor.core.polygon.PolygonManagerImpl import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.core.polyline.PolylineState import com.mapconductor.here.HereActualPolygon import com.mapconductor.here.HereViewHolder import com.mapconductor.here.toGeoCoordinates @@ -44,12 +38,13 @@ class HerePolygonOverlayRenderer( override suspend fun createPolygon(state: PolygonState): HereActualPolygon? { val geoPolygon = createGeoPolygon(state) val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) - val mapPolygon = MapPolygon( - geoPolygon, - Color.valueOf(state.fillColor.toArgb()), - Color.valueOf(state.strokeColor.toArgb()), - lineWidth, - ) + val mapPolygon = + MapPolygon( + geoPolygon, + Color.valueOf(state.fillColor.toArgb()), + Color.valueOf(state.strokeColor.toArgb()), + lineWidth, + ) coroutine.launch { holder.map.addMapPolygon(mapPolygon) } @@ -59,7 +54,7 @@ class HerePolygonOverlayRenderer( override suspend fun updatePolygonProperties( polygon: HereActualPolygon, current: PolygonEntity, - prev: PolygonEntity + prev: PolygonEntity, ): HereActualPolygon? = withContext(coroutine.coroutineContext) { val finger = current.fingerPrint @@ -78,7 +73,8 @@ class HerePolygonOverlayRenderer( if (finger.strokeWidth != prevFinger.strokeWidth) { val lineWidth = ResourceProvider.dpToPx( - current.state.strokeWidth.value.toDouble(), + current.state.strokeWidth.value + .toDouble(), ) current.polygon.outlineWidth = lineWidth } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index e0b8b2a2..b7621100 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -136,10 +136,11 @@ internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonCon holder = holder, ) - val conductor = MapboxPolygonConductor( - polygonOverlay = polygonOverlayRenderer, - polylineOverlay = polylineOverlayRenderer, - ) + val conductor = + MapboxPolygonConductor( + polygonOverlay = polygonOverlayRenderer, + polylineOverlay = polylineOverlayRenderer, + ) return conductor } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt index 92e65b5c..03c43389 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt @@ -29,7 +29,7 @@ internal fun createMapboxLines( return splitByMeridian(geoPoints, geodesic).mapIndexed { index, linePoints -> val points = linePoints.map { GeoPoint.from(it).toPoint() } - val id = "polyline-${id}-$index" + val id = "polyline-$id-$index" return@mapIndexed Feature.fromGeometry( LineString.fromLngLats(points), diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt index c33b71ff..7c243ad2 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt @@ -5,41 +5,41 @@ import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.polygon.PolygonEntity import com.mapconductor.core.polygon.PolygonEntityImpl import com.mapconductor.core.polygon.PolygonEvent -import com.mapconductor.core.polygon.PolygonManager import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.core.polyline.PolylineEntity import com.mapconductor.core.polyline.PolylineEntityImpl import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.mapbox.MapboxOutlineAndFill import com.mapconductor.mapbox.polyline.MapboxPolylineOverlayRenderer class MapboxPolygonConductor( val polygonOverlay: MapboxPolygonOverlayRenderer, val polylineOverlay: MapboxPolylineOverlayRenderer, ) : OverlayController< - PolygonState, - PolygonState, - PolygonEntity, - PolygonEvent, -> { + PolygonState, + PolygonState, + PolygonEntity, + PolygonEvent, + > { override val zIndex: Int = 2 + override suspend fun add(data: List) { data.forEach { polygonState -> polygonOverlay.createPolygon(polygonState)?.let { polygon -> - val polygonEntity = PolygonEntityImpl( - polygon = polygon, - state = polygonState, - ) + val polygonEntity = + PolygonEntityImpl( + polygon = polygon, + state = polygonState, + ) polygonOverlay.polygonManager.registerEntity(polygonEntity) } val polylineState = polygonState.toPolylineState() polylineOverlay.createPolyline(polylineState)?.let { polyline -> - val polylineEntity = PolylineEntityImpl( - polyline = polyline, - state = polylineState, - ) + val polylineEntity = + PolylineEntityImpl( + polyline = polyline, + state = polylineState, + ) polylineOverlay.polylineManager.registerEntity(polylineEntity) } } @@ -48,21 +48,22 @@ class MapboxPolygonConductor( } override suspend fun update(state: PolygonState) { - polygonOverlay.createPolygon(state)?.let { polygon -> - val polygonEntity = PolygonEntityImpl( - polygon = polygon, - state = state, - ) + val polygonEntity = + PolygonEntityImpl( + polygon = polygon, + state = state, + ) polygonOverlay.polygonManager.registerEntity(polygonEntity) } val polylineState = state.toPolylineState() polylineOverlay.createPolyline(polylineState)?.let { polyline -> - val polylineEntity = PolylineEntityImpl( - polyline = polyline, - state = polylineState, - ) + val polylineEntity = + PolylineEntityImpl( + polyline = polyline, + state = polylineState, + ) polylineOverlay.polylineManager.registerEntity(polylineEntity) } polygonOverlay.onPostProcess() @@ -70,9 +71,8 @@ class MapboxPolygonConductor( } override var clickListener: ((PolygonEvent) -> Unit)? = null - override fun find(position: IGeoPoint): PolygonEntity? { - return null - } + + override fun find(position: IGeoPoint): PolygonEntity? = null override suspend fun clear() { } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt index 88cfa0c7..15f076d7 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt @@ -5,7 +5,6 @@ import com.mapbox.geojson.Feature import com.mapbox.geojson.Polygon import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer -import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.polygon.PolygonEntity import com.mapconductor.core.polygon.PolygonManager import com.mapconductor.core.polygon.PolygonState @@ -23,7 +22,6 @@ class MapboxPolygonOverlayRenderer( override val holder: MapboxMapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), ) : AbstractPolygonOverlayRenderer() { - override suspend fun onRemove(data: List>) { // val featureIds = data.map { entity -> // entity.polygon.getStringProperty("id") @@ -53,19 +51,21 @@ class MapboxPolygonOverlayRenderer( } else { points } - return listOf(Feature.fromGeometry( - Polygon.fromLngLats(listOf(closedPoints)), - JsonObject().apply { - addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) - }, - "polygon-${state.id}", - )) + return listOf( + Feature.fromGeometry( + Polygon.fromLngLats(listOf(closedPoints)), + JsonObject().apply { + addProperty(MapboxPolygonLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) + }, + "polygon-${state.id}", + ), + ) } override suspend fun updatePolygonProperties( polygon: MapboxActualPolygon, current: PolygonEntity, - prev: PolygonEntity + prev: PolygonEntity, ): MapboxActualPolygon? { // val state = current.state // val points = state.points.map { GeoPoint.from(it).toPoint() } @@ -94,6 +94,4 @@ class MapboxPolygonOverlayRenderer( // For now, we'll implement a simple workaround return polygonManager.allEntities() } - - } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt index b4824847..821ebea2 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt @@ -1,26 +1,14 @@ package com.mapconductor.mapbox.polyline -import com.google.gson.JsonObject -import com.mapbox.geojson.Feature -import com.mapbox.geojson.LineString import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.createInterpolatePoints -import com.mapconductor.core.createLinearInterpolatePoints -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.features.normalize import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer import com.mapconductor.core.polyline.PolylineController import com.mapconductor.core.polyline.PolylineEntity import com.mapconductor.core.polyline.PolylineManager import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.core.splitByMeridian import com.mapconductor.mapbox.MapboxActualPolyline import com.mapconductor.mapbox.MapboxMapViewHolder import com.mapconductor.mapbox.createMapboxLines -import com.mapconductor.mapbox.toMapboxColorString -import com.mapconductor.mapbox.toPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch From f8ed87392a1af771f806ef4348cbae987edd1a84 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sun, 31 Aug 2025 17:06:02 +0900 Subject: [PATCH 36/44] Fix: Polyline.strokeWidth is incorrect on Mapbox SDK --- .../src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt index 03c43389..044d7fd6 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxPolyUtils.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.unit.Dp import com.google.gson.JsonObject import com.mapbox.geojson.Feature import com.mapbox.geojson.LineString -import com.mapconductor.core.ResourceProvider import com.mapconductor.core.createInterpolatePoints import com.mapconductor.core.createLinearInterpolatePoints import com.mapconductor.core.features.GeoPoint @@ -35,7 +34,7 @@ internal fun createMapboxLines( LineString.fromLngLats(points), JsonObject().apply { addProperty(MapboxPolylineLayer.Prop.STROKE_COLOR, strokeColor.toMapboxColorString()) - addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, ResourceProvider.dpToPx(strokeWidth.value)) + addProperty(MapboxPolylineLayer.Prop.STROKE_WIDTH, strokeWidth.value) addProperty("id", id) }, id, From 40f1445b136da5dfefff92707ba693aab9604194 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Tue, 2 Sep 2025 02:37:20 +0900 Subject: [PATCH 37/44] create marker icon demo --- .../pages/map/flyto/FlyToMapComponent.kt | 8 +- .../pages/marker/MarkerBasicMapComponent.kt | 278 +++- .../example/pages/marker/MarkerBasicPage.kt | 2 +- .../pages/polygon/PolygonMapPageViewModel.kt | 7 +- .../pages/polyline/PolylinePageViewModel.kt | 5 +- .../example/pages/stores/DemoData.kt | 1240 +++++------------ .../example/pages/stores/StoreInfoView.kt | 10 +- .../example/pages/stores/StoreMapComponent.kt | 7 +- .../src/main/res/drawable/coffee_bean.png | Bin 52511 -> 19912 bytes example-app/src/main/res/drawable/human.jpg | Bin 0 -> 232132 bytes .../main/res/drawable/marker_with_label.png | Bin 0 -> 4444 bytes .../mapconductor/core/marker/DefaultIcon.kt | 59 +- .../com/mapconductor/core/marker/Marker.kt | 5 +- .../mapconductor/core/marker/MarkerCompose.kt | 3 +- .../mapconductor/core/marker/MarkerIcon.kt | 39 +- 15 files changed, 685 insertions(+), 978 deletions(-) create mode 100644 example-app/src/main/res/drawable/human.jpg create mode 100644 example-app/src/main/res/drawable/marker_with_label.png diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt index e8fe290b..f6c4758f 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt @@ -26,16 +26,12 @@ fun FlyToMapComponent( ) { // Render polylines connecting all markers polylines.forEach { polyline -> - key(polyline.id) { - Polyline(polyline) - } + Polyline(polyline) } // Render markers for fly to destinations markers.forEach { marker -> - key(marker.id) { - Marker(marker) - } + Marker(marker) } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt index 083b8065..9d3cb514 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt @@ -1,62 +1,274 @@ package com.mapconductor.example.pages.marker +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.drawable.toDrawable +import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.info.InfoBubble import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.DrawableDefaultIcon +import com.mapconductor.core.marker.ImageIcon import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.MarkerState import com.mapconductor.example.MapViewContainer +import com.mapconductor.example.R +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.BitmapDrawable @Composable fun MarkerBasicMapComponent( mapViewState: MapViewState<*>, modifier: Modifier = Modifier, ) { + val context = LocalContext.current + var selected by remember { mutableStateOf(null) } + val darkTheme: Boolean = isSystemInDarkTheme() + val bubbleColor = if (darkTheme) Color.Black else Color.White + MapViewContainer( state = mapViewState, modifier = modifier, + onMarkerClick = { selected = it }, + onMapClick = { selected = null }, ) { -// Marker( -// position = GeoPoint.fromLatLong(0.0, 0.004), -// ) -// Marker( -// position = GeoPoint.fromLatLong(0.0, 0.008), -// icon = DefaultIcon( -// fillColor = Color.Yellow, -// strokeColor = Color.Black, -// strokeWidth = 2.dp, -// ), -// ) -// Marker( -// position = GeoPoint.fromLatLong(0.0, 0.012), -// icon = DefaultIcon( -// label = "AB", -// labelTextColor = Color.White, -// labelStrokeColor = Color.Black, -// ), -// ) Marker( - position = GeoPoint.fromLatLong(-0.004, 0.004), + position = GeoPoint.fromLatLong(0.018, 0.004), icon = DefaultIcon( scale = 0.7f, + label = "0.7", + debug = true, + ), + extra = """ + DefaultIcon( + scale = 0.7f, + label = "0.7", + debug = true, + ), + """.trimIndent() + ) + + Marker( + position = GeoPoint.fromLatLong(0.018, 0.006), + icon = DefaultIcon( + scale = 1.0f, + label = "1.0", + debug = true, + ), + extra = """ + DefaultIcon( + scale = 1.0f, + label = "1.0", + debug = true, + ), + """.trimIndent() + ) + Marker( + position = GeoPoint.fromLatLong(0.018, 0.009), + icon = DefaultIcon( + scale = 1.4f, + label = "1.4", + debug = true, + ), + extra = """ + DefaultIcon( + scale = 1.4f, + label = "1.4", + debug = true, + ), + """.trimIndent() + ) + + Marker( + position = GeoPoint.fromLatLong(0.018, 0.013), + icon = DefaultIcon( + scale = 2.1f, + label = "2.1", + debug = true, + ), + extra = """ + DefaultIcon( + scale = 2.1f, + label = "2.1", + debug = true, + ), + """.trimIndent() + ) + + + + Marker( + position = GeoPoint.fromLatLong(0.014, 0.004), + extra = "DefaultIcon()", + ) + Marker( + position = GeoPoint.fromLatLong(0.014, 0.008), + icon = DefaultIcon( + fillColor = Color.Yellow, + strokeColor = Color.Black, + strokeWidth = 2.dp, + ), + extra = """ + DefaultIcon( + fillColor = Color.Yellow, + strokeColor = Color.Black, + strokeWidth = 2.dp, + ) + """.trimIndent() + ) + Marker( + position = GeoPoint.fromLatLong(0.014, 0.012), + icon = DefaultIcon( + fillColor = Color( + red = 0x2E, + green = 0xF5, + blue = 0x27, + ), + strokeColor = Color( + red = 0xFC, + green = 0x22, + blue = 0x5C, ), + label = "AB", + labelTextColor = Color.White, + labelStrokeColor = Color.Black, + ), + extra = """ + DefaultIcon( + fillColor = Color( + red = 0x2E, + green = 0xF5, + blue = 0x27, + ), + strokeColor = Color( + red = 0xFC, + green = 0x22, + blue = 0x5C, + ), + label = "AB", + labelTextColor = Color.White, + labelStrokeColor = Color.Black, + ) + """.trimIndent() ) -// Marker( -// position = GeoPoint.fromLatLong(-0.004, 0.008), -// icon = DefaultIcon( -// scale = 1.4f, -// ), -// ) -// -// Marker( -// position = GeoPoint.fromLatLong(-0.004, 0.012), -// icon = DefaultIcon( -// scale = 2.1f, -// ), -// ) + + AppCompatResources.getDrawable(context, R.drawable.human)?.let { icon -> + Marker( + position = GeoPoint.fromLatLong(0.01, 0.004), + icon = DrawableDefaultIcon( + backgroundDrawable = icon, + ), + extra = """ + DrawableDefaultIcon( + backgroundDrawable = icon, + ) + """.trimIndent() + ) + } + + AppCompatResources.getDrawable(context, R.drawable.ic_launcher_foreground)?.let { icon -> + Marker( + position = GeoPoint.fromLatLong(0.01, 0.006), + icon = DrawableDefaultIcon( + backgroundDrawable = icon, + strokeColor = Color.Black, + scale = 1.5f, + ), + extra =""" + DrawableDefaultIcon( + backgroundDrawable = icon, + strokeColor = Color.Black, + scale = 1.5f, + ) + """.trimIndent(), + ) + } + + + AppCompatResources.getDrawable(context, R.drawable.wmo_00_clear)?.let { icon -> + Marker( + position = GeoPoint.fromLatLong(0.01, 0.009), + icon = ImageIcon( + drawable = icon, + debug = true, + anchor = Offset(0.5f, 1.0f), + ), + extra = """ + ImageIcon( + drawable = icon, + debug = true, + anchor = Offset(0.5f, 1.0f), + ) + """.trimIndent() + ) + } + + createMarkerWithLabelIcon(context, "Label")?.let { + Marker( + position = GeoPoint.fromLatLong(0.01, 0.012), + icon = ImageIcon( + drawable = it, + anchor = Offset(0.5f, 1.0f), + ), + extra = """ + ImageIcon( + drawable = createMarkerWithLabelIcon(label), + anchor = Offset(0.5f, 1.0f), + ) + """.trimIndent() + ) + } + (selected?.extra as? String)?.let { snippet -> + InfoBubble( + marker = selected!!, + bubbleColor = bubbleColor, + ) { + + Text( + text = snippet, + textAlign = TextAlign.Left, + ) + } + } } } + +fun createMarkerWithLabelIcon(context: Context, label: String): BitmapDrawable? { + val drawable = AppCompatResources.getDrawable(context, R.drawable.marker_with_label) ?: return null + + val iconBitmap = drawable.toBitmap() + Canvas(iconBitmap).apply { + val textPaint = + Paint().apply { + color = Color.White.toArgb() + this.textSize = ResourceProvider.dpToPx(40f).toFloat() + textAlign = Paint.Align.CENTER + isAntiAlias = true + isSubpixelText = true + } + + drawText(label, ResourceProvider.dpToPx(77f).toFloat(), ResourceProvider.dpToPx(120f).toFloat(), textPaint) + } + + return iconBitmap.toDrawable(context.resources) +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt index 4733b674..174a6abd 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicPage.kt @@ -18,7 +18,7 @@ fun MarkerBasicPage(onToggleSidebar: () -> Unit = {}) { val initCameraPosition = remember { MapCameraPosition( - position = GeoPoint(0.0, 0.0), + position = GeoPoint(0.014, 0.008), zoom = 15.0, ) } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt index f672b2c9..ed33a526 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt @@ -76,10 +76,7 @@ class PolygonMapPageViewModelImpl : ), id = "vertex_$index", draggable = true, - extra = - Bundle().apply { - putInt("index", index) - }, + extra = index, ) } @@ -99,7 +96,7 @@ class PolygonMapPageViewModelImpl : } override fun onMarkerDrag(dragged: MarkerState) { - (dragged.extra as? Bundle)?.getInt("index")?.let { index -> + (dragged.extra as? Int)?.let { index -> if (index >= 0 && index < polygonVertices.size) { polygonVertices[index] = GeoPoint.from(dragged.position) } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt index aece0396..d761aa55 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt @@ -77,10 +77,7 @@ class PolylinePageViewModelImpl : label = label, ), draggable = true, - extra = - Bundle().apply { - putInt("index", index) - }, + extra = index, ) } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt index 95067474..54bf856e 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt @@ -6,30 +6,37 @@ import androidx.compose.ui.unit.sp import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerState -import android.os.Bundle +import java.io.Serializable /** * This example uses publicly available business addresses (e.g., Starbucks) and geocodes them * using the U.S. Census Bureau Geocoding API. * No personally identifiable information (PII) is used or inferred. */ +data class StoreInfo( + val name: String, + val address: String, + val instore: Boolean, + val driveThrough: Boolean, + val onlyReserved: Boolean, + val store: String +) : Serializable + val StarbucksHI_list = listOf( MarkerState( - position = - GeoPoint( - latitude = 21.647441446388, - longitude = -158.062544988096, - ), - extra = - Bundle().apply { - putString("name", "Pupukea (North Shore)") - putString("address", "59-720 Kamehameha Highway, Haleiwa, HI 96712") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, + position = GeoPoint( + latitude = 21.647441446388, + longitude = -158.062544988096, + ), + extra = StoreInfo( + name = "Pupukea (North Shore)", + address = "59-720 Kamehameha Highway, Haleiwa, HI 96712", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_bean" + ), icon = DefaultIcon( label = "店", @@ -41,874 +48,339 @@ val StarbucksHI_list = ), ), MarkerState( - position = - GeoPoint( - latitude = 21.33310051533, - longitude = -157.922371535818, - ), - extra = - Bundle().apply { - putString("name", "Honolulu Airport (HNL) – Main") - putString("address", "300 Rogers Blvd, Honolulu, HI 96820") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.378981027427, - longitude = -157.930536387573, - ), - extra = - Bundle().apply { - putString("name", "Aiea Shopping Center") - putString("address", "99-115 Aiea Heights Drive #125, Aiea, HI 96701") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.38441101519, - longitude = -157.944839558127, - ), - extra = - Bundle().apply { - putString("name", "Pearlridge Center") - putString("address", "98-125 Kaonohi Street, Aiea, HI 96701") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, -// icon = -// MarkerIcon.Companion.Triangle( -// outsideColor = 0xFF008000.toInt(), -// strokeWidth = 2f, -// triangleHeight = 24f, -// triangleWidth = 24f, -// ), - ), - MarkerState( - position = - GeoPoint( - latitude = 21.363785189939, - longitude = -157.928412704343, - ), - extra = - Bundle().apply { - putString("name", "Stadium Marketplace") - putString("address", "4561 Salt Lake Boulevard, Aiea, HI 96818") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.386340299119, - longitude = -157.941897795274, - ), - extra = - Bundle().apply { - putString("name", "Pearlridge Mall") - putString("address", "98-1005 Moanalua Road, Aiea, HI 96701") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 19.69971686484, - longitude = -155.067322812851, - ), - extra = - Bundle().apply { - putString("name", "Waiakea Center (Hilo)") - putString("address", "315-325 Makaala Street, Hilo, HI 96720") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 19.695097953188, - longitude = -155.06690203818, - ), - extra = - Bundle().apply { - putString("name", "Prince Kuhio Plaza (Hilo)") - putString("address", "111 East Puainako Street, Hilo, HI 96720") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 19.719877684807, - longitude = -155.082770375139, - ), - extra = - Bundle().apply { - putString("name", "Downtown Hilo (Kilauea Ave)") - putString("address", "438 Kilauea Ave, Hilo, HI 96720") - putBoolean("instore", true) - putBoolean("drive_through", true) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.33593, - longitude = -157.91581, - ), - extra = - Bundle().apply { - putString("name", "Airport Trade Center") - putString("address", "Airport Trade Center, 550 Paiea St, Honolulu, HI 96819") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.307358712377, - longitude = -157.865194116049, - ), - extra = - Bundle().apply { - putString("name", "Aloha Tower") - putString("address", "1 Aloha Tower Drive, Honolulu, HI 96813") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.30846253, - longitude = -157.8614898, - ), - extra = - Bundle().apply { - putString("name", "Bishop (Downtown)") - putString("address", "1000 Bishop Street #104, Honolulu, HI 96813") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.307604966533, - longitude = -157.860743724617, - ), - extra = - Bundle().apply { - putString("name", "Pickup – King & Alakea") - putString("address", "220 South King Street, Honolulu, HI 96813") - putBoolean("instore", false) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "honolulu_coffee") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.285300825278, - longitude = -157.83841421971, - ), - extra = - Bundle().apply { - putString("name", "Discovery Bay Center") - putString("address", "1778 Ala Moana Boulevard, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.334058693598, - longitude = -158.023228524098, - ), - extra = - Bundle().apply { - putString("name", "Ewa Beach – Laulani Village") - putString("address", "91-1401 Fort Weaver Road, Ewa Beach, HI 96706") - putBoolean("instore", true) - putBoolean("drive_through", true) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.280578442859, - longitude = -157.828071689214, - ), - extra = - Bundle().apply { - putString("name", "DFS (Duty Free) Waikiki") - putString("address", "330 Royal Hawaiian Avenue, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.308557010703, - longitude = -157.862582769768, - ), - extra = - Bundle().apply { - putString("name", "Financial Plaza (Downtown)") - putString("address", "130 Merchant Street #111, Honolulu, HI 96813") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "honolulu_coffee") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.282048, - longitude = -157.713041, - ), - extra = - Bundle().apply { - putString("name", "Hawaii Kai Town Center") - putString("address", "6700 Kalanianaole Highway, Honolulu, HI 96825") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.291792650634, - longitude = -157.849735879475, - ), - extra = - Bundle().apply { - putString("name", "Hokua (Ala Moana)") - putString("address", "1288 Ala Moana Blvd, Honolulu, HI 96814") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.335246981366, - longitude = -157.868748238078, - ), - extra = - Bundle().apply { - putString("name", "Kamehameha Shopping Center") - putString("address", "1620 North School Street, Honolulu, HI 96817") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.27852422, - longitude = -157.7875773, - ), - extra = - Bundle().apply { - putString("name", "Kahala Mall") - putString("address", "4211 Waialae Avenue, Honolulu, HI 96816") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.279056707748, - longitude = -157.813890137018, - ), - extra = - Bundle().apply { - putString("name", "Kapahulu Avenue") - putString("address", "625 Kapahulu Avenue, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.276148191143, - longitude = -157.704922547261, - ), - extra = - Bundle().apply { - putString("name", "Koko Marina Center") - putString("address", "7192 Kalanianaole Highway, Honolulu, HI 96825") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.30985278855, - longitude = -157.810260198584, - ), - extra = - Bundle().apply { - putString("name", "Manoa Valley") - putString("address", "2902 East Manoa Road, Honolulu, HI 96822") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.289750395336, - longitude = -157.843910788044, - ), - extra = - Bundle().apply { - putString("name", "Macy’s Ala Moana Center") - putString("address", "1450 Ala Moana Boulevard, Honolulu, HI 96814") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "honolulu_coffee") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.341260775481, - longitude = -157.929507250967, - ), - extra = - Bundle().apply { - putString("name", "Moanalua Shopping Center") - putString("address", "930 Valkenburgh Street, Honolulu, HI 96818") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.279011840151, - longitude = -157.825557564916, - ), - extra = - Bundle().apply { - putString("name", "Ohana Waikiki West") - putString("address", "2330 Kuhio Avenue, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.2786604527, - longitude = -157.828371626919, - ), - extra = - Bundle().apply { - putString("name", "Waikiki Shopping Plaza") - putString("address", "2270 Kalakaua Avenue #1800, Honolulu, HI 96815") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.293774306726, - longitude = -157.85297798269, - ), - extra = - Bundle().apply { - putString("name", "Ward Entertainment Center") - putString("address", "310 Kamakee Street #6, Honolulu, HI 96814") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.406095, - longitude = -157.800761, - ), - extra = - Bundle().apply { - putString("name", "Windward City Shopping Center") - putString("address", "45-480 Kaneohe Bay Drive, Kaneohe, HI 96744") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.888659282451, - longitude = -156.477197459052, - ), - extra = - Bundle().apply { - putString("name", "Queen Kaahumanu Center") - putString("address", "275 West Kaahumanu Avenue #1200, Kahului, HI 96732") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.881960703032, - longitude = -156.45511618549, - ), - extra = - Bundle().apply { - putString("name", "Maui Marketplace") - putString("address", "270 Dairy Road, Kahului, HI 96732") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.393471214679, - longitude = -157.740438744365, - ), - extra = - Bundle().apply { - putString("name", "Kailua Village") - putString("address", "539 Kailua Road, Kailua, HI 96734") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 19.65018280057, - longitude = -155.987752998108, - ), - extra = - Bundle().apply { - putString("name", "Kona Coast Shopping Center") - putString("address", "74-5588 Palani Road, Kailua-Kona, HI 96740") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.020593379111, - longitude = -155.668585540658, - ), - extra = - Bundle().apply { - putString("name", "Parker Ranch Center") - putString("address", "67-1185 Mamalahoa Highway #D108, Kamuela, HI 96743") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.344027896932, - longitude = -158.11830127628, - ), - extra = - Bundle().apply { - putString("name", "Halekuai Center") - putString("address", "563 Farrington Highway #101, Kapolei, HI 96707") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.328579072139, - longitude = -158.086506230214, - ), - extra = - Bundle().apply { - putString("name", "Kapolei Parkway & Kamokila") - putString("address", "338 Kamokila Boulevard #108, Kapolei, HI 96797") - putBoolean("instore", true) - putBoolean("drive_through", true) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.734513943855, - longitude = -156.452970465534, - ), - extra = - Bundle().apply { - putString("name", "Kukui Mall") - putString("address", "1819 South Kihei Road, Kihei, HI 96738") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.750703062197, - longitude = -156.451408824978, - ), - extra = - Bundle().apply { - putString("name", "Piilani Village Shopping Center") - putString("address", "247 Piikea Avenue #106, Kihei, HI 96753") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 19.94001271876, - longitude = -155.856842731652, - ), - extra = - Bundle().apply { - putString("name", "Mauna Lani (Kohala Coast)") - putString("address", "68-1330 Mauna Lani Drive #H-101B, Kohala Coast, HI 96743") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.886244, - longitude = -156.684697, - ), - extra = - Bundle().apply { - putString("name", "Lahaina Cannery Mall") - putString("address", "1221 Honoapiilani Highway, Lahaina, HI 96761") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.877708110758, - longitude = -156.679031878844, - ), - extra = - Bundle().apply { - putString("name", "Lahaina (Front Street)") - putString("address", "845 Wainee Street, Lahaina, HI 96761") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_bean") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.969553724378, - longitude = -159.388283368972, - ), - extra = - Bundle().apply { - putString("name", "Kukui Grove Center") - putString("address", "3-2600 Kaumualii Highway #A8, Lihue, HI 96766") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.889156401906, - longitude = -156.449318101378, - ), - extra = - Bundle().apply { - putString("name", "Kahului Airport (OGG)") - putString("address", "1 Keolani Place, Kahului, HI 96732") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.458431746129, - longitude = -158.015862355331, - ), - extra = - Bundle().apply { - putString("name", "Mililani Shopping Center") - putString("address", "95-221 Kipapa Drive, Mililani, HI 96789") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 20.838230357792, - longitude = -156.342698446307, - ), - extra = - Bundle().apply { - putString("name", "Pukalani Foodland Center") - putString("address", "55 Pukalani Street, Pukalani, HI 96768") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.378675, - longitude = -157.728499, - ), - extra = - Bundle().apply { - putString("name", "Enchanted Lake Center (Kailua)") - putString("address", "1020 Keolu Drive, Kailua, HI 96734") - putBoolean("instore", true) - putBoolean("drive_through", true) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.389003512015, - longitude = -158.033431400538, - ), - extra = - Bundle().apply { - putString("name", "Kunia Shopping Center") - putString("address", "94-673 Kupuohi Street, Waipahu, HI 96797") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.881002766882, - longitude = -159.457726341723, - ), - extra = - Bundle().apply { - putString("name", "Poipu Shopping Village") - putString("address", "2360 Kiahuna Plantation Drive #E70, Koloa, HI 96756") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.966834230955, - longitude = -159.381526209527, - ), - extra = - Bundle().apply { - putString("name", "Safeway Lihue") - putString("address", "4454 Nuhou Street, Lihue, HI 96766") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.970935289068, - longitude = -159.375643172372, - ), - extra = - Bundle().apply { - putString("name", "Target Lihue (Kauai)") - putString("address", "4303 Nawiliwili Road, Lihue, HI 96766") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 22.061786387888, - longitude = -159.320539848567, - ), - extra = - Bundle().apply { - putString("name", "Kauai Village SC (Kapaa)") - putString("address", "4-831 Kuhio Highway #208, Kapaa, HI 96746") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "starbucks") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.32874860701, - longitude = -158.091318912219, - ), - extra = - Bundle().apply { - putString("name", "Target Kapolei") - putString("address", "4450 Kapolei Parkway, Kapolei, HI 96707") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.340541119127, - longitude = -158.124703887408, - ), - extra = - Bundle().apply { - putString("name", "Ko Olina Station") - putString("address", "92-1047 Olani Street, Kapolei, HI 96707") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, - ), - MarkerState( - position = - GeoPoint( - latitude = 21.34213, - longitude = -157.95157, - ), - extra = - Bundle().apply { - putString("name", "Hickam AFB (Base Access)") - putString("address", "Bldg B-1250, Hickam AFB, Honolulu, HI 96853") - putBoolean("instore", true) - putBoolean("drive_through", false) - putBoolean("only_reserved", false) - putString("store", "coffee_extra") - }, + position = GeoPoint( + latitude = 21.33310051533, + longitude = -157.922371535818, + ), + extra = StoreInfo( + name = "Honolulu Airport (HNL) – Main", + address = "300 Rogers Blvd, Honolulu, HI 96820", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_bean" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.378981027427, + longitude = -157.930536387573, + ), + extra = StoreInfo( + name = "Aiea Shopping Center", + address = "99-115 Aiea Heights Drive #125, Aiea, HI 96701", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.38441101519, + longitude = -157.944839558127, + ), + extra = StoreInfo( + name = "Pearlridge Center", + address = "98-125 Kaonohi Street, Aiea, HI 96701", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.363785189939, + longitude = -157.928412704343, + ), + extra = StoreInfo( + name = "Stadium Marketplace", + address = "4561 Salt Lake Boulevard, Aiea, HI 96818", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.386340299119, + longitude = -157.941897795274, + ), + extra = StoreInfo( + name = "Pearlridge Mall", + address = "98-1005 Moanalua Road, Aiea, HI 96701", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 19.69971686484, + longitude = -155.067322812851, + ), + extra = StoreInfo( + name = "Waiakea Center (Hilo)", + address = "315-325 Makaala Street, Hilo, HI 96720", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 19.695097953188, + longitude = -155.06690203818, + ), + extra = StoreInfo( + name = "Prince Kuhio Plaza (Hilo)", + address = "111 East Puainako Street, Hilo, HI 96720", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 19.719877684807, + longitude = -155.082770375139, + ), + extra = StoreInfo( + name = "Downtown Hilo (Kilauea Ave)", + address = "438 Kilauea Ave, Hilo, HI 96720", + instore = true, + driveThrough = true, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.33593, + longitude = -157.91581, + ), + extra = StoreInfo( + name = "Airport Trade Center", + address = "Airport Trade Center, 550 Paiea St, Honolulu, HI 96819", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.307358712377, + longitude = -157.865194116049, + ), + extra = StoreInfo( + name = "Aloha Tower", + address = "1 Aloha Tower Drive, Honolulu, HI 96813", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.30846253, + longitude = -157.8614898, + ), + extra = StoreInfo( + name = "Bishop (Downtown)", + address = "1000 Bishop Street #104, Honolulu, HI 96813", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.307604966533, + longitude = -157.860743724617, + ), + extra = StoreInfo( + name = "Pickup – King & Alakea", + address = "220 South King Street, Honolulu, HI 96813", + instore = false, + driveThrough = false, + onlyReserved = false, + store = "honolulu_coffee" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.285300825278, + longitude = -157.83841421971, + ), + extra = StoreInfo( + name = "Discovery Bay Center", + address = "1778 Ala Moana Boulevard, Honolulu, HI 96815", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.334058693598, + longitude = -158.023228524098, + ), + extra = StoreInfo( + name = "Ewa Beach – Laulani Village", + address = "91-1401 Fort Weaver Road, Ewa Beach, HI 96706", + instore = true, + driveThrough = true, + onlyReserved = false, + store = "coffee_bean" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.280578442859, + longitude = -157.828071689214, + ), + extra = StoreInfo( + name = "DFS (Duty Free) Waikiki", + address = "330 Royal Hawaiian Avenue, Honolulu, HI 96815", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.308557010703, + longitude = -157.862582769768, + ), + extra = StoreInfo( + name = "Financial Plaza (Downtown)", + address = "130 Merchant Street #111, Honolulu, HI 96813", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "honolulu_coffee" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.282048, + longitude = -157.713041, + ), + extra = StoreInfo( + name = "Hawaii Kai Town Center", + address = "6700 Kalanianaole Highway, Honolulu, HI 96825", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.291792650634, + longitude = -157.849735879475, + ), + extra = StoreInfo( + name = "Hokua (Ala Moana)", + address = "1288 Ala Moana Blvd, Honolulu, HI 96814", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_bean" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.335246981366, + longitude = -157.868748238078, + ), + extra = StoreInfo( + name = "Kamehameha Shopping Center", + address = "1620 North School Street, Honolulu, HI 96817", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.27852422, + longitude = -157.7875773, + ), + extra = StoreInfo( + name = "Kahala Mall", + address = "4211 Waialae Avenue, Honolulu, HI 96816", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.279056707748, + longitude = -157.813890137018, + ), + extra = StoreInfo( + name = "Kapahulu Avenue", + address = "625 Kapahulu Avenue, Honolulu, HI 96815", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.276148191143, + longitude = -157.704922547261, + ), + extra = StoreInfo( + name = "Koko Marina Center", + address = "7192 Kalanianaole Highway, Honolulu, HI 96825", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.30985278855, + longitude = -157.810260198584, + ), + extra = StoreInfo( + name = "Manoa Valley", + address = "2902 East Manoa Road, Honolulu, HI 96822", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks" + ), + ), + MarkerState( + position = GeoPoint( + latitude = 21.289750395336, + longitude = -157.843910788044, + ), + extra = StoreInfo( + name = "Macy’s Ala Moana Center", + address = "1450 Ala Moana Boulevard, Honolulu, HI 96814", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "honolulu_coffee" + ), ), ) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt index 7bb05dd5..3bb2ce40 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt @@ -27,7 +27,7 @@ import android.os.Bundle @Composable fun StoreInfoView( - info: Bundle, + info: StoreInfo, onClick: () -> Unit = {}, ) { val darkTheme: Boolean = isSystemInDarkTheme() @@ -36,10 +36,10 @@ fun StoreInfoView( Column( modifier = Modifier.wrapContentSize(), ) { - val name = info.getString("name", "Starbucks") - val address = info.getString("address", "address") - val instore = info.getBoolean("instore", false) - val driveThrough = info.getBoolean("drive_through", false) + val name = info.name + val address = info.address + val instore = info.instore + val driveThrough = info.driveThrough Text(name, fontWeight = FontWeight.Bold, fontSize = 15.sp) Text(address, fontSize = 13.sp) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapComponent.kt index 63ea54c5..5d47f108 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreMapComponent.kt @@ -19,7 +19,6 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.example.MapViewContainer import com.mapconductor.example.R -import android.os.Bundle @Composable fun StoreMapComponent( @@ -61,8 +60,8 @@ fun StoreMapComponent( val markerList = remember { markers.map { state -> - (state.extra as Bundle).let { info -> - val storeIcon = info.getString("store") ?: "coffee_extra" + (state.extra as StoreInfo).let { info -> + val storeIcon = info.store state.copy( icon = icons[storeIcon], ) @@ -89,7 +88,7 @@ fun StoreMapComponent( marker = it, ) { StoreInfoView( - info = it.extra as Bundle, + info = it.extra as StoreInfo, onClick = { onDirectionButtonClick(it) }, diff --git a/example-app/src/main/res/drawable/coffee_bean.png b/example-app/src/main/res/drawable/coffee_bean.png index 6789014c680baccc8ddca3d8130b75a777cbdfb6..459aac42ebdc86303b6745ce101f9c5c737b8878 100644 GIT binary patch literal 19912 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 Cl9O5h literal 52511 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 diff --git a/example-app/src/main/res/drawable/human.jpg b/example-app/src/main/res/drawable/human.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c2dd484c294c263e83b41693090434187d3ddbaf GIT binary patch literal 232132 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 z lMi8yJ@mxWBxvM&{Al#ufk5O{NvZ9Hz5z>?$3` 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�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 AApigX literal 0 HcmV?d00001 diff --git a/example-app/src/main/res/drawable/marker_with_label.png b/example-app/src/main/res/drawable/marker_with_label.png new file mode 100644 index 0000000000000000000000000000000000000000..4f8999790b2bd7600507ca761c6e24873bf221c2 GIT binary patch literal 4444 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 { - drawable.bitmap.scale(width, height) - } - else -> { - val bitmap = createBitmap(width, height) - val canvas = Canvas(bitmap) - drawable.setBounds(0, 0, canvas.width, canvas.height) - drawable.draw(canvas) - return bitmap + return if (drawable is BitmapDrawable && !debug) { + drawable.bitmap.scale(width, height) + } else { + val bitmap = createBitmap(width, height) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + + if (debug) { + drawDebugFrame(canvas) } + return bitmap } } } From 312646790dc691fc3faaf10dd9bbecb847ebbf92 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Tue, 2 Sep 2025 21:21:55 +0900 Subject: [PATCH 38/44] fix: The polyline in the polyline example does not update when the example markers are dragged. --- .../example/pages/polyline/PolylineMapComponent.kt | 2 ++ .../example/pages/polyline/PolylinePageViewModel.kt | 2 +- .../main/java/com/mapconductor/core/polygon/Polygon.kt | 1 - .../java/com/mapconductor/core/polyline/Polyline.kt | 10 ++++++++-- .../mapconductor/core/polyline/PolylineController.kt | 4 +++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt index a33adb92..d70b0d78 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapComponent.kt @@ -22,7 +22,9 @@ fun PolylineMapComponent( MapViewContainer( modifier = modifier, state = it, + onMarkerDragStart = onMarkerDrag, onMarkerDrag = onMarkerDrag, + onMarkerDragEnd = onMarkerDrag, ) { // Polyline Polyline(polylineState) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt index d761aa55..c2abae84 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt @@ -99,7 +99,7 @@ class PolylinePageViewModelImpl : } override fun onMarkerDrag(dragged: MarkerState) { - (dragged.extra as? Bundle)?.getInt("index")?.let { index -> + (dragged.extra as? Int)?.let { index -> if (index >= 0 && index < polylinePoints.size) { polylinePoints[index] = GeoPoint.from(dragged.position) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt index 2ba99219..b18c8c67 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/Polygon.kt @@ -40,7 +40,6 @@ class PolygonState( var fillColor by mutableStateOf(fillColor) var geodesic by mutableStateOf(geodesic) var points by StateFlowDelegate>(points) - var extra by mutableStateOf(extra) private fun polygonId(hashCodes: List): Int = diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt index d241594f..d77ce269 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.mapconductor.core.StateFlowDelegate import com.mapconductor.core.features.IGeoPoint import android.os.Parcelable import kotlinx.coroutines.flow.Flow @@ -35,7 +36,7 @@ class PolylineState( var strokeColor by mutableStateOf(strokeColor) var strokeWidth by mutableStateOf(strokeWidth) var geodesic by mutableStateOf(geodesic) - var points by mutableStateOf>(points) + var points by StateFlowDelegate>(points) var extra by mutableStateOf(extra) private fun polylineId(hashCodes: List): Int = @@ -104,4 +105,9 @@ data class PolylineFingerPrint( val extra: Int, ) -typealias OnPolylineEventHandler = (PolylineState) -> Unit +data class PolylineEvent( + val state: PolylineEvent, + val clicked: IGeoPoint?, +) + +typealias OnPolylineEventHandler = (PolylineEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt index 38c4de02..a57c2e73 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt @@ -13,7 +13,7 @@ abstract class PolylineController( ActualPolyline, PolylineState, PolylineEntity, - PolylineState, + PolylineEvent, > { override val zIndex: Int = 5 val semaphore = Semaphore(1) @@ -128,6 +128,7 @@ abstract class PolylineController( ) polylineManager.registerEntity(entity) } + renderer.onPostProcess() } } @@ -135,6 +136,7 @@ abstract class PolylineController( semaphore.withPermit { val entities: List> = polylineManager.allEntities() renderer.onRemove(entities) + renderer.onPostProcess() polylineManager.clear() } } From 9e38b1a868c0cb5e4f0f43f40a13f84cd536a054 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Tue, 2 Sep 2025 22:44:22 +0900 Subject: [PATCH 39/44] wip:fixing the flyTo demo --- .../com/mapconductor/example/MainActivity.kt | 2 +- .../pages/map/flyto/FlyToPageViewModel.kt | 163 +++++++++--------- .../example/pages/polyline/PolylineMapPage.kt | 1 + .../mapconductor/core/polyline/Polyline.kt | 2 +- 4 files changed, 85 insertions(+), 83 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt index 6eb8b3a0..d1271a7e 100644 --- a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt +++ b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt @@ -12,7 +12,7 @@ class MainActivity : ComponentActivity() { setContent { DemoAppScreen( - initPage = "marker-basic", + initPage = "polyline", ) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToPageViewModel.kt index 991b4eba..e052dad2 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToPageViewModel.kt @@ -1,6 +1,7 @@ package com.mapconductor.example.pages.map.flyto import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color @@ -112,91 +113,91 @@ class FlyToPageViewModelImpl( ), ) - override val polylines - get() = - listOf( - // Honolulu to NewYork - PolylineState( - id = "honolulu_to_newyork", - points = listOf(honoluluLocation, newYorkLocation), - strokeColor = Color.Green.copy(alpha = 0.7f), - strokeWidth = 3.dp, - geodesic = geodesic, + override val polylines = mutableStateListOf( + // Honolulu to NewYork + PolylineState( + id = "honolulu_to_newyork", + points = listOf(honoluluLocation, newYorkLocation), + strokeColor = Color.Green.copy(alpha = 0.7f), + strokeWidth = 3.dp, + geodesic = geodesic, + ), + // Honolulu to Sydney + PolylineState( + id = "honolulu_to_sydney", + points = listOf(honoluluLocation, sydneyLocation), + strokeColor = + Color( // lime + red = 0.7f, + green = 0f, + blue = 0f, + alpha = 0.7f, ), - // Honolulu to Sydney - PolylineState( - id = "honolulu_to_sydney", - points = listOf(honoluluLocation, sydneyLocation), - strokeColor = - Color( // lime - red = 0.7f, - green = 0f, - blue = 0f, - alpha = 0.7f, - ), - strokeWidth = 3.dp, - geodesic = geodesic, + strokeWidth = 3.dp, + geodesic = geodesic, + ), + // Tokyo to London + PolylineState( + id = "tokyo_to_london", + points = listOf(tokyoLocation, londonLocation), + strokeColor = + Color( // Fuchsia + red = 1.0f, + green = 0f, + blue = 1.0f, + alpha = 0.7f, ), - // Tokyo to London - PolylineState( - id = "tokyo_to_london", - points = listOf(tokyoLocation, londonLocation), - strokeColor = - Color( // Fuchsia - red = 1.0f, - green = 0f, - blue = 1.0f, - alpha = 0.7f, - ), - strokeWidth = 3.dp, - geodesic = geodesic, + strokeWidth = 3.dp, + geodesic = geodesic, + ), + // Tokyo to NewYork + PolylineState( + id = "tokyo_to_newyork", + points = listOf(tokyoLocation, newYorkLocation), + strokeColor = Color.Blue.copy(alpha = 0.7f), + strokeWidth = 3.dp, + geodesic = geodesic, + ), + // Tokyo to Honolulu + PolylineState( + id = "tokyo_to_honolulu", + points = listOf(tokyoLocation, honoluluLocation), + strokeColor = + Color( // Neon orange + red = 1.0f, + green = 0.35f, + blue = 0.12f, + alpha = 0.7f, ), - // Tokyo to NewYork - PolylineState( - id = "tokyo_to_newyork", - points = listOf(tokyoLocation, newYorkLocation), - strokeColor = Color.Blue.copy(alpha = 0.7f), - strokeWidth = 3.dp, - geodesic = geodesic, + strokeWidth = 3.dp, + geodesic = geodesic, + ), + // London to New York + PolylineState( + id = "london_to_newyork", + points = listOf(londonLocation, newYorkLocation), + strokeColor = + Color( // maroon + red = 0.75f, + green = 0f, + blue = 0f, + alpha = 0.7f, ), - // Tokyo to Honolulu - PolylineState( - id = "tokyo_to_honolulu", - points = listOf(tokyoLocation, honoluluLocation), - strokeColor = - Color( // Neon orange - red = 1.0f, - green = 0.35f, - blue = 0.12f, - alpha = 0.7f, - ), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - // London to New York - PolylineState( - id = "london_to_newyork", - points = listOf(londonLocation, newYorkLocation), - strokeColor = - Color( // maroon - red = 0.75f, - green = 0f, - blue = 0f, - alpha = 0.7f, - ), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - // London to Sydney - PolylineState( - id = "london_to_sydney", - points = listOf(londonLocation, sydneyLocation), - strokeColor = - Color.Magenta.copy(alpha = 0.7f), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - ) + strokeWidth = 3.dp, + geodesic = geodesic, + ), + // London to Sydney + PolylineState( + id = "london_to_sydney", + points = listOf(londonLocation, sydneyLocation), + strokeColor = + Color.Magenta.copy(alpha = 0.7f), + strokeWidth = 3.dp, + geodesic = geodesic, + ), + ) +// override val polylines: List +// get() = _polylines.toList() override fun onMapViewChanged(state: MapViewState<*>) { _mapViewState.value = state diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt index e3afbe31..d5ee3340 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylineMapPage.kt @@ -10,6 +10,7 @@ import com.mapconductor.example.ui.DemoMapPageScaffold fun PolylineMapPage(onToggleSidebar: () -> Unit = {}) { val viewModel = remember { PolylinePageViewModelImpl() } DemoMapPageScaffold( + initSelect = 1, menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, onMapViewStateChanged = viewModel::onMapViewChanged, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt index d77ce269..e40471e3 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt @@ -17,7 +17,7 @@ class PolylineState( points: List, id: String? = null, strokeColor: Color = Color.Black, - strokeWidth: Dp = 2.dp, + strokeWidth: Dp = 1.dp, geodesic: Boolean = false, extra: Parcelable? = null, ) { From 6a30a869774df6bfab439ce90bee4702e625bf61 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 3 Sep 2025 01:03:17 +0900 Subject: [PATCH 40/44] Fix: map.flyTo demo does not work --- .../pages/map/flyto/FlyToPageViewModel.kt | 163 +++++++++--------- .../mapconductor/core/polyline/Polyline.kt | 2 +- 2 files changed, 82 insertions(+), 83 deletions(-) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToPageViewModel.kt index e052dad2..991b4eba 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToPageViewModel.kt @@ -1,7 +1,6 @@ package com.mapconductor.example.pages.map.flyto import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color @@ -113,91 +112,91 @@ class FlyToPageViewModelImpl( ), ) - override val polylines = mutableStateListOf( - // Honolulu to NewYork - PolylineState( - id = "honolulu_to_newyork", - points = listOf(honoluluLocation, newYorkLocation), - strokeColor = Color.Green.copy(alpha = 0.7f), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - // Honolulu to Sydney - PolylineState( - id = "honolulu_to_sydney", - points = listOf(honoluluLocation, sydneyLocation), - strokeColor = - Color( // lime - red = 0.7f, - green = 0f, - blue = 0f, - alpha = 0.7f, + override val polylines + get() = + listOf( + // Honolulu to NewYork + PolylineState( + id = "honolulu_to_newyork", + points = listOf(honoluluLocation, newYorkLocation), + strokeColor = Color.Green.copy(alpha = 0.7f), + strokeWidth = 3.dp, + geodesic = geodesic, ), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - // Tokyo to London - PolylineState( - id = "tokyo_to_london", - points = listOf(tokyoLocation, londonLocation), - strokeColor = - Color( // Fuchsia - red = 1.0f, - green = 0f, - blue = 1.0f, - alpha = 0.7f, + // Honolulu to Sydney + PolylineState( + id = "honolulu_to_sydney", + points = listOf(honoluluLocation, sydneyLocation), + strokeColor = + Color( // lime + red = 0.7f, + green = 0f, + blue = 0f, + alpha = 0.7f, + ), + strokeWidth = 3.dp, + geodesic = geodesic, ), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - // Tokyo to NewYork - PolylineState( - id = "tokyo_to_newyork", - points = listOf(tokyoLocation, newYorkLocation), - strokeColor = Color.Blue.copy(alpha = 0.7f), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - // Tokyo to Honolulu - PolylineState( - id = "tokyo_to_honolulu", - points = listOf(tokyoLocation, honoluluLocation), - strokeColor = - Color( // Neon orange - red = 1.0f, - green = 0.35f, - blue = 0.12f, - alpha = 0.7f, + // Tokyo to London + PolylineState( + id = "tokyo_to_london", + points = listOf(tokyoLocation, londonLocation), + strokeColor = + Color( // Fuchsia + red = 1.0f, + green = 0f, + blue = 1.0f, + alpha = 0.7f, + ), + strokeWidth = 3.dp, + geodesic = geodesic, ), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - // London to New York - PolylineState( - id = "london_to_newyork", - points = listOf(londonLocation, newYorkLocation), - strokeColor = - Color( // maroon - red = 0.75f, - green = 0f, - blue = 0f, - alpha = 0.7f, + // Tokyo to NewYork + PolylineState( + id = "tokyo_to_newyork", + points = listOf(tokyoLocation, newYorkLocation), + strokeColor = Color.Blue.copy(alpha = 0.7f), + strokeWidth = 3.dp, + geodesic = geodesic, ), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - // London to Sydney - PolylineState( - id = "london_to_sydney", - points = listOf(londonLocation, sydneyLocation), - strokeColor = - Color.Magenta.copy(alpha = 0.7f), - strokeWidth = 3.dp, - geodesic = geodesic, - ), - ) -// override val polylines: List -// get() = _polylines.toList() + // Tokyo to Honolulu + PolylineState( + id = "tokyo_to_honolulu", + points = listOf(tokyoLocation, honoluluLocation), + strokeColor = + Color( // Neon orange + red = 1.0f, + green = 0.35f, + blue = 0.12f, + alpha = 0.7f, + ), + strokeWidth = 3.dp, + geodesic = geodesic, + ), + // London to New York + PolylineState( + id = "london_to_newyork", + points = listOf(londonLocation, newYorkLocation), + strokeColor = + Color( // maroon + red = 0.75f, + green = 0f, + blue = 0f, + alpha = 0.7f, + ), + strokeWidth = 3.dp, + geodesic = geodesic, + ), + // London to Sydney + PolylineState( + id = "london_to_sydney", + points = listOf(londonLocation, sydneyLocation), + strokeColor = + Color.Magenta.copy(alpha = 0.7f), + strokeWidth = 3.dp, + geodesic = geodesic, + ), + ) override fun onMapViewChanged(state: MapViewState<*>) { _mapViewState.value = state diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt index e40471e3..d2c45ea3 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt @@ -36,7 +36,7 @@ class PolylineState( var strokeColor by mutableStateOf(strokeColor) var strokeWidth by mutableStateOf(strokeWidth) var geodesic by mutableStateOf(geodesic) - var points by StateFlowDelegate>(points) + var points by mutableStateOf>(points) var extra by mutableStateOf(extra) private fun polylineId(hashCodes: List): Int = From 57fe4b4d65a2a47b5e2f0a7b209bcf489c3f8332 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Wed, 3 Sep 2025 01:04:05 +0900 Subject: [PATCH 41/44] Use Android-NDK27 for mapbox https://docs.mapbox.com/android/ja/maps/guides/#ndk-27 --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ecb653b2..b10dca69 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ compose = "1.8.1" composeBom = "2025.05.00" runner = "1.6.2" -mapboxAndroid = "11.13.1" +mapboxAndroid = "11.14.3" playServicesMaps = "19.2.0" secretsGradlePlugin = "2.0.1" @@ -61,7 +61,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "compose" } # Mapbox -mapbox-android = { module = "com.mapbox.maps:android", version.ref = "mapboxAndroid" } +mapbox-android = { module = "com.mapbox.maps:android-ndk27", version.ref = "mapboxAndroid" } # Google Maps play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" } From b3ecfbb45f3427a33a9c4544d9a708ea89b60537 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Thu, 4 Sep 2025 02:06:46 +0900 Subject: [PATCH 42/44] Refactor overlay architecture to use unified overlay pattern Standardizes all overlay types (circles, polygons, polylines, ground images) to use the new overlay renderer pattern across all map providers, improving consistency and maintainability. --- .claude/settings.local.json | 3 +- .../pages/circle/CirclePageViewModel.kt | 6 +- .../pages/map/flyto/FlyToMapComponent.kt | 1 - .../pages/marker/MarkerBasicMapComponent.kt | 238 ++++--- .../pages/polygon/PolygonMapPageViewModel.kt | 1 - .../pages/polyline/PolylinePageViewModel.kt | 1 - .../example/pages/stores/DemoData.kt | 652 ++++++++++-------- .../example/pages/stores/StoreInfoView.kt | 1 - .../com/mapconductor/core/OverlayProvider.kt | 83 +-- .../circle/AbstractCircleOverlayRenderer.kt | 44 ++ .../com/mapconductor/core/circle/Circle.kt | 7 +- .../mapconductor/core/circle/CircleCapable.kt | 9 + .../core/circle/CircleController.kt | 141 ++++ .../mapconductor/core/circle/CircleManager.kt | 60 +- .../mapconductor/core/circle/CircleOverlay.kt | 23 + .../core/circle/CircleOverlayManager.kt | 2 +- .../core/circle/CircleOverlayRenderer.kt | 20 + .../core/circle/CircleRenderer.kt | 9 - .../core/controller/BaseMapViewController.kt | 27 +- .../core/controller/MapViewController.kt | 14 +- .../core/controller/OverlayController.kt | 2 +- .../AbstractGroundImageOverlayRenderer.kt | 46 ++ .../core/groundimage/GroundImageController.kt | 119 ++-- .../core/groundimage/GroundImageManager.kt | 37 + .../core/groundimage/GroundImageOverlay.kt | 7 + .../groundimage/GroundImageOverlayRenderer.kt | 20 + .../com/mapconductor/core/map/MapViewBase.kt | 13 +- .../core/marker/AbstractMarkerController.kt | 1 - .../mapconductor/core/marker/DefaultIcon.kt | 30 +- .../com/mapconductor/core/marker/Marker.kt | 1 - .../mapconductor/core/marker/MarkerCompose.kt | 1 - .../mapconductor/core/marker/MarkerOverlay.kt | 7 + .../core/polygon/PolygonController.kt | 1 - .../core/polygon/PolygonOverlay.kt | 7 + .../mapconductor/core/polyline/Polyline.kt | 5 +- .../core/polyline/PolylineController.kt | 1 - .../core/polyline/PolylineOverlay.kt | 7 + .../core/polyline/PolylineOverlayManager.kt | 140 ---- .../core/polyline/PolylineRenderer.kt | 38 - .../com/mapconductor/arcgis/ArcGISMapView.kt | 2 +- .../arcgis/ArcGISMapViewControllerImpl.kt | 93 ++- .../arcgis/ArcGISViewControllerStore.kt | 34 +- .../circle/ArcGISCircleOverlayController.kt | 11 + .../circle/ArcGISCircleOverlayRenderer.kt | 116 ++++ .../arcgis/circle/ArcGISCircleRenderer.kt | 137 ---- .../polygon/ArcGISPolygonOverlayController.kt | 11 + ...ler.kt => ArcGISPolygonOverlayRenderer.kt} | 8 - .../ArcGISPolylineOverlayController.kt | 11 + ...er.kt => ArcGISPolylineOverlayRenderer.kt} | 9 +- .../mapconductor/googlemaps/GoogleMapView.kt | 2 +- .../googlemaps/GoogleMapViewControllerImpl.kt | 66 +- .../GoogleMapViewControllerStore.kt | 49 +- .../circle/GoogleMapCircleController.kt | 11 + .../circle/GoogleMapCircleOverlayRenderer.kt | 72 ++ .../circle/GoogleMapCircleRenderer.kt | 95 --- .../GoogleMapGroundImageController.kt | 11 + .../GoogleMapGroundImageOverlayRenderer.kt | 65 ++ .../GoogleMapGroundImageRenderer.kt | 75 -- .../polygon/GoogleMapPolygonController.kt | 11 + ....kt => GoogleMapPolygonOverlayRenderer.kt} | 8 - .../polyline/GoogleMapPolylineController.kt | 70 -- .../GoogleMapPolylineOverlayRenderer.kt | 73 ++ .../java/com/mapconductor/here/HereMapView.kt | 2 +- .../here/HereViewControllerImpl.kt | 57 +- .../here/HereViewControllerStore.kt | 18 +- .../here/circle/HereCircleController.kt | 10 + .../here/circle/HereCircleOverlayRenderer.kt | 147 ++++ .../here/circle/HereCircleRenderer.kt | 162 ----- .../here/polygon/HerePolygonController.kt | 91 +++ .../here/polygon/HerePolygonRenderer.kt | 91 +-- .../here/polyline/HerePolylineController.kt | 98 --- .../polyline/HerePolylineOverlayRenderer.kt | 101 +++ .../com/mapconductor/mapbox/MapboxMapView.kt | 33 +- .../mapbox/MapboxMapViewControllerImpl.kt | 84 +-- .../mapbox/circle/MapboxCircleController.kt | 10 + .../circle/MapboxCircleOverlayRenderer.kt | 85 +++ .../mapbox/circle/MapboxCircleRenderer.kt | 18 - .../mapbox/marker/MapboxMarkerController.kt | 2 +- ...erer.kt => MapboxMarkerOverlayRenderer.kt} | 2 +- .../mapconductor/mapbox/marker/MarkerLayer.kt | 6 +- .../mapbox/polygon/MapboxPolygonConductor.kt | 1 - .../polyline/MapboxPolylineController.kt | 62 -- .../polyline/MapboxPolylineOverlayRenderer.kt | 66 ++ 83 files changed, 2101 insertions(+), 1910 deletions(-) create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/circle/AbstractCircleOverlayRenderer.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCapable.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleController.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayRenderer.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/AbstractGroundImageOverlayRenderer.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageManager.kt create mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayRenderer.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayManager.kt delete mode 100644 mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineRenderer.kt create mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayController.kt create mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayRenderer.kt delete mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleRenderer.kt create mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayController.kt rename mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/{ArcGISPolygonController.kt => ArcGISPolygonOverlayRenderer.kt} (91%) create mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayController.kt rename mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/{ArcGISPolylineController.kt => ArcGISPolylineOverlayRenderer.kt} (91%) create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleController.kt create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleOverlayRenderer.kt delete mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageController.kt create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageOverlayRenderer.kt delete mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonController.kt rename mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/{GoogleMapPolygonRenderer.kt => GoogleMapPolygonOverlayRenderer.kt} (87%) create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineOverlayRenderer.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleController.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleOverlayRenderer.kt delete mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleRenderer.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineOverlayRenderer.kt create mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleController.kt create mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt rename mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/{MapboxMarkerRenderer.kt => MapboxMarkerOverlayRenderer.kt} (99%) create mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt diff --git a/.claude/settings.local.json b/.claude/settings.local.json index df15c18c..3f3779e0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,8 @@ "permissions": { "allow": [ "Bash(./gradlew:*)", - "Bash(grep:*)" + "Bash(grep:*)", + "Bash(rm:*)" ], "deny": [] } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt index e0fa5626..3f7b7144 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel -import com.mapconductor.core.circle.CircleClickEvent +import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.map.MapCameraPosition @@ -41,7 +41,7 @@ interface CirclePageViewModel { fun onMapClick(clicked: GeoPoint) - fun onCircleClick(event: CircleClickEvent) + fun onCircleClick(event: CircleEvent) fun onMarkerMove(dragged: MarkerState) @@ -148,7 +148,7 @@ class CirclePageViewModelImpl : showToast("Map clicked at: ${clicked.toUrlValue()}") } - override fun onCircleClick(event: CircleClickEvent) { + override fun onCircleClick(event: CircleEvent) { this.tapIdx = (this.tapIdx + 1) % this.colors.size event.state.fillColor = this.colors[this.tapIdx].copy(alpha = fillOpacity) showToast("Circle clicked - Radius: ${radiusMeters.toInt()}m") diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt index f6c4758f..28321be1 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/flyto/FlyToMapComponent.kt @@ -2,7 +2,6 @@ package com.mapconductor.example.pages.map.flyto import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.mapconductor.core.map.MapViewState diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt index 9d3cb514..68568071 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/MarkerBasicMapComponent.kt @@ -49,7 +49,6 @@ fun MarkerBasicMapComponent( onMarkerClick = { selected = it }, onMapClick = { selected = null }, ) { - Marker( position = GeoPoint.fromLatLong(0.018, 0.004), icon = @@ -58,143 +57,157 @@ fun MarkerBasicMapComponent( label = "0.7", debug = true, ), - extra = """ - DefaultIcon( - scale = 0.7f, - label = "0.7", - debug = true, - ), - """.trimIndent() + extra = + """ + DefaultIcon( + scale = 0.7f, + label = "0.7", + debug = true, + ), + """.trimIndent(), ) Marker( position = GeoPoint.fromLatLong(0.018, 0.006), - icon = DefaultIcon( - scale = 1.0f, - label = "1.0", - debug = true, - ), - extra = """ - DefaultIcon( - scale = 1.0f, - label = "1.0", - debug = true, - ), - """.trimIndent() + icon = + DefaultIcon( + scale = 1.0f, + label = "1.0", + debug = true, + ), + extra = + """ + DefaultIcon( + scale = 1.0f, + label = "1.0", + debug = true, + ), + """.trimIndent(), ) Marker( position = GeoPoint.fromLatLong(0.018, 0.009), - icon = DefaultIcon( - scale = 1.4f, - label = "1.4", - debug = true, - ), - extra = """ - DefaultIcon( - scale = 1.4f, - label = "1.4", - debug = true, - ), - """.trimIndent() + icon = + DefaultIcon( + scale = 1.4f, + label = "1.4", + debug = true, + ), + extra = + """ + DefaultIcon( + scale = 1.4f, + label = "1.4", + debug = true, + ), + """.trimIndent(), ) Marker( position = GeoPoint.fromLatLong(0.018, 0.013), - icon = DefaultIcon( - scale = 2.1f, - label = "2.1", - debug = true, - ), - extra = """ - DefaultIcon( - scale = 2.1f, - label = "2.1", - debug = true, - ), - """.trimIndent() + icon = + DefaultIcon( + scale = 2.1f, + label = "2.1", + debug = true, + ), + extra = + """ + DefaultIcon( + scale = 2.1f, + label = "2.1", + debug = true, + ), + """.trimIndent(), ) - - Marker( position = GeoPoint.fromLatLong(0.014, 0.004), extra = "DefaultIcon()", ) Marker( position = GeoPoint.fromLatLong(0.014, 0.008), - icon = DefaultIcon( - fillColor = Color.Yellow, - strokeColor = Color.Black, - strokeWidth = 2.dp, - ), - extra = """ - DefaultIcon( - fillColor = Color.Yellow, - strokeColor = Color.Black, - strokeWidth = 2.dp, - ) - """.trimIndent() + icon = + DefaultIcon( + fillColor = Color.Yellow, + strokeColor = Color.Black, + strokeWidth = 2.dp, + ), + extra = + """ + DefaultIcon( + fillColor = Color.Yellow, + strokeColor = Color.Black, + strokeWidth = 2.dp, + ) + """.trimIndent(), ) Marker( position = GeoPoint.fromLatLong(0.014, 0.012), - icon = DefaultIcon( - fillColor = Color( - red = 0x2E, - green = 0xF5, - blue = 0x27, - ), - strokeColor = Color( - red = 0xFC, - green = 0x22, - blue = 0x5C, - ), - label = "AB", - labelTextColor = Color.White, - labelStrokeColor = Color.Black, - ), - extra = """ - DefaultIcon( - fillColor = Color( + icon = + DefaultIcon( + fillColor = + Color( red = 0x2E, green = 0xF5, blue = 0x27, ), - strokeColor = Color( + strokeColor = + Color( red = 0xFC, green = 0x22, blue = 0x5C, ), - label = "AB", - labelTextColor = Color.White, - labelStrokeColor = Color.Black, - ) - """.trimIndent() + label = "AB", + labelTextColor = Color.White, + labelStrokeColor = Color.Black, + ), + extra = + """ + DefaultIcon( + fillColor = Color( + red = 0x2E, + green = 0xF5, + blue = 0x27, + ), + strokeColor = Color( + red = 0xFC, + green = 0x22, + blue = 0x5C, + ), + label = "AB", + labelTextColor = Color.White, + labelStrokeColor = Color.Black, + ) + """.trimIndent(), ) - AppCompatResources.getDrawable(context, R.drawable.human)?.let { icon -> Marker( position = GeoPoint.fromLatLong(0.01, 0.004), - icon = DrawableDefaultIcon( - backgroundDrawable = icon, - ), - extra = """ + icon = + DrawableDefaultIcon( + backgroundDrawable = icon, + ), + extra = + """ DrawableDefaultIcon( backgroundDrawable = icon, ) - """.trimIndent() + """.trimIndent(), ) } AppCompatResources.getDrawable(context, R.drawable.ic_launcher_foreground)?.let { icon -> Marker( position = GeoPoint.fromLatLong(0.01, 0.006), - icon = DrawableDefaultIcon( - backgroundDrawable = icon, - strokeColor = Color.Black, - scale = 1.5f, - ), - extra =""" + icon = + DrawableDefaultIcon( + backgroundDrawable = icon, + strokeColor = Color.Black, + scale = 1.5f, + ), + extra = + """ DrawableDefaultIcon( backgroundDrawable = icon, strokeColor = Color.Black, @@ -204,38 +217,41 @@ fun MarkerBasicMapComponent( ) } - AppCompatResources.getDrawable(context, R.drawable.wmo_00_clear)?.let { icon -> Marker( position = GeoPoint.fromLatLong(0.01, 0.009), - icon = ImageIcon( - drawable = icon, - debug = true, - anchor = Offset(0.5f, 1.0f), - ), - extra = """ + icon = + ImageIcon( + drawable = icon, + debug = true, + anchor = Offset(0.5f, 1.0f), + ), + extra = + """ ImageIcon( drawable = icon, debug = true, anchor = Offset(0.5f, 1.0f), ) - """.trimIndent() + """.trimIndent(), ) } createMarkerWithLabelIcon(context, "Label")?.let { Marker( position = GeoPoint.fromLatLong(0.01, 0.012), - icon = ImageIcon( - drawable = it, - anchor = Offset(0.5f, 1.0f), - ), - extra = """ + icon = + ImageIcon( + drawable = it, + anchor = Offset(0.5f, 1.0f), + ), + extra = + """ ImageIcon( drawable = createMarkerWithLabelIcon(label), anchor = Offset(0.5f, 1.0f), ) - """.trimIndent() + """.trimIndent(), ) } (selected?.extra as? String)?.let { snippet -> @@ -243,7 +259,6 @@ fun MarkerBasicMapComponent( marker = selected!!, bubbleColor = bubbleColor, ) { - Text( text = snippet, textAlign = TextAlign.Left, @@ -253,7 +268,10 @@ fun MarkerBasicMapComponent( } } -fun createMarkerWithLabelIcon(context: Context, label: String): BitmapDrawable? { +fun createMarkerWithLabelIcon( + context: Context, + label: String, +): BitmapDrawable? { val drawable = AppCompatResources.getDrawable(context, R.drawable.marker_with_label) ?: return null val iconBitmap = drawable.toBitmap() diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt index ed33a526..ab084aff 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/PolygonMapPageViewModel.kt @@ -13,7 +13,6 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.polygon.PolygonState -import android.os.Bundle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt index c2abae84..04722a0f 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/polyline/PolylinePageViewModel.kt @@ -10,7 +10,6 @@ import com.mapconductor.core.map.MapViewState import com.mapconductor.core.marker.DefaultIcon import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.polyline.PolylineState -import android.os.Bundle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt index 54bf856e..10e84479 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/DemoData.kt @@ -19,24 +19,26 @@ data class StoreInfo( val instore: Boolean, val driveThrough: Boolean, val onlyReserved: Boolean, - val store: String + val store: String, ) : Serializable val StarbucksHI_list = listOf( MarkerState( - position = GeoPoint( - latitude = 21.647441446388, - longitude = -158.062544988096, - ), - extra = StoreInfo( - name = "Pupukea (North Shore)", - address = "59-720 Kamehameha Highway, Haleiwa, HI 96712", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "coffee_bean" - ), + position = + GeoPoint( + latitude = 21.647441446388, + longitude = -158.062544988096, + ), + extra = + StoreInfo( + name = "Pupukea (North Shore)", + address = "59-720 Kamehameha Highway, Haleiwa, HI 96712", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_bean", + ), icon = DefaultIcon( label = "店", @@ -48,339 +50,387 @@ val StarbucksHI_list = ), ), MarkerState( - position = GeoPoint( - latitude = 21.33310051533, - longitude = -157.922371535818, - ), - extra = StoreInfo( - name = "Honolulu Airport (HNL) – Main", - address = "300 Rogers Blvd, Honolulu, HI 96820", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "coffee_bean" - ), + position = + GeoPoint( + latitude = 21.33310051533, + longitude = -157.922371535818, + ), + extra = + StoreInfo( + name = "Honolulu Airport (HNL) – Main", + address = "300 Rogers Blvd, Honolulu, HI 96820", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_bean", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.378981027427, - longitude = -157.930536387573, - ), - extra = StoreInfo( - name = "Aiea Shopping Center", - address = "99-115 Aiea Heights Drive #125, Aiea, HI 96701", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 21.378981027427, + longitude = -157.930536387573, + ), + extra = + StoreInfo( + name = "Aiea Shopping Center", + address = "99-115 Aiea Heights Drive #125, Aiea, HI 96701", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.38441101519, - longitude = -157.944839558127, - ), - extra = StoreInfo( - name = "Pearlridge Center", - address = "98-125 Kaonohi Street, Aiea, HI 96701", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 21.38441101519, + longitude = -157.944839558127, + ), + extra = + StoreInfo( + name = "Pearlridge Center", + address = "98-125 Kaonohi Street, Aiea, HI 96701", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.363785189939, - longitude = -157.928412704343, - ), - extra = StoreInfo( - name = "Stadium Marketplace", - address = "4561 Salt Lake Boulevard, Aiea, HI 96818", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 21.363785189939, + longitude = -157.928412704343, + ), + extra = + StoreInfo( + name = "Stadium Marketplace", + address = "4561 Salt Lake Boulevard, Aiea, HI 96818", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.386340299119, - longitude = -157.941897795274, - ), - extra = StoreInfo( - name = "Pearlridge Mall", - address = "98-1005 Moanalua Road, Aiea, HI 96701", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "coffee_extra" - ), + position = + GeoPoint( + latitude = 21.386340299119, + longitude = -157.941897795274, + ), + extra = + StoreInfo( + name = "Pearlridge Mall", + address = "98-1005 Moanalua Road, Aiea, HI 96701", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( - position = GeoPoint( - latitude = 19.69971686484, - longitude = -155.067322812851, - ), - extra = StoreInfo( - name = "Waiakea Center (Hilo)", - address = "315-325 Makaala Street, Hilo, HI 96720", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 19.69971686484, + longitude = -155.067322812851, + ), + extra = + StoreInfo( + name = "Waiakea Center (Hilo)", + address = "315-325 Makaala Street, Hilo, HI 96720", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 19.695097953188, - longitude = -155.06690203818, - ), - extra = StoreInfo( - name = "Prince Kuhio Plaza (Hilo)", - address = "111 East Puainako Street, Hilo, HI 96720", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 19.695097953188, + longitude = -155.06690203818, + ), + extra = + StoreInfo( + name = "Prince Kuhio Plaza (Hilo)", + address = "111 East Puainako Street, Hilo, HI 96720", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 19.719877684807, - longitude = -155.082770375139, - ), - extra = StoreInfo( - name = "Downtown Hilo (Kilauea Ave)", - address = "438 Kilauea Ave, Hilo, HI 96720", - instore = true, - driveThrough = true, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 19.719877684807, + longitude = -155.082770375139, + ), + extra = + StoreInfo( + name = "Downtown Hilo (Kilauea Ave)", + address = "438 Kilauea Ave, Hilo, HI 96720", + instore = true, + driveThrough = true, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.33593, - longitude = -157.91581, - ), - extra = StoreInfo( - name = "Airport Trade Center", - address = "Airport Trade Center, 550 Paiea St, Honolulu, HI 96819", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 21.33593, + longitude = -157.91581, + ), + extra = + StoreInfo( + name = "Airport Trade Center", + address = "Airport Trade Center, 550 Paiea St, Honolulu, HI 96819", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.307358712377, - longitude = -157.865194116049, - ), - extra = StoreInfo( - name = "Aloha Tower", - address = "1 Aloha Tower Drive, Honolulu, HI 96813", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "coffee_extra" - ), + position = + GeoPoint( + latitude = 21.307358712377, + longitude = -157.865194116049, + ), + extra = + StoreInfo( + name = "Aloha Tower", + address = "1 Aloha Tower Drive, Honolulu, HI 96813", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.30846253, - longitude = -157.8614898, - ), - extra = StoreInfo( - name = "Bishop (Downtown)", - address = "1000 Bishop Street #104, Honolulu, HI 96813", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "coffee_extra" - ), + position = + GeoPoint( + latitude = 21.30846253, + longitude = -157.8614898, + ), + extra = + StoreInfo( + name = "Bishop (Downtown)", + address = "1000 Bishop Street #104, Honolulu, HI 96813", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.307604966533, - longitude = -157.860743724617, - ), - extra = StoreInfo( - name = "Pickup – King & Alakea", - address = "220 South King Street, Honolulu, HI 96813", - instore = false, - driveThrough = false, - onlyReserved = false, - store = "honolulu_coffee" - ), + position = + GeoPoint( + latitude = 21.307604966533, + longitude = -157.860743724617, + ), + extra = + StoreInfo( + name = "Pickup – King & Alakea", + address = "220 South King Street, Honolulu, HI 96813", + instore = false, + driveThrough = false, + onlyReserved = false, + store = "honolulu_coffee", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.285300825278, - longitude = -157.83841421971, - ), - extra = StoreInfo( - name = "Discovery Bay Center", - address = "1778 Ala Moana Boulevard, Honolulu, HI 96815", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "coffee_extra" - ), + position = + GeoPoint( + latitude = 21.285300825278, + longitude = -157.83841421971, + ), + extra = + StoreInfo( + name = "Discovery Bay Center", + address = "1778 Ala Moana Boulevard, Honolulu, HI 96815", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.334058693598, - longitude = -158.023228524098, - ), - extra = StoreInfo( - name = "Ewa Beach – Laulani Village", - address = "91-1401 Fort Weaver Road, Ewa Beach, HI 96706", - instore = true, - driveThrough = true, - onlyReserved = false, - store = "coffee_bean" - ), + position = + GeoPoint( + latitude = 21.334058693598, + longitude = -158.023228524098, + ), + extra = + StoreInfo( + name = "Ewa Beach – Laulani Village", + address = "91-1401 Fort Weaver Road, Ewa Beach, HI 96706", + instore = true, + driveThrough = true, + onlyReserved = false, + store = "coffee_bean", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.280578442859, - longitude = -157.828071689214, - ), - extra = StoreInfo( - name = "DFS (Duty Free) Waikiki", - address = "330 Royal Hawaiian Avenue, Honolulu, HI 96815", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "coffee_extra" - ), + position = + GeoPoint( + latitude = 21.280578442859, + longitude = -157.828071689214, + ), + extra = + StoreInfo( + name = "DFS (Duty Free) Waikiki", + address = "330 Royal Hawaiian Avenue, Honolulu, HI 96815", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.308557010703, - longitude = -157.862582769768, - ), - extra = StoreInfo( - name = "Financial Plaza (Downtown)", - address = "130 Merchant Street #111, Honolulu, HI 96813", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "honolulu_coffee" - ), + position = + GeoPoint( + latitude = 21.308557010703, + longitude = -157.862582769768, + ), + extra = + StoreInfo( + name = "Financial Plaza (Downtown)", + address = "130 Merchant Street #111, Honolulu, HI 96813", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "honolulu_coffee", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.282048, - longitude = -157.713041, - ), - extra = StoreInfo( - name = "Hawaii Kai Town Center", - address = "6700 Kalanianaole Highway, Honolulu, HI 96825", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 21.282048, + longitude = -157.713041, + ), + extra = + StoreInfo( + name = "Hawaii Kai Town Center", + address = "6700 Kalanianaole Highway, Honolulu, HI 96825", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.291792650634, - longitude = -157.849735879475, - ), - extra = StoreInfo( - name = "Hokua (Ala Moana)", - address = "1288 Ala Moana Blvd, Honolulu, HI 96814", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "coffee_bean" - ), + position = + GeoPoint( + latitude = 21.291792650634, + longitude = -157.849735879475, + ), + extra = + StoreInfo( + name = "Hokua (Ala Moana)", + address = "1288 Ala Moana Blvd, Honolulu, HI 96814", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_bean", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.335246981366, - longitude = -157.868748238078, - ), - extra = StoreInfo( - name = "Kamehameha Shopping Center", - address = "1620 North School Street, Honolulu, HI 96817", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 21.335246981366, + longitude = -157.868748238078, + ), + extra = + StoreInfo( + name = "Kamehameha Shopping Center", + address = "1620 North School Street, Honolulu, HI 96817", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.27852422, - longitude = -157.7875773, - ), - extra = StoreInfo( - name = "Kahala Mall", - address = "4211 Waialae Avenue, Honolulu, HI 96816", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 21.27852422, + longitude = -157.7875773, + ), + extra = + StoreInfo( + name = "Kahala Mall", + address = "4211 Waialae Avenue, Honolulu, HI 96816", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.279056707748, - longitude = -157.813890137018, - ), - extra = StoreInfo( - name = "Kapahulu Avenue", - address = "625 Kapahulu Avenue, Honolulu, HI 96815", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "coffee_extra" - ), + position = + GeoPoint( + latitude = 21.279056707748, + longitude = -157.813890137018, + ), + extra = + StoreInfo( + name = "Kapahulu Avenue", + address = "625 Kapahulu Avenue, Honolulu, HI 96815", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "coffee_extra", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.276148191143, - longitude = -157.704922547261, - ), - extra = StoreInfo( - name = "Koko Marina Center", - address = "7192 Kalanianaole Highway, Honolulu, HI 96825", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 21.276148191143, + longitude = -157.704922547261, + ), + extra = + StoreInfo( + name = "Koko Marina Center", + address = "7192 Kalanianaole Highway, Honolulu, HI 96825", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.30985278855, - longitude = -157.810260198584, - ), - extra = StoreInfo( - name = "Manoa Valley", - address = "2902 East Manoa Road, Honolulu, HI 96822", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "starbucks" - ), + position = + GeoPoint( + latitude = 21.30985278855, + longitude = -157.810260198584, + ), + extra = + StoreInfo( + name = "Manoa Valley", + address = "2902 East Manoa Road, Honolulu, HI 96822", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "starbucks", + ), ), MarkerState( - position = GeoPoint( - latitude = 21.289750395336, - longitude = -157.843910788044, - ), - extra = StoreInfo( - name = "Macy’s Ala Moana Center", - address = "1450 Ala Moana Boulevard, Honolulu, HI 96814", - instore = true, - driveThrough = false, - onlyReserved = false, - store = "honolulu_coffee" - ), + position = + GeoPoint( + latitude = 21.289750395336, + longitude = -157.843910788044, + ), + extra = + StoreInfo( + name = "Macy’s Ala Moana Center", + address = "1450 Ala Moana Boulevard, Honolulu, HI 96814", + instore = true, + driveThrough = false, + onlyReserved = false, + store = "honolulu_coffee", + ), ), ) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt index 3bb2ce40..2a2c6db2 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/stores/StoreInfoView.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import android.os.Bundle @Composable fun StoreInfoView( diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index c4980a71..f46517a3 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -2,7 +2,7 @@ package com.mapconductor.core import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.compositionLocalOf +import com.mapconductor.core.circle.CircleOverlay import com.mapconductor.core.circle.CircleState import com.mapconductor.core.controller.MapViewControllerAlias import com.mapconductor.core.groundimage.GroundImageOverlay @@ -17,51 +17,6 @@ import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.PolylineOverlay import com.mapconductor.core.polyline.PolylineState import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -// data class OverlayProvider( -// val compositionLocal: ProvidableCompositionLocal>>, -// val stateFlow: MutableStateFlow>, -// ) - -// @Composable -// fun ProvideOverlayLocals( -// providers: List>, -// content: @Composable () -> Unit, -// ) { -// val wrapped = -// providers.foldRight(content) { provider, acc -> -// @Suppress("UNCHECKED_CAST") -// { -// val local = provider.compositionLocal as ProvidableCompositionLocal>> -// val flow = provider.stateFlow as MutableStateFlow> -// CompositionLocalProvider(local provides flow) { -// acc() -// } -// } -// } -// -// wrapped() -// } - -// @Composable -// fun CollectAndRenderOverlays( -// map: T?, -// registry: MapOverlayRegistry, -// controller: MapViewController, -// ) { -// registry.getAll().forEach { overlay -> -// @Suppress("UNCHECKED_CAST") -// val typedOverlay = overlay as MapOverlay -// -// val flowState = typedOverlay.flow.collectAsState() -// -// LaunchedEffect(map, flowState.value) { -// if (map == null) return@LaunchedEffect -// typedOverlay.render(flowState.value, controller) -// } -// } -// } open class MapViewScope { val markerFlow = MutableStateFlow>(emptyList()) @@ -82,42 +37,6 @@ open class MapViewScope { } } -class CircleOverlay( - override val flow: StateFlow>, -) : MapOverlay { - override suspend fun render( - data: List, - controller: MapViewControllerAlias, - ) { - controller.addCircles(data) - } -} - -val LocalMarkerCollector = - compositionLocalOf>> { - error("Marker must be under the ") - } - -val LocalPolylineCollector = - compositionLocalOf>> { - error("Polyline must be under the ") - } - -val LocalCircleCollector = - compositionLocalOf>> { - error("Circle must be under the ") - } - -val LocalPolygonCollector = - compositionLocalOf>> { - error("Polygon must be under the ") - } - -val LocalGroundImageCollector = - compositionLocalOf>> { - error("GroundImage must be under the ") - } - @Composable fun CollectAndRenderOverlays( registry: MapOverlayRegistry, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/AbstractCircleOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/AbstractCircleOverlayRenderer.kt new file mode 100644 index 00000000..2d656f1c --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/AbstractCircleOverlayRenderer.kt @@ -0,0 +1,44 @@ +package com.mapconductor.core.circle + +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleOverlayRenderer +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.map.MapViewHolder +import kotlinx.coroutines.CoroutineScope + +abstract class AbstractCircleOverlayRenderer : CircleOverlayRenderer { + abstract val holder: MapViewHolder<*, *> + abstract val coroutine: CoroutineScope + + override suspend fun onPostProcess() { + // Default implementation - can be overridden by subclasses + } + + abstract suspend fun removeCircle(entity: CircleEntity) + + abstract suspend fun createCircle(state: CircleState): ActualCircle? + + abstract suspend fun updateCircleProperties( + circle: ActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): ActualCircle? + + override suspend fun onAdd(data: List): List = + data.map { params -> createCircle(params.state) } + + override suspend fun onChange(data: List>): List = + data.map { params -> + updateCircleProperties( + circle = params.prev.circle, + current = params.current, + prev = params.prev, + ) + } + + override suspend fun onRemove(data: List>) { + data.forEach { entity -> + removeCircle(entity) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/Circle.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/Circle.kt index e19c32cf..bd7d3cc4 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/Circle.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/Circle.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.marker.MarkerState import android.os.Parcelable @@ -134,9 +133,9 @@ data class CircleFingerPrint( val extra: Int, ) -data class CircleClickEvent( +data class CircleEvent( val state: CircleState, - val position: GeoPoint, + val clicked: IGeoPoint, ) -typealias OnCircleEventHandler = (CircleClickEvent) -> Unit +typealias OnCircleEventHandler = (CircleEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCapable.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCapable.kt new file mode 100644 index 00000000..67cba2f3 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleCapable.kt @@ -0,0 +1,9 @@ +package com.mapconductor.core.circle + +interface CircleCapable { + suspend fun compositionCircles(data: List) + + suspend fun updateCircle(state: CircleState) + + fun setOnCircleClickListener(listener: OnCircleEventHandler?) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleController.kt new file mode 100644 index 00000000..a21ccb10 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleController.kt @@ -0,0 +1,141 @@ +package com.mapconductor.core.circle + +import com.mapconductor.core.controller.OverlayController +import com.mapconductor.core.features.IGeoPoint +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit + +abstract class CircleController( + val circleManager: CircleManager, + open val renderer: CircleOverlayRenderer, + override var clickListener: OnCircleEventHandler? = null, +) : OverlayController< + CircleState, + CircleEntity, + CircleEvent, + > { + override val zIndex: Int = 3 + val semaphore = Semaphore(1) + + override suspend fun add(data: List) { + semaphore.withPermit { + val modifiedEntities = mutableListOf>() + val previous = circleManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() + val removed = mutableListOf>() + + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = circleManager.getEntity(state.id)!! + updated.add( + object : CircleOverlayRenderer.ChangeParams { + override val current: CircleEntity = + CircleEntityImpl( + state = state, + circle = prevEntity.circle, + ) + override val prev: CircleEntity = prevEntity + }, + ) + previous.remove(state.id) + } else { + added.add( + object : CircleOverlayRenderer.AddParams { + override val state: CircleState = state + }, + ) + previous.remove(state.id) + } + } + + previous.forEach { remainId -> + circleManager.removeEntity(remainId)?.let { removedEntity -> + removed.add(removedEntity) + } + } + + // Remove circle + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + + // Add new circles + if (added.isNotEmpty()) { + val actualCircles: List = renderer.onAdd(added) + actualCircles.forEachIndexed { index, circle -> + circle?.let { + val entity = + CircleEntityImpl( + circle = circle, + state = added[index].state, + ) + circleManager.registerEntity(entity) + modifiedEntities.add(entity) + } + } + } + + // Update changed circles + if (updated.isNotEmpty()) { + val actualCircles: List = renderer.onChange(updated) + actualCircles.forEachIndexed { index, circle -> + circle?.let { + val params = updated[index] + val entity = + CircleEntityImpl( + state = params.current.state, + circle = circle, + ) + circleManager.registerEntity(entity) + } + } + } + + renderer.onPostProcess() + } + } + + override suspend fun update(state: CircleState) { + semaphore.withPermit { + val prevEntity = circleManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinger = prevEntity.fingerPrint + if (currentFinger == prevFinger) { + return + } + + val circle = prevEntity.circle + val entity = + CircleEntityImpl( + circle = circle, + state = state, + ) + val circleParams = + object : CircleOverlayRenderer.ChangeParams { + override val current: CircleEntity = entity + override val prev: CircleEntity = prevEntity + } + val circles = renderer.onChange(listOf(circleParams)) + + circles[0]?.let { + val entity = + CircleEntityImpl( + circle = it, + state = state, + ) + circleManager.registerEntity(entity) + } + } + } + + override suspend fun clear() { + semaphore.withPermit { + val entities: List> = circleManager.allEntities() + renderer.onRemove(entities) + circleManager.clear() + } + } + + override fun find(position: IGeoPoint): CircleEntity? = circleManager.find(position) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleManager.kt index d306f50b..e164058a 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleManager.kt @@ -5,17 +5,31 @@ import com.mapconductor.core.features.IGeoPoint import com.mapconductor.core.spherical.haversineDistance import java.util.concurrent.ConcurrentHashMap -class CircleManager { +interface CircleManager { + fun registerEntity(entity: CircleEntity) + + fun removeEntity(id: String): CircleEntity? + + fun getEntity(id: String): CircleEntity? + + fun allEntities(): List> + + fun clear() + + fun find(position: IGeoPoint): CircleEntity? +} + +class CircleManagerImpl : CircleManager { private val entities: ConcurrentHashMap> = ConcurrentHashMap() - fun getEntity(id: String): CircleEntity? = entities.get(id) + override fun getEntity(id: String): CircleEntity? = entities.get(id) - fun removeEntity(id: String): CircleEntity? { + override fun removeEntity(id: String): CircleEntity? { val removed = entities.remove(id) return removed } - fun registerEntity(entity: CircleEntity) { + override fun registerEntity(entity: CircleEntity) { entities[entity.state.id] = entity } @@ -23,13 +37,13 @@ class CircleManager { entities[entity.state.id] = entity } - fun allEntities(): List> = entities.values.toList() + override fun allEntities(): List> = entities.values.toList() - fun clear() { + override fun clear() { entities.clear() } - fun find(position: IGeoPoint): CircleEntity? { + override fun find(position: IGeoPoint): CircleEntity? { val filtered = allEntities().filter { entity -> val centerPos = entity.state.center @@ -53,36 +67,4 @@ class CircleManager { } return maxEntity } - -// private fun isPolygonContains(path: MutableList, point: LatLng?): Boolean { -// var wn = 0 -// val visibleRegion: VisibleRegion = projection.getVisibleRegion() -// val bounds: LatLngBounds = visibleRegion.latLngBounds -// val sw: Point = projection.toScreenLocation(bounds.southwest) -// -// val touchPoint: Point = projection.toScreenLocation(point) -// touchPoint.y = sw.y - touchPoint.y -// var vt: Double -// -// for (i in 0.. touchPoint.y)) { -// vt = (touchPoint.y.toDouble() - a.y.toDouble()) / (b.y.toDouble() - a.y.toDouble()) -// if (touchPoint.x < (a.x.toDouble() + (vt * (b.x.toDouble() - a.x.toDouble())))) { -// wn++ -// } -// } else if ((a.y > touchPoint.y) && (b.y <= touchPoint.y)) { -// vt = (touchPoint.y.toDouble() - a.y.toDouble()) / (b.y.toDouble() - a.y.toDouble()) -// if (touchPoint.x < (a.x.toDouble() + (vt * (b.x.toDouble() - a.x.toDouble())))) { -// wn-- -// } -// } -// } -// -// return (wn != 0) -// } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt new file mode 100644 index 00000000..fd442363 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt @@ -0,0 +1,23 @@ +package com.mapconductor.core.circle + +import androidx.compose.runtime.compositionLocalOf +import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +val LocalCircleCollector = + compositionLocalOf>> { + error("Circle must be under the ") + } + +class CircleOverlay( + override val flow: StateFlow>, +) : MapOverlay { + override suspend fun render( + data: List, + controller: MapViewControllerAlias, + ) { + (controller as? CircleCapable)?.compositionCircles(data) + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayManager.kt index 74a9fd86..3e0409c1 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayManager.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayManager.kt @@ -24,7 +24,7 @@ class CircleOverlayManagerImpl( val onChange: suspend (List>) -> List, val onRemove: suspend (List>) -> Unit, val onPostProcess: (suspend () -> Unit)? = null, - val circleManager: CircleManager = CircleManager(), + val circleManager: CircleManagerImpl = CircleManagerImpl(), ) : CircleOverlayManager { val semaphore = Semaphore(1) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayRenderer.kt new file mode 100644 index 00000000..50346fdf --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlayRenderer.kt @@ -0,0 +1,20 @@ +package com.mapconductor.core.circle + +interface CircleOverlayRenderer { + interface AddParams { + val state: CircleState + } + + interface ChangeParams { + val current: CircleEntity + val prev: CircleEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onPostProcess() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleRenderer.kt index 49840852..50f034dd 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleRenderer.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleRenderer.kt @@ -3,15 +3,6 @@ package com.mapconductor.core.circle import com.mapconductor.core.map.MapViewHolder import kotlinx.coroutines.CoroutineScope -interface CircleRendererFactory { - fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)? = null, - ): CircleOverlayManager -} - interface CircleRenderer { interface UpdateParams { val entity: CircleEntity diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt index 6c8b90fb..6b3b694f 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt @@ -1,33 +1,12 @@ package com.mapconductor.core.controller -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRenderer -import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler -import com.mapconductor.core.polygon.OnPolygonEventHandler - -abstract class BaseMapViewController : MapViewController { - abstract val circleRenderer: CircleRenderer - - override val circleOverlayManager: CircleOverlayManager by lazy { - createCircleOverlayManager().also { overlayManager -> - circleRenderer.init(overlayManager) - onCircleOverlayManagerInitialized(overlayManager) - } - } - - protected abstract fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) - - protected abstract fun createCircleOverlayManager(): CircleOverlayManager +abstract class BaseMapViewController : MapViewController { protected var cameraMoveCallback: OnCameraMoveHandler? = null protected var mapClickCallback: OnMapEventHandler? = null protected var mapLongClickCallback: OnMapEventHandler? = null - protected var circleClickCallback: OnCircleEventHandler? = null - protected var polygonClickCallback: OnPolygonEventHandler? = null - - abstract fun setupListeners() override fun setCameraMoveListener(listener: OnCameraMoveHandler?) { this.cameraMoveCallback = listener @@ -40,8 +19,4 @@ abstract class BaseMapViewController : MapViewController { +interface MapViewController { val holder: MapViewHolder<*, *> val coroutine: CoroutineScope - val circleOverlayManager: CircleOverlayManager - - suspend fun addCircles(data: List) - - suspend fun updateCircle(state: CircleState) suspend fun clearOverlays() @@ -24,8 +16,6 @@ interface MapViewController { fun setMapClickListener(listener: OnMapEventHandler?) fun setMapLongClickListener(listener: OnMapEventHandler?) - - fun setCircleClickListener(listener: OnCircleEventHandler?) } -typealias MapViewControllerAlias = MapViewController<*> +typealias MapViewControllerAlias = MapViewController diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt index 25f053de..9f7545fa 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/OverlayController.kt @@ -2,7 +2,7 @@ package com.mapconductor.core.controller import com.mapconductor.core.features.IGeoPoint -interface OverlayController { +interface OverlayController { val zIndex: Int suspend fun add(data: List) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/AbstractGroundImageOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/AbstractGroundImageOverlayRenderer.kt new file mode 100644 index 00000000..7854571a --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/AbstractGroundImageOverlayRenderer.kt @@ -0,0 +1,46 @@ +package com.mapconductor.core.groundimage + +import com.mapconductor.core.map.MapViewHolder +import kotlin.collections.forEach +import kotlinx.coroutines.CoroutineScope + +abstract class AbstractGroundImageOverlayRenderer : GroundImageOverlayRenderer { + abstract val holder: MapViewHolder<*, *> + abstract val coroutine: CoroutineScope + + override suspend fun onPostProcess() { + // Default implementation - can be overridden by subclasses + } + + abstract suspend fun createGroundImage(state: GroundImageState): ActualGroundImage? + + abstract suspend fun updateGroundImageProperties( + groundImage: ActualGroundImage, + current: GroundImageEntity, + prev: GroundImageEntity, + ): ActualGroundImage? + + abstract suspend fun removeGroundImage(entity: GroundImageEntity) + + override suspend fun onAdd(data: List): List = + data.map { params -> + createGroundImage(params.state) + } + + override suspend fun onChange( + data: List>, + ): List = + data.map { params -> + updateGroundImageProperties( + groundImage = params.prev.groundImage, + current = params.current, + prev = params.prev, + ) + } + + override suspend fun onRemove(data: List>) { + data.forEach { entity -> + removeGroundImage(entity) + } + } +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt index 29a2e5d0..5260b3e9 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageController.kt @@ -1,74 +1,81 @@ package com.mapconductor.core.groundimage import com.mapconductor.core.controller.OverlayController -import com.mapconductor.core.controller.OverlayRenderer import com.mapconductor.core.features.IGeoPoint import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -class GroundImageController( - val renderer: OverlayRenderer>, +abstract class GroundImageController( + val groundImageManager: GroundImageManager, + open val renderer: GroundImageOverlayRenderer, override var clickListener: OnGroundImageEventHandler? = null, ) : OverlayController< - ActualGroundImage, GroundImageState, GroundImageEntity, GroundImageEvent, > { override val zIndex: Int = 2 - val entities = mutableMapOf>() val semaphore = Semaphore(1) override suspend fun add(data: List) { semaphore.withPermit { - val previous = entities.keys.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>>() + val modifiedEntities = mutableListOf>() + val previous = groundImageManager.allEntities().map { it.state.id }.toMutableSet() + val added = mutableListOf() + val updated = mutableListOf>() val removed = mutableListOf>() - data.forEach { - if (previous.contains(it.id)) { - val prevEntity = entities[it.id]!! + data.forEach { state -> + if (previous.contains(state.id)) { + val prevEntity = groundImageManager.getEntity(state.id)!! updated.add( - object : OverlayRenderer.ChangeParams> { - override val current = + object : GroundImageOverlayRenderer.ChangeParams { + override val current: GroundImageEntity = GroundImageEntityImpl( groundImage = prevEntity.groundImage, - state = it, + state = state, ) - override val prev = prevEntity + override val prev: GroundImageEntity = prevEntity }, ) - previous.remove(it.id) + previous.remove(state.id) } else { - added.add(it) - previous.remove(it.id) + added.add( + object : GroundImageOverlayRenderer.AddParams { + override val state: GroundImageState = state + }, + ) + previous.remove(state.id) } } previous.forEach { remainId -> - entities.remove(remainId)?.let { removedEntity -> + groundImageManager.removeEntity(remainId)?.let { removedEntity -> removed.add(removedEntity) } } + if (removed.isNotEmpty()) { + renderer.onRemove(removed) + } + if (added.isNotEmpty()) { val actualOverlays = renderer.onAdd(added) actualOverlays.forEachIndexed { index, actualOverlay -> actualOverlay?.let { - val state = added[index] val entity = GroundImageEntityImpl( groundImage = it, - state = state, + state = added[index].state, ) - entities[state.id] = entity + groundImageManager.registerEntity(entity) + modifiedEntities.add(entity) } } } if (updated.isNotEmpty()) { - val actualOverlays = renderer.onChange(updated.toList()) + val actualOverlays: List = renderer.onChange(updated) actualOverlays.forEachIndexed { index, actualOverlay -> actualOverlay?.let { val state = updated[index].current.state @@ -77,59 +84,57 @@ class GroundImageController( groundImage = it, state = state, ) - entities[state.id] = entity + groundImageManager.registerEntity(entity) } } } - if (removed.isNotEmpty()) { - renderer.onRemove(removed) - } - renderer.onPostProcess() } } override suspend fun update(state: GroundImageState) { semaphore.withPermit { - val updated = mutableListOf>>() - val prevEntity = entities[state.id]!! - updated.add( - object : OverlayRenderer.ChangeParams> { - override val current: GroundImageEntity = - GroundImageEntityImpl( - groundImage = prevEntity.groundImage, - state = state, - ) - override val prev: GroundImageEntity = prevEntity - }, - ) + val prevEntity = groundImageManager.getEntity(state.id) ?: return + val currentFinger = state.fingerPrint() + val prevFinder = prevEntity.fingerPrint + if (currentFinger == prevFinder) { + return + } - val actualOverlays: List = renderer.onChange(updated) - actualOverlays.forEachIndexed { index, actualOverlay -> - actualOverlay?.let { - val entity = - GroundImageEntityImpl( - groundImage = it, - state = state, - ) - entities[state.id] = entity + val groundImage = prevEntity.groundImage + val entity = + GroundImageEntityImpl( + groundImage = groundImage, + state = state, + ) + val groundImageParams = + object : GroundImageOverlayRenderer.ChangeParams { + override val current: GroundImageEntity = entity + override val prev: GroundImageEntity = prevEntity } + val groundImages = renderer.onChange(listOf(groundImageParams)) + + groundImages[0]?.let { + val entity = + GroundImageEntityImpl( + groundImage = it, + state = state, + ) + groundImageManager.registerEntity(entity) } + renderer.onPostProcess() } } override suspend fun clear() { semaphore.withPermit { - renderer.onRemove(entities.values.toList()) - entities.clear() + val entities: List> = groundImageManager.allEntities() + renderer.onRemove(entities) + renderer.onPostProcess() + groundImageManager.clear() } } - override fun find(position: IGeoPoint): GroundImageEntity? { - // TODO: Improve this implementation later - return entities.values.find { entity -> - entity.state.bounds.contains(position) - } - } + override fun find(position: IGeoPoint): GroundImageEntity? = groundImageManager.find(position) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageManager.kt new file mode 100644 index 00000000..f4c222f6 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageManager.kt @@ -0,0 +1,37 @@ +package com.mapconductor.core.groundimage + +import com.mapconductor.core.features.IGeoPoint + +interface GroundImageManager { + fun registerEntity(entity: GroundImageEntity) + + fun removeEntity(id: String): GroundImageEntity? + + fun getEntity(id: String): GroundImageEntity? + + fun allEntities(): List> + + fun clear() + + fun find(position: IGeoPoint): GroundImageEntity? +} + +class GroundImageManagerImpl : GroundImageManager { + private val entities = mutableMapOf>() + + override fun registerEntity(entity: GroundImageEntity) { + entities[entity.state.id] = entity + } + + override fun removeEntity(id: String): GroundImageEntity? = entities.remove(id) + + override fun getEntity(id: String): GroundImageEntity? = entities[id] + + override fun allEntities(): List> = entities.values.toList() + + override fun clear() { + entities.clear() + } + + override fun find(position: IGeoPoint): GroundImageEntity? = entities.values.firstOrNull() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt index 31b66d84..ea686e28 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt @@ -1,9 +1,16 @@ package com.mapconductor.core.groundimage +import androidx.compose.runtime.compositionLocalOf import com.mapconductor.core.controller.MapViewControllerAlias import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +val LocalGroundImageCollector = + compositionLocalOf>> { + error("GroundImage must be under the ") + } + class GroundImageOverlay( override val flow: StateFlow>, ) : MapOverlay { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayRenderer.kt new file mode 100644 index 00000000..06efdcc6 --- /dev/null +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlayRenderer.kt @@ -0,0 +1,20 @@ +package com.mapconductor.core.groundimage + +interface GroundImageOverlayRenderer { + interface AddParams { + val state: GroundImageState + } + + interface ChangeParams { + val current: GroundImageEntity + val prev: GroundImageEntity + } + + suspend fun onAdd(data: List): List + + suspend fun onChange(data: List>): List + + suspend fun onRemove(data: List>) + + suspend fun onPostProcess() +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index e7a77d04..75bbd221 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -22,21 +22,22 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import com.mapconductor.core.CollectAndRenderOverlays -import com.mapconductor.core.LocalCircleCollector -import com.mapconductor.core.LocalGroundImageCollector -import com.mapconductor.core.LocalMarkerCollector -import com.mapconductor.core.LocalPolygonCollector -import com.mapconductor.core.LocalPolylineCollector import com.mapconductor.core.MapViewScope import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.circle.LocalCircleCollector import com.mapconductor.core.controller.MapViewControllerAlias import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.groundimage.GroundImageCapable +import com.mapconductor.core.groundimage.LocalGroundImageCollector import com.mapconductor.core.info.InfoBubbleOverlay import com.mapconductor.core.info.LocalInfoBubbleCollector import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.LocalMarkerCollector import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.LocalPolygonCollector import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.LocalPolylineCollector import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.settings.Settings import android.view.View @@ -129,7 +130,7 @@ fun < circles.value.forEach { circleState -> LaunchedEffect(circleState.id) { circleState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - controller.updateCircle(circleState) + (controller as? CircleCapable)?.updateCircle(circleState) } } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt index 696fd159..e2f2f4b3 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt @@ -53,7 +53,6 @@ abstract class AbstractMarkerController( open val renderer: MarkerOverlayRenderer, override var clickListener: OnMarkerEventHandler? = null, ) : OverlayController< - ActualMarker, MarkerState, MarkerEntity, MarkerState, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt index 94e1859f..ce75d008 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/DefaultIcon.kt @@ -142,22 +142,24 @@ abstract class AbstractDefaultIcon( // Since canvasSize is already scaled (baseCanvasSize * scale), // we don't need to apply iconScale again to the markerScale calculation - val scaledStrokeWidth = ResourceProvider - .dpToPx(strokeWidth.value * iconScale) - .toFloat() + val scaledStrokeWidth = + ResourceProvider + .dpToPx(strokeWidth.value * iconScale) + .toFloat() // Reserve space for stroke on sides and top, but not bottom (point should touch edge) val epsilon = 0.75f val padding = (scaledStrokeWidth / 2f - epsilon).coerceAtLeast(0f) val availableWidth = canvasSize - (padding * 2f) - val availableHeight = canvasSize - padding // Only top padding, bottom point touches edge + val availableHeight = canvasSize - padding // Only top padding, bottom point touches edge // Calculate scale to fit marker within available space // DO NOT multiply by iconScale here as canvasSize already includes it - val markerScale = minOf( - availableWidth / originalSize.width, - availableHeight / originalSize.height - ) + val markerScale = + minOf( + availableWidth / originalSize.width, + availableHeight / originalSize.height, + ) val scaledWidth = originalSize.width * markerScale val scaledHeight = originalSize.height * markerScale @@ -240,12 +242,14 @@ abstract class AbstractDefaultIcon( Paint().apply { color = strokeColor.toArgb() style = Paint.Style.STROKE - strokeWidth = ResourceProvider.dpToPx( - this@AbstractDefaultIcon.strokeWidth.value * iconScale - ).toFloat() + strokeWidth = + ResourceProvider + .dpToPx( + this@AbstractDefaultIcon.strokeWidth.value * iconScale, + ).toFloat() isAntiAlias = true - strokeJoin = Paint.Join.ROUND // 追加 - strokeCap = Paint.Cap.ROUND // 追加(尖り部のにじみ軽減) + strokeJoin = Paint.Join.ROUND // 追加 + strokeCap = Paint.Cap.ROUND // 追加(尖り部のにじみ軽減) } /** diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt index 5a6b65c1..84ad9fe9 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/Marker.kt @@ -13,7 +13,6 @@ import com.mapconductor.core.features.IGeoPoint import java.io.ByteArrayOutputStream import java.io.Serializable import android.graphics.Bitmap -import android.os.Parcelable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt index d61ffcb9..9a61db0f 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerCompose.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.LaunchedEffect import com.mapconductor.core.MapViewScope import com.mapconductor.core.features.IGeoPoint import java.io.Serializable -import android.os.Parcelable @Composable fun MapViewScope.Marker(state: MarkerState) { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt index 5abdfc71..7e1b9ef6 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt @@ -1,9 +1,16 @@ package com.mapconductor.core.marker +import androidx.compose.runtime.compositionLocalOf import com.mapconductor.core.controller.MapViewControllerAlias import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +val LocalMarkerCollector = + compositionLocalOf>> { + error("Marker must be under the ") + } + class MarkerOverlay( override val flow: StateFlow>, ) : MapOverlay { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt index 4c3c9c06..9fde87e0 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonController.kt @@ -10,7 +10,6 @@ abstract class PolygonController( open val renderer: PolygonOverlayRenderer, override var clickListener: OnPolygonEventHandler? = null, ) : OverlayController< - ActualPolygon, PolygonState, PolygonEntity, PolygonEvent, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt index 9498957f..87477356 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt @@ -1,9 +1,16 @@ package com.mapconductor.core.polygon +import androidx.compose.runtime.compositionLocalOf import com.mapconductor.core.controller.MapViewControllerAlias import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +val LocalPolygonCollector = + compositionLocalOf>> { + error("Polygon must be under the ") + } + class PolygonOverlay( override val flow: StateFlow>, ) : MapOverlay { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt index d2c45ea3..48267c6e 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/Polyline.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.mapconductor.core.StateFlowDelegate import com.mapconductor.core.features.IGeoPoint import android.os.Parcelable import kotlinx.coroutines.flow.Flow @@ -106,8 +105,8 @@ data class PolylineFingerPrint( ) data class PolylineEvent( - val state: PolylineEvent, - val clicked: IGeoPoint?, + val state: PolylineState, + val clicked: IGeoPoint, ) typealias OnPolylineEventHandler = (PolylineEvent) -> Unit diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt index a57c2e73..ceb76bed 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineController.kt @@ -10,7 +10,6 @@ abstract class PolylineController( open val renderer: PolylineOverlayRenderer, override var clickListener: OnPolylineEventHandler? = null, ) : OverlayController< - ActualPolyline, PolylineState, PolylineEntity, PolylineEvent, diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt index d73b7bd4..a7637d2e 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt @@ -1,9 +1,16 @@ package com.mapconductor.core.polyline +import androidx.compose.runtime.compositionLocalOf import com.mapconductor.core.controller.MapViewControllerAlias import com.mapconductor.core.map.MapOverlay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +val LocalPolylineCollector = + compositionLocalOf>> { + error("Polyline must be under the ") + } + class PolylineOverlay( override val flow: StateFlow>, ) : MapOverlay { diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayManager.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayManager.kt deleted file mode 100644 index 096562ab..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlayManager.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.mapconductor.core.polyline - -import com.mapconductor.core.polyline.PolylineRenderer.UpdateParams -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit - -interface PolylineOverlayManager { - suspend fun addPolylines(polylines: List) - - suspend fun updatePolyline(polyline: PolylineState) - - suspend fun clearOverlays() - - fun getPolylineState(id: String): PolylineState? - - fun getAllEntities(): List> -} - -class PolylineOverlayManagerImpl( - val onAdd: suspend (List) -> List, - val onChange: suspend (List>) -> List, - val onRemove: suspend (List>) -> Unit, - val onPostProcess: (suspend () -> Unit)? = null, -) : PolylineOverlayManager { - val polylineEntities = mutableMapOf>() - - val semaphore = Semaphore(1) - - override suspend fun addPolylines(polylines: List) { - semaphore.withPermit { - val previous = polylineEntities.keys.toMutableSet() - val added = mutableListOf() - val updated = mutableListOf>() - val removed = mutableListOf>() - polylines.forEach { - if (previous.contains(it.id)) { - val prevEntity = polylineEntities.get(it.id)!! - updated.add( - object : UpdateParams { - override val entity: PolylineEntity = - PolylineEntityImpl( - state = it, - polyline = prevEntity.polyline, - ) - override val prevEntity: PolylineEntity = prevEntity - }, - ) - previous.remove(it.id) - return@forEach - } - added.add(it) - previous.remove(it.id) - } - previous.forEach { remainId -> - polylineEntities.remove(remainId)?.let { removedEntity -> - removed.add(removedEntity) - } - } - - if (added.isNotEmpty()) { - val actualPolylines = onAdd(added) - actualPolylines.forEachIndexed { index, actualPolyline -> - actualPolyline?.let { - val state = added[index] - val entity = - PolylineEntityImpl( - polyline = it, - state = state, - ) - polylineEntities[state.id] = entity - } - } - } - - if (updated.isNotEmpty()) { - val actualPolylines: List = onChange(updated) - actualPolylines.forEachIndexed { index, actualPolyline -> - actualPolyline?.let { - val state = updated[index].entity.state - val entity = - PolylineEntityImpl( - polyline = it, - state = state, - ) - polylineEntities[state.id] = entity - } - } - } - - if (removed.isNotEmpty()) { - onRemove(removed) - } - onPostProcess?.invoke() - } - } - - override suspend fun updatePolyline(state: PolylineState) { - semaphore.withPermit { - polylineEntities[state.id]?.let { prevEntity -> - - val updates = - listOf( - object : PolylineRenderer.UpdateParams { - override val entity: PolylineEntity = - PolylineEntityImpl( - state = state, - polyline = prevEntity.polyline, - ) - override val prevEntity: PolylineEntity = prevEntity - }, - ) - - val actualPolylines: List = onChange(updates) - actualPolylines.forEachIndexed { index, actualPolyline -> - actualPolyline?.let { - val entity = - PolylineEntityImpl( - state = state, - polyline = actualPolyline, - ) - polylineEntities[state.id] = entity - } - } - } - onPostProcess?.invoke() - } - } - - override suspend fun clearOverlays() { - semaphore.withPermit { - val entities = polylineEntities.values.toList() - onRemove(entities) - polylineEntities.clear() - } - } - - override fun getPolylineState(id: String): PolylineState? = polylineEntities.get(id)?.state - - override fun getAllEntities(): List> = polylineEntities.values.toList() -} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineRenderer.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineRenderer.kt deleted file mode 100644 index d588459c..00000000 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineRenderer.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.mapconductor.core.polyline - -import com.mapconductor.core.map.MapViewHolder -import kotlinx.coroutines.CoroutineScope - -interface PolylineRendererFactory { - fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)? = null, - ): PolylineOverlayManager -} - -interface PolylineRenderer { - interface UpdateParams { - val entity: PolylineEntity - val prevEntity: PolylineEntity - } - - fun init(polylineOverlayManager: PolylineOverlayManager) - - suspend fun addPolylines(newLines: List): List - - suspend fun removePolylines(removeEntities: List>) - - suspend fun changePolylines(changes: List>): List -} - -abstract class AbstractPolylineRenderer : PolylineRenderer { - protected lateinit var polylineOverlayManager: PolylineOverlayManager - abstract val holder: MapViewHolder<*, *> - abstract val coroutine: CoroutineScope - - override fun init(polylineOverlayManager: PolylineOverlayManager) { - this.polylineOverlayManager = polylineOverlayManager - } -} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index bd799847..a0e10332 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -65,7 +65,7 @@ fun ArcGISMapView( controller.holder.mapView.onResume(owner) controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) - controller.setCircleClickListener(onCircleClick) + controller.setOnCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) controller.setOnPolygonClickListener(onPolygonClick) controller.setOnMarkerClickListener(onMarkerClick) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt index d2f85d2e..a5ff8ea5 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt @@ -1,23 +1,19 @@ package com.mapconductor.arcgis -import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.mapping.view.LongPressEvent import com.arcgismaps.mapping.view.PanChangeEvent import com.arcgismaps.mapping.view.SingleTapConfirmedEvent -import com.arcgismaps.mapping.view.SurfacePlacement import com.arcgismaps.mapping.view.UpEvent import com.arcgismaps.mapping.view.extensions.motionEvent -import com.mapconductor.arcgis.circle.ArcGISCircleRenderer -import com.mapconductor.arcgis.circle.DefaultArcGISCircleRenderer +import com.mapconductor.arcgis.circle.ArcGISCircleOverlayController import com.mapconductor.arcgis.marker.ArcGISMarkerController import com.mapconductor.arcgis.marker.SelectedMarker -import com.mapconductor.arcgis.polygon.ArcGISPolygonController -import com.mapconductor.arcgis.polyline.ArcGISPolylineController -import com.mapconductor.core.circle.CircleClickEvent -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRenderer -import com.mapconductor.core.circle.CircleRendererFactory +import com.mapconductor.arcgis.polygon.ArcGISPolygonOverlayController +import com.mapconductor.arcgis.polyline.ArcGISPolylineOverlayController +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapCameraPosition @@ -31,6 +27,7 @@ import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineCapable +import com.mapconductor.core.polyline.PolylineEvent import com.mapconductor.core.polyline.PolylineState import com.mapconductor.settings.Settings import android.view.MotionEvent @@ -39,12 +36,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch interface ArcGISMapViewController : - MapViewController< - ArcGISActualCircle, - >, + MapViewController, MarkerCapable, PolylineCapable, - PolygonCapable { + PolygonCapable, + CircleCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -59,47 +55,23 @@ interface ArcGISMapViewController : class ArcGISMapViewControllerImpl( override val holder: ArcGISMapViewHolder, - private val polylineController: ArcGISPolylineController, - private val polygonController: ArcGISPolygonController, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - private val circleLayer: GraphicsOverlay = - GraphicsOverlay().apply { - sceneProperties.surfacePlacement = SurfacePlacement.DrapedFlat - }, - private val circleRendererFactory: CircleRendererFactory = - DefaultArcGISCircleRenderer(), private val markerController: ArcGISMarkerController, -) : BaseMapViewController< - ArcGISActualCircle, - >(), + private val polylineController: ArcGISPolylineOverlayController, + private val polygonController: ArcGISPolygonOverlayController, + private val circleController: ArcGISCircleOverlayController, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : BaseMapViewController(), ArcGISMapViewController { - override fun createCircleOverlayManager(): CircleOverlayManager = - circleRendererFactory.create( - onAdd = circleRenderer::addCircles, - onChange = circleRenderer::changeCircle, - onRemove = circleRenderer::removeCircles, - ) - - override val circleRenderer: CircleRenderer = - ArcGISCircleRenderer( - circleLayer = circleLayer, - holder = holder, - coroutine = coroutine, - ) - - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } - init { holder.map.graphicsOverlays.clear() - holder.map.graphicsOverlays.add(circleLayer) + holder.map.graphicsOverlays.add(circleController.renderer.circleLayer) holder.map.graphicsOverlays.add(polygonController.renderer.polygonLayer) holder.map.graphicsOverlays.add(polylineController.renderer.polylineLayer) holder.map.graphicsOverlays.add(markerController.renderer.markerLayer) setupListeners() } - override fun setupListeners() { + fun setupListeners() { coroutine.launch { holder.map.onSingleTapConfirmed.collect { onMapTap(it) } } @@ -207,14 +179,23 @@ class ArcGISMapViewControllerImpl( return } - val circleEntity = circleOverlayManager.find(touchPosition) - circleEntity?.let { + circleController.find(touchPosition)?.let { circleEntity -> val event = - CircleClickEvent( + CircleEvent( state = circleEntity.state, - position = touchPosition, + clicked = touchPosition, ) - circleClickCallback?.invoke(event) + circleController.clickListener?.invoke(event) + return + } + + polylineController.find(touchPosition)?.let { polylineEntity -> + val event = + PolylineEvent( + state = polylineEntity.state, + clicked = touchPosition, + ) + polylineController.clickListener?.invoke(event) return } @@ -224,7 +205,7 @@ class ArcGISMapViewControllerImpl( state = polygonEntity.state, clicked = touchPosition, ) - polygonClickCallback?.invoke(event) + polygonController.clickListener?.invoke(event) return } @@ -251,9 +232,13 @@ class ArcGISMapViewControllerImpl( override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) + override suspend fun compositionCircles(data: List) = circleController.add(data) - override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) + override suspend fun updateCircle(state: CircleState) = circleController.update(state) + + override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { + this.circleController.clickListener = listener + } override fun moveCamera( dstPosition: MapCameraPosition, @@ -313,6 +298,6 @@ class ArcGISMapViewControllerImpl( } override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { - this.polygonClickCallback = listener + this.polygonController.clickListener = listener } } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt index ac92557b..0b6578ad 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISViewControllerStore.kt @@ -3,11 +3,13 @@ package com.mapconductor.arcgis import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.mapping.view.SceneView import com.arcgismaps.mapping.view.SurfacePlacement +import com.mapconductor.arcgis.circle.ArcGISCircleOverlayController +import com.mapconductor.arcgis.circle.ArcGISCircleOverlayRenderer import com.mapconductor.arcgis.marker.ArcGISMarkerController import com.mapconductor.arcgis.marker.ArcGISMarkerRenderer -import com.mapconductor.arcgis.polygon.ArcGISPolygonController +import com.mapconductor.arcgis.polygon.ArcGISPolygonOverlayController import com.mapconductor.arcgis.polygon.ArcGISPolygonOverlayRenderer -import com.mapconductor.arcgis.polyline.ArcGISPolylineController +import com.mapconductor.arcgis.polyline.ArcGISPolylineOverlayController import com.mapconductor.arcgis.polyline.ArcGISPolylineOverlayRenderer import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewHolder @@ -42,12 +44,32 @@ object ArcGISViewControllerStore : markerController = getMarkerController(holder), polylineController = getPolylineController(holder), polygonController = getPolygonController(holder), + circleController = getCircleController(holder), ) this.set(id, controller) return controller } - private fun getPolylineController(holder: ArcGISMapViewHolder): ArcGISPolylineController { + private fun getCircleController(holder: ArcGISMapViewHolder): ArcGISCircleOverlayController { + val circleLayer: GraphicsOverlay = + GraphicsOverlay().apply { + sceneProperties.surfacePlacement = SurfacePlacement.DrapedFlat + } + + val renderer = + ArcGISCircleOverlayRenderer( + circleLayer = circleLayer, + holder = holder, + ) + + val controller = + ArcGISCircleOverlayController( + renderer = renderer, + ) + return controller + } + + private fun getPolylineController(holder: ArcGISMapViewHolder): ArcGISPolylineOverlayController { val polylineLayer: GraphicsOverlay = GraphicsOverlay().apply { sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded @@ -60,13 +82,13 @@ object ArcGISViewControllerStore : ) val controller = - ArcGISPolylineController( + ArcGISPolylineOverlayController( renderer = renderer, ) return controller } - private fun getPolygonController(holder: ArcGISMapViewHolder): ArcGISPolygonController { + private fun getPolygonController(holder: ArcGISMapViewHolder): ArcGISPolygonOverlayController { val polygonLayer: GraphicsOverlay = GraphicsOverlay().apply { sceneProperties.surfacePlacement = SurfacePlacement.DrapedBillboarded @@ -79,7 +101,7 @@ object ArcGISViewControllerStore : ) val controller = - ArcGISPolygonController( + ArcGISPolygonOverlayController( renderer = renderer, ) return controller diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayController.kt new file mode 100644 index 00000000..151bad11 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.arcgis.circle + +import com.mapconductor.arcgis.ArcGISActualCircle +import com.mapconductor.core.circle.CircleController +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.core.circle.CircleManagerImpl + +class ArcGISCircleOverlayController( + circleManager: CircleManager = CircleManagerImpl(), + override val renderer: ArcGISCircleOverlayRenderer, +) : CircleController(circleManager, renderer) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayRenderer.kt new file mode 100644 index 00000000..2a1b06ab --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleOverlayRenderer.kt @@ -0,0 +1,116 @@ +package com.mapconductor.arcgis.circle + +import com.arcgismaps.geometry.GeodeticCurveType +import com.arcgismaps.geometry.GeometryEngine +import com.arcgismaps.geometry.LinearUnit +import com.arcgismaps.geometry.LinearUnitId +import com.arcgismaps.mapping.symbology.SimpleFillSymbol +import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle +import com.arcgismaps.mapping.symbology.SimpleLineSymbol +import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle +import com.arcgismaps.mapping.view.Graphic +import com.arcgismaps.mapping.view.GraphicsOverlay +import com.mapconductor.arcgis.ArcGISActualCircle +import com.mapconductor.arcgis.ArcGISMapViewHolder +import com.mapconductor.arcgis.toArcGISColor +import com.mapconductor.arcgis.toPoint +import com.mapconductor.core.circle.AbstractCircleOverlayRenderer +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ArcGISCircleOverlayRenderer( + val circleLayer: GraphicsOverlay, + override val holder: ArcGISMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractCircleOverlayRenderer() { + override suspend fun createCircle(state: CircleState): ArcGISActualCircle? = + withContext(coroutine.coroutineContext) { + val spec = + holder.mapView.sceneView.scene + ?.spatialReference + val centerPoint = GeoPoint.from(state.center).toPoint(spec) + val circleGeometry = + GeometryEngine.bufferGeodeticOrNull( + geometry = centerPoint, + distance = state.radiusMeters, + distanceUnit = LinearUnit(LinearUnitId.Meters), + maxDeviation = Double.NaN, + curveType = GeodeticCurveType.NormalSection, + ) + val stroke = + SimpleLineSymbol( + style = SimpleLineSymbolStyle.Solid, + color = state.strokeColor.toArcGISColor(), + width = state.strokeWidth.value, + ) + val fillSymbol = + SimpleFillSymbol( + style = SimpleFillSymbolStyle.Solid, + color = state.fillColor.toArcGISColor(), + outline = stroke, + ) + val circle = Graphic(circleGeometry, fillSymbol) + + circleLayer.graphics.add(circle) + circle + } + + override suspend fun removeCircle(entity: CircleEntity) { + coroutine.launch { + val circles = listOf(entity.circle) + circleLayer.graphics.removeAll(circles) + } + } + + override suspend fun updateCircleProperties( + circle: ArcGISActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): ArcGISActualCircle? = + withContext(coroutine.coroutineContext) { + val spec = + holder.mapView.sceneView.scene + ?.spatialReference + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + val graphic = current.circle + + if (finger.center != prevFinger.center || finger.radiusMeters != prevFinger.radiusMeters) { + val centerPoint = GeoPoint.from(current.state.center).toPoint(spec) + + val newGeometry = + GeometryEngine.bufferGeodeticOrNull( + geometry = centerPoint, + distance = current.state.radiusMeters, + distanceUnit = LinearUnit(LinearUnitId.Meters), + maxDeviation = Double.NaN, + curveType = GeodeticCurveType.NormalSection, + ) + newGeometry?.let { + graphic.geometry = it + } + } + + (graphic.symbol as SimpleFillSymbol).let { symbol -> + if (finger.fillColor != prevFinger.fillColor) { + symbol.color = + current.state.fillColor.toArcGISColor() + } + symbol.outline?.let { outline -> + if (finger.strokeColor != prevFinger.strokeColor) { + outline.color = + current.state.strokeColor.toArcGISColor() + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + outline.width = current.state.strokeWidth.value + } + } + } + return@withContext graphic + } +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleRenderer.kt deleted file mode 100644 index 620dddb2..00000000 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/circle/ArcGISCircleRenderer.kt +++ /dev/null @@ -1,137 +0,0 @@ -package com.mapconductor.arcgis.circle - -import com.arcgismaps.geometry.GeodeticCurveType -import com.arcgismaps.geometry.GeometryEngine -import com.arcgismaps.geometry.LinearUnit -import com.arcgismaps.geometry.LinearUnitId -import com.arcgismaps.mapping.symbology.SimpleFillSymbol -import com.arcgismaps.mapping.symbology.SimpleFillSymbolStyle -import com.arcgismaps.mapping.symbology.SimpleLineSymbol -import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle -import com.arcgismaps.mapping.view.Graphic -import com.arcgismaps.mapping.view.GraphicsOverlay -import com.mapconductor.arcgis.ArcGISMapViewHolder -import com.mapconductor.arcgis.toArcGISColor -import com.mapconductor.arcgis.toPoint -import com.mapconductor.core.circle.AbstractCircleRenderer -import com.mapconductor.core.circle.CircleEntity -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleOverlayManagerImpl -import com.mapconductor.core.circle.CircleRenderer.UpdateParams -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.features.GeoPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultArcGISCircleRenderer : CircleRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): CircleOverlayManager = - CircleOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class ArcGISCircleRenderer( - val circleLayer: GraphicsOverlay, - override val holder: ArcGISMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractCircleRenderer() { - override suspend fun addCircles(newCircles: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newCircles.map { state -> - val spec = - holder.mapView.sceneView.scene - ?.spatialReference - val centerPoint = GeoPoint.from(state.center).toPoint(spec) - val circleGeometry = - GeometryEngine.bufferGeodeticOrNull( - geometry = centerPoint, - distance = state.radiusMeters, - distanceUnit = LinearUnit(LinearUnitId.Meters), - maxDeviation = Double.NaN, - curveType = GeodeticCurveType.NormalSection, - ) - val stroke = - SimpleLineSymbol( - style = SimpleLineSymbolStyle.Solid, - color = state.strokeColor.toArcGISColor(), - width = state.strokeWidth.value, - ) - val fillSymbol = - SimpleFillSymbol( - style = SimpleFillSymbolStyle.Solid, - color = state.fillColor.toArcGISColor(), - outline = stroke, - ) - val circle = Graphic(circleGeometry, fillSymbol) - - circleLayer.graphics.add(circle) - circle - } - } - } - - override suspend fun removeCircles(removeEntities: List>) { - val circles = removeEntities.map { it.circle } - coroutine.launch { - circleLayer.graphics.removeAll(circles) - } - } - - override suspend fun changeCircle(changes: List>): List { - return withContext(coroutine.coroutineContext) { - val spec = - holder.mapView.sceneView.scene - ?.spatialReference - return@withContext changes.map { params -> - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - val graphic = params.entity.circle - - if (finger.center != prevFinger.center || finger.radiusMeters != prevFinger.radiusMeters) { - val centerPoint = GeoPoint.from(params.entity.state.center).toPoint(spec) - - val newGeometry = - GeometryEngine.bufferGeodeticOrNull( - geometry = centerPoint, - distance = params.entity.state.radiusMeters, - distanceUnit = LinearUnit(LinearUnitId.Meters), - maxDeviation = Double.NaN, - curveType = GeodeticCurveType.NormalSection, - ) - newGeometry?.let { - graphic.geometry = it - } - } - - (graphic.symbol as SimpleFillSymbol).let { symbol -> - if (finger.fillColor != prevFinger.fillColor) { - symbol.color = - params.entity.state.fillColor - .toArcGISColor() - } - symbol.outline?.let { outline -> - if (finger.strokeColor != prevFinger.strokeColor) { - outline.color = - params.entity.state.strokeColor - .toArcGISColor() - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - outline.width = params.entity.state.strokeWidth.value - } - } - } - graphic - } - } - } -} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayController.kt new file mode 100644 index 00000000..1519d161 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.arcgis.polygon + +import com.mapconductor.arcgis.ArcGISActualPolygon +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonManagerImpl + +class ArcGISPolygonOverlayController( + polygonManager: PolygonManager = PolygonManagerImpl(), + override val renderer: ArcGISPolygonOverlayRenderer, +) : PolygonController(polygonManager, renderer) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayRenderer.kt similarity index 91% rename from mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt rename to mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayRenderer.kt index 542c92b2..8724d3ab 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polygon/ArcGISPolygonOverlayRenderer.kt @@ -15,10 +15,7 @@ import com.mapconductor.arcgis.toPoint import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer -import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonManager -import com.mapconductor.core.polygon.PolygonManagerImpl import com.mapconductor.core.polygon.PolygonState import kotlin.collections.set import kotlinx.coroutines.CoroutineScope @@ -26,11 +23,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class ArcGISPolygonController( - polygonManager: PolygonManager = PolygonManagerImpl(), - override val renderer: ArcGISPolygonOverlayRenderer, -) : PolygonController(polygonManager, renderer) - class ArcGISPolygonOverlayRenderer( val polygonLayer: GraphicsOverlay, override val holder: ArcGISMapViewHolder, diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayController.kt new file mode 100644 index 00000000..742d0f85 --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.arcgis.polyline + +import com.mapconductor.arcgis.ArcGISActualPolyline +import com.mapconductor.core.polyline.PolylineController +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineManagerImpl + +class ArcGISPolylineOverlayController( + polylineManager: PolylineManager = PolylineManagerImpl(), + override val renderer: ArcGISPolylineOverlayRenderer, +) : PolylineController(polylineManager, renderer) diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayRenderer.kt similarity index 91% rename from mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt rename to mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayRenderer.kt index 1b1c17a8..85799970 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineController.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/polyline/ArcGISPolylineOverlayRenderer.kt @@ -13,22 +13,15 @@ import com.mapconductor.arcgis.toPoint import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer -import com.mapconductor.core.polyline.PolylineController import com.mapconductor.core.polyline.PolylineEntity -import com.mapconductor.core.polyline.PolylineManager -import com.mapconductor.core.polyline.PolylineManagerImpl import com.mapconductor.core.polyline.PolylineState import com.mapconductor.core.spherical.Spherical +import kotlin.collections.set import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class ArcGISPolylineController( - polylineManager: PolylineManager = PolylineManagerImpl(), - override val renderer: ArcGISPolylineOverlayRenderer, -) : PolylineController(polylineManager, renderer) - class ArcGISPolylineOverlayRenderer( val polylineLayer: GraphicsOverlay, override val holder: ArcGISMapViewHolder, diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index 36fde1b3..923be570 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -87,7 +87,7 @@ fun GoogleMapsView( controller.setOnMarkerDragStart(onMarkerDragStart) controller.setOnMarkerDrag(onMarkerDrag) controller.setOnMarkerDragEnd(onMarkerDragEnd) - controller.setCircleClickListener(onCircleClick) + controller.setOnCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) controller.setOnPolygonClickListener(onPolygonClick) controller.setOnMarkerAnimateStart(onMarkerAnimateStart) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt index 992c775b..c671921e 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt @@ -7,17 +7,14 @@ import com.google.android.gms.maps.GoogleMap.OnCameraMoveCanceledListener import com.google.android.gms.maps.GoogleMap.OnCameraMoveListener import com.google.android.gms.maps.GoogleMap.OnCameraMoveStartedListener import com.google.android.gms.maps.GoogleMap.OnMapClickListener -import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.LatLng -import com.mapconductor.core.circle.CircleClickEvent -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRenderer -import com.mapconductor.core.circle.CircleRendererFactory +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.groundimage.GroundImageCapable -import com.mapconductor.core.groundimage.GroundImageController import com.mapconductor.core.groundimage.GroundImageEvent import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.groundimage.OnGroundImageEventHandler @@ -28,26 +25,27 @@ import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.OnPolygonEventHandler import com.mapconductor.core.polygon.PolygonCapable -import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.googlemaps.circle.DefaultGoogleMapCircleRenderer -import com.mapconductor.googlemaps.circle.GoogleMapCircleRenderer +import com.mapconductor.googlemaps.circle.GoogleMapCircleController +import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageController import com.mapconductor.googlemaps.marker.GoogleMapMarkerController +import com.mapconductor.googlemaps.polygon.GoogleMapPolygonController import com.mapconductor.googlemaps.polyline.GoogleMapPolylineController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch interface GoogleMapViewController : - MapViewController, + MapViewController, GroundImageCapable, PolygonCapable, MarkerCapable, - PolylineCapable { + PolylineCapable, + CircleCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -64,38 +62,22 @@ class GoogleMapViewControllerImpl( override val holder: GoogleMapViewHolder, private val markerController: GoogleMapMarkerController, private val polylineController: GoogleMapPolylineController, + private val polygonController: GoogleMapPolygonController, + private val groundImageController: GoogleMapGroundImageController, + private val circleController: GoogleMapCircleController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - private val circleRendererFactory: CircleRendererFactory = DefaultGoogleMapCircleRenderer(), - private val groundImageController: GroundImageController, - private val polygonController: PolygonController, -) : BaseMapViewController(), +) : BaseMapViewController(), GoogleMapViewController, OnCameraMoveStartedListener, OnCameraMoveCanceledListener, OnCameraMoveListener, OnCameraIdleListener, OnMapClickListener { - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } - - override val circleRenderer: CircleRenderer = - GoogleMapCircleRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun createCircleOverlayManager(): CircleOverlayManager = - circleRendererFactory.create( - onAdd = circleRenderer::addCircles, - onChange = circleRenderer::changeCircle, - onRemove = circleRenderer::removeCircles, - ) - init { setupListeners() } - override fun setupListeners() { + fun setupListeners() { holder.map.setOnCameraMoveStartedListener(this) holder.map.setOnCameraMoveCanceledListener(this) holder.map.setOnCameraMoveListener(this) @@ -144,16 +126,20 @@ class GoogleMapViewControllerImpl( groundImageController.clear() polylineController.clear() polygonController.clear() - circleOverlayManager.clearOverlays() + circleController.clear() } override suspend fun compositionMarkers(data: List) = markerController.add(data) override suspend fun updateMarker(state: MarkerState) = markerController.update(state) - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) + override suspend fun compositionCircles(data: List) = circleController.add(data) - override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) + override suspend fun updateCircle(state: CircleState) = circleController.update(state) + + override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { + this.circleController.clickListener = listener + } override suspend fun compositionPolylines(data: List) = polylineController.add(data) @@ -190,13 +176,15 @@ class GoogleMapViewControllerImpl( override fun onMapClick(position: LatLng) { val touchPosition = position.toGeoPoint() - circleOverlayManager.find(touchPosition)?.let { entity -> + circleController.find(touchPosition)?.let { entity -> val event = - CircleClickEvent( + CircleEvent( state = entity.state, - position = touchPosition, + clicked = touchPosition, ) - circleClickCallback?.invoke(event) + coroutine.launch { + circleController.clickListener?.invoke(event) + } return } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt index a0b3cd87..adba5359 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerStore.kt @@ -4,12 +4,14 @@ import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.MapView import com.mapconductor.core.geocell.HexGeocell -import com.mapconductor.core.groundimage.GroundImageController import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.projection.WebMercator -import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageRenderer +import com.mapconductor.googlemaps.circle.GoogleMapCircleController +import com.mapconductor.googlemaps.circle.GoogleMapCircleOverlayRenderer +import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageController +import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageOverlayRenderer import com.mapconductor.googlemaps.marker.GoogleMapMarkerController import com.mapconductor.googlemaps.marker.GoogleMapMarkerRenderer import com.mapconductor.googlemaps.polygon.GoogleMapPolygonController @@ -45,6 +47,7 @@ object GoogleMapViewControllerStore : StaticHolder( groundImageController = getGroundImageController(holder), polylineController = getPolylineController(holder), polygonController = getPolygonController(holder), + circleController = getCircleController(holder), holder = holder, ) this.set(id, controller) @@ -65,6 +68,32 @@ object GoogleMapViewControllerStore : StaticHolder( return controller } + private fun getGroundImageController(holder: GoogleMapViewHolder): GoogleMapGroundImageController { + val renderer = + GoogleMapGroundImageOverlayRenderer( + holder = holder, + ) + + val controller = + GoogleMapGroundImageController( + renderer = renderer, + ) + return controller + } + + private fun getCircleController(holder: GoogleMapViewHolder): GoogleMapCircleController { + val renderer = + GoogleMapCircleOverlayRenderer( + holder = holder, + ) + + val controller = + GoogleMapCircleController( + renderer = renderer, + ) + return controller + } + private fun getPolylineController(holder: GoogleMapViewHolder): GoogleMapPolylineController { val renderer = GoogleMapPolylineOverlayRenderer( @@ -99,22 +128,6 @@ object GoogleMapViewControllerStore : StaticHolder( return markerController } - - private fun getGroundImageController( - holder: GoogleMapViewHolder, - ): GroundImageController { - val groundImageRenderer = - GoogleMapGroundImageRenderer( - holder = holder, - ) - - val groundImageController = - GroundImageController( - renderer = groundImageRenderer, - ) - - return groundImageController - } } internal fun Context.findActivity(): Activity? = diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleController.kt new file mode 100644 index 00000000..013bd86f --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.googlemaps.circle + +import com.mapconductor.core.circle.CircleController +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.core.circle.CircleManagerImpl +import com.mapconductor.googlemaps.GoogleMapActualCircle + +class GoogleMapCircleController( + circleManager: CircleManager = CircleManagerImpl(), + renderer: GoogleMapCircleOverlayRenderer, +) : CircleController(circleManager, renderer) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleOverlayRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleOverlayRenderer.kt new file mode 100644 index 00000000..7ba268f8 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleOverlayRenderer.kt @@ -0,0 +1,72 @@ +package com.mapconductor.googlemaps.circle + +import androidx.compose.ui.graphics.toArgb +import com.google.android.gms.maps.model.CircleOptions +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.circle.AbstractCircleOverlayRenderer +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.googlemaps.GoogleMapActualCircle +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLng +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class GoogleMapCircleOverlayRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractCircleOverlayRenderer() { + override suspend fun createCircle(state: CircleState): GoogleMapActualCircle? = + withContext(coroutine.coroutineContext) { + val center = GeoPoint.from(state.center).toLatLng() + val options = + CircleOptions() + .center(center) + .radius(state.radiusMeters) + .strokeColor(state.strokeColor.toArgb()) + .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) + .fillColor(state.fillColor.toArgb()) + .clickable(false) + holder.map.addCircle(options).also { + it.tag = state.id + } + } + + override suspend fun removeCircle(entity: CircleEntity) { + coroutine.launch { + entity.circle.remove() + } + } + + override suspend fun updateCircleProperties( + circle: GoogleMapActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): GoogleMapActualCircle? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.center != prevFinger.center) { + circle.center = GeoPoint.from(current.state.center).toLatLng() + } + if (finger.radiusMeters != prevFinger.radiusMeters) { + circle.radius = current.state.radiusMeters + } + if (finger.strokeColor != prevFinger.strokeColor) { + circle.strokeColor = + current.state.strokeColor.toArgb() + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + circle.strokeWidth = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + } + if (finger.fillColor != prevFinger.fillColor) { + circle.fillColor = + current.state.fillColor.toArgb() + } + circle + } +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt deleted file mode 100644 index 806e1a31..00000000 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/circle/GoogleMapCircleRenderer.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.mapconductor.googlemaps.circle - -import androidx.compose.ui.graphics.toArgb -import com.google.android.gms.maps.model.Circle -import com.google.android.gms.maps.model.CircleOptions -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.circle.AbstractCircleRenderer -import com.mapconductor.core.circle.CircleEntity -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleOverlayManagerImpl -import com.mapconductor.core.circle.CircleRenderer.UpdateParams -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.googlemaps.GoogleMapViewHolder -import com.mapconductor.googlemaps.toLatLng -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DefaultGoogleMapCircleRenderer : CircleRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): CircleOverlayManager = - CircleOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class GoogleMapCircleRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope, -) : AbstractCircleRenderer() { - override suspend fun addCircles(newCircles: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext newCircles.map { state -> - val center = GeoPoint.from(state.center).toLatLng() - val options = - CircleOptions() - .center(center) - .radius(state.radiusMeters) - .strokeColor(state.strokeColor.toArgb()) - .strokeWidth(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) - .fillColor(state.fillColor.toArgb()) - .clickable(false) - holder.map.addCircle(options).also { - it.tag = state.id - } - } - } - } - - override suspend fun removeCircles(removeEntities: List>) { - coroutine.launch { - removeEntities.forEach { entity -> entity.circle.remove() } - } - } - - override suspend fun changeCircle(changes: List>): List { - return withContext(coroutine.coroutineContext) { - return@withContext changes.map { params -> - val circle = params.entity.circle - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - - if (finger.center != prevFinger.center) { - circle.center = GeoPoint.from(params.entity.state.center).toLatLng() - } - if (finger.radiusMeters != prevFinger.radiusMeters) { - circle.radius = params.entity.state.radiusMeters - } - if (finger.strokeColor != prevFinger.strokeColor) { - circle.strokeColor = - params.entity.state.strokeColor - .toArgb() - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - circle.strokeWidth = ResourceProvider.dpToPx(params.entity.state.strokeWidth).toFloat() - } - if (finger.fillColor != prevFinger.fillColor) { - circle.fillColor = - params.entity.state.fillColor - .toArgb() - } - return@map circle - } - } - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageController.kt new file mode 100644 index 00000000..d3f9bcba --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.googlemaps.groundimage + +import com.mapconductor.core.groundimage.GroundImageController +import com.mapconductor.core.groundimage.GroundImageManager +import com.mapconductor.core.groundimage.GroundImageManagerImpl +import com.mapconductor.googlemaps.GoogleMapActualGroundImage + +class GoogleMapGroundImageController( + groundImageManager: GroundImageManager = GroundImageManagerImpl(), + renderer: GoogleMapGroundImageOverlayRenderer, +) : GroundImageController(groundImageManager, renderer) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageOverlayRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageOverlayRenderer.kt new file mode 100644 index 00000000..b493eeac --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageOverlayRenderer.kt @@ -0,0 +1,65 @@ +package com.mapconductor.googlemaps.groundimage + +import androidx.core.graphics.drawable.toBitmap +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.GroundOverlayOptions +import com.mapconductor.core.groundimage.AbstractGroundImageOverlayRenderer +import com.mapconductor.core.groundimage.GroundImageEntity +import com.mapconductor.core.groundimage.GroundImageState +import com.mapconductor.googlemaps.GoogleMapActualGroundImage +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLngBounds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class GoogleMapGroundImageOverlayRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractGroundImageOverlayRenderer() { + override suspend fun createGroundImage(state: GroundImageState): GoogleMapActualGroundImage? = + withContext(coroutine.coroutineContext) { + val bounds = state.bounds.toLatLngBounds() ?: return@withContext null + val image = BitmapDescriptorFactory.fromBitmap(state.image.toBitmap()) + val opacity = state.opacity + val options = + GroundOverlayOptions() + .image(image) + .positionFromBounds(bounds) + .transparency(1.0f - opacity) + holder.map.addGroundOverlay(options)?.also { + it.tag = state.id + } + } + + override suspend fun removeGroundImage(entity: GroundImageEntity) { + coroutine.launch { + entity.groundImage.remove() + } + } + + override suspend fun updateGroundImageProperties( + groundImage: GoogleMapActualGroundImage, + current: GroundImageEntity, + prev: GroundImageEntity, + ): GoogleMapActualGroundImage? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + if (finger.bounds != prevFinger.bounds) { + current.state.bounds.toLatLngBounds()?.let { + groundImage.setPositionFromBounds(it) + } + } + groundImage.transparency = 1.0f - current.state.opacity + if (finger.image != prevFinger.image) { + val bitmap = + current.state.image + .toBitmap() + val bitmapDesc = BitmapDescriptorFactory.fromBitmap(bitmap) + groundImage.setImage(bitmapDesc) + } + groundImage + } +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt deleted file mode 100644 index d77e82a3..00000000 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/groundimage/GoogleMapGroundImageRenderer.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.mapconductor.googlemaps.groundimage - -import androidx.core.graphics.drawable.toBitmap -import com.google.android.gms.maps.model.BitmapDescriptorFactory -import com.google.android.gms.maps.model.GroundOverlay -import com.google.android.gms.maps.model.GroundOverlayOptions -import com.mapconductor.core.controller.OverlayRenderer -import com.mapconductor.core.groundimage.GroundImageEntity -import com.mapconductor.core.groundimage.GroundImageState -import com.mapconductor.googlemaps.GoogleMapActualGroundImage -import com.mapconductor.googlemaps.GoogleMapViewHolder -import com.mapconductor.googlemaps.toLatLngBounds -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class GoogleMapGroundImageRenderer( - val holder: GoogleMapViewHolder, - val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), -) : OverlayRenderer> { - override suspend fun onAdd(data: List): List { - return withContext(coroutine.coroutineContext) { - return@withContext data.map { state -> - val bounds = state.bounds.toLatLngBounds() ?: return@withContext emptyList() - val image = BitmapDescriptorFactory.fromBitmap(state.image.toBitmap()) - val opacity = state.opacity - val options = - GroundOverlayOptions() - .image(image) - .positionFromBounds(bounds) - .transparency(1.0f - opacity) - holder.map.addGroundOverlay(options)?.also { - it.tag = state.id - } - } - } - } - - override suspend fun onRemove(data: List>) { - coroutine.launch { - data.forEach { params -> params.groundImage.remove() } - } - } - - override suspend fun onPostProcess() { - // Do nothing here - } - - override suspend fun onChange( - data: List>>, - ): List { - return withContext(coroutine.coroutineContext) { - return@withContext data.map { params -> - val groundOverlay = params.current.groundImage - val finger = params.current.fingerPrint - val prevFinger = params.prev.fingerPrint - if (finger.bounds != prevFinger.bounds) { - params.current.state.bounds.toLatLngBounds()?.let { - groundOverlay.setPositionFromBounds(it) - } - } - groundOverlay.transparency = 1.0f - params.current.state.opacity - if (finger.image != prevFinger.image) { - val bitmap = - params.current.state.image - .toBitmap() - val bitmapDesc = BitmapDescriptorFactory.fromBitmap(bitmap) - groundOverlay.setImage(bitmapDesc) - } - return@map groundOverlay - } - } - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonController.kt new file mode 100644 index 00000000..c114fa63 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonController.kt @@ -0,0 +1,11 @@ +package com.mapconductor.googlemaps.polygon + +import com.mapconductor.core.polygon.PolygonController +import com.mapconductor.core.polygon.PolygonManager +import com.mapconductor.core.polygon.PolygonManagerImpl +import com.mapconductor.googlemaps.GoogleMapActualPolygon + +class GoogleMapPolygonController( + polygonManager: PolygonManager = PolygonManagerImpl(), + renderer: GoogleMapPolygonOverlayRenderer, +) : PolygonController(polygonManager, renderer) diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonOverlayRenderer.kt similarity index 87% rename from mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt rename to mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonOverlayRenderer.kt index f0a691d1..0bf3acc7 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonRenderer.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polygon/GoogleMapPolygonOverlayRenderer.kt @@ -5,10 +5,7 @@ import com.google.android.gms.maps.model.PolygonOptions import com.mapconductor.core.ResourceProvider import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer -import com.mapconductor.core.polygon.PolygonController import com.mapconductor.core.polygon.PolygonEntity -import com.mapconductor.core.polygon.PolygonManager -import com.mapconductor.core.polygon.PolygonManagerImpl import com.mapconductor.core.polygon.PolygonState import com.mapconductor.googlemaps.GoogleMapActualPolygon import com.mapconductor.googlemaps.GoogleMapViewHolder @@ -19,11 +16,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class GoogleMapPolygonController( - polygonManager: PolygonManager = PolygonManagerImpl(), - renderer: GoogleMapPolygonOverlayRenderer, -) : PolygonController(polygonManager, renderer) - class GoogleMapPolygonOverlayRenderer( override val holder: GoogleMapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt index a5b7eaa3..8425fc69 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineController.kt @@ -1,81 +1,11 @@ package com.mapconductor.googlemaps.polyline -import androidx.compose.ui.graphics.toArgb -import com.google.android.gms.maps.model.Polyline -import com.google.android.gms.maps.model.PolylineOptions -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer import com.mapconductor.core.polyline.PolylineController -import com.mapconductor.core.polyline.PolylineEntity import com.mapconductor.core.polyline.PolylineManager import com.mapconductor.core.polyline.PolylineManagerImpl -import com.mapconductor.core.polyline.PolylineState import com.mapconductor.googlemaps.GoogleMapActualPolyline -import com.mapconductor.googlemaps.GoogleMapViewHolder -import com.mapconductor.googlemaps.toLatLng -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class GoogleMapPolylineController( polylineManager: PolylineManager = PolylineManagerImpl(), renderer: GoogleMapPolylineOverlayRenderer, ) : PolylineController(polylineManager, renderer) - -class GoogleMapPolylineOverlayRenderer( - override val holder: GoogleMapViewHolder, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), -) : AbstractPolylineOverlayRenderer() { - override suspend fun createPolyline(state: PolylineState): GoogleMapActualPolyline? = - withContext(coroutine.coroutineContext) { - val points = state.points.map { GeoPoint.from(it).toLatLng() } - val options = - PolylineOptions() - .addAll(points) - .color(state.strokeColor.toArgb()) - .width(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) - .geodesic(state.geodesic) - .clickable(true) - - holder.map.addPolyline(options).also { - it.tag = state.id - } - } - - override suspend fun updatePolylineProperties( - polyline: GoogleMapActualPolyline, - current: PolylineEntity, - prev: PolylineEntity, - ): Polyline? = - withContext(coroutine.coroutineContext) { - val finger = current.fingerPrint - val prevFinger = prev.fingerPrint - - if (finger.points != prevFinger.points) { - val points = current.state.points.map { GeoPoint.from(it).toLatLng() } - polyline.points = points - } - - if (finger.geodesic != prevFinger.geodesic) { - polyline.isGeodesic = current.state.geodesic - } - - if (finger.strokeWidth != prevFinger.strokeWidth) { - polyline.width = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() - } - - if (finger.strokeColor != prevFinger.strokeColor) { - polyline.color = current.state.strokeColor.toArgb() - } - - polyline - } - - override suspend fun removePolyline(entity: PolylineEntity) { - coroutine.launch { - entity.polyline.remove() - } - } -} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineOverlayRenderer.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineOverlayRenderer.kt new file mode 100644 index 00000000..7c18076f --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/polyline/GoogleMapPolylineOverlayRenderer.kt @@ -0,0 +1,73 @@ +package com.mapconductor.googlemaps.polyline + +import androidx.compose.ui.graphics.toArgb +import com.google.android.gms.maps.model.Polyline +import com.google.android.gms.maps.model.PolylineOptions +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.googlemaps.GoogleMapActualPolyline +import com.mapconductor.googlemaps.GoogleMapViewHolder +import com.mapconductor.googlemaps.toLatLng +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class GoogleMapPolylineOverlayRenderer( + override val holder: GoogleMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): GoogleMapActualPolyline? = + withContext(coroutine.coroutineContext) { + val points = state.points.map { GeoPoint.from(it).toLatLng() } + val options = + PolylineOptions() + .addAll(points) + .color(state.strokeColor.toArgb()) + .width(ResourceProvider.dpToPx(state.strokeWidth).toFloat()) + .geodesic(state.geodesic) + .clickable(true) + + holder.map.addPolyline(options).also { + it.tag = state.id + } + } + + override suspend fun updatePolylineProperties( + polyline: GoogleMapActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): Polyline? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.points != prevFinger.points) { + val points = current.state.points.map { GeoPoint.from(it).toLatLng() } + polyline.points = points + } + + if (finger.geodesic != prevFinger.geodesic) { + polyline.isGeodesic = current.state.geodesic + } + + if (finger.strokeWidth != prevFinger.strokeWidth) { + polyline.width = ResourceProvider.dpToPx(current.state.strokeWidth).toFloat() + } + + if (finger.strokeColor != prevFinger.strokeColor) { + polyline.color = current.state.strokeColor.toArgb() + } + + polyline + } + + override suspend fun removePolyline(entity: PolylineEntity) { + coroutine.launch { + entity.polyline.remove() + } + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index ed8364c3..6f1fb737 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -79,7 +79,7 @@ fun HereMapView( controller.setOnMarkerDragEnd(onMarkerDragEnd) controller.setOnMarkerAnimateStart(onMarkerAnimateStart) controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) - controller.setCircleClickListener(onCircleClick) + controller.setOnCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) controller.setOnPolygonClickListener(onPolygonClick) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt index 057d4a1d..e4b79cd1 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt @@ -11,14 +11,13 @@ import com.here.sdk.mapview.MapCameraAnimationFactory import com.here.sdk.mapview.MapCameraListener import com.here.sdk.mapview.MapCameraUpdateFactory import com.here.sdk.mapview.MapMeasure -import com.here.sdk.mapview.MapPolygon import com.here.sdk.mapview.MapScene import com.here.sdk.mapview.MapView import com.here.time.Duration -import com.mapconductor.core.circle.CircleClickEvent -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRendererFactory +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint @@ -35,8 +34,7 @@ import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.here.circle.DefaultHereMapCircleRenderer -import com.mapconductor.here.circle.HereMapCircleRenderer +import com.mapconductor.here.circle.HereCircleController import com.mapconductor.here.marker.HereMarkerController import com.mapconductor.here.polygon.HerePolygonController import com.mapconductor.here.polyline.HerePolylineController @@ -45,10 +43,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch interface HereMapViewController : - MapViewController, + MapViewController, MarkerCapable, PolygonCapable, - PolylineCapable { + PolylineCapable, + CircleCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MoveCameraCallback? = null, @@ -65,10 +64,11 @@ class HereMapViewControllerImpl( private val markerController: HereMarkerController, private val polylineController: HerePolylineController, private val polygonController: HerePolygonController, + private val circleController: HereCircleController, override val holder: MapViewHolder, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), - private val circleRendererFactory: CircleRendererFactory = DefaultHereMapCircleRenderer(), -) : BaseMapViewController(), +) : BaseMapViewController(), + CircleCapable, HereMapViewController, MapCameraListener, TapListener, @@ -77,27 +77,11 @@ class HereMapViewControllerImpl( private const val ZOOM_ADJUST_VALUE = 0.1 // バイナリテストで確定 } - override val circleRenderer = - HereMapCircleRenderer( - holder = holder, - coroutine = coroutine, - ) - - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } - - override fun createCircleOverlayManager(): CircleOverlayManager = - circleRendererFactory.create( - onAdd = circleRenderer::addCircles, - onChange = circleRenderer::changeCircle, - onRemove = circleRenderer::removeCircles, - ) - override suspend fun clearOverlays() { markerController.clear() polylineController.clear() polygonController.clear() - circleOverlayManager.clearOverlays() + circleController.clear() } override suspend fun compositionMarkers(data: List) = markerController.add(data) @@ -128,9 +112,13 @@ class HereMapViewControllerImpl( markerController.clickListener = listener } - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) + override suspend fun compositionCircles(data: List) = circleController.add(data) - override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) + override suspend fun updateCircle(state: CircleState) = circleController.update(state) + + override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { + this.circleController.clickListener = listener + } override suspend fun compositionPolylines(data: List) = polylineController.add(data) @@ -142,10 +130,9 @@ class HereMapViewControllerImpl( init { setupListeners() - circleRenderer.init(circleOverlayManager) } - override fun setupListeners() { + fun setupListeners() { holder.mapView.camera.removeListener(this) holder.mapView.camera.addListener(this) holder.mapView.gestures.tapListener = this @@ -222,13 +209,13 @@ class HereMapViewControllerImpl( return } - circleOverlayManager.find(touchPosition)?.let { entity -> + circleController.find(touchPosition)?.let { entity -> val event = - CircleClickEvent( + CircleEvent( state = entity.state, - position = touchPosition, + clicked = touchPosition, ) - circleClickCallback?.invoke(event) + circleController.clickListener?.invoke(event) return } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt index b9c33495..aa4efbe7 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerStore.kt @@ -1,5 +1,6 @@ package com.mapconductor.here +import HerePolygonOverlayRenderer import com.here.sdk.core.engine.AuthenticationMode import com.here.sdk.core.engine.SDKNativeEngine import com.here.sdk.core.engine.SDKOptions @@ -10,10 +11,11 @@ import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.StaticHolder import com.mapconductor.core.marker.MarkerManager import com.mapconductor.core.projection.WebMercator +import com.mapconductor.here.circle.HereCircleController +import com.mapconductor.here.circle.HereCircleOverlayRenderer import com.mapconductor.here.marker.HereMarkerController import com.mapconductor.here.marker.HereMarkerRenderer import com.mapconductor.here.polygon.HerePolygonController -import com.mapconductor.here.polygon.HerePolygonOverlayRenderer import com.mapconductor.here.polyline.HerePolylineController import com.mapconductor.here.polyline.HerePolylineOverlayRenderer import android.content.Context @@ -93,6 +95,7 @@ object HereMapViewControllerStore : StaticHolder() { markerController = getMarkerController(holder), polylineController = getPolylineController(holder), polygonController = getPolygonController(holder), + circleController = getHereCircleController(holder), ) this.set(id, controller) return controller @@ -132,6 +135,19 @@ object HereMapViewControllerStore : StaticHolder() { return controller } + private fun getHereCircleController(holder: HereViewHolder): HereCircleController { + val renderer = + HereCircleOverlayRenderer( + holder = holder, + ) + + val controller = + HereCircleController( + renderer = renderer, + ) + return controller + } + private fun getPolygonController(holder: HereViewHolder): HerePolygonController { val renderer = HerePolygonOverlayRenderer( diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleController.kt new file mode 100644 index 00000000..09342ef9 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleController.kt @@ -0,0 +1,10 @@ +package com.mapconductor.here.circle + +import com.mapconductor.core.circle.CircleController +import com.mapconductor.core.circle.CircleManagerImpl +import com.mapconductor.here.HereActualCircle + +class HereCircleController( + circleManager: CircleManagerImpl = CircleManagerImpl(), + renderer: HereCircleOverlayRenderer, +) : CircleController(circleManager, renderer) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleOverlayRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleOverlayRenderer.kt new file mode 100644 index 00000000..ec263a65 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleOverlayRenderer.kt @@ -0,0 +1,147 @@ +package com.mapconductor.here.circle + +import androidx.compose.ui.graphics.toArgb +import com.here.sdk.core.Color +import com.here.sdk.core.GeoCircle +import com.here.sdk.core.GeoCoordinates +import com.here.sdk.core.GeoPolygon +import com.here.sdk.mapview.MapPolygon +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.circle.AbstractCircleOverlayRenderer +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.here.HereActualCircle +import com.mapconductor.here.HereViewHolder +import com.mapconductor.here.toGeoCoordinates +import kotlin.math.cos +import kotlin.math.sin +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class HereCircleOverlayRenderer( + override val holder: HereViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractCircleOverlayRenderer() { + override suspend fun createCircle(state: CircleState): HereActualCircle? { + val geoCircle = createCirclePolygon(state) + val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) + val mapCircle = + MapPolygon( + geoCircle, + Color.valueOf(state.fillColor.toArgb()), + Color.valueOf(state.strokeColor.toArgb()), + lineWidth, + ) + coroutine.launch { + holder.map.addMapPolygon(mapCircle) + } + return mapCircle + } + + override suspend fun removeCircle(entity: CircleEntity) { + coroutine.launch { + holder.map.removeMapPolygon(entity.circle) + } + } + + override suspend fun updateCircleProperties( + circle: HereActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): HereActualCircle? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + // Update geometry if center or radius changed + if (finger.center != prevFinger.center || finger.radiusMeters != prevFinger.radiusMeters) { + val geoCircle = createCirclePolygon(current.state) + current.circle.geometry = geoCircle + } + + // Update stroke color + if (finger.strokeColor != prevFinger.strokeColor) { + current.circle.outlineColor = + Color.valueOf( + current.state.strokeColor + .toArgb(), + ) + } + + // Update stroke width + if (finger.strokeWidth != prevFinger.strokeWidth) { + val lineWidth = + ResourceProvider.dpToPx( + current.state.strokeWidth.value + .toDouble(), + ) + current.circle.outlineWidth = lineWidth + } + + // Update fill color + if (finger.fillColor != prevFinger.fillColor) { + current.circle.fillColor = + Color.valueOf( + current.state.fillColor + .toArgb(), + ) + } + current.circle.outlineWidth = + current.state.strokeWidth.value + .toDouble() + + circle + } + + /** + * Creates a circle that approximates a circle by generating points around the circumference + */ + private fun createCirclePolygon(state: CircleState): GeoPolygon { + val center = GeoPoint.from(state.center).toGeoCoordinates() + // val radiusMeters = state.radiusMeters + +// val points = mutableListOf() +// +// // Generate points around the circle +// for (i in 0 until CIRCLE_POINT_COUNT) { +// val angle = 2.0 * PI * i / CIRCLE_POINT_COUNT +// val point = calculateCirclePoint(center, radiusMeters, angle) +// points.add(point) +// } +// +// // Close the circle by adding the first point at the end +// if (points.isNotEmpty()) { +// points.add(points.first()) +// } + val geoCircle = GeoCircle(center, state.radiusMeters) + val geoPolygon = GeoPolygon(geoCircle) + + return geoPolygon + } + + /** + * Calculate a point on a circle given center, radius and angle + * Uses approximate conversion from meters to degrees for small circles + */ + private fun calculateCirclePoint( + center: GeoCoordinates, + radiusMeters: Double, + angleRadians: Double, + ): GeoCoordinates { + // Approximate conversion: 1 degree latitude ≈ 111,320 meters + // Longitude conversion varies by latitude, use cosine correction + val latDegrees = radiusMeters / 111320.0 + val lonDegrees = radiusMeters / (111320.0 * cos(Math.toRadians(center.latitude))) + + val deltaLat = latDegrees * cos(angleRadians) + val deltaLon = lonDegrees * sin(angleRadians) + + return GeoCoordinates( + center.latitude + deltaLat, + center.longitude + deltaLon, + ) + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleRenderer.kt deleted file mode 100644 index d407b62b..00000000 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/circle/HereCircleRenderer.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.mapconductor.here.circle - -import androidx.compose.ui.graphics.toArgb -import com.here.sdk.core.Color -import com.here.sdk.core.GeoCoordinates -import com.here.sdk.core.GeoPolygon -import com.here.sdk.mapview.MapPolygon -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.circle.AbstractCircleRenderer -import com.mapconductor.core.circle.CircleEntity -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleOverlayManagerImpl -import com.mapconductor.core.circle.CircleRenderer.UpdateParams -import com.mapconductor.core.circle.CircleRendererFactory -import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.here.HereViewHolder -import com.mapconductor.here.toGeoCoordinates -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class DefaultHereMapCircleRenderer : CircleRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): CircleOverlayManager = - CircleOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - -class HereMapCircleRenderer( - override val holder: HereViewHolder, - override val coroutine: CoroutineScope, -) : AbstractCircleRenderer() { - companion object { - // Number of points to approximate a circle - more points = smoother circle - private const val CIRCLE_POINT_COUNT = 64 - } - - override suspend fun addCircles(newCircles: List): List { - val polygons = - newCircles.map { state -> - val geoPolygon = createCirclePolygon(state) - val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) - MapPolygon( - geoPolygon, - Color.valueOf(state.fillColor.toArgb()), - Color.valueOf(state.strokeColor.toArgb()), - lineWidth, - ) - } - coroutine.launch { - polygons.forEach { holder.map.addMapPolygon(it) } - } - return polygons - } - - override suspend fun removeCircles(removeEntities: List>) { - coroutine.launch { - removeEntities.forEach { holder.map.removeMapPolygon(it.circle) } - } - } - - override suspend fun changeCircle(changes: List>): List { - return changes.map { params -> - val finger = params.entity.fingerPrint - val prevFinger = params.prevEntity.fingerPrint - - // Update geometry if center or radius changed - if (finger.center != prevFinger.center || finger.radiusMeters != prevFinger.radiusMeters) { - val geoPolygon = createCirclePolygon(params.entity.state) - params.entity.circle.geometry = geoPolygon - } - - // Update stroke color - if (finger.strokeColor != prevFinger.strokeColor) { - params.entity.circle.outlineColor = - Color.valueOf( - params.entity.state.strokeColor - .toArgb(), - ) - } - - // Update stroke width - if (finger.strokeWidth != prevFinger.strokeWidth) { - val lineWidth = - ResourceProvider.dpToPx( - params.entity.state.strokeWidth.value - .toDouble(), - ) - params.entity.circle.outlineWidth = lineWidth - } - - // Update fill color - if (finger.fillColor != prevFinger.fillColor) { - params.entity.circle.fillColor = - Color.valueOf( - params.entity.state.fillColor - .toArgb(), - ) - } - - return@map params.entity.circle - } - } - - /** - * Creates a polygon that approximates a circle by generating points around the circumference - */ - private fun createCirclePolygon(state: CircleState): GeoPolygon { - val center = GeoPoint.from(state.center).toGeoCoordinates() - val radiusMeters = state.radiusMeters - - val points = mutableListOf() - - // Generate points around the circle - for (i in 0 until CIRCLE_POINT_COUNT) { - val angle = 2.0 * PI * i / CIRCLE_POINT_COUNT - val point = calculateCirclePoint(center, radiusMeters, angle) - points.add(point) - } - - // Close the polygon by adding the first point at the end - if (points.isNotEmpty()) { - points.add(points.first()) - } - - return GeoPolygon(points) - } - - /** - * Calculate a point on a circle given center, radius and angle - * Uses approximate conversion from meters to degrees for small circles - */ - private fun calculateCirclePoint( - center: GeoCoordinates, - radiusMeters: Double, - angleRadians: Double, - ): GeoCoordinates { - // Approximate conversion: 1 degree latitude ≈ 111,320 meters - // Longitude conversion varies by latitude, use cosine correction - val latDegrees = radiusMeters / 111320.0 - val lonDegrees = radiusMeters / (111320.0 * cos(Math.toRadians(center.latitude))) - - val deltaLat = latDegrees * cos(angleRadians) - val deltaLon = lonDegrees * sin(angleRadians) - - return GeoCoordinates( - center.latitude + deltaLat, - center.longitude + deltaLon, - ) - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt new file mode 100644 index 00000000..bbeed14f --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonController.kt @@ -0,0 +1,91 @@ +import androidx.compose.ui.graphics.toArgb +import com.here.sdk.core.Color +import com.here.sdk.core.GeoPolygon +import com.here.sdk.mapview.MapPolygon +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer +import com.mapconductor.core.polygon.PolygonEntity +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.here.HereActualPolygon +import com.mapconductor.here.HereViewHolder +import com.mapconductor.here.toGeoCoordinates +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class HerePolygonOverlayRenderer( + override val holder: HereViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolygonOverlayRenderer() { + override suspend fun removePolygon(entity: PolygonEntity) { + coroutine.launch { + holder.map.removeMapPolygon(entity.polygon) + } + } + + override suspend fun createPolygon(state: PolygonState): HereActualPolygon? { + val geoPolygon = createGeoPolygon(state) + val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) + val mapPolygon = + MapPolygon( + geoPolygon, + Color.valueOf(state.fillColor.toArgb()), + Color.valueOf(state.strokeColor.toArgb()), + lineWidth, + ) + coroutine.launch { + holder.map.addMapPolygon(mapPolygon) + } + return mapPolygon + } + + override suspend fun updatePolygonProperties( + polygon: HereActualPolygon, + current: PolygonEntity, + prev: PolygonEntity, + ): HereActualPolygon? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + if (finger.points != prevFinger.points) { + val geoPolygon = createGeoPolygon(current.state) + current.polygon.geometry = geoPolygon + } + if (finger.strokeColor != prevFinger.strokeColor) { + current.polygon.outlineColor = + Color.valueOf( + current.state.strokeColor.toArgb(), + ) + } + if (finger.strokeWidth != prevFinger.strokeWidth) { + val lineWidth = + ResourceProvider.dpToPx( + current.state.strokeWidth.value + .toDouble(), + ) + current.polygon.outlineWidth = lineWidth + } + if (finger.fillColor != prevFinger.fillColor) { + current.polygon.fillColor = + Color.valueOf( + current.state.fillColor.toArgb(), + ) + } + polygon + } + + private fun createGeoPolygon(state: PolygonState): GeoPolygon { + val points = state.points.map { GeoPoint.from(it).toGeoCoordinates() } + // Ensure the polygon is closed by adding the first point at the end if not already closed + val closedPoints = + if (points.first() != points.last()) { + points + points.first() + } else { + points + } + return GeoPolygon(closedPoints) + } +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt index 48589818..e34963bf 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polygon/HerePolygonRenderer.kt @@ -1,101 +1,12 @@ package com.mapconductor.here.polygon -import androidx.compose.ui.graphics.toArgb -import com.here.sdk.core.Color -import com.here.sdk.core.GeoPolygon -import com.here.sdk.mapview.MapPolygon -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.polygon.AbstractPolygonOverlayRenderer +import HerePolygonOverlayRenderer import com.mapconductor.core.polygon.PolygonController -import com.mapconductor.core.polygon.PolygonEntity import com.mapconductor.core.polygon.PolygonManager import com.mapconductor.core.polygon.PolygonManagerImpl -import com.mapconductor.core.polygon.PolygonState import com.mapconductor.here.HereActualPolygon -import com.mapconductor.here.HereViewHolder -import com.mapconductor.here.toGeoCoordinates -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class HerePolygonController( polygonManager: PolygonManager = PolygonManagerImpl(), renderer: HerePolygonOverlayRenderer, ) : PolygonController(polygonManager, renderer) - -class HerePolygonOverlayRenderer( - override val holder: HereViewHolder, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), -) : AbstractPolygonOverlayRenderer() { - override suspend fun removePolygon(entity: PolygonEntity) { - coroutine.launch { - holder.map.removeMapPolygon(entity.polygon) - } - } - - override suspend fun createPolygon(state: PolygonState): HereActualPolygon? { - val geoPolygon = createGeoPolygon(state) - val lineWidth = ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()) - val mapPolygon = - MapPolygon( - geoPolygon, - Color.valueOf(state.fillColor.toArgb()), - Color.valueOf(state.strokeColor.toArgb()), - lineWidth, - ) - coroutine.launch { - holder.map.addMapPolygon(mapPolygon) - } - return mapPolygon - } - - override suspend fun updatePolygonProperties( - polygon: HereActualPolygon, - current: PolygonEntity, - prev: PolygonEntity, - ): HereActualPolygon? = - withContext(coroutine.coroutineContext) { - val finger = current.fingerPrint - val prevFinger = prev.fingerPrint - - if (finger.points != prevFinger.points) { - val geoPolygon = createGeoPolygon(current.state) - current.polygon.geometry = geoPolygon - } - if (finger.strokeColor != prevFinger.strokeColor) { - current.polygon.outlineColor = - Color.valueOf( - current.state.strokeColor.toArgb(), - ) - } - if (finger.strokeWidth != prevFinger.strokeWidth) { - val lineWidth = - ResourceProvider.dpToPx( - current.state.strokeWidth.value - .toDouble(), - ) - current.polygon.outlineWidth = lineWidth - } - if (finger.fillColor != prevFinger.fillColor) { - current.polygon.fillColor = - Color.valueOf( - current.state.fillColor.toArgb(), - ) - } - polygon - } - - private fun createGeoPolygon(state: PolygonState): GeoPolygon { - val points = state.points.map { GeoPoint.from(it).toGeoCoordinates() } - // Ensure the polygon is closed by adding the first point at the end if not already closed - val closedPoints = - if (points.first() != points.last()) { - points + points.first() - } else { - points - } - return GeoPolygon(closedPoints) - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt index 1c255e21..931eb0ac 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineController.kt @@ -1,109 +1,11 @@ package com.mapconductor.here.polyline -import androidx.compose.ui.graphics.toArgb -import com.here.sdk.core.Color -import com.here.sdk.core.GeoPolyline -import com.here.sdk.mapview.LineCap -import com.here.sdk.mapview.MapMeasureDependentRenderSize -import com.here.sdk.mapview.MapPolyline -import com.here.sdk.mapview.RenderSize -import com.mapconductor.core.ResourceProvider -import com.mapconductor.core.createInterpolatePoints -import com.mapconductor.core.createLinearInterpolatePoints -import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.IGeoPoint -import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer import com.mapconductor.core.polyline.PolylineController -import com.mapconductor.core.polyline.PolylineEntity import com.mapconductor.core.polyline.PolylineManager import com.mapconductor.core.polyline.PolylineManagerImpl -import com.mapconductor.core.polyline.PolylineState import com.mapconductor.here.HereActualPolyline -import com.mapconductor.here.HereViewHolder -import com.mapconductor.here.toGeoCoordinates -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class HerePolylineController( polylineManager: PolylineManager = PolylineManagerImpl(), renderer: HerePolylineOverlayRenderer, ) : PolylineController(polylineManager, renderer) - -class HerePolylineOverlayRenderer( - override val holder: HereViewHolder, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), -) : AbstractPolylineOverlayRenderer() { - override suspend fun createPolyline(state: PolylineState): HereActualPolyline? { - val geoPolyline = createGeoPolyline(state) - val representation = createRepresentation(state) - val mapPolyline = MapPolyline(geoPolyline, representation) - - coroutine.launch { - holder.map.addMapPolylines(listOf(mapPolyline)) - } - - return mapPolyline - } - - override suspend fun updatePolylineProperties( - polyline: HereActualPolyline, - current: PolylineEntity, - prev: PolylineEntity, - ): HereActualPolyline? = - withContext(coroutine.coroutineContext) { - val finger = current.fingerPrint - val prevFinger = prev.fingerPrint - - var needsReAdd = false - - if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { - val geoPolyline = createGeoPolyline(current.state) - polyline.geometry = geoPolyline - needsReAdd = true - } - - if (finger.strokeColor != prevFinger.strokeColor || finger.strokeWidth != prevFinger.strokeWidth) { - val representation = createRepresentation(current.state) - polyline.setRepresentation(representation) - needsReAdd = true - } - - if (needsReAdd) { - coroutine.launch { - holder.map.removeMapPolylines(listOf(polyline)) - holder.map.addMapPolylines(listOf(polyline)) - } - } - - polyline - } - - override suspend fun removePolyline(entity: PolylineEntity) { - coroutine.launch { - holder.map.removeMapPolylines(listOf(entity.polyline)) - } - } - - private fun createGeoPolyline(state: PolylineState): GeoPolyline { - val geoPoints: List = - when (state.geodesic) { - true -> createInterpolatePoints(state.points) - false -> createLinearInterpolatePoints(state.points) - } - val points = geoPoints.map { GeoPoint.from(it).toGeoCoordinates() } - return GeoPolyline(points) - } - - private fun createRepresentation(state: PolylineState): MapPolyline.Representation { - val lineWidth = - MapMeasureDependentRenderSize( - RenderSize.Unit.PIXELS, - ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()), - ) - val lineColor = Color.valueOf(state.strokeColor.toArgb()) - val lineCap = LineCap.SQUARE - return MapPolyline.SolidRepresentation(lineWidth, lineColor, lineCap) - } -} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineOverlayRenderer.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineOverlayRenderer.kt new file mode 100644 index 00000000..e61844e6 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/polyline/HerePolylineOverlayRenderer.kt @@ -0,0 +1,101 @@ +package com.mapconductor.here.polyline + +import androidx.compose.ui.graphics.toArgb +import com.here.sdk.core.Color +import com.here.sdk.core.GeoPolyline +import com.here.sdk.mapview.LineCap +import com.here.sdk.mapview.MapMeasureDependentRenderSize +import com.here.sdk.mapview.MapPolyline +import com.here.sdk.mapview.RenderSize +import com.mapconductor.core.ResourceProvider +import com.mapconductor.core.createInterpolatePoints +import com.mapconductor.core.createLinearInterpolatePoints +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.features.IGeoPoint +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.here.HereActualPolyline +import com.mapconductor.here.HereViewHolder +import com.mapconductor.here.toGeoCoordinates +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class HerePolylineOverlayRenderer( + override val holder: HereViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): HereActualPolyline? { + val geoPolyline = createGeoPolyline(state) + val representation = createRepresentation(state) + val mapPolyline = MapPolyline(geoPolyline, representation) + + coroutine.launch { + holder.map.addMapPolylines(listOf(mapPolyline)) + } + + return mapPolyline + } + + override suspend fun updatePolylineProperties( + polyline: HereActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): HereActualPolyline? = + withContext(coroutine.coroutineContext) { + val finger = current.fingerPrint + val prevFinger = prev.fingerPrint + + var needsReAdd = false + + if (finger.points != prevFinger.points || finger.geodesic != prevFinger.geodesic) { + val geoPolyline = createGeoPolyline(current.state) + polyline.geometry = geoPolyline + needsReAdd = true + } + + if (finger.strokeColor != prevFinger.strokeColor || finger.strokeWidth != prevFinger.strokeWidth) { + val representation = createRepresentation(current.state) + polyline.setRepresentation(representation) + needsReAdd = true + } + + if (needsReAdd) { + coroutine.launch { + holder.map.removeMapPolylines(listOf(polyline)) + holder.map.addMapPolylines(listOf(polyline)) + } + } + + polyline + } + + override suspend fun removePolyline(entity: PolylineEntity) { + coroutine.launch { + holder.map.removeMapPolylines(listOf(entity.polyline)) + } + } + + private fun createGeoPolyline(state: PolylineState): GeoPolyline { + val geoPoints: List = + when (state.geodesic) { + true -> createInterpolatePoints(state.points) + false -> createLinearInterpolatePoints(state.points) + } + val points = geoPoints.map { GeoPoint.from(it).toGeoCoordinates() } + return GeoPolyline(points) + } + + private fun createRepresentation(state: PolylineState): MapPolyline.Representation { + val lineWidth = + MapMeasureDependentRenderSize( + RenderSize.Unit.PIXELS, + ResourceProvider.dpToPx(state.strokeWidth.value.toDouble()), + ) + val lineColor = Color.valueOf(state.strokeColor.toArgb()) + val lineCap = LineCap.SQUARE + return MapPolyline.SolidRepresentation(lineWidth, lineColor, lineCap) + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index b7621100..adf22ccb 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import com.mapbox.maps.MapInitOptions +import com.mapconductor.core.circle.CircleManagerImpl import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.geocell.HexGeocell import com.mapconductor.core.map.MapViewBase @@ -17,8 +18,11 @@ import com.mapconductor.core.polygon.PolygonManagerImpl import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineManagerImpl import com.mapconductor.core.projection.WebMercator +import com.mapconductor.mapbox.circle.MapboxCircleController +import com.mapconductor.mapbox.circle.MapboxCircleLayer +import com.mapconductor.mapbox.circle.MapboxCircleOverlayRenderer import com.mapconductor.mapbox.marker.MapboxMarkerController -import com.mapconductor.mapbox.marker.MapboxMarkerRenderer +import com.mapconductor.mapbox.marker.MapboxMarkerOverlayRenderer import com.mapconductor.mapbox.polygon.MapboxPolygonConductor import com.mapconductor.mapbox.polygon.MapboxPolygonLayer import com.mapconductor.mapbox.polygon.MapboxPolygonOverlayRenderer @@ -82,13 +86,14 @@ fun MapboxMapView( markerController = getMarkerController(holder), polylineController = getPolylineController(holder), polygonController = getPolygonController(holder), + circleController = getCircleController(holder), ) (state as? MapboxViewStateImpl)?.let { mapViewState -> mapViewState.controller = controller controller.setCameraMoveListener(mapViewState::onCameraChange) } controller.setMapClickListener(onMapClick) - controller.setCircleClickListener(onCircleClick) + controller.setOnCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) controller.setOnPolygonClickListener(onPolygonClick) controller.setOnMarkerClickListener(onMarkerClick) @@ -144,6 +149,28 @@ internal fun getPolygonController(holder: MapboxMapViewHolder): MapboxPolygonCon return conductor } +internal fun getCircleController(holder: MapboxMapViewHolder): MapboxCircleController { + val circleLayer: MapboxCircleLayer = + MapboxCircleLayer( + sourceId = "circle-source", + layerId = "circle-layer", + ) + val circleManager = CircleManagerImpl() + + val renderer = + MapboxCircleOverlayRenderer( + layer = circleLayer, + circleManager = circleManager, + holder = holder, + ) + + val controller = + MapboxCircleController( + renderer = renderer, + ) + return controller +} + internal fun getPolylineController(holder: MapboxMapViewHolder): MapboxPolylineController { val polylineLayer: MapboxPolylineLayer = MapboxPolylineLayer( @@ -175,7 +202,7 @@ internal fun getMarkerController(holder: MapboxMapViewHolder): MapboxMarkerContr val manager = MarkerManager(hexGeocell) val renderer = - MapboxMarkerRenderer( + MapboxMarkerOverlayRenderer( holder = holder, markerManager = manager, ) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt index eefe403c..f3419ad9 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt @@ -21,10 +21,10 @@ import com.mapbox.maps.plugin.gestures.addOnMoveListener import com.mapbox.maps.plugin.gestures.removeOnMapClickListener import com.mapbox.maps.plugin.gestures.removeOnMapLongClickListener import com.mapbox.maps.plugin.gestures.removeOnMoveListener -import com.mapconductor.core.circle.CircleClickEvent -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleRendererFactory +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.controller.BaseMapViewController import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapCameraPosition @@ -39,9 +39,7 @@ import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.mapbox.circle.DefaultMapboxCircleRenderer -import com.mapconductor.mapbox.circle.MapboxCircleLayer -import com.mapconductor.mapbox.circle.MapboxCircleRenderer +import com.mapconductor.mapbox.circle.MapboxCircleController import com.mapconductor.mapbox.marker.MapboxMarkerController import com.mapconductor.mapbox.polygon.MapboxPolygonConductor import com.mapconductor.mapbox.polyline.MapboxPolylineController @@ -51,12 +49,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch interface MapboxMapViewController : - MapViewController< - MapboxActualCircle, - >, + MapViewController, MarkerCapable, PolylineCapable, - PolygonCapable { + PolygonCapable, + CircleCapable { fun moveCamera( dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback? = null, @@ -74,17 +71,9 @@ internal class MapboxMapViewControllerImpl( private val markerController: MapboxMarkerController, private val polylineController: MapboxPolylineController, private val polygonController: MapboxPolygonConductor, + private val circleController: MapboxCircleController, override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), - private val circleLayer: MapboxCircleLayer = - MapboxCircleLayer( - sourceId = "circle-source", - layerId = "circle-layer", - ), - private val circleRendererFactory: CircleRendererFactory = - DefaultMapboxCircleRenderer(), -) : BaseMapViewController< - MapboxActualCircle, - >(), +) : BaseMapViewController(), MapboxMapViewController, CameraChangedCallback, OnMapClickListener, @@ -94,29 +83,11 @@ internal class MapboxMapViewControllerImpl( private const val ZOOM_ADJUST_VALUE = 1.0 } - override fun createCircleOverlayManager(): CircleOverlayManager = - circleRendererFactory.create( - onAdd = circleRenderer::addCircles, - onChange = circleRenderer::changeCircle, - onRemove = circleRenderer::removeCircles, - onPostProcess = circleRenderer::redraw, - ) - - override val circleRenderer: MapboxCircleRenderer = - MapboxCircleRenderer( - holder = holder, - coroutine = coroutine, - layer = circleLayer, - ) - - override fun onCircleOverlayManagerInitialized(overlayManager: CircleOverlayManager) { - } - init { holder.map.getStyle { style -> // Circle - style.addSource(circleLayer.source) - style.addLayer(circleLayer.layer) + style.addSource(circleController.renderer.layer.source) + style.addLayer(circleController.renderer.layer.layer) // Polygon style.addSource(polygonController.polygonOverlay.layer.source) @@ -137,7 +108,7 @@ internal class MapboxMapViewControllerImpl( setupListeners() } - override fun setupListeners() { + fun setupListeners() { holder.map.subscribeCameraChanged(this) holder.map.removeOnMapClickListener(this) holder.map.addOnMapClickListener(this) @@ -167,9 +138,13 @@ internal class MapboxMapViewControllerImpl( override suspend fun updatePolygon(state: PolygonState) = polygonController.update(state) - override suspend fun addCircles(data: List) = circleOverlayManager.addCircles(data) + override suspend fun compositionCircles(data: List) = circleController.add(data) + + override suspend fun updateCircle(state: CircleState) = circleController.update(state) - override suspend fun updateCircle(state: CircleState) = circleOverlayManager.updateCircle(state) + override fun setOnCircleClickListener(listener: OnCircleEventHandler?) { + this.circleController.clickListener = listener + } override fun run(cameraChanged: CameraChanged) { cameraMoveCallback?.let { @@ -277,14 +252,13 @@ internal class MapboxMapViewControllerImpl( return true } - val circleEntity = this.circleOverlayManager.find(touchPosition) - circleEntity?.let { + circleController.find(touchPosition)?.let { entity -> val event = - CircleClickEvent( - state = circleEntity.state, - position = touchPosition, + CircleEvent( + state = entity.state, + clicked = touchPosition, ) - circleClickCallback?.invoke(event) + circleController.clickListener?.invoke(event) return true } @@ -294,7 +268,7 @@ internal class MapboxMapViewControllerImpl( state = polygonEntity.state, clicked = touchPosition, ) - polygonClickCallback?.invoke(event) + polygonController.clickListener?.invoke(event) return true } @@ -302,14 +276,6 @@ internal class MapboxMapViewControllerImpl( return true } -// override fun drawPolyline(geoPoints: List) { -// val points = -// geoPoints.map { -// GeoPoint.from(it).toPoint() -// } -// lineSource.geometry(LineString.fromLngLats(points)) -// } - override fun onMove(detector: MoveGestureDetector): Boolean { markerController.renderer.dragLayer.selected?.let { entity -> @@ -378,6 +344,6 @@ internal class MapboxMapViewControllerImpl( } override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { - this.polygonClickCallback = listener + polygonController.clickListener = listener } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleController.kt new file mode 100644 index 00000000..a1e1fdef --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleController.kt @@ -0,0 +1,10 @@ +package com.mapconductor.mapbox.circle + +import com.mapconductor.core.circle.CircleController +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.mapbox.MapboxActualCircle + +class MapboxCircleController( + override val renderer: MapboxCircleOverlayRenderer, + circleManager: CircleManager = renderer.circleManager, +) : CircleController(circleManager, renderer) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt new file mode 100644 index 00000000..30ca64a2 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleOverlayRenderer.kt @@ -0,0 +1,85 @@ +package com.mapconductor.mapbox.circle + +import com.google.gson.JsonObject +import com.mapbox.geojson.Feature +import com.mapbox.geojson.Point +import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures +import com.mapconductor.core.circle.AbstractCircleOverlayRenderer +import com.mapconductor.core.circle.CircleEntity +import com.mapconductor.core.circle.CircleManager +import com.mapconductor.core.circle.CircleState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.mapbox.MapboxActualCircle +import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.toMapboxColorString +import com.mapconductor.mapbox.toPoint +import kotlin.math.cos +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class MapboxCircleOverlayRenderer( + val layer: MapboxCircleLayer = + MapboxCircleLayer( + sourceId = "circles-source", + layerId = "circles-layer", + ), + val circleManager: CircleManager, + override val holder: MapboxMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractCircleOverlayRenderer() { + override suspend fun removeCircle(entity: CircleEntity) { + val featureIds = listOf("circle-${entity.state.id}") + layer.source.removeGeoJSONSourceFeatures(featureIds) + } + + override suspend fun createCircle(state: CircleState): MapboxActualCircle? { + val centerPoint = GeoPoint.from(state.center).toPoint() + return Feature.fromGeometry( + Point.fromLngLat(centerPoint.longitude(), centerPoint.latitude()), + JsonObject().apply { + val latitudeCorrection = cos(Math.toRadians(centerPoint.latitude())) + addProperty(MapboxCircleLayer.Prop.LATITUDE_CORRECTION, latitudeCorrection) + addProperty(MapboxCircleLayer.Prop.RADIUS, state.radiusMeters) + addProperty(MapboxCircleLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) + addProperty(MapboxCircleLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) + addProperty(MapboxCircleLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) + }, + "circle-${state.id}", + ) + } + + override suspend fun updateCircleProperties( + circle: MapboxActualCircle, + current: CircleEntity, + prev: CircleEntity, + ): MapboxActualCircle? { + val state = current.state + val centerPoint = GeoPoint.from(state.center).toPoint() + return Feature.fromGeometry( + Point.fromLngLat(centerPoint.longitude(), centerPoint.latitude()), + JsonObject().apply { + val latitudeCorrection = cos(Math.toRadians(centerPoint.latitude())) + addProperty(MapboxCircleLayer.Prop.LATITUDE_CORRECTION, latitudeCorrection) + addProperty(MapboxCircleLayer.Prop.RADIUS, state.radiusMeters) + addProperty(MapboxCircleLayer.Prop.FILL_COLOR, state.fillColor.toMapboxColorString()) + addProperty(MapboxCircleLayer.Prop.STROKE_COLOR, state.strokeColor.toMapboxColorString()) + addProperty(MapboxCircleLayer.Prop.STROKE_WIDTH, state.strokeWidth.value) + }, + "circle-${state.id}", + ) + } + + override suspend fun onPostProcess() { + val circles = getAllCircleEntities() + coroutine.launch { + layer.draw(circles) + } + } + + private fun getAllCircleEntities(): List> { + // This would need access to the polyline manager + // For now, we'll implement a simple workaround + return circleManager.allEntities() + } +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleRenderer.kt index 7a477bfb..38023744 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/circle/MapboxCircleRenderer.kt @@ -7,10 +7,7 @@ import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures import com.mapbox.maps.extension.style.sources.updateGeoJSONSourceFeatures import com.mapconductor.core.circle.AbstractCircleRenderer import com.mapconductor.core.circle.CircleEntity -import com.mapconductor.core.circle.CircleOverlayManager -import com.mapconductor.core.circle.CircleOverlayManagerImpl import com.mapconductor.core.circle.CircleRenderer.UpdateParams -import com.mapconductor.core.circle.CircleRendererFactory import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoPoint import com.mapconductor.mapbox.MapboxActualCircle @@ -21,21 +18,6 @@ import kotlin.math.cos import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -class DefaultMapboxCircleRenderer : CircleRendererFactory { - override fun create( - onAdd: suspend (List) -> List, - onChange: suspend (List>) -> List, - onRemove: suspend (List>) -> Unit, - onPostProcess: (suspend () -> Unit)?, - ): CircleOverlayManager = - CircleOverlayManagerImpl( - onRemove = onRemove, - onAdd = onAdd, - onChange = onChange, - onPostProcess = onPostProcess, - ) -} - class MapboxCircleRenderer( override val holder: MapboxMapViewHolder, override val coroutine: CoroutineScope, diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt index e8ba5ebe..f2dc64bf 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerController.kt @@ -10,7 +10,7 @@ import com.mapconductor.mapbox.MapboxActualMarker import com.mapconductor.settings.Settings class MapboxMarkerController( - override val renderer: MapboxMarkerRenderer, + override val renderer: MapboxMarkerOverlayRenderer, ) : AbstractMarkerController( markerManager = renderer.markerManager, renderer = renderer, diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt similarity index 99% rename from mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt rename to mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt index f71cc1ee..4940c9f9 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerRenderer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MapboxMarkerOverlayRenderer.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class MapboxMarkerRenderer( +class MapboxMarkerOverlayRenderer( holder: MapboxMapViewHolder, coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), val markerManager: MarkerManager, diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt index 9a948396..9048f3bf 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/marker/MarkerLayer.kt @@ -18,15 +18,15 @@ open class MarkerLayer( val layer = symbolLayer(layerId, sourceId) { // iconSize(Expression.get(MapboxMarkerRenderer.Prop.SCALE)) - iconImage(Expression.get(MapboxMarkerRenderer.Prop.ICON_ID)) + iconImage(Expression.get(MapboxMarkerOverlayRenderer.Prop.ICON_ID)) iconAllowOverlap(true) iconIgnorePlacement(true) iconAnchor(IconAnchor.TOP_LEFT) iconTranslateAnchor(IconTranslateAnchor.MAP) iconOffset( switchCase { - has(MapboxMarkerRenderer.Prop.ICON_ANCHOR) - get(MapboxMarkerRenderer.Prop.ICON_ANCHOR) + has(MapboxMarkerOverlayRenderer.Prop.ICON_ANCHOR) + get(MapboxMarkerOverlayRenderer.Prop.ICON_ANCHOR) literal(listOf(0.0, 0.0)) // center-middle }, ) diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt index 7c243ad2..8e9f9ba6 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt @@ -14,7 +14,6 @@ class MapboxPolygonConductor( val polygonOverlay: MapboxPolygonOverlayRenderer, val polylineOverlay: MapboxPolylineOverlayRenderer, ) : OverlayController< - PolygonState, PolygonState, PolygonEntity, PolygonEvent, diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt index 821ebea2..e2fbd223 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineController.kt @@ -1,72 +1,10 @@ package com.mapconductor.mapbox.polyline -import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures -import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer import com.mapconductor.core.polyline.PolylineController -import com.mapconductor.core.polyline.PolylineEntity import com.mapconductor.core.polyline.PolylineManager -import com.mapconductor.core.polyline.PolylineState import com.mapconductor.mapbox.MapboxActualPolyline -import com.mapconductor.mapbox.MapboxMapViewHolder -import com.mapconductor.mapbox.createMapboxLines -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch class MapboxPolylineController( override val renderer: MapboxPolylineOverlayRenderer, polylineManager: PolylineManager = renderer.polylineManager, ) : PolylineController(polylineManager, renderer) - -class MapboxPolylineOverlayRenderer( - val layer: MapboxPolylineLayer, - val polylineManager: PolylineManager, - override val holder: MapboxMapViewHolder, - override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), -) : AbstractPolylineOverlayRenderer() { - override suspend fun createPolyline(state: PolylineState): MapboxActualPolyline? = - createMapboxLines( - id = state.id, - points = state.points, - geodesic = state.geodesic, - strokeColor = state.strokeColor, - strokeWidth = state.strokeWidth, - ) - - override suspend fun updatePolylineProperties( - polyline: MapboxActualPolyline, - current: PolylineEntity, - prev: PolylineEntity, - ): MapboxActualPolyline? { - // For Mapbox, we need to recreate the features when properties change - return createMapboxLines( - id = current.state.id, - points = current.state.points, - geodesic = current.state.geodesic, - strokeColor = current.state.strokeColor, - strokeWidth = current.state.strokeWidth, - ) - } - - override suspend fun removePolyline(entity: PolylineEntity) { - val featureIds = - entity.polyline.map { feature -> - feature.getStringProperty("id") - } - layer.source.removeGeoJSONSourceFeatures(featureIds) - } - - override suspend fun onPostProcess() { - // Redraw all polylines on the layer - val polylines = getAllPolylineEntities() - coroutine.launch { - layer.draw(polylines) - } - } - - private fun getAllPolylineEntities(): List> { - // This would need access to the polyline manager - // For now, we'll implement a simple workaround - return polylineManager.allEntities() - } -} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt new file mode 100644 index 00000000..e213b5b7 --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polyline/MapboxPolylineOverlayRenderer.kt @@ -0,0 +1,66 @@ +package com.mapconductor.mapbox.polyline + +import com.mapbox.maps.extension.style.sources.removeGeoJSONSourceFeatures +import com.mapconductor.core.polyline.AbstractPolylineOverlayRenderer +import com.mapconductor.core.polyline.PolylineEntity +import com.mapconductor.core.polyline.PolylineManager +import com.mapconductor.core.polyline.PolylineState +import com.mapconductor.mapbox.MapboxActualPolyline +import com.mapconductor.mapbox.MapboxMapViewHolder +import com.mapconductor.mapbox.createMapboxLines +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class MapboxPolylineOverlayRenderer( + val layer: MapboxPolylineLayer, + val polylineManager: PolylineManager, + override val holder: MapboxMapViewHolder, + override val coroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : AbstractPolylineOverlayRenderer() { + override suspend fun createPolyline(state: PolylineState): MapboxActualPolyline? = + createMapboxLines( + id = state.id, + points = state.points, + geodesic = state.geodesic, + strokeColor = state.strokeColor, + strokeWidth = state.strokeWidth, + ) + + override suspend fun updatePolylineProperties( + polyline: MapboxActualPolyline, + current: PolylineEntity, + prev: PolylineEntity, + ): MapboxActualPolyline? { + // For Mapbox, we need to recreate the features when properties change + return createMapboxLines( + id = current.state.id, + points = current.state.points, + geodesic = current.state.geodesic, + strokeColor = current.state.strokeColor, + strokeWidth = current.state.strokeWidth, + ) + } + + override suspend fun removePolyline(entity: PolylineEntity) { + val featureIds = + entity.polyline.map { feature -> + feature.getStringProperty("id") + } + layer.source.removeGeoJSONSourceFeatures(featureIds) + } + + override suspend fun onPostProcess() { + // Redraw all polylines on the layer + val polylines = getAllPolylineEntities() + coroutine.launch { + layer.draw(polylines) + } + } + + private fun getAllPolylineEntities(): List> { + // This would need access to the polyline manager + // For now, we'll implement a simple workaround + return polylineManager.allEntities() + } +} From 9b0a0b6ca1acf11b00a80114c916eab0368c22a5 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Thu, 4 Sep 2025 22:42:24 +0900 Subject: [PATCH 43/44] add: firebase app distribution dependencies --- build.gradle.kts | 4 +++- example-app/build.gradle.kts | 3 +++ google-services.json | 29 +++++++++++++++++++++++++++++ gradle/libs.versions.toml | 10 ++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 google-services.json diff --git a/build.gradle.kts b/build.gradle.kts index 2dccd789..8e8930bc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,9 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.android.library) apply false - id("org.jlleitschuh.gradle.ktlint") version "13.0.0-rc.1" + alias(libs.plugins.google.firebase.appdistribution) apply false + alias(libs.plugins.google.services) apply false + alias(libs.plugins.jlleitschuh.ktlint) apply false } buildscript { diff --git a/example-app/build.gradle.kts b/example-app/build.gradle.kts index 3ef09501..9b28b28e 100644 --- a/example-app/build.gradle.kts +++ b/example-app/build.gradle.kts @@ -4,6 +4,8 @@ plugins { alias(libs.plugins.kotlin.compose) id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") id("org.jlleitschuh.gradle.ktlint") + id("com.google.gms.google-services") + alias(libs.plugins.google.firebase.appdistribution) } ktlint { @@ -81,6 +83,7 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.appcompat) + implementation(platform(libs.firebase.bom)) // Google Maps SDK implementation(libs.play.services.maps) diff --git a/google-services.json b/google-services.json new file mode 100644 index 00000000..eca34aad --- /dev/null +++ b/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "35981014220", + "project_id": "mapconductor", + "storage_bucket": "mapconductor.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:35981014220:android:4265a83c3abbd61e8d7e69", + "android_client_info": { + "package_name": "com.mapconductor.example" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyAplsr7yaYVUKq8Kiaw-C4tICkDdaSWO0U" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b10dca69..4346a385 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,10 @@ arcgisMapsKotlin = "200.7.0" uiTooling = "1.8.1" vectordrawable = "1.2.0" roomRuntimeAndroid = "2.7.2" +googleServices = "4.4.3" +firebaseBom = "34.2.0" +googleFirebaseAppdistribution = "5.1.1" +ktLint = "13.1.0" [libraries] # Android 基本 @@ -81,8 +85,14 @@ ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = androidx-vectordrawable = { group = "androidx.vectordrawable", name = "vectordrawable", version.ref = "vectordrawable" } androidx-room-runtime-android = { group = "androidx.room", name = "room-runtime-android", version.ref = "roomRuntimeAndroid" } +# Firebase +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } android-library = { id = "com.android.library", version.ref = "agp" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices"} +google-firebase-appdistribution = { id = "com.google.firebase.appdistribution", version.ref = "googleFirebaseAppdistribution" } +jlleitschuh-ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktLint"} From 8143875d0692ea8c62388fd1b36fc8ca7d49c6bb Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 6 Sep 2025 02:22:51 +0900 Subject: [PATCH 44/44] merge: main branch into ground_overlay branch --- README.md | 90 ++++++++- build.gradle.kts | 2 - example-app/build.gradle.kts | 2 - .../com/mapconductor/example/DemoAppScreen.kt | 26 +++ .../com/mapconductor/example/MainActivity.kt | 2 +- .../pages/animation/AnimationMapComponent.kt | 36 ++++ .../pages/animation/AnimationMapPage.kt | 69 +++++++ .../pages/animation/AnimationPageViewModel.kt | 107 ++++++++++ .../pages/mapDesign/MapDesignMapComponent.kt | 22 +++ .../pages/mapDesign/MapDesignMapPage.kt | 143 ++++++++++++++ .../pages/mapDesign/MapDesignPageViewModel.kt | 186 ++++++++++++++++++ .../example/ui/DemoMapPageScaffold.kt | 15 -- .../example/ui/sidebar/Sidebar.kt | 12 +- gradle/libs.versions.toml | 1 - .../com/mapconductor/core/OverlayProvider.kt | 4 +- .../mapconductor/core/circle/CircleOverlay.kt | 4 +- .../core/controller/MapViewController.kt | 15 +- .../core/groundimage/GroundImageOverlay.kt | 4 +- .../com/mapconductor/core/map/MapViewBase.kt | 6 +- .../mapconductor/core/map/MapViewHolder.kt | 2 +- .../com/mapconductor/core/map/MapViewState.kt | 19 +- .../core/marker/AbstractMarkerController.kt | 2 +- .../mapconductor/core/marker/MarkerOverlay.kt | 6 +- .../core/polygon/PolygonOverlay.kt | 4 +- .../core/polyline/PolylineOverlay.kt | 4 +- .../com/mapconductor/settings/Settings.kt | 2 +- .../arcgis/ArcGISMapDesignType.kt | 10 +- .../com/mapconductor/arcgis/ArcGISMapView.kt | 10 +- .../arcgis/ArcGISMapViewController.kt | 20 ++ .../arcgis/ArcGISMapViewControllerImpl.kt | 48 +++-- ...ViewState.kt => ArcGISMapViewStateImpl.kt} | 77 ++++++-- .../arcgis/marker/ArcGISMarkerRenderer.kt | 4 +- .../googlemaps/GoogleMapDesign.kt | 9 + .../mapconductor/googlemaps/GoogleMapView.kt | 11 +- .../googlemaps/GoogleMapViewController.kt | 22 +++ .../googlemaps/GoogleMapViewControllerImpl.kt | 69 ++++--- .../googlemaps/GoogleMapViewStateImpl.kt | 104 +++++----- .../java/com/mapconductor/here/HereMapView.kt | 22 +-- .../here/HereMapViewController.kt | 19 ++ ...erImpl.kt => HereMapViewControllerImpl.kt} | 48 +++-- .../mapconductor/here/HereViewStateImpl.kt | 71 ++++--- .../mapconductor/mapbox/MapboxMapDesign.kt | 20 ++ .../com/mapconductor/mapbox/MapboxMapView.kt | 11 +- .../mapbox/MapboxMapViewController.kt | 18 ++ .../mapbox/MapboxMapViewControllerImpl.kt | 56 +++--- .../mapbox/MapboxViewStateImpl.kt | 70 +++++-- ...ygonFillLayer.kt => MapboxPolygonLayer.kt} | 0 ...ler.kt => MapboxPolygonOverlayRenderer.kt} | 0 48 files changed, 1178 insertions(+), 326 deletions(-) create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapComponent.kt create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapPage.kt create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationPageViewModel.kt create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapComponent.kt create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapPage.kt create mode 100644 example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignPageViewModel.kt create mode 100644 mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt rename mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/{ArcGISMapViewState.kt => ArcGISMapViewStateImpl.kt} (65%) create mode 100644 mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt create mode 100644 mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt rename mapconductor-for-here/src/main/java/com/mapconductor/here/{HereViewControllerImpl.kt => HereMapViewControllerImpl.kt} (93%) create mode 100644 mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt rename mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/{MapboxPolygonFillLayer.kt => MapboxPolygonLayer.kt} (100%) rename mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/{MapboxPolygonController.kt => MapboxPolygonOverlayRenderer.kt} (100%) diff --git a/README.md b/README.md index e0f4433a..442119e2 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,95 @@ -# セットアップ +# MapConductor Android SDK -1. リポジトリをクローン -``` +A unified mapping library that provides a common API for multiple map providers including Google Maps, Mapbox, HERE, and ArcGIS. Write once, deploy across all major mapping platforms. + +## Features + +- **🗺️ Multi-Provider Support**: Seamlessly switch between Google Maps, Mapbox, HERE, and ArcGIS with a single API +- **🎯 Unified Interface**: Common abstractions for markers, circles, polylines, polygons, and ground overlays +- **⚡ High Performance**: Spatial indexing with hexagonal cells for efficient marker clustering +- **🔄 Reactive State**: Built on Kotlin StateFlow for reactive UI updates +- **🎨 Jetpack Compose**: Modern Android UI toolkit integration +- **📱 Production Ready**: Used in real-world applications with comprehensive testing + +## Architecture + +### Module Structure + +- **`mapconductor-core`**: Core abstractions and shared functionality +- **`mapconductor-for-googlemaps`**: Google Maps implementation +- **`mapconductor-for-mapbox`**: Mapbox implementation +- **`mapconductor-for-here`**: HERE Maps implementation +- **`mapconductor-for-arcgis`**: ArcGIS implementation +- **`mapconductor-icons`**: Reusable marker icon components +- **`example-app`**: Comprehensive demo application + +### Key Components + +- **MapViewController**: Abstract controller interface for all map providers +- **MapViewBase**: Generic Compose-based map view component +- **Overlay Managers**: Separate managers for markers, circles, polylines, and polygons +- **Projection Utilities**: WebMercator and WGS84 coordinate transformations +- **HexGeocell**: Spatial indexing system for performance optimization + +## Quick Start + +### 1. Setup + +Clone the repository: +```bash git clone https://github.com/MapConductor/android-sdk.git ``` -2. https://github.com/MapConductor/map-sdk-credentials/ から`secrets.properties` をプロジェクトルートに追加保存する +Add `secrets.properties` to the project root from https://github.com/MapConductor/map-sdk-credentials/ + +### 2. Basic Usage + +```kotlin +@Composable +fun MyMapScreen() { + GoogleMapView( + modifier = Modifier.fillMaxSize(), + onMapReady = { controller -> + // Add markers, circles, polylines, etc. + } + ) { mapState, controller -> + // Your map content here + } +} +``` + +### 3. Switch Map Providers -3. Android Studioでビルド +Simply change the map view component: +```kotlin +// Google Maps +GoogleMapView { /* ... */ } -# コーディングスタイル +// Mapbox +MapboxMapView { /* ... */ } -KtLintに従います。ローカルで実行する場合は、下記コマンドを実行します(可能ならば自動修正されます)。 +// HERE Maps +HereMapView { /* ... */ } +// ArcGIS +ArcGISMapView { /* ... */ } ``` + +## Development + +### Building +```bash +./gradlew build +``` + +### Code Style +This project follows KtLint conventions: +```bash ./gradlew allLintChecks ``` -# 基本実装状況 (Android) + +## Feature Implementation Status | | Google Maps | Mapbox | Here | ArcGIS | |-----------------|-------------|----------|----------|----------------| @@ -26,6 +98,6 @@ KtLintに従います。ローカルで実行する場合は、下記コマン | Circle | ☑ | ☑ | ☐ | ☐ (wip) | | Polyline | ☑ | ☑ | ☑ | ☑ | | Polygon | ☐ | ☐ | ☐ | ☐ | -| GroundOverlay | ☐ | ☐ | ☐ | ☐ | +| GroundImage | ☐ | N/A | N/A | N/A | | RasterTileLayer | ☐ | ☐ | ☐ | ☐ | | VectorTileLayer | ☐ | ☐ | ☐ | ☐ | diff --git a/build.gradle.kts b/build.gradle.kts index 8e8930bc..8dd3a7f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,6 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.android.library) apply false - alias(libs.plugins.google.firebase.appdistribution) apply false - alias(libs.plugins.google.services) apply false alias(libs.plugins.jlleitschuh.ktlint) apply false } diff --git a/example-app/build.gradle.kts b/example-app/build.gradle.kts index 9b28b28e..bb1ea10c 100644 --- a/example-app/build.gradle.kts +++ b/example-app/build.gradle.kts @@ -4,8 +4,6 @@ plugins { alias(libs.plugins.kotlin.compose) id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") id("org.jlleitschuh.gradle.ktlint") - id("com.google.gms.google-services") - alias(libs.plugins.google.firebase.appdistribution) } ktlint { diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index bc6319d3..3e0d9c36 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -3,9 +3,11 @@ package com.mapconductor.example import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Place import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,11 +21,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import com.mapconductor.example.navigation.NavigationViewModel +import com.mapconductor.example.pages.animation.AnimationMapPage import com.mapconductor.example.pages.circle.CircleMapPage import com.mapconductor.example.pages.groundimage.GroundImageMapPage import com.mapconductor.example.pages.groundimage.GroundImageResources import com.mapconductor.example.pages.map.flyto.FlyToMapIcons import com.mapconductor.example.pages.map.flyto.FlyToMapPage +import com.mapconductor.example.pages.mapDesign.MapDesignMapPage import com.mapconductor.example.pages.marker.MarkerBasicPage import com.mapconductor.example.pages.polygon.PolygonMapPage import com.mapconductor.example.pages.polyline.PolylineMapPage @@ -114,6 +118,18 @@ fun DemoAppScreen(initPage: String = "map") { icon = Icons.Default.PlayArrow, route = "polygon", ), + SidebarItem( + id = "animation", + title = "Animation ", + icon = Icons.Default.Place, + route = "animation", + ), + SidebarItem( + id = "mapDesign", + title = "MapDesign Demo", + icon = Icons.Default.Build, + route = "mapDesign", + ), // SidebarItem( // id = "examples", // title = "Map Examples", @@ -170,6 +186,16 @@ fun DemoAppScreen(initPage: String = "map") { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "animation" -> { + AnimationMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } + "mapDesign" -> { + MapDesignMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } "settings" -> { // Placeholder for settings page Box( diff --git a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt index d1271a7e..42a5f92b 100644 --- a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt +++ b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt @@ -12,7 +12,7 @@ class MainActivity : ComponentActivity() { setContent { DemoAppScreen( - initPage = "polyline", + initPage = "map", ) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapComponent.kt new file mode 100644 index 00000000..8fccfe6d --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapComponent.kt @@ -0,0 +1,36 @@ +package com.mapconductor.example.pages.animation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mapconductor.core.circle.OnCircleEventHandler +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.map.OnMapEventHandler +import com.mapconductor.core.marker.Marker +import com.mapconductor.core.marker.OnMarkerEventHandler +import com.mapconductor.example.MapViewContainer + +@Composable +fun AnimationMapComponent( + mapViewState: MapViewState<*>?, + viewModel: AnimationPageViewModel, + modifier: Modifier = Modifier, + onMapClick: OnMapEventHandler = {}, + onMarkerClick: OnMarkerEventHandler = {}, + onCircleClick: OnCircleEventHandler = {}, + onMarkerDrag: OnMarkerEventHandler = {}, +) { + mapViewState?.let { + MapViewContainer( + modifier = modifier, + state = it, + onMapClick = onMapClick, + onMarkerClick = onMarkerClick, + onCircleClick = onCircleClick, + onMarkerDrag = onMarkerDrag, + ) { + viewModel.allMarkers.forEach { marker -> + Marker(marker) + } + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapPage.kt new file mode 100644 index 00000000..f6d2c0a6 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationMapPage.kt @@ -0,0 +1,69 @@ +package com.mapconductor.example.pages.animation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.mapconductor.example.ui.DefaultMapViewItems +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.MessageCard + +@Composable +fun AnimationMapPage( + viewModel: AnimationPageViewModel = AnimationPageViewModelImpl(), + onToggleSidebar: () -> Unit = {}, +) { + DemoMapPageScaffold( + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = viewModel::onMapViewChanged, + ) { paddingValues -> + val mapViewState = viewModel.mapViewState.collectAsState() + + AnimationMapComponent( + mapViewState = mapViewState.value, + viewModel = viewModel, + onMarkerClick = viewModel::onMarkerClick, + ) + + // Message Card + MessageCard( + modifier = + Modifier + .align(Alignment.BottomStart) + .padding( + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, + ), + title = "Animation list", + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + viewModel.allMarkers.forEach { marker -> + Row { + Button( + modifier = Modifier.weight(1f), + onClick = { viewModel.onMarkerClick(marker) }, + ) { + Text(viewModel.getSpotName(marker.id)) + } + } + } + } + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationPageViewModel.kt new file mode 100644 index 00000000..d2c695a3 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/animation/AnimationPageViewModel.kt @@ -0,0 +1,107 @@ +package com.mapconductor.example.pages.animation + +import androidx.compose.runtime.getValue +import androidx.lifecycle.ViewModel +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewState +import com.mapconductor.core.marker.DefaultIcon +import com.mapconductor.core.marker.MarkerAnimation +import com.mapconductor.core.marker.MarkerState +import com.mapconductor.example.toast.ToastMessage +import java.util.NoSuchElementException +import android.util.Log +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +private data class Spot( + val id: String, + val name: String, + val animation: MarkerAnimation, + val point: GeoPoint, +) + +private val exampleSpots = + listOf( + Spot( + id = "s1", + name = "Bounce", + animation = MarkerAnimation.Bounce, + point = GeoPoint.fromLatLong(21.3069, -157.8583), + ), + Spot( + id = "s2", + name = "Drop", + animation = MarkerAnimation.Drop, + point = GeoPoint.fromLatLong(21.4513, -158.0152), + ), + ) + +interface AnimationPageViewModel { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + val messages: StateFlow> + val allMarkers: List + + val circleCenter: GeoPoint + + fun getSpotName(markerId: String): String + + fun onMapViewChanged(state: MapViewState<*>) + + fun onMarkerClick(clicked: MarkerState) +} + +class AnimationPageViewModelImpl : + ViewModel(), + AnimationPageViewModel { + private val _messages: MutableStateFlow> = MutableStateFlow(emptyList()) + override val messages: StateFlow> = _messages.asStateFlow() + + override val initCameraPosition = + MapCameraPosition( + position = + GeoPoint.fromLatLong( + latitude = 21.382314, + longitude = -157.933097, + ), + zoom = 9.0, + bearing = 0.0, + tilt = 0.0, + paddings = null, + ) + + override val circleCenter = GeoPoint.fromLatLong(21.382314, -157.933097) + + private val markers: Map = + exampleSpots.associate { spot -> + spot.id to + MarkerState( + id = spot.id, + position = spot.point, + icon = DefaultIcon(label = spot.name), + animation = null, + ) + } + override val allMarkers: List get() = markers.values.toList() + + private val _mapViewState = MutableStateFlow?>(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + override fun getSpotName(markerId: String): String = + exampleSpots.firstOrNull { it.id == markerId }?.name + ?: throw NoSuchElementException("Spot not found: $markerId") + + override fun onMapViewChanged(state: MapViewState<*>) { + this._mapViewState.value = state + } + + override fun onMarkerClick(clicked: MarkerState) { + // When you want to activate the marker, set the animation for the marker. + Log.i("AnimationPageViewModelImpl", "onMarkerClick: ${clicked.id}") + + val spot = exampleSpots.firstOrNull { it.id == clicked.id } + clicked.setAnimation(spot?.animation) + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapComponent.kt b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapComponent.kt new file mode 100644 index 00000000..e4408951 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapComponent.kt @@ -0,0 +1,22 @@ +package com.mapconductor.example.pages.mapDesign + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.mapconductor.core.map.MapViewState +import com.mapconductor.example.MapViewContainer + +@Composable +fun MapDesignMapComponent( + mapViewState: MapViewState<*>?, + modifier: Modifier = Modifier, +) { + mapViewState?.let { state -> + + MapViewContainer( + modifier = modifier, + state = state, + ) {} + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapPage.kt new file mode 100644 index 00000000..763ec664 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignMapPage.kt @@ -0,0 +1,143 @@ +package com.mapconductor.example.pages.mapDesign + +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.mapconductor.arcgis.ArcGISDesignType +import com.mapconductor.arcgis.ArcGISMapViewState +import com.mapconductor.core.map.MapViewState +import com.mapconductor.example.ui.DefaultMapViewItems +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.MessageCard +import com.mapconductor.googlemaps.GoogleMapDesignType +import com.mapconductor.googlemaps.GoogleMapViewState +import com.mapconductor.here.HereMapDesignType +import com.mapconductor.here.HereViewState +import com.mapconductor.mapbox.MapboxDesignType +import com.mapconductor.mapbox.MapboxViewState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MapDesignMapPage(onToggleSidebar: () -> Unit = {}) { + val viewModel = remember { MapDesignPageViewModelImpl() } + + val mapDesignOptions = viewModel.mapDesignOptions.collectAsState() + + DemoMapPageScaffold( + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = viewModel::onMapViewChanged, + ) { paddingValues -> + val mapViewState = viewModel.mapViewState.collectAsState() + val mapDesignOptions = viewModel.mapDesignOptions.collectAsState() + + MapDesignMapComponent( + mapViewState = mapViewState.value, + ) + + mapViewState.value?.let { state -> + // Message Card + MessageCard( + modifier = + Modifier + .align(Alignment.BottomStart) + .padding( + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, + ), + title = "Select Map Design...", + ) { + key(state) { + MapDesignTypeSelector( + state = state, + mapDesignOptions = mapDesignOptions.value, + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MapDesignTypeSelector( + state: MapViewState<*>, + mapDesignOptions: List, +) { + var expanded by remember { mutableStateOf(false) } + + var selectedLabel by rememberSaveable(state.id) { + mutableStateOf(mapDesignOptions.firstOrNull()?.label ?: "") + } + + val onClick: (MapDesignOption) -> Unit = fun (mapDesignOption: MapDesignOption) { + selectedLabel = mapDesignOption.label + expanded = false + when (state) { + is GoogleMapViewState -> { + @Suppress("UNCHECKED_CAST") + state.mapDesignType = mapDesignOption.design as GoogleMapDesignType + } + is HereViewState -> { + @Suppress("UNCHECKED_CAST") + state.mapDesignType = mapDesignOption.design as HereMapDesignType + } + is ArcGISMapViewState -> { + @Suppress("UNCHECKED_CAST") + state.mapDesignType = mapDesignOption.design as ArcGISDesignType + } + is MapboxViewState -> { + @Suppress("UNCHECKED_CAST") + state.mapDesignType = mapDesignOption.design as MapboxDesignType + } + else -> throw IllegalArgumentException("Not implemented yet") + } + } + + key(state.id) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + ) { + TextField( + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), + value = selectedLabel, + onValueChange = {}, + readOnly = true, + label = { Text("Map design") }, + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + mapDesignOptions.forEach { item -> + DropdownMenuItem( + text = { Text(item.label) }, + onClick = { + onClick(item) + }, + ) + } + } + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignPageViewModel.kt new file mode 100644 index 00000000..06739d45 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/mapDesign/MapDesignPageViewModel.kt @@ -0,0 +1,186 @@ +package com.mapconductor.example.pages.mapDesign + +import androidx.lifecycle.ViewModel +import com.mapconductor.arcgis.ArcGISDesign +import com.mapconductor.arcgis.ArcGISMapViewState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapDesignType +import com.mapconductor.core.map.MapViewState +import com.mapconductor.googlemaps.GoogleMapDesign +import com.mapconductor.googlemaps.GoogleMapViewState +import com.mapconductor.here.HereMapDesign +import com.mapconductor.here.HereViewState +import com.mapconductor.mapbox.MapboxMapDesign +import com.mapconductor.mapbox.MapboxViewState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +data class MapDesignOption( + val label: String, + val design: MapDesignType<*>, +) + +interface MapDesignPageViewModel { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + + val mapDesignOptions: StateFlow> + + fun onMapViewChanged(state: MapViewState<*>) +} + +class MapDesignPageViewModelImpl : + ViewModel(), + MapDesignPageViewModel { + override val initCameraPosition = + MapCameraPosition( + position = + GeoPoint.fromLatLong( + latitude = 21.382314, + longitude = -157.933097, + ), + zoom = 12.0, + bearing = 0.0, + tilt = 0.0, + paddings = null, + ) + + private val _mapViewState = MutableStateFlow?>(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + private val _mapDesignOptions: MutableStateFlow> = MutableStateFlow(emptyList()) + override val mapDesignOptions: StateFlow> = _mapDesignOptions.asStateFlow() + + override fun onMapViewChanged(state: MapViewState<*>) { + this._mapViewState.value = state + when (state) { + is GoogleMapViewState -> { + _mapDesignOptions.value = googleMapDesigns + } + is HereViewState -> { + _mapDesignOptions.value = hereMapDesigns + } + is MapboxViewState -> { + _mapDesignOptions.value = mapboxMapDesigns + } + is ArcGISMapViewState -> { + _mapDesignOptions.value = arcGISMapDesigns + } + } + } + + private val googleMapDesigns = + listOf( + MapDesignOption(label = "Normal", design = GoogleMapDesign.Normal), + MapDesignOption(label = "Satellite", design = GoogleMapDesign.Satellite), + MapDesignOption(label = "Hybrid", design = GoogleMapDesign.Hybrid), + MapDesignOption(label = "Terrain", design = GoogleMapDesign.Terrain), + MapDesignOption(label = "None", design = GoogleMapDesign.None), + ) + + private val hereMapDesigns = + listOf( + MapDesignOption(label = "NormalDay", design = HereMapDesign.NormalDay), + MapDesignOption(label = "NormalNigh", design = HereMapDesign.NormalNigh), + MapDesignOption(label = "Satellite", design = HereMapDesign.Satellite), + MapDesignOption(label = "HybridDay", design = HereMapDesign.HybridDay), + MapDesignOption(label = "HybridNight", design = HereMapDesign.HybridNight), + MapDesignOption(label = "LiteDay", design = HereMapDesign.LiteDay), + MapDesignOption(label = "LiteNight", design = HereMapDesign.LiteNight), + MapDesignOption(label = "LiteHybridDay", design = HereMapDesign.LiteHybridDay), + MapDesignOption(label = "LiteHybridNight", design = HereMapDesign.LiteHybridNight), + MapDesignOption(label = "LogisticsDay", design = HereMapDesign.LogisticsDay), + MapDesignOption(label = "LogisticsNight", design = HereMapDesign.LogisticsNight), + MapDesignOption(label = "LogisticsHybridDay", design = HereMapDesign.LogisticsHybridDay), + MapDesignOption(label = "RoadNetworkDay", design = HereMapDesign.RoadNetworkDay), + MapDesignOption(label = "RoadNetworkNight", design = HereMapDesign.RoadNetworkNight), + ) + + private val mapboxMapDesigns = + listOf( + MapDesignOption(label = "Standard", design = MapboxMapDesign.Standard), + MapDesignOption(label = "StandardSatellite", design = MapboxMapDesign.StandardSatellite), + MapDesignOption(label = "Streets", design = MapboxMapDesign.Streets), + MapDesignOption(label = "Outdoors", design = MapboxMapDesign.Outdoors), + MapDesignOption(label = "Light", design = MapboxMapDesign.Light), + MapDesignOption(label = "Dark", design = MapboxMapDesign.Dark), + MapDesignOption(label = "Satellite", design = MapboxMapDesign.Satellite), + MapDesignOption(label = "SatelliteStreets", design = MapboxMapDesign.SatelliteStreets), + MapDesignOption(label = "NavigationDay", design = MapboxMapDesign.NavigationDay), + MapDesignOption(label = "NavigationNight", design = MapboxMapDesign.NavigationNight), + ) + + private val arcGISMapDesigns = + listOf( + MapDesignOption(label = "Streets", design = ArcGISDesign.Companion.Streets), + MapDesignOption(label = "Imagery", design = ArcGISDesign.Companion.Imagery), + MapDesignOption(label = "ImageryStandard", design = ArcGISDesign.Companion.ImageryStandard), + MapDesignOption(label = "ImageryLabels", design = ArcGISDesign.Companion.ImageryLabels), + MapDesignOption(label = "LightGray", design = ArcGISDesign.Companion.LightGray), + MapDesignOption(label = "LightGrayBase", design = ArcGISDesign.Companion.LightGrayBase), + MapDesignOption(label = "LightGrayLabels", design = ArcGISDesign.Companion.LightGrayLabels), + MapDesignOption(label = "DarkGray", design = ArcGISDesign.Companion.DarkGray), + MapDesignOption(label = "DarkGrayBase", design = ArcGISDesign.Companion.DarkGrayBase), + MapDesignOption(label = "DarkGrayLabels", design = ArcGISDesign.Companion.DarkGrayLabels), + MapDesignOption(label = "Navigation", design = ArcGISDesign.Companion.Navigation), + MapDesignOption(label = "NavigationNight", design = ArcGISDesign.Companion.NavigationNight), + MapDesignOption(label = "StreetsNight", design = ArcGISDesign.Companion.StreetsNight), + MapDesignOption(label = "StreetsRelief", design = ArcGISDesign.Companion.StreetsRelief), + MapDesignOption(label = "Topographic", design = ArcGISDesign.Companion.Topographic), + MapDesignOption(label = "Oceans", design = ArcGISDesign.Companion.Oceans), + MapDesignOption(label = "OceansBase", design = ArcGISDesign.Companion.OceansBase), + MapDesignOption(label = "OceansLabels", design = ArcGISDesign.Companion.OceansLabels), + MapDesignOption(label = "Terrain", design = ArcGISDesign.Companion.Terrain), + MapDesignOption(label = "TerrainBase", design = ArcGISDesign.Companion.TerrainBase), + MapDesignOption(label = "TerrainDetail", design = ArcGISDesign.Companion.TerrainDetail), + MapDesignOption(label = "Community", design = ArcGISDesign.Companion.Community), + MapDesignOption(label = "ChartedTerritory", design = ArcGISDesign.Companion.ChartedTerritory), + MapDesignOption(label = "ColoredPencil", design = ArcGISDesign.Companion.ColoredPencil), + MapDesignOption(label = "Nova", design = ArcGISDesign.Companion.Nova), + MapDesignOption(label = "ModernAntique", design = ArcGISDesign.Companion.ModernAntique), + MapDesignOption(label = "Midcentury", design = ArcGISDesign.Companion.Midcentury), + MapDesignOption(label = "Newspaper", design = ArcGISDesign.Companion.Newspaper), + MapDesignOption(label = "HillshadeLight", design = ArcGISDesign.Companion.HillshadeLight), + MapDesignOption(label = "HillshadeDark", design = ArcGISDesign.Companion.HillshadeDark), + MapDesignOption(label = "StreetsReliefBase", design = ArcGISDesign.Companion.StreetsReliefBase), + MapDesignOption(label = "TopographicBase", design = ArcGISDesign.Companion.TopographicBase), + MapDesignOption(label = "ChartedTerritoryBase", design = ArcGISDesign.Companion.ChartedTerritoryBase), + MapDesignOption(label = "ModernAntiqueBase", design = ArcGISDesign.Companion.ModernAntiqueBase), + MapDesignOption(label = "HumanGeography", design = ArcGISDesign.Companion.HumanGeography), + MapDesignOption(label = "HumanGeographyBase", design = ArcGISDesign.Companion.HumanGeographyBase), + MapDesignOption(label = "HumanGeographyDetail", design = ArcGISDesign.Companion.HumanGeographyDetail), + MapDesignOption(label = "HumanGeographyLabels", design = ArcGISDesign.Companion.HumanGeographyLabels), + MapDesignOption(label = "HumanGeographyDark", design = ArcGISDesign.Companion.HumanGeographyDark), + MapDesignOption(label = "HumanGeographyDarkBase", design = ArcGISDesign.Companion.HumanGeographyDarkBase), + MapDesignOption( + label = "HumanGeographyDarkDetail", + design = + ArcGISDesign.Companion.HumanGeographyDarkDetail, + ), + MapDesignOption( + label = "HumanGeographyDarkLabels", + design = + ArcGISDesign.Companion.HumanGeographyDarkLabels, + ), + MapDesignOption(label = "Outdoor", design = ArcGISDesign.Companion.Outdoor), + MapDesignOption(label = "OsmStandard", design = ArcGISDesign.Companion.OsmStandard), + MapDesignOption(label = "OsmStandardRelief", design = ArcGISDesign.Companion.OsmStandardRelief), + MapDesignOption(label = "OsmStandardReliefBase", design = ArcGISDesign.Companion.OsmStandardReliefBase), + MapDesignOption(label = "OsmStreets", design = ArcGISDesign.Companion.OsmStreets), + MapDesignOption(label = "OsmStreetsRelief", design = ArcGISDesign.Companion.OsmStreetsRelief), + MapDesignOption(label = "OsmLightGray", design = ArcGISDesign.Companion.OsmLightGray), + MapDesignOption(label = "OsmLightGrayBase", design = ArcGISDesign.Companion.OsmLightGrayBase), + MapDesignOption(label = "OsmLightGrayLabels", design = ArcGISDesign.Companion.OsmLightGrayLabels), + MapDesignOption(label = "OsmDarkGray", design = ArcGISDesign.Companion.OsmDarkGray), + MapDesignOption(label = "OsmDarkGrayBase", design = ArcGISDesign.Companion.OsmDarkGrayBase), + MapDesignOption(label = "OsmDarkGrayLabels", design = ArcGISDesign.Companion.OsmDarkGrayLabels), + MapDesignOption(label = "OsmStreetsReliefBase", design = ArcGISDesign.Companion.OsmStreetsReliefBase), + MapDesignOption(label = "OsmBlueprint", design = ArcGISDesign.Companion.OsmBlueprint), + MapDesignOption(label = "OsmHybrid", design = ArcGISDesign.Companion.OsmHybrid), + MapDesignOption(label = "OsmHybridDetail", design = ArcGISDesign.Companion.OsmHybridDetail), + MapDesignOption(label = "OsmNavigation", design = ArcGISDesign.Companion.OsmNavigation), + MapDesignOption(label = "OsmNavigationDark", design = ArcGISDesign.Companion.OsmNavigationDark), + ) +} diff --git a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt index 14348109..a20cac9a 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt @@ -195,21 +195,6 @@ fun DemoMapPageScaffold( ) } } - -// // Message Card -// MessageCard( -// modifier = -// Modifier -// .align(Alignment.BottomStart) -// .padding( -// bottom = paddingValues.calculateBottomPadding() + 16.dp, -// start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, -// end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, -// ), -// title = "Messages", -// ) { -// MapViewStatePanel(selectedMapViewState.mapCameraPosition.collectAsState().value) -// } } } } diff --git a/example-app/src/main/java/com/mapconductor/example/ui/sidebar/Sidebar.kt b/example-app/src/main/java/com/mapconductor/example/ui/sidebar/Sidebar.kt index 7dc6188a..9431e85e 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/sidebar/Sidebar.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/sidebar/Sidebar.kt @@ -190,12 +190,12 @@ private fun SidebarItemView( Row( verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = item.icon, - contentDescription = if (isExpanded) null else item.title, - modifier = Modifier.size(20.dp), - tint = contentColor, - ) +// Icon( +// imageVector = item.icon, +// contentDescription = if (isExpanded) null else item.title, +// modifier = Modifier.size(20.dp), +// tint = contentColor, +// ) AnimatedVisibility( visible = isExpanded, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4346a385..ed17ea89 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -81,7 +81,6 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } -ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" } androidx-vectordrawable = { group = "androidx.vectordrawable", name = "vectordrawable", version.ref = "vectordrawable" } androidx-room-runtime-android = { group = "androidx.room", name = "room-runtime-android", version.ref = "roomRuntimeAndroid" } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt index f46517a3..6b905ac3 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/OverlayProvider.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import com.mapconductor.core.circle.CircleOverlay import com.mapconductor.core.circle.CircleState -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.groundimage.GroundImageOverlay import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.info.InfoBubbleEntry @@ -40,7 +40,7 @@ open class MapViewScope { @Composable fun CollectAndRenderOverlays( registry: MapOverlayRegistry, - controller: MapViewControllerAlias, + controller: MapViewController, ) { registry.getAll().forEach { overlay -> @Suppress("UNCHECKED_CAST") diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt index fd442363..2b11b69b 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/circle/CircleOverlay.kt @@ -1,7 +1,7 @@ package com.mapconductor.core.circle import androidx.compose.runtime.compositionLocalOf -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapOverlay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,7 +16,7 @@ class CircleOverlay( ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewControllerAlias, + controller: MapViewController, ) { (controller as? CircleCapable)?.compositionCircles(data) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt index 6d8b16e4..811ceda8 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/controller/MapViewController.kt @@ -1,6 +1,8 @@ package com.mapconductor.core.controller +import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewHolder +import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.OnCameraMoveHandler import com.mapconductor.core.map.OnMapEventHandler import kotlinx.coroutines.CoroutineScope @@ -16,6 +18,15 @@ interface MapViewController { fun setMapClickListener(listener: OnMapEventHandler?) fun setMapLongClickListener(listener: OnMapEventHandler?) -} -typealias MapViewControllerAlias = MapViewController + fun moveCamera( + dstPosition: MapCameraPosition, + listener: MapViewState.MoveCameraCallback? = null, + ) + + fun animateCamera( + dstPosition: MapCameraPosition, + duration: Long, + listener: MapViewState.MoveCameraCallback? = null, + ) +} diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt index ea686e28..8aec54d6 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt @@ -1,7 +1,7 @@ package com.mapconductor.core.groundimage import androidx.compose.runtime.compositionLocalOf -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapOverlay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,7 +16,7 @@ class GroundImageOverlay( ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewControllerAlias, + controller: MapViewController, ) { (controller as? GroundImageCapable)?.compositionGroundImages(data) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt index 75bbd221..65b910af 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewBase.kt @@ -26,7 +26,7 @@ import com.mapconductor.core.MapViewScope import com.mapconductor.core.ResourceProvider import com.mapconductor.core.circle.CircleCapable import com.mapconductor.core.circle.LocalCircleCollector -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.groundimage.GroundImageCapable import com.mapconductor.core.groundimage.LocalGroundImageCollector @@ -55,7 +55,7 @@ fun < SpecificState : MapViewState<*>, // Replace Any with a base MapViewController if you have one // Generic type for the actual Android Map View (e.g., com.google.android.gms.maps.MapView) - SpecificController : MapViewControllerAlias, + SpecificController : MapViewController, ActualMapView : View, // Generic type for the actual Map SDK object (e.g., GoogleMap, HereMapSDK.MapController) ActualMap : Any, @@ -138,7 +138,7 @@ fun < markers.value.forEach { markerState -> LaunchedEffect(markerState.id) { markerState.asFlow().debounce(Settings.Default.composeEventDebounce).collectLatest { - (controller as? MarkerCapable<*>)?.updateMarker(markerState) + (controller as? MarkerCapable)?.updateMarker(markerState) } } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt index 8fc18530..2a338aeb 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewHolder.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.geometry.Offset import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.features.IGeoPoint -interface MapViewHolder { +interface MapViewHolder { val mapView: ActualMapViewType val map: ActualMapType diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt index 59e0eca4..0f0a47c2 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/map/MapViewState.kt @@ -1,6 +1,6 @@ package com.mapconductor.core.map -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint import android.util.Log import kotlinx.coroutines.CoroutineScope @@ -17,16 +17,16 @@ enum class InitState { Failed, } -interface MapViewState { +interface MapViewState { interface MoveCameraCallback { - fun onComplete(result: Boolean) + fun onComplete() } val id: String val initCameraPosition: MapCameraPosition val isInitialized: StateFlow val cameraPosition: StateFlow - val mapDesignType: MapDesignType + var mapDesignType: ActualMapDesignType fun initAsync(init: suspend () -> Boolean) @@ -43,12 +43,13 @@ interface MapViewState { durationMs: Long = 0, listener: MoveCameraCallback? = null, ) + + fun getMapViewHolder(): MapViewHolder<*, *>? } -abstract class MapViewStateImpl( - protected val mainCoroutine: CoroutineScope = - CoroutineScope(Dispatchers.Main), -) : MapViewState { +abstract class MapViewStateImpl( + protected val mainCoroutine: CoroutineScope = CoroutineScope(Dispatchers.Main), +) : MapViewState { private val tag = this.javaClass.name private val _isInitialized = MutableStateFlow(InitState.NotStarted) @@ -87,7 +88,7 @@ interface MapOverlay { suspend fun render( data: List, - controller: MapViewControllerAlias, + controller: MapViewController, ) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt index e2f2f4b3..cc0aac69 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/AbstractMarkerController.kt @@ -4,7 +4,7 @@ import com.mapconductor.core.controller.OverlayController import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -interface MarkerCapable { +interface MarkerCapable { suspend fun compositionMarkers(data: List) suspend fun updateMarker(state: MarkerState) diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt index 7e1b9ef6..1e4dc393 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt @@ -1,7 +1,7 @@ package com.mapconductor.core.marker import androidx.compose.runtime.compositionLocalOf -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapOverlay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,8 +16,8 @@ class MarkerOverlay( ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewControllerAlias, + controller: MapViewController, ) { - (controller as? MarkerCapable<*>)?.compositionMarkers(data) + (controller as? MarkerCapable)?.compositionMarkers(data) } } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt index 87477356..4b532c6f 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt @@ -1,7 +1,7 @@ package com.mapconductor.core.polygon import androidx.compose.runtime.compositionLocalOf -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapOverlay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,7 +16,7 @@ class PolygonOverlay( ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewControllerAlias, + controller: MapViewController, ) { (controller as? PolygonCapable)?.compositionPolygons(data) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt index a7637d2e..6a0e030e 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt @@ -1,7 +1,7 @@ package com.mapconductor.core.polyline import androidx.compose.runtime.compositionLocalOf -import com.mapconductor.core.controller.MapViewControllerAlias +import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapOverlay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,7 +16,7 @@ class PolylineOverlay( ) : MapOverlay { override suspend fun render( data: List, - controller: MapViewControllerAlias, + controller: MapViewController, ) { (controller as? PolylineCapable)?.compositionPolylines(data) } diff --git a/mapconductor-core/src/main/java/com/mapconductor/settings/Settings.kt b/mapconductor-core/src/main/java/com/mapconductor/settings/Settings.kt index 939789fb..558976e8 100644 --- a/mapconductor-core/src/main/java/com/mapconductor/settings/Settings.kt +++ b/mapconductor-core/src/main/java/com/mapconductor/settings/Settings.kt @@ -13,7 +13,7 @@ sealed class Settings( ) { object Default : Settings( tapTolerance = 14.dp, - markerDropAnimateDuration = 100, + markerDropAnimateDuration = 300, markerBounceAnimateDuration = 2000, iconSize = MarkerIconSize.Regular, iconStroke = 1.dp, diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapDesignType.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapDesignType.kt index 8909c017..485caa50 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapDesignType.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapDesignType.kt @@ -3,11 +3,13 @@ package com.mapconductor.arcgis import com.arcgismaps.mapping.BasemapStyle import com.mapconductor.core.map.MapDesignType -interface ArcGISDesignType : MapDesignType +interface ArcGISDesignType : MapDesignType { + val elevationSources: List +} data class ArcGISDesign( override val id: String, - val elevationSources: List = emptyList(), + override val elevationSources: List = emptyList(), ) : ArcGISDesignType { override fun getValue(): String = id @@ -30,7 +32,7 @@ data class ArcGISDesign( val Navigation = ArcGISDesign("arc_gis_navigation") val NavigationNight = ArcGISDesign("arc_gis_navigation_night") val StreetsNight = ArcGISDesign("arc_gis_streets_night") - val StreetsRelief = ArcGISDesign("arc_gis_Streets_relief") + val StreetsRelief = ArcGISDesign("arc_gis_streets_relief") val Topographic = ArcGISDesign("arc_gis_topographic") val Oceans = ArcGISDesign("arc_gis_oceans") val OceansBase = ArcGISDesign("arc_gis_oceans_base") @@ -53,7 +55,7 @@ data class ArcGISDesign( val ModernAntiqueBase = ArcGISDesign("arc_gis_modern_antique_base") val HumanGeography = ArcGISDesign("arc_gis_human_geography") val HumanGeographyBase = ArcGISDesign("arc_gis_human_geography_base") - val HumanGeographyDetail = ArcGISDesign("arc_gis_humanGeography_detail") + val HumanGeographyDetail = ArcGISDesign("arc_gis_human_geography_detail") val HumanGeographyLabels = ArcGISDesign("arc_gis_human_geography_labels") val HumanGeographyDark = ArcGISDesign("arc_gis_human_geography_dark") val HumanGeographyDarkBase = ArcGISDesign("arc_gis_human_geography_dark_base") diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt index a0e10332..da9be8bf 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapView.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.node.Ref import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner import com.mapconductor.core.circle.OnCircleEventHandler -import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewBase import com.mapconductor.core.map.OnMapEventHandler import com.mapconductor.core.marker.OnMarkerEventHandler @@ -60,7 +59,6 @@ fun ArcGISMapView( id = state.id, options = options, ) - state.controller = controller controller.holder.mapView.onCreate(owner) controller.holder.mapView.onResume(owner) controller.setCameraMoveListener(state::onCameraChange) @@ -74,12 +72,10 @@ fun ArcGISMapView( controller.setOnMarkerDragEnd(onMarkerDragEnd) controller.setOnMarkerAnimateStart(onMarkerAnimateStart) controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) + controller.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) + state.setController(controller) - state.controller = controller - - val restoreCameraPosition = - state.cameraPosition.value - ?: MapCameraPosition.from(state.initCameraPosition) + val restoreCameraPosition = state.cameraPosition.value controller.moveCamera(restoreCameraPosition) controllerRef.value = controller diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt new file mode 100644 index 00000000..7d8f33aa --- /dev/null +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewController.kt @@ -0,0 +1,20 @@ +package com.mapconductor.arcgis + +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.PolylineCapable + +typealias ArcGISDesignTypeChangeHandler = (ArcGISDesignType) -> Unit + +interface ArcGISMapViewController : + MapViewController, + MarkerCapable, + PolylineCapable, + PolygonCapable, + CircleCapable { + fun setMapDesignType(value: ArcGISDesignType) + + fun setMapDesignTypeChangeListener(listener: ArcGISDesignTypeChangeHandler) +} diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt index a5ff8ea5..f4e5e1a1 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewControllerImpl.kt @@ -1,5 +1,6 @@ package com.mapconductor.arcgis +import com.arcgismaps.mapping.Basemap import com.arcgismaps.mapping.view.LongPressEvent import com.arcgismaps.mapping.view.PanChangeEvent import com.arcgismaps.mapping.view.SingleTapConfirmedEvent @@ -10,23 +11,18 @@ import com.mapconductor.arcgis.marker.ArcGISMarkerController import com.mapconductor.arcgis.marker.SelectedMarker import com.mapconductor.arcgis.polygon.ArcGISPolygonOverlayController import com.mapconductor.arcgis.polyline.ArcGISPolylineOverlayController -import com.mapconductor.core.circle.CircleCapable import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.controller.BaseMapViewController -import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.OnPolygonEventHandler -import com.mapconductor.core.polygon.PolygonCapable import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler -import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineEvent import com.mapconductor.core.polyline.PolylineState import com.mapconductor.settings.Settings @@ -35,24 +31,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface ArcGISMapViewController : - MapViewController, - MarkerCapable, - PolylineCapable, - PolygonCapable, - CircleCapable { - fun moveCamera( - dstPosition: MapCameraPosition, - listener: MapViewState.MoveCameraCallback? = null, - ) - - fun animateCamera( - dstPosition: MapCameraPosition, - duration: Int, - listener: MapViewState.MoveCameraCallback? = null, - ) -} - class ArcGISMapViewControllerImpl( override val holder: ArcGISMapViewHolder, private val markerController: ArcGISMarkerController, @@ -249,12 +227,12 @@ class ArcGISMapViewControllerImpl( holder.map.setViewpointCamera( camera = dstCameraPosition, ) - listener?.onComplete(true) + listener?.onComplete() } override fun animateCamera( dstPosition: MapCameraPosition, - duration: Int, + duration: Long, listener: MapViewState.MoveCameraCallback?, ) { val dstCameraPosition = dstPosition.toCamera() @@ -265,7 +243,7 @@ class ArcGISMapViewControllerImpl( camera = dstCameraPosition, duration = duration.toFloat() / 1000.0f, ) - listener?.onComplete(result.isSuccess) + listener?.onComplete() } } @@ -300,4 +278,22 @@ class ArcGISMapViewControllerImpl( override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { this.polygonController.clickListener = listener } + + private var mapDesignType: ArcGISDesignType = ArcGISDesign.Streets + private var mapDesignTypeChangeListener: ArcGISDesignTypeChangeHandler? = null + + override fun setMapDesignType(value: ArcGISDesignType) { + holder.map.scene?.let { scene -> + val baseMapStyle = ArcGISDesign.toBasemapStyle(value) + val baseMap = Basemap(baseMapStyle) + coroutine.launch { + scene.setBasemap(baseMap) + } + } + } + + override fun setMapDesignTypeChangeListener(listener: ArcGISDesignTypeChangeHandler) { + mapDesignTypeChangeListener = listener + listener(mapDesignType) + } } diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewState.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewStateImpl.kt similarity index 65% rename from mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewState.kt rename to mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewStateImpl.kt index a0695e43..ba2f5ba2 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewState.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/ArcGISMapViewStateImpl.kt @@ -22,43 +22,64 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface ArcGISMapViewState : MapViewState +interface ArcGISMapViewState : MapViewState class ArcGISMapViewStateImpl( override val id: String, + mapDesignType: ArcGISDesignType, override val initCameraPosition: MapCameraPosition, - override val mapDesignType: ArcGISDesign, -) : MapViewStateImpl(), +) : MapViewStateImpl(), ArcGISMapViewState { // Map padding private val _padding = MutableStateFlow(MapPaddingsImpl.Zeros) val padding: StateFlow = _padding.asStateFlow() - internal var controller: ArcGISMapViewController? = null - // Camera position private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() + private var controller: ArcGISMapViewController? = null + private var _mapDesignType: ArcGISDesignType = mapDesignType + + override var mapDesignType: ArcGISDesignType + set(value) { + value?.let { + _mapDesignType = value + this.controller?.setMapDesignType(value) + } + } + get() = _mapDesignType + + internal fun setController(controller: ArcGISMapViewController) { + this.controller = controller + _mapDesignType?.let { + controller.setMapDesignType(it) + } + controller.moveCamera(_cameraPosition.value) + } + + internal fun onMapDesignTypeChange(value: ArcGISDesignType) { + _mapDesignType = value + } + override fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long, listener: MapViewState.MoveCameraCallback?, ) { - if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) - return - } - - val dstCameraPosition = MapCameraPosition.from(cameraPosition) - controller?.let { - if (durationMs == 0L) { - it.moveCamera(dstCameraPosition, listener) - } else { - it.animateCamera(dstCameraPosition, durationMs.toInt(), listener) + controller?.let { ctrl -> + if (this.isInitialized.value == InitState.Initialized) { + val dstCameraPosition = MapCameraPosition.from(cameraPosition) + if (durationMs == 0L) { + ctrl.moveCamera(dstCameraPosition, listener) + } else { + ctrl.animateCamera(dstCameraPosition, durationMs, listener) + } + return } - } ?: listener?.onComplete(false) + } + _cameraPosition.value = cameraPosition + listener?.onComplete() } override fun moveCameraTo( @@ -66,9 +87,25 @@ class ArcGISMapViewStateImpl( durationMs: Long, listener: MapViewState.MoveCameraCallback?, ) { - // Do nothing here + if (this.isInitialized.value != InitState.Initialized) { + _cameraPosition.value = + MapCameraPosition( + position = position, + ) + listener?.onComplete() + return + } + val currentPosition = this.cameraPosition.value + val newPosition = + currentPosition.copy( + position = position, + ) + this.moveCameraTo(newPosition, durationMs, listener) } + @Suppress("UNCHECKED_CAST") + override fun getMapViewHolder(): ArcGISMapViewHolder? = controller?.holder as? ArcGISMapViewHolder + internal fun onCameraChange(cameraPosition: MapCameraPosition) { this._cameraPosition.value = cameraPosition } @@ -81,7 +118,7 @@ class ArcGISMapViewSaver : BaseMapViewSaver() { state: ArcGISMapViewStateImpl, bundle: Bundle, ) { - bundle.putString("id", state.mapDesignType.id) + bundle.putString("id", state.mapDesignType?.id ?: ArcGISDesign.Streets.id) } override fun createState( diff --git a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt index 90a7425b..2b31ae4c 100644 --- a/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt +++ b/mapconductor-for-arcgis/src/main/java/com/mapconductor/arcgis/marker/ArcGISMarkerRenderer.kt @@ -29,7 +29,9 @@ class ArcGISMarkerRenderer( markerEntity: MarkerEntity, position: GeoPoint, ) { - markerEntity.marker.geometry = position.toPoint(holder.map.scene?.spatialReference) + coroutine.launch { + markerEntity.marker.geometry = position.toPoint(holder.map.scene?.spatialReference) + } } override suspend fun onAdd(data: List): List { diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapDesign.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapDesign.kt index 873a17cf..8e810801 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapDesign.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapDesign.kt @@ -33,5 +33,14 @@ sealed class GoogleMapDesign( Terrain.id -> Terrain else -> None } + + fun toMapDesignType(id: Int): GoogleMapDesignType = + when (id) { + MAP_TYPE_NORMAL -> Normal + MAP_TYPE_SATELLITE -> Satellite + MAP_TYPE_HYBRID -> Hybrid + MAP_TYPE_TERRAIN -> Terrain + else -> None + } } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt index 923be570..0ed5e9a6 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapView.kt @@ -23,7 +23,7 @@ import android.view.ViewGroup @Composable fun GoogleMapsView( - state: GoogleMapViewState, + state: GoogleMapViewStateImpl, modifier: Modifier = Modifier, onMapClick: OnMapEventHandler? = null, onMarkerClick: OnMarkerEventHandler? = null, @@ -69,7 +69,7 @@ fun GoogleMapsView( val mapInitOptions = GoogleMapOptions() - .mapType(state.mapDesignType.getValue()) + .mapType(state.mapDesignType?.getValue() ?: GoogleMapDesign.None.getValue()) .camera(cameraPosition) val controller = @@ -78,10 +78,8 @@ fun GoogleMapsView( id = state.id, options = mapInitOptions, ) - (state as? GoogleMapViewStateImpl)?.let { mapViewState -> - mapViewState.controller = controller - controller.setCameraMoveListener(mapViewState::onCameraChange) - } + state.setController(controller) + controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) controller.setOnMarkerClickListener(onMarkerClick) controller.setOnMarkerDragStart(onMarkerDragStart) @@ -93,6 +91,7 @@ fun GoogleMapsView( controller.setOnMarkerAnimateStart(onMarkerAnimateStart) controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) controller.setOnGroundImageClickListener(onGroundImageClick) + controller.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) holderRef.value = controller.holder controllerRef.value = controller diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt new file mode 100644 index 00000000..731e0da4 --- /dev/null +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewController.kt @@ -0,0 +1,22 @@ +package com.mapconductor.googlemaps + +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.groundimage.GroundImageCapable +import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.PolylineCapable + +typealias GoogleMapDesignTypeChangeHandler = (GoogleMapDesignType) -> Unit + +interface GoogleMapViewController : + MapViewController, + GroundImageCapable, + PolygonCapable, + MarkerCapable, + PolylineCapable, + CircleCapable { + fun setMapDesignType(value: GoogleMapDesignType) + + fun setMapDesignTypeChangeListener(listener: GoogleMapDesignTypeChangeHandler) +} diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt index c671921e..f25603a4 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewControllerImpl.kt @@ -1,6 +1,7 @@ package com.mapconductor.googlemaps import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMap.CancelableCallback import com.google.android.gms.maps.GoogleMap.OnCameraIdleListener import com.google.android.gms.maps.GoogleMap.OnCameraMoveCanceledListener @@ -8,27 +9,21 @@ import com.google.android.gms.maps.GoogleMap.OnCameraMoveListener import com.google.android.gms.maps.GoogleMap.OnCameraMoveStartedListener import com.google.android.gms.maps.GoogleMap.OnMapClickListener import com.google.android.gms.maps.model.LatLng -import com.mapconductor.core.circle.CircleCapable import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.controller.BaseMapViewController -import com.mapconductor.core.controller.MapViewController -import com.mapconductor.core.groundimage.GroundImageCapable import com.mapconductor.core.groundimage.GroundImageEvent import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.groundimage.OnGroundImageEventHandler import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.OnPolygonEventHandler -import com.mapconductor.core.polygon.PolygonCapable import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler -import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState import com.mapconductor.googlemaps.circle.GoogleMapCircleController import com.mapconductor.googlemaps.groundimage.GoogleMapGroundImageController @@ -39,25 +34,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface GoogleMapViewController : - MapViewController, - GroundImageCapable, - PolygonCapable, - MarkerCapable, - PolylineCapable, - CircleCapable { - fun moveCamera( - dstPosition: MapCameraPosition, - listener: MapViewState.MoveCameraCallback? = null, - ) - - fun animateCamera( - dstPosition: MapCameraPosition, - duration: Int, - listener: MapViewState.MoveCameraCallback? = null, - ) -} - class GoogleMapViewControllerImpl( override val holder: GoogleMapViewHolder, private val markerController: GoogleMapMarkerController, @@ -72,7 +48,8 @@ class GoogleMapViewControllerImpl( OnCameraMoveCanceledListener, OnCameraMoveListener, OnCameraIdleListener, - OnMapClickListener { + OnMapClickListener, + GoogleMap.OnMapLoadedCallback { init { setupListeners() } @@ -83,38 +60,39 @@ class GoogleMapViewControllerImpl( holder.map.setOnCameraMoveListener(this) holder.map.setOnCameraIdleListener(this) holder.map.setOnMapClickListener(this) + holder.map.setOnMapLoadedCallback(this) } override fun moveCamera( - position: MapCameraPosition, + dstPosition: MapCameraPosition, listener: MapViewState.MoveCameraCallback?, ) { coroutine.launch { - val dstCameraPosition = position.toCameraPosition() + val dstCameraPosition = dstPosition.toCameraPosition() val cameraUpdate = CameraUpdateFactory.newCameraPosition(dstCameraPosition) holder.map.moveCamera(cameraUpdate) - listener?.onComplete(true) + listener?.onComplete() } } override fun animateCamera( - position: MapCameraPosition, - duration: Int, + dstPosition: MapCameraPosition, + duration: Long, listener: MapViewState.MoveCameraCallback?, ) { - val dstCameraPosition = position.toCameraPosition() + val dstCameraPosition = dstPosition.toCameraPosition() coroutine.launch { val cameraUpdate = CameraUpdateFactory.newCameraPosition(dstCameraPosition) holder.map.animateCamera( cameraUpdate, - duration, + duration.toInt(), object : CancelableCallback { override fun onCancel() { - listener?.onComplete(false) + listener?.onComplete() } override fun onFinish() { - listener?.onComplete(true) + listener?.onComplete() } }, ) @@ -260,4 +238,25 @@ class GoogleMapViewControllerImpl( override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { this.polygonController.clickListener = listener } + + private var _mapDesignType: GoogleMapDesignType = GoogleMapDesign.None + private var _mapDesignTypeChangeListener: GoogleMapDesignTypeChangeHandler? = null + + override fun setMapDesignType(value: GoogleMapDesignType) { + coroutine.launch { + holder.map.mapType = value.getValue() + } + _mapDesignType = value + _mapDesignTypeChangeListener?.invoke(value) + } + + override fun setMapDesignTypeChangeListener(listener: GoogleMapDesignTypeChangeHandler) { + _mapDesignTypeChangeListener = listener + listener(_mapDesignType) + } + + override fun onMapLoaded() { + val mapDesignType = GoogleMapDesign.toMapDesignType(holder.map.mapType) + _mapDesignTypeChangeListener?.invoke(mapDesignType) + } } diff --git a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt index 88879caa..eb708ea3 100644 --- a/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt +++ b/mapconductor-for-googlemaps/src/main/java/com/mapconductor/googlemaps/GoogleMapViewStateImpl.kt @@ -18,23 +18,45 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface GoogleMapViewState : MapViewState +interface GoogleMapViewState : MapViewState class GoogleMapViewStateImpl( override val id: String, - override val mapDesignType: GoogleMapDesignType, + mapDesignType: GoogleMapDesignType, override val initCameraPosition: MapCameraPosition, -) : MapViewStateImpl(), +) : MapViewStateImpl(), GoogleMapViewState { // Map padding private val _padding = MutableStateFlow(MapPaddingsImpl.Zeros) val padding: StateFlow = _padding.asStateFlow() // Camera position - private val _cameraPosition = MutableStateFlow(initCameraPosition) - override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() + private val _cameraPosition = MutableStateFlow(initCameraPosition) + override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() - internal var controller: GoogleMapViewController? = null + private var _mapDesignType: GoogleMapDesignType = mapDesignType + + override var mapDesignType: GoogleMapDesignType + set(value) { + value?.let { + _mapDesignType = value + this.controller?.setMapDesignType(value) + } + } + get() = _mapDesignType + private var controller: GoogleMapViewController? = null + + internal fun setController(controller: GoogleMapViewController) { + this.controller = controller + _mapDesignType?.let { + controller.setMapDesignType(it) + } + controller.moveCamera(_cameraPosition.value) + } + + internal fun onMapDesignTypeChange(value: GoogleMapDesignType) { + _mapDesignType = value + } override fun moveCameraTo( position: GeoPoint, @@ -42,63 +64,47 @@ class GoogleMapViewStateImpl( listener: MapViewState.MoveCameraCallback?, ) { if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) - return - } - this.cameraPosition.value?.let { current -> - val newPosition = - current.copy( + _cameraPosition.value = + MapCameraPosition( position = position, ) - this.moveCameraTo(newPosition, durationMs, listener) + listener?.onComplete() + return } + val currentPosition = this.cameraPosition.value + val newPosition = + currentPosition.copy( + position = position, + ) + this.moveCameraTo(newPosition, durationMs, listener) } + @Suppress("UNCHECKED_CAST") + override fun getMapViewHolder(): GoogleMapViewHolder? = controller?.holder as? GoogleMapViewHolder + override fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long, listener: MapViewState.MoveCameraCallback?, ) { - if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) - return - } - - val dstCameraPosition = MapCameraPosition.from(cameraPosition) - controller?.let { - if (durationMs == 0L) { - it.moveCamera(dstCameraPosition, listener) - } else { - it.animateCamera(dstCameraPosition, durationMs.toInt(), listener) + controller?.let { ctrl -> + if (this.isInitialized.value == InitState.Initialized) { + val dstCameraPosition = MapCameraPosition.from(cameraPosition) + if (durationMs == 0L) { + ctrl.moveCamera(dstCameraPosition, listener) + } else { + ctrl.animateCamera(dstCameraPosition, durationMs, listener) + } + return } - } ?: listener?.onComplete(false) + } + _cameraPosition.value = cameraPosition + listener?.onComplete() } internal fun onCameraChange(cameraPosition: MapCameraPosition) { this._cameraPosition.value = cameraPosition } - -// override fun onCameraMoveStart(cameraPosition: CameraPosition) { -// this._cameraPosition.value = cameraPosition -// } -// -// override fun onCameraMove(cameraPosition: CameraPosition) { -// this._cameraPosition.value = cameraPosition -// } -// -// override fun onCameraMoveEnd(cameraPosition: CameraPosition) { -// this._cameraPosition.value = cameraPosition -// } -// -// override fun onMarkerAdd(state: MarkerState) { -// // Do nothing here -// } -// -// override fun onMarkerRemove(id: String) { -// // Do nothing here -// } } // GoogleMapViewSaver implementation @@ -109,7 +115,7 @@ class GoogleMapViewSaver : BaseMapViewSaver() { state: GoogleMapViewStateImpl, bundle: Bundle, ) { - bundle.putInt("id", state.mapDesignType.id) + bundle.putInt("id", state.mapDesignType?.id ?: GoogleMapDesign.None.id) } override fun createState( @@ -131,7 +137,7 @@ class GoogleMapViewSaver : BaseMapViewSaver() { @Composable fun rememberGoogleMapViewState( - mapDesign: GoogleMapDesignType = GoogleMapDesign.Normal, + mapDesign: GoogleMapDesign = GoogleMapDesign.Normal, cameraPosition: IMapCameraPosition = MapCameraPosition.Default, ): GoogleMapViewStateImpl { val stateId by rememberSaveable { diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt index 6f1fb737..fdc30e26 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapView.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine @OptIn(ExperimentalCoroutinesApi::class) @Composable fun HereMapView( - state: HereViewState, + state: HereViewStateImpl, modifier: Modifier = Modifier, onMapClick: OnMapEventHandler? = null, onMarkerClick: OnMarkerEventHandler? = null, @@ -58,7 +58,7 @@ fun HereMapView( val mapInitOptions = HereViewInitOptions( - scheme = state.mapDesignType.id, + scheme = state.mapDesignType.getValue(), ) val controller = @@ -68,10 +68,8 @@ fun HereMapView( options = mapInitOptions, ) - (state as? HereViewStateImpl)?.let { mapViewState -> - mapViewState.controller = controller - controller.setCameraMoveListener(mapViewState::onCameraChange) - } + controller.setCameraMoveListener(state::onCameraChange) + controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) controller.setOnMarkerClickListener(onMarkerClick) controller.setOnMarkerDragStart(onMarkerDragStart) @@ -82,10 +80,10 @@ fun HereMapView( controller.setOnCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) controller.setOnPolygonClickListener(onPolygonClick) + state.setController(controller) + controller.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) - (state as? HereViewStateImpl)?.controller = controller - - controller.holder.mapView.mapScene.loadScene(state.mapDesignType.id) { mapError -> + controller.holder.mapView.mapScene.loadScene(state.mapDesignType.getValue()) { mapError -> if (mapError != null) { throw Throwable("Loading map failed: mapError: " + mapError.name) } @@ -95,13 +93,13 @@ fun HereMapView( controllerRef.value = controller return@MapViewBase suspendCancellableCoroutine { cont -> - val restoreCameraPosition = state.cameraPosition.value ?: state.initCameraPosition + val restoreCameraPosition = state.cameraPosition.value controller.moveCamera( dstPosition = restoreCameraPosition, listener = object : MapViewState.MoveCameraCallback { - override fun onComplete(result: Boolean) { - cont.resume(result) { } + override fun onComplete() { + cont.resume(true) { } } }, ) diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt new file mode 100644 index 00000000..f4b7d456 --- /dev/null +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewController.kt @@ -0,0 +1,19 @@ +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.PolylineCapable +import com.mapconductor.here.HereMapDesignType + +typealias HereMapDesignTypeChangeHandler = (HereMapDesignType) -> Unit + +interface HereMapViewController : + MapViewController, + MarkerCapable, + PolygonCapable, + PolylineCapable, + CircleCapable { + fun setMapDesignType(value: HereMapDesignType) + + fun setMapDesignTypeChangeListener(listener: HereMapDesignTypeChangeHandler) +} diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt similarity index 93% rename from mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt rename to mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt index e4b79cd1..90e619b8 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewControllerImpl.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereMapViewControllerImpl.kt @@ -1,5 +1,7 @@ package com.mapconductor.here +import HereMapDesignTypeChangeHandler +import HereMapViewController import com.here.sdk.animation.AnimationState import com.here.sdk.core.GeoOrientation import com.here.sdk.core.Point2D @@ -19,20 +21,16 @@ import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.controller.BaseMapViewController -import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewHolder import com.mapconductor.core.map.MapViewState.MoveCameraCallback -import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.OnPolygonEventHandler -import com.mapconductor.core.polygon.PolygonCapable import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler -import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState import com.mapconductor.here.circle.HereCircleController import com.mapconductor.here.marker.HereMarkerController @@ -42,24 +40,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface HereMapViewController : - MapViewController, - MarkerCapable, - PolygonCapable, - PolylineCapable, - CircleCapable { - fun moveCamera( - dstPosition: MapCameraPosition, - listener: MoveCameraCallback? = null, - ) - - fun animateCamera( - dstPosition: MapCameraPosition, - durationMs: Long, - listener: MoveCameraCallback? = null, - ) -} - class HereMapViewControllerImpl( private val markerController: HereMarkerController, private val polylineController: HerePolylineController, @@ -152,7 +132,7 @@ class HereMapViewControllerImpl( ) camera.applyUpdate(adjustCameraUpdate) - listener?.onComplete(true) + listener?.onComplete() } override fun animateCamera( @@ -179,8 +159,8 @@ class HereMapViewControllerImpl( when (animState) { // Do nothing here AnimationState.STARTED -> Unit - AnimationState.COMPLETED -> listener?.onComplete(true) - AnimationState.CANCELLED -> listener?.onComplete(false) + AnimationState.COMPLETED -> listener?.onComplete() + AnimationState.CANCELLED -> listener?.onComplete() } } } @@ -287,4 +267,22 @@ class HereMapViewControllerImpl( override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { polygonController.clickListener = listener } + + private var _mapDesignType: HereMapDesignType = HereMapDesign.NormalDay + private var _mapDesignTypeChangeListener: HereMapDesignTypeChangeHandler? = null + + override fun setMapDesignType(value: HereMapDesignType) { + val scene = value.getValue() + coroutine.launch { + holder.mapView.mapScene.loadScene(scene) { + _mapDesignType = value + _mapDesignTypeChangeListener?.invoke(value) + } + } + } + + override fun setMapDesignTypeChangeListener(listener: HereMapDesignTypeChangeHandler) { + _mapDesignTypeChangeListener = listener + listener(_mapDesignType) + } } diff --git a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt index 300eb401..bd282a3e 100644 --- a/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt +++ b/mapconductor-for-here/src/main/java/com/mapconductor/here/HereViewStateImpl.kt @@ -1,10 +1,10 @@ package com.mapconductor.here +import HereMapViewController import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable -import com.here.sdk.mapview.MapScheme import com.mapconductor.core.features.GeoPoint import com.mapconductor.core.map.BaseMapViewSaver import com.mapconductor.core.map.IMapCameraPosition @@ -24,19 +24,29 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface HereViewState : MapViewState +interface HereViewState : MapViewState class HereViewStateImpl( override val id: String, - override val mapDesignType: HereMapDesignType, + mapDesignType: HereMapDesignType, override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, -) : MapViewStateImpl(), +) : MapViewStateImpl(), HereViewState { - internal var controller: HereMapViewController? = null + private var controller: HereMapViewController? = null // Camera center position private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() + private var _mapDesignType: HereMapDesignType = mapDesignType + + override var mapDesignType: HereMapDesignType + set(value) { + value?.let { + _mapDesignType = value + this.controller?.setMapDesignType(value) + } + } + get() = _mapDesignType override fun moveCameraTo( position: GeoPoint, @@ -44,43 +54,58 @@ class HereViewStateImpl( listener: MoveCameraCallback?, ) { if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) + _cameraPosition.value = + MapCameraPosition( + position = position, + ) + listener?.onComplete() return } - val currCameraPosition = this.cameraPosition.value + val currentPosition = this.cameraPosition.value val newPosition = - currCameraPosition.copy( + currentPosition.copy( position = position, ) this.moveCameraTo(newPosition, durationMs, listener) } + @Suppress("UNCHECKED_CAST") + override fun getMapViewHolder(): HereViewHolder? = controller?.holder as? HereViewHolder + override fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long, listener: MoveCameraCallback?, ) { - if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) - return + controller?.let { ctrl -> + if (this.isInitialized.value == InitState.Initialized) { + val dstCameraPosition = MapCameraPosition.from(cameraPosition) + if (durationMs == 0L) { + ctrl.moveCamera(dstCameraPosition, listener) + } else { + ctrl.animateCamera(dstCameraPosition, durationMs, listener) + } + return + } } + _cameraPosition.value = cameraPosition + listener?.onComplete() + } - if (controller == null) { - listener?.onComplete(false) - return - } + internal fun onCameraChange(cameraState: MapCameraPosition) { + this._cameraPosition.value = cameraState + } - if (durationMs == 0L) { - controller!!.moveCamera(cameraPosition, listener) - } else { - controller!!.animateCamera(cameraPosition, durationMs.toLong(), listener) + internal fun setController(controller: HereMapViewController) { + this.controller = controller + _mapDesignType?.let { + controller.setMapDesignType(it) } + controller.moveCamera(_cameraPosition.value) } - internal fun onCameraChange(cameraState: MapCameraPosition) { - this._cameraPosition.value = cameraState + internal fun onMapDesignTypeChange(value: HereMapDesignType) { + _mapDesignType = value } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt index e802769c..40686987 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapDesign.kt @@ -1,6 +1,8 @@ package com.mapconductor.mapbox +import com.mapbox.maps.Style import com.mapconductor.core.map.MapDesignType +import com.mapconductor.mapbox.MapboxMapDesign.Companion.MAPBOX_URL typealias MapboxDesignType = MapDesignType @@ -52,3 +54,21 @@ sealed class MapboxMapDesign( } } } + +fun Style.toMapDesignType(): MapboxDesignType = + when (this.styleURI) { + "$MAPBOX_URL/${MapboxMapDesign.Standard.id}" -> MapboxMapDesign.Standard + "$MAPBOX_URL/${MapboxMapDesign.StandardSatellite.id}" -> MapboxMapDesign.StandardSatellite + "$MAPBOX_URL/${MapboxMapDesign.Streets.id}" -> MapboxMapDesign.Streets + "$MAPBOX_URL/${MapboxMapDesign.Outdoors.id}" -> MapboxMapDesign.Outdoors + "$MAPBOX_URL/${MapboxMapDesign.Light.id}" -> MapboxMapDesign.Light + "$MAPBOX_URL/${MapboxMapDesign.Dark.id}" -> MapboxMapDesign.Dark + "$MAPBOX_URL/${MapboxMapDesign.Satellite.id}" -> MapboxMapDesign.Satellite + "$MAPBOX_URL/${MapboxMapDesign.SatelliteStreets.id}" -> MapboxMapDesign.SatelliteStreets + "$MAPBOX_URL/${MapboxMapDesign.NavigationDay.id}" -> MapboxMapDesign.NavigationDay + "$MAPBOX_URL/${MapboxMapDesign.NavigationNight.id}" -> MapboxMapDesign.NavigationNight + else -> + MapboxMapDesign.Custom( + layerId = this.styleURI.replaceFirst("${MAPBOX_URL}/", ""), + ) + } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index adf22ccb..74e54ae6 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -35,7 +35,7 @@ import android.content.ContextWrapper @Composable fun MapboxMapView( - state: MapboxViewState, + state: MapboxViewStateImpl, modifier: Modifier = Modifier, onMapClick: OnMapEventHandler? = null, onMarkerClick: OnMarkerEventHandler? = null, @@ -67,7 +67,7 @@ fun MapboxMapView( MapboxInitSDK(context) val cameraOptions = - state.cameraPosition.value?.toCameraOptions() + state.cameraPosition.value.toCameraOptions() val styleUri = state.mapDesignType.getValue() val mapInitOptions = @@ -88,10 +88,7 @@ fun MapboxMapView( polygonController = getPolygonController(holder), circleController = getCircleController(holder), ) - (state as? MapboxViewStateImpl)?.let { mapViewState -> - mapViewState.controller = controller - controller.setCameraMoveListener(mapViewState::onCameraChange) - } + controller.setCameraMoveListener(state::onCameraChange) controller.setMapClickListener(onMapClick) controller.setOnCircleClickListener(onCircleClick) controller.setOnPolylineClickListener(onPolylineClick) @@ -102,6 +99,8 @@ fun MapboxMapView( controller.setOnMarkerDragEnd(onMarkerDragEnd) controller.setOnMarkerAnimateStart(onMarkerAnimateStart) controller.setOnMarkerAnimateEnd(onMarkerAnimateEnd) + controller.setMapDesignTypeChangeListener(state::onMapDesignTypeChange) + state.setController(controller) holderRef.value = holder controllerRef.value = controller diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt new file mode 100644 index 00000000..13314bda --- /dev/null +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewController.kt @@ -0,0 +1,18 @@ +import com.mapconductor.core.circle.CircleCapable +import com.mapconductor.core.controller.MapViewController +import com.mapconductor.core.marker.MarkerCapable +import com.mapconductor.core.polygon.PolygonCapable +import com.mapconductor.core.polyline.PolylineCapable +import com.mapconductor.mapbox.MapboxDesignType +import com.mapconductor.mapbox.MapboxMapDesignTypeChangeHandler + +interface MapboxMapViewController : + MapViewController, + MarkerCapable, + PolylineCapable, + PolygonCapable, + CircleCapable { + fun setMapDesignType(value: MapboxDesignType) + + fun setMapDesignTypeChangeListener(listener: MapboxMapDesignTypeChangeHandler) +} diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt index f3419ad9..0296aecf 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxMapViewControllerImpl.kt @@ -1,5 +1,6 @@ package com.mapconductor.mapbox +import MapboxMapViewController import androidx.compose.ui.geometry.Offset import com.mapbox.android.gestures.MoveGestureDetector import com.mapbox.geojson.Point @@ -8,6 +9,8 @@ import com.mapbox.maps.CameraChangedCallback import com.mapbox.maps.CameraOptions import com.mapbox.maps.CameraState import com.mapbox.maps.ScreenCoordinate +import com.mapbox.maps.StyleLoaded +import com.mapbox.maps.StyleLoadedCallback import com.mapbox.maps.extension.style.layers.addLayer import com.mapbox.maps.extension.style.sources.addSource import com.mapbox.maps.plugin.animation.MapAnimationOptions @@ -21,23 +24,18 @@ import com.mapbox.maps.plugin.gestures.addOnMoveListener import com.mapbox.maps.plugin.gestures.removeOnMapClickListener import com.mapbox.maps.plugin.gestures.removeOnMapLongClickListener import com.mapbox.maps.plugin.gestures.removeOnMoveListener -import com.mapconductor.core.circle.CircleCapable import com.mapconductor.core.circle.CircleEvent import com.mapconductor.core.circle.CircleState import com.mapconductor.core.circle.OnCircleEventHandler import com.mapconductor.core.controller.BaseMapViewController -import com.mapconductor.core.controller.MapViewController import com.mapconductor.core.map.MapCameraPosition import com.mapconductor.core.map.MapViewState -import com.mapconductor.core.marker.MarkerCapable import com.mapconductor.core.marker.MarkerState import com.mapconductor.core.marker.OnMarkerEventHandler import com.mapconductor.core.polygon.OnPolygonEventHandler -import com.mapconductor.core.polygon.PolygonCapable import com.mapconductor.core.polygon.PolygonEvent import com.mapconductor.core.polygon.PolygonState import com.mapconductor.core.polyline.OnPolylineEventHandler -import com.mapconductor.core.polyline.PolylineCapable import com.mapconductor.core.polyline.PolylineState import com.mapconductor.mapbox.circle.MapboxCircleController import com.mapconductor.mapbox.marker.MapboxMarkerController @@ -48,23 +46,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -interface MapboxMapViewController : - MapViewController, - MarkerCapable, - PolylineCapable, - PolygonCapable, - CircleCapable { - fun moveCamera( - dstPosition: MapCameraPosition, - listener: MapViewState.MoveCameraCallback? = null, - ) - - fun animateCamera( - dstPosition: MapCameraPosition, - duration: Long, - listener: MapViewState.MoveCameraCallback? = null, - ) -} +typealias MapboxMapDesignTypeChangeHandler = (MapboxDesignType) -> Unit internal class MapboxMapViewControllerImpl( override val holder: MapboxMapViewHolder, @@ -76,6 +58,7 @@ internal class MapboxMapViewControllerImpl( ) : BaseMapViewController(), MapboxMapViewController, CameraChangedCallback, + StyleLoadedCallback, OnMapClickListener, OnMapLongClickListener, OnMoveListener { @@ -110,6 +93,7 @@ internal class MapboxMapViewControllerImpl( fun setupListeners() { holder.map.subscribeCameraChanged(this) + holder.map.subscribeStyleLoaded(this) holder.map.removeOnMapClickListener(this) holder.map.addOnMapClickListener(this) @@ -177,7 +161,7 @@ internal class MapboxMapViewControllerImpl( coroutine.launch { holder.map.setCamera(cameraOptions) } - listener?.onComplete(true) + listener?.onComplete() } override fun animateCamera( @@ -208,11 +192,11 @@ internal class MapboxMapViewControllerImpl( } override fun onAnimationEnd(animation: Animator) { - listener?.onComplete(true) + listener?.onComplete() } override fun onAnimationCancel(animation: Animator) { - listener?.onComplete(false) + listener?.onComplete() } override fun onAnimationRepeat(animation: Animator) { @@ -346,4 +330,26 @@ internal class MapboxMapViewControllerImpl( override fun setOnPolygonClickListener(listener: OnPolygonEventHandler?) { polygonController.clickListener = listener } + + private var mapDesignType: MapboxDesignType = MapboxMapDesign.Standard + + private var mapDesignTypeChangeListener: MapboxMapDesignTypeChangeHandler? = null + + override fun setMapDesignType(value: MapboxDesignType) { + coroutine.launch { + holder.mapView.mapboxMap.loadStyle(value.getValue()) + } + } + + override fun setMapDesignTypeChangeListener(listener: MapboxMapDesignTypeChangeHandler) { + mapDesignTypeChangeListener = listener + listener(mapDesignType) + } + + override fun run(styleLoaded: StyleLoaded) { + holder.map.style?.toMapDesignType()?.let { mapDesignType -> + this@MapboxMapViewControllerImpl.mapDesignType = mapDesignType + mapDesignTypeChangeListener?.invoke(mapDesignType) + } + } } diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt index 35c1f7e6..c0c3d49e 100644 --- a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt +++ b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt @@ -1,5 +1,6 @@ package com.mapconductor.mapbox +import MapboxMapViewController import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -18,28 +19,54 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -interface MapboxViewState : MapViewState +interface MapboxViewState : MapViewState class MapboxViewStateImpl( + mapDesignType: MapboxDesignType, override val id: String, - override val mapDesignType: MapboxDesignType, override val initCameraPosition: MapCameraPosition = MapCameraPosition.Default, -) : MapViewStateImpl(), +) : MapViewStateImpl(), MapboxViewState { - internal var controller: MapboxMapViewController? = null + private var controller: MapboxMapViewController? = null // Camera center position private val _cameraPosition = MutableStateFlow(initCameraPosition) override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() + private var _mapDesignType: MapboxDesignType = mapDesignType + + override var mapDesignType: MapboxDesignType + set(value) { + value?.let { + _mapDesignType = it + this.controller?.setMapDesignType(it) + } + } + get() = _mapDesignType + + internal fun setController(controller: MapboxMapViewController) { + this.controller = controller + _mapDesignType?.let { + controller.setMapDesignType(it) + } + controller.moveCamera(_cameraPosition.value) + } + + internal fun onMapDesignTypeChange(value: MapboxDesignType) { + _mapDesignType = value + } + override fun moveCameraTo( position: GeoPoint, durationMs: Long, listener: MapViewState.MoveCameraCallback?, ) { if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) + _cameraPosition.value = + MapCameraPosition( + position = position, + ) + listener?.onComplete() return } val currentPosition = this.cameraPosition.value @@ -50,26 +77,27 @@ class MapboxViewStateImpl( this.moveCameraTo(newPosition, durationMs, listener) } + @Suppress("UNCHECKED_CAST") + override fun getMapViewHolder(): MapboxMapViewHolder? = controller?.holder as? MapboxMapViewHolder + override fun moveCameraTo( cameraPosition: MapCameraPosition, durationMs: Long, listener: MapViewState.MoveCameraCallback?, ) { - if (this.isInitialized.value != InitState.Initialized) { - this.warningLog("moveCameraTo() called before map is initialized.") - listener?.onComplete(false) - return - } - val dstCameraPosition = MapCameraPosition.from(cameraPosition) - if (controller == null) { - listener?.onComplete(false) - return - } - if (durationMs == 0L) { - controller!!.moveCamera(dstCameraPosition, listener) - } else { - controller!!.animateCamera(dstCameraPosition, durationMs.toLong(), listener) + controller?.let { ctrl -> + if (this.isInitialized.value == InitState.Initialized) { + val dstCameraPosition = MapCameraPosition.from(cameraPosition) + if (durationMs == 0L) { + ctrl.moveCamera(dstCameraPosition, listener) + } else { + ctrl.animateCamera(dstCameraPosition, durationMs, listener) + } + return + } } + _cameraPosition.value = cameraPosition + listener?.onComplete() } internal fun onCameraChange(cameraPosition: MapCameraPosition) { @@ -84,7 +112,7 @@ class MapboxMapViewSaver : BaseMapViewSaver() { state: MapboxViewStateImpl, bundle: Bundle, ) { - bundle.putString("id", state.mapDesignType.id) + bundle.putString("id", state.mapDesignType?.id ?: "null") } override fun createState( diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonFillLayer.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonLayer.kt similarity index 100% rename from mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonFillLayer.kt rename to mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonLayer.kt diff --git a/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt b/mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt similarity index 100% rename from mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonController.kt rename to mapconductor-for-mapbox/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonOverlayRenderer.kt