windMap3D.vue 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719
  1. <template>
  2. <div id="loading" class="dataLoading" v-if="loading">
  3. <span class="loadText">数据加载中...</span>
  4. </div>
  5. <div class="mapBox">
  6. <div id="cesiumContainer" style="width: 100%; height: 100vh"></div>
  7. <div :class="!menuComTStyB ? 'menuComTSty' : 'menuComT'" v-if="0">
  8. <menuCom
  9. :showbasemap="false"
  10. :showwindspeed="false"
  11. :showcloud="false"
  12. :showrainfall="false"
  13. :showtemperature="false"
  14. :showcity="false"
  15. :showwind="false"
  16. :showexit="true"
  17. :showwindModel="true"
  18. @handleWindModel="showwindmodel"
  19. @handleInit="handleInitView"
  20. @handleExit="switchLayer"
  21. />
  22. </div>
  23. <el-drawer
  24. v-model="windDrawer"
  25. direction="rtl"
  26. class="windModelDrawer"
  27. :before-close="handleClose"
  28. >
  29. <template #header>
  30. <h3 style="font-weight: bold">{{ windDrawerHeader }}</h3>
  31. </template>
  32. <template #default>
  33. <div class="windDrawerCla">
  34. <div class="line" v-if="!showModelMsg">
  35. <div class="leftContent">
  36. <span>{{ windDrawerTitle }}</span>
  37. </div>
  38. </div>
  39. <div class="jcxx" v-if="showBasicMsg">
  40. <windHome :modelValItem="modelVal" />
  41. </div>
  42. <div class="spjk" v-if="showVideoMsg">
  43. <!-- <iframe
  44. src="/static/windVideo.mp4"
  45. frameborder="0"
  46. style="width: 100%; height: 100%"
  47. ></iframe> -->
  48. <video ref="videoPlayer" controls width="95%" muted autoplay>
  49. <source src="/static/windVideo.mp4" type="video/mp4" />
  50. </video>
  51. </div>
  52. <div class="gzck" v-if="showProblemMsg">
  53. <windPro />
  54. </div>
  55. <div class="third" v-if="showModelMsg">
  56. <ModelUnpack
  57. :modelUnpackType="modelUnpackType"
  58. :showModelMsg="showModelMsg"
  59. />
  60. </div>
  61. </div>
  62. </template>
  63. </el-drawer>
  64. <comModelDialog
  65. :showcomModelDia="showcomModelDia"
  66. :modelVal="modelVal"
  67. @showDia="showComDia"
  68. />
  69. <windView
  70. v-if="showWindDetail"
  71. :cesiumViewer="viewer"
  72. @coverOnChange="coverOnChange"
  73. :currentHeight="currentHeight"
  74. :fjLonLatJsonArr="fjLonLatJsonArr.data"
  75. @showDetail="menuComTSty"
  76. @backStations="backStations"
  77. @resetChangeWind="resetChangeWind"
  78. @initView="resetWindViewport"
  79. />
  80. </div>
  81. </template>
  82. <script>
  83. import * as Cesium from "../../Cesium";
  84. import "../../Cesium/Widgets/widgets.css";
  85. import fjMYLonLatJson from "../fjLonLatJson/fj_MY.json";
  86. import fjWHZLonLatJson from "../fjLonLatJson/fj_WHZ.json"; //京能旺海庄
  87. import fjYPLLonLatJson from "../fjLonLatJson/fj_YPL.json"; //京能营盘梁
  88. import fjSMSLonLatJson from "../fjLonLatJson/fj_SMS.json"; //京能苏木山
  89. import wsRes from "../fjLonLatJson/wsRes.json"; // 模拟 ws 返回数据
  90. import mountainPosJson from "../fjLonLatJson/mountainPos.json";
  91. import allStationJson from "./allStationJson.json";
  92. import basicGeoJson from "../../assets/geoJson/basic.json";
  93. import comModelDialog from "@/components/comModelDialog.vue";
  94. import windView from "./windView.vue";
  95. import menuCom from "../menuCom.vue";
  96. import dayjs from "dayjs";
  97. import cloudJson from "/public/static/exportData/cloud/layer.json";
  98. import rainJson from "/public/static/exportData/rain/layer.json";
  99. import tempJson from "/public/static/exportData/tmp/layer.json";
  100. //风场展示图标
  101. import fc from "@/assets/windimgs/fanSvg/fc.png";
  102. //火电展示图标
  103. import hd from "@/assets/windimgs/fanSvg/hd.png";
  104. //光伏电站展示图标
  105. import gf from "@/assets/windimgs/fanSvg/gf.png";
  106. import windHome from "@/components/windHome/index.vue";
  107. import windPro from "@/components/windProDetail/windProblem.vue";
  108. import ModelUnpack from "@/components/modelUnpack.vue";
  109. import { WindLayer } from "cesium-wind";
  110. import windGridData from "./windGridData.json";
  111. import { HeightReference } from "cesium";
  112. import AdvancedBillboardGenerator from "@/tools/lightsign.js";
  113. import webSocketService from "@/tools/ws.js";
  114. export default {
  115. name: "windMap3D",
  116. components: {
  117. comModelDialog,
  118. windView,
  119. menuCom,
  120. windHome,
  121. windPro,
  122. ModelUnpack,
  123. },
  124. data() {
  125. return {
  126. restLatLon: [
  127. {
  128. longitude: 114.48789,
  129. latitude: 35.32916,
  130. name: "MYFDC",
  131. },
  132. {
  133. longitude: 112.88355172,
  134. latitude: 40.46617836,
  135. name: "JNWHZ",
  136. },
  137. {
  138. longitude: 112.5270545,
  139. latitude: 40.35920334,
  140. name: "JNYPL",
  141. },
  142. {
  143. longitude: 112.69922452,
  144. latitude: 40.31857399,
  145. name: "JNSMS",
  146. },
  147. ],
  148. fjLonLatJsonArr: [],
  149. loading: true,
  150. viewer: null,
  151. windLayer: null, // 风场图
  152. windLayerTimmer: null, // 风场图计时器
  153. cloudImagesLayer: [], // 卫星云图
  154. cloudLayer: null, // 卫星云图
  155. cloudintervalId: null,
  156. rainImagesLayer: [], // 降雨图
  157. rainLayer: null, // 降雨图
  158. rainintervalId: null, // 降雨图
  159. tempImagesLayer: [], //温度图
  160. temperatureLayer: null, //温度图
  161. tempintervalId: null, //温度图
  162. showcomModelDia: false,
  163. currentHeight: 0,
  164. modelVal: null,
  165. menuComTStyB: false,
  166. modelUnpackType: "fengji",
  167. windDrawer: false,
  168. windDrawerTitle: "",
  169. windDrawerHeader: "",
  170. showBasicMsg: false,
  171. showVideoMsg: false,
  172. showProblemMsg: false,
  173. showModelMsg: true,
  174. showWindDetail: true,
  175. allStationentitys: [],
  176. allWindEntitys: [],
  177. urlTiles: "/static/tiles/{z}/{x}/{y}.jpg",
  178. verticalExaggeration: 1.0,
  179. infoBoxes: [],
  180. entityxy: {},
  181. entityxyArray: [],
  182. isFakeData: false,
  183. fakeDataTimmer: null,
  184. };
  185. },
  186. mounted() {
  187. this.initCesium();
  188. setTimeout(() => {
  189. this.loading = false;
  190. }, 1000);
  191. },
  192. methods: {
  193. async initCesium() {
  194. const box = document.getElementById("cesiumContainer");
  195. const viewer = new Cesium.Viewer(box, {
  196. // terrainProvider: Cesium.createWorldTerrain(),
  197. baseLayerPicker: false, //是否显示底图切换按钮
  198. animation: false, //是否显示动画控制按钮
  199. vrButton: false,
  200. geocoder: false, //是否显示地理编码按钮
  201. homeButton: false, //是否显示地图导航按钮
  202. infoBox: false,
  203. sceneModePicker: false, //是否显示场景模式切换按钮
  204. selectionIndicator: false,
  205. timeline: false, //是否显示时间轴
  206. fullscreenButton: false, //是否显示全屏按钮
  207. navigationHelpButton: false,
  208. shouldAnimate: true,
  209. imageryProvider: false, //控制默认底图的显示
  210. // terrain: Cesium.Terrain.fromWorldTerrain(),
  211. });
  212. viewer._cesiumWidget._creditContainer.style.display = "none";
  213. // this.csceneElliposid(viewer)
  214. const utcStart = dayjs(
  215. `${dayjs().format("YYYY-MM-DD")} 00:00:00`
  216. ).subtract(4, "hour");
  217. const startTime = Cesium.JulianDate.fromDate(new Date(utcStart));
  218. const utcStop = dayjs(
  219. `${dayjs().format("YYYY-MM-DD")} 23:59:59`
  220. ).subtract(4, "hour");
  221. const stopTime = Cesium.JulianDate.fromDate(new Date(utcStop));
  222. const utcNow = dayjs().subtract(4, "hour");
  223. const nowTime = Cesium.JulianDate.fromDate(new Date(utcNow));
  224. // dayjs().format("YYYY-MM-DD")} 00:00:00
  225. viewer.clock.startTime = startTime;
  226. viewer.clock.stopTime = stopTime;
  227. viewer.clock.currentTime = nowTime;
  228. viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; // 循环模式:到达结束时跳回开始
  229. // viewer.clock.multiplier = 10000; // 时间流逝速度(加快模拟)
  230. viewer.clock.shouldAnimate = true;
  231. this.viewer = viewer;
  232. // 展示场站
  233. // this.showAllStation(viewer)
  234. // 展示风机
  235. this.initCesiumTerrain();
  236. this.initCesiumBaseMapImage();
  237. // this.initGeoJsonData();
  238. this.showWindFromStation(viewer);
  239. },
  240. // 初始化底图
  241. async initCesiumBaseMapImage() {
  242. const imageryProvider = await new Cesium.UrlTemplateImageryProvider({
  243. // url: "http://localhost:3007/tiles/map/{z}/{x}/{y}",
  244. url:
  245. import.meta.env.VITE_APP_ENV_NAME === "dev"
  246. ? "https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}"
  247. : "/static/ditu/{z}/{x}/{y}.png",
  248. // minimumLevel: 11,
  249. maximumLevel: 24,
  250. credit: "影像地图",
  251. });
  252. imageryProvider.alpha = 0.55; // 透明度
  253. imageryProvider.brightness = 1; // 亮度
  254. imageryProvider.contrast = 1; // 对比度
  255. this.viewer.imageryLayers.addImageryProvider(imageryProvider);
  256. },
  257. // 初始化 geoJson 数据
  258. async initGeoJsonData() {
  259. // 创建GeoJSON数据源
  260. await new Cesium.GeoJsonDataSource.load(basicGeoJson, {
  261. stroke: Cesium.Color.GRAY, // 边界线颜色
  262. fill: Cesium.Color.BLACK.withAlpha(0), // 填充颜色
  263. strokeWidth: 1, // 边界线宽度
  264. markerSymbol: "?", // 点要素的符号
  265. clampToGround: true, // 贴地
  266. }).then((dataSource) => {
  267. // 添加到视图
  268. this.viewer.dataSources.add(dataSource);
  269. var entities = dataSource.entities.values;
  270. for (let i = 0; i < entities.length; i++) {
  271. let entity = entities[i];
  272. entity.polygon.hierarchy.getValue(Cesium.JulianDate.now()).positions;
  273. //单独设置线条样式
  274. var positions = entity.polygon.hierarchy._value.positions;
  275. entity.polyline = {
  276. positions: positions,
  277. width: 2,
  278. outline: false,
  279. // clampToGround: true,
  280. show: true,
  281. };
  282. entity.wall = {
  283. positions: entity.polyline.positions.getValue(
  284. Cesium.JulianDate.now()
  285. ),
  286. // maximumHeights: new Array(
  287. // entity.polyline.positions.getValue(Cesium.JulianDate.now()).length
  288. // ).fill(-3000), // 向下30米
  289. minimumHeights: new Array(
  290. entity.polyline.positions.getValue(Cesium.JulianDate.now()).length
  291. ).fill(-50000), // 向下50米
  292. material: Cesium.Color.BLACK.withAlpha(0.1),
  293. outline: false,
  294. // outlineColor: Cesium.Color.WHITE,
  295. };
  296. }
  297. // 添加中文标签图层
  298. const labelLayer = new Cesium.LabelCollection();
  299. this.viewer.scene.primitives.add(labelLayer);
  300. const cities = [];
  301. basicGeoJson?.features?.forEach((ele) => {
  302. if (Array.isArray(ele.properties.centroid)) {
  303. const name = ele.properties.name;
  304. const lon = ele.properties.centroid[0];
  305. const lat = ele.properties.centroid[1];
  306. cities.push({ name, lon, lat });
  307. }
  308. });
  309. cities.forEach((city, index) => {
  310. labelLayer.add({
  311. id: index,
  312. name: "cityLabel",
  313. position: Cesium.Cartesian3.fromDegrees(city.lon, city.lat, 10),
  314. text: city.name,
  315. font: 'bold 14px "Microsoft YaHei", sans-serif',
  316. fillColor: Cesium.Color.fromCssColorString("#000"),
  317. outlineColor: Cesium.Color.WHITE,
  318. outlineWidth: 2,
  319. style: Cesium.LabelStyle.FILL_AND_OUTLINE,
  320. pixelOffset: new Cesium.Cartesian2(0, 0), // 设置为0
  321. horizontalOrigin: Cesium.HorizontalOrigin.CENTER, // 水平居中
  322. verticalOrigin: Cesium.VerticalOrigin.CENTER, // 垂直居中
  323. });
  324. });
  325. this.labelLayer = labelLayer;
  326. // this.csceneElliposid(this.viewer, null)
  327. // this.resetViewport1();
  328. });
  329. },
  330. // 初始化地形
  331. async initCesiumTerrain() {
  332. const terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl(
  333. // "http://localhost:3007/tiles/dixing",
  334. "/static/dixing",
  335. {
  336. requestWaterMask: true, // 如果需要水效果,设置为true
  337. requestVertexNormals: true, // 如果需要光照效果,设置为true
  338. requestMetadata: true, // 请求元数据
  339. minimumLevel: 4,
  340. maximumLevel: 12,
  341. }
  342. );
  343. this.viewer.scene.verticalExaggeration = this.verticalExaggeration;
  344. // this.viewer.scene.verticalExaggerationRelativeHeight = 2400.0;
  345. this.viewer.terrainProvider = terrainProvider;
  346. // const terrainProvider = new Cesium.CesiumTerrainProvider({
  347. // url: "http://localhost:3007/tiles/dixing", // 地形数据所在目录的URL
  348. // requestWaterMask: true, // 如果需要水效果,设置为true
  349. // requestVertexNormals: true, // 如果需要光照效果,设置为true
  350. // minimumLevel: 10,
  351. // maximumLevel: 15,
  352. // });
  353. // this.viewer.terrainProvider = terrainProvider;
  354. // console.log(111, terrainProvider);
  355. // const imageryProvider = new Cesium.UrlTemplateImageryProvider({
  356. // url: "http://localhost:3007/tiles/dixing/{z}/{x}/{y}",
  357. // // url: "/static/dixing/{z}/{x}/{y}.terrain",
  358. // credit: "地形数据",
  359. // });
  360. // this.viewer.imageryLayers.addImageryProvider(imageryProvider);
  361. // this.viewer.scene.globe.depthTestAgainstTerrain = true; //地形遮挡效果开关,打开后 地形会遮挡看不到的区域
  362. this.viewer.scene.globe.enableLighting = true; //对大气和雾启用动态照明效
  363. },
  364. backStations() {
  365. this.showWindDetail = false;
  366. this.allWindEntitys.forEach(({ entity, handler }) => {
  367. this.viewer.entities.remove(entity); // 移除实体
  368. if (!handler.isDestroyed()) {
  369. handler.destroy(); // 销毁事件处理器(关键!)
  370. }
  371. });
  372. this.allWindEntitys = [];
  373. // if (this.viewer && this.viewer.destroy) {
  374. // this.viewer.destroy();
  375. // this.viewer = null
  376. // }
  377. // this.initCesium();
  378. this.showAllStation(this.viewer);
  379. },
  380. resetChangeWind(data) {
  381. let that = this;
  382. that.viewer.camera.flyTo({
  383. destination: Cesium.Cartesian3.fromDegrees(
  384. data.longitude,
  385. data.latitude - 0.02,
  386. 1000
  387. ),
  388. orientation: {
  389. heading: Cesium.Math.toRadians(0),
  390. pitch: Cesium.Math.toRadians(-30),
  391. roll: 0.0,
  392. },
  393. duration: 3.0,
  394. });
  395. },
  396. // 展示所有风场
  397. showAllStation(viewer) {
  398. allStationJson.station.forEach((e, index) => {
  399. if (e.energytype === "Wind") {
  400. this.showStationFn(viewer, e, index, fc);
  401. } else if (e.energytype === "Fire") {
  402. this.showStationFn(viewer, e, index, hd);
  403. } else {
  404. this.showStationFn(viewer, e, index, gf);
  405. }
  406. });
  407. this.resetAllStationViewport();
  408. },
  409. // 根据状态展示不同颜色风机贴图
  410. showStationFn(viewer, e, index, images) {
  411. const position = Cesium.Cartesian3.fromDegrees(e.longitude, e.latitude);
  412. const entity = viewer.entities.add({
  413. id: index,
  414. position, // 模型位置
  415. billboard: {
  416. image: images, // 也可以是 SVG 路径,如 'icon.svg'
  417. scale: 0.5,
  418. verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
  419. horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
  420. // 模型贴地
  421. heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
  422. },
  423. label: {
  424. text: e.plantname,
  425. font: "14px sans-serif",
  426. fillColor: Cesium.Color.fromBytes(255, 255, 255),
  427. heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
  428. },
  429. });
  430. let that = this;
  431. const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
  432. handler.setInputAction(function (movement) {
  433. var position = movement.position;
  434. var pickedObject = viewer.scene.pick(position);
  435. if (pickedObject && pickedObject.id.id === index) {
  436. console.log("你点击了标签或模型!", entity);
  437. console.log("选中风场或新能源场", e.plantname);
  438. that.showWindDetail = true;
  439. that.allStationentitys.forEach(({ entity, handler }) => {
  440. viewer.entities.remove(entity); // 移除实体
  441. if (!handler.isDestroyed()) {
  442. handler.destroy(); // 销毁事件处理器(关键!)
  443. }
  444. });
  445. that.allStationentitys = [];
  446. that.showWindFromStation(viewer);
  447. }
  448. }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  449. that.allStationentitys.push({ entity, handler });
  450. },
  451. // 展示所选风场的风机
  452. showWindFromStation(viewer) {
  453. let stationName = this.$route.query.nameEn;
  454. let fjLonLatJson = [];
  455. this.fjLonLatJsonArr = [];
  456. webSocketService.connect({
  457. url: `ws/${this.getWsUrl(stationName)}`,
  458. autoReconnect: true,
  459. maxReconnectAttempts: 5,
  460. reconnectInterval: 3000,
  461. onOpen: (event) => {
  462. console.log("✅ WebSocket 连接成功", event);
  463. },
  464. onError: (error) => {
  465. console.log("❌ WebSocket 错误:", error);
  466. if (stationName === "MYFDC") {
  467. fjLonLatJson = JSON.parse(JSON.stringify(fjMYLonLatJson));
  468. } else if (stationName === "JNWHZ") {
  469. fjLonLatJson = JSON.parse(JSON.stringify(fjWHZLonLatJson));
  470. } else if (stationName === "JNYPL") {
  471. fjLonLatJson = JSON.parse(JSON.stringify(fjYPLLonLatJson));
  472. } else if (stationName === "JNSMS") {
  473. fjLonLatJson = JSON.parse(JSON.stringify(fjSMSLonLatJson));
  474. }
  475. let wtArray = JSON.parse(JSON.stringify(wsRes.windMachineList));
  476. this.isFakeData = true;
  477. fjLonLatJson.data.forEach((e) => {
  478. let fjItem =
  479. wtArray.find((findEle) => {
  480. return findEle.fjbh === e.fjbh;
  481. }) || {};
  482. const fjMix = Object.assign({}, e, fjItem);
  483. fjMix.status = this.getStatus(fjMix.fjzt);
  484. e = fjMix;
  485. this.showStatuswind(viewer, e);
  486. });
  487. this.fjLonLatJsonArr = fjLonLatJson;
  488. this.fakeDataTimmer = setInterval(() => {
  489. // viewer.entities.removeAll();
  490. }, 5000);
  491. },
  492. });
  493. webSocketService.on("message", (res) => {
  494. if (stationName === "MYFDC") {
  495. fjLonLatJson = fjMYLonLatJson;
  496. } else if (stationName === "JNWHZ") {
  497. fjLonLatJson = fjWHZLonLatJson;
  498. } else if (stationName === "JNYPL") {
  499. fjLonLatJson = fjYPLLonLatJson;
  500. } else if (stationName === "JNSMS") {
  501. fjLonLatJson = fjSMSLonLatJson;
  502. }
  503. let wtArray = res.windMachineList;
  504. fjLonLatJson.data.forEach((e, index) => {
  505. let fjItem =
  506. wtArray.find((findEle) => {
  507. return findEle.fjbh === e.fjbh;
  508. }) || {};
  509. const fjMix = Object.assign({}, e, fjItem);
  510. e = fjMix;
  511. this.showStatuswind(viewer, e);
  512. });
  513. this.fjLonLatJsonArr = fjLonLatJson;
  514. });
  515. this.resetWindViewport();
  516. },
  517. getWsUrl(stationName) {
  518. let wsurl = "";
  519. if (stationName === "MYFDC") {
  520. wsurl = "HZMY";
  521. } else if (stationName === "JNWHZ") {
  522. wsurl = "DHWH";
  523. } else if (stationName === "JNYPL") {
  524. wsurl = "DHYP";
  525. } else if (stationName === "JNSMS") {
  526. wsurl = "DHSM";
  527. }
  528. return wsurl;
  529. },
  530. getStatus(sName) {
  531. let status = 0;
  532. if (/待机/.test(sName)) {
  533. status = 1;
  534. } else if (/故障/.test(sName)) {
  535. status = 2;
  536. } else if (/检修/.test(sName)) {
  537. status = 3;
  538. } else if (/限电/.test(sName)) {
  539. status = 4;
  540. } else if (/离线/.test(sName)) {
  541. status = 5;
  542. } else if (/受累/.test(sName)) {
  543. status = 6;
  544. } else if (/并网/.test(sName)) {
  545. status = 7;
  546. }
  547. return status;
  548. },
  549. // 根据状态展示不同颜色风机贴图
  550. showStatuswind(viewer, e) {
  551. this.addModel(
  552. viewer,
  553. e.fjbh,
  554. e.longitude,
  555. e.latitude,
  556. e.status,
  557. e.height || 0,
  558. e
  559. );
  560. },
  561. //给模型上描边
  562. getWtStatue(code, type) {
  563. if (code === 1) {
  564. //待机
  565. // return "#05bb4c";
  566. return type === "color" ? "#05bb4c" : type === "en" ? "dj" : "待机";
  567. } else if (code === 2) {
  568. //故障
  569. // return "#d83238";
  570. return type === "color" ? "#d83238" : type === "en" ? "gz" : "故障";
  571. } else if (code === 3) {
  572. //检修
  573. // return "#ff8300";
  574. return type === "color" ? "#ff8300" : type === "en" ? "jx" : "检修";
  575. } else if (code === 4) {
  576. //限电
  577. // return "#c732ca";
  578. return type === "color" ? "#c732ca" : type === "en" ? "xd" : "限电";
  579. } else if (code === 5) {
  580. //离线
  581. // return "#6f7881";
  582. return type === "color" ? "#6f7881" : type === "en" ? "lx" : "离线";
  583. } else if (code === 6) {
  584. //受累
  585. // return "#cbd1d7";
  586. return type === "color" ? "#cbd1d7" : type === "en" ? "sl" : "受累";
  587. } else {
  588. //并网
  589. // return "#42a7f9";
  590. return type === "color" ? "#42a7f9" : type === "en" ? "bw" : "并网";
  591. }
  592. },
  593. addTreeain(viewer, mountainMsg) {
  594. const hpRoll = new Cesium.HeadingPitchRoll(90.0, 0.0, 0.0);
  595. const position = Cesium.Cartesian3.fromDegrees(
  596. mountainMsg.longitude,
  597. mountainMsg.latitude,
  598. mountainMsg.height || 0
  599. );
  600. const orientation = Cesium.Transforms.headingPitchRollQuaternion(
  601. position,
  602. hpRoll
  603. );
  604. const offsetLocal = new Cesium.Cartesian3(
  605. mountainMsg.east,
  606. mountainMsg.north,
  607. mountainMsg.up
  608. ); // E, N, U
  609. const transform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
  610. const offsetGlobal = new Cesium.Cartesian3();
  611. Cesium.Matrix4.multiplyByPointAsVector(
  612. transform,
  613. offsetLocal,
  614. offsetGlobal
  615. );
  616. const finalPos = Cesium.Cartesian3.add(
  617. position,
  618. offsetGlobal,
  619. new Cesium.Cartesian3()
  620. );
  621. const terrain = viewer.entities.add({
  622. name: mountainMsg.modelName, // 模型名称
  623. position: finalPos, // 模型位置
  624. orientation, // 模型朝向
  625. animation: false,
  626. model: {
  627. uri: `/static/model/dixing/${mountainMsg.modelNameEn}.glb`,
  628. // uri: "/static/model/dixing/terrain.glb",
  629. scale: mountainMsg.scale,
  630. // 模型贴地
  631. heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
  632. },
  633. });
  634. },
  635. //添加模型
  636. addModel(viewer, name, lon, lat, status, height, model) {
  637. let that = this;
  638. const hpRoll = new Cesium.HeadingPitchRoll(45.0, 0.0, 0.0);
  639. const position = Cesium.Cartesian3.fromDegrees(lon, lat, height || 0);
  640. const orientation = Cesium.Transforms.headingPitchRollQuaternion(
  641. position,
  642. hpRoll
  643. );
  644. const statueColor = this.getWtStatue(status, "color");
  645. const statueFJ = this.getWtStatue(status, "en");
  646. const statueZh = this.getWtStatue(status, "zh");
  647. const entity = viewer.entities.add({
  648. name, // 模型名称
  649. position, // 模型位置
  650. orientation, // 模型朝向
  651. animation: false,
  652. model: {
  653. uri: `/static/model/fjStatus/fj_${statueFJ}.glb`,
  654. scale: 0.5,
  655. // 模型贴地
  656. heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
  657. // heightReference: Cesium.HeightReference.NONE,
  658. silhouetteSize: 1,
  659. silhouetteColor: Cesium.Color.fromCssColorString(statueColor),
  660. minimumPixelSize: 75,
  661. // maximumScale:0.5
  662. // runAnimations: wtStatue !== 7 ? false : true,
  663. },
  664. label: {
  665. text: name,
  666. font: "14px sans-serif",
  667. fillColor: Cesium.Color.fromBytes(255, 255, 255),
  668. heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
  669. },
  670. });
  671. const btn = document.getElementById("windBtn");
  672. btn.addEventListener("click", function (event) {
  673. entityxy.show = !entityxy.show;
  674. });
  675. // viewer.camera.moveEnd.addEventListener(() => {
  676. // // 获取相机的笛卡尔3D坐标
  677. // const position = viewer.camera.position;
  678. // // 将笛卡尔坐标转换为地理坐标(弧度)
  679. // const cartographic = Cesium.Cartographic.fromCartesian(position);
  680. // // 高度(以米为单位)
  681. // // 注意:这个高度是相对于WGS84椭球体的高度,不是海拔高度(MSL)
  682. // const height = Cesium.Math.toDegrees(cartographic.height);
  683. // if (height > 3000000) {
  684. // entityxy.show = false;
  685. // console.log("entityxy false", entityxy.show);
  686. // }
  687. // if (height < 3000000) {
  688. // entityxy.show = true;
  689. // console.log("entityxy true", entityxy.show);
  690. // }
  691. // // 强制请求一次场景重绘
  692. // if (viewer && viewer.scene) {
  693. // viewer.scene.requestRender();
  694. // }
  695. // console.log("相机高度 (椭球体高):", height, "米");
  696. // });
  697. // 创建事件处理器
  698. const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
  699. handler.setInputAction(function (movement) {
  700. var position = movement.position;
  701. var pickedObject = viewer.scene.pick(position);
  702. if (pickedObject && pickedObject.id === entity) {
  703. console.log("你点击了标签或模型!", entity);
  704. // console.log("标签或模型数据!", val);
  705. that.modelVal = model;
  706. // 找到实体,显示包含实体信息的弹框
  707. that.showRightClickPopup(position, model);
  708. return;
  709. }
  710. }, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
  711. // 使用高级灯牌生成器
  712. const advancedGenerator = new AdvancedBillboardGenerator();
  713. let statusItems = [
  714. {
  715. label: "功率",
  716. value: `${
  717. this.isFakeData
  718. ? this.randomNum(100, 1000)
  719. : model.actualPower.toFixed(2)
  720. }MW`,
  721. },
  722. {
  723. label: "风速",
  724. value: `${
  725. this.isFakeData
  726. ? this.randomNum(100, 1000)
  727. : model.windSpeed.toFixed(1)
  728. }m/s`,
  729. },
  730. {
  731. label: "转速",
  732. value: `${
  733. this.isFakeData
  734. ? this.randomNum(100, 1000)
  735. : model.rotateSpeed.toFixed(1)
  736. }p`,
  737. },
  738. ];
  739. const billboardImage = advancedGenerator.generateAdvancedBillboard({
  740. title: name,
  741. statusItems,
  742. borderGradient: ["#ccc", "#ccc"],
  743. backgroundColor: "transparent",
  744. padding: 6,
  745. titleHeight: 22,
  746. itemHeight: 17,
  747. titleColor: "#1890ff",
  748. titleStrokeStyle: "#000",
  749. titleStrokeWidth: 1,
  750. labelStrokeStyle: "#000",
  751. labelStrokeWidth: 1,
  752. valueStrokeStyle: "#000",
  753. valueStrokeWidth: 1,
  754. });
  755. let entityxy =
  756. this.entityxyArray.find((findEle) => {
  757. return findEle.fjbh === model.fjbh;
  758. }) || null;
  759. if (entityxy) {
  760. entityxy.billboard.image._value = billboardImage;
  761. } else {
  762. entityxy = viewer.entities.add({
  763. name,
  764. position,
  765. billboard: {
  766. image: billboardImage,
  767. scale: 1,
  768. verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 原来已经是CENTER,现在改为BOTTOM
  769. pixelOffset: new Cesium.Cartesian2(80, -50), // 原来是-20,现在改为30,向上移动
  770. eyeOffset: new Cesium.Cartesian3(0, 0, 0), // 保持固定大小
  771. heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
  772. scaleByDistance: new Cesium.NearFarScalar(
  773. 6000, // 5010米内
  774. 1.2, // 1倍大小
  775. 9000, // 10000米外
  776. 1 // 0.1倍大小
  777. ),
  778. // 透明度渐变
  779. // translucencyByDistance: new Cesium.NearFarScalar(
  780. // 6000, // 500米内
  781. // 1.0, // 完全不透明
  782. // 9000, // 3000米外
  783. // 0.5 // 完全透明
  784. // ),
  785. },
  786. });
  787. entityxy.fjbh = model.fjbh;
  788. this.entityxyArray.push(entityxy);
  789. }
  790. },
  791. // 右键展示元素
  792. showRightClickPopup(screenPosition, val) {
  793. // 创建或获取弹框元素
  794. var popup = document.getElementById("rightClickPopup");
  795. if (!popup) {
  796. popup = document.createElement("div");
  797. popup.id = "rightClickPopup";
  798. popup.style.position = "absolute";
  799. // popup.style.backgroundColor = 'white';
  800. popup.style.border = "1px solid rgba(255, 255, 255, 0.2)";
  801. popup.style.borderRadius = "4px";
  802. popup.style.padding = "10px";
  803. popup.style.boxShadow = "0 2px 10px rgba(0,0,0,0.2)";
  804. popup.style.backdropFilter = `blur(10px)`;
  805. popup.style.zIndex = "1000";
  806. popup.style.pointerEvents = "auto"; // 确保弹框可交互
  807. // 添加一个最小宽度,避免内容过短
  808. popup.style.minWidth = "100px";
  809. // 可选:添加箭头指向点击点
  810. // 这需要更复杂的 CSS 或额外的 DOM 元素
  811. document.body.appendChild(popup);
  812. }
  813. // <span class="popup-menu-item" data-message-type="model" style="cursor:pointer">模型解构</span>
  814. let content = `
  815. <div style="display: flex;gap: 10px;flex-direction: column;text-align: center;color: #fff">
  816. <span class="popup-menu-item" data-message-type="basic" style="cursor:pointer">基本信息</span>
  817. <span class="popup-menu-item" data-message-type="video" style="cursor:pointer">视频监控</span>
  818. <span class="popup-menu-item" data-message-type="problem" style="cursor:pointer">故障详情</span>
  819. <span class="popup-menu-item" data-message-type="third" style="cursor:pointer">模型解构</span>
  820. </div>
  821. `;
  822. // 设置弹框内容
  823. popup.innerHTML = content;
  824. // --- 关键修正:准确计算弹框位置 ---
  825. // 1. 获取 Cesium 画布 (Canvas) 相对于视口 (viewport) 的位置
  826. var canvas = this.viewer.scene.canvas;
  827. var canvasRect = canvas.getBoundingClientRect();
  828. // console.log('Canvas Rect:', canvasRect); // 调试用
  829. // 2. 计算弹框左上角在页面中的绝对 X 坐标
  830. // screenPosition.x 是相对于画布左上角的 X 偏移
  831. // canvasRect.left 是画布左上角相对于浏览器视口左上角的 X 偏移
  832. // window.pageXOffset 是页面水平滚动的距离
  833. var popupLeft = canvasRect.left + screenPosition.x + window.pageXOffset;
  834. // 3. 计算弹框左上角在页面中的绝对 Y 坐标
  835. var popupTop = canvasRect.top + screenPosition.y + window.pageYOffset;
  836. // --- 可选:添加偏移量,避免完全覆盖鼠标指针 ---
  837. // 例如,向下和向右偏移 10px
  838. var offsetX = 10;
  839. var offsetY = 10;
  840. popupLeft += offsetX;
  841. popupTop += offsetY;
  842. // --- 可选:边界检查,防止弹框超出屏幕 ---
  843. var popupWidth = popup.offsetWidth || 150; // 确保有宽度
  844. var popupHeight = popup.offsetHeight || 50; // 确保有高度
  845. var windowWidth = window.innerWidth;
  846. var windowHeight = window.innerHeight;
  847. // 如果弹框右边会超出窗口,则左移
  848. if (popupLeft + popupWidth > windowWidth) {
  849. popupLeft = windowWidth - popupWidth - 10; // 靠右留 10px 边距
  850. }
  851. // 如果弹框底边会超出窗口,则上移
  852. if (popupTop + popupHeight > windowHeight) {
  853. popupTop = windowHeight - popupHeight - 10; // 靠下留 10px 边距
  854. }
  855. // 如果左边超出 (不太可能,但安全起见)
  856. if (popupLeft < 0) popupLeft = 10;
  857. // 如果顶边超出
  858. if (popupTop < 0) popupTop = 10;
  859. // --- 设置最终位置 ---
  860. popup.style.left = popupLeft + "px";
  861. popup.style.top = popupTop + "px";
  862. // console.log('Popup Position:', { left: popupLeft, top: popupTop }); // 调试用
  863. // 显示弹框
  864. popup.style.display = "block";
  865. // --- 隐藏弹框的逻辑 (保持不变) ---
  866. function hidePopup(event) {
  867. // 检查点击是否发生在弹框内部,如果是,则不隐藏
  868. if (event && popup.contains(event.target)) {
  869. return;
  870. }
  871. popup.style.display = "none";
  872. document.removeEventListener("click", hidePopup);
  873. document.removeEventListener("keydown", keyDownHandler);
  874. }
  875. function keyDownHandler(event) {
  876. if (event.key === "Escape") {
  877. hidePopup();
  878. }
  879. }
  880. let that = this;
  881. that.showBasicMsg = false;
  882. that.showVideoMsg = false;
  883. that.showProblemMsg = false;
  884. that.showModelMsg = false;
  885. function popupMenuItemClick(event) {
  886. // 检查点击的是否是菜单项
  887. if (event.target.classList.contains("popup-menu-item")) {
  888. event.stopPropagation(); // 阻止冒泡,防止弹框立即关闭
  889. var messageType = event.target.dataset.messageType; // 获取 data-message-type
  890. console.log("点击了菜单项:", messageType);
  891. // 调用您的 showMessage 函数
  892. that.showMessage(messageType, val);
  893. hidePopup();
  894. // 可选:执行后关闭弹框
  895. // hideRightClickPopup();
  896. }
  897. }
  898. // 移除旧的监听器(避免重复绑定)
  899. document.removeEventListener("click", hidePopup);
  900. document.removeEventListener("click", popupMenuItemClick);
  901. document.removeEventListener("keydown", keyDownHandler);
  902. // 添加新的监听器
  903. document.addEventListener("click", hidePopup);
  904. document.addEventListener("click", popupMenuItemClick);
  905. document.addEventListener("keydown", keyDownHandler);
  906. },
  907. showMessage(type, val) {
  908. console.log("type===>>>", type);
  909. this.windDrawer = true;
  910. this.windDrawerHeader = val.name + "数据详情";
  911. if (type === "basic") {
  912. this.windDrawerTitle = "基础信息";
  913. this.showBasicMsg = true;
  914. } else if (type === "video") {
  915. this.windDrawerTitle = "视频监控";
  916. this.showVideoMsg = true;
  917. } else if (type === "problem") {
  918. this.windDrawerTitle = "故障查看";
  919. this.showProblemMsg = true;
  920. } else if (type === "third") {
  921. this.windDrawerHeader = `${val.name}部件查询解构`;
  922. this.windDrawerTitle = "模型解构";
  923. this.showModelMsg = true;
  924. }
  925. },
  926. showwindmodel() {
  927. this.windDrawer = true;
  928. this.showBasicMsg = false;
  929. this.showVideoMsg = false;
  930. this.showProblemMsg = false;
  931. this.windDrawerHeader = "风机模型可视化解构说明";
  932. this.showModelMsg = true;
  933. },
  934. handleClose() {
  935. this.windDrawer = false;
  936. this.showBasicMsg = false;
  937. this.showVideoMsg = false;
  938. this.showProblemMsg = false;
  939. this.showModelMsg = false;
  940. },
  941. showComDia(val) {
  942. this.showcomModelDia = val;
  943. this.modelVal = null;
  944. },
  945. randomNum(minNum, maxNum) {
  946. switch (arguments.length) {
  947. case 1:
  948. return parseInt(Math.random() * minNum + 1, 10);
  949. case 2:
  950. return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
  951. default:
  952. return 0;
  953. }
  954. },
  955. csceneElliposid(viewer) {
  956. let that = this;
  957. // 获取 scene 和 ellipsoid
  958. var scene = viewer.scene;
  959. var labels = viewer.scene.primitives.add(new Cesium.LabelCollection());
  960. // 创建 ScreenSpaceEventHandler
  961. var handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
  962. // 监听左键点击事件
  963. handler.setInputAction(async (click) => {
  964. // 获取点击位置的笛卡尔坐标
  965. var position = click.position;
  966. if (!position) return;
  967. // 使用 globe.pick 获取包含地形高度的坐标
  968. var ray = viewer.camera.getPickRay(position);
  969. var cartesian = viewer.scene.globe.pick(ray, viewer.scene);
  970. if (cartesian) {
  971. // 转换为地理坐标
  972. var cartographic = Cesium.Cartographic.fromCartesian(cartesian);
  973. var longitude = Cesium.Math.toDegrees(cartographic.longitude);
  974. var latitude = Cesium.Math.toDegrees(cartographic.latitude);
  975. var height = cartographic.height;
  976. // 格式化坐标
  977. var text = `经度: ${longitude.toFixed(6)}°\n纬度: ${latitude.toFixed(
  978. 6
  979. )}°`;
  980. // 创建一个标签
  981. var label = labels.add({
  982. position: cartesian,
  983. text: text,
  984. font: "14px monospace",
  985. fillColor: Cesium.Color.fromCssColorString("#1d70df"),
  986. // style: Cesium.LabelStyle.FILL_AND_OUTLINE,
  987. // outlineColor: Cesium.Color.BLACK,
  988. outlineWidth: 2,
  989. verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 标签在点的上方
  990. pixelOffset: new Cesium.Cartesian2(0, -20), // 向上偏移一点
  991. disableDepthTest: true, // 让标签始终可见(即使在地球背面)
  992. scale: 0.8,
  993. });
  994. // 5秒后移除标签
  995. setTimeout(function () {
  996. labels.remove(label);
  997. }, 5000);
  998. console.log(
  999. `点击坐标: ${longitude.toFixed(6)}, ${latitude.toFixed(
  1000. 6
  1001. )}, ${height.toFixed(2)}m`
  1002. );
  1003. }
  1004. }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  1005. },
  1006. generateUniqueId(prefix = "id") {
  1007. let idCounter = 0;
  1008. return `${prefix}-${Date.now()}-${Math.random()
  1009. .toString(36)
  1010. .substr(2, 9)}-${idCounter++}`;
  1011. },
  1012. handleInitView() {
  1013. if (this.showWindDetail) {
  1014. this.resetWindViewport();
  1015. } else {
  1016. this.resetAllStationViewport();
  1017. }
  1018. },
  1019. // 重置所有风场视角
  1020. resetAllStationViewport() {
  1021. this.viewer.camera.flyTo({
  1022. destination: Cesium.Cartesian3.fromDegrees(
  1023. 114.502778,
  1024. 35.326667,
  1025. 3000000
  1026. ),
  1027. orientation: {
  1028. heading: Cesium.Math.toRadians(0),
  1029. pitch: Cesium.Math.toRadians(-90),
  1030. roll: 0.0,
  1031. },
  1032. duration: 3.0,
  1033. });
  1034. },
  1035. // 重置风场中所有风机视角
  1036. resetWindViewport() {
  1037. let fromLon = this.$route.query.longitude * 1;
  1038. let fromLat = this.$route.query.latitude * 1;
  1039. let fromheight = this.$route.query.height * 1;
  1040. let fromname = this.$route.query.nameEn;
  1041. // 设置镜头到指定的经纬度(度)、高度(米)
  1042. this.viewer.camera.setView({
  1043. destination: Cesium.Cartesian3.fromDegrees(
  1044. fromLon, // 经度 (degrees)
  1045. fromLat, // 纬度 (degrees)
  1046. fromheight // 高度 (meters)
  1047. ),
  1048. orientation: {
  1049. heading: Cesium.Math.toRadians(0.0), // 偏航角 (方向,0 指向北方)
  1050. pitch: Cesium.Math.toRadians(-90.0), // 俯仰角 (-90 是垂直向下)
  1051. roll: 0.0, // 翻滚角
  1052. },
  1053. });
  1054. // 目标位置:经度、纬度、高度
  1055. // const targetLon = 107.034945;
  1056. // const targetLat = 37.309099;
  1057. let targetLon = null;
  1058. let targetLat = null;
  1059. this.restLatLon.forEach((it) => {
  1060. if (it.name === fromname) {
  1061. targetLon = it.longitude;
  1062. targetLat = it.latitude;
  1063. }
  1064. });
  1065. const targetHeight = 5000;
  1066. const draggableHeightTolerance = 5000; // 允许拖拽的高度范围:20,000 ~ 30,000
  1067. const minHeight = 500; // 最低高度
  1068. const maxHeight = 10000; // 最高高度
  1069. const allowedOffsetDegrees = 2; // 允许拖拽的最大偏移(经纬度)
  1070. const Lat3d = targetLat - 0.3;
  1071. const minLon = targetLon - allowedOffsetDegrees;
  1072. const maxLon = targetLon + allowedOffsetDegrees;
  1073. const minLat = Lat3d - allowedOffsetDegrees;
  1074. const maxLat = Lat3d + allowedOffsetDegrees;
  1075. let that = this;
  1076. that.viewer.camera.flyTo({
  1077. destination: Cesium.Cartesian3.fromDegrees(
  1078. targetLon,
  1079. Lat3d,
  1080. targetHeight
  1081. ),
  1082. // orientation: {
  1083. // heading: Cesium.Math.toRadians(0),
  1084. // pitch: Cesium.Math.toRadians(-90),
  1085. // roll: 0.0,
  1086. // },
  1087. orientation: {
  1088. heading: 0, //北京天安门角度 0 上海角度 1
  1089. pitch: -0.3,
  1090. roll: 0,
  1091. },
  1092. duration: 3.0,
  1093. // complete: function () {
  1094. // console.log("飞入完成,启用拖拽限制逻辑");
  1095. // enableHeightBasedDragControl();
  1096. // },
  1097. });
  1098. // ===== 控制逻辑:根据高度决定是否允许拖拽 =====
  1099. function enableHeightBasedDragControl() {
  1100. that.viewer.scene.preUpdate.addEventListener(function (scene, time) {
  1101. const camera = that.viewer.camera;
  1102. const posCartographic = camera.positionCartographic;
  1103. if (!posCartographic) return;
  1104. const currentHeight = posCartographic.height;
  1105. that.currentHeight = currentHeight;
  1106. const currentLon = Cesium.Math.toDegrees(posCartographic.longitude);
  1107. const currentLat = Cesium.Math.toDegrees(posCartographic.latitude);
  1108. // === 第一步:限制相机高度不能超出 [10000, 100000] ===
  1109. if (currentHeight < minHeight) {
  1110. // 强制拉高到 minHeight,但保持当前水平视角
  1111. const newPos = Cesium.Cartesian3.fromDegrees(
  1112. currentLon,
  1113. currentLat,
  1114. minHeight
  1115. );
  1116. camera.setView({
  1117. destination: newPos,
  1118. orientation: {
  1119. heading: camera.heading,
  1120. pitch: camera.pitch,
  1121. roll: camera.roll,
  1122. },
  1123. });
  1124. return; // 避免后续逻辑冲突
  1125. }
  1126. if (currentHeight > maxHeight) {
  1127. const newPos = Cesium.Cartesian3.fromDegrees(
  1128. currentLon,
  1129. currentLat,
  1130. maxHeight
  1131. );
  1132. camera.setView({
  1133. destination: newPos,
  1134. orientation: {
  1135. heading: camera.heading,
  1136. pitch: camera.pitch,
  1137. roll: camera.roll,
  1138. },
  1139. });
  1140. return;
  1141. }
  1142. // if (currentHeight < (maxHeight-700000)) {
  1143. // that.cancleAllLayer();
  1144. // }
  1145. // === 第二步:判断是否在“可拖拽高度区间” ===
  1146. const isInDraggableRange =
  1147. currentHeight >= targetHeight - draggableHeightTolerance &&
  1148. currentHeight <= targetHeight + draggableHeightTolerance;
  1149. if (isInDraggableRange) {
  1150. // ✅ 允许拖拽,但限制在指定范围内
  1151. const correctedLon = Cesium.Math.clamp(currentLon, minLon, maxLon);
  1152. const correctedLat = Cesium.Math.clamp(currentLat, minLat, maxLat);
  1153. if (
  1154. Math.abs(correctedLon - currentLon) > 1e-8 ||
  1155. Math.abs(correctedLat - currentLat) > 1e-8
  1156. ) {
  1157. // 越界了,纠正位置,保留当前高度和视角
  1158. camera.setView({
  1159. destination: Cesium.Cartesian3.fromDegrees(
  1160. correctedLon,
  1161. correctedLat,
  1162. currentHeight
  1163. ),
  1164. orientation: {
  1165. heading: camera.heading,
  1166. pitch: camera.pitch,
  1167. roll: camera.roll,
  1168. },
  1169. });
  1170. }
  1171. } else {
  1172. // ❌ 不在可拖拽高度:禁止平移,强制回正到目标点
  1173. const correctedPosition = Cesium.Cartesian3.fromDegrees(
  1174. targetLon,
  1175. targetLat,
  1176. currentHeight // 保留当前缩放高度
  1177. );
  1178. camera.setView({
  1179. destination: correctedPosition,
  1180. orientation: {
  1181. heading: camera.heading,
  1182. pitch: camera.pitch,
  1183. roll: camera.roll,
  1184. },
  1185. });
  1186. }
  1187. });
  1188. }
  1189. },
  1190. coverOnChange(val) {
  1191. if (val.value === "风场") {
  1192. this.switchWindLayer(val.check);
  1193. } else if (val.value === "云层") {
  1194. this.switchCloudLayer(val.check);
  1195. } else if (val.value === "降雨") {
  1196. this.switchRainLayer(val.check);
  1197. } else if (val.value === "温度") {
  1198. this.switchTemperatureLayerr(val.check);
  1199. }
  1200. },
  1201. // 提供控制函数以便在需要时停止循环
  1202. stopCycling(intervalId) {
  1203. if (intervalId) {
  1204. clearInterval(intervalId);
  1205. intervalId = null;
  1206. console.log("循环已停止");
  1207. }
  1208. },
  1209. // 切换风场图显隐
  1210. switchWindLayer() {
  1211. this.viewer.scene.screenSpaceCameraController.enableZoom = true;
  1212. if (this.rainLayer || this.rainImagesLayer.length > 0) {
  1213. this.removeRainLayer();
  1214. this.stopCycling(this.rainintervalId);
  1215. }
  1216. if (this.cloudLayer || this.cloudImagesLayer.length > 0) {
  1217. this.removeCloudLayer();
  1218. this.stopCycling(this.cloudintervalId);
  1219. }
  1220. if (this.temperatureLayer || this.tempImagesLayer.length > 0) {
  1221. this.removeTemperatureLayer();
  1222. this.stopCycling(this.tempintervalId);
  1223. }
  1224. if (this.windLayer) {
  1225. this.removeWindLayer();
  1226. } else {
  1227. this.showWindLayer();
  1228. }
  1229. },
  1230. // 添加风场图
  1231. async showWindLayer() {
  1232. if (!this.windLayer) {
  1233. this.windLayer = new WindLayer(windGridData, {
  1234. particleSize: 2.0,
  1235. particleOpacity: 0.6,
  1236. particleSpeed: 0.01,
  1237. maxVelocity: 25, // 风速最大值(用于颜色映射和速度缩放)
  1238. minVelocity: 0, // 风速最小值阈值(低于此值不显示粒子)
  1239. colorScale: [
  1240. "rgb(36,104, 180)",
  1241. "rgb(60,157, 194)",
  1242. "rgb(128,205,193)",
  1243. "rgb(151,218,168)",
  1244. "rgb(198,231,181)",
  1245. "rgb(238,247,217)",
  1246. "rgb(255,238,159)",
  1247. "rgb(252,217,125)",
  1248. "rgb(255,182,100)",
  1249. "rgb(252,150,75)",
  1250. "rgb(250,112,52)",
  1251. "rgb(245,64,32)",
  1252. "rgb(237,45,28)",
  1253. "rgb(220,24,32)",
  1254. "rgb(180,0,35)",
  1255. ], // 颜色强度缩放
  1256. frameRate: 15,
  1257. fadeOpacity: 0.995,
  1258. particleAge: 150,
  1259. maxAge: 60,
  1260. globalAlpha: 0.8,
  1261. velocityScale: 1 / 30, // 粒子移动速度缩放因子(控制动画快慢)
  1262. paths: 500,
  1263. lineWidth: 2,
  1264. });
  1265. this.windLayer.addTo(this.viewer);
  1266. }
  1267. },
  1268. // 切换卫星云图显隐
  1269. switchCloudLayer(val) {
  1270. if (this.windLayer) {
  1271. this.removeWindLayer();
  1272. }
  1273. if (this.rainLayer || this.rainImagesLayer.length > 0) {
  1274. this.removeRainLayer();
  1275. this.stopCycling(this.rainintervalId);
  1276. }
  1277. if (this.temperatureLayer || this.tempImagesLayer.length > 0) {
  1278. this.removeTemperatureLayer();
  1279. this.stopCycling(this.tempintervalId);
  1280. }
  1281. if (!val || this.cloudLayer || this.cloudImagesLayer.length > 0) {
  1282. this.removeCloudLayer();
  1283. this.stopCycling(this.cloudintervalId);
  1284. } else {
  1285. this.showCloudLayer();
  1286. }
  1287. },
  1288. // 切换降雨图显隐
  1289. switchRainLayer() {
  1290. this.viewer.scene.screenSpaceCameraController.enableZoom = true;
  1291. if (this.windLayer) {
  1292. this.removeWindLayer();
  1293. }
  1294. if (this.cloudLayer || this.cloudImagesLayer.length > 0) {
  1295. this.removeCloudLayer();
  1296. this.stopCycling(this.cloudintervalId);
  1297. }
  1298. if (this.temperatureLayer || this.tempImagesLayer.length > 0) {
  1299. this.removeTemperatureLayer();
  1300. this.stopCycling(this.tempintervalId);
  1301. }
  1302. if (this.rainLayer || this.rainImagesLayer.length > 0) {
  1303. this.removeRainLayer();
  1304. this.stopCycling(this.rainintervalId);
  1305. } else {
  1306. this.showRainLayer();
  1307. }
  1308. },
  1309. // 切换温度图显隐
  1310. switchTemperatureLayerr() {
  1311. this.viewer.scene.screenSpaceCameraController.enableZoom = true;
  1312. if (this.windLayer) {
  1313. this.removeWindLayer();
  1314. }
  1315. if (this.cloudLayer || this.cloudImagesLayer.length > 0) {
  1316. this.removeCloudLayer();
  1317. this.stopCycling(this.cloudintervalId);
  1318. }
  1319. if (this.rainLayer || this.rainImagesLayer.length > 0) {
  1320. this.removeRainLayer();
  1321. this.stopCycling(this.rainintervalId);
  1322. }
  1323. if (this.temperatureLayer || this.tempImagesLayer.length > 0) {
  1324. this.removeTemperatureLayer();
  1325. this.stopCycling(this.tempintervalId);
  1326. } else {
  1327. this.showTemperatureLayer();
  1328. }
  1329. },
  1330. // 显示云图
  1331. showCloudLayer() {
  1332. const imageUrls = [];
  1333. cloudJson.forEach((it) => {
  1334. imageUrls.push("/static" + it.path);
  1335. });
  1336. this.showeveryTypeImagesLayer(
  1337. imageUrls,
  1338. this.cloudintervalId,
  1339. this.cloudImagesLayer
  1340. );
  1341. },
  1342. //显示降雨图
  1343. showRainLayer() {
  1344. const imageUrls = [];
  1345. rainJson.forEach((it) => {
  1346. imageUrls.push("/static" + it.path);
  1347. });
  1348. this.showeveryTypeImagesLayer(
  1349. imageUrls,
  1350. this.rainintervalId,
  1351. this.rainImagesLayer
  1352. );
  1353. this.csceneElliposid(this.viewer, "rain");
  1354. },
  1355. //显示温度图
  1356. showTemperatureLayer() {
  1357. const imageUrls = [];
  1358. tempJson.forEach((it) => {
  1359. imageUrls.push("/static" + it.path);
  1360. });
  1361. this.showeveryTypeImagesLayer(
  1362. imageUrls,
  1363. this.tempintervalId,
  1364. this.tempImagesLayer
  1365. );
  1366. this.csceneElliposid(this.viewer, "temp");
  1367. },
  1368. async showeveryTypeImagesLayer(imageUrls, intervalId, ImagesLayers) {
  1369. // 存储所有图片图层的数组
  1370. let imageLayers = [];
  1371. // 当前显示的图片索引
  1372. let currentImageIndex = -1; // 初始为-1,表示没有图片显示
  1373. // 创建所有图片图层并添加到Viewer,初始时全部隐藏
  1374. await imageUrls.forEach((url) => {
  1375. const provider = new Cesium.SingleTileImageryProvider({
  1376. url: url,
  1377. // url: URL.createObjectURL(url),
  1378. rectangle: Cesium.Rectangle.fromDegrees(-180.0, -90.0, 180.0, 90.0), // 全球覆盖
  1379. tileWidth: 1440, // 根据你的图片实际宽度修改
  1380. tileHeight: 721,
  1381. // 如果你的图片只覆盖特定区域,请修改rectangle参数
  1382. });
  1383. const Layer = this.viewer.imageryLayers.addImageryProvider(provider);
  1384. Layer.alpha = 0.8; // 透明度
  1385. Layer.brightness = 1; // 亮度
  1386. Layer.contrast = 1; // 对比度
  1387. Layer.show = false; // 初始隐藏
  1388. imageLayers.push(Layer);
  1389. ImagesLayers.push(Layer);
  1390. });
  1391. function showNextImage() {
  1392. // 隐藏当前图片
  1393. if (currentImageIndex >= 0 && currentImageIndex < imageLayers.length) {
  1394. imageLayers[currentImageIndex].show = false;
  1395. }
  1396. // 计算下一张图片的索引
  1397. currentImageIndex = (currentImageIndex + 1) % imageLayers.length;
  1398. // 显示下一张图片
  1399. imageLayers[currentImageIndex].show = true;
  1400. // imageLayers[currentImageIndex + 1].show = true;
  1401. console.log("当前显示图片: " + imageUrls[currentImageIndex]);
  1402. }
  1403. // 设置切换间隔(毫秒),例如每5秒切换一次
  1404. const intervalMs = 5000;
  1405. intervalId = setInterval(showNextImage, intervalMs);
  1406. // 初始显示第一张图片
  1407. showNextImage();
  1408. },
  1409. // 移除风场图
  1410. removeWindLayer() {
  1411. if (this.windLayer) {
  1412. // this.windLayer.destroy();
  1413. this.windLayer.remove();
  1414. this.windLayer = null;
  1415. }
  1416. },
  1417. // 移除卫星云图
  1418. removeCloudLayer() {
  1419. if (this.cloudLayer) {
  1420. this.tagMsg = null;
  1421. this.viewer.imageryLayers.remove(this.cloudLayer);
  1422. this.cloudLayer = null;
  1423. }
  1424. if (this.cloudImagesLayer.length > 0) {
  1425. this.cloudImagesLayer.forEach((it) => {
  1426. this.viewer.imageryLayers.remove(it);
  1427. });
  1428. this.cloudImagesLayer = [];
  1429. }
  1430. if (this.imageryProviderV) {
  1431. this.viewer.imageryLayers.remove(this.imageryProviderV);
  1432. this.imageryProviderV = null;
  1433. }
  1434. },
  1435. // 移除降雨图
  1436. removeRainLayer() {
  1437. if (this.rainLayer) {
  1438. this.tagMsg = null;
  1439. this.viewer.imageryLayers.remove(this.rainLayer);
  1440. this.rainLayer = null;
  1441. this.setMapImageryProvider();
  1442. this.handlerAction.removeInputAction(
  1443. Cesium.ScreenSpaceEventType.LEFT_CLICK
  1444. );
  1445. }
  1446. if (this.rainImagesLayer.length > 0) {
  1447. this.rainImagesLayer.forEach((it) => {
  1448. this.viewer.imageryLayers.remove(it);
  1449. });
  1450. this.rainImagesLayer = [];
  1451. }
  1452. if (this.imageryProviderV) {
  1453. this.viewer.imageryLayers.remove(this.imageryProviderV);
  1454. this.imageryProviderV = null;
  1455. }
  1456. },
  1457. // 移除温度图
  1458. removeTemperatureLayer() {
  1459. if (this.temperatureLayer) {
  1460. this.tagMsg = null;
  1461. this.viewer.imageryLayers.remove(this.temperatureLayer);
  1462. this.temperatureLayer = null;
  1463. this.setMapImageryProvider();
  1464. this.handlerAction.removeInputAction(
  1465. Cesium.ScreenSpaceEventType.LEFT_CLICK
  1466. );
  1467. }
  1468. if (this.tempImagesLayer.length > 0) {
  1469. this.tempImagesLayer.forEach((it) => {
  1470. this.viewer.imageryLayers.remove(it);
  1471. });
  1472. this.tempImagesLayer = [];
  1473. }
  1474. if (this.imageryProviderV) {
  1475. this.viewer.imageryLayers.remove(this.imageryProviderV);
  1476. this.imageryProviderV = null;
  1477. }
  1478. },
  1479. //取消所有图层加载
  1480. cancleAllLayer() {
  1481. if (this.windLayer) {
  1482. this.removeWindLayer();
  1483. }
  1484. if (this.rainLayer || this.rainImagesLayer.length > 0) {
  1485. this.removeRainLayer();
  1486. this.stopCycling(this.rainintervalId);
  1487. }
  1488. if (this.cloudLayer || this.cloudImagesLayer.length > 0) {
  1489. this.removeCloudLayer();
  1490. this.stopCycling(this.cloudintervalId);
  1491. }
  1492. if (this.temperatureLayer || this.tempImagesLayer.length > 0) {
  1493. this.removeTemperatureLayer();
  1494. this.stopCycling(this.tempintervalId);
  1495. }
  1496. },
  1497. switchLayer() {
  1498. this.$router.push({
  1499. path: "/",
  1500. });
  1501. },
  1502. menuComTSty(val) {
  1503. this.menuComTStyB = val;
  1504. },
  1505. },
  1506. };
  1507. </script>
  1508. <style lang="less" scoped>
  1509. .dataLoading {
  1510. width: 100vw;
  1511. height: 100vh;
  1512. background: rgba(0, 0, 0, 0.5);
  1513. z-index: 999;
  1514. position: fixed;
  1515. .loadText {
  1516. position: absolute;
  1517. top: 50%;
  1518. left: 50%;
  1519. transform: translate(-50%, -50%);
  1520. background: rgba(255, 255, 255, 0.7);
  1521. padding: 15px 20px;
  1522. border-radius: 6px;
  1523. color: black;
  1524. font-size: 14px;
  1525. font-weight: bold;
  1526. }
  1527. }
  1528. .mapBox {
  1529. width: 100%;
  1530. height: 100%;
  1531. position: relative;
  1532. box-sizing: content-box;
  1533. overflow: hidden;
  1534. .menuComT {
  1535. position: fixed;
  1536. bottom: 400px;
  1537. left: 20px;
  1538. }
  1539. .menuComTSty {
  1540. position: fixed;
  1541. bottom: 20px;
  1542. left: 20px;
  1543. }
  1544. }
  1545. </style>
  1546. <style lang="less">
  1547. .el-overlay {
  1548. background-color: transparent !important;
  1549. .windModelDrawer {
  1550. width: 80% !important;
  1551. backdrop-filter: blur(15px) !important;
  1552. background: rgba(255, 255, 255, 0.8) !important;
  1553. border-radius: 10px 0 0 10px !important;
  1554. .el-drawer__body {
  1555. overflow: hidden;
  1556. padding-top: 0;
  1557. }
  1558. }
  1559. }
  1560. .windDrawerCla {
  1561. .line {
  1562. display: flex;
  1563. flex-direction: row;
  1564. align-items: center;
  1565. justify-content: space-between;
  1566. width: 100%;
  1567. margin-bottom: 10px;
  1568. .leftContent {
  1569. width: 242px;
  1570. height: 41px;
  1571. display: flex;
  1572. align-items: center;
  1573. background: url("@/assets/cesiumImg/title_left_bg.png") no-repeat;
  1574. span {
  1575. font-size: 16px;
  1576. font-family: Microsoft YaHei;
  1577. font-weight: 400;
  1578. color: #ffffff;
  1579. margin-left: 25px;
  1580. }
  1581. }
  1582. }
  1583. .jcxx,
  1584. .gzck {
  1585. height: 100%;
  1586. }
  1587. .spjk,
  1588. .third {
  1589. height: 80vh;
  1590. }
  1591. }
  1592. </style>