From ec4ca333b299f7b937a6cfa7447ccd10d5ff2244 Mon Sep 17 00:00:00 2001 From: zhangquan <2523589960@qq.com> Date: Fri, 11 Jul 2025 17:17:49 +0800 Subject: [PATCH] =?UTF-8?q?1.=E6=B7=BB=E5=8A=A0=E6=A3=AE=E6=9E=97=E9=98=B2?= =?UTF-8?q?=E7=81=AB=E5=9C=B0=E5=9B=BE=E6=A8=A1=E6=9D=BF=E9=A1=B5=E7=9A=84?= =?UTF-8?q?=E5=88=9D=E7=89=88=EF=BC=9B2.=E6=B7=BB=E5=8A=A0=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=A0=8F=EF=BC=8C=E9=9B=86=E6=88=90=E7=BB=98=E5=88=B6?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E3=80=81=E7=BA=BF=E3=80=81=E9=9D=A2=E3=80=81?= =?UTF-8?q?=E6=9B=B2=E9=9D=A2=E3=80=81=E7=9B=B4=E7=AE=AD=E5=A4=B4=E3=80=81?= =?UTF-8?q?=E5=AE=BD=E7=AE=AD=E5=A4=B4=E3=80=81=E6=94=BB=E5=87=BB=E7=AE=AD?= =?UTF-8?q?=E5=A4=B4=E3=80=81=E5=8F=8C=E7=AE=AD=E5=A4=B4=E3=80=81=E6=B5=8B?= =?UTF-8?q?=E8=B7=9D=E3=80=81=E6=B5=8B=E9=9D=A2=E5=8A=9F=E8=83=BD=EF=BC=9B?= =?UTF-8?q?3.=E6=B7=BB=E5=8A=A0=E6=B8=85=E9=99=A4=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4=E6=89=80=E6=9C=89=E7=BB=98=E5=88=B6?= =?UTF-8?q?=E3=80=81=E6=B5=8B=E9=87=8F=E7=9A=84=E5=9C=B0=E5=9B=BE=E8=A6=81?= =?UTF-8?q?=E7=B4=A0=EF=BC=9B4.=E6=B7=BB=E5=8A=A0=E4=BB=93=E5=BA=93?= =?UTF-8?q?=E3=80=81=E6=B0=B4=E6=BA=90=E5=9C=B0=E7=9A=84=E7=BB=98=E5=88=B6?= =?UTF-8?q?=EF=BC=9B5.=E5=AE=9E=E7=8E=B0=E5=B1=9E=E6=80=A7=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E3=80=81=E7=BC=96=E8=BE=91=E6=9C=BA=E5=88=B6=EF=BC=9B?= =?UTF-8?q?6.=E6=B7=BB=E5=8A=A0=E5=BC=B9=E7=AA=97=EF=BC=8C=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E6=98=BE=E7=A4=BA=E5=92=8C=E4=BF=AE=E6=94=B9=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E6=96=B0=E5=A2=9E=E3=80=81=E7=82=B9=E5=87=BB=E7=9A=84?= =?UTF-8?q?=E5=9C=B0=E5=9B=BE=E8=A6=81=E7=B4=A0=E5=B1=9E=E6=80=A7=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + src/assets/icons/draw_location.png | Bin 0 -> 1546 bytes src/assets/icons/fire_warehouse.png | Bin 0 -> 8227 bytes src/assets/icons/toolbar_attack_arrow.png | Bin 0 -> 864 bytes src/assets/icons/toolbar_clear.png | Bin 0 -> 1175 bytes src/assets/icons/toolbar_curve_polygon.png | Bin 0 -> 1020 bytes src/assets/icons/toolbar_double_arrow.png | Bin 0 -> 1145 bytes src/assets/icons/toolbar_location.png | Bin 0 -> 1010 bytes src/assets/icons/toolbar_measure_area.png | Bin 0 -> 1621 bytes src/assets/icons/toolbar_measure_distance.png | Bin 0 -> 639 bytes src/assets/icons/toolbar_polygon.png | Bin 0 -> 1078 bytes src/assets/icons/toolbar_polyline.png | Bin 0 -> 849 bytes src/assets/icons/toolbar_straight_arrow.png | Bin 0 -> 753 bytes src/assets/icons/toolbar_warehouse.png | Bin 0 -> 822 bytes src/assets/icons/toolbar_watersource.png | Bin 0 -> 839 bytes src/assets/icons/toolbar_wide_arrow.png | Bin 0 -> 1132 bytes .../CesiumMap/mixins/useConfigSetting.js | 4 + .../useDrawTool/draw/attackArrowGraphic.js | 94 +++ .../draw/attackStraightArrowGraphic.js | 42 ++ .../useDrawTool/draw/curvePolygonGraphic.js | 50 ++ .../useDrawTool/draw/doubleArrowGraphic.js | 188 +++++ .../mixins/useDrawTool/draw/drawUtil.js | 672 ++++++++++++++++++ .../useDrawTool/draw/straightArrowGraphic.js | 41 ++ .../useDrawTool/draw/wideArrowGraphic.js | 93 +++ .../mixins/useDrawTool/drawAttackArrow.js | 137 ++++ .../mixins/useDrawTool/drawCurvePolygon.js | 132 ++++ .../mixins/useDrawTool/drawDoubleArrow.js | 137 ++++ .../CesiumMap/mixins/useDrawTool/drawPoint.js | 56 ++ .../mixins/useDrawTool/drawPolygon.js | 131 ++++ .../mixins/useDrawTool/drawPolyline.js | 105 +++ .../mixins/useDrawTool/drawStraightArrow.js | 148 ++++ .../mixins/useDrawTool/drawWideArrow.js | 134 ++++ .../CesiumMap/mixins/useDrawTool/index.js | 145 ++++ .../CesiumMap/mixins/useEventBus.js | 127 ++++ .../CesiumMap/mixins/useHoverPosition.js | 47 ++ .../CesiumMap/mixins/useMeasureTool.js | 223 ++++++ src/router/index.js | 16 + .../DialogPropertyGrid/PropertyGrid.vue | 596 ++++++++++++++++ .../forestFire/DialogPropertyGrid/index.js | 131 ++++ .../forestFire/DialogPropertyGrid/property.js | 123 ++++ .../systemTemplate/forestFire/Toolbar.vue | 514 ++++++++++++++ src/views/systemTemplate/forestFire/index.vue | 131 ++++ 42 files changed, 4219 insertions(+) create mode 100644 src/assets/icons/draw_location.png create mode 100644 src/assets/icons/fire_warehouse.png create mode 100644 src/assets/icons/toolbar_attack_arrow.png create mode 100644 src/assets/icons/toolbar_clear.png create mode 100644 src/assets/icons/toolbar_curve_polygon.png create mode 100644 src/assets/icons/toolbar_double_arrow.png create mode 100644 src/assets/icons/toolbar_location.png create mode 100644 src/assets/icons/toolbar_measure_area.png create mode 100644 src/assets/icons/toolbar_measure_distance.png create mode 100644 src/assets/icons/toolbar_polygon.png create mode 100644 src/assets/icons/toolbar_polyline.png create mode 100644 src/assets/icons/toolbar_straight_arrow.png create mode 100644 src/assets/icons/toolbar_warehouse.png create mode 100644 src/assets/icons/toolbar_watersource.png create mode 100644 src/assets/icons/toolbar_wide_arrow.png create mode 100644 src/components/CesiumMap/mixins/useDrawTool/draw/attackArrowGraphic.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/draw/attackStraightArrowGraphic.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/draw/curvePolygonGraphic.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/draw/doubleArrowGraphic.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/draw/drawUtil.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/draw/straightArrowGraphic.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/draw/wideArrowGraphic.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/drawAttackArrow.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/drawCurvePolygon.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/drawDoubleArrow.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/drawPoint.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/drawPolygon.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/drawPolyline.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/drawStraightArrow.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/drawWideArrow.js create mode 100644 src/components/CesiumMap/mixins/useDrawTool/index.js create mode 100644 src/components/CesiumMap/mixins/useEventBus.js create mode 100644 src/components/CesiumMap/mixins/useHoverPosition.js create mode 100644 src/components/CesiumMap/mixins/useMeasureTool.js create mode 100644 src/views/systemTemplate/forestFire/DialogPropertyGrid/PropertyGrid.vue create mode 100644 src/views/systemTemplate/forestFire/DialogPropertyGrid/index.js create mode 100644 src/views/systemTemplate/forestFire/DialogPropertyGrid/property.js create mode 100644 src/views/systemTemplate/forestFire/Toolbar.vue create mode 100644 src/views/systemTemplate/forestFire/index.vue diff --git a/package.json b/package.json index cf061db..6f643bc 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@element-plus/icons-vue": "2.3.1", + "@turf/turf": "^7.2.0", "@vueup/vue-quill": "1.2.0", "@vueuse/core": "10.11.0", "axios": "0.28.1", @@ -26,6 +27,7 @@ "js-cookie": "3.0.5", "jsencrypt": "3.3.2", "json-editor-vue": "^0.18.1", + "mitt": "^3.0.1", "nprogress": "0.2.0", "pinia": "2.1.7", "splitpanes": "3.1.5", diff --git a/src/assets/icons/draw_location.png b/src/assets/icons/draw_location.png new file mode 100644 index 0000000000000000000000000000000000000000..312d14631307e5b1ea4f283e869eb73aed1a7ac5 GIT binary patch literal 1546 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy~!3HF^H`mkxDajJoh?3y^w370~qErUQl>DSr z1<%~X^wgl##FWaylc_cg49rTIArU1JzCKpT`MG+DAT@dwxdlMo3=B5*6$OdO*{LN8 zNvY|XdA3ULckfqH$V{%1*XSQL?vFu&J;D8jzb> zlBiITo0C^;Rbi_HHrEQs1_|pcDS(xfWZNo192Makpx~Tel&WB=XRMoSU}&gdW~OIo zVrph)sH0$HU}&Uo07PcGh9*{~W>!Y#3Q(W~w5=#5%__*n4QdyVXRDM^Qc_^0uU}qX zu2*iXmtT~wZ)j<02{OaTNEfI=x41H|B(Xv_uUHvof=g;~a#3bMNoIbY0?5R~r2Ntn zTP2`NAzsKWfE$}v3=Jk=fazBx7U&!58GyV5Q|Rl9UukYGTy=3tP%6T`SPd=?sVqp< z4@xc0FD*(2MqHXQ$f^P>=c3falKi5O{QMkPCP^Az7BifB^?{Dj2SqGWM8kxDsRzV_CtDx~p72xifT_I*n5>z18O~r}V5;+UaSW-r zwInn)@0NqWyvW5D+|?^zDo=GE=89ELSH$KoILYY~s#@7HPvHxTufM>( zRc#(Wm@d@nu)Z(3#(m5_+vDoQHCy-YzF%kj`K*ppqwRkiyT8Fr!U`D-3s(Q1xPU>l zCxj#Z>F!r^bLG?OclodQ% zF4Ubdb5rBjD|@!szYfV>%`SSVb*9(P2TD_a2~InGQ-rVPwwUx>jhA~Te0;aa_RTJV zD5;$u4+EMr75didpUOI<{POz=`)NIKlW(`5lXh&Gxg#U}kp9JpxNQ|L)DB&=-62^Y zv;O4L_7@QeGB125IEO42y*#;Q(MHj=GvD8AiA`jg^jhI@m!gJxQVWNXa~${al$!j& zjV~r|?y(5^_^@rEgN>;~_vwghX-};rUDve&-oxboqXU0=4+ZL-?o2us+?Co01 z^y9|7s6*dRWV`P^dFIj9msyt{=}QW(_I7$DtuT?LRI!Dd?bMacPcvE)mszSr-ufL= zc>SQmS^n%xx2GpP{=X>qeSKuy&&sz8*-n2y{Q7uw(}ut+d)_=LFtE1nI=Q5*t#~=_ z6PEuCVk_?VZU4))qv}D`k>5ENLgqE5%rx0t&VB2gZk9jiWRDlV=lj{L^;i1Z)SGb} zc^mw0qB^74J!9_=Y42C>&HdCQyI|kT&OrAUXYYmC{)oT4;C0^K)Bm^(82&m+r?)jL QSAt4sPgg&ebxsLQ0MgT1FiyEtDX5ZE?C9Yr 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 literal 0 HcmV?d00001 diff --git a/src/assets/icons/toolbar_curve_polygon.png b/src/assets/icons/toolbar_curve_polygon.png new file mode 100644 index 0000000000000000000000000000000000000000..a1e47945a69da715baddad88b7a5e3eae1377028 GIT binary patch literal 1020 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 literal 0 HcmV?d00001 diff --git a/src/assets/icons/toolbar_measure_area.png b/src/assets/icons/toolbar_measure_area.png new file mode 100644 index 0000000000000000000000000000000000000000..749c8c394cf40158696141a463c33c93c8c32095 GIT binary patch literal 1621 zcmV-b2CDgqP)t{Oos zGgA;G6@x;AXlsZKn5L#>Ym0`6N@7gq4;7>JBN#*7&1UZ1-MNoBGxrQLcR#{k_4(`G zea^Xi&ilU4^S)!vwtk869{vxIIBLyA>i^{wLy-9cWPnXMDidZ&pIrp^N~`7)4w>;z z#J@Wc>vuZ)eSblY)|@WOIvg-q3H*4<@zDvhdCz?_(OXJl(u_9(oqhzWj>ycGjK+P^ zS+glm0T)U9ErPYxiv6X(AV;IEF*BY3DiV)Q$84Q2^OxlAR2NvrWMr)N|7a)zwb(3t zq=;S8>Qc%P;C$c@<`}D*{jExCiM<9Rz_+W8o5Q8p#wA$ zb^M}GgpFBUOs`nZ0KQWl=gL#iw$(2%B@fn~XySE4=b*v272V0w$zrWx^w7Tm!TvHq|S3IPlF>wU_P%qmvfo~*g z8Qkih1-t99RRiadd&wklp?@Drfne64Eb*Co#U2cs0`8UET*`ifalwD1i1pR7{nm4N zO71tfsr$aj1zOVCO7W5MUb4^!7RzP8E@?%n7q<6Fyr_tEJDeT&Tp=ZrxUOEYhr(Wu zR$Fh%;HbA>!Sfx*y1ow#L|{`rwkP|$a@;AMQBJ!>c`@R|ym9&GV*8Wl@|64#*y2Bi zv9;#xv9Q;pnd)uIc&aOB@vAiELtlWCtrVa7<(<%5aFjA>aDm{3dB=M{Z}tz*HKmGD zo-h~#QiILa*baof9$rzFb^FKZg2~LW=@t2a$2X)kqbWyn3qGDX*6lQV(Q|oMJZQ$H zf*C8Zu{LVQ!d{QOBOdgM5nc!R{VvoYhhnZ4BIq%-Li z)7u3vDPnv_*`5u1J-h?vO{t7{cq-=8p(A=M=>t9jLoPUApMH&ROg<{WdBtK3Xr}sz zH=n$&9;`WT7>w*tK^IfdnByH$#vz}xqJ7>Yt?48j%Nx^QJ8=2VioKA3w~Izw*PF4Y z+myst>J^*llRYV(o~2!*yTrc_8Ig%~GJXo2R`mZ?s`Hg`7~^a&qq1XcdtQtI%~W4P zd@X<7UW@I%{JD#Q3BBwC12RkuVj16`E^}?jS+|lq9Ktwrsd=E0s9{j+`hc;u?(7GB z^5vPrtLd$NpqX=Q7)|;6Pz`tp9;i9Cg`9;!!eCVO-O?m zkV?(s0S0|wXpn&YD;!q^4R?8|Lds8o4+19@vA$lm=K}AsJJm&*_<6UW#W7;W!v&T# zqQODJx6|n|*AHqqZ>PQ{-jz%7{X3jJA9#q-i&H7A0ITK zfZhSJc@Yg1VGsUgXt4AswA0000000NkvXXu0mjf4R{p3 literal 0 HcmV?d00001 diff --git a/src/assets/icons/toolbar_measure_distance.png b/src/assets/icons/toolbar_measure_distance.png new file mode 100644 index 0000000000000000000000000000000000000000..afab6c23161aa97429806a2d30288aba528815b0 GIT binary patch literal 639 zcmV-_0)YLAP)&fd8V+w3#4tegR}#?kv;8= z14M|B2d*npFT2dd)r3IRS7TPRi3Hj?yc<(C!)&OCBWTOh92{8nTq!j(9d}&tbw$7w zKcMX2g0>$?#DmEp%tTP1cV+(>SoHO3q#SdEr*p2X+rkj2spbQp8yVs2oGVLlJ|Qp) z#sUd}&QAeh_0#T8abHSUC*S0VO zs(}_zJ{~}TA}NM0@FdVeV~~K|h*F&MCge@k*T*QQNpZ)^$U8gGG(%MueO{V))8%4C z#O&ObTA*j5h(`#YJy)(R26{@BPklF{&KX$P*0&m2(#J}sBY^9!tpIQ8D{y?E^s(tIt^>8d@mo8e78VQKr{UX z;ubiHs8nI>G?7#^TE~9)?SQ6nV za8sdP9@v5e?Qfp`m=V4A4Mh-B?>z(NgCrF)X%jI%;JU3~0DEKYSr>gjNJha}U@R~e Z_yZb(05CQP=wbi>002ovPDHLkV1h=O9CiQz literal 0 HcmV?d00001 diff --git a/src/assets/icons/toolbar_polygon.png b/src/assets/icons/toolbar_polygon.png new file mode 100644 index 0000000000000000000000000000000000000000..9a1cf320f529aa7b5cbe15a51b0e83e939ae3637 GIT binary patch literal 1078 zcmV-61j+k}P)E_=DlR=lMV9J%8SL zKb~WBnOGVjWgieqJXx#QUN4>l^Q|sWpVeB;bLfnIjg8gN-t+fuSgfT3B-61fXuTQdIa$OS!7vW!0T(2<*{8Jqe?5#!ixDT{paZ|rzT#=`?MousR^c!3Yl=8$=@dXt5 zLRbd;3>*jUFz9ck%qn6<&Fn<$p|fx!@D*^Yk1WLXs{q$~08QYSVA5a`IGVv^0F%=p z$9I^`xkvU(r4ut8K&%JmvXJG^nw>1VY{%kMBN7J;%6TMU9x<+rBl!!<9PKM}>}mMT z%+4Hy4#bJ>HTYNt5+k+9KJa22`IyIvRs!R|YQcB4$ZjqYE`;4uIG6HIz7%0kEwZP) z*oI#&aUy46V>b*q#u6PgxDWAbwPII$p^Y9IC~r4r>#kjb!EKSbzw|7;fqJ5GbX zHO_qZ4aBKlK)mAG<&ewWAy;UQo{vC0Q@Ne&-!JnU@kq5|Z?&YHo*R&4^_0JWyyrdu|zu!o_Aea$iS%Cl#4q931s2_s4ra=I0JldMnj^Z5{XPLUGQ3>6(zXI#BCVEkehPVa%bGs z7E^+6aKC_wl4xY&RbZVf{moc28rlzj8J-XBkSWh`i{=>uh7d!o(OT@2&cW%zKX0G3 zfV(fqz`1ZcUYxQDV~8v_lp8anV?F4;Ke}e16Fa44;S|H!VjecJ*$bygdv2h>ZAPVh2}OS<2p)W zgTJGMj01SKT<6&t1*%TN5Dxe;iAu?GuOBC=w$q@73`6B2lU93Ml2l`a1~L%FFCOuJ zY`K40foiH4z=+>$${Z*~+?8iF>0vosk?_|)AK=N#US@fAFC$KCjd0BW4q&{1B)n9L zc&e>H^9g}kJ}nnH)EWn>sXhg;NyKZh<#!>NL^7r&D41V}#)ASc5%@Lh!RSyQfAIN~ z0!gaPBtG`HTf!WZR;Z|3!y(+&~-#5fW!l=}Eno=v7Ha8MfTbNDka zdPKY)TYeo1ZKz1S52e{3pjkL!@%Rq%S~9K|N%&7RNvtnfej6^hhp+`ns_zNB;>QBc zTZ`3O$-nb_I+D6}vw?jAK9Rv^rhyY67UV}QG)_Sfki=2fvr$=NF(T|MljC_d6Cdi@ zq8|6?_57H5Bzzu6e5=*cmMKrcT&bmcS-|m708p@4IGYum7l9F{4MsTHeE6)ng;~Ev z@g1@rn%Xcg0{)Mt-{C6*tOZa$(x}#1M@1-30;A% bKv&=nRQ)3`6EPbB00000NkvXXu0mjfm=lJ) literal 0 HcmV?d00001 diff --git a/src/assets/icons/toolbar_straight_arrow.png b/src/assets/icons/toolbar_straight_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..cddd6e602727ca4b8880afd071353787c067224c GIT binary patch literal 753 zcmVGD7{;GxU7cA;FLepr0lRb%f-VIiA)S&ap?@p{9lCU>z%jZ7bLdoQ5?zWqd9l)< zA~FiYQ$Y|x;5kU?5Oq+(omKaZZ=HFW)!lVw9rDueH19Lte9v$H%+3?$cr;k?vj%b*ie%I!dP63+=q*RVF(R0<}tOfxM1w4_qP9LwgT3*3IyGt(V z98gyvD1Q#%sD{lV`0eJnQ|AaZ1yU_vPf;NXLlz&03!~7K&-oY!a9YzC81wMKXFlV+g_B`< zNFed3)*q1&juvI_D)5Owho%V-U~1v(XLqK}DnyZhNI(!hcnO$XB&U#EM93wI7q1dfiR|>64M9l^F~($PCQ3pG zUi9E4QO_P+JV`ED(1QmNQBWj^nAJ$^-k*L$%~aRUUuI^_xO>TSnX0$G?t13yDr1we zFxn*kvp^%&zFfTJrzev8Y~GL8#kvI=sh*YF*mx#{4sjLZL^ZZALN)>;3)C_lFmV)k z+RX(wS2R(dV8y+qYVd&17ckyUD)yOwbRYu-B-yzo4&>rZgB{HbZX!O2V&5cG;2Rkyt7SenGx@BCz5>Uk=>8>MwARK?`S>5id1B61&HnUR2X*NoWgG@x zGnn%6cNi!CE_3;~S?;qyhrng2^y@M&Dd(uclRmx$T*xb&nKAprX9Jm&9=42GgI9np zKK|Y!-uY{iPufL-KrPjN_Yck3?&gA2a8VUbzhU-!QMVq>O4~{qM+C1%rM#?Fx1Ieg zW2QJGL7<-MC*U!`5^!m?!l}b%*NgSLp&>n@ta*SB<}eunQx&ETn%xQlja1Lc*m1qQ z<)bt9?{7suPU_O5WF$mhtX)eYJO zJof~~E8xFt8$$$y%59GmeyS%{#fM?Bjgs;5>#_aN$aG9Od%}W8#zGQX5`sGjd}**Z1Z#X+jqQcG zCGCuIuJxWt$Rl>mlx?A&>I(3@Pn~{qB$!WP+tI+DUN1&T-f%f)0Miv-$21+^Cy1&1pSD=$o z1zNXY)d!{4kM+3)uB6l~Xcdp;7Hl;41U6mZHUx>0Fhhll1^@s607*qoM6N<$f+0|R A!2kdN literal 0 HcmV?d00001 diff --git a/src/assets/icons/toolbar_watersource.png b/src/assets/icons/toolbar_watersource.png new file mode 100644 index 0000000000000000000000000000000000000000..af329d1ce19354eca60598a67e264c193d11413e GIT binary patch literal 839 zcmV-N1GxN&P)KbXG2WE)+rV0fwmPpn@*45wdfkK_U1=MM>8Lq7WGsQ8d*PMkJdk zf`Z^qcj7XHEV76oF*`*kI~55Lx~ivBM{ZB|p{MPxu8xr)_^qn`d;0cQb*s)TahZ`H zT_*nf0$Fam(?nl;FeCp+AS>*kjJGg8Yc}+h56r+t39On$R~4)aducF*!I3o4i~W+? zT$Dhouun>3Z{$gzh^)Dw6+f@R%mP_%9|*SpNtng94omUGN~|xo&ZaYyK&x#}BaV77 zN(5HYiNsy|Vx954g4zOE$L^!#n8bYVYQz06eRvyYY+RAB^A+h=4<}GdVBKt~n{yg* zgNIWvIGQGU)`JPu63E*2xx^k1YEr_kl|p0wU$G2TNQ2#00Fy(fdcsFh6^}*06l>(XBjVk!XgBqCeb2H(lRzX(~nfNq~wbu(qs1)cu{RQVds3fdW z6mCT8e9Zo(f^L8QwPvEGmyJ*<5NNgSEaG|(DzIK7qANNcC#&m&P z??buYg9;1|r-}AuxqT^k%%Bs;T=%l{yBCcxRbb8RS}XVoh&>$7_ZMRBpDTD1NJC)8 z+i62@3<}0ERUpvH?FHb72P2sAn@T=pjwgXzwnC0CCOrJSwAn8kOD%ywE4Np}ivi4e zIDyDG86`Y=$@YQ)hc@jYUEr`_hlkrK3_dy^^V+J^@w9@O1On@3@n+5gL@cW$LF_O% zW8zq}z&A~)?Zq{iS>RGrOW;spx6O&2Wyu|a+v#%Uq$k1PED@(=oa)4!I3QIvvPyxq z+@33qcOOxZnTx|N7q!R0CEIfT=<8-eU@nBhQO9YERd zjD5fV11$x*YT6GF17sK2G@$Q*=74m8_4b2}g18*$7$5_vY$BNK26`Xp9f)heiu-;+ z9RO6b3TXJZ|9$`ePx${5s0-*kpw-Z@0xJITukXiSkX>NG{vUsV9_;)14`?+IF!@d7 z1cr4^NswPK1D8wvHb=3r31|NzPlQyyVc*6+4z}Im5Z@&Qm#khL9T@POZv{=M7kC>R2DE5aAywrO(ThlarN~ z$r@lJk@9hoQ}ph0&LO93I!ij(OUg={Ga9GMC9~~aSRh}1Zec*y{;e&HaSTGK7m^G_ zT=wal`M#mmeNoeHF6A^kD>gPs^N*dLQ4h|q_o^~Xee-C`yH%wr#|}J|aA8f*16yPt3#Sh=h(_1ubsmYi!R@XJ2^nEi@-!pehBr#|Z} zK6A)4`2LPc-jmUPMUTFJbFbplyp%@a+70uYJi6|ESbTZT3C&5zE^mH3l42gZSFiEBhjN@7W>RdP`(kYX@0Ff!FOFxNG( z2r)3RGBB_*Hqi!>1_nnQ&jg@o$jwj5OsmALVXI8Z8=wXaxD6$lxv9k^iMa*1^*rJX RH3sTo@O1TaS?83{1OSwWVIKeh literal 0 HcmV?d00001 diff --git a/src/components/CesiumMap/mixins/useConfigSetting.js b/src/components/CesiumMap/mixins/useConfigSetting.js index b12cb5c..31830d2 100644 --- a/src/components/CesiumMap/mixins/useConfigSetting.js +++ b/src/components/CesiumMap/mixins/useConfigSetting.js @@ -63,6 +63,10 @@ export function useConfigSetting(viewer) { // 地图设置视角 const mapSetView = (options) => { + if (!options.setView) { + return; + } + let destination = Cesium.Cartesian3.fromDegrees(options.setView.x, options.setView.y, options.setView.z); let orientation = { heading: Cesium.Math.toRadians(options.setView.heading), diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/attackArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/attackArrowGraphic.js new file mode 100644 index 0000000..c5470d4 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/attackArrowGraphic.js @@ -0,0 +1,94 @@ +import * as Cesium from 'cesium' +import DrawUtil from './drawUtil' +import { createAttackStraightArrowPositions } from './attackStraightArrowGraphic' + +// const tailWidthFactor = 0.07 +const neckWidthFactor = 0.07 +const headWidthFactor = 0.1 +const headAngle = Math.PI - Math.PI / 8.5 +const neckAngle = Math.PI - Math.PI / 13 + +// 创建箭头点数组 +export function createAttackArrowPositions(worldPositions) { + if (!worldPositions || worldPositions.length < 3) { + return [] + } else if (worldPositions.length === 3) { + return createAttackStraightArrowPositions(worldPositions) + } + + let pnts = worldPositions.map((car3) => { + const carto = Cesium.Cartographic.fromCartesian(car3) + return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)] + }) + // 箭头尾部中间 + let pntTail = DrawUtil.mid(pnts[0], pnts[1]) + // 箭头顶点 + let pntHead = pnts[pnts.length - 1] + // 靠近尾点的点 + let pntTailNear = pnts[1] + // 靠近顶点的点 + let pntNeck = pnts[pnts.length - 2] + + let len = DrawUtil.getBaseLength(pnts) + // let tailWidth = len * tailWidthFactor + // let neckWidth = len * neckWidthFactor + // let headWidth = len * headWidthFactor + const tailWidth = DrawUtil.getBaseLength(pnts.slice(0, 2)) / 2 + const neckWidth = tailWidth + const headWidth = (tailWidth * 10) / 7 + const isClockWise = DrawUtil.isClockWise(pnts[0], pnts[1], pnts[2]) + // 箭头左尾点 + let tailLeft = isClockWise ? pnts[0] : pnts[1] + // 箭头右尾点 + let tailRight = isClockWise ? pnts[1] : pnts[0] + // 箭头左端点 + let headLeft = DrawUtil.getThirdPoint(pntNeck, pntHead, headAngle, headWidth, false) + // 箭头右端点 + let headRight = DrawUtil.getThirdPoint(pntNeck, pntHead, headAngle, headWidth, true) + // 箭头左颈点 + let neckLeft = DrawUtil.getThirdPoint(pntNeck, pntHead, neckAngle, neckWidth, false) + // 箭头右颈点 + let neckRight = DrawUtil.getThirdPoint(pntNeck, pntHead, neckAngle, neckWidth, true) + + // 生成样条曲线 + const innerPoints = pnts.slice(2, pnts.length - 1) + const minHalfWidth = neckWidth * Math.sin(Math.PI / 13) + const leftControls = [ + tailLeft, + ...DrawUtil.getArrowAsideControlPoints( + [tailLeft, ...innerPoints, neckLeft], + tailWidth, + true, + minHalfWidth, + ), + neckLeft, + ] + // const leftPoints = DrawUtil.getBezierPoints(leftControls) + const leftPoints = DrawUtil.getQBSplinePoints(leftControls) + // innerPoints.reverse() + const rightControls = [ + neckRight, + ...DrawUtil.getArrowAsideControlPoints( + [tailRight, ...innerPoints, neckRight], + tailWidth, + false, + minHalfWidth, + ), + tailRight, + ] + // const rightPoints = DrawUtil.getBezierPoints(rightControls) + const rightPoints = DrawUtil.getQBSplinePoints(rightControls) + const pts = [ + tailLeft, + ...leftPoints, + neckLeft, + headLeft, + pntHead, + headRight, + neckRight, + ...rightPoints, + tailRight, + ] + + return DrawUtil.positionDistinct(pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/attackStraightArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/attackStraightArrowGraphic.js new file mode 100644 index 0000000..42c58b2 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/attackStraightArrowGraphic.js @@ -0,0 +1,42 @@ +import * as Cesium from 'cesium' +import DrawUtil from './drawUtil' + +// const tailWidthFactor = 0.07 +const neckWidthFactor = 0.07 +const headWidthFactor = 0.1 +const headAngle = Math.PI - Math.PI / 8.5 +const neckAngle = Math.PI - Math.PI / 13 + +// 创建箭头点数组 +export function createAttackStraightArrowPositions(worldPositions) { + // let pnts = Parse.parsePolygonCoordToArray( + // Transform.transformCartesianArrayToWGS84Array(this._positions) + // )[0] + let pnts = worldPositions.map((car3) => { + const carto = Cesium.Cartographic.fromCartesian(car3) + return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)] + }) + // 箭头尾部中间 + let pnt1 = DrawUtil.mid(pnts[0], pnts[1]) + // 箭头顶点 + let pnt2 = pnts[2] + let len = DrawUtil.getBaseLength(pnts) + let tailWidth = DrawUtil.getBaseLength(pnts.slice(0, 2)) / 2 + let neckWidth = len * neckWidthFactor + let headWidth = len * headWidthFactor + const isClockWise = DrawUtil.isClockWise(pnts[0], pnts[1], pnts[2]) + // 箭头左尾点 + let tailLeft = isClockWise ? pnts[0] : pnts[1] + // 箭头右尾点 + let tailRight = isClockWise ? pnts[1] : pnts[0] + // 箭头左端点 + let headLeft = DrawUtil.getThirdPoint(pnt1, pnt2, headAngle, headWidth, false) + // 箭头右端点 + let headRight = DrawUtil.getThirdPoint(pnt1, pnt2, headAngle, headWidth, true) + // 箭头左颈点 + let neckLeft = DrawUtil.getThirdPoint(pnt1, pnt2, neckAngle, neckWidth, false) + // 箭头右颈点 + let neckRight = DrawUtil.getThirdPoint(pnt1, pnt2, neckAngle, neckWidth, true) + const pts = [tailLeft, neckLeft, headLeft, pnt2, headRight, neckRight, tailRight] + return DrawUtil.positionDistinct(pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/curvePolygonGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/curvePolygonGraphic.js new file mode 100644 index 0000000..a00f2a5 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/curvePolygonGraphic.js @@ -0,0 +1,50 @@ +import * as Cesium from 'cesium' +import DrawUtil from './drawUtil' + +const FITTING_COUNT = 100 +const t = 0.4 + +export function createCurvePolygonPositions(worldPositions) { + let pnts = worldPositions.map((car3) => { + const carto = Cesium.Cartographic.fromCartesian(car3) + return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)] + }) + + if (pnts.length === 2) { + let mid = DrawUtil.mid(pnts[0], pnts[1]) + let d = DrawUtil.distance(pnts[0], mid) / 2 + let pntLeft = DrawUtil.getThirdPoint(pnts[0], mid, Cesium.Math.PI_OVER_TWO, d, false) + let pntRight = DrawUtil.getThirdPoint(pnts[0], mid, Cesium.Math.PI_OVER_TWO, d, true) + pnts = [pnts[0], pntLeft, pnts[1], pntRight] + } + // let mid = DrawUtil.mid(pnts[0], pnts[2]) + pnts.push(pnts[0], pnts[1]) + let normals = [] + for (let i = 0; i < pnts.length - 2; i++) { + let pnt1 = pnts[i] + let pnt2 = pnts[i + 1] + let pnt3 = pnts[i + 2] + let normalPoints = DrawUtil.getBisectorNormals(t, pnt1, pnt2, pnt3) + normals = normals.concat(normalPoints) + } + let count = normals.length + normals = [normals[count - 1]].concat(normals.slice(0, count - 1)) + let pList = [] + for (let i = 0; i < pnts.length - 2; i++) { + let pnt1 = pnts[i] + let pnt2 = pnts[i + 1] + pList.push(pnt1) + for (let t = 0; t <= FITTING_COUNT; t++) { + let pnt = DrawUtil.getCubicValue( + t / FITTING_COUNT, + pnt1, + normals[i * 2], + normals[i * 2 + 1], + pnt2, + ) + pList.push(pnt) + } + pList.push(pnt2) + } + return DrawUtil.positionDistinct(pList.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/doubleArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/doubleArrowGraphic.js new file mode 100644 index 0000000..cfa5c25 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/doubleArrowGraphic.js @@ -0,0 +1,188 @@ +import * as Cesium from 'cesium' +import DrawUtil from './drawUtil' + +// const tailWidthFactor = 0.07 +// const neckWidthFactor = 0.07 +// const headWidthFactor = 0.10 + +const headHeightFactor = 0.25 +const headWidthFactor = 0.3 +const neckHeightFactor = 0.85 +const neckWidthFactor = 0.15 +const headAngle = Math.PI - Math.PI / 8.5 +const neckAngle = Math.PI - Math.PI / 13 + +export function createDoubleArrowPositions(worldPositions) { + let count = worldPositions.length + let tempPoint4 = undefined + let connPoint = undefined + let pnts = worldPositions.map((car3) => { + const carto = Cesium.Cartographic.fromCartesian(car3) + return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)] + }) + let pnt1 = pnts[0] + let pnt2 = pnts[1] + let pnt3 = pnts[2] + if (count === 3) tempPoint4 = getTempPoint4(pnt1, pnt2, pnt3) + else tempPoint4 = pnts[3] + if (count === 3 || count === 4) connPoint = DrawUtil.mid(pnt1, pnt2) + else connPoint = pnts[4] + let leftArrowPnts, rightArrowPnts + if (DrawUtil.isClockWise(pnt1, pnt2, pnt3)) { + leftArrowPnts = getArrowPoints(pnt1, connPoint, tempPoint4, false) + rightArrowPnts = getArrowPoints(connPoint, pnt2, pnt3, true) + } else { + leftArrowPnts = getArrowPoints(pnt2, connPoint, pnt3, false) + rightArrowPnts = getArrowPoints(connPoint, pnt1, tempPoint4, true) + } + let m = leftArrowPnts.length + let t = (m - 5) / 2 + let llBodyPnts = leftArrowPnts.slice(0, t) + let lArrowPnts = leftArrowPnts.slice(t, t + 5) + let lrBodyPnts = leftArrowPnts.slice(t + 5, m) + let rlBodyPnts = rightArrowPnts.slice(0, t) + let rArrowPnts = rightArrowPnts.slice(t, t + 5) + let rrBodyPnts = rightArrowPnts.slice(t + 5, m) + rlBodyPnts = DrawUtil.getBezierPoints(rlBodyPnts) + let bodyPnts = DrawUtil.getBezierPoints(rrBodyPnts.concat(llBodyPnts.slice(1))) + lrBodyPnts = DrawUtil.getBezierPoints(lrBodyPnts) + // return new Cesium.PolygonHierarchy( + // Transform.transformWGS84ArrayToCartesianArray( + // Parse.parsePositions( + // rlBodyPnts.concat(rArrowPnts, bodyPnts, lArrowPnts, lrBodyPnts) + // ) + // ) + // ) + const pts = rlBodyPnts.concat(rArrowPnts, bodyPnts, lArrowPnts, lrBodyPnts) + return pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1])) +} + +function getArrowPoints(pnt1, pnt2, pnt3, clockWise) { + let midPnt = DrawUtil.mid(pnt1, pnt2) + let len = DrawUtil.distance(midPnt, pnt3) + let midPnt1 = DrawUtil.getThirdPoint(pnt3, midPnt, Cesium.Math.PI, len * 0.3, true) + let midPnt2 = DrawUtil.getThirdPoint(pnt3, midPnt, Cesium.Math.PI, len * 0.5, true) + midPnt1 = DrawUtil.getThirdPoint(midPnt, midPnt1, Cesium.Math.PI_OVER_TWO, len / 5, clockWise) + midPnt2 = DrawUtil.getThirdPoint(midPnt, midPnt2, Cesium.Math.PI_OVER_TWO, len / 4, clockWise) + let points = [midPnt, midPnt1, midPnt2, pnt3] + // 计算箭头部分 + let arrowPnts = getArrowHeadPoints(points) + let neckLeftPoint = arrowPnts[0] + let neckRightPoint = arrowPnts[4] + // 计算箭身部分 + let tailWidthFactor = DrawUtil.distance(pnt1, pnt2) / DrawUtil.getBaseLength(points) / 2 + let bodyPnts = getArrowBodyPoints(points, neckLeftPoint, neckRightPoint, tailWidthFactor) + let n = bodyPnts.length + let lPoints = bodyPnts.slice(0, n / 2) + let rPoints = bodyPnts.slice(n / 2, n) + lPoints.push(neckLeftPoint) + rPoints.push(neckRightPoint) + lPoints = lPoints.reverse() + lPoints.push(pnt2) + rPoints = rPoints.reverse() + rPoints.push(pnt1) + return lPoints.reverse().concat(arrowPnts, rPoints) +} + +function getArrowHeadPoints(points) { + let len = DrawUtil.getBaseLength(points) + let headHeight = len * headHeightFactor + let headPnt = points[points.length - 1] + let headWidth = headHeight * headWidthFactor + let neckWidth = headHeight * neckWidthFactor + let neckHeight = headHeight * neckHeightFactor + let headEndPnt = DrawUtil.getThirdPoint( + points[points.length - 2], + headPnt, + Cesium.Math.PI, + headHeight, + true, + ) + let neckEndPnt = DrawUtil.getThirdPoint( + points[points.length - 2], + headPnt, + Cesium.Math.PI, + neckHeight, + true, + ) + let headLeft = DrawUtil.getThirdPoint( + headPnt, + headEndPnt, + Cesium.Math.PI_OVER_TWO, + headWidth, + false, + ) + let headRight = DrawUtil.getThirdPoint( + headPnt, + headEndPnt, + Cesium.Math.PI_OVER_TWO, + headWidth, + true, + ) + let neckLeft = DrawUtil.getThirdPoint( + headPnt, + neckEndPnt, + Cesium.Math.PI_OVER_TWO, + neckWidth, + false, + ) + let neckRight = DrawUtil.getThirdPoint( + headPnt, + neckEndPnt, + Cesium.Math.PI_OVER_TWO, + neckWidth, + true, + ) + + return [neckLeft, headLeft, headPnt, headRight, neckRight] +} + +function getArrowBodyPoints(points, neckLeft, neckRight, tailWidthFactor) { + let allLen = DrawUtil.wholeDistance(points) + let len = DrawUtil.getBaseLength(points) + let tailWidth = len * tailWidthFactor + let neckWidth = DrawUtil.distance(neckLeft, neckRight) + let widthDif = (tailWidth - neckWidth) / 2 + let tempLen = 0 + let leftBodyPnts = [] + let rightBodyPnts = [] + for (let i = 1; i < points.length - 1; i++) { + let angle = DrawUtil.getAngleOfThreePoints(points[i - 1], points[i], points[i + 1]) / 2 + tempLen += DrawUtil.distance(points[i - 1], points[i]) + let w = (tailWidth / 2 - (tempLen / allLen) * widthDif) / Math.sin(angle) + let left = DrawUtil.getThirdPoint(points[i - 1], points[i], Math.PI - angle, w, true) + let right = DrawUtil.getThirdPoint(points[i - 1], points[i], angle, w, false) + leftBodyPnts.push(left) + rightBodyPnts.push(right) + } + return leftBodyPnts.concat(rightBodyPnts) +} + +function getTempPoint4(linePnt1, linePnt2, point) { + let midPnt = DrawUtil.mid(linePnt1, linePnt2) + let len = DrawUtil.distance(midPnt, point) + let angle = DrawUtil.getAngleOfThreePoints(linePnt1, midPnt, point) + let symPnt, distance1, distance2, mid + if (angle < Cesium.Math.PI_OVER_TWO) { + distance1 = len * Math.sin(angle) + distance2 = len * Math.cos(angle) + mid = DrawUtil.getThirdPoint(linePnt1, midPnt, Cesium.Math.PI_OVER_TWO, distance1, false) + symPnt = DrawUtil.getThirdPoint(midPnt, mid, Cesium.Math.PI_OVER_TWO, distance2, true) + } else if (angle >= Cesium.Math.PI_OVER_TWO && angle < Math.PI) { + distance1 = len * Math.sin(Math.PI - angle) + distance2 = len * Math.cos(Math.PI - angle) + mid = DrawUtil.getThirdPoint(linePnt1, midPnt, Cesium.Math.PI_OVER_TWO, distance1, false) + symPnt = DrawUtil.getThirdPoint(midPnt, mid, Cesium.Math.PI_OVER_TWO, distance2, false) + } else if (angle >= Math.PI && angle < Math.PI * 1.5) { + distance1 = len * Math.sin(angle - Math.PI) + distance2 = len * Math.cos(angle - Math.PI) + mid = DrawUtil.getThirdPoint(linePnt1, midPnt, Cesium.Math.PI_OVER_TWO, distance1, true) + symPnt = DrawUtil.getThirdPoint(midPnt, mid, Cesium.Math.PI_OVER_TWO, distance2, true) + } else { + distance1 = len * Math.sin(Math.PI * 2 - angle) + distance2 = len * Math.cos(Math.PI * 2 - angle) + mid = DrawUtil.getThirdPoint(linePnt1, midPnt, Cesium.Math.PI_OVER_TWO, distance1, true) + symPnt = DrawUtil.getThirdPoint(midPnt, mid, Cesium.Math.PI_OVER_TWO, distance2, false) + } + return symPnt +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/drawUtil.js b/src/components/CesiumMap/mixins/useDrawTool/draw/drawUtil.js new file mode 100644 index 0000000..776901e --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/drawUtil.js @@ -0,0 +1,672 @@ +/** + * @Author : Caven Chen + */ +import * as Cesium from 'cesium' +import * as turf from '@turf/turf' + +const TWO_PI = Math.PI * 2 +const FITTING_COUNT = 100 +const ZERO_TOLERANCE = 0.0001 + +class DrawUtil { + /** + * @param pnt1 + * @param pnt2 + * @returns {number} + */ + static distance(pnt1, pnt2) { + return Math.sqrt(Math.pow(pnt1[0] - pnt2[0], 2) + Math.pow(pnt1[1] - pnt2[1], 2)) + } + + /** + * @param points + * @returns {number} + */ + static wholeDistance(points) { + let distance = 0 + for (let i = 0; i < points.length - 1; i++) distance += this.distance(points[i], points[i + 1]) + return distance + } + + /** + * 把经纬度当平面坐标计算长度 + * @param points + * @returns {number} + */ + // static getBaseLength(points) { + // return Math.pow(this.wholeDistance(points), 0.99) + // } + static getBaseLength(points) { + let len = 0 + if (points.length < 2) return len + + for (let i = 1; i < points.length; i++) { + len += Math.sqrt( + Math.pow(points[i][0] - points[i - 1][0], 2) + Math.pow(points[i][1] - points[i - 1][1], 2), + ) + } + return len + } + + /** + * @param pnt1 + * @param pnt2 + * @returns {number[]} + */ + static mid(pnt1, pnt2) { + return [(pnt1[0] + pnt2[0]) / 2, (pnt1[1] + pnt2[1]) / 2] + } + + /** + * @param pnt1 + * @param pnt2 + * @param pnt3 + * @returns {[*, *]|[*, *]|[*, number]} + */ + static getCircleCenterOfThreePoints(pnt1, pnt2, pnt3) { + const pntA = [(pnt1[0] + pnt2[0]) / 2, (pnt1[1] + pnt2[1]) / 2] + const pntB = [pntA[0] - pnt1[1] + pnt2[1], pntA[1] + pnt1[0] - pnt2[0]] + const pntC = [(pnt1[0] + pnt3[0]) / 2, (pnt1[1] + pnt3[1]) / 2] + const pntD = [pntC[0] - pnt1[1] + pnt3[1], pntC[1] + pnt1[0] - pnt3[0]] + return this.getIntersectPoint(pntA, pntB, pntC, pntD) + } + + /** + * @param pntA + * @param pntB + * @param pntC + * @param pntD + * @returns {(*|number)[]|*[]} + */ + static getIntersectPoint(pntA, pntB, pntC, pntD) { + let x, y, f, e + if (pntA[1] === pntB[1]) { + f = (pntD[0] - pntC[0]) / (pntD[1] - pntC[1]) + x = f * (pntA[1] - pntC[1]) + pntC[0] + y = pntA[1] + return [x, y] + } + if (pntC[1] === pntD[1]) { + e = (pntB[0] - pntA[0]) / (pntB[1] - pntA[1]) + x = e * (pntC[1] - pntA[1]) + pntA[0] + y = pntC[1] + return [x, y] + } + e = (pntB[0] - pntA[0]) / (pntB[1] - pntA[1]) + f = (pntD[0] - pntC[0]) / (pntD[1] - pntC[1]) + y = (e * pntA[1] - pntA[0] - f * pntC[1] + pntC[0]) / (e - f) + x = e * y - e * pntA[1] + pntA[0] + return [x, y] + } + + /** + * 计算方位角 + * @param startPnt + * @param endPnt + * @returns {number} + */ + static getAzimuth(startPnt, endPnt) { + const lon1 = startPnt[0] + const lat1 = startPnt[1] + const lon2 = endPnt[0] + const lat2 = endPnt[1] + + if (lon1 === lon2) { + if (lat2 >= lat1) { + return 0 + } else { + return Math.PI + } + } + if (lat1 === lat2) { + if (lon2 > lon1) { + return Cesium.Math.PI_OVER_TWO + } else if (lon2 < lon1) { + return Cesium.Math.THREE_PI_OVER_TWO + } + } + + const a = Math.atan(((lon2 - lon1) * Math.cos(lat2)) / (lat2 - lat1)) + if (lat2 > lat1) { + if (lon2 > lon1) { + return a + } else { + return a + Cesium.Math.TWO_PI + } + } else { + return a + Cesium.Math.PI + } + } + + /** + * @param pntA + * @param pntB + * @param pntC + * @returns {number} + */ + static getAngleOfThreePoints(pntA, pntB, pntC) { + const angle = this.getAzimuth(pntB, pntC) - this.getAzimuth(pntB, pntA) + return angle < 0 ? angle + TWO_PI : angle + } + + /** + * @param pnt1 + * @param pnt2 + * @param pnt3 + * @returns {boolean} + */ + static isClockWise(pnt1, pnt2, pnt3) { + return (pnt3[1] - pnt1[1]) * (pnt2[0] - pnt1[0]) > (pnt2[1] - pnt1[1]) * (pnt3[0] - pnt1[0]) + // return (pnt2[1] - pnt1[1]) * (pnt3[0] - pnt1[0]) > (pnt2[1] - pnt1[0]) * (pnt3[1] - pnt1[1]) + } + + /** + * @param t + * @param startPnt + * @param endPnt + * @returns {*[]} + */ + static getPointOnLine(t, startPnt, endPnt) { + const x = startPnt[0] + t * (endPnt[0] - startPnt[0]) + const y = startPnt[1] + t * (endPnt[1] - startPnt[1]) + return [x, y] + } + + /** + * @param t + * @param startPnt + * @param cPnt1 + * @param cPnt2 + * @param endPnt + * @returns {number[]} + */ + static getCubicValue(t, startPnt, cPnt1, cPnt2, endPnt) { + t = Math.max(Math.min(t, 1), 0) + const tp = 1 - t + const t2 = t * t + const t3 = t2 * t + const tp2 = tp * tp + const tp3 = tp2 * tp + const x = tp3 * startPnt[0] + 3 * tp2 * t * cPnt1[0] + 3 * tp * t2 * cPnt2[0] + t3 * endPnt[0] + const y = tp3 * startPnt[1] + 3 * tp2 * t * cPnt1[1] + 3 * tp * t2 * cPnt2[1] + t3 * endPnt[1] + return [x, y] + } + + /** + * 根据第一点和第二点,从第二点沿固定角度延长一定距离,得到第三点 + * @param startPnt + * @param endPnt + * @param angle + * @param distance + * @param clockWise + * @returns {*[]} + */ + static getThirdPoint(startPnt, endPnt, angle, distance, clockWise) { + const azimuth = this.getAzimuth(startPnt, endPnt) + const alpha = clockWise ? azimuth + angle : azimuth - angle + const dx = distance * Math.sin(alpha) + const dy = distance * Math.cos(alpha) + return [endPnt[0] + dx, endPnt[1] + dy] + } + + /** + * @param center + * @param radius + * @param startAngle + * @param endAngle + * @returns {[]} + */ + static getArcPoints(center, radius, startAngle, endAngle) { + let x, + y, + pnts = [] + let angleDiff = endAngle - startAngle + angleDiff = angleDiff < 0 ? angleDiff + TWO_PI : angleDiff + for (let i = 0; i <= FITTING_COUNT; i++) { + const angle = startAngle + (angleDiff * i) / FITTING_COUNT + x = center[0] + radius * Math.cos(angle) + y = center[1] + radius * Math.sin(angle) + pnts.push([x, y]) + } + return pnts + } + + /** + * @param t + * @param pnt1 + * @param pnt2 + * @param pnt3 + * @returns {*[][]} + */ + static getBisectorNormals(t, pnt1, pnt2, pnt3) { + const normal = this.getNormal(pnt1, pnt2, pnt3) + const dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1]) + const uX = normal[0] / dist + const uY = normal[1] / dist + const d1 = this.distance(pnt1, pnt2) + const d2 = this.distance(pnt2, pnt3) + let dt, x, y, bisectorNormalLeft, bisectorNormalRight + if (dist > ZERO_TOLERANCE) { + if (this.isClockWise(pnt1, pnt2, pnt3)) { + dt = t * d1 + x = pnt2[0] - dt * uY + y = pnt2[1] + dt * uX + bisectorNormalRight = [x, y] + dt = t * d2 + x = pnt2[0] + dt * uY + y = pnt2[1] - dt * uX + bisectorNormalLeft = [x, y] + } else { + dt = t * d1 + x = pnt2[0] + dt * uY + y = pnt2[1] - dt * uX + bisectorNormalRight = [x, y] + dt = t * d2 + x = pnt2[0] - dt * uY + y = pnt2[1] + dt * uX + bisectorNormalLeft = [x, y] + } + } else { + x = pnt2[0] + t * (pnt1[0] - pnt2[0]) + y = pnt2[1] + t * (pnt1[1] - pnt2[1]) + bisectorNormalRight = [x, y] + x = pnt2[0] + t * (pnt3[0] - pnt2[0]) + y = pnt2[1] + t * (pnt3[1] - pnt2[1]) + bisectorNormalLeft = [x, y] + } + return [bisectorNormalRight, bisectorNormalLeft] + } + + /** + * @param pnt1 + * @param pnt2 + * @param pnt3 + * @returns {number[]} + */ + static getNormal(pnt1, pnt2, pnt3) { + let dX1 = pnt1[0] - pnt2[0] + let dY1 = pnt1[1] - pnt2[1] + const d1 = Math.sqrt(dX1 * dX1 + dY1 * dY1) + dX1 /= d1 + dY1 /= d1 + + let dX2 = pnt3[0] - pnt2[0] + let dY2 = pnt3[1] - pnt2[1] + const d2 = Math.sqrt(dX2 * dX2 + dY2 * dY2) + dX2 /= d2 + dY2 /= d2 + + const uX = dX1 + dX2 + const uY = dY1 + dY2 + return [uX, uY] + } + + /** + * @param t + * @param controlPoints + * @returns {[]} + */ + static getCurvePoints(t, controlPoints) { + const leftControl = this.getLeftMostControlPoint(t, controlPoints) + let normals = [leftControl] + let pnt1, pnt2, pnt3, normalPoints + for (let i = 0; i < controlPoints.length - 2; i++) { + pnt1 = controlPoints[i] + pnt2 = controlPoints[i + 1] + pnt3 = controlPoints[i + 2] + normalPoints = this.getBisectorNormals(t, pnt1, pnt2, pnt3) + normals = normals.concat(normalPoints) + } + const rightControl = this.getRightMostControlPoint(t, controlPoints) + normals.push(rightControl) + const points = [] + for (let i = 0; i < controlPoints.length - 1; i++) { + pnt1 = controlPoints[i] + pnt2 = controlPoints[i + 1] + points.push(pnt1) + for (let t = 0; t < FITTING_COUNT; t++) { + const pnt = this.getCubicValue( + t / FITTING_COUNT, + pnt1, + normals[i * 2], + normals[i * 2 + 1], + pnt2, + ) + points.push(pnt) + } + points.push(pnt2) + } + return points + } + + /** + * @param t + * @param controlPoints + * @returns {number[]} + */ + static getLeftMostControlPoint(t, controlPoints) { + const pnt1 = controlPoints[0] + const pnt2 = controlPoints[1] + const pnt3 = controlPoints[2] + const pnts = this.getBisectorNormals(0, pnt1, pnt2, pnt3) + const normalRight = pnts[0] + const normal = this.getNormal(pnt1, pnt2, pnt3) + const dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1]) + let controlX, controlY + if (dist > ZERO_TOLERANCE) { + const mid = this.mid(pnt1, pnt2) + const pX = pnt1[0] - mid[0] + const pY = pnt1[1] - mid[1] + const d1 = this.distance(pnt1, pnt2) + // normal at midpoint + const n = 2.0 / d1 + const nX = -n * pY + const nY = n * pX + // upper triangle of symmetric transform matrix + const a11 = nX * nX - nY * nY + const a12 = 2 * nX * nY + const a22 = nY * nY - nX * nX + const dX = normalRight[0] - mid[0] + const dY = normalRight[1] - mid[1] + // coordinates of reflected vector + controlX = mid[0] + a11 * dX + a12 * dY + controlY = mid[1] + a12 * dX + a22 * dY + } else { + controlX = pnt1[0] + t * (pnt2[0] - pnt1[0]) + controlY = pnt1[1] + t * (pnt2[1] - pnt1[1]) + } + return [controlX, controlY] + } + + /** + * @param t + * @param controlPoints + * @returns {number[]} + */ + static getRightMostControlPoint(t, controlPoints) { + const count = controlPoints.length + const pnt1 = controlPoints[count - 3] + const pnt2 = controlPoints[count - 2] + const pnt3 = controlPoints[count - 1] + const pnts = this.getBisectorNormals(0, pnt1, pnt2, pnt3) + const normalLeft = pnts[1] + const normal = this.getNormal(pnt1, pnt2, pnt3) + const dist = Math.sqrt(normal[0] * normal[0] + normal[1] * normal[1]) + let controlX, controlY + if (dist > ZERO_TOLERANCE) { + const mid = this.mid(pnt2, pnt3) + const pX = pnt3[0] - mid[0] + const pY = pnt3[1] - mid[1] + + const d1 = this.distance(pnt2, pnt3) + // normal at midpoint + const n = 2.0 / d1 + const nX = -n * pY + const nY = n * pX + + // upper triangle of symmetric transform matrix + const a11 = nX * nX - nY * nY + const a12 = 2 * nX * nY + const a22 = nY * nY - nX * nX + + const dX = normalLeft[0] - mid[0] + const dY = normalLeft[1] - mid[1] + + // coordinates of reflected vector + controlX = mid[0] + a11 * dX + a12 * dY + controlY = mid[1] + a12 * dX + a22 * dY + } else { + controlX = pnt3[0] + t * (pnt2[0] - pnt3[0]) + controlY = pnt3[1] + t * (pnt2[1] - pnt3[1]) + } + return [controlX, controlY] + } + + /** + * @param points + * @returns {[]|*} + */ + static getBezierPoints(points) { + if (points.length <= 2) return points + const bezierPoints = [] + const n = points.length - 1 + for (let t = 0; t <= 1; t += 0.01) { + let x = 0 + let y = 0 + for (let index = 0; index <= n; index++) { + const factor = this.getBinomialFactor(n, index) + const a = Math.pow(t, index) + const b = Math.pow(1 - t, n - index) + x += factor * a * b * points[index][0] + y += factor * a * b * points[index][1] + } + bezierPoints.push([x, y]) + } + bezierPoints.push(points[n]) + return bezierPoints + } + + /** + * + * @param n + * @param index + * @returns {number} + */ + static getBinomialFactor(n, index) { + return this.getFactorial(n) / (this.getFactorial(index) * this.getFactorial(n - index)) + } + + /** + * @param n + * @returns {number} + */ + static getFactorial(n) { + if (n <= 1) return 1 + if (n === 2) return 2 + if (n === 3) return 6 + if (n === 4) return 24 + if (n === 5) return 120 + let result = 1 + for (let i = 1; i <= n; i++) result *= i + return result + } + + /** + * @param points + * @returns {[]|*} + */ + static getQBSplinePoints(points) { + if (points.length <= 2) return points + const n = 2 + const bSplinePoints = [] + const m = points.length - n - 1 + bSplinePoints.push(points[0]) + for (let i = 0; i <= m; i++) { + for (let t = 0; t <= 1; t += 0.05) { + let x = 0 + let y = 0 + for (let k = 0; k <= n; k++) { + const factor = this.getQuadricBSplineFactor(k, t) + x += factor * points[i + k][0] + y += factor * points[i + k][1] + } + bSplinePoints.push([x, y]) + } + } + bSplinePoints.push(points[points.length - 1]) + return bSplinePoints + } + + /** + * @param k + * @param t + * @returns {number} + */ + static getQuadricBSplineFactor(k, t) { + if (k === 0) return Math.pow(t - 1, 2) / 2 + if (k === 1) return (-2 * Math.pow(t, 2) + 2 * t + 1) / 2 + if (k === 2) return Math.pow(t, 2) / 2 + return 0 + } + + /** + * 获取箭头上单边的曲线的控制点,要求最少三个点 + * @param points + * @param distance + * @param isLeft + * @returns {[]} + */ + static getArrowAsideControlPoints(points, distance, isLeft, minHalfWidth = 0) { + const pnts = [] + const len = this.getBaseLength(points) + let subLen = 0 + const isReverseAngle = false + for (let i = 1; i < points.length - 1; i++) { + const pnt0 = points[i - 1] + const pnt1 = points[i] + const pnt2 = points[i + 1] + + const line1 = new Cesium.Cartesian2(pnt1[0] - pnt0[0], pnt1[1] - pnt0[1]) + const line2 = new Cesium.Cartesian2(pnt2[0] - pnt1[0], pnt2[1] - pnt1[1]) + const isReverseAngle = Cesium.Cartesian2.cross(line1, line2) >= 0 + const halfAngle = + (Cesium.Cartesian2.angleBetween(line1, line2) * (isReverseAngle ? -1 : 1)) / 2 + // const plusAngle = Cesium.Math.PI_OVER_TWO * ((isLeft ^ isReverseAngle) ? -1 : 1) + const plusAngle = Cesium.Math.PI_OVER_TWO * (isLeft ? -1 : 1) + // 计算距离 + subLen += this.distance(pnt0, pnt1) + const asideLen = (distance - minHalfWidth) * (1 - subLen / len) + minHalfWidth + pnts.push(this.getThirdPoint(pnt0, pnt1, halfAngle + plusAngle, asideLen, true)) + } + if (!isLeft) { + pnts.reverse() + } + return pnts + } + + /** + * 获取条带上单边的控制点,要求最少两个点 + * @param {*} points 主干点列表 + * @param {*} isLeft 是否生成左侧控制点 + * @param {*} width 单边宽度(米) + * @returns 单边控制点列表 + */ + static getStripAsideControlPoints(points, isLeft, width) { + const pnts = [] + if (points.length === 2) { + const azimuth = Cesium.Math.toDegrees(this.getAzimuth(points[0], points[1])) + const angle = this.getTurfAngle(azimuth + (isLeft ? -90 : 90)) + pnts.push(this.getTurfDestination(points[0], width, angle)) + pnts.push(this.getTurfDestination(points[1], width, angle)) + } else if (points.length > 2) { + // 添加第一个点 + const azimuthFirst = Cesium.Math.toDegrees(this.getAzimuth(points[0], points[1])) + const angleFirst = this.getTurfAngle(azimuthFirst + (isLeft ? -90 : 90)) + pnts.push(this.getTurfDestination(points[0], width, angleFirst)) + // 添加中间控制点 + for (let i = 1; i < points.length - 1; i++) { + const pnt0 = points[i - 1] + const pnt1 = points[i] + const pnt2 = points[i + 1] + + // 重写计算二分距离的算法 + const azimuth0 = Cesium.Math.toDegrees(this.getAzimuth(pnt0, pnt1)) + const azimuth1 = Cesium.Math.toDegrees(this.getAzimuth(pnt1, pnt2)) + let angle = this.getTurfAngle((azimuth0 + azimuth1) / 2) + const angleDiff = Math.abs(azimuth1 - azimuth0) + const isReverseAngle = angleDiff > 180 + // console.log('azimuth0', azimuth0, 'azimuth1', azimuth1, 'angle', angle, 'azimuth1-azimuth0', azimuth1-azimuth0) + angle = this.getTurfAngle(angle + 90 * (isLeft ^ isReverseAngle ? -1 : 1)) + // console.log('计算后angle', angle) + + // 计算等宽的条带控制点 + // const azimuth1 = Cesium.Math.toDegrees(this.getAzimuth(pnt0, pnt1)) + // const angle1 = this.getTurfAngle(azimuth1 + (isLeft ? -90 : 90)) + // const p11 = this.getTurfDestination(pnt0, width, angle1) + // const p12 = this.getTurfDestination(pnt1, width, angle1) + // const azimuth2 = Cesium.Math.toDegrees(this.getAzimuth(pnt1, pnt2)) + // const angle2 = this.getTurfAngle(azimuth2 + (isLeft ? -90 : 90)) + // const p21 = this.getTurfDestination(pnt1, width, angle2) + // const p22 = this.getTurfDestination(pnt2, width, angle2) + // if(turf.booleanIntersects(turf.lineString([p11, p12]), turf.lineString([p21, p22]))) { + // const interPnt = this.getIntersectPoint(p11, p12, p21, p22) + // console.log('interPnt', interPnt) + // pnts.push(interPnt) + // } else { + // pnts.push(p12) + // pnts.push(p21) + // } + + // 计算距离 + pnts.push(this.getTurfDestination(pnt1, width, angle)) + } + // 添加最后一个点 + const azimuthLast = Cesium.Math.toDegrees( + this.getAzimuth(points[points.length - 2], points[points.length - 1]), + ) + const angleLast = this.getTurfAngle(azimuthLast + (isLeft ? -90 : 90)) + pnts.push(this.getTurfDestination(points[points.length - 1], width, angleLast)) + } + // 右侧反序 + if (!isLeft) { + pnts.reverse() + } + return pnts + } + + /** + * 坐标点去重 + * @param {*} worldPositions 坐标点列表 + */ + static positionDistinct(worldPositions) { + // 坐标点列表浅拷贝,因为后续的splice作用于数组本身,需要避免影响到传入的参数 + const positions = [...worldPositions] + const pnts = positions.map((car3) => { + const carto = Cesium.Cartographic.fromCartesian(car3) + return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)] + }) + for (let index = pnts.length - 1; index > 0; index--) { + const p1 = pnts[index] + const p0 = pnts[index - 1] + // 两点之间的距离小于1e-9,认为是同一个点 + // 删除后一个点 + if ( + (p1[0] === p0[0] && p1[1] === p0[1]) || + Math.sqrt(Math.pow(p1[0] - p0[0], 2) + Math.pow(p1[1] - p0[1], 2)) < 1e-9 + ) { + positions.splice(index, 1) + } + } + return positions + } + + /** + * 获取turf中的角度(-180, 180) + * @param angle 角度值 + * @returns + */ + static getTurfAngle(angle) { + let res = angle + while (res > 180) { + res -= 360 + } + while (res < -180) { + res += 360 + } + return res + } + + /** + * 获取turf中的点 + * @param pnt 点 + * @param distance 距离(米) + * @param angle 角度 + * @returns [经度, 纬度] + */ + static getTurfDestination(pnt, distance, angle) { + const json = turf.destination(pnt, distance / 1000, angle) + return json.geometry.coordinates + } +} + +export default DrawUtil diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/straightArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/straightArrowGraphic.js new file mode 100644 index 0000000..af2ee5e --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/straightArrowGraphic.js @@ -0,0 +1,41 @@ +import * as Cesium from 'cesium' +import DrawUtil from './drawUtil' + +const tailWidthFactor = 0.07 +const neckWidthFactor = 0.07 +const headWidthFactor = 0.1 +const headAngle = Math.PI - Math.PI / 8.5 +const neckAngle = Math.PI - Math.PI / 13 + +// 创建箭头点数组 +export function createStraightArrowPositions(worldPositions) { + // let pnts = Parse.parsePolygonCoordToArray( + // Transform.transformCartesianArrayToWGS84Array(this._positions) + // )[0] + let pnts = worldPositions.map((car3) => { + const carto = Cesium.Cartographic.fromCartesian(car3) + return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)] + }) + // 箭头尾部中间 + let pnt1 = pnts[0] + // 箭头顶点 + let pnt2 = pnts[1] + let len = DrawUtil.getBaseLength(pnts) + let tailWidth = len * tailWidthFactor + let neckWidth = len * neckWidthFactor + let headWidth = len * headWidthFactor + // 箭头左尾点 + let tailLeft = DrawUtil.getThirdPoint(pnt2, pnt1, Cesium.Math.PI_OVER_TWO, tailWidth, true) + // 箭头右尾点 + let tailRight = DrawUtil.getThirdPoint(pnt2, pnt1, Cesium.Math.PI_OVER_TWO, tailWidth, false) + // 箭头左端点 + let headLeft = DrawUtil.getThirdPoint(pnt1, pnt2, headAngle, headWidth, false) + // 箭头右端点 + let headRight = DrawUtil.getThirdPoint(pnt1, pnt2, headAngle, headWidth, true) + // 箭头左颈点 + let neckLeft = DrawUtil.getThirdPoint(pnt1, pnt2, neckAngle, neckWidth, false) + // 箭头右颈点 + let neckRight = DrawUtil.getThirdPoint(pnt1, pnt2, neckAngle, neckWidth, true) + const pts = [tailLeft, neckLeft, headLeft, pnt2, headRight, neckRight, tailRight] + return DrawUtil.positionDistinct(pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/draw/wideArrowGraphic.js b/src/components/CesiumMap/mixins/useDrawTool/draw/wideArrowGraphic.js new file mode 100644 index 0000000..adaaf40 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/draw/wideArrowGraphic.js @@ -0,0 +1,93 @@ +import * as Cesium from 'cesium' +import DrawUtil from './drawUtil' +import { createStraightArrowPositions } from './straightArrowGraphic' + +const tailWidthFactor = 0.07 +const neckWidthFactor = 0.07 +const headWidthFactor = 0.1 +const headAngle = Math.PI - Math.PI / 8.5 +const neckAngle = Math.PI - Math.PI / 13 + +// 创建箭头点数组 +export function createWideArrowPositions(worldPositions) { + if (!worldPositions || worldPositions.length < 2) { + return [] + } else if (worldPositions.length === 2) { + // 2点时,直接创建一个直箭头 + return createStraightArrowPositions(worldPositions) + } + + let pnts = worldPositions.map((car3) => { + const carto = Cesium.Cartographic.fromCartesian(car3) + return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)] + }) + // 箭头尾部中间 + let pntTail = pnts[0] + // 箭头顶点 + let pntHead = pnts[pnts.length - 1] + // 靠近尾点的点 + let pntTailNear = pnts[1] + // 靠近顶点的点 + let pntNeck = pnts[pnts.length - 2] + + let len = DrawUtil.getBaseLength(pnts) + let tailWidth = len * tailWidthFactor + let neckWidth = len * neckWidthFactor + let headWidth = len * headWidthFactor + // 箭头左尾点 + let tailLeft = DrawUtil.getThirdPoint( + pntTailNear, + pntTail, + Cesium.Math.PI_OVER_TWO, + tailWidth, + true, + ) + // 箭头右尾点 + let tailRight = DrawUtil.getThirdPoint( + pntTailNear, + pntTail, + Cesium.Math.PI_OVER_TWO, + tailWidth, + false, + ) + // 箭头左端点 + let headLeft = DrawUtil.getThirdPoint(pntNeck, pntHead, headAngle, headWidth, false) + // 箭头右端点 + let headRight = DrawUtil.getThirdPoint(pntNeck, pntHead, headAngle, headWidth, true) + // 箭头左颈点 + let neckLeft = DrawUtil.getThirdPoint(pntNeck, pntHead, neckAngle, neckWidth, false) + // 箭头右颈点 + let neckRight = DrawUtil.getThirdPoint(pntNeck, pntHead, neckAngle, neckWidth, true) + + // 生成样条曲线 + // const innerPoints = pnts.slice(1, pnts.length - 1) + const minHalfWidth = neckWidth * Math.sin(Math.PI / 13) + const leftControls = [ + tailLeft, + ...DrawUtil.getArrowAsideControlPoints(pnts, tailWidth, true, minHalfWidth), + neckLeft, + ] + // const leftPoints = DrawUtil.getBezierPoints(leftControls) + const leftPoints = DrawUtil.getQBSplinePoints(leftControls) + // innerPoints.reverse() + const rightControls = [ + neckRight, + ...DrawUtil.getArrowAsideControlPoints(pnts, tailWidth, false, minHalfWidth), + tailRight, + ] + // const rightPoints = DrawUtil.getBezierPoints(rightControls) + const rightPoints = DrawUtil.getQBSplinePoints(rightControls) + const pts = [ + tailLeft, + ...leftPoints, + neckLeft, + headLeft, + pntHead, + headRight, + neckRight, + ...rightPoints, + tailRight, + ] + + return DrawUtil.positionDistinct(pts.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1]))) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawAttackArrow.js b/src/components/CesiumMap/mixins/useDrawTool/drawAttackArrow.js new file mode 100644 index 0000000..1a8cc8a --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawAttackArrow.js @@ -0,0 +1,137 @@ +import { + Cartesian3, + Entity, + CallbackProperty, + Color as CesiumColor, + ScreenSpaceEventType, + PolygonHierarchy, + HeightReference, +} from 'cesium' +import { createAttackArrowPositions } from './draw/attackArrowGraphic' + +/** + * 绘制攻击箭头的具体逻辑 + * @param deps 绘制所需的依赖 + * @returns Promise 绘制完成的所有点坐标数组 + */ +export const drawAttackArrow = (deps) => { + const { viewer, bus, position, status, registerAbort, globalAbort } = deps + + // 如果在执行其它操作,则取消 + globalAbort() + + status.value = 'Drawing' + const featurePositions = [] + + // 临时多边形实体 (包含填充和边界线) + const tempPolygon = new Entity({ + name: '__draw_temp_attack_arrow', + polygon: { + hierarchy: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点,形成动态效果 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点 + ) { + positions.push(position.value) + } + // 攻击箭头需要至少3个点形成 + if (positions.length < 3) { + return null + } + return new PolygonHierarchy(createAttackArrowPositions(positions)) + }, false), + heightReference: HeightReference.CLAMP_TO_GROUND, + material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3), + }, + // 绘制边界线,连接所有点和第一个点,形成闭合效果 + polyline: { + positions: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) + ) { + positions.push(position.value) + } + // 攻击箭头需要至少2个点才能显示线 + if (positions.length < 2) { + return null + } + // 只有2个点时,显示箭头尾部的线 + if (positions.length === 2) { + return positions + } + const pts = createAttackArrowPositions(positions) + return [...pts, pts[0]] + }, false), + // clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度 + width: 2, + material: CesiumColor.fromCssColorString('#ff0000'), + }, + }) + + viewer.entities.add(tempPolygon) + + return new Promise((resolve, reject) => { + // 清理资源 + const dispose = () => { + registerAbort(null) // 取消注册当前的 abort 回调 + status.value = 'Default' + viewer.entities.remove(tempPolygon) + bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + } + + // 取消绘制 + const abort = (info) => { + dispose() + reject(info || '取消攻击箭头绘制!') + } + + // 左键加点 + const handleAddPoint = () => { + if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) { + featurePositions.push(position.value) + } + } + + // 左键双击完成绘制 + const handleEndDraw = () => { + // 双击时,如果鼠标位置与最后一个点不同,先添加当前点 + handleAddPoint() + if (featurePositions.length < 3) { + // TODO: 替换为实际的错误提示方式 + console.error('请保证攻击箭头有至少3个特征点!') + return + } + dispose() + // 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题 + setTimeout(() => { + resolve(featurePositions) + }, 0) + } + + // 右键取消绘制或删除最后一个点 + const handleCancelDraw = () => { + if (featurePositions.length > 0) { + // 取消最后绘制的点 + featurePositions.pop() + } else { + // 如果没有点,则取消整个绘制 + abort() + } + } + + // 注册当前的取消回调 + registerAbort(abort) + + // 绑定绘制相关事件 + bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawCurvePolygon.js b/src/components/CesiumMap/mixins/useDrawTool/drawCurvePolygon.js new file mode 100644 index 0000000..4304024 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawCurvePolygon.js @@ -0,0 +1,132 @@ +import { + Cartesian3, + Entity, + CallbackProperty, + Color as CesiumColor, + ScreenSpaceEventType, + PolygonHierarchy, + HeightReference, +} from 'cesium' +import { createCurvePolygonPositions } from './draw/curvePolygonGraphic' + +/** + * 绘制多边形的具体逻辑 + * @param deps 绘制所需的依赖 + * @returns Promise 绘制完成的所有点坐标数组 + */ +export const drawCurvePolygon = (deps) => { + const { viewer, bus, position, status, registerAbort, globalAbort } = deps + + // 如果在执行其它操作,则取消 + globalAbort() + + status.value = 'Drawing' + const featurePositions = [] + + // 临时多边形实体 (包含填充和边界线) + const tempPolygon = new Entity({ + name: '__draw_temp_curve_polygon', + polygon: { + hierarchy: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点,形成动态效果 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点 + ) { + positions.push(position.value) + } + // 曲面需要至少2个点才能形成 + if (positions.length < 2) { + return null + } + return new PolygonHierarchy(createCurvePolygonPositions(positions)) + }, false), + heightReference: HeightReference.CLAMP_TO_GROUND, + material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3), + }, + // 绘制边界线,连接所有点和第一个点,形成闭合效果 + polyline: { + positions: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) + ) { + positions.push(position.value) + } + // 曲面需要至少2个点才能形成 + if (positions.length < 2) { + return null + } + return createCurvePolygonPositions(positions) + }, false), + // clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度 + width: 2, + material: CesiumColor.fromCssColorString('#ff0000'), + }, + }) + + viewer.entities.add(tempPolygon) + + return new Promise((resolve, reject) => { + // 清理资源 + const dispose = () => { + registerAbort(null) // 取消注册当前的 abort 回调 + status.value = 'Default' + viewer.entities.remove(tempPolygon) + bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + } + + // 取消绘制 + const abort = (info) => { + dispose() + reject(info || '取消曲面绘制!') + } + + // 左键加点 + const handleAddPoint = () => { + if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) { + featurePositions.push(position.value) + } + } + + // 左键双击完成绘制 + const handleEndDraw = () => { + // 双击时,如果鼠标位置与最后一个点不同,先添加当前点 + handleAddPoint() + if (featurePositions.length < 2) { + // TODO: 替换为实际的错误提示方式 + console.error('请保曲面至少有2个特征点!') + return + } + dispose() + // 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题 + setTimeout(() => { + resolve(featurePositions) + }, 0) + } + + // 右键取消绘制或删除最后一个点 + const handleCancelDraw = () => { + if (featurePositions.length > 0) { + // 取消最后绘制的点 + featurePositions.pop() + } else { + // 如果没有点,则取消整个绘制 + abort() + } + } + + // 注册当前的取消回调 + registerAbort(abort) + + // 绑定绘制相关事件 + bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawDoubleArrow.js b/src/components/CesiumMap/mixins/useDrawTool/drawDoubleArrow.js new file mode 100644 index 0000000..8a5a607 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawDoubleArrow.js @@ -0,0 +1,137 @@ +import { + Cartesian3, + Entity, + CallbackProperty, + Color as CesiumColor, + ScreenSpaceEventType, + PolygonHierarchy, + HeightReference, +} from 'cesium' +import { createDoubleArrowPositions } from './draw/doubleArrowGraphic' + +/** + * 绘制双箭头的具体逻辑 + * @param deps 绘制所需的依赖 + * @returns Promise 绘制完成的所有点坐标数组 + */ +export const drawDoubleArrow = (deps) => { + const { viewer, bus, position, status, registerAbort, globalAbort } = deps + + // 如果在执行其它操作,则取消 + globalAbort() + + status.value = 'Drawing' + const featurePositions = [] + + // 临时多边形实体 (包含填充和边界线) + const tempPolygon = new Entity({ + name: '__draw_temp_double_arrow', + polygon: { + hierarchy: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点,形成动态效果 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点 + ) { + positions.push(position.value) + } + // 双箭头需要至少3个点形成 + if (positions.length < 3) { + return null + } + return new PolygonHierarchy(createDoubleArrowPositions(positions)) + }, false), + heightReference: HeightReference.CLAMP_TO_GROUND, + material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3), + }, + // 绘制边界线,连接所有点和第一个点,形成闭合效果 + polyline: { + positions: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) + ) { + positions.push(position.value) + } + // 双箭头需要至少2个点才能显示线 + if (positions.length < 2) { + return null + } + // 只有2个点时,显示箭头尾部的线 + if (positions.length === 2) { + return positions + } + const pts = createDoubleArrowPositions(positions) + return [...pts, pts[0]] + }, false), + // clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度 + width: 2, + material: CesiumColor.fromCssColorString('#ff0000'), + }, + }) + + viewer.entities.add(tempPolygon) + + return new Promise((resolve, reject) => { + // 清理资源 + const dispose = () => { + registerAbort(null) // 取消注册当前的 abort 回调 + status.value = 'Default' + viewer.entities.remove(tempPolygon) + bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + } + + // 取消绘制 + const abort = (info) => { + dispose() + reject(info || '取消双箭头绘制!') + } + + // 左键加点 + const handleAddPoint = () => { + if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) { + featurePositions.push(position.value) + } + } + + // 左键双击完成绘制 + const handleEndDraw = () => { + // 双击时,如果鼠标位置与最后一个点不同,先添加当前点 + handleAddPoint() + if (featurePositions.length < 3) { + // TODO: 替换为实际的错误提示方式 + console.error('请保证双箭头有至少3个特征点!') + return + } + dispose() + // 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题 + setTimeout(() => { + resolve(featurePositions) + }, 0) + } + + // 右键取消绘制或删除最后一个点 + const handleCancelDraw = () => { + if (featurePositions.length > 0) { + // 取消最后绘制的点 + featurePositions.pop() + } else { + // 如果没有点,则取消整个绘制 + abort() + } + } + + // 注册当前的取消回调 + registerAbort(abort) + + // 绑定绘制相关事件 + bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawPoint.js b/src/components/CesiumMap/mixins/useDrawTool/drawPoint.js new file mode 100644 index 0000000..2ce7848 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawPoint.js @@ -0,0 +1,56 @@ +import { Cartesian3, ScreenSpaceEventType } from 'cesium' + +/** + * 绘制点的具体逻辑 + * @param deps 绘制所需的依赖 + * @returns Promise 绘制完成的点坐标 + */ +export const drawPoint = (deps) => { + const { bus, position, status, registerAbort, globalAbort } = deps + + // 如果在执行其它操作,则取消 + globalAbort() + + status.value = 'Drawing' + + return new Promise((resolve, reject) => { + // 清理资源 + const dispose = () => { + registerAbort(null) // 取消注册当前的 abort 回调 + status.value = 'Default' + bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleDraw) + bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancel) + } + + // 取消绘制 + const abort = (info) => { + dispose() + reject(info || '取消点绘制!') + } + + // 左键绘制 + const handleDraw = () => { + if (!position.value) { + console.error('坐标信息转换失败,获取的世界坐标异常!') + return + } + const p = position.value + dispose() + + // 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题 + setTimeout(() => { + resolve(p) + }, 0) + } + + // 右键取消 + const handleCancel = () => abort() + + // 注册当前的取消回调 + registerAbort(abort) + + // 绑定绘制相关事件 + bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleDraw) + bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancel) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawPolygon.js b/src/components/CesiumMap/mixins/useDrawTool/drawPolygon.js new file mode 100644 index 0000000..25eebe9 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawPolygon.js @@ -0,0 +1,131 @@ +import { + Cartesian3, + Entity, + CallbackProperty, + Color as CesiumColor, + ScreenSpaceEventType, + PolygonHierarchy, + HeightReference, +} from 'cesium' + +/** + * 绘制多边形的具体逻辑 + * @param deps 绘制所需的依赖 + * @returns Promise 绘制完成的所有点坐标数组 + */ +export const drawPolygon = (deps) => { + const { viewer, bus, position, status, registerAbort, globalAbort } = deps + + // 如果在执行其它操作,则取消 + globalAbort() + + status.value = 'Drawing' + const featurePositions = [] + + // 临时多边形实体 (包含填充和边界线) + const tempPolygon = new Entity({ + name: '__draw_temp_polygon', + polygon: { + hierarchy: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点,形成动态效果 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点 + ) { + positions.push(position.value) + } + // 多边形需要至少3个点才能形成 + if (positions.length < 3) { + return null + } + return new PolygonHierarchy(positions) + }, false), + heightReference: HeightReference.CLAMP_TO_GROUND, + material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3), + }, + // 绘制边界线,连接所有点和第一个点,形成闭合效果 + polyline: { + positions: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) + ) { + positions.push(position.value) + } + // 如果有至少一个点,则连接到第一个点形成闭合线 + if (featurePositions.length > 0 && featurePositions[0]) { + positions.push(featurePositions[0]) + } + return positions + }, false), + // clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度 + width: 2, + material: CesiumColor.fromCssColorString('#ff0000'), + }, + }) + + viewer.entities.add(tempPolygon) + + return new Promise((resolve, reject) => { + // 清理资源 + const dispose = () => { + registerAbort(null) // 取消注册当前的 abort 回调 + status.value = 'Default' + viewer.entities.remove(tempPolygon) + bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + } + + // 取消绘制 + const abort = (info) => { + dispose() + reject(info || '取消多边形绘制!') + } + + // 左键加点 + const handleAddPoint = () => { + if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) { + featurePositions.push(position.value) + } + } + + // 左键双击完成绘制 + const handleEndDraw = () => { + // 双击时,如果鼠标位置与最后一个点不同,先添加当前点 + handleAddPoint() + if (featurePositions.length < 3) { + // TODO: 替换为实际的错误提示方式 + console.error('请保多边形至少有三个点!') + return + } + dispose() + // 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题 + setTimeout(() => { + resolve(featurePositions) + }, 0) + } + + // 右键取消绘制或删除最后一个点 + const handleCancelDraw = () => { + if (featurePositions.length > 0) { + // 取消最后绘制的点 + featurePositions.pop() + } else { + // 如果没有点,则取消整个绘制 + abort() + } + } + + // 注册当前的取消回调 + registerAbort(abort) + + // 绑定绘制相关事件 + bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawPolyline.js b/src/components/CesiumMap/mixins/useDrawTool/drawPolyline.js new file mode 100644 index 0000000..936beb4 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawPolyline.js @@ -0,0 +1,105 @@ +import { + Cartesian3, + Entity, + CallbackProperty, + Color as CesiumColor, + ScreenSpaceEventType, +} from 'cesium' + +/** + * 绘制折线的具体逻辑 + * @param deps 绘制所需的依赖 + * @returns Promise 绘制完成的所有点坐标数组 + */ +export const drawPolyline = (deps) => { + const { viewer, bus, position, status, registerAbort, globalAbort } = deps + + // 如果在执行其它操作,则取消 + globalAbort() + + status.value = 'Drawing' + const featurePositions = [] + + // 临时折线实体 + const tempPolyline = new Entity({ + name: '__draw_temp_polyline', + polyline: { + positions: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点,形成动态效果 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点 + ) { + positions.push(position.value) + } + return positions + }, false), + width: 2, + material: CesiumColor.fromCssColorString('#FF0000'), + clampToGround: true, + }, + }) + + viewer.entities.add(tempPolyline) + + return new Promise((resolve, reject) => { + // 清理资源 + const dispose = () => { + registerAbort(null) // 取消注册当前的 abort 回调 + status.value = 'Default' + viewer.entities.remove(tempPolyline) + bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + } + + // 取消绘制 + const abort = (info) => { + dispose() + reject(info || '取消折线绘制!') + } + + // 左键加点 + const handleAddPoint = () => { + if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) { + featurePositions.push(position.value) + } + } + + // 左键双击完成绘制 + const handleEndDraw = () => { + // 双击时,如果鼠标位置与最后一个点不同,先添加当前点 + handleAddPoint() + if (featurePositions.length < 2) { + // TODO: 替换为实际的错误提示方式 + console.error('请保证折线至少有两个点!') + return + } + dispose() + // 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题 + setTimeout(() => { + resolve(featurePositions) + }, 0) + } + + // 右键取消绘制或删除最后一个点 + const handleCancelDraw = () => { + if (featurePositions.length > 0) { + // 取消最后绘制的点 + featurePositions.pop() + } else { + // 如果没有点,则取消整个绘制 + abort() + } + } + + // 注册当前的取消回调 + registerAbort(abort) + + // 绑定绘制相关事件 + bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawStraightArrow.js b/src/components/CesiumMap/mixins/useDrawTool/drawStraightArrow.js new file mode 100644 index 0000000..cbfcc78 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawStraightArrow.js @@ -0,0 +1,148 @@ +import { + Cartesian3, + Entity, + CallbackProperty, + Color as CesiumColor, + ScreenSpaceEventType, + PolygonHierarchy, + HeightReference, +} from 'cesium' +import { createStraightArrowPositions } from './draw/straightArrowGraphic' + +/** + * 绘制直箭头的具体逻辑 + * @param deps 绘制所需的依赖 + * @returns Promise 绘制完成的所有点坐标数组 + */ +export const drawStraightArrow = (deps) => { + const { viewer, bus, position, status, registerAbort, globalAbort } = deps + + // 如果在执行其它操作,则取消 + globalAbort() + + status.value = 'Drawing' + const featurePositions = [] + + // 临时多边形实体 (包含填充和边界线) + const tempPolygon = new Entity({ + name: '__draw_temp_straight_arrow', + polygon: { + hierarchy: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点,形成动态效果 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点 + ) { + positions.push(position.value) + } + // 直箭头需要2个点形成 + if (positions.length < 2) { + return null + } + return new PolygonHierarchy(createStraightArrowPositions(positions)) + }, false), + heightReference: HeightReference.CLAMP_TO_GROUND, + material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3), + }, + // 绘制边界线,连接所有点和第一个点,形成闭合效果 + polyline: { + positions: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) + ) { + positions.push(position.value) + } + // 直箭头需要2个点形成 + if (positions.length < 2) { + return null + } + const pts = createStraightArrowPositions(positions) + return [...pts, pts[0]] + }, false), + // clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度 + width: 2, + material: CesiumColor.fromCssColorString('#ff0000'), + }, + }) + + viewer.entities.add(tempPolygon) + + return new Promise((resolve, reject) => { + // 清理资源 + const dispose = () => { + registerAbort(null) // 取消注册当前的 abort 回调 + status.value = 'Default' + viewer.entities.remove(tempPolygon) + bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + } + + // 取消绘制 + const abort = (info) => { + dispose() + reject(info || '取消曲面绘制!') + } + + // 左键加点 + const handleAddPoint = () => { + // 直箭头最多绘制2个点 + if (featurePositions.length >= 2) return + + if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) { + featurePositions.push(position.value) + + // 如果达到2个点,自动完成绘制 + if (featurePositions.length === 2) { + dispose() + setTimeout(() => { + featurePositions[0] && + featurePositions[1] && + resolve([featurePositions[0], featurePositions[1]]) + }, 0) + } + } + } + + // 左键双击完成绘制 + const handleEndDraw = () => { + // 双击时,如果鼠标位置与最后一个点不同,先添加当前点 + handleAddPoint() + if (featurePositions.length < 2) { + // TODO: 替换为实际的错误提示方式 + console.error('请保证直箭头有2个特征点!') + return + } + dispose() + // 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题 + setTimeout(() => { + featurePositions[0] && + featurePositions[1] && + resolve([featurePositions[0], featurePositions[1]]) + }, 0) + } + + // 右键取消绘制或删除最后一个点 + const handleCancelDraw = () => { + if (featurePositions.length > 0) { + // 取消最后绘制的点 + featurePositions.pop() + } else { + // 如果没有点,则取消整个绘制 + abort() + } + } + + // 注册当前的取消回调 + registerAbort(abort) + + // 绑定绘制相关事件 + bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/drawWideArrow.js b/src/components/CesiumMap/mixins/useDrawTool/drawWideArrow.js new file mode 100644 index 0000000..8afef2d --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/drawWideArrow.js @@ -0,0 +1,134 @@ +import { + Cartesian3, + Entity, + CallbackProperty, + Color as CesiumColor, + ScreenSpaceEventType, + PolygonHierarchy, + HeightReference, +} from 'cesium' +import { createWideArrowPositions } from './draw/wideArrowGraphic' + +/** + * 绘制宽箭头的具体逻辑 + * @param deps 绘制所需的依赖 + * @returns Promise 绘制完成的所有点坐标数组 + */ +export const drawWideArrow = (deps) => { + const { viewer, bus, position, status, registerAbort, globalAbort } = deps + + // 如果在执行其它操作,则取消 + globalAbort() + + status.value = 'Drawing' + const featurePositions = [] + + // 临时多边形实体 (包含填充和边界线) + const tempPolygon = new Entity({ + name: '__draw_temp_wide_arrow', + polygon: { + hierarchy: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点,形成动态效果 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) // 避免重复添加同一个点 + ) { + positions.push(position.value) + } + // 宽箭头需要至少2个点形成 + if (positions.length < 2) { + return null + } + return new PolygonHierarchy(createWideArrowPositions(positions)) + }, false), + heightReference: HeightReference.CLAMP_TO_GROUND, + material: CesiumColor.fromCssColorString('#ff0000').withAlpha(0.3), + }, + // 绘制边界线,连接所有点和第一个点,形成闭合效果 + polyline: { + positions: new CallbackProperty(() => { + const positions = [...featurePositions] + // 添加当前鼠标位置作为最后一个点 + if ( + position.value && + !position.value.equals(featurePositions[featurePositions.length - 1]) + ) { + positions.push(position.value) + } + // 宽箭头需要至少2个点形成 + if (positions.length < 2) { + return null + } + const pts = createWideArrowPositions(positions) + return [...pts, pts[0]] + // return [...pts] + }, false), + // clampToGround: true, // 多边形边界线通常不需要 clampToGround,因为多边形本身已经处理了高度 + width: 2, + material: CesiumColor.fromCssColorString('#ff0000'), + }, + }) + + viewer.entities.add(tempPolygon) + + return new Promise((resolve, reject) => { + // 清理资源 + const dispose = () => { + registerAbort(null) // 取消注册当前的 abort 回调 + status.value = 'Default' + viewer.entities.remove(tempPolygon) + bus.offScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.offScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.offScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + } + + // 取消绘制 + const abort = (info) => { + dispose() + reject(info || '取消宽箭头绘制!') + } + + // 左键加点 + const handleAddPoint = () => { + if (position.value && !position.value.equals(featurePositions[featurePositions.length - 1])) { + featurePositions.push(position.value) + } + } + + // 左键双击完成绘制 + const handleEndDraw = () => { + // 双击时,如果鼠标位置与最后一个点不同,先添加当前点 + handleAddPoint() + if (featurePositions.length < 2) { + // TODO: 替换为实际的错误提示方式 + console.error('请保证宽箭头有至少2个特征点!') + return + } + dispose() + // 使用 setTimeout 避免立即 resolve 可能导致的事件冲突或其他问题 + setTimeout(() => { + resolve(featurePositions) + }, 0) + } + + // 右键取消绘制或删除最后一个点 + const handleCancelDraw = () => { + if (featurePositions.length > 0) { + // 取消最后绘制的点 + featurePositions.pop() + } else { + // 如果没有点,则取消整个绘制 + abort() + } + } + + // 注册当前的取消回调 + registerAbort(abort) + + // 绑定绘制相关事件 + bus.onScreen(ScreenSpaceEventType.LEFT_CLICK, handleAddPoint) + bus.onScreen(ScreenSpaceEventType.RIGHT_CLICK, handleCancelDraw) + bus.onScreen(ScreenSpaceEventType.LEFT_DOUBLE_CLICK, handleEndDraw) + }) +} diff --git a/src/components/CesiumMap/mixins/useDrawTool/index.js b/src/components/CesiumMap/mixins/useDrawTool/index.js new file mode 100644 index 0000000..c5fc9e7 --- /dev/null +++ b/src/components/CesiumMap/mixins/useDrawTool/index.js @@ -0,0 +1,145 @@ +import { ref, watch } from 'vue' +import { + Viewer, + Cartesian3, + CustomDataSource, + Entity, + CallbackPositionProperty, + Color as CesiumColor, +} from 'cesium' +import { useEventBus } from '../useEventBus' +import { useHoverPosition } from '../useHoverPosition' + +// 导入拆分后的类型和绘制逻辑 +import { drawPoint as _drawPoint } from './drawPoint' +import { drawPolyline as _drawPolyline } from './drawPolyline' +import { drawPolygon as _drawPolygon } from './drawPolygon' +import { drawCurvePolygon as _drawCurvePolygon } from './drawCurvePolygon' +import { drawStraightArrow as _drawStraightArrow } from './drawStraightArrow' +import { drawWideArrow as _drawWideArrow } from './drawWideArrow' +import { drawAttackArrow as _drawAttackArrow } from './drawAttackArrow' +import { drawDoubleArrow as _drawDoubleArrow } from './drawDoubleArrow' + +import { createCurvePolygonPositions } from './draw/curvePolygonGraphic' +import { createStraightArrowPositions } from './draw/straightArrowGraphic' +import { createWideArrowPositions } from './draw/wideArrowGraphic' +import { createAttackArrowPositions } from './draw/attackArrowGraphic' +import { createDoubleArrowPositions } from './draw/doubleArrowGraphic' + +// 所有useDrawTool实例共用一个绘制状态和取消回调 +// 如果需要每个实例独立,则将此变量移入 useDrawTool 函数内部 +let abortCallback = null + +// 存储编辑状态辅助entity的图层 +let assistLayer = null + +export const useDrawTool = (viewer) => { + // 获取实时位置 + const { isHover, position } = useHoverPosition(viewer) + // 获取事件总线 + const bus = useEventBus(viewer) + const status = ref('Default') + + // 判断辅助点是否显示 + watch( + () => [status.value, isHover.value], + ([nowStatus, hover]) => { + if (hover && nowStatus === 'Drawing') { + assistPoint.show = true + } else { + assistPoint.show = false + } + }, + ) + + if (!assistLayer) { + assistLayer = new CustomDataSource('__assist_edit') + viewer.dataSources.add(assistLayer) + } + + // 辅助点,用于显示获取的实际位置 + const assistPoint = new Entity({ + name: '__draw_icon', + show: false, + position: new CallbackPositionProperty(() => { + return position.value || undefined + }, false), + point: { + pixelSize: 7, + color: CesiumColor.BLUE, + outlineColor: CesiumColor.WHITE, + outlineWidth: 1, + }, + // 可以考虑是否需要 HeightReference.CLAMP_TO_GROUND + // heightReference: HeightReference.CLAMP_TO_GROUND, + }) + viewer.entities.add(assistPoint) + + // 全局取消当前绘制操作的函数 + const abort = () => { + if (abortCallback) { + abortCallback() // 调用当前注册的取消回调 + abortCallback = null // 清空回调 + } + } + + // 注册当前绘制操作的取消回调函数 + const registerAbort = (cb) => { + abortCallback = cb + } + + // 准备传递给绘制函数的依赖对象 + const drawDependencies = { + viewer, + bus, + position, + status, + registerAbort, + globalAbort: abort, // 将全局 abort 函数也传递进去,方便内部调用 + } + + // 调用拆分后的绘制函数,并传递依赖 + const drawPoint = () => _drawPoint(drawDependencies) + const drawPolyline = () => _drawPolyline(drawDependencies) + const drawPolygon = () => _drawPolygon(drawDependencies) + const drawCurvePolygon = () => _drawCurvePolygon(drawDependencies) + const drawStraightArrow = () => _drawStraightArrow(drawDependencies) + const drawWideArrow = () => _drawWideArrow(drawDependencies) + const drawAttackArrow = () => _drawAttackArrow(drawDependencies) + const drawDoubleArrow = () => _drawDoubleArrow(drawDependencies) + + // TODO: 添加 startEdit 等其他编辑功能 + // 在 hook 卸载时清理资源 (如果 hook 实例会被销毁) + // Vue 3 Composition API 的 setup 函数返回的对象没有 beforeDestroy/unmounted 生命周期钩子 + // 如果 useDrawTool 是在 setup 中调用,并且 setup 返回了 useDrawTool 的结果, + // 那么当组件卸载时,Cesium 实体和事件监听可能不会自动清理。 + // 需要手动处理清理逻辑,例如在组件的 onUnmounted 钩子中调用一个 cleanup 函数。 + // 或者 useEventBus 内部已经处理了 Viewer 销毁时的清理。 + // 这里暂时不添加清理逻辑,假设 useEventBus 和 useHoverPosition 已经处理或外部会手动清理。 + // 如果需要,可以在这里返回一个 cleanup 函数,并在组件 unmounted 时调用。 + /* + const cleanup = () => { + abort(); // 取消所有正在进行的绘制 + viewer.dataSources.remove(assistLayer, true); // 移除并销毁数据源 + viewer.entities.remove(assistPoint); // 移除辅助点 + // useEventBus 和 useHoverPosition 可能也需要清理 + }; + // return { ..., cleanup }; + */ + + return { + drawPoint, + drawPolyline, + drawPolygon, + drawCurvePolygon, + drawStraightArrow, + drawWideArrow, + drawAttackArrow, + drawDoubleArrow, + abort, + // 如果需要,可以暴露 status + // status + } +} + +export { createCurvePolygonPositions, createStraightArrowPositions, createWideArrowPositions, createAttackArrowPositions, createDoubleArrowPositions } diff --git a/src/components/CesiumMap/mixins/useEventBus.js b/src/components/CesiumMap/mixins/useEventBus.js new file mode 100644 index 0000000..247ab7c --- /dev/null +++ b/src/components/CesiumMap/mixins/useEventBus.js @@ -0,0 +1,127 @@ +import mitt from 'mitt' +import { Viewer, ScreenSpaceEventHandler, ScreenSpaceEventType } from 'cesium' + + +// 使用 Map 来缓存每个 viewer 对应的事件总线组件 +// Viewer 实例作为 Map 的键 +const viewerEventBusCache = new Map() + +/** + * 消息总线,接管全局屏幕空间事件,仅作用于单个 Viewer。 + * 调用该hook后,所有鼠标事件必须改为调用返回对象中的方法。 + * 使用方法参考发布订阅模式,Cesium.ScreenSpaceEventType的订阅/解订用onScreen/offScreen,自定义事件用on/off。 + * 注意,这里不单独划分带键盘按键的鼠标事件,而是到具体事件中划分。 + * @param viewer Cesium Viewer 实例 + * @returns EventBus 对象 + */ +export const useEventBus = (viewer) => { + let cache = viewerEventBusCache.get(viewer) + if (!cache) { + cache = { + // 创建一个 mitt 实例用于处理所有屏幕空间事件 + screenEmitter: mitt(), + // 创建一个 mitt 实例用于处理自定义事件 + customEmitter: mitt(), + // 存储创建的 ScreenSpaceEventHandler 实例,以便销毁 + // 注意:这里没有使用viewer中自带的ScreenSpaceEventHandler实例,故不会接管其绑定函数 + handler: new ScreenSpaceEventHandler(viewer.canvas), + } + viewerEventBusCache.set(viewer, cache) + } + // 如果缓存中不存在 ScreenSpaceEventHandler 实例,则创建一个 + if (!cache.handler) { + cache.handler = new ScreenSpaceEventHandler(viewer.canvas) + } + + // 构建返回的 EventBus 对象 + const bus = { + // 移除 mode 属性的定义 + + // onScreen 直接注册到 screenEmitter + // onScreen(event, handler) { + // screenEmitter.on(event, handler) + // }, + onScreen: cache.screenEmitter.on, + + // offScreen 直接从 screenEmitter 移除 + // offScreen(event, handler) { + // screenEmitter.off(event, handler) + // }, + offScreen: cache.screenEmitter.off, + + // on 保持不变 + // on(event, handler) { + // customEmitter.on(event, handler as (param: any[]) => void) + // }, + on: cache.customEmitter.on, + + // off 保持不变 + // off(event, handler) { + // customEmitter.off(event, handler as ((param?: any[]) => void) | undefined) + // }, + off: cache.customEmitter.off, + + // emit 方法的实现需要处理两种不同的事件类型和参数结构 + emit(event, ...args) { + // 使用 any 在实现层面处理联合类型和重载 + if (typeof event === 'string') { + // 触发自定义事件 + // mitt.emit(type, event), event 是 payload + cache.customEmitter.emit(event, args) // 将 ...args 作为单个数组 payload 传递 + } else if (Object.values(ScreenSpaceEventType).includes(event)) { + // 触发屏幕空间事件 (触发所有监听器,无模式检查) + // 屏幕空间事件通常只有一个参数 + cache.screenEmitter.emit(event, args[0]) + } else { + console.warn(`EventBus: 不符合规范的触发事件: ${event}`) + } + }, + + dispose() { + if (cache.handler && !cache.handler.isDestroyed()) { + cache.handler.destroy() + } + cache.handler = null + // mitt 实例本身不需要显式销毁,它们会被垃圾回收 + }, + } + + // 用于 LEFT_CLICK 节流的状态 + let isLeftClickThrottled = false + + // 遍历所有 Cesium 屏幕空间事件类型 + for (const eventTypeString in ScreenSpaceEventType) { + const eventType = ScreenSpaceEventType[eventTypeString] + const eventTypeCode = Number(eventType) // 获取对应的数字代码 + + // 定义 Cesium handler 实际执行的函数 + + // 特殊处理 LEFT_CLICK 进行节流 + // 观测到cesium双击会触发2次单击,所以在单击里加一个200毫秒的节流 + if (eventTypeCode === ScreenSpaceEventType.LEFT_CLICK) { + const throttledClickAction = (args) => { + if (isLeftClickThrottled) return // 如果正在节流,则忽略本次事件 + isLeftClickThrottled = true // 设置节流状态 + + // 触发 LEFT_CLICK 事件到总线 + bus.emit(eventType, args) + + // 200ms 后解除节流状态 + window.setTimeout(() => { + isLeftClickThrottled = false + }, 200) + } + // 将节流后的函数设置到 Cesium handler + cache.handler.setInputAction(throttledClickAction, eventTypeCode) + } else { + // 将 Cesium 原生事件参数转发到我们的 mitt 总线 + const cesiumHandlerAction = (args) => { + // 直接通过 bus.emit 触发事件,mitt 会负责分发给所有监听器 + bus.emit(eventType, args) + } + cache.handler.setInputAction(cesiumHandlerAction, eventTypeCode) + } + } + + return bus +} diff --git a/src/components/CesiumMap/mixins/useHoverPosition.js b/src/components/CesiumMap/mixins/useHoverPosition.js new file mode 100644 index 0000000..e3d70a7 --- /dev/null +++ b/src/components/CesiumMap/mixins/useHoverPosition.js @@ -0,0 +1,47 @@ +import { + Viewer, + Cartesian3, + Cartographic, + Cartesian2, + ScreenSpaceEventType, + ScreenSpaceEventHandler, +} from 'cesium' +import { computed, ref, shallowRef } from 'vue' +import { useElementHover } from '@vueuse/core' +import { useEventBus } from './useEventBus' + +// 使用 Map 来缓存每个 viewer 对应的位置缓存 +// Viewer 实例作为 Map 的键 +const viewerHoverPositionCache = new Map() +export const useHoverPosition = (viewer) => { + let cache = viewerHoverPositionCache.get(viewer) + if (!cache) { + // cesium所在的canvas元素 + const canvasRef = ref(viewer.canvas) + // 动态监听鼠标是否悬停在地图内 + const isHover = useElementHover(canvasRef) + const screenPosition = shallowRef(null) + const position = shallowRef(null) + // const coordinate = shallowRef(null) + // 添加鼠标移动事件 + const bus = useEventBus(viewer) + bus.onScreen(ScreenSpaceEventType.MOUSE_MOVE, (param) => { + screenPosition.value = param.endPosition.clone() + position.value = viewer.scene.pickPosition(param.endPosition) + bus.emit('hoverTest', { + screenPosition: screenPosition.value, + position: position.value, + }) + // coordinate.value = Cartographic.fromCartesian(position.value) + }) + cache = { + isHover, + screenPosition, + position, + // coordinate, + } + viewerHoverPositionCache.set(viewer, cache) + } + + return cache +} diff --git a/src/components/CesiumMap/mixins/useMeasureTool.js b/src/components/CesiumMap/mixins/useMeasureTool.js new file mode 100644 index 0000000..0ce60d6 --- /dev/null +++ b/src/components/CesiumMap/mixins/useMeasureTool.js @@ -0,0 +1,223 @@ +import * as Cesium from 'cesium' +import * as turf from '@turf/turf' +import { useDrawTool } from './useDrawTool' + +// 存储测量内容的辅助图层 +let measureLayer = null + +/** + * 计算折线长度 + * @param {Cesium.Cartesian3[]} positions 位置列表 + * @returns 折线长度,单位米 + */ +const getDistance = (positions) => { + const line = turf.lineString(positions.map((pos) => { + const cartographic = Cesium.Cartographic.fromCartesian(pos) + return [Cesium.Math.toDegrees(cartographic.longitude), Cesium.Math.toDegrees(cartographic.latitude)] + })) + return turf.length(line, { units: 'kilometers' }) * 1000 // 返回米 +} + +/** + * 计算多边形面积 + * @param {Cesium.Cartesian3[]} positions 位置列表 + * @returns 多边形面积,单位平方米 + */ +const getArea = (positions) => { + const pts = [...positions, positions[0]] // 闭合多边形 + const polygon = turf.polygon([pts.map((pos) => { + const cartographic = Cesium.Cartographic.fromCartesian(pos) + return [Cesium.Math.toDegrees(cartographic.longitude), Cesium.Math.toDegrees(cartographic.latitude)] + })]) + return turf.area(polygon) // 返回平方米 +} + +/** + * 获取多个点的中心位置,根据边界范围确定 + * @param {Cesium.Cartesian3[]} positions 位置列表 + * @param {number} height 返回位置的高度,默认为0 + * @returns 根据边界范围确定的中心点位置 + */ +const getBoundingCenter = (positions, height = 0) => { + const coord = getBoundingCenterCoordinate(positions) + return Cesium.Cartesian3.fromDegrees(coord[0], coord[1], height) +} + +/** + * 获取多个点的中心位置坐标,根据边界范围确定 + * @param {Cesium.Cartesian3[]} positions 位置列表 + * @returns 根据边界范围确定的中心点位置坐标,格式为 [经度, 纬度] + */ +const getBoundingCenterCoordinate = (positions) => { + const polygon = turf.lineString(positions.map((pos) => { + const cartographic = Cesium.Cartographic.fromCartesian(pos) + return [Cesium.Math.toDegrees(cartographic.longitude), Cesium.Math.toDegrees(cartographic.latitude)] + })) + const centerFeature = turf.center(polygon) + return centerFeature.geometry.coordinates +} + +export const useMeasureTool = (viewer) => { + if (!viewer) { + throw new Error('获取 Viewer 实例失败,请确保在 Viewer 初始化后调用 useMeasureTool 方法!') + } + + const editTool = useDrawTool(viewer) + if (!measureLayer) { + measureLayer = new Cesium.CustomDataSource('__assist_measure') + viewer.dataSources.add(measureLayer) + } + + // 距离测量 + const measureDistance = () => { + return editTool.drawPolyline().then((positions) => { + const style = { + width: 5, + material: Cesium.Color.fromCssColorString('#57e0e0'), + } + let arr = [] + for (let i = 0; i < positions.length; i++) { + if (i + 1 < positions.length) { + if (!positions[i] || !positions[i + 1]) continue + arr.push([positions[i], positions[i + 1]]) + } + } + console.log(arr) + let result = 0 + arr.forEach((item) => { + let start = Cesium.Cartographic.fromCartesian(item[0]) + let end = Cesium.Cartographic.fromCartesian(item[1]) + let startObj = { longitude: '', latitude: '' } + let endObj = { longitude: '', latitude: '' } + startObj.longitude = Cesium.Math.toDegrees(start.longitude) + startObj.latitude = Cesium.Math.toDegrees(start.latitude) + endObj.longitude = Cesium.Math.toDegrees(end.longitude) + endObj.latitude = Cesium.Math.toDegrees(end.latitude) + + console.log(startObj) + console.log(endObj) + + let from = turf.point([startObj.longitude, startObj.latitude]) + let to = turf.point([endObj.longitude, endObj.latitude]) + + let distance = turf.distance(from, to, { units: 'kilometers' }) * 1000 + + result += distance + + const tempPolyline = new Cesium.Entity({ + name: '__measure_distance', + // position: [ {x: (item[0].x + item[1].x) / 2, y: (item[0].y + item[1].y) / 2, z: (item[0].z + item[1].z) / 2} ], + position: new Cesium.Cartesian3( + (item[0].x + item[1].x) / 2, + (item[0].y + item[1].y) / 2, + (item[0].z + item[1].z) / 2, + ), + polyline: { + positions: [...item], + width: style.width, + material: style.material, + clampToGround: true, + }, + label: new Cesium.LabelGraphics({ + text: distance.toFixed(2) + '米', + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + pixelOffset: new Cesium.Cartesian2(10, 10), + disableDepthTestDistance: Number.POSITIVE_INFINITY, + }), + }) + + measureLayer.entities.add(tempPolyline) + return positions + }) + + let len = positions.length - 1 + if (len > 1) { + let resultLabel = new Cesium.Entity({ + name: 'measure_distance_length', + position: new Cesium.Cartesian3( + (positions[0].x + positions[len].x) / 2, + (positions[0].y + positions[len].y) / 2, + (positions[0].z + positions[len].z) / 2, + ), + label: new Cesium.LabelGraphics({ + text: '总距离:' + result.toFixed(2) + '米', + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + disableDepthTestDistance: Number.POSITIVE_INFINITY, + fillColor: Cesium.Color.AQUAMARINE, + }), + }) + measureLayer.entities.add(resultLabel) + } + return positions + }) + } + + // 面积测量 + const measureArea = () => { + return editTool.drawPolygon().then((positions) => { + let polygon = [] + positions.forEach((item) => { + let obj = Cesium.Cartographic.fromCartesian(item) + polygon.push([Cesium.Math.toDegrees(obj.longitude), Cesium.Math.toDegrees(obj.latitude)]) + }) + + let features = turf.points(polygon) + let center = turf.center(features) + + console.log(center) + + let points0 = Cesium.Cartographic.fromCartesian(positions[0]) + polygon.push([ + Cesium.Math.toDegrees(points0.longitude), + Cesium.Math.toDegrees(points0.latitude), + ]) + let areaPolygon = turf.polygon([polygon]) + let area = turf.area(areaPolygon) + + const tempPolygon = new Cesium.Entity({ + name: '__measure_area', + position: Cesium.Cartesian3.fromDegrees( + center.geometry.coordinates[0], + center.geometry.coordinates[1], + ), + polygon: { + hierarchy: new Cesium.PolygonHierarchy(positions), + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + material: Cesium.Color.fromCssColorString('#ffff00').withAlpha(0.3), + }, + polyline: { + positions: [...positions, positions[0]], + clampToGround: true, + width: 2, + material: Cesium.Color.fromCssColorString('#ffff00'), + }, + label: new Cesium.LabelGraphics({ + text: area.toFixed(2) + '平方米', + heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, + disableDepthTestDistance: Number.POSITIVE_INFINITY, + }), + }) + measureLayer.entities.add(tempPolygon) + return positions + }) + } + + // 取消当前测量 + const abort = () => { + editTool.abort() // 调用编辑工具的取消方法 + } + + // 清除所有测量结果 + const clear = () => { + measureLayer.entities.removeAll() + } + + return { + distance: measureDistance, + area: measureArea, + abort, + clear, + } +} + +export { getDistance, getArea, getBoundingCenter, getBoundingCenterCoordinate } diff --git a/src/router/index.js b/src/router/index.js index 37a167b..a5ce564 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -84,6 +84,22 @@ export const constantRoutes = [ } ] }, + { + path: '/systemTemplate', + component: Layout, + alwaysShow: true, + meta: { + title: '系统模板' + }, + children: [ + { + path: 'forestFire', + component: () => import('@/views/systemTemplate/forestFire/index.vue'), + name: 'forestFire', + meta: { title: '森林防火' }, + }, + ] + }, { path: '/gisManagement', component: Layout, diff --git a/src/views/systemTemplate/forestFire/DialogPropertyGrid/PropertyGrid.vue b/src/views/systemTemplate/forestFire/DialogPropertyGrid/PropertyGrid.vue new file mode 100644 index 0000000..4cffc9c --- /dev/null +++ b/src/views/systemTemplate/forestFire/DialogPropertyGrid/PropertyGrid.vue @@ -0,0 +1,596 @@ + + + + + diff --git a/src/views/systemTemplate/forestFire/DialogPropertyGrid/index.js b/src/views/systemTemplate/forestFire/DialogPropertyGrid/index.js new file mode 100644 index 0000000..47b30e0 --- /dev/null +++ b/src/views/systemTemplate/forestFire/DialogPropertyGrid/index.js @@ -0,0 +1,131 @@ +// src/utils/DialogPropertyGrid.js +import { createApp, h, ref, watch } from 'vue'; +import * as Cesium from 'cesium'; + +// 导入修改后的 PropertyGrid 组件 (现在作为 DialogPropertyGrid 的内容) +import PropertyGridContent from './PropertyGrid.vue'; // 假设文件名为 PropertyGrid.vue +import { getPropertyData } from './property'; + +// 用于存储当前弹窗的应用实例、容器和根组件实例,实现单例 +let currentApp = null; +let currentContainer = null; +let currentWrapperInstance = null; // 存储 wrapperComponent 实例的引用 + +/** + * 显示 DialogPropertyGrid 弹窗 + * @param {Cesium.Entity} entity - 获取属性的实体对象 + * @param {Function} [onSave] - 点击保存按钮后调用的回调函数,接收所有属性数据作为参数 + */ +const show = (entity = null, onSave = null) => { + // 0. 解析 data + const data = getPropertyData(entity); + + // 如果已经有弹窗实例存在 + if (currentApp && currentWrapperInstance) { + console.log('DialogPropertyGrid instance already exists. Updating content.'); + // 更新现有实例的数据和保存回调 + currentWrapperInstance.updateContent(data, onSave); + return; // 退出,不再创建新实例 + } + + // 1. 创建一个临时的 DOM 容器 + currentContainer = document.createElement('div'); + // 将容器添加到 body + document.body.appendChild(currentContainer); + + // 2. 定义清理函数 + const cleanup = () => { + if (currentApp) { + currentApp.unmount(); // 卸载 Vue 应用 + currentApp = null; + } + if (currentContainer) { + currentContainer.remove(); // 移除 DOM 元素 + currentContainer = null; + } + currentWrapperInstance = null; // 清理 wrapperComponent 实例引用 + console.log('DialogPropertyGrid cleaned up.'); + }; + + // 3. 创建 Vue 应用实例的根组件 (一个简单的 wrapper) + const wrapperComponent = { + setup() { + // 使用 ref 来存储和响应式更新传递给 PropertyGridContent 的 props + const componentData = ref(data); + const onSaveCallback = ref(onSave); // 默认回调为一个空函数 + + // 方法:更新数据和回调 (供外部 show 方法调用) + const updateContent = (data, cb) => { + componentData.value = data; + onSaveCallback.value = cb; + // 当数据更新时,如果弹窗之前被关闭了,这里不需要显式“显示” + // 因为组件本身是 fixed 定位,只要 mounted 就在那里 + // 如果需要控制显隐,可以在 PropertyGridContent 内部加一个 v-show 或 v-if + // 但为了单例和替换内容的需求,mounted/unmounted 更符合生命周期管理 + }; + + // 监听 PropertyGridContent 的 save 事件 + const handlePropertyGridSave = (allPropertyData) => { + // 调用外部传入的保存回调 + if (typeof onSaveCallback.value === 'function') { + onSaveCallback.value(allPropertyData); + } + // 保存后执行清理 (关闭弹窗) + cleanup(); + }; + + // 监听 PropertyGridContent 的 close 事件 + const handlePropertyGridClose = () => { + // 直接执行清理 (关闭弹窗) + cleanup(); + }; + + // 监听 PropertyGridContent 的 property-change 事件 (可选,如果需要在外部实时响应) + const handlePropertyChange = (key, newValue, property) => { + // console.log(`实时属性变更: ${property.name} = ${newValue}`); + // 如果需要,可以在这里添加额外的实时处理逻辑 + // 注意:这个事件是 PropertyGridContent 内部实时触发的,与 save 事件不同 + }; + + + // 暴露 updateContent 方法给外部 show 方法调用 + return { + updateContent, // 暴露更新方法 + componentData, // 暴露给 render 函数使用 + handlePropertyGridSave, // 暴露给 render 函数使用 + handlePropertyGridClose, // 暴露给 render 函数使用 + handlePropertyChange, // 暴露给 render 函数使用 + }; + }, + render() { + // 渲染 PropertyGridContent 组件 + // 将响应式的 ref 作为 props 传递 + return h(PropertyGridContent, { + data: this.componentData, // 绑定数据 + // 将回调函数作为 prop 传递 + onSaveCallback: this.handlePropertyGridSave, + // 监听 PropertyGridContent 内部触发的 close 事件 + onClose: this.handlePropertyGridClose, + // 监听 PropertyGridContent 内部触发的 property-change 事件 (可选) + onPropertyChange: this.handlePropertyChange, + // 注意:PropertyGridContent 内部的 save 事件现在直接调用了 onSaveCallback prop, + // 所以这里不再需要监听 @save 事件。 + }); + } + }; + + // 4. 创建 Vue 应用实例并挂载 + currentApp = createApp(wrapperComponent); + + // 5. 将应用挂载到临时容器 + // 挂载后,获取根组件实例,以便后续更新数据 + currentWrapperInstance = currentApp.mount(currentContainer); + + // 6. 立即调用 updateContent 初始化并显示弹窗 + currentWrapperInstance.updateContent(data, onSave); +}; + +// 导出 show 方法 +export default { + show +}; diff --git a/src/views/systemTemplate/forestFire/DialogPropertyGrid/property.js b/src/views/systemTemplate/forestFire/DialogPropertyGrid/property.js new file mode 100644 index 0000000..9bdd852 --- /dev/null +++ b/src/views/systemTemplate/forestFire/DialogPropertyGrid/property.js @@ -0,0 +1,123 @@ +import * as Cesium from 'cesium' + + // 定义水源地的属性 +export const watersource = [ + { + key: "type", + name: "要素类型", + type: "text", + value: "水源地", + disabled: true, + }, + { + key: "name", + name: "名称", + type: "text", + value: "", + disabled: false, + }, + { + key: "area", + name: "占地面积", + type: "text", + value: "0", + disabled: true, + }, + { + key: "longitude", + name: "经度", + type: "text", + value: "0", + disabled: true, + }, + { + key: "latitude", + name: "纬度", + type: "text", + value: "0", + disabled: true, + }, + { + key: "volume", + name: "储水量", + type: "text", + value: "0", + disabled: false, + }, +]; + + // 定义仓库的属性 +export const warehouse = [ + { + key: "type", + name: "要素类型", + type: "text", + value: "仓库", + disabled: true, + }, + { + key: "name", + name: "名称", + type: "text", + value: "", + disabled: false, + }, + { + key: "longitude", + name: "经度", + type: "text", + value: "0", + disabled: true, + }, + { + key: "latitude", + name: "纬度", + type: "text", + value: "0", + disabled: true, + }, +]; + +const getClone = (config) => { + return config.map(item => ({ ...item })) +} + +export const getPropertyData = (entity) => { + let data = [] + if(!entity || !entity.properties) { + return data // 如果没有实体或属性,返回空数组 + } + + const properties = entity.properties.getValue() + if (properties.__type === "watersource") { + data = getClone(watersource) + } else if (properties.__type === "warehouse") { + data = getClone(warehouse) + } + + // 遍历数据数组,将properties有的属性赋给对应的属性 + // 注意:地图要素属性主要参考这里的数据结构,就算properties没有值,也会显示出来 + data.forEach(item => { + const value = properties[item.key] + if (value !== undefined) { + item.value = value + } + }); + return data +}; + +export const setPropertyData = (data, entity) => { + if (!entity || !entity.properties) { + console.warn("Entity or properties not found") + return + } + + const newProperties = entity.properties.getValue() + data.forEach(item => { + if (item.key && item.value !== undefined) { + newProperties[item.key] = item.value + } + }) + // 替换 properties + entity.properties = new Cesium.PropertyBag(newProperties) +} diff --git a/src/views/systemTemplate/forestFire/Toolbar.vue b/src/views/systemTemplate/forestFire/Toolbar.vue new file mode 100644 index 0000000..b03ed56 --- /dev/null +++ b/src/views/systemTemplate/forestFire/Toolbar.vue @@ -0,0 +1,514 @@ + + + + + \ No newline at end of file diff --git a/src/views/systemTemplate/forestFire/index.vue b/src/views/systemTemplate/forestFire/index.vue new file mode 100644 index 0000000..756597c --- /dev/null +++ b/src/views/systemTemplate/forestFire/index.vue @@ -0,0 +1,131 @@ + + + + + \ No newline at end of file