controlPanel.vue 129 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804
  1. <template>
  2. <div id="controlPanelBox">
  3. <div>
  4. <div class="sceneNameBox">
  5. <div class="">
  6. 场景名称:
  7. <el-cascader
  8. :disabled="$route.query.sceneId != undefined"
  9. v-model="SceneValue"
  10. placeholder="试试搜索:距离"
  11. :options="SceneList"
  12. :props="{ expandTrigger: 'hover' }"
  13. @change="handleChange"
  14. filterable
  15. ></el-cascader>
  16. <el-tooltip
  17. v-if="SceneValue && dmsServerItem.functionalDefinition"
  18. :content="dmsServerItem.functionalDefinition"
  19. placement="bottom"
  20. :popper-style="{ maxWidth: '300px' }"
  21. >
  22. <el-icon
  23. style="margin-left: 1rem; width: 1rem; height: 1rem"
  24. v-show="SceneValue && dmsServerItem.functionalDefinition"
  25. ><QuestionFilled
  26. /></el-icon>
  27. </el-tooltip>
  28. </div>
  29. <div>
  30. <el-button type="primary" @click="sendGeometriesToBackend"
  31. >发送<i class="el-icon-s-promotion el-icon--right"></i
  32. ></el-button>
  33. </div>
  34. </div>
  35. <el-divider></el-divider>
  36. <div>元素个数:{{ SceneValue && dmsServerItem ? dmsServerItem.numberOf : "0" }}</div>
  37. <div>参数类型:{{ SceneValue && dmsServerItem ? dmsServerItem.elementTypes : "[]" }}</div>
  38. <div>接口地址:{{ SceneValue && dmsServerItem ? dmsServerItem.apiUrl : "/" }}</div>
  39. </div>
  40. <el-divider></el-divider>
  41. <div>
  42. <!-- 元素文本渲染和操作区域 -->
  43. <el-tabs tab-position="left" style="height: calc(100vh - 250px)" class="demo-tabs">
  44. <el-tab-pane label="入参">
  45. <div v-if="SceneValue && dmsServerItem && ifHasType('file')">
  46. <el-upload
  47. ref="sceneFileUpload"
  48. v-model:file-list="fileList"
  49. class="upload-demo"
  50. action=""
  51. accept=".geojson,.csv,.xlsx,.zip"
  52. :limit="1"
  53. :on-change="handleFileChange"
  54. :on-exceed="handleExceed"
  55. :on-remove="uploadRemove"
  56. :auto-upload="false"
  57. >
  58. <el-button type="primary">上传文件</el-button>
  59. </el-upload>
  60. </div>
  61. <div v-if="SceneValue && dmsServerItem">
  62. <div v-for="item in dmsServerItem.elementTypes" :key="item">
  63. <div
  64. v-if="SceneValue && dmsServerItem && selectOptions[item]"
  65. style="margin: 0.5rem 0"
  66. >
  67. {{ item }}:
  68. <el-input
  69. v-if="item === 'geocodeColumnKeyword' && isGeocodeImportZip"
  70. v-model="params[item]"
  71. placeholder="zip 内含多个表格时请手动输入列名"
  72. clearable
  73. style="width: 240px"
  74. @input="handleSelectChange(item, $event)"
  75. />
  76. <el-select
  77. v-else-if="selectOptions[item]"
  78. @change="handleSelectChange(item, $event)"
  79. v-model="params[item]"
  80. :placeholder="
  81. item === 'geocodeColumnKeyword' && (!selectOptions[item] || !selectOptions[item].length)
  82. ? '请先上传 csv / xlsx 解析列名'
  83. : '请选择' + item
  84. "
  85. style="width: 240px"
  86. :disabled="
  87. item === 'geocodeColumnKeyword' &&
  88. !isGeocodeImportZip &&
  89. (!selectOptions[item] || !selectOptions[item].length)
  90. "
  91. filterable
  92. clearable
  93. >
  94. <el-option
  95. v-for="item2 in selectOptions[item]"
  96. :key="item2.value"
  97. :label="item2.label"
  98. :value="item2.value"
  99. />
  100. </el-select>
  101. </div>
  102. <div
  103. v-if="
  104. ['lon', 'lat', 'lonKey', 'latKey', 'filePath', 'EPSILON', 'distance'].includes(
  105. item
  106. )
  107. "
  108. >
  109. {{ item }}:
  110. <el-input
  111. v-model="params[item]"
  112. @input="handleSelectChange(item, $event)"
  113. style="width: 240px"
  114. :placeholder="'请输入' + item"
  115. />
  116. </div>
  117. </div>
  118. </div>
  119. <div
  120. class="vueJsonEditor_box"
  121. v-show="
  122. SceneValue &&
  123. dmsServerItem &&
  124. (ifHasType('point') || ifHasType('polyline') || ifHasType('polygon'))
  125. "
  126. >
  127. <div class="vueJsonEditor_tools">
  128. <span
  129. v-if="jsonData && (jsonData.features || jsonData.geometry)"
  130. @click="showToMap(jsonData, 'input')"
  131. >渲染到地图中</span
  132. >
  133. <el-tooltip content="定位到当前入参渲染要素" placement="top">
  134. <span v-if="renderStatus.input" @click="locateRenderedGeojson('input')">
  135. 定位
  136. </span>
  137. </el-tooltip>
  138. <span @click="copyJsonData(jsonData)">copy</span>
  139. </div>
  140. <vue-json-editor
  141. :key="'json-input-editor-' + inputEditorKey"
  142. v-model="jsonData"
  143. :value="jsonData"
  144. :show-btns="false"
  145. :mode="'code'"
  146. :lang="'zh'"
  147. @json-change="handleJsonChange"
  148. >
  149. </vue-json-editor>
  150. </div>
  151. </el-tab-pane>
  152. <el-tab-pane label="返回">
  153. <div
  154. v-if="backData.message || backData.error"
  155. :style="{
  156. backgroundColor: backData.code === 200 ? '#67C23A' : '#F56C6C',
  157. color: '#fff',
  158. padding: '5px 10px',
  159. fontSize: '14px',
  160. }"
  161. >
  162. {{ backData.message || backData.error }}
  163. </div>
  164. <div class="vueJsonEditor_box" v-show="SceneValue && dmsServerItem">
  165. <div class="vueJsonEditor_tools">
  166. <span
  167. v-if="backData.content && (backData.content.features || backData.content.geometry)"
  168. @click="showToMap(backData.content, 'output')"
  169. >渲染到地图中</span
  170. >
  171. <el-tooltip content="定位到当前返回渲染要素" placement="top">
  172. <span v-if="renderStatus.output" @click="locateRenderedGeojson('output')">
  173. 定位
  174. </span>
  175. </el-tooltip>
  176. <span @click="copyJsonData(backData.content)">copy</span>
  177. </div>
  178. <vue-json-editor
  179. v-if="backData.content"
  180. :key="'json-output-editor-' + outputEditorKey"
  181. v-model="backData.content"
  182. :value="backData.content"
  183. @json-change="handleJsonChange2"
  184. :show-btns="false"
  185. :mode="'code'"
  186. :lang="'zh'"
  187. >
  188. </vue-json-editor></div
  189. ></el-tab-pane>
  190. </el-tabs>
  191. </div>
  192. <el-dialog
  193. v-model="propertyDialog.visible"
  194. title="要素属性"
  195. width="420px"
  196. draggable
  197. :modal="false"
  198. append-to-body
  199. class="feature-property-dialog"
  200. >
  201. <div class="feature-property-content">
  202. <div class="feature-property-tip">
  203. 支持直接编辑属性值,失焦后自动同步 JSON。选中地图上的点/线/面后,可按住顶点拖动以实时修改坐标(与右侧入参 JSON 同步)。
  204. </div>
  205. <div
  206. class="feature-property-item"
  207. v-for="item in propertyDialog.list"
  208. :key="'feature-property-' + item.id"
  209. >
  210. <div class="feature-property-row">
  211. <el-input
  212. v-model="item.editKey"
  213. class="feature-property-input feature-property-key-input"
  214. placeholder="属性名"
  215. @change="handlePropertyKeyChange(item)"
  216. />
  217. <el-button
  218. type="danger"
  219. plain
  220. size="small"
  221. class="feature-property-delete-btn"
  222. @click="deleteProperty(item)"
  223. >
  224. 删除
  225. </el-button>
  226. </div>
  227. <el-input
  228. v-model="item.editValue"
  229. class="feature-property-input"
  230. placeholder="属性值"
  231. @change="handlePropertyValueChange(item)"
  232. />
  233. </div>
  234. <el-empty
  235. v-if="!propertyDialog.list.length"
  236. description="当前要素无属性信息"
  237. :image-size="80"
  238. />
  239. </div>
  240. <template #footer>
  241. <div class="feature-property-footer">
  242. <el-button type="primary" plain @click="addProperty">+ 添加字段</el-button>
  243. <div class="feature-property-actions">
  244. <el-button link type="primary" @click="startEditGeometry">更改绘制</el-button>
  245. <el-button link type="danger" @click="deleteSelectedFeature">删除</el-button>
  246. </div>
  247. </div>
  248. </template>
  249. </el-dialog>
  250. <div
  251. v-if="hoverHint.visible"
  252. class="map-edit-hover-hint"
  253. :style="{ left: hoverHint.left + 'px', top: hoverHint.top + 'px' }"
  254. >
  255. {{ hoverHint.text }}
  256. </div>
  257. <!-- 绘制工具栏 -->
  258. <div
  259. class="toolbar"
  260. v-if="
  261. SceneValue &&
  262. dmsServerItem &&
  263. (ifHasType('point') || ifHasType('polyline') || ifHasType('polygon'))
  264. "
  265. >
  266. <div
  267. v-if="ifHasType('point')"
  268. class="tool-item"
  269. @click="activateDraw('point')"
  270. :class="{ active: currentTool === 'point' }"
  271. >
  272. 绘制点
  273. <el-tooltip
  274. content="绘制开关,蓝色状态代表开启,再次点击结束绘制,地图左键点击选点,"
  275. placement="right"
  276. >
  277. <el-icon style="width: 1rem; height: 1rem"><QuestionFilled /></el-icon>
  278. </el-tooltip>
  279. </div>
  280. <div
  281. v-if="ifHasType('polyline')"
  282. class="tool-item"
  283. @click="activateDraw('polyline')"
  284. :class="{ active: currentTool === 'polyline' }"
  285. >
  286. 绘制线<el-tooltip
  287. content="绘制线最少需要两个点,鼠标左键点击选点,点之间自动连线,鼠标右键结束绘制"
  288. placement="right"
  289. >
  290. <el-icon style="width: 1rem; height: 1rem"><QuestionFilled /></el-icon>
  291. </el-tooltip>
  292. </div>
  293. <div
  294. v-if="ifHasType('polygon')"
  295. class="tool-item"
  296. @click="activateDraw('polygon')"
  297. :class="{ active: currentTool === 'polygon' }"
  298. >
  299. 绘制面<el-tooltip
  300. content="绘制面最少需要三个点,鼠标左键点击选点,面自动必合,鼠标右键结束绘制"
  301. placement="right"
  302. >
  303. <el-icon style="width: 1rem; height: 1rem"><QuestionFilled /></el-icon>
  304. </el-tooltip>
  305. </div>
  306. <!-- <div class="tool-item" @click="startHoleDrawing" :class="{ active: isDrawingHole }">
  307. 绘制镂空
  308. </div> -->
  309. <div class="tool-item" @click="clearAll">清除所有</div>
  310. </div>
  311. </div>
  312. </template>
  313. <script>
  314. import * as XLSX from "xlsx";
  315. import wgn from "../../api/wgn";
  316. import { getFile } from "../../utils/request";
  317. import { WGN_SCENE_LIST } from "@/data/wgnSceneList";
  318. // 控制面板
  319. export default {
  320. name: "ControlPanel",
  321. // 2. 接收父组件传递的 props(单向数据流,子组件不能直接修改)
  322. props: {
  323. btnText: {
  324. type: String, // 类型限制
  325. default: "默认按钮", // 默认值
  326. required: false, // 是否必传
  327. },
  328. showCount: {
  329. type: Boolean,
  330. default: false,
  331. },
  332. },
  333. data() {
  334. return {
  335. currentFile: null, // 当前选中的文件
  336. fileList: [],
  337. // 其他参数
  338. params: {
  339. unit: "",
  340. outFileType: "",
  341. outPrj: "",
  342. inPrj: "",
  343. compressionRatio: "",
  344. lon: "",
  345. lat: "",
  346. // 非空转空时,类型字段下拉选择[地址\村居\街镇\区县\要素ID]
  347. keyType: "",
  348. // 非空转空时,上传文件解析csv或xlsx文件后,渲染列名到下拉选择框中,指定某列作为类型字段
  349. geocodeColumnKeyword: "",
  350. },
  351. // 请求参数
  352. jsonData: {},
  353. // 返回参数
  354. backData: {},
  355. // 当前场景值
  356. SceneValue: "",
  357. // 场景列表(与微功能中心侧栏同源)
  358. SceneList: WGN_SCENE_LIST,
  359. dmsServerItem: {},
  360. selectOptions: {
  361. // 单位
  362. unit: [
  363. {
  364. value: "METER",
  365. label: "米",
  366. },
  367. {
  368. value: "KILOMETER",
  369. label: "千米",
  370. },
  371. ],
  372. inPrj: [
  373. {
  374. value: "WGS84",
  375. label: "WGS84",
  376. },
  377. {
  378. value: "SH2000",
  379. label: "SH2000",
  380. },
  381. {
  382. value: "UTM",
  383. label: "UTM",
  384. },
  385. {
  386. value: "WEB_Mercator",
  387. label: "WEB_Mercator",
  388. },
  389. ],
  390. compressionRatio: [
  391. {
  392. value: "100%",
  393. label: "100%",
  394. },
  395. {
  396. value: "80%",
  397. label: "80%",
  398. },
  399. {
  400. value: "50%",
  401. label: "50%",
  402. },
  403. {
  404. value: "25%",
  405. label: "25%",
  406. },
  407. ],
  408. outPrj: [
  409. {
  410. value: "WGS84",
  411. label: "WGS84",
  412. },
  413. {
  414. value: "SH2000",
  415. label: "SH2000",
  416. },
  417. {
  418. value: "UTM",
  419. label: "UTM",
  420. },
  421. {
  422. value: "WEB_Mercator",
  423. label: "WEB_Mercator",
  424. },
  425. ],
  426. outFileType: [
  427. {
  428. value: "geoJson",
  429. label: "geoJson",
  430. },
  431. {
  432. value: "shp",
  433. label: "shp",
  434. },
  435. {
  436. value: "csv",
  437. label: "csv",
  438. },
  439. {
  440. value: "xlsx",
  441. label: "xlsx",
  442. },
  443. ],
  444. keyType: [
  445. { value: "地址", label: "地址" },
  446. { value: "村居", label: "村居" },
  447. { value: "街镇", label: "街镇" },
  448. { value: "区县", label: "区县" },
  449. // { value: "要素ID", label: "要素ID" },
  450. ],
  451. geocodeColumnKeyword: [],
  452. },
  453. currentTool: null,
  454. drawingMode: null,
  455. handler: null,
  456. drawnEntities: [],
  457. geometries: [], // 存储绘制的几何对象
  458. // 绘制状态相关
  459. currentEntity: null, // 当前正在绘制的实体
  460. currentPositions: [], // 当前正在绘制的位置数组
  461. isDrawingHole: false, // 是否正在绘制镂空
  462. currentPolygonEntity: null, // 当前要添加镂空的多边形实体
  463. currentPolygonGeometry: null, // 当前要添加镂空的多边形几何对象
  464. tempEntity: null, // 临时预览实体
  465. // 地图渲染状态(入参/返回)
  466. renderStatus: {
  467. input: false,
  468. output: false,
  469. },
  470. // 缓存两侧最近一次成功渲染的geojson,用于定位时恢复
  471. renderedGeojsonCache: {
  472. input: null,
  473. output: null,
  474. },
  475. currentRenderedSource: "",
  476. // 地图点击属性弹窗
  477. featurePickHandler: null,
  478. inputEditorKey: 0,
  479. outputEditorKey: 0,
  480. propertyIdSeed: 1,
  481. propertyDialog: {
  482. visible: false,
  483. list: [],
  484. source: "",
  485. propertiesRef: null,
  486. featureIndex: -1,
  487. },
  488. /** 地图编辑选中:高亮要素 + 顶点柄 */
  489. mapEditSelection: null,
  490. vertexHandleEntities: [],
  491. hoveredVertexIndex: -1,
  492. hoveredHandleEntity: null,
  493. hoveredFeatureEntity: null,
  494. geometryEditMode: false,
  495. pickedFeatureRef: null,
  496. hoverHint: {
  497. visible: false,
  498. text: "",
  499. left: 0,
  500. top: 0,
  501. },
  502. /** 拖动/绘制时临时关闭 requestRenderMode,避免面片异步剖分导致与顶点不同步 */
  503. _interactiveRenderDepth: 0,
  504. _savedSceneRenderMode: null,
  505. };
  506. },
  507. computed: {
  508. /** zip 多表场景下列名无法预解析,改用手动输入 */
  509. isGeocodeImportZip() {
  510. const f = this.currentFile;
  511. const name = f ? f.name || (f.raw && f.raw.name) || "" : "";
  512. return String(name).toLowerCase().endsWith(".zip");
  513. },
  514. },
  515. mounted() {
  516. this.SceneValue = this.$route.query.sceneId ? this.$route.query.sceneId : "";
  517. },
  518. beforeUnmount() {
  519. // 组件卸载前清理资源
  520. this.deactivateDraw();
  521. if (this.handler) {
  522. this.handler.destroy();
  523. }
  524. if (this.featurePickHandler) {
  525. this.featurePickHandler.destroy();
  526. this.featurePickHandler = null;
  527. }
  528. this.resetPickDragState();
  529. this.setScreenSpaceCameraForDrag(true);
  530. while (this._interactiveRenderDepth > 0) {
  531. this.exitInteractiveSceneRender();
  532. }
  533. },
  534. watch: {
  535. SceneValue(newVal, oldVal) {
  536. if (newVal && newVal != oldVal) {
  537. // 请求DMS
  538. this.searchServerList(this.SceneValue);
  539. }
  540. },
  541. dmsServerItem: {
  542. handler() {
  543. this.$nextTick(() => this.syncDrawToolWithElementTypes());
  544. },
  545. deep: true,
  546. },
  547. },
  548. methods: {
  549. /** 与 DMS 入参规则 c_input_parameter_rules 解析后的 elementTypes 对齐,支持少量别名 */
  550. expandParameterTypeAliases(type) {
  551. const aliases = [type, type + "Z"];
  552. if (type === "polyline") {
  553. aliases.push("line", "lineZ", "LineString", "LineStringZ");
  554. }
  555. if (type === "polygon") {
  556. aliases.push("multipolygon", "MultiPolygon", "MultiPolygonZ");
  557. }
  558. return aliases;
  559. },
  560. ifHasType(type) {
  561. if (
  562. !this.dmsServerItem ||
  563. !this.dmsServerItem.elementTypes ||
  564. this.dmsServerItem.elementTypes.length === 0
  565. ) {
  566. return false;
  567. }
  568. const declared = this.dmsServerItem.elementTypes;
  569. const candidates = this.expandParameterTypeAliases(type);
  570. return declared.some((t) => candidates.includes(t));
  571. },
  572. /** DMS 声明的单个参数是否属于点/线/面几何槽位,返回规范名 point | polyline | polygon */
  573. geometrySlotCanonical(declaredType) {
  574. if (declaredType == null || declaredType === "") {
  575. return null;
  576. }
  577. const d = String(declaredType);
  578. for (const canon of ["point", "polyline", "polygon"]) {
  579. const aliases = this.expandParameterTypeAliases(canon);
  580. const hit = aliases.some(
  581. (a) => String(a) === d || String(a).toLowerCase() === d.toLowerCase()
  582. );
  583. if (hit) {
  584. return canon;
  585. }
  586. }
  587. return null;
  588. },
  589. /** GeoJSON geometry.type -> 与 geometrySlotCanonical 同一套规范名 */
  590. featureGeometryCanonical(geoJsonType) {
  591. if (!geoJsonType) {
  592. return null;
  593. }
  594. const t = String(geoJsonType);
  595. if (t === "Point") {
  596. return "point";
  597. }
  598. if (t === "LineString" || t === "MultiLineString") {
  599. return "polyline";
  600. }
  601. if (t === "Polygon" || t === "MultiPolygon") {
  602. return "polygon";
  603. }
  604. return null;
  605. },
  606. /**
  607. * 发送前校验:当「元素个数」numberOf > 0 时,与入参 FeatureCollection.features 数量对齐;
  608. * numberOf 为 0 时表示几何要素个数不固定(支持多个),不拦截数量。
  609. * 「参数类型」elementTypes 中的 point/polyline/polygon 仅表示允许的几何类型(可多选),
  610. * 不要求每种类型各来一个,也不要求与声明顺序一一对应(忽略 unit/file 等非几何项)。
  611. */
  612. validateSendGeometryParams() {
  613. if (!this.dmsServerItem || !Array.isArray(this.dmsServerItem.elementTypes)) {
  614. return { ok: true };
  615. }
  616. const types = this.dmsServerItem.elementTypes;
  617. const geoSlots = types
  618. .map((declared) => ({
  619. declared,
  620. canon: this.geometrySlotCanonical(declared),
  621. }))
  622. .filter((s) => s.canon != null);
  623. if (geoSlots.length === 0) {
  624. return { ok: true };
  625. }
  626. const allowedCanonSet = new Set(geoSlots.map((s) => s.canon));
  627. const rawN = this.dmsServerItem.numberOf;
  628. let expectedGeomCount = null;
  629. if (rawN !== undefined && rawN !== null && rawN !== "") {
  630. const parsedN = typeof rawN === "number" ? rawN : Number(rawN);
  631. // 0 表示不固定个数、支持多个要素,不做「元素个数」与 features 数量对齐拦截
  632. if (Number.isFinite(parsedN) && parsedN > 0) {
  633. expectedGeomCount = parsedN;
  634. }
  635. }
  636. const fc = this.jsonData;
  637. const features = fc && Array.isArray(fc.features) ? fc.features : [];
  638. if (expectedGeomCount !== null) {
  639. if (features.length > expectedGeomCount) {
  640. return {
  641. ok: false,
  642. message: `入参几何要素过多:当前 ${features.length} 个,场景要求 ${expectedGeomCount} 个(元素个数)`,
  643. };
  644. }
  645. if (features.length < expectedGeomCount) {
  646. return {
  647. ok: false,
  648. message: `入参几何要素不足:当前 ${features.length} 个,场景要求 ${expectedGeomCount} 个(元素个数),请绘制或渲染足够要素`,
  649. };
  650. }
  651. }
  652. for (let i = 0; i < features.length; i++) {
  653. const f = features[i];
  654. const g = f && f.geometry;
  655. if (!g || !g.type) {
  656. return {
  657. ok: false,
  658. message: `第 ${i + 1} 个几何要素缺少有效的 geometry`,
  659. };
  660. }
  661. const fk = this.featureGeometryCanonical(g.type);
  662. if (!fk) {
  663. return {
  664. ok: false,
  665. message: `第 ${i + 1} 个几何要素类型「${g.type}」不符合场景要求,请使用点、线或面`,
  666. };
  667. }
  668. if (!allowedCanonSet.has(fk)) {
  669. const allowedLabel = [...allowedCanonSet].join("、");
  670. return {
  671. ok: false,
  672. message: `第 ${i + 1} 个几何要素类型「${g.type}」不在场景允许的类型内,允许:${allowedLabel}`,
  673. };
  674. }
  675. }
  676. return { ok: true };
  677. },
  678. /** 场景切换后若当前绘制类型不在参数类型中,则关闭绘制避免“隐形”仍在绘制 */
  679. syncDrawToolWithElementTypes() {
  680. if (!this.currentTool) {
  681. return;
  682. }
  683. const t = this.currentTool;
  684. if (t === "point" && !this.ifHasType("point")) {
  685. this.deactivateDraw();
  686. } else if (t === "polyline" && !this.ifHasType("polyline")) {
  687. this.deactivateDraw();
  688. } else if (t === "polygon" && !this.ifHasType("polygon")) {
  689. this.deactivateDraw();
  690. }
  691. },
  692. handleJsonChange(jsonStr) {
  693. this.jsonData = jsonStr;
  694. },
  695. handleJsonChange2(jsonStr) {
  696. this.backData.content = jsonStr;
  697. },
  698. // 搜索微功能服务
  699. searchServerList() {
  700. let requestParams = {
  701. columnId: systemConfig.columnIds[5],
  702. states: 0,
  703. pageSize: 999,
  704. page: 0,
  705. };
  706. if (this.SceneValue) {
  707. requestParams.search = JSON.stringify([
  708. {
  709. field: "c_scene_name",
  710. searchType: 1,
  711. content: { value: this.SceneValue },
  712. },
  713. ]);
  714. // 获取微功能服务列表
  715. wgn
  716. .getDmsData(requestParams)
  717. .then((res) => {
  718. if (res.code === 200 && res.content.data[0]) {
  719. let dmsServiceData = res.content.data[0];
  720. this.dmsServerItem = {
  721. // 功能描述
  722. functionalDefinition: dmsServiceData.content,
  723. // 元素类型
  724. elementTypes: JSON.parse(dmsServiceData.c_input_parameter_rules),
  725. // 元素个数
  726. numberOf: JSON.parse(dmsServiceData.c_number_of_elements),
  727. // 后台接口路径
  728. apiUrl: dmsServiceData.c_url,
  729. };
  730. } else {
  731. this.$message({
  732. message: "搜索微功能服务失败,请检查场景名称是否正确",
  733. type: "warning",
  734. });
  735. }
  736. })
  737. .catch((e) => {
  738. this.$message({
  739. message: "搜索微功能服务失败" + e,
  740. type: "error",
  741. });
  742. });
  743. }
  744. },
  745. getSourceGeojsonData(source) {
  746. return source === "output" ? this.backData.content : this.jsonData;
  747. },
  748. syncGeometriesFromRenderedSource() {
  749. const src = this.currentRenderedSource || "input";
  750. const geojson = this.getSourceGeojsonData(src);
  751. if (!geojson) {
  752. return;
  753. }
  754. const list = [];
  755. if (geojson.features && Array.isArray(geojson.features)) {
  756. geojson.features.forEach((f) => {
  757. if (f && f.geometry && f.geometry.type && f.geometry.coordinates) {
  758. list.push({
  759. type: f.geometry.type,
  760. coordinates: JSON.parse(JSON.stringify(f.geometry.coordinates)),
  761. });
  762. }
  763. });
  764. } else if (geojson.geometry && geojson.geometry.type && geojson.geometry.coordinates) {
  765. list.push({
  766. type: geojson.geometry.type,
  767. coordinates: JSON.parse(JSON.stringify(geojson.geometry.coordinates)),
  768. });
  769. }
  770. if (list.length) {
  771. this.geometries = list;
  772. }
  773. },
  774. // 将用户输入或后台返回的geojson渲染到地图中
  775. showToMap(geojson, source = "input") {
  776. if (!geojson || (!geojson.features && !geojson.geometry)) {
  777. this.$message({
  778. message: "当前JSON缺少有效几何信息,无法渲染",
  779. type: "warning",
  780. });
  781. return;
  782. }
  783. this.currentRenderedSource = source;
  784. // 1. 清除所有地图中的元素
  785. this.clearAllMap();
  786. // 2. 将geojson添加到地图中
  787. this.addToMap(geojson);
  788. this.currentRenderedSource = source;
  789. this.renderStatus[source] = true;
  790. // 仅缓存纯数据,避免响应式对象带来的引用副作用
  791. this.renderedGeojsonCache[source] = JSON.parse(JSON.stringify(geojson));
  792. this.flyToGeojson(geojson);
  793. },
  794. // 定位到已渲染要素
  795. locateRenderedGeojson(source) {
  796. if (!this.renderStatus[source]) {
  797. return;
  798. }
  799. // 当前地图不是该来源数据时,优先恢复对应渲染结果,再飞行定位
  800. if (this.currentRenderedSource !== source && this.renderedGeojsonCache[source]) {
  801. this.showToMap(this.renderedGeojsonCache[source], source);
  802. return;
  803. }
  804. const sourceGeojson = this.getSourceGeojsonData(source) || this.renderedGeojsonCache[source];
  805. this.flyToGeojson(sourceGeojson);
  806. },
  807. collectGeometryCoordinates(geometry) {
  808. if (!geometry || !geometry.coordinates) {
  809. return [];
  810. }
  811. const { type, coordinates } = geometry;
  812. const points = [];
  813. if (type === "Point") {
  814. points.push([coordinates[0], coordinates[1]]);
  815. } else if (type === "LineString") {
  816. coordinates.forEach((coord) => points.push([coord[0], coord[1]]));
  817. } else if (type === "Polygon") {
  818. coordinates.forEach((ring) => {
  819. ring.forEach((coord) => points.push([coord[0], coord[1]]));
  820. });
  821. } else if (type === "MultiPolygon") {
  822. coordinates.forEach((polygon) => {
  823. polygon.forEach((ring) => {
  824. ring.forEach((coord) => points.push([coord[0], coord[1]]));
  825. });
  826. });
  827. }
  828. return points;
  829. },
  830. collectGeojsonCoordinates(geojson) {
  831. if (!geojson) {
  832. return [];
  833. }
  834. if (geojson.features && Array.isArray(geojson.features)) {
  835. return geojson.features.flatMap((feature) =>
  836. this.collectGeometryCoordinates(feature.geometry)
  837. );
  838. }
  839. if (geojson.geometry) {
  840. return this.collectGeometryCoordinates(geojson.geometry);
  841. }
  842. return [];
  843. },
  844. // 飞行定位到geojson范围
  845. flyToGeojson(geojson) {
  846. if (!viewer || !geojson) {
  847. return;
  848. }
  849. const points = this.collectGeojsonCoordinates(geojson);
  850. if (!points.length) {
  851. return;
  852. }
  853. if (points.length === 1) {
  854. viewer.camera.flyTo({
  855. destination: SkyScenery.Cartesian3.fromDegrees(points[0][0], points[0][1], 1200),
  856. duration: 1.1,
  857. orientation: {
  858. heading: viewer.camera.heading,
  859. pitch: SkyScenery.Math.toRadians(-65),
  860. roll: 0,
  861. },
  862. });
  863. viewer.scene.requestRender();
  864. return;
  865. }
  866. let minLon = Infinity;
  867. let maxLon = -Infinity;
  868. let minLat = Infinity;
  869. let maxLat = -Infinity;
  870. points.forEach((point) => {
  871. minLon = Math.min(minLon, point[0]);
  872. maxLon = Math.max(maxLon, point[0]);
  873. minLat = Math.min(minLat, point[1]);
  874. maxLat = Math.max(maxLat, point[1]);
  875. });
  876. const lonPad = Math.max((maxLon - minLon) * 0.25, 0.0008);
  877. const latPad = Math.max((maxLat - minLat) * 0.25, 0.0008);
  878. viewer.camera.flyTo({
  879. destination: SkyScenery.Rectangle.fromDegrees(
  880. minLon - lonPad,
  881. minLat - latPad,
  882. maxLon + lonPad,
  883. maxLat + latPad
  884. ),
  885. duration: 1.1,
  886. });
  887. viewer.scene.requestRender();
  888. },
  889. // 把属性对象格式化为可编辑列表
  890. formatFeatureProperties(properties) {
  891. if (!properties || typeof properties !== "object") {
  892. return [];
  893. }
  894. return Object.keys(properties).map((key) => {
  895. const rawValue = properties[key];
  896. const editValue =
  897. rawValue === null || rawValue === undefined
  898. ? ""
  899. : typeof rawValue === "object"
  900. ? JSON.stringify(rawValue)
  901. : String(rawValue);
  902. return {
  903. id: this.propertyIdSeed++,
  904. originalKey: key,
  905. editKey: key,
  906. editValue,
  907. };
  908. });
  909. },
  910. parsePropertyValue(inputValue) {
  911. if (typeof inputValue !== "string") {
  912. return inputValue;
  913. }
  914. const value = inputValue.trim();
  915. if (value === "") {
  916. return "";
  917. }
  918. if (value === "true") {
  919. return true;
  920. }
  921. if (value === "false") {
  922. return false;
  923. }
  924. if (value === "null") {
  925. return null;
  926. }
  927. if (!isNaN(Number(value))) {
  928. return Number(value);
  929. }
  930. if (
  931. (value.startsWith("{") && value.endsWith("}")) ||
  932. (value.startsWith("[") && value.endsWith("]"))
  933. ) {
  934. try {
  935. return JSON.parse(value);
  936. } catch (e) {
  937. return inputValue;
  938. }
  939. }
  940. return inputValue;
  941. },
  942. syncJsonEditorData(source) {
  943. if (source === "output") {
  944. if (!this.backData.content) {
  945. return;
  946. }
  947. this.backData = {
  948. ...this.backData,
  949. content: JSON.parse(JSON.stringify(this.backData.content)),
  950. };
  951. } else {
  952. this.jsonData = JSON.parse(JSON.stringify(this.jsonData));
  953. }
  954. if (source === "output") {
  955. this.outputEditorKey += 1;
  956. } else {
  957. this.inputEditorKey += 1;
  958. }
  959. this.renderedGeojsonCache[source] = JSON.parse(
  960. JSON.stringify(this.getSourceGeojsonData(source))
  961. );
  962. const latestGeojson = this.getSourceGeojsonData(source);
  963. if (latestGeojson && this.propertyDialog.visible && this.propertyDialog.source === source) {
  964. if (
  965. this.propertyDialog.featureIndex >= 0 &&
  966. latestGeojson.features &&
  967. latestGeojson.features[this.propertyDialog.featureIndex]
  968. ) {
  969. if (!latestGeojson.features[this.propertyDialog.featureIndex].properties) {
  970. latestGeojson.features[this.propertyDialog.featureIndex].properties = {};
  971. }
  972. this.propertyDialog.propertiesRef =
  973. latestGeojson.features[this.propertyDialog.featureIndex].properties;
  974. } else {
  975. if (!latestGeojson.properties) {
  976. latestGeojson.properties = {};
  977. }
  978. this.propertyDialog.propertiesRef = latestGeojson.properties;
  979. }
  980. }
  981. },
  982. handlePropertyValueChange(item) {
  983. if (!this.propertyDialog.propertiesRef || !item) {
  984. return;
  985. }
  986. const currentKey = (item.editKey || "").trim();
  987. if (!currentKey) {
  988. this.$message({
  989. message: "属性名不能为空",
  990. type: "warning",
  991. });
  992. item.editKey = item.originalKey || "";
  993. return;
  994. }
  995. this.propertyDialog.propertiesRef[currentKey] = this.parsePropertyValue(item.editValue);
  996. item.originalKey = currentKey;
  997. item.editKey = currentKey;
  998. this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
  999. this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
  1000. },
  1001. handlePropertyKeyChange(item) {
  1002. if (!this.propertyDialog.propertiesRef || !item) {
  1003. return;
  1004. }
  1005. const oldKey = item.originalKey;
  1006. const newKey = (item.editKey || "").trim();
  1007. if (!newKey) {
  1008. this.$message({
  1009. message: "属性名不能为空",
  1010. type: "warning",
  1011. });
  1012. item.editKey = oldKey;
  1013. return;
  1014. }
  1015. if (
  1016. newKey !== oldKey &&
  1017. Object.prototype.hasOwnProperty.call(this.propertyDialog.propertiesRef, newKey)
  1018. ) {
  1019. this.$message({
  1020. message: "属性名已存在,请更换",
  1021. type: "warning",
  1022. });
  1023. item.editKey = oldKey;
  1024. return;
  1025. }
  1026. const oldValue = this.propertyDialog.propertiesRef[oldKey];
  1027. if (newKey !== oldKey) {
  1028. delete this.propertyDialog.propertiesRef[oldKey];
  1029. this.propertyDialog.propertiesRef[newKey] = oldValue;
  1030. }
  1031. item.originalKey = newKey;
  1032. item.editKey = newKey;
  1033. this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
  1034. this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
  1035. },
  1036. deleteProperty(item) {
  1037. if (!this.propertyDialog.propertiesRef || !item) {
  1038. return;
  1039. }
  1040. const key = (item.originalKey || "").trim();
  1041. if (key && Object.prototype.hasOwnProperty.call(this.propertyDialog.propertiesRef, key)) {
  1042. delete this.propertyDialog.propertiesRef[key];
  1043. }
  1044. this.propertyDialog.list = this.propertyDialog.list.filter(
  1045. (property) => property.id !== item.id
  1046. );
  1047. this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
  1048. this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
  1049. },
  1050. addProperty() {
  1051. if (!this.propertyDialog.propertiesRef) {
  1052. this.$message({
  1053. message: "请先点击地图中的已渲染要素",
  1054. type: "warning",
  1055. });
  1056. return;
  1057. }
  1058. let index = 1;
  1059. let newKey = "newKey";
  1060. while (Object.prototype.hasOwnProperty.call(this.propertyDialog.propertiesRef, newKey)) {
  1061. newKey = `newKey${index}`;
  1062. index += 1;
  1063. }
  1064. this.propertyDialog.propertiesRef[newKey] = "";
  1065. this.propertyDialog.list.push({
  1066. id: this.propertyIdSeed++,
  1067. originalKey: newKey,
  1068. editKey: newKey,
  1069. editValue: "",
  1070. });
  1071. this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
  1072. this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
  1073. },
  1074. startEditGeometry() {
  1075. const picked = this.pickedFeatureRef;
  1076. if (!picked || !picked.entity) {
  1077. this.$message({
  1078. message: "请先选择要素",
  1079. type: "warning",
  1080. });
  1081. return;
  1082. }
  1083. this.geometryEditMode = true;
  1084. this.deactivateDraw();
  1085. this.applyMapEditSelection(picked.entity);
  1086. this.propertyDialog.visible = false;
  1087. this.$message({
  1088. message: "已进入节点编辑模式,可拖拽顶点或中点调整绘制",
  1089. type: "success",
  1090. });
  1091. },
  1092. reindexEntityFeatureRefs(source, deletedIndex) {
  1093. this.drawnEntities.forEach((ent) => {
  1094. if (!ent || !ent.__featureRef) return;
  1095. if (
  1096. ent.__featureRef.source === source &&
  1097. typeof ent.__featureRef.featureIndex === "number" &&
  1098. ent.__featureRef.featureIndex > deletedIndex
  1099. ) {
  1100. ent.__featureRef.featureIndex -= 1;
  1101. }
  1102. });
  1103. },
  1104. deleteSelectedFeature() {
  1105. const sel = this.mapEditSelection || this.pickedFeatureRef;
  1106. if (!sel || !sel.entity || sel.featureIndex < 0) {
  1107. this.$message({
  1108. message: "请先选择要素",
  1109. type: "warning",
  1110. });
  1111. return;
  1112. }
  1113. const source = sel.source || this.currentRenderedSource || "input";
  1114. const geojson = this.getSourceGeojsonData(source);
  1115. if (!geojson || !geojson.features || !Array.isArray(geojson.features)) {
  1116. this.$message({
  1117. message: "当前数据不是 FeatureCollection,无法删除",
  1118. type: "warning",
  1119. });
  1120. return;
  1121. }
  1122. const idx = sel.featureIndex;
  1123. if (idx < 0 || idx >= geojson.features.length) {
  1124. this.$message({
  1125. message: "要素索引无效,删除失败",
  1126. type: "error",
  1127. });
  1128. return;
  1129. }
  1130. geojson.features.splice(idx, 1);
  1131. const toRemove = this.drawnEntities.filter(
  1132. (ent) =>
  1133. ent &&
  1134. ent.__featureRef &&
  1135. ent.__featureRef.source === source &&
  1136. ent.__featureRef.featureIndex === idx
  1137. );
  1138. toRemove.forEach((ent) => {
  1139. try {
  1140. viewer.entities.remove(ent);
  1141. } catch (e) {
  1142. /* ignore */
  1143. }
  1144. });
  1145. this.drawnEntities = this.drawnEntities.filter((ent) => !toRemove.includes(ent));
  1146. this.reindexEntityFeatureRefs(source, idx);
  1147. this.propertyDialog.visible = false;
  1148. this.propertyDialog.list = [];
  1149. this.propertyDialog.source = "";
  1150. this.propertyDialog.propertiesRef = null;
  1151. this.propertyDialog.featureIndex = -1;
  1152. this.clearMapEditSelection();
  1153. this.syncJsonEditorData(source);
  1154. this.$message({
  1155. message: "删除成功",
  1156. type: "success",
  1157. });
  1158. },
  1159. syncPropertyToEntity(propertiesRef) {
  1160. if (!propertiesRef || !viewer || !this.currentRenderedSource) {
  1161. return;
  1162. }
  1163. const targetSource = this.propertyDialog.source || this.currentRenderedSource;
  1164. const targetIndex = this.propertyDialog.featureIndex;
  1165. this.drawnEntities.forEach((entity) => {
  1166. if (!entity || !entity.__featureRef) {
  1167. return;
  1168. }
  1169. if (
  1170. entity.__featureRef.source === targetSource &&
  1171. entity.__featureRef.featureIndex === targetIndex
  1172. ) {
  1173. entity.__featureProperties = propertiesRef;
  1174. }
  1175. });
  1176. },
  1177. // 将geojson添加到地图中
  1178. addToMap(geojson) {
  1179. const source = this.currentRenderedSource || "input";
  1180. if (!geojson.features && geojson.geometry) {
  1181. const { type, coordinates } = geojson.geometry;
  1182. const featureProperties = geojson.properties || {};
  1183. const featureRefInfo = {
  1184. source,
  1185. featureIndex: -1,
  1186. };
  1187. switch (type) {
  1188. case "Point":
  1189. // 点
  1190. this.addPoint(coordinates, featureProperties, featureRefInfo);
  1191. break;
  1192. case "LineString":
  1193. // 线
  1194. this.addLine(coordinates, featureProperties, featureRefInfo);
  1195. break;
  1196. case "Polygon":
  1197. case "MultiPolygon":
  1198. // 面
  1199. this.addPolygon(coordinates, featureProperties, featureRefInfo);
  1200. break;
  1201. default:
  1202. break;
  1203. }
  1204. } else if (geojson.features) {
  1205. const features = geojson.features;
  1206. // 2. 遍历features,根据type添加到地图中
  1207. console.log("features", features);
  1208. features.forEach((feature, featureIndex) => {
  1209. const { type, coordinates } = feature.geometry;
  1210. const featureProperties = feature.properties || {};
  1211. const featureRefInfo = {
  1212. source,
  1213. featureIndex,
  1214. };
  1215. switch (type) {
  1216. case "Point":
  1217. // 点
  1218. this.addPoint(coordinates, featureProperties, featureRefInfo);
  1219. break;
  1220. case "LineString":
  1221. // 线
  1222. this.addLine(coordinates, featureProperties, featureRefInfo);
  1223. break;
  1224. case "Polygon":
  1225. case "MultiPolygon":
  1226. // 面
  1227. this.addPolygon(coordinates, featureProperties, featureRefInfo);
  1228. break;
  1229. default:
  1230. break;
  1231. }
  1232. });
  1233. }
  1234. setTimeout(() => {
  1235. viewer.scene.requestRender();
  1236. });
  1237. },
  1238. // 添加点到地图中
  1239. addPoint(coordinates, featureProperties = null, featureRefInfo = null) {
  1240. // 1. 解析点的坐标
  1241. console.log("addPoint coordinates", coordinates);
  1242. // 2. 创建点实体
  1243. const pointEntity = viewer.entities.add({
  1244. name: "point",
  1245. position:
  1246. coordinates.length > 2
  1247. ? SkyScenery.Cartesian3.fromDegrees(coordinates[0], coordinates[1], coordinates[2])
  1248. : SkyScenery.Cartesian3.fromDegrees(coordinates[0], coordinates[1]),
  1249. point: {
  1250. show: true,
  1251. color: SkyScenery.Color.RED,
  1252. pixelSize: 10,
  1253. outlineColor: SkyScenery.Color.WHITE,
  1254. outlineWidth: 2,
  1255. },
  1256. });
  1257. pointEntity.__featureProperties = featureProperties;
  1258. pointEntity.__featureRef = featureRefInfo;
  1259. // 3. 将点实体添加到drawnEntities中
  1260. this.drawnEntities.push(pointEntity);
  1261. },
  1262. // 添加线到地图中
  1263. addLine(coordinates, featureProperties = null, featureRefInfo = null) {
  1264. // 1. 解析线的坐标
  1265. console.log("addLine coordinates", coordinates);
  1266. // 2. 处理坐标格式:如果是二维数组则取第一个元素,否则直接使用
  1267. const lineCoordinates =
  1268. Array.isArray(coordinates[0]) && Array.isArray(coordinates[0][0])
  1269. ? coordinates[0]
  1270. : coordinates;
  1271. // 3. 检测坐标是否包含Z值
  1272. const hasZValues = lineCoordinates.some(
  1273. (coord) => coord.length > 2 && typeof coord[2] === "number"
  1274. );
  1275. // 4. 根据是否有Z值选择合适的坐标转换方法
  1276. let positions;
  1277. if (hasZValues) {
  1278. // 包含Z值,使用带高度的坐标转换方法
  1279. const flatCoordinatesWithHeights = [];
  1280. lineCoordinates.forEach((coord) => {
  1281. flatCoordinatesWithHeights.push(coord[0], coord[1], coord[2] || 0);
  1282. });
  1283. positions = SkyScenery.Cartesian3.fromDegreesArrayHeights(flatCoordinatesWithHeights);
  1284. } else {
  1285. // 不包含Z值,使用普通坐标转换方法
  1286. const flatCoordinates = [];
  1287. lineCoordinates.forEach((coord) => {
  1288. flatCoordinates.push(coord[0], coord[1]);
  1289. });
  1290. positions = SkyScenery.Cartesian3.fromDegreesArray(flatCoordinates);
  1291. }
  1292. // 5. 创建线实体
  1293. const lineEntity = viewer.entities.add({
  1294. name: "line",
  1295. polyline: {
  1296. show: true,
  1297. positions: positions,
  1298. material: SkyScenery.Color.BLUE.withAlpha(0.8),
  1299. width: 3,
  1300. },
  1301. });
  1302. lineEntity.__featureProperties = featureProperties;
  1303. lineEntity.__featureRef = featureRefInfo;
  1304. // 6. 将线实体添加到drawnEntities中
  1305. this.drawnEntities.push(lineEntity);
  1306. },
  1307. // 添加面到地图中
  1308. addPolygon(coordinates, featureProperties = null, featureRefInfo = null) {
  1309. // 检测是否为MultiPolygon类型(MultiPolygon的坐标是三维数组)
  1310. if (
  1311. Array.isArray(coordinates[0]) &&
  1312. Array.isArray(coordinates[0][0]) &&
  1313. Array.isArray(coordinates[0][0][0])
  1314. ) {
  1315. console.log("MultiPolygon coordinates", coordinates);
  1316. // 是MultiPolygon类型,遍历每个Polygon
  1317. coordinates.forEach((polygonCoordinates) => {
  1318. this.renderSinglePolygon(polygonCoordinates, featureProperties, featureRefInfo);
  1319. });
  1320. } else {
  1321. // 是单个Polygon类型
  1322. this.renderSinglePolygon(coordinates, featureProperties, featureRefInfo);
  1323. }
  1324. },
  1325. // 渲染单个Polygon
  1326. renderSinglePolygon(coordinates, featureProperties = null, featureRefInfo = null) {
  1327. // 1. 处理坐标格式:确保获取外部环坐标
  1328. const outerRingCoordinates =
  1329. Array.isArray(coordinates[0]) && Array.isArray(coordinates[0][0])
  1330. ? coordinates[0]
  1331. : coordinates;
  1332. // 2. 检测坐标是否包含Z值
  1333. const hasZValues = outerRingCoordinates.some(
  1334. (coord) => coord.length > 2 && typeof coord[2] === "number"
  1335. );
  1336. // 3. 根据是否有Z值选择合适的坐标转换方法
  1337. let positions;
  1338. let baseHeight = 0;
  1339. let extrudedHeight = 0; // 默认挤压高度
  1340. if (hasZValues) {
  1341. // 包含Z值,使用带高度的坐标转换方法
  1342. const flatCoordinatesWithHeights = [];
  1343. let minZ = Infinity;
  1344. let maxZ = -Infinity;
  1345. // 计算Z值的范围
  1346. outerRingCoordinates.forEach((coord) => {
  1347. const z = coord[2] || 0;
  1348. minZ = Math.min(minZ, z);
  1349. maxZ = Math.max(maxZ, z);
  1350. flatCoordinatesWithHeights.push(coord[0], coord[1], minZ); // 使用最小Z值作为基础高度
  1351. });
  1352. positions = SkyScenery.Cartesian3.fromDegreesArrayHeights(flatCoordinatesWithHeights);
  1353. baseHeight = minZ;
  1354. extrudedHeight = maxZ;
  1355. } else {
  1356. // 不包含Z值,使用普通坐标转换方法
  1357. const flatCoordinates = [];
  1358. outerRingCoordinates.forEach((coord) => {
  1359. flatCoordinates.push(coord[0], coord[1]);
  1360. });
  1361. positions = SkyScenery.Cartesian3.fromDegreesArray(flatCoordinates);
  1362. }
  1363. // 4. 创建面实体
  1364. const polygonEntity = viewer.entities.add({
  1365. name: "polygon",
  1366. polygon: {
  1367. hierarchy: {
  1368. positions: positions,
  1369. },
  1370. height: baseHeight, // 多边形底部高度
  1371. extrudedHeight: extrudedHeight, // 多边形顶部高度,实现3D挤压效果
  1372. heightReference: hasZValues
  1373. ? SkyScenery.HeightReference.NONE
  1374. : SkyScenery.HeightReference.CLAMP_TO_GROUND,
  1375. material: SkyScenery.Color.GREEN.withAlpha(0.5),
  1376. outline: true,
  1377. outlineColor: SkyScenery.Color.WHITE,
  1378. outlineWidth: 2,
  1379. // 设置侧面材质
  1380. extrudedMaterial: SkyScenery.Color.GREEN.withAlpha(0.8),
  1381. },
  1382. });
  1383. polygonEntity.__featureProperties = featureProperties;
  1384. polygonEntity.__featureRef = featureRefInfo;
  1385. // 5. 将面实体添加到drawnEntities中
  1386. this.drawnEntities.push(polygonEntity);
  1387. },
  1388. handleSelectChange(item, value) {
  1389. this.jsonData[item] = value;
  1390. },
  1391. // 复制json数据
  1392. copyJsonData(data) {
  1393. let textarea = document.createElement("textarea");
  1394. try {
  1395. textarea.value = JSON.stringify(data, null, 2);
  1396. document.body.appendChild(textarea);
  1397. // 3. 选中文本(兼容移动端)
  1398. textarea.select();
  1399. // 兼容移动端:设置选择范围覆盖全部文本
  1400. textarea.setSelectionRange(0, textarea.value.length);
  1401. // 4. 执行复制命令
  1402. const isSuccess = document.execCommand("copy");
  1403. if (isSuccess) {
  1404. this.$message({
  1405. message: "复制成功",
  1406. type: "success",
  1407. });
  1408. }
  1409. } catch (err) {
  1410. this.$message({
  1411. message: err,
  1412. type: "error",
  1413. });
  1414. } finally {
  1415. document.body.removeChild(textarea);
  1416. }
  1417. },
  1418. handleExceed(file) {
  1419. this.$message({
  1420. message: "最多只能上传一个文件",
  1421. type: "warning",
  1422. });
  1423. },
  1424. /** 时空格式转换等场景:上传文件仅允许 geojson / csv / xlsx / zip */
  1425. isAllowedSceneUploadFile(file) {
  1426. const name = (file && file.name) || (file.raw && file.raw.name) || "";
  1427. const lower = String(name).toLowerCase();
  1428. return (
  1429. lower.endsWith(".geojson") ||
  1430. lower.endsWith(".csv") ||
  1431. lower.endsWith(".xlsx") ||
  1432. lower.endsWith(".zip")
  1433. );
  1434. },
  1435. handleFileChange(file, fileList) {
  1436. if (!this.isAllowedSceneUploadFile(file)) {
  1437. this.$message.error("仅支持上传 .geojson、.csv、.xlsx、.zip 格式的文件,请重新选择");
  1438. this.currentFile = null;
  1439. this.fileList = [];
  1440. this.$nextTick(() => {
  1441. if (this.$refs.sceneFileUpload && this.$refs.sceneFileUpload.clearFiles) {
  1442. this.$refs.sceneFileUpload.clearFiles();
  1443. }
  1444. });
  1445. return;
  1446. }
  1447. this.currentFile = file;
  1448. this.refreshImportColumnOptions(file);
  1449. },
  1450. uploadRemove() {
  1451. this.currentFile = null;
  1452. this.params.geocodeColumnKeyword = "";
  1453. this.selectOptions.geocodeColumnKeyword = [];
  1454. },
  1455. parseCsvHeaderLine(line) {
  1456. const out = [];
  1457. let cur = "";
  1458. let inq = false;
  1459. for (let i = 0; i < line.length; i++) {
  1460. const c = line[i];
  1461. if (c === '"') {
  1462. inq = !inq;
  1463. continue;
  1464. }
  1465. if (!inq && c === ",") {
  1466. out.push(cur.trim());
  1467. cur = "";
  1468. continue;
  1469. }
  1470. cur += c;
  1471. }
  1472. out.push(cur.trim());
  1473. return out.filter((h) => h.length > 0);
  1474. },
  1475. async refreshImportColumnOptions(file) {
  1476. this.params.geocodeColumnKeyword = "";
  1477. this.selectOptions.geocodeColumnKeyword = [];
  1478. const raw = file && (file.raw || file);
  1479. if (!raw || !raw.name) {
  1480. return;
  1481. }
  1482. const name = String(raw.name).toLowerCase();
  1483. if (name.endsWith(".zip")) {
  1484. this.$message.info("zip 包请在列名输入框中填写与各 xlsx 表头一致的列名");
  1485. return;
  1486. }
  1487. try {
  1488. if (name.endsWith(".csv")) {
  1489. const text = await raw.text();
  1490. const firstLine = (text.split(/\r?\n/).find((l) => l.trim().length > 0) || "").trim();
  1491. if (!firstLine) {
  1492. return;
  1493. }
  1494. const headers = this.parseCsvHeaderLine(firstLine);
  1495. this.selectOptions.geocodeColumnKeyword = headers.map((h) => ({
  1496. value: h,
  1497. label: h,
  1498. }));
  1499. this.$message.success("已解析 CSV 表头,共 " + headers.length + " 列");
  1500. } else if (name.endsWith(".xlsx") || name.endsWith(".xls")) {
  1501. const buf = await raw.arrayBuffer();
  1502. const wb = XLSX.read(buf, { type: "array" });
  1503. const sheetName = wb.SheetNames[0];
  1504. const sheet = wb.Sheets[sheetName];
  1505. const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: "" });
  1506. const headers = (rows[0] || []).map((c) => String(c).trim()).filter((c) => c.length > 0);
  1507. this.selectOptions.geocodeColumnKeyword = headers.map((h) => ({
  1508. value: h,
  1509. label: h,
  1510. }));
  1511. this.$message.success("已解析 Excel 首行表头,共 " + headers.length + " 列");
  1512. }
  1513. } catch (e) {
  1514. console.error(e);
  1515. this.$message.warning("解析表头失败,可手动在「列名」中输入与表格一致的列名");
  1516. }
  1517. },
  1518. handleChange(emit) {
  1519. this.params.unit = "";
  1520. this.params.keyType = "";
  1521. this.params.geocodeColumnKeyword = "";
  1522. this.selectOptions.geocodeColumnKeyword = [];
  1523. this.fileList = [];
  1524. this.currentFile = null;
  1525. // 清除所有地图中的元素
  1526. this.clearAll();
  1527. this.jsonData = {};
  1528. this.backData = {};
  1529. this.renderStatus = {
  1530. input: false,
  1531. output: false,
  1532. };
  1533. this.renderedGeojsonCache = {
  1534. input: null,
  1535. output: null,
  1536. };
  1537. this.currentRenderedSource = "";
  1538. this.propertyDialog.visible = false;
  1539. this.propertyDialog.list = [];
  1540. this.propertyDialog.source = "";
  1541. this.propertyDialog.propertiesRef = null;
  1542. this.propertyDialog.featureIndex = -1;
  1543. this.SceneValue = emit[emit.length - 1];
  1544. },
  1545. // 初始化绘制处理器
  1546. initDrawHandler() {
  1547. // 创建绘制处理器
  1548. if (!this.handler) {
  1549. this.handler = new SkyScenery.ScreenSpaceEventHandler(viewer.canvas);
  1550. }
  1551. this.initFeaturePickHandler();
  1552. },
  1553. resetPickDragState() {
  1554. this._pickDrag = null;
  1555. },
  1556. /** @param {boolean} exitEditMode 为 false 时仅清除选中与高亮,用于在「节点编辑」内切换要素 */
  1557. clearMapEditSelection(exitEditMode = true) {
  1558. this.removeVertexHandleEntities();
  1559. const prevEntity = this.mapEditSelection && this.mapEditSelection.entity;
  1560. this.mapEditSelection = null;
  1561. this.hoveredVertexIndex = -1;
  1562. this.hoveredHandleEntity = null;
  1563. this.clearHoveredFeatureStyle();
  1564. if (exitEditMode) {
  1565. this.geometryEditMode = false;
  1566. this.pickedFeatureRef = null;
  1567. }
  1568. this.hideHoverHint();
  1569. this.restoreEntityHighlight(prevEntity);
  1570. this.setScreenSpaceCameraForDrag(true);
  1571. if (viewer) {
  1572. viewer.scene.requestRender();
  1573. }
  1574. },
  1575. removeVertexHandleEntities() {
  1576. if (!viewer || !this.vertexHandleEntities.length) {
  1577. this.vertexHandleEntities = [];
  1578. return;
  1579. }
  1580. this.vertexHandleEntities.forEach((e) => {
  1581. try {
  1582. viewer.entities.remove(e);
  1583. } catch (err) {
  1584. /* ignore */
  1585. }
  1586. });
  1587. this.vertexHandleEntities = [];
  1588. },
  1589. restoreEntityHighlight(entIn) {
  1590. const ent = entIn || (this.mapEditSelection && this.mapEditSelection.entity);
  1591. if (!ent || !ent.__editBackup || !viewer) {
  1592. return;
  1593. }
  1594. const b = ent.__editBackup;
  1595. if (ent.point && b.point) {
  1596. ent.point.pixelSize = b.point.pixelSize;
  1597. ent.point.color = b.point.color;
  1598. ent.point.outlineColor = b.point.outlineColor;
  1599. ent.point.outlineWidth = b.point.outlineWidth;
  1600. }
  1601. if (ent.polyline && b.polyline) {
  1602. ent.polyline.width = b.polyline.width;
  1603. ent.polyline.material = b.polyline.material;
  1604. }
  1605. if (ent.polygon && b.polygon) {
  1606. if (b.polygon.material != null) {
  1607. ent.polygon.material = b.polygon.material;
  1608. }
  1609. if (b.polygon.outlineWidth != null) {
  1610. ent.polygon.outlineWidth = b.polygon.outlineWidth;
  1611. }
  1612. if (b.polygon.outlineColor != null) {
  1613. ent.polygon.outlineColor = b.polygon.outlineColor;
  1614. }
  1615. }
  1616. delete ent.__editBackup;
  1617. },
  1618. applyEntityHighlight(entity, geometryType) {
  1619. if (!entity || !viewer) {
  1620. return;
  1621. }
  1622. if (!entity.__editBackup) {
  1623. entity.__editBackup = {};
  1624. if (entity.point) {
  1625. const p = entity.point;
  1626. entity.__editBackup.point = {
  1627. pixelSize: p.pixelSize,
  1628. color: p.color,
  1629. outlineColor: p.outlineColor,
  1630. outlineWidth: p.outlineWidth,
  1631. };
  1632. }
  1633. if (entity.polyline) {
  1634. const pl = entity.polyline;
  1635. entity.__editBackup.polyline = {
  1636. width: pl.width,
  1637. material: pl.material,
  1638. };
  1639. }
  1640. if (entity.polygon) {
  1641. const pg = entity.polygon;
  1642. entity.__editBackup.polygon = {
  1643. material: pg.material,
  1644. outlineWidth: pg.outlineWidth,
  1645. outlineColor: pg.outlineColor,
  1646. };
  1647. }
  1648. }
  1649. const C = SkyScenery.Color;
  1650. if (geometryType === "Point" && entity.point) {
  1651. entity.point.pixelSize = 16;
  1652. entity.point.color = C.YELLOW.withAlpha(0.95);
  1653. entity.point.outlineColor = C.DEEPSKYBLUE;
  1654. entity.point.outlineWidth = 3;
  1655. } else if (geometryType === "LineString" && entity.polyline) {
  1656. entity.polyline.width = 6;
  1657. entity.polyline.material = C.CYAN.withAlpha(0.95);
  1658. } else if (geometryType === "Polygon" && entity.polygon) {
  1659. entity.polygon.outlineWidth = 4;
  1660. entity.polygon.outlineColor = C.CYAN;
  1661. entity.polygon.material = C.LIME.withAlpha(0.45);
  1662. }
  1663. },
  1664. getVertexHandleStyle(mode, handleType = "vertex") {
  1665. const C = SkyScenery.Color;
  1666. if (handleType === "midpoint") {
  1667. if (mode === "pressed") {
  1668. return {
  1669. pixelSize: 12,
  1670. color: C.ORANGE,
  1671. outlineColor: C.WHITE,
  1672. outlineWidth: 3,
  1673. };
  1674. }
  1675. if (mode === "hover") {
  1676. return {
  1677. pixelSize: 10,
  1678. color: C.YELLOW,
  1679. outlineColor: C.DARKBLUE,
  1680. outlineWidth: 2,
  1681. };
  1682. }
  1683. return {
  1684. pixelSize: 7,
  1685. color: C.RED.withAlpha(0.9),
  1686. outlineColor: C.WHITE,
  1687. outlineWidth: 1,
  1688. };
  1689. }
  1690. if (mode === "pressed") {
  1691. return {
  1692. pixelSize: 15,
  1693. color: C.ORANGE,
  1694. outlineColor: C.WHITE,
  1695. outlineWidth: 3,
  1696. };
  1697. }
  1698. if (mode === "hover") {
  1699. return {
  1700. pixelSize: 12,
  1701. color: C.YELLOW,
  1702. outlineColor: C.DARKBLUE,
  1703. outlineWidth: 2,
  1704. };
  1705. }
  1706. return {
  1707. pixelSize: 9,
  1708. color: C.WHITE,
  1709. outlineColor: C.DEEPSKYBLUE,
  1710. outlineWidth: 2,
  1711. };
  1712. },
  1713. applyVertexHandleStyles() {
  1714. if (!this.vertexHandleEntities.length) {
  1715. return;
  1716. }
  1717. const dragging = this._pickDrag && this._pickDrag.dragging;
  1718. const dragHandle = dragging ? this._pickDrag.dragHandleEntity || null : null;
  1719. this.vertexHandleEntities.forEach((h) => {
  1720. if (!h || !h.point) {
  1721. return;
  1722. }
  1723. let mode = "default";
  1724. if (dragging && dragHandle && h === dragHandle) {
  1725. mode = "pressed";
  1726. } else if (this.hoveredHandleEntity && h === this.hoveredHandleEntity) {
  1727. mode = "hover";
  1728. }
  1729. const s = this.getVertexHandleStyle(mode, h.__handleType || "vertex");
  1730. h.point.pixelSize = s.pixelSize;
  1731. h.point.color = s.color;
  1732. h.point.outlineColor = s.outlineColor;
  1733. h.point.outlineWidth = s.outlineWidth;
  1734. });
  1735. },
  1736. applyPointSelectionVisualByHoverPress() {
  1737. const sel = this.mapEditSelection;
  1738. if (!sel || sel.geometryType !== "Point" || !sel.entity || !sel.entity.point) {
  1739. return;
  1740. }
  1741. const ent = sel.entity;
  1742. const dragging = this._pickDrag && this._pickDrag.dragging && this._pickDrag.geometryType === "Point";
  1743. const C = SkyScenery.Color;
  1744. if (dragging) {
  1745. ent.point.pixelSize = 18;
  1746. ent.point.color = C.ORANGE.withAlpha(0.98);
  1747. ent.point.outlineColor = C.WHITE;
  1748. ent.point.outlineWidth = 4;
  1749. } else if (this.hoveredVertexIndex === 0) {
  1750. ent.point.pixelSize = 17;
  1751. ent.point.color = C.LIME.withAlpha(0.95);
  1752. ent.point.outlineColor = C.DEEPSKYBLUE;
  1753. ent.point.outlineWidth = 3;
  1754. } else {
  1755. this.applyEntityHighlight(ent, "Point");
  1756. }
  1757. },
  1758. refreshVertexHandlesFromPositions(positions) {
  1759. if (!positions || !this.vertexHandleEntities.length) {
  1760. return;
  1761. }
  1762. this.vertexHandleEntities.forEach((h) => {
  1763. const i = h.__vertexIndex;
  1764. if (h.__handleType === "vertex" && i >= 0 && i < positions.length) {
  1765. h.position = SkyScenery.Cartesian3.clone(positions[i], new SkyScenery.Cartesian3());
  1766. } else if (h.__handleType === "midpoint") {
  1767. const a = h.__insertAfter;
  1768. const b = a + 1;
  1769. if (a >= 0 && b < positions.length) {
  1770. h.position = SkyScenery.Cartesian3.midpoint(
  1771. positions[a],
  1772. positions[b],
  1773. new SkyScenery.Cartesian3()
  1774. );
  1775. }
  1776. }
  1777. });
  1778. if (viewer) {
  1779. viewer.scene.requestRender();
  1780. }
  1781. },
  1782. hideHoverHint() {
  1783. this.hoverHint.visible = false;
  1784. this.hoverHint.text = "";
  1785. },
  1786. clearHoveredFeatureStyle() {
  1787. const ent = this.hoveredFeatureEntity;
  1788. if (!ent || !ent.__hoverBackup) {
  1789. this.hoveredFeatureEntity = null;
  1790. return;
  1791. }
  1792. const b = ent.__hoverBackup;
  1793. if (ent.point && b.point) {
  1794. ent.point.pixelSize = b.point.pixelSize;
  1795. ent.point.color = b.point.color;
  1796. ent.point.outlineColor = b.point.outlineColor;
  1797. }
  1798. if (ent.polyline && b.polyline) {
  1799. ent.polyline.width = b.polyline.width;
  1800. ent.polyline.material = b.polyline.material;
  1801. }
  1802. if (ent.polygon && b.polygon) {
  1803. ent.polygon.outlineWidth = b.polygon.outlineWidth;
  1804. ent.polygon.outlineColor = b.polygon.outlineColor;
  1805. }
  1806. delete ent.__hoverBackup;
  1807. this.hoveredFeatureEntity = null;
  1808. },
  1809. applyHoveredFeatureStyle(entity) {
  1810. if (!entity || (this.mapEditSelection && this.mapEditSelection.entity === entity)) {
  1811. this.clearHoveredFeatureStyle();
  1812. return;
  1813. }
  1814. if (this.hoveredFeatureEntity === entity) {
  1815. return;
  1816. }
  1817. this.clearHoveredFeatureStyle();
  1818. entity.__hoverBackup = {};
  1819. const C = SkyScenery.Color;
  1820. if (entity.point) {
  1821. entity.__hoverBackup.point = {
  1822. pixelSize: entity.point.pixelSize,
  1823. color: entity.point.color,
  1824. outlineColor: entity.point.outlineColor,
  1825. };
  1826. entity.point.pixelSize = 13;
  1827. entity.point.color = C.CYAN.withAlpha(0.95);
  1828. entity.point.outlineColor = C.WHITE;
  1829. } else if (entity.polyline) {
  1830. entity.__hoverBackup.polyline = {
  1831. width: entity.polyline.width,
  1832. material: entity.polyline.material,
  1833. };
  1834. entity.polyline.width = 5;
  1835. entity.polyline.material = C.CYAN.withAlpha(0.92);
  1836. } else if (entity.polygon) {
  1837. entity.__hoverBackup.polygon = {
  1838. outlineWidth: entity.polygon.outlineWidth,
  1839. outlineColor: entity.polygon.outlineColor,
  1840. };
  1841. entity.polygon.outlineWidth = 3;
  1842. entity.polygon.outlineColor = C.CYAN;
  1843. }
  1844. this.hoveredFeatureEntity = entity;
  1845. },
  1846. findNearestHandleByScreen(screenPosition, maxDistPx = 18) {
  1847. if (!this.vertexHandleEntities || !this.vertexHandleEntities.length) {
  1848. return null;
  1849. }
  1850. let best = null;
  1851. let bestD = maxDistPx;
  1852. this.vertexHandleEntities.forEach((h) => {
  1853. if (!h || !h.position) return;
  1854. const p = h.position.getValue ? h.position.getValue(viewer.clock.currentTime) : h.position;
  1855. const d = this.screenDistanceToCartesian(screenPosition, p);
  1856. if (d < bestD) {
  1857. bestD = d;
  1858. best = h;
  1859. }
  1860. });
  1861. return best;
  1862. },
  1863. showHoverHint(screenPosition, text) {
  1864. if (!screenPosition || !text) {
  1865. this.hideHoverHint();
  1866. return;
  1867. }
  1868. this.hoverHint.visible = true;
  1869. this.hoverHint.text = text;
  1870. this.hoverHint.left = screenPosition.x + 14;
  1871. this.hoverHint.top = screenPosition.y + 16;
  1872. },
  1873. createVertexHandlesForSelection(entity, geometryType, positionsArray) {
  1874. this.removeVertexHandleEntities();
  1875. if (!viewer || !entity || !positionsArray || !positionsArray.length) {
  1876. return;
  1877. }
  1878. const featureRef = entity.__featureRef;
  1879. const C = SkyScenery.Color;
  1880. let indices = positionsArray.map((_, i) => i);
  1881. const n = positionsArray.length;
  1882. if (
  1883. geometryType === "Polygon" &&
  1884. n > 2 &&
  1885. SkyScenery.Cartesian3.distance(positionsArray[0], positionsArray[n - 1]) < 0.5
  1886. ) {
  1887. indices = indices.slice(0, -1);
  1888. }
  1889. indices.forEach((vertexIndex) => {
  1890. const pos = positionsArray[vertexIndex];
  1891. const h = viewer.entities.add({
  1892. position: SkyScenery.Cartesian3.clone(pos, new SkyScenery.Cartesian3()),
  1893. point: {
  1894. show: true,
  1895. color: C.WHITE,
  1896. pixelSize: 9,
  1897. outlineColor: C.DEEPSKYBLUE,
  1898. outlineWidth: 2,
  1899. disableDepthTestDistance: Number.POSITIVE_INFINITY,
  1900. },
  1901. });
  1902. h.__vertexHandle = true;
  1903. h.__handleType = "vertex";
  1904. h.__vertexIndex = vertexIndex;
  1905. h.__parentEntity = entity;
  1906. h.__featureRef = featureRef;
  1907. this.vertexHandleEntities.push(h);
  1908. });
  1909. // 在相邻顶点中间增加可插入顶点的中点柄
  1910. const segCount = geometryType === "Polygon" ? indices.length : Math.max(0, indices.length - 1);
  1911. for (let i = 0; i < segCount; i++) {
  1912. const aIdx = indices[i];
  1913. const bIdx = geometryType === "Polygon" ? indices[(i + 1) % indices.length] : indices[i + 1];
  1914. if (aIdx == null || bIdx == null) continue;
  1915. const a = positionsArray[aIdx];
  1916. const b = positionsArray[bIdx];
  1917. if (!a || !b) continue;
  1918. const mid = SkyScenery.Cartesian3.midpoint(a, b, new SkyScenery.Cartesian3());
  1919. const m = viewer.entities.add({
  1920. position: mid,
  1921. point: {
  1922. show: true,
  1923. color: C.RED.withAlpha(0.9),
  1924. pixelSize: 7,
  1925. outlineColor: C.WHITE,
  1926. outlineWidth: 1,
  1927. disableDepthTestDistance: Number.POSITIVE_INFINITY,
  1928. },
  1929. });
  1930. m.__vertexHandle = true;
  1931. m.__handleType = "midpoint";
  1932. m.__insertAfter = aIdx;
  1933. m.__vertexIndex = -1;
  1934. m.__parentEntity = entity;
  1935. m.__featureRef = featureRef;
  1936. this.vertexHandleEntities.push(m);
  1937. }
  1938. this.applyVertexHandleStyles();
  1939. },
  1940. applyMapEditSelection(entity) {
  1941. if (!this.geometryEditMode) {
  1942. return;
  1943. }
  1944. if (!entity || !viewer || !entity.__featureRef) {
  1945. return;
  1946. }
  1947. const g =
  1948. this.getSourceGeojsonData(entity.__featureRef.source || "input") ||
  1949. this.renderedGeojsonCache[entity.__featureRef.source];
  1950. const fi = entity.__featureRef.featureIndex;
  1951. if (!g || !g.features || fi < 0 || !g.features[fi] || !g.features[fi].geometry) {
  1952. return;
  1953. }
  1954. const geom = g.features[fi].geometry;
  1955. const gt = geom.type === "MultiPolygon" ? null : geom.type;
  1956. if (!gt || gt === "MultiPolygon") {
  1957. return;
  1958. }
  1959. if (this.mapEditSelection && this.mapEditSelection.entity === entity) {
  1960. const now = viewer.clock.currentTime;
  1961. const gType = this.mapEditSelection.geometryType;
  1962. this.removeVertexHandleEntities();
  1963. if (gType === "LineString" && entity.polyline && entity.polyline.positions) {
  1964. const arr = entity.polyline.positions.getValue(now);
  1965. if (arr && arr.length) {
  1966. this.createVertexHandlesForSelection(entity, "LineString", arr);
  1967. }
  1968. } else if (gType === "Polygon" && entity.polygon && entity.polygon.hierarchy) {
  1969. const hProp = entity.polygon.hierarchy;
  1970. const hier =
  1971. hProp && typeof hProp.getValue === "function" ? hProp.getValue(now) : hProp;
  1972. const arr = hier && hier.positions;
  1973. if (arr && arr.length) {
  1974. this.createVertexHandlesForSelection(entity, "Polygon", arr);
  1975. }
  1976. }
  1977. this.applyVertexHandleStyles();
  1978. this.applyPointSelectionVisualByHoverPress();
  1979. return;
  1980. }
  1981. this.clearMapEditSelection(false);
  1982. this.mapEditSelection = {
  1983. entity,
  1984. geometryType: gt,
  1985. source: entity.__featureRef.source || "input",
  1986. featureIndex: fi,
  1987. };
  1988. this.applyEntityHighlight(entity, gt === "LineString" ? "LineString" : gt === "Polygon" ? "Polygon" : "Point");
  1989. const now = viewer.clock.currentTime;
  1990. if (gt === "LineString" && entity.polyline && entity.polyline.positions) {
  1991. const arr = entity.polyline.positions.getValue(now);
  1992. if (arr && arr.length) {
  1993. this.createVertexHandlesForSelection(entity, "LineString", arr);
  1994. }
  1995. } else if (gt === "Polygon" && entity.polygon && entity.polygon.hierarchy) {
  1996. const hProp = entity.polygon.hierarchy;
  1997. const hier = hProp && typeof hProp.getValue === "function" ? hProp.getValue(now) : hProp;
  1998. const arr = hier && hier.positions;
  1999. if (arr && arr.length) {
  2000. this.createVertexHandlesForSelection(entity, "Polygon", arr);
  2001. }
  2002. }
  2003. this.hoveredVertexIndex = -1;
  2004. this.applyVertexHandleStyles();
  2005. this.applyPointSelectionVisualByHoverPress();
  2006. viewer.scene.requestRender();
  2007. },
  2008. updateVertexHoverFromPick(screenPosition, precomputedNearHandle = null) {
  2009. if (!this.mapEditSelection || !this.mapEditSelection.entity) {
  2010. this.hoveredVertexIndex = -1;
  2011. this.hoveredHandleEntity = null;
  2012. this.hideHoverHint();
  2013. return;
  2014. }
  2015. if (this._pickDrag && this._pickDrag.dragging) {
  2016. this.hoveredVertexIndex = this._pickDrag.vertexIndex;
  2017. this.hoveredHandleEntity = this._pickDrag.dragHandleEntity || null;
  2018. this.applyVertexHandleStyles();
  2019. this.applyPointSelectionVisualByHoverPress();
  2020. this.showHoverHint(screenPosition, "可拖拽调整节点位置");
  2021. return;
  2022. }
  2023. // 与按下逻辑一致:优先按屏幕距离命中顶点/中点柄。纯 pick 常会先拾取到面/线填充而非小点实体。
  2024. let e =
  2025. precomputedNearHandle ||
  2026. (this.vertexHandleEntities && this.vertexHandleEntities.length
  2027. ? this.findNearestHandleByScreen(screenPosition, 32)
  2028. : null);
  2029. if (!e) {
  2030. const pickedObject = viewer.scene.pick(screenPosition);
  2031. e = pickedObject && pickedObject.id && pickedObject.id.__vertexHandle ? pickedObject.id : null;
  2032. }
  2033. if (e && e.__vertexHandle) {
  2034. this.hoveredHandleEntity = e;
  2035. this.hoveredVertexIndex = typeof e.__vertexIndex === "number" ? e.__vertexIndex : -1;
  2036. if (e.__handleType === "midpoint") {
  2037. this.showHoverHint(screenPosition, "拖动可新增节点");
  2038. } else {
  2039. this.showHoverHint(screenPosition, "可拖拽调整节点位置");
  2040. }
  2041. } else if (this.mapEditSelection.geometryType === "Point") {
  2042. const pickedPoint = viewer.scene.pick(screenPosition);
  2043. const pe = pickedPoint && pickedPoint.id;
  2044. if (pe === this.mapEditSelection.entity) {
  2045. this.hoveredVertexIndex = 0;
  2046. this.hoveredHandleEntity = null;
  2047. this.showHoverHint(screenPosition, "可拖拽调整节点位置");
  2048. } else {
  2049. this.hoveredVertexIndex = -1;
  2050. this.hoveredHandleEntity = null;
  2051. this.hideHoverHint();
  2052. }
  2053. } else {
  2054. this.hoveredVertexIndex = -1;
  2055. this.hoveredHandleEntity = null;
  2056. this.hideHoverHint();
  2057. }
  2058. this.applyVertexHandleStyles();
  2059. this.applyPointSelectionVisualByHoverPress();
  2060. if (viewer) {
  2061. viewer.scene.requestRender();
  2062. }
  2063. },
  2064. setScreenSpaceCameraForDrag(enable) {
  2065. if (!viewer || !viewer.scene || !viewer.scene.screenSpaceCameraController) {
  2066. return;
  2067. }
  2068. const c = viewer.scene.screenSpaceCameraController;
  2069. c.enableInputs = enable;
  2070. c.enableRotate = enable;
  2071. c.enableTranslate = enable;
  2072. c.enableZoom = enable;
  2073. c.enableTilt = enable;
  2074. c.enableLook = enable;
  2075. },
  2076. /** 拖动顶点/线/面或折线面绘制时启用连续渲染,结束后恢复 Example 中的 requestRenderMode */
  2077. enterInteractiveSceneRender() {
  2078. if (!viewer || !viewer.scene) {
  2079. return;
  2080. }
  2081. if (this._interactiveRenderDepth === 0) {
  2082. this._savedSceneRenderMode = {
  2083. requestRenderMode: viewer.scene.requestRenderMode,
  2084. maximumRenderTimeChange: viewer.scene.maximumRenderTimeChange,
  2085. };
  2086. viewer.scene.requestRenderMode = false;
  2087. viewer.scene.maximumRenderTimeChange = 0;
  2088. }
  2089. this._interactiveRenderDepth++;
  2090. },
  2091. exitInteractiveSceneRender() {
  2092. if (!viewer || !viewer.scene || this._interactiveRenderDepth <= 0) {
  2093. return;
  2094. }
  2095. this._interactiveRenderDepth--;
  2096. if (this._interactiveRenderDepth === 0 && this._savedSceneRenderMode) {
  2097. viewer.scene.requestRenderMode = this._savedSceneRenderMode.requestRenderMode;
  2098. viewer.scene.maximumRenderTimeChange = this._savedSceneRenderMode.maximumRenderTimeChange;
  2099. this._savedSceneRenderMode = null;
  2100. }
  2101. },
  2102. /** globe.pick 在快速移动时易失败;补充 pickPosition / 椭球拾取,保证几何与柄同步 */
  2103. pickSurfaceCartesian(screenPosition) {
  2104. if (!viewer || !viewer.scene || !viewer.camera || !screenPosition) {
  2105. return null;
  2106. }
  2107. const ray = viewer.camera.getPickRay(screenPosition);
  2108. if (!ray) {
  2109. return null;
  2110. }
  2111. let cart = viewer.scene.globe.pick(ray, viewer.scene);
  2112. if (cart) {
  2113. return cart;
  2114. }
  2115. if (typeof viewer.scene.pickPosition === "function") {
  2116. cart = viewer.scene.pickPosition(screenPosition);
  2117. if (cart) {
  2118. return cart;
  2119. }
  2120. }
  2121. const ellipsoid = viewer.scene.globe && viewer.scene.globe.ellipsoid;
  2122. if (ellipsoid && typeof viewer.camera.pickEllipsoid === "function") {
  2123. return viewer.camera.pickEllipsoid(screenPosition, ellipsoid);
  2124. }
  2125. return null;
  2126. },
  2127. wgs84PointToScreen(cartesian) {
  2128. if (!viewer || !cartesian) {
  2129. return null;
  2130. }
  2131. const ST = SkyScenery.SceneTransforms;
  2132. if (!ST || typeof ST.wgs84ToWindowCoordinates !== "function") {
  2133. return null;
  2134. }
  2135. const win = new SkyScenery.Cartesian2();
  2136. if (!ST.wgs84ToWindowCoordinates(viewer.scene, cartesian, win)) {
  2137. return null;
  2138. }
  2139. return win;
  2140. },
  2141. screenDistanceToCartesian(screenPos, cartesian) {
  2142. const win = this.wgs84PointToScreen(cartesian);
  2143. if (!win) {
  2144. return Infinity;
  2145. }
  2146. const dx = win.x - screenPos.x;
  2147. const dy = win.y - screenPos.y;
  2148. return Math.sqrt(dx * dx + dy * dy);
  2149. },
  2150. findClosestVertexIndexScreen(positions, screenPos, maxDistPx) {
  2151. if (!positions || !positions.length) {
  2152. return -1;
  2153. }
  2154. let best = -1;
  2155. let bestD = maxDistPx;
  2156. for (let i = 0; i < positions.length; i++) {
  2157. const d = this.screenDistanceToCartesian(screenPos, positions[i]);
  2158. if (d < bestD) {
  2159. bestD = d;
  2160. best = i;
  2161. }
  2162. }
  2163. return best;
  2164. },
  2165. cloneCartesian3Array(arr) {
  2166. return arr.map((p) => SkyScenery.Cartesian3.clone(p, new SkyScenery.Cartesian3()));
  2167. },
  2168. openFeaturePropertyDialogForEntity(entity) {
  2169. const featureIndex =
  2170. entity.__featureRef && typeof entity.__featureRef.featureIndex === "number"
  2171. ? entity.__featureRef.featureIndex
  2172. : -1;
  2173. const propertySource =
  2174. (entity.__featureRef && entity.__featureRef.source) || this.currentRenderedSource || "input";
  2175. const geojsonData =
  2176. this.getSourceGeojsonData(propertySource) || this.renderedGeojsonCache[propertySource];
  2177. let propertiesRef = null;
  2178. if (geojsonData) {
  2179. if (
  2180. entity.__featureRef &&
  2181. typeof entity.__featureRef.featureIndex === "number" &&
  2182. entity.__featureRef.featureIndex >= 0 &&
  2183. geojsonData.features &&
  2184. geojsonData.features[entity.__featureRef.featureIndex]
  2185. ) {
  2186. if (!geojsonData.features[entity.__featureRef.featureIndex].properties) {
  2187. geojsonData.features[entity.__featureRef.featureIndex].properties = {};
  2188. }
  2189. propertiesRef = geojsonData.features[entity.__featureRef.featureIndex].properties;
  2190. } else {
  2191. if (!geojsonData.properties) {
  2192. geojsonData.properties = {};
  2193. }
  2194. propertiesRef = geojsonData.properties;
  2195. }
  2196. }
  2197. const propertyList = this.formatFeatureProperties(
  2198. propertiesRef || entity.__featureProperties || {}
  2199. );
  2200. if (
  2201. propertiesRef ||
  2202. (entity.__featureProperties && typeof entity.__featureProperties === "object")
  2203. ) {
  2204. this.pickedFeatureRef = {
  2205. entity,
  2206. source: propertySource,
  2207. featureIndex,
  2208. };
  2209. this.propertyDialog.source = propertySource;
  2210. this.propertyDialog.propertiesRef = propertiesRef || entity.__featureProperties || {};
  2211. this.propertyDialog.featureIndex = featureIndex;
  2212. this.propertyDialog.list = propertyList;
  2213. this.propertyDialog.visible = true;
  2214. } else {
  2215. this.pickedFeatureRef = {
  2216. entity,
  2217. source: propertySource,
  2218. featureIndex,
  2219. };
  2220. this.propertyDialog.visible = false;
  2221. this.propertyDialog.list = [];
  2222. this.propertyDialog.source = "";
  2223. this.propertyDialog.propertiesRef = null;
  2224. this.propertyDialog.featureIndex = -1;
  2225. }
  2226. },
  2227. commitDraggedGeometryToGeojson(st) {
  2228. if (!st || !st.entity || st.featureIndex < 0) {
  2229. return;
  2230. }
  2231. const g = this.getSourceGeojsonData(st.source);
  2232. if (!g || !g.features || !g.features[st.featureIndex] || !g.features[st.featureIndex].geometry) {
  2233. return;
  2234. }
  2235. const geom = g.features[st.featureIndex].geometry;
  2236. const now = viewer.clock.currentTime;
  2237. const toLonLat = (c3, origCoord) => {
  2238. const c = SkyScenery.Cartographic.fromCartesian(c3);
  2239. const lon = SkyScenery.Math.toDegrees(c.longitude);
  2240. const lat = SkyScenery.Math.toDegrees(c.latitude);
  2241. if (origCoord && origCoord.length > 2 && typeof origCoord[2] === "number") {
  2242. return [lon, lat, origCoord[2]];
  2243. }
  2244. return [lon, lat];
  2245. };
  2246. if (geom.type === "Point" && st.entity.position) {
  2247. const p = st.entity.position.getValue(now);
  2248. if (!p) {
  2249. return;
  2250. }
  2251. const orig = geom.coordinates;
  2252. geom.coordinates = toLonLat(p, orig);
  2253. return;
  2254. }
  2255. if (geom.type === "LineString" && st.workingPositions) {
  2256. geom.coordinates = st.workingPositions.map((c3, i) => toLonLat(c3, geom.coordinates[i]));
  2257. return;
  2258. }
  2259. if (geom.type === "Polygon" && st.workingPositions) {
  2260. const ring = st.workingPositions.map((c3, i) => {
  2261. const origRing = (geom.coordinates && geom.coordinates[0]) || [];
  2262. return toLonLat(c3, origRing[i]);
  2263. });
  2264. if (ring.length > 1) {
  2265. const a = ring[0];
  2266. const b = ring[ring.length - 1];
  2267. if (a[0] !== b[0] || a[1] !== b[1] || (a[2] !== undefined && b[2] !== undefined && a[2] !== b[2])) {
  2268. ring.push([...a]);
  2269. }
  2270. }
  2271. if (!geom.coordinates) {
  2272. geom.coordinates = [];
  2273. }
  2274. geom.coordinates[0] = ring;
  2275. }
  2276. },
  2277. syncPolygonEntityHierarchy(entity, positions) {
  2278. if (!entity || !entity.polygon) {
  2279. return;
  2280. }
  2281. const n = positions.length;
  2282. if (n > 1) {
  2283. const a = positions[0];
  2284. const b = positions[n - 1];
  2285. if (SkyScenery.Cartesian3.distance(a, b) < 0.5) {
  2286. positions[n - 1] = SkyScenery.Cartesian3.clone(a, new SkyScenery.Cartesian3());
  2287. }
  2288. }
  2289. entity.polygon.hierarchy = new SkyScenery.PolygonHierarchy(this.cloneCartesian3Array(positions));
  2290. },
  2291. tryBeginPickDrag(event) {
  2292. if (this.currentTool || this.isDrawingHole) {
  2293. return;
  2294. }
  2295. const nearHandle = this.geometryEditMode ? this.findNearestHandleByScreen(event.position, 32) : null;
  2296. const pickedObject = nearHandle ? { id: nearHandle } : viewer.scene.pick(event.position);
  2297. if (!SkyScenery.defined(pickedObject) || !SkyScenery.defined(pickedObject.id)) {
  2298. this.propertyDialog.visible = false;
  2299. this.propertyDialog.list = [];
  2300. this.propertyDialog.source = "";
  2301. this.propertyDialog.propertiesRef = null;
  2302. this.propertyDialog.featureIndex = -1;
  2303. this.resetPickDragState();
  2304. this.clearMapEditSelection();
  2305. return;
  2306. }
  2307. const entity = pickedObject.id;
  2308. if (!this.geometryEditMode) {
  2309. if (!entity.__featureRef || typeof entity.__featureRef.featureIndex !== "number") {
  2310. this.resetPickDragState();
  2311. return;
  2312. }
  2313. this._pickDrag = {
  2314. entity,
  2315. source: entity.__featureRef.source || this.currentRenderedSource || "input",
  2316. featureIndex: entity.__featureRef.featureIndex,
  2317. selectOnly: true,
  2318. noDrag: true,
  2319. startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
  2320. dragging: false,
  2321. };
  2322. return;
  2323. }
  2324. if (
  2325. (entity && entity.__vertexHandle && entity.__parentEntity) ||
  2326. (nearHandle && nearHandle.__vertexHandle && nearHandle.__parentEntity)
  2327. ) {
  2328. const handleEntity = nearHandle || entity;
  2329. const parent = handleEntity.__parentEntity;
  2330. if (!parent.__featureRef || typeof parent.__featureRef.featureIndex !== "number") {
  2331. this.resetPickDragState();
  2332. return;
  2333. }
  2334. const featureIndex = parent.__featureRef.featureIndex;
  2335. const source = parent.__featureRef.source || this.currentRenderedSource || "input";
  2336. const g = this.getSourceGeojsonData(source) || this.renderedGeojsonCache[source];
  2337. if (!g || !g.features || featureIndex < 0 || !g.features[featureIndex]) {
  2338. this.resetPickDragState();
  2339. return;
  2340. }
  2341. const geom = g.features[featureIndex].geometry;
  2342. if (!geom || geom.type === "MultiPolygon") {
  2343. this.resetPickDragState();
  2344. return;
  2345. }
  2346. // 避免每次点柄都 remove/recreate 顶点实体,防止与当前按下目标不同步、pick/hover 失效
  2347. if (!this.mapEditSelection || this.mapEditSelection.entity !== parent) {
  2348. this.applyMapEditSelection(parent);
  2349. }
  2350. const now = viewer.clock.currentTime;
  2351. const vi = handleEntity.__vertexIndex;
  2352. if (geom.type === "LineString" && parent.polyline && parent.polyline.positions) {
  2353. const positions = parent.polyline.positions.getValue(now);
  2354. if (!positions || !positions.length) {
  2355. this.resetPickDragState();
  2356. return;
  2357. }
  2358. let working = this.cloneCartesian3Array(positions);
  2359. let dragIndex = vi;
  2360. if (handleEntity.__handleType === "midpoint") {
  2361. const after = handleEntity.__insertAfter;
  2362. const insertAt = after + 1;
  2363. const midPos =
  2364. handleEntity.position && handleEntity.position.getValue
  2365. ? handleEntity.position.getValue(now)
  2366. : null;
  2367. if (after < 0 || insertAt > working.length || !midPos) {
  2368. this.resetPickDragState();
  2369. return;
  2370. }
  2371. working.splice(insertAt, 0, SkyScenery.Cartesian3.clone(midPos, new SkyScenery.Cartesian3()));
  2372. parent.polyline.positions = this.cloneCartesian3Array(working);
  2373. this.refreshVertexHandlesFromPositions(working);
  2374. dragIndex = insertAt;
  2375. }
  2376. if (dragIndex < 0 || dragIndex >= working.length) {
  2377. this.resetPickDragState();
  2378. return;
  2379. }
  2380. this._pickDrag = {
  2381. entity: parent,
  2382. source,
  2383. featureIndex,
  2384. geometryType: "LineString",
  2385. vertexIndex: dragIndex,
  2386. dragHandleEntity: handleEntity,
  2387. workingPositions: working,
  2388. startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
  2389. cameraLocked: true,
  2390. dragging: false,
  2391. };
  2392. this.setScreenSpaceCameraForDrag(false);
  2393. this.applyVertexHandleStyles();
  2394. this.applyPointSelectionVisualByHoverPress();
  2395. return;
  2396. }
  2397. if (geom.type === "Polygon" && parent.polygon && parent.polygon.hierarchy) {
  2398. const hProp = parent.polygon.hierarchy;
  2399. const hier =
  2400. hProp && typeof hProp.getValue === "function" ? hProp.getValue(now) : hProp;
  2401. const positions = hier && hier.positions;
  2402. if (!positions || !positions.length) {
  2403. this.resetPickDragState();
  2404. return;
  2405. }
  2406. let working = this.cloneCartesian3Array(positions);
  2407. let dragIndex = vi;
  2408. if (handleEntity.__handleType === "midpoint") {
  2409. const after = handleEntity.__insertAfter;
  2410. const insertAt = after + 1;
  2411. const midPos =
  2412. handleEntity.position && handleEntity.position.getValue
  2413. ? handleEntity.position.getValue(now)
  2414. : null;
  2415. // insertAt === length 表示在末尾追加(闭合边上 v_last→v0 之间插点);用 >= 会误拒开环多边形
  2416. if (after < 0 || insertAt > working.length || !midPos) {
  2417. this.resetPickDragState();
  2418. return;
  2419. }
  2420. working.splice(insertAt, 0, SkyScenery.Cartesian3.clone(midPos, new SkyScenery.Cartesian3()));
  2421. this.syncPolygonEntityHierarchy(parent, working);
  2422. this.refreshVertexHandlesFromPositions(working);
  2423. dragIndex = insertAt;
  2424. }
  2425. if (dragIndex < 0 || dragIndex >= working.length) {
  2426. this.resetPickDragState();
  2427. return;
  2428. }
  2429. this._pickDrag = {
  2430. entity: parent,
  2431. source,
  2432. featureIndex,
  2433. geometryType: "Polygon",
  2434. vertexIndex: dragIndex,
  2435. dragHandleEntity: handleEntity,
  2436. workingPositions: working,
  2437. startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
  2438. cameraLocked: true,
  2439. dragging: false,
  2440. };
  2441. this.setScreenSpaceCameraForDrag(false);
  2442. this.applyVertexHandleStyles();
  2443. this.applyPointSelectionVisualByHoverPress();
  2444. return;
  2445. }
  2446. this.resetPickDragState();
  2447. return;
  2448. }
  2449. if (!entity.__featureRef || typeof entity.__featureRef.featureIndex !== "number") {
  2450. this.resetPickDragState();
  2451. return;
  2452. }
  2453. const featureIndex = entity.__featureRef.featureIndex;
  2454. const source = entity.__featureRef.source || this.currentRenderedSource || "input";
  2455. const g = this.getSourceGeojsonData(source) || this.renderedGeojsonCache[source];
  2456. if (!g || !g.features || featureIndex < 0 || !g.features[featureIndex]) {
  2457. this.resetPickDragState();
  2458. return;
  2459. }
  2460. const geom = g.features[featureIndex].geometry;
  2461. if (!geom || geom.type === "MultiPolygon") {
  2462. this.resetPickDragState();
  2463. return;
  2464. }
  2465. const now = viewer.clock.currentTime;
  2466. const maxPx = 28;
  2467. if (entity.point && entity.position) {
  2468. const pos = entity.position.getValue(now);
  2469. if (!pos) {
  2470. this.resetPickDragState();
  2471. return;
  2472. }
  2473. if (this.screenDistanceToCartesian(event.position, pos) > maxPx) {
  2474. this.resetPickDragState();
  2475. return;
  2476. }
  2477. this.applyMapEditSelection(entity);
  2478. this._pickDrag = {
  2479. entity,
  2480. source,
  2481. featureIndex,
  2482. geometryType: "Point",
  2483. vertexIndex: 0,
  2484. dragHandleEntity: null,
  2485. startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
  2486. cameraLocked: true,
  2487. dragging: false,
  2488. };
  2489. this.setScreenSpaceCameraForDrag(false);
  2490. this.applyPointSelectionVisualByHoverPress();
  2491. return;
  2492. }
  2493. if (entity.polyline && entity.polyline.positions) {
  2494. const positions = entity.polyline.positions.getValue(now);
  2495. if (!positions || !positions.length) {
  2496. this.resetPickDragState();
  2497. return;
  2498. }
  2499. const vi = this.findClosestVertexIndexScreen(positions, event.position, maxPx);
  2500. if (vi < 0) {
  2501. // 点在折线段上但未贴近任一顶点:仍选中并高亮,便于查看属性
  2502. this.applyMapEditSelection(entity);
  2503. this._pickDrag = {
  2504. entity,
  2505. source,
  2506. featureIndex,
  2507. geometryType: "LineString",
  2508. vertexIndex: -1,
  2509. selectOnly: true,
  2510. startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
  2511. dragging: false,
  2512. };
  2513. return;
  2514. }
  2515. this.applyMapEditSelection(entity);
  2516. this._pickDrag = {
  2517. entity,
  2518. source,
  2519. featureIndex,
  2520. geometryType: "LineString",
  2521. vertexIndex: vi,
  2522. dragHandleEntity: this.vertexHandleEntities.find(
  2523. (h) => h.__handleType === "vertex" && h.__vertexIndex === vi
  2524. ),
  2525. workingPositions: this.cloneCartesian3Array(positions),
  2526. startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
  2527. cameraLocked: true,
  2528. dragging: false,
  2529. };
  2530. this.setScreenSpaceCameraForDrag(false);
  2531. this.applyVertexHandleStyles();
  2532. return;
  2533. }
  2534. if (entity.polygon && entity.polygon.hierarchy) {
  2535. const hProp = entity.polygon.hierarchy;
  2536. const hier =
  2537. hProp && typeof hProp.getValue === "function" ? hProp.getValue(now) : hProp;
  2538. const positions = hier && hier.positions;
  2539. if (!positions || !positions.length) {
  2540. this.resetPickDragState();
  2541. return;
  2542. }
  2543. const vi = this.findClosestVertexIndexScreen(positions, event.position, maxPx);
  2544. if (vi < 0) {
  2545. // 点在面填充/边线上但未贴近顶点:仍选中高亮并显示全部顶点柄
  2546. this.applyMapEditSelection(entity);
  2547. this._pickDrag = {
  2548. entity,
  2549. source,
  2550. featureIndex,
  2551. geometryType: "Polygon",
  2552. vertexIndex: -1,
  2553. selectOnly: true,
  2554. startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
  2555. dragging: false,
  2556. };
  2557. return;
  2558. }
  2559. this.applyMapEditSelection(entity);
  2560. this._pickDrag = {
  2561. entity,
  2562. source,
  2563. featureIndex,
  2564. geometryType: "Polygon",
  2565. vertexIndex: vi,
  2566. dragHandleEntity: this.vertexHandleEntities.find(
  2567. (h) => h.__handleType === "vertex" && h.__vertexIndex === vi
  2568. ),
  2569. workingPositions: this.cloneCartesian3Array(positions),
  2570. startScreen: new SkyScenery.Cartesian2(event.position.x, event.position.y),
  2571. cameraLocked: true,
  2572. dragging: false,
  2573. };
  2574. this.setScreenSpaceCameraForDrag(false);
  2575. this.applyVertexHandleStyles();
  2576. return;
  2577. }
  2578. this.resetPickDragState();
  2579. },
  2580. onPickDragMove(event) {
  2581. if (!this.geometryEditMode) {
  2582. const po = viewer.scene.pick(event.endPosition);
  2583. const ent = po && po.id && po.id.__featureRef ? po.id : null;
  2584. if (ent) {
  2585. this.applyHoveredFeatureStyle(ent);
  2586. } else {
  2587. this.clearHoveredFeatureStyle();
  2588. }
  2589. this.hideHoverHint();
  2590. return;
  2591. }
  2592. this.clearHoveredFeatureStyle();
  2593. const st = this._pickDrag;
  2594. const hoverNearHandle =
  2595. this.vertexHandleEntities && this.vertexHandleEntities.length
  2596. ? this.findNearestHandleByScreen(event.endPosition, 32)
  2597. : null;
  2598. if (st && st.entity && !st.selectOnly) {
  2599. const dx = event.endPosition.x - st.startScreen.x;
  2600. const dy = event.endPosition.y - st.startScreen.y;
  2601. const movedPx = Math.sqrt(dx * dx + dy * dy);
  2602. if (!st.dragging && movedPx > 1) {
  2603. st.dragging = true;
  2604. this.setScreenSpaceCameraForDrag(false);
  2605. this.enterInteractiveSceneRender();
  2606. }
  2607. if (st.dragging) {
  2608. const cart = this.pickSurfaceCartesian(event.endPosition);
  2609. if (cart) {
  2610. if (st.geometryType === "Point" && st.entity.position) {
  2611. st.entity.position = cart;
  2612. } else if (st.geometryType === "LineString" && st.workingPositions) {
  2613. const prev = st.workingPositions[st.vertexIndex];
  2614. const cPrev = SkyScenery.Cartographic.fromCartesian(prev);
  2615. const cNew = SkyScenery.Cartographic.fromCartesian(cart);
  2616. cNew.height = cPrev.height;
  2617. st.workingPositions[st.vertexIndex] = SkyScenery.Cartesian3.fromRadians(
  2618. cNew.longitude,
  2619. cNew.latitude,
  2620. cNew.height
  2621. );
  2622. st.entity.polyline.positions = st.workingPositions;
  2623. this.refreshVertexHandlesFromPositions(st.workingPositions);
  2624. } else if (st.geometryType === "Polygon" && st.workingPositions) {
  2625. const prev = st.workingPositions[st.vertexIndex];
  2626. const cPrev = SkyScenery.Cartographic.fromCartesian(prev);
  2627. const cNew = SkyScenery.Cartographic.fromCartesian(cart);
  2628. cNew.height = cPrev.height;
  2629. st.workingPositions[st.vertexIndex] = SkyScenery.Cartesian3.fromRadians(
  2630. cNew.longitude,
  2631. cNew.latitude,
  2632. cNew.height
  2633. );
  2634. const n = st.workingPositions.length;
  2635. // 仅当 positions 已带「首尾重合」闭合点时才同步;开环多边形首尾是不同角点,不能互拷否则会吞掉顶点
  2636. const ringClosedDuplicate =
  2637. n > 2 &&
  2638. SkyScenery.Cartesian3.distance(st.workingPositions[0], st.workingPositions[n - 1]) <
  2639. 0.5;
  2640. if (ringClosedDuplicate) {
  2641. if (st.vertexIndex === 0) {
  2642. st.workingPositions[n - 1] = SkyScenery.Cartesian3.clone(
  2643. st.workingPositions[0],
  2644. st.workingPositions[n - 1]
  2645. );
  2646. } else if (st.vertexIndex === n - 1) {
  2647. st.workingPositions[0] = SkyScenery.Cartesian3.clone(
  2648. st.workingPositions[n - 1],
  2649. st.workingPositions[0]
  2650. );
  2651. }
  2652. }
  2653. this.syncPolygonEntityHierarchy(st.entity, st.workingPositions);
  2654. this.refreshVertexHandlesFromPositions(st.workingPositions);
  2655. }
  2656. viewer.scene.requestRender();
  2657. }
  2658. }
  2659. }
  2660. // 相机默认先于自定义 handler 响应拖拽;在靠近顶点/中点或正在拖动节点时关闭相机输入,避免“拖地图”抢事件
  2661. const vertexInteraction =
  2662. !!(st && st.dragging) ||
  2663. !!(st && st.cameraLocked && st.entity && !st.selectOnly) ||
  2664. !!hoverNearHandle;
  2665. this.setScreenSpaceCameraForDrag(!vertexInteraction);
  2666. if (!(st && st.dragging)) {
  2667. this.updateVertexHoverFromPick(event.endPosition, hoverNearHandle);
  2668. } else {
  2669. this.applyVertexHandleStyles();
  2670. this.applyPointSelectionVisualByHoverPress();
  2671. }
  2672. },
  2673. onPickDragUp(event) {
  2674. const st = this._pickDrag;
  2675. if (!st || !st.entity) {
  2676. this.resetPickDragState();
  2677. return;
  2678. }
  2679. const wasDraggingGeometry = st.dragging;
  2680. const dx = event.position.x - st.startScreen.x;
  2681. const dy = event.position.y - st.startScreen.y;
  2682. const movedPx = Math.sqrt(dx * dx + dy * dy);
  2683. if (st.dragging) {
  2684. this.commitDraggedGeometryToGeojson(st);
  2685. this.syncJsonEditorData(st.source);
  2686. this.applyMapEditSelection(st.entity);
  2687. } else if (movedPx <= 5) {
  2688. this.openFeaturePropertyDialogForEntity(st.entity);
  2689. }
  2690. if (st.cameraLocked) {
  2691. this.setScreenSpaceCameraForDrag(true);
  2692. }
  2693. if (wasDraggingGeometry) {
  2694. this.exitInteractiveSceneRender();
  2695. }
  2696. this.resetPickDragState();
  2697. },
  2698. // 初始化地图要素拾取:单击打开属性;按住拖动顶点/点实时改坐标并同步 JSON
  2699. initFeaturePickHandler() {
  2700. if (this.featurePickHandler || !viewer || !viewer.canvas) {
  2701. return;
  2702. }
  2703. const h = new SkyScenery.ScreenSpaceEventHandler(viewer.canvas);
  2704. const SS = SkyScenery.ScreenSpaceEventType;
  2705. h.setInputAction((e) => this.tryBeginPickDrag(e), SS.LEFT_DOWN);
  2706. h.setInputAction((e) => this.onPickDragMove(e), SS.MOUSE_MOVE);
  2707. h.setInputAction((e) => this.onPickDragUp(e), SS.LEFT_UP);
  2708. this.featurePickHandler = h;
  2709. },
  2710. // 激活绘制工具
  2711. activateDraw(type) {
  2712. // 进入绘制工具时退出“更改绘制”模式,避免交互冲突
  2713. if (this.geometryEditMode) {
  2714. this.clearMapEditSelection();
  2715. }
  2716. // 取消镂空绘制模式
  2717. this.isDrawingHole = false;
  2718. // 如果已经是当前激活的工具,则取消激活
  2719. if (this.currentTool === type) {
  2720. this.deactivateDraw();
  2721. return;
  2722. }
  2723. // 先取消之前的绘制模式
  2724. this.deactivateDraw();
  2725. // 设置当前工具
  2726. this.currentTool = type;
  2727. this.drawingMode = type;
  2728. // 从当前渲染/编辑器数据同步几何,避免新绘制覆盖已有要素
  2729. this.syncGeometriesFromRenderedSource();
  2730. // 重置当前绘制状态
  2731. this.resetDrawingState();
  2732. switch (type) {
  2733. case "point":
  2734. this.drawPoint();
  2735. break;
  2736. case "polyline":
  2737. this.drawPolyline();
  2738. break;
  2739. case "polygon":
  2740. this.drawPolygon();
  2741. break;
  2742. }
  2743. if (type === "polyline" || type === "polygon") {
  2744. this.enterInteractiveSceneRender();
  2745. }
  2746. },
  2747. // 重置绘制状态
  2748. resetDrawingState() {
  2749. this.currentPositions = [];
  2750. this.currentEntity = null;
  2751. // 移除临时预览实体
  2752. if (this.tempEntity) {
  2753. viewer.entities.remove(this.tempEntity);
  2754. viewer.scene.requestRender();
  2755. this.tempEntity = null;
  2756. }
  2757. },
  2758. // 取消绘制模式
  2759. deactivateDraw() {
  2760. this.exitInteractiveSceneRender();
  2761. if (this.handler) {
  2762. // 移除所有事件监听器
  2763. this.handler.removeInputAction(SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
  2764. this.handler.removeInputAction(SkyScenery.ScreenSpaceEventType.MOUSE_MOVE);
  2765. this.handler.removeInputAction(SkyScenery.ScreenSpaceEventType.RIGHT_CLICK);
  2766. }
  2767. // 重置状态
  2768. this.resetDrawingState();
  2769. this.currentTool = null;
  2770. this.drawingMode = null;
  2771. this.isDrawingHole = false;
  2772. },
  2773. // 绘制点
  2774. drawPoint() {
  2775. const that = this;
  2776. this.handler.setInputAction(function (event) {
  2777. const position = that.pickSurfaceCartesian(event.position);
  2778. if (position) {
  2779. const cartographic = SkyScenery.Cartographic.fromCartesian(position);
  2780. const longitude = SkyScenery.Math.toDegrees(cartographic.longitude);
  2781. const latitude = SkyScenery.Math.toDegrees(cartographic.latitude);
  2782. // 创建点实体(实时渲染)
  2783. const entity = viewer.entities.add({
  2784. position: position,
  2785. point: {
  2786. show: true,
  2787. color: SkyScenery.Color.RED,
  2788. pixelSize: 10,
  2789. outlineColor: SkyScenery.Color.WHITE,
  2790. outlineWidth: 2,
  2791. },
  2792. });
  2793. viewer.scene.requestRender();
  2794. that.drawnEntities.push(entity);
  2795. // 转换为geometry格式并保存
  2796. const geometry = {
  2797. type: "Point",
  2798. coordinates: [longitude, latitude],
  2799. };
  2800. that.geometries.push(geometry);
  2801. that.changeGeometries();
  2802. const source = "input";
  2803. const featureIndex = that.geometries.length - 1;
  2804. entity.__featureRef = { source, featureIndex };
  2805. entity.__featureProperties = {};
  2806. console.log("绘制了点:", geometry);
  2807. }
  2808. }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
  2809. },
  2810. // 绘制线
  2811. drawPolyline() {
  2812. const that = this;
  2813. // 鼠标移动时更新临时线(实时渲染预览)
  2814. this.handler.setInputAction(function (event) {
  2815. if (that.currentPositions.length > 0) {
  2816. const endPosition = that.pickSurfaceCartesian(event.endPosition);
  2817. if (endPosition) {
  2818. const last = that.currentPositions[that.currentPositions.length - 1];
  2819. const pendingSeg = [last, endPosition];
  2820. // 待落点使用虚线预览
  2821. if (!that.tempEntity) {
  2822. that.tempEntity = viewer.entities.add({
  2823. polyline: {
  2824. show: true,
  2825. positions: pendingSeg,
  2826. material: new SkyScenery.PolylineDashMaterialProperty({
  2827. color: SkyScenery.Color.RED.withAlpha(0.9),
  2828. dashLength: 10,
  2829. }),
  2830. width: 3,
  2831. clampToGround: true,
  2832. },
  2833. });
  2834. viewer.scene.requestRender();
  2835. } else {
  2836. that.tempEntity.polyline.positions = pendingSeg;
  2837. viewer.scene.requestRender();
  2838. }
  2839. }
  2840. }
  2841. }, SkyScenery.ScreenSpaceEventType.MOUSE_MOVE);
  2842. // 左键点击添加点
  2843. this.handler.setInputAction(function (event) {
  2844. const position = that.pickSurfaceCartesian(event.position);
  2845. if (position) {
  2846. that.currentPositions.push(position.clone());
  2847. // 已落点使用实线
  2848. if (that.currentPositions.length > 1) {
  2849. if (!that.currentEntity) {
  2850. that.currentEntity = viewer.entities.add({
  2851. polyline: {
  2852. show: true,
  2853. positions: [...that.currentPositions],
  2854. material: SkyScenery.Color.RED,
  2855. width: 3,
  2856. clampToGround: true,
  2857. },
  2858. });
  2859. } else {
  2860. that.currentEntity.polyline.positions = [...that.currentPositions];
  2861. }
  2862. viewer.scene.requestRender();
  2863. }
  2864. }
  2865. }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
  2866. // 右键点击完成绘制
  2867. this.handler.setInputAction(function () {
  2868. if (that.currentPositions.length > 1) {
  2869. // 移除临时预览实体
  2870. if (that.tempEntity) {
  2871. viewer.entities.remove(that.tempEntity);
  2872. that.tempEntity = null;
  2873. }
  2874. // 否则创建线
  2875. // 确保当前实体存在
  2876. if (!that.currentEntity) {
  2877. that.currentEntity = viewer.entities.add({
  2878. polyline: {
  2879. show: true,
  2880. positions: [...that.currentPositions],
  2881. material: SkyScenery.Color.RED,
  2882. width: 3,
  2883. clampToGround: true,
  2884. },
  2885. });
  2886. }
  2887. that.drawnEntities.push(that.currentEntity);
  2888. viewer.scene.requestRender();
  2889. // 转换为geometry格式并保存
  2890. const coordinates = [];
  2891. that.currentPositions.forEach((pos) => {
  2892. const cartographic = SkyScenery.Cartographic.fromCartesian(pos);
  2893. coordinates.push([
  2894. SkyScenery.Math.toDegrees(cartographic.longitude),
  2895. SkyScenery.Math.toDegrees(cartographic.latitude),
  2896. ]);
  2897. });
  2898. const geometry = {
  2899. type: "LineString",
  2900. coordinates: coordinates,
  2901. };
  2902. that.geometries.push(geometry);
  2903. that.changeGeometries();
  2904. const source = "input";
  2905. const featureIndex = that.geometries.length - 1;
  2906. that.currentEntity.__featureRef = { source, featureIndex };
  2907. that.currentEntity.__featureProperties = {};
  2908. console.log("绘制了线:", geometry);
  2909. // 取消当前绘制模式
  2910. that.deactivateDraw();
  2911. }
  2912. }, SkyScenery.ScreenSpaceEventType.RIGHT_CLICK);
  2913. },
  2914. // 绘制面
  2915. drawPolygon() {
  2916. const that = this;
  2917. // 鼠标移动时更新临时面(实时渲染预览)
  2918. this.handler.setInputAction(function (event) {
  2919. if (that.currentPositions.length > 0) {
  2920. const endPosition = that.pickSurfaceCartesian(event.endPosition);
  2921. if (endPosition) {
  2922. const last = that.currentPositions[that.currentPositions.length - 1];
  2923. const first = that.currentPositions[0];
  2924. const pending = [last, endPosition, first];
  2925. // 待落点边使用虚线
  2926. if (!that.tempEntity) {
  2927. that.tempEntity = viewer.entities.add({
  2928. polyline: {
  2929. show: true,
  2930. positions: pending,
  2931. material: new SkyScenery.PolylineDashMaterialProperty({
  2932. color: SkyScenery.Color.RED.withAlpha(0.95),
  2933. dashLength: 10,
  2934. }),
  2935. width: 3,
  2936. clampToGround: true,
  2937. },
  2938. });
  2939. viewer.scene.requestRender();
  2940. } else {
  2941. that.tempEntity.polyline.positions = pending;
  2942. viewer.scene.requestRender();
  2943. }
  2944. }
  2945. }
  2946. }, SkyScenery.ScreenSpaceEventType.MOUSE_MOVE);
  2947. // 左键点击添加点
  2948. this.handler.setInputAction(function (event) {
  2949. const position = that.pickSurfaceCartesian(event.position);
  2950. if (position) {
  2951. // 检查是否点击了第一个点附近(自动闭合)
  2952. if (
  2953. that.currentPositions.length > 2 &&
  2954. that.isPositionNearFirst(position, that.currentPositions[0])
  2955. ) {
  2956. // 完成多边形绘制
  2957. that.finalizePolygonDrawing();
  2958. that.deactivateDraw();
  2959. return;
  2960. }
  2961. that.currentPositions.push(position.clone());
  2962. // 至少两个点时先显示已落点实线(第三个点后切换为实面+实边)
  2963. if (that.currentPositions.length === 2) {
  2964. const linePositions = [...that.currentPositions];
  2965. if (!that.currentEntity || !that.currentEntity.polyline) {
  2966. if (that.currentEntity) {
  2967. viewer.entities.remove(that.currentEntity);
  2968. }
  2969. that.currentEntity = viewer.entities.add({
  2970. polyline: {
  2971. show: true,
  2972. positions: linePositions,
  2973. material: SkyScenery.Color.RED,
  2974. width: 3,
  2975. clampToGround: true,
  2976. },
  2977. });
  2978. } else {
  2979. that.currentEntity.polyline.positions = linePositions;
  2980. }
  2981. viewer.scene.requestRender();
  2982. } else if (that.currentPositions.length > 2) {
  2983. // 闭合多边形
  2984. const closedPositions = [...that.currentPositions, that.currentPositions[0]];
  2985. if (!that.currentEntity) {
  2986. that.currentEntity = viewer.entities.add({
  2987. polygon: {
  2988. show: true,
  2989. hierarchy: new SkyScenery.PolygonHierarchy(closedPositions),
  2990. material: SkyScenery.Color.RED.withAlpha(0.35),
  2991. outline: true,
  2992. outlineColor: SkyScenery.Color.RED,
  2993. outlineWidth: 2,
  2994. },
  2995. });
  2996. } else if (that.currentEntity.polygon) {
  2997. that.currentEntity.polygon.hierarchy = new SkyScenery.PolygonHierarchy(closedPositions);
  2998. } else {
  2999. // 第二点阶段是线实体,第三点开始改为面实体
  3000. viewer.entities.remove(that.currentEntity);
  3001. that.currentEntity = viewer.entities.add({
  3002. polygon: {
  3003. show: true,
  3004. hierarchy: new SkyScenery.PolygonHierarchy(closedPositions),
  3005. material: SkyScenery.Color.RED.withAlpha(0.35),
  3006. outline: true,
  3007. outlineColor: SkyScenery.Color.RED,
  3008. outlineWidth: 2,
  3009. },
  3010. });
  3011. }
  3012. viewer.scene.requestRender();
  3013. }
  3014. }
  3015. }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
  3016. // 右键点击完成绘制
  3017. this.handler.setInputAction(function () {
  3018. if (that.currentPositions.length > 2) {
  3019. that.finalizePolygonDrawing();
  3020. // 取消当前绘制模式
  3021. that.deactivateDraw();
  3022. }
  3023. }, SkyScenery.ScreenSpaceEventType.RIGHT_CLICK);
  3024. },
  3025. // 完成多边形绘制
  3026. finalizePolygonDrawing() {
  3027. // 移除临时预览实体
  3028. if (this.tempEntity) {
  3029. viewer.entities.remove(this.tempEntity);
  3030. this.tempEntity = null;
  3031. }
  3032. // 确保多边形是闭合的
  3033. let closedPositions = [...this.currentPositions];
  3034. // 创建最终的多边形实体
  3035. if (this.currentEntity) {
  3036. viewer.entities.remove(this.currentEntity);
  3037. }
  3038. this.currentEntity = viewer.entities.add({
  3039. polygon: {
  3040. show: true,
  3041. hierarchy: new SkyScenery.PolygonHierarchy(closedPositions),
  3042. material: SkyScenery.Color.RED.withAlpha(0.5),
  3043. outline: true,
  3044. outlineColor: SkyScenery.Color.BLUE,
  3045. outlineWidth: 2,
  3046. },
  3047. });
  3048. viewer.scene.requestRender();
  3049. this.drawnEntities.push(this.currentEntity);
  3050. // 转换为geometry格式并保存(不包含重复的最后一个点)
  3051. const coordinates = [];
  3052. const mainRing = [];
  3053. for (let i = 0; i < closedPositions.length; i++) {
  3054. const cartographic = SkyScenery.Cartographic.fromCartesian(closedPositions[i]);
  3055. mainRing.push([
  3056. SkyScenery.Math.toDegrees(cartographic.longitude),
  3057. SkyScenery.Math.toDegrees(cartographic.latitude),
  3058. ]);
  3059. }
  3060. const cartographic = SkyScenery.Cartographic.fromCartesian(closedPositions[0]);
  3061. mainRing.push([
  3062. SkyScenery.Math.toDegrees(cartographic.longitude),
  3063. SkyScenery.Math.toDegrees(cartographic.latitude),
  3064. ]);
  3065. coordinates.push(mainRing);
  3066. const geometry = {
  3067. type: "Polygon",
  3068. coordinates: coordinates, // GeoJSON格式
  3069. holes: [], // 用于存储镂空区域
  3070. };
  3071. // 保存实体和几何对象的关联
  3072. this.currentEntity.geometryRef = geometry;
  3073. this.geometries.push(geometry);
  3074. this.changeGeometries();
  3075. const source = "input";
  3076. const featureIndex = this.geometries.length - 1;
  3077. this.currentEntity.__featureRef = { source, featureIndex };
  3078. this.currentEntity.__featureProperties = {};
  3079. console.log("绘制了面:", geometry);
  3080. },
  3081. // 开始绘制镂空
  3082. startHoleDrawing() {
  3083. // 如果已经在绘制镂空,则取消
  3084. if (this.isDrawingHole) {
  3085. this.isDrawingHole = false;
  3086. this.deactivateDraw();
  3087. return;
  3088. }
  3089. // 取消其他绘制模式
  3090. this.deactivateDraw();
  3091. // 设置为镂空绘制模式
  3092. this.isDrawingHole = true;
  3093. console.log("请点击一个多边形以添加镂空");
  3094. // 设置点击事件选择多边形
  3095. const that = this;
  3096. this.handler.setInputAction(function (event) {
  3097. // 拾取实体
  3098. const pickedObject = viewer.scene.pick(event.position);
  3099. if (pickedObject && pickedObject.id && pickedObject.id.polygon) {
  3100. const polygonEntity = pickedObject.id;
  3101. // 检查是否有对应的几何对象
  3102. if (polygonEntity.geometryRef && polygonEntity.geometryRef.type === "Polygon") {
  3103. that.currentPolygonEntity = polygonEntity;
  3104. that.currentPolygonGeometry = polygonEntity.geometryRef;
  3105. console.log("已选择多边形,现在绘制镂空区域");
  3106. // 开始绘制镂空区域(使用多边形绘制逻辑,但最后添加为镂空)
  3107. that.resetDrawingState();
  3108. that.drawHole();
  3109. } else {
  3110. console.log("选择的不是有效的多边形");
  3111. }
  3112. } else {
  3113. console.log("未选中任何多边形");
  3114. }
  3115. }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
  3116. },
  3117. // 绘制镂空区域
  3118. drawHole() {
  3119. const that = this;
  3120. this.enterInteractiveSceneRender();
  3121. // 清除之前的事件
  3122. this.handler.removeInputAction(SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
  3123. // 鼠标移动时更新临时镂空区域
  3124. this.handler.setInputAction(function (event) {
  3125. if (that.currentPositions.length > 0) {
  3126. const endPosition = that.pickSurfaceCartesian(event.endPosition);
  3127. if (endPosition) {
  3128. // 为了预览效果,临时闭合多边形
  3129. const tempPositions = [...that.currentPositions, endPosition, that.currentPositions[0]];
  3130. // 更新临时预览实体
  3131. if (!that.tempEntity) {
  3132. that.tempEntity = viewer.entities.add({
  3133. polygon: {
  3134. show: true,
  3135. hierarchy: new SkyScenery.PolygonHierarchy(tempPositions),
  3136. material: new SkyScenery.ColorMaterialProperty(
  3137. new SkyScenery.Color(1, 0, 0, 0.3)
  3138. ),
  3139. outline: true,
  3140. outlineColor: SkyScenery.Color.RED,
  3141. outlineWidth: 2,
  3142. },
  3143. });
  3144. viewer.scene.requestRender();
  3145. } else {
  3146. that.tempEntity.polygon.hierarchy = new SkyScenery.PolygonHierarchy(tempPositions);
  3147. }
  3148. }
  3149. }
  3150. }, SkyScenery.ScreenSpaceEventType.MOUSE_MOVE);
  3151. // 左键点击添加点
  3152. this.handler.setInputAction(function (event) {
  3153. const position = that.pickSurfaceCartesian(event.position);
  3154. if (position) {
  3155. // 检查是否点击了第一个点附近(自动闭合)
  3156. if (
  3157. that.currentPositions.length > 2 &&
  3158. that.isPositionNearFirst(position, that.currentPositions[0])
  3159. ) {
  3160. // 完成镂空绘制
  3161. that.finalizeHoleDrawing();
  3162. return;
  3163. }
  3164. that.currentPositions.push(position.clone());
  3165. }
  3166. }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
  3167. // 右键点击完成绘制
  3168. this.handler.setInputAction(function () {
  3169. if (that.currentPositions.length > 2) {
  3170. that.finalizeHoleDrawing();
  3171. }
  3172. }, SkyScenery.ScreenSpaceEventType.RIGHT_CLICK);
  3173. },
  3174. // 完成镂空绘制
  3175. finalizeHoleDrawing() {
  3176. // 移除临时预览实体
  3177. if (this.tempEntity) {
  3178. viewer.entities.remove(this.tempEntity);
  3179. viewer.scene.requestRender();
  3180. this.tempEntity = null;
  3181. }
  3182. // 确保镂空区域是闭合的
  3183. let closedPositions = [...this.currentPositions];
  3184. // 转换为几何坐标
  3185. const holeCoordinates = [];
  3186. for (let i = 0; i < closedPositions.length; i++) {
  3187. const cartographic = SkyScenery.Cartographic.fromCartesian(closedPositions[i]);
  3188. holeCoordinates.push([
  3189. SkyScenery.Math.toDegrees(cartographic.longitude),
  3190. SkyScenery.Math.toDegrees(cartographic.latitude),
  3191. ]);
  3192. }
  3193. const cartographic = SkyScenery.Cartographic.fromCartesian(closedPositions[0]);
  3194. holeCoordinates.push([
  3195. SkyScenery.Math.toDegrees(cartographic.longitude),
  3196. SkyScenery.Math.toDegrees(cartographic.latitude),
  3197. ]);
  3198. // 添加到几何对象的镂空数组
  3199. if (!this.currentPolygonGeometry.holes) {
  3200. this.currentPolygonGeometry.holes = [];
  3201. }
  3202. this.currentPolygonGeometry.holes.push(holeCoordinates);
  3203. // 更新多边形的层级结构以包含镂空
  3204. const polygonHierarchy = this.buildPolygonHierarchyWithHoles(
  3205. this.currentPolygonGeometry.coordinates[0],
  3206. this.currentPolygonGeometry.holes
  3207. );
  3208. // 更新实体显示
  3209. this.currentPolygonEntity.polygon.hierarchy = polygonHierarchy;
  3210. console.log("添加了镂空区域:", holeCoordinates);
  3211. console.log("更新后的多边形几何:", this.currentPolygonGeometry);
  3212. // 重置状态
  3213. this.resetDrawingState();
  3214. this.isDrawingHole = false;
  3215. this.currentPolygonEntity = null;
  3216. this.currentPolygonGeometry = null;
  3217. // 取消绘制模式
  3218. this.deactivateDraw();
  3219. },
  3220. // 构建包含镂空的多边形层级结构
  3221. buildPolygonHierarchyWithHoles(outerRing, holes) {
  3222. // 将外部环转换为Cartesian3数组
  3223. const outerRingPositions = [];
  3224. outerRing.forEach((coord) => {
  3225. const cartesian = SkyScenery.Cartesian3.fromDegrees(coord[0], coord[1]);
  3226. outerRingPositions.push(cartesian);
  3227. });
  3228. // 闭合外部环
  3229. outerRingPositions.push(outerRingPositions[0]);
  3230. // 创建层级结构
  3231. const hierarchy = new SkyScenery.PolygonHierarchy(outerRingPositions);
  3232. // 添加镂空
  3233. if (holes && holes.length > 0) {
  3234. hierarchy.holes = holes.map((hole) => {
  3235. const holePositions = [];
  3236. hole.forEach((coord) => {
  3237. const cartesian = SkyScenery.Cartesian3.fromDegrees(coord[0], coord[1]);
  3238. holePositions.push(cartesian);
  3239. });
  3240. // 闭合镂空环
  3241. holePositions.push(holePositions[0]);
  3242. return new SkyScenery.PolygonHierarchy(holePositions);
  3243. });
  3244. }
  3245. return hierarchy;
  3246. },
  3247. // 检查点是否靠近第一个点
  3248. isPositionNearFirst(position, firstPosition, tolerance = 10) {
  3249. // 单位:米
  3250. const distance = SkyScenery.Cartesian3.distance(position, firstPosition);
  3251. return distance < tolerance;
  3252. },
  3253. changeGeometries() {
  3254. let requestData = {};
  3255. const geometriesToSend = this.geometries;
  3256. const baseFeatures =
  3257. this.jsonData && Array.isArray(this.jsonData.features) ? this.jsonData.features : [];
  3258. let FeatureCollectionFeatures = [];
  3259. geometriesToSend.forEach((item, idx) => {
  3260. const base = baseFeatures[idx] || {};
  3261. FeatureCollectionFeatures.push({
  3262. type: "Feature",
  3263. properties: base.properties ? JSON.parse(JSON.stringify(base.properties)) : {},
  3264. geometry: {
  3265. type: item.type,
  3266. coordinates: item.coordinates,
  3267. },
  3268. });
  3269. });
  3270. // 构造请求数据
  3271. requestData = {
  3272. type: "FeatureCollection",
  3273. features: FeatureCollectionFeatures,
  3274. timestamp: new Date().getTime(),
  3275. };
  3276. if (
  3277. this.SceneValue &&
  3278. this.dmsServerItem &&
  3279. this.dmsServerItem.elementTypes &&
  3280. this.ifHasType("unit") &&
  3281. this.params.unit
  3282. ) {
  3283. requestData.unit = this.params.unit;
  3284. }
  3285. if (
  3286. this.SceneValue &&
  3287. this.dmsServerItem &&
  3288. this.dmsServerItem.elementTypes &&
  3289. this.ifHasType("EPSILON") &&
  3290. this.params.EPSILON
  3291. ) {
  3292. requestData.EPSILON = this.params.EPSILON;
  3293. }
  3294. if (
  3295. this.SceneValue &&
  3296. this.dmsServerItem &&
  3297. this.dmsServerItem.elementTypes &&
  3298. this.ifHasType("inPrj") &&
  3299. this.params.inPrj
  3300. ) {
  3301. requestData.inPrj = this.params.inPrj;
  3302. }
  3303. if (
  3304. this.SceneValue &&
  3305. this.dmsServerItem &&
  3306. this.dmsServerItem.elementTypes &&
  3307. this.ifHasType("compressionRatio") &&
  3308. this.params.compressionRatio
  3309. ) {
  3310. requestData.compressionRatio = this.params.compressionRatio;
  3311. }
  3312. if (
  3313. this.SceneValue &&
  3314. this.dmsServerItem &&
  3315. this.dmsServerItem.elementTypes &&
  3316. this.ifHasType("outPrj") &&
  3317. this.params.outPrj
  3318. ) {
  3319. requestData.outPrj = this.params.outPrj;
  3320. }
  3321. if (
  3322. this.SceneValue &&
  3323. this.dmsServerItem &&
  3324. this.dmsServerItem.elementTypes &&
  3325. this.ifHasType("distance") &&
  3326. this.params.distance
  3327. ) {
  3328. requestData.distance = this.params.distance;
  3329. }
  3330. this.jsonData = requestData;
  3331. },
  3332. // 发送几何数据到后台接口
  3333. sendGeometriesToBackend() {
  3334. // 这里使用axios发送请求,需要确保项目中已安装并导入axios
  3335. if (!this.dmsServerItem || !this.dmsServerItem.apiUrl) {
  3336. this.$message({
  3337. message: "请先选择有效场景并加载接口信息",
  3338. type: "warning",
  3339. });
  3340. return;
  3341. }
  3342. let requestData;
  3343. this.backData = {};
  3344. let requestUrl = this.dmsServerItem.apiUrl;
  3345. if (this.ifHasType("point") || this.ifHasType("polyline") || this.ifHasType("polygon")) {
  3346. const check = this.validateSendGeometryParams();
  3347. if (!check.ok) {
  3348. this.$message({
  3349. message: check.message,
  3350. type: "warning",
  3351. });
  3352. return;
  3353. }
  3354. requestData = this.jsonData;
  3355. } else {
  3356. requestData = new FormData();
  3357. this.dmsServerItem.elementTypes.forEach((key) => {
  3358. if (key == "file") {
  3359. if (!this.currentFile) {
  3360. return this.$message({
  3361. message: "请选择文件",
  3362. type: "error",
  3363. });
  3364. }
  3365. requestData.append("file", this.currentFile.raw);
  3366. } else {
  3367. requestData.append(key, this.params[key]);
  3368. }
  3369. });
  3370. requestData.append("token", localStorage.getItem("token"));
  3371. }
  3372. let that = this;
  3373. // 实际项目中使用以下代码发送请求
  3374. wgn
  3375. .topology(requestUrl, requestData)
  3376. .then((res) => {
  3377. if (requestUrl.indexOf("downloadFile") == -1) {
  3378. that.backData = res;
  3379. if (res.code && res.code == 200) {
  3380. that.$message({
  3381. message: res.message,
  3382. type: "success",
  3383. });
  3384. const downPath = that.pickDownFilePathFromResponse(res);
  3385. if (downPath) {
  3386. that.$nextTick(() => {
  3387. that.downloadConvertedFile(downPath);
  3388. });
  3389. }
  3390. } else {
  3391. that.$message({
  3392. message: res.content,
  3393. type: "error",
  3394. });
  3395. }
  3396. } else {
  3397. const blob = res; // 响应体是 Blob 类型
  3398. if (!blob) {
  3399. that.$message.error("下载失败:文件流为空");
  3400. reject("文件流为空");
  3401. return;
  3402. }
  3403. let _downloadFile = that.params.filePath + "";
  3404. let fileName = _downloadFile.substring(_downloadFile.indexOf("down_files") + 11);
  3405. // D:\work\backCode\one_map_portal_server\down_files\0378b1c2e92a4c36ab447f552c0c7888.xlsx
  3406. // 替换原代码中创建 url 的逻辑,先加校验
  3407. console.log("blob 类型:", Object.prototype.toString.call(blob)); // 正常应输出 [object Blob]
  3408. console.log("blob 大小:", blob.size); // 正常应大于 0
  3409. if (!(blob instanceof Blob) || blob.size === 0) {
  3410. this.$message.error("文件流解析失败,无法生成下载链接");
  3411. return;
  3412. }
  3413. const url = window.URL.createObjectURL(blob); // 将 Blob 转为临时 URL
  3414. const link = document.createElement("a");
  3415. link.href = url;
  3416. link.download = fileName; // 设置下载文件名
  3417. link.style.display = "none";
  3418. document.body.appendChild(link);
  3419. link.click(); // 触发点击下载
  3420. document.body.removeChild(link);
  3421. window.URL.revokeObjectURL(url); // 销毁临时 URL,避免内存泄漏
  3422. }
  3423. })
  3424. .catch((error) => {
  3425. console.error("后台接口调用失败:", error);
  3426. });
  3427. },
  3428. /**
  3429. * 从接口返回 JSON 中解析 downFilePath(支持 content 为对象或 JSON 字符串、或顶层字段)。
  3430. */
  3431. pickDownFilePathFromResponse(res) {
  3432. if (!res || typeof res !== "object") {
  3433. return null;
  3434. }
  3435. const tryVal = (v) => {
  3436. if (typeof v !== "string") {
  3437. return null;
  3438. }
  3439. const s = v.trim();
  3440. if (!s || s.indexOf("未知文件") !== -1) {
  3441. return null;
  3442. }
  3443. return s;
  3444. };
  3445. let p = tryVal(res.downFilePath);
  3446. if (p) {
  3447. return p;
  3448. }
  3449. const c = res.content;
  3450. if (c && typeof c === "object") {
  3451. p = tryVal(c.downFilePath);
  3452. if (p) {
  3453. return p;
  3454. }
  3455. }
  3456. if (typeof c === "string") {
  3457. try {
  3458. const parsed = JSON.parse(c);
  3459. if (parsed && typeof parsed === "object") {
  3460. p = tryVal(parsed.downFilePath);
  3461. if (p) {
  3462. return p;
  3463. }
  3464. }
  3465. } catch (e) {
  3466. /* ignore */
  3467. }
  3468. }
  3469. return null;
  3470. },
  3471. /** 根据后端返回的 downFilePath,请求 downloadFile 并触发浏览器下载(与「下载文件」场景一致) */
  3472. downloadConvertedFile(serverFilePath) {
  3473. const base =
  3474. typeof systemConfig !== "undefined" && systemConfig.baseServicerPath
  3475. ? systemConfig.baseServicerPath
  3476. : "";
  3477. const url = base + "/spatiotemporal_data_format_conversion/downloadFile";
  3478. getFile(url, { filePath: serverFilePath })
  3479. .then((blob) => {
  3480. if (!(blob instanceof Blob) || blob.size === 0) {
  3481. this.$message.warning(
  3482. "转换文件下载失败(空流),请从返回 JSON 的 downFilePath 手动获取"
  3483. );
  3484. return;
  3485. }
  3486. const name = this.fileNameFromPath(serverFilePath);
  3487. const href = window.URL.createObjectURL(blob);
  3488. const link = document.createElement("a");
  3489. link.href = href;
  3490. link.download = name;
  3491. link.style.display = "none";
  3492. document.body.appendChild(link);
  3493. link.click();
  3494. document.body.removeChild(link);
  3495. window.URL.revokeObjectURL(href);
  3496. this.$message.success("已开始下载转换文件:" + name);
  3497. })
  3498. .catch((e) => {
  3499. console.error(e);
  3500. this.$message.error("转换文件下载失败");
  3501. });
  3502. },
  3503. fileNameFromPath(fullPath) {
  3504. if (!fullPath) {
  3505. return "download";
  3506. }
  3507. const s = String(fullPath).replace(/\\/g, "/");
  3508. const i = s.lastIndexOf("/");
  3509. return i >= 0 ? s.slice(i + 1) : s;
  3510. },
  3511. // 清除所有绘制的元素,并更新geometries
  3512. clearAll() {
  3513. this.clearAllMap();
  3514. this.changeGeometries();
  3515. },
  3516. // 清除所有绘制的元素
  3517. clearAllMap() {
  3518. this.removeVertexHandleEntities();
  3519. this.mapEditSelection = null;
  3520. this.hoveredVertexIndex = -1;
  3521. this.hoveredHandleEntity = null;
  3522. this.clearHoveredFeatureStyle();
  3523. this.geometryEditMode = false;
  3524. this.pickedFeatureRef = null;
  3525. this.hideHoverHint();
  3526. // 移除所有实体
  3527. this.drawnEntities.forEach((entity) => {
  3528. viewer.entities.remove(entity);
  3529. });
  3530. viewer.scene.requestRender();
  3531. // 清空数组
  3532. this.drawnEntities = [];
  3533. this.geometries = [];
  3534. this.currentRenderedSource = "";
  3535. this.propertyDialog.visible = false;
  3536. this.propertyDialog.list = [];
  3537. this.propertyDialog.source = "";
  3538. this.propertyDialog.propertiesRef = null;
  3539. this.propertyDialog.featureIndex = -1;
  3540. this.resetPickDragState();
  3541. this.setScreenSpaceCameraForDrag(true);
  3542. // 取消当前绘制模式
  3543. this.deactivateDraw();
  3544. console.log("已清除所有绘制的元素");
  3545. },
  3546. },
  3547. };
  3548. </script>
  3549. <style lang="less" scoped>
  3550. // 操作栏样式
  3551. #controlPanelBox {
  3552. width: calc(30vw - 2rem);
  3553. min-width: 500px;
  3554. height: calc(100vh - 2rem);
  3555. display: flex;
  3556. flex-direction: column;
  3557. padding: 1rem;
  3558. position: fixed;
  3559. top: 0px;
  3560. right: 0;
  3561. background: #08224a;
  3562. }
  3563. // 绘制按钮区域
  3564. .toolbar {
  3565. position: absolute;
  3566. top: 10px;
  3567. left: -120px;
  3568. z-index: 1000;
  3569. background: #08224a;
  3570. border-radius: 8px;
  3571. padding: 3px 5px;
  3572. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  3573. display: flex;
  3574. flex-direction: column;
  3575. gap: 8px;
  3576. }
  3577. .tool-item {
  3578. padding: 10px 20px;
  3579. cursor: pointer;
  3580. border-radius: 4px;
  3581. transition: all 0.3s ease;
  3582. user-select: none;
  3583. font-size: 14px;
  3584. }
  3585. .tool-item:hover {
  3586. background: #ffffff32;
  3587. }
  3588. .tool-item.active {
  3589. background: #409eff;
  3590. color: white;
  3591. }
  3592. .sceneNameBox {
  3593. display: flex;
  3594. align-items: center;
  3595. justify-content: space-between;
  3596. }
  3597. .vueJsonEditor_box {
  3598. width: 100%;
  3599. height: 100%;
  3600. position: relative;
  3601. }
  3602. .vueJsonEditor_tools {
  3603. position: absolute;
  3604. top: 8px;
  3605. right: 10px;
  3606. font-size: 14px;
  3607. z-index: 1;
  3608. color: #fff;
  3609. }
  3610. .vueJsonEditor_tools span {
  3611. padding: 2px 5px;
  3612. opacity: 0.8;
  3613. cursor: pointer;
  3614. border: 1px solid #ffffff00;
  3615. border-radius: 2px;
  3616. margin-right: 5px;
  3617. &:hover {
  3618. background-color: rgba(255, 255, 255, 0.2);
  3619. border-color: rgba(255, 255, 255, 0.4);
  3620. }
  3621. }
  3622. .feature-property-content {
  3623. max-height: 360px;
  3624. overflow: auto;
  3625. padding-right: 4px;
  3626. }
  3627. .feature-property-tip {
  3628. color: #9ed2ff;
  3629. margin-bottom: 12px;
  3630. font-size: 13px;
  3631. }
  3632. .feature-property-item {
  3633. padding: 10px 12px;
  3634. margin-bottom: 8px;
  3635. border-radius: 8px;
  3636. background: rgba(61, 132, 205, 0.14);
  3637. border: 1px solid rgba(111, 186, 255, 0.35);
  3638. }
  3639. .feature-property-row {
  3640. display: flex;
  3641. align-items: center;
  3642. gap: 8px;
  3643. margin-bottom: 8px;
  3644. }
  3645. .feature-property-delete-btn {
  3646. flex-shrink: 0;
  3647. }
  3648. .feature-property-footer {
  3649. width: 100%;
  3650. display: flex;
  3651. align-items: center;
  3652. justify-content: space-between;
  3653. }
  3654. .feature-property-actions {
  3655. display: flex;
  3656. align-items: center;
  3657. gap: 12px;
  3658. }
  3659. .feature-property-key-input {
  3660. width: 180px;
  3661. }
  3662. .map-edit-hover-hint {
  3663. position: fixed;
  3664. z-index: 3000;
  3665. pointer-events: none;
  3666. color: #4a4a4a;
  3667. background: #fff;
  3668. border-radius: 4px;
  3669. padding: 7px 10px;
  3670. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
  3671. font-size: 12px;
  3672. line-height: 1.2;
  3673. transform-origin: left top;
  3674. animation: mapHintPop 120ms ease-out;
  3675. }
  3676. @keyframes mapHintPop {
  3677. from {
  3678. transform: scale(0.92);
  3679. opacity: 0.5;
  3680. }
  3681. to {
  3682. transform: scale(1);
  3683. opacity: 1;
  3684. }
  3685. }
  3686. :deep(.feature-property-input .el-input__wrapper) {
  3687. background: rgba(8, 34, 74, 0.68);
  3688. box-shadow: inset 0 0 0 1px rgba(130, 198, 255, 0.4);
  3689. }
  3690. :deep(.feature-property-input .el-input__inner) {
  3691. color: #ffffff;
  3692. }
  3693. :deep(.feature-property-dialog .el-dialog) {
  3694. background: rgba(7, 24, 48, 0.95);
  3695. border: 1px solid rgba(111, 186, 255, 0.5);
  3696. }
  3697. :deep(.feature-property-dialog .el-dialog__title) {
  3698. color: #e8f4ff;
  3699. font-weight: 700;
  3700. }
  3701. :deep(.feature-property-dialog .el-dialog__headerbtn .el-dialog__close) {
  3702. color: #d8ecff;
  3703. }
  3704. :deep(.feature-property-dialog .el-dialog__body) {
  3705. padding-top: 12px;
  3706. }
  3707. :deep(.ace_editor) {
  3708. height: 600px !important;
  3709. max-height: calc(100vh - 300px) !important;
  3710. }
  3711. :deep(.jsoneditor-modes),
  3712. :deep(.jsoneditor-poweredBy) {
  3713. display: none;
  3714. }
  3715. </style>