cesium.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. <template>
  2. <div class="mapBox">
  3. <div id="cesiumContainer" style="width: 100%; height: 100vh"></div>
  4. <div class="menuBox" :class="allyShow ? '' : 'switch'">
  5. <div class="item">
  6. <span>图源:</span>
  7. <el-select
  8. v-model="basicMapId"
  9. size="small"
  10. style="width: 120px"
  11. @change="setMapImageryProvider"
  12. >
  13. <el-option
  14. v-for="item in basicMapList"
  15. :key="item.id"
  16. :label="item.name"
  17. :value="item.id"
  18. />
  19. </el-select>
  20. </div>
  21. <div class="item">
  22. <el-button type="primary" size="small" @click="resetViewport()"
  23. >初始化视角</el-button
  24. >
  25. </div>
  26. <div class="item">
  27. <el-button
  28. size="small"
  29. :type="windLayer ? 'danger' : 'primary'"
  30. @click="switchWindLayer"
  31. >{{ windLayer ? "关闭" : "显示" }}风场图</el-button
  32. >
  33. </div>
  34. <div class="item">
  35. <el-button
  36. size="small"
  37. :type="cloudLayer ? 'danger' : 'primary'"
  38. @click="switchCloudLayer"
  39. >{{ cloudLayer ? "关闭" : "显示" }}云图</el-button
  40. >
  41. </div>
  42. <div class="item">
  43. <el-button
  44. size="small"
  45. :type="rainLayer ? 'danger' : 'primary'"
  46. @click="switchRainLayer"
  47. >{{ rainLayer ? "关闭" : "显示" }}降雨图</el-button
  48. >
  49. </div>
  50. <div class="item">
  51. <el-button
  52. size="small"
  53. :type="rainLayer ? 'danger' : 'primary'"
  54. @click="switchTemperatureLayerr"
  55. >{{ rainLayer ? "关闭" : "显示" }}温度图</el-button
  56. >
  57. </div>
  58. <div class="item">
  59. <el-button size="small" type="primary" @click="switchTopographicMap"
  60. >风机</el-button
  61. >
  62. </div>
  63. <el-tooltip
  64. class="box-item"
  65. effect="dark"
  66. :content="`点击${allyShow ? '隐藏' : '常显'}菜单栏`"
  67. placement="bottom-end"
  68. >
  69. <el-icon
  70. style="margin-left: 20px"
  71. size="20px"
  72. :color="allyShow ? '#1890ff' : '#f25656'"
  73. @click="allyShow = !allyShow"
  74. >
  75. <House
  76. :style="`transform: rotate(${
  77. allyShow ? -45 : 45
  78. }deg); transition: 0.2s; cursor: pointer;`"
  79. />
  80. </el-icon>
  81. </el-tooltip>
  82. </div>
  83. <div
  84. class="tag"
  85. :style="`left:${userClickLeft}px;top:${userClickTop}px`"
  86. v-if="tagMsg || tagMsg === ''"
  87. >
  88. <el-icon class="is-loading" v-if="tagMsg === ''">
  89. <Loading />
  90. </el-icon>
  91. <span v-else>{{ tagMsg || "" }}</span>
  92. </div>
  93. <div class="devInfoBox" v-if="showDevInfoBox">
  94. <div class="item">===&nbsp;帧率与内存&nbsp;===</div>
  95. <div class="item">运行帧率:&nbsp;{{ fps }}</div>
  96. <div class="item">响应时长:&nbsp;{{ ms }}</div>
  97. <div class="item">内存占用:&nbsp;{{ jsHeapSize }}</div>
  98. <template v-if="gVendor || gRenderer">
  99. <div class="item" style="margin-top: 12px">
  100. ====&nbsp;显卡信息&nbsp;====
  101. </div>
  102. <el-tooltip
  103. effect="dark"
  104. :content="gVendor"
  105. placement="top-end"
  106. v-if="gVendor"
  107. >
  108. <div class="item">制造商:&nbsp;{{ gVendor }}</div>
  109. </el-tooltip>
  110. <el-tooltip
  111. effect="dark"
  112. :content="gRenderer"
  113. placement="top-end"
  114. v-if="gRenderer"
  115. >
  116. <div class="item">型号:&nbsp;{{ gRenderer }}</div>
  117. </el-tooltip>
  118. </template>
  119. </div>
  120. </div>
  121. <el-dialog
  122. class="modelDialog"
  123. v-model="showFjDialog"
  124. title="Tips"
  125. top="50px"
  126. width="80%"
  127. :before-close="handleClose"
  128. >
  129. <el-tabs
  130. v-model="showFjDialogActiveName"
  131. style="width: 100%"
  132. type="border-card"
  133. >
  134. <el-tab-pane label="基础信息" name="jcxx">基础信息</el-tab-pane>
  135. <el-tab-pane label="视频监控" name="spjk">视频监控</el-tab-pane>
  136. <el-tab-pane label="故障查看" name="gzck">故障查看</el-tab-pane>
  137. <el-tab-pane label="模型解构" name="mxjg">
  138. <ModelUnpack v-if="showFjDialogActiveName === 'mxjg'" />
  139. </el-tab-pane>
  140. </el-tabs>
  141. </el-dialog>
  142. </template>
  143. <script>
  144. // import * as Cesium from "../Cesium";
  145. // import "../Cesium/Widgets/widgets.css";
  146. import * as Cesium from "cesium";
  147. import "cesium/Build/Cesium/Widgets/widgets.css";
  148. import Windy from "../assets/wind/Windy_source.js";
  149. import basicGeoJson from "../assets/geoJson/basic.json";
  150. import windLineJson from "../assets/geoJson/windLine_2017121300.json";
  151. import axios from "axios";
  152. import ModelUnpack from "@/components/modelUnpack.vue";
  153. export default {
  154. name: "CesiumMap",
  155. components: {
  156. ModelUnpack,
  157. },
  158. data() {
  159. return {
  160. showFjDialog: false,
  161. showFjDialogActiveName: "jcxx",
  162. checkMode: false, // 调试模式
  163. allyShow: false,
  164. viewer: null,
  165. windLayer: null, // 风场图
  166. windLayerTimmer: null, // 风场图计时器
  167. cloudLayer: null, // 卫星云图
  168. rainLayer: null, // 降雨图
  169. basicMapId: "gaodeyingxiang", // 地球底图 ID
  170. // 地球底图数组
  171. basicMapList: [
  172. {
  173. id: "gaodeyingxiang",
  174. name: "高德影像地图",
  175. url: "https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}",
  176. minimumLevel: 3,
  177. maximumLevel: 18,
  178. credit: "basicMap",
  179. },
  180. {
  181. id: "gaodeshiliang",
  182. name: "高德矢量地图",
  183. url: "https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
  184. minimumLevel: 3,
  185. maximumLevel: 18,
  186. credit: "basicMap",
  187. },
  188. {
  189. id: "carto",
  190. name: "Carto地图",
  191. url: "http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
  192. credit: "basicMap",
  193. },
  194. ],
  195. earthLayer: [],
  196. userClickLeft: 0,
  197. userClickTop: 0,
  198. tagMsg: null,
  199. systemInfoTimmer: null,
  200. showDevInfoBox: true,
  201. fps: "", // 设备帧率
  202. ms: "", // 设备响应时间
  203. jsHeapSize: "", // 内存占用
  204. gVendor: "",
  205. gRenderer: "",
  206. labelLayer: null, // 城市名称 label 集合
  207. loadDone: false, // 地球首次加载滚动到 reset 位置是否完成
  208. };
  209. },
  210. mounted() {
  211. this.initEventListener();
  212. this.initCesium();
  213. if (this.showDevInfoBox) {
  214. this.initSystemInfo();
  215. }
  216. this.getData(106.169866, 38.46637);
  217. },
  218. unmounted() {
  219. if (this.windLayer !== null) {
  220. clearInterval(this.windLayerTimmer);
  221. this.windLayer.removeLines();
  222. this.windLayer = null;
  223. this.windLayerTimmer = null;
  224. }
  225. clearInterval(this.systemInfoTimmer);
  226. this.systemInfoTimmer = null;
  227. },
  228. methods: {
  229. getData(lat, lon) {
  230. // axios
  231. // .get(
  232. // `https://api.waqi.info/feed/geo:${lat};${lon}/?token=904a1bc6edf77c428347f2fe54cf663bcffaec21`
  233. // )
  234. // .then((res) => {
  235. // console.log(1122, res);
  236. // });
  237. },
  238. // 初始化一些监听事件
  239. initEventListener() {
  240. const mapBox = document.querySelector(".mapBox");
  241. mapBox.addEventListener("click", (e) => {
  242. const rect = mapBox.getBoundingClientRect();
  243. this.userClickLeft = (e.clientX - rect.left).toFixed(0);
  244. this.userClickTop = (e.clientY - rect.top + 20).toFixed(0);
  245. });
  246. },
  247. // 初始化地球
  248. async initCesium() {
  249. // 需要从 https://cesium.com/ion/signup 获取
  250. Cesium.Ion.defaultAccessToken =
  251. "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwYTQwNDk3MC05YTZkLTQ2ZTEtODc0MS1lZTFkYjFlOTFmNmQiLCJpZCI6MTcyNDQ1LCJpYXQiOjE3NTQ4ODA4MzF9.KnhENYiHxNwTkhTWRA-lHqG59coLVT2FsIyOru2TV3E";
  252. // 修改 Cesium 默认地图视角为宁夏,狗东西没效果不知道为什么
  253. // Cesium.Camera.DEFAULT_VIEW_RACTANGLE = Cesium.Rectangle.fromDegrees(
  254. // 104.17,
  255. // 35.14,
  256. // 107.72,
  257. // 39.23
  258. // );
  259. const viewer = new Cesium.Viewer("cesiumContainer", {
  260. geocoder: false, // 地址搜索控件
  261. homeButton: false, // 返回地图初始位置控件
  262. infoBox: false, // 地图默认的信息控件
  263. sceneModePicker: false, // 场景模式切换控件
  264. baseLayerPicker: false, // 底图切换控件
  265. navigationHelpButton: false, // 帮助控件
  266. animation: false, // 动画控制控件
  267. timeline: false, // 时间线控件
  268. fullscreenButton: false, // 全屏按钮控件
  269. imageryProvider: false, // 是否显示 Cesium 默认地图的底图
  270. vrButton: false,
  271. selectionIndicator: false,
  272. shouldAnimate: true,
  273. // terrainProvider: await Cesium.createWorldTerrainAsync({
  274. // requestVertexNormals: true,
  275. // requestWaterMask: true,
  276. // }),
  277. // terrainProvider: new Cesium.CesiumTerrainProvider({
  278. // url: "/static/layer.json", // 对应 public/terrain-data 目录
  279. // requestVertexNormals: true, // 保留法线数据(光照效果)
  280. // requestWaterMask: false, // 本地地形通常无水面效果(需自定义)
  281. // }),
  282. });
  283. // 隐藏 Cesium Logo
  284. viewer.cesiumWidget.creditContainer.style.display = "none";
  285. this.viewer = viewer;
  286. this.setMapImageryProvider();
  287. this.initGeoJsonData();
  288. // 添加一些3D模型
  289. this.addModel(
  290. "./static/model/fengjiduli/model.glb",
  291. "风机",
  292. 106.169866,
  293. 38.46637
  294. );
  295. },
  296. addModel(uri, name, lon, lat) {
  297. const hpRoll = new Cesium.HeadingPitchRoll(90.0, 0.0, 0.0);
  298. const position = Cesium.Cartesian3.fromDegrees(lon, lat);
  299. const orientation = Cesium.Transforms.headingPitchRollQuaternion(
  300. position,
  301. hpRoll
  302. );
  303. this.viewer.entities.add({
  304. name, // 模型名称
  305. position, // 模型位置
  306. orientation, // 模型朝向
  307. model: {
  308. uri,
  309. scale: 100.0,
  310. // 模型贴地
  311. heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
  312. },
  313. });
  314. },
  315. // 初始化Cesium内部鼠标事件
  316. initEventInputAction() {
  317. this.$nextTick(() => {
  318. const viewer = this.viewer;
  319. // 添加点击事件显示坐标
  320. viewer.screenSpaceEventHandler.setInputAction((movement) => {
  321. const ray = this.viewer.camera.getPickRay(movement.position);
  322. if (!ray) {
  323. this.tagMsg = null;
  324. console.log("无法获取射线");
  325. return;
  326. }
  327. const position = this.viewer.scene.globe.pick(ray, this.viewer.scene);
  328. if (!position) {
  329. this.tagMsg = null;
  330. console.log("未找到地球表面交点");
  331. return;
  332. }
  333. const cartographic = Cesium.Cartographic.fromCartesian(position);
  334. if (!cartographic) {
  335. this.tagMsg = null;
  336. console.log("坐标转换失败");
  337. return;
  338. }
  339. this.getClickCloudOpacity(cartographic);
  340. this.getLocationData(cartographic);
  341. return;
  342. const cartesian = viewer.camera.pickEllipsoid(
  343. movement.position,
  344. viewer.scene.globe.ellipsoid
  345. );
  346. if (cartesian) {
  347. const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
  348. const lon = Cesium.Math.toDegrees(cartographic.longitude).toFixed(
  349. 5
  350. );
  351. const lat = Cesium.Math.toDegrees(cartographic.latitude).toFixed(5);
  352. viewer.entities.removeAll();
  353. viewer.entities.add({
  354. position: cartesian,
  355. point: {
  356. pixelSize: 10,
  357. color: Cesium.Color.RED,
  358. },
  359. label: {
  360. text: `经度: ${lon}°, 纬度: ${lat}°`,
  361. font: '16px "Microsoft YaHei"',
  362. fillColor: Cesium.Color.WHITE,
  363. outlineColor: Cesium.Color.BLACK,
  364. outlineWidth: 2,
  365. style: Cesium.LabelStyle.FILL_AND_OUTLINE,
  366. pixelOffset: new Cesium.Cartesian2(0, -30),
  367. },
  368. });
  369. }
  370. }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  371. // 监听鼠标滚轮事件
  372. viewer.screenSpaceEventHandler.setInputAction((wheelment) => {
  373. this.tagMsg = null;
  374. }, Cesium.ScreenSpaceEventType.WHEEL);
  375. // 监听鼠标移动事件
  376. // viewer.screenSpaceEventHandler.setInputAction((movement) => {
  377. // this.getHoverCityLabel(movement);
  378. // }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
  379. });
  380. },
  381. // 获取鼠标划过位置的城市名称 label
  382. getHoverCityLabel(movement) {
  383. const picked = this.viewer.scene.pick(movement.startPosition);
  384. const label = picked ? picked.primitive : null;
  385. if (label) {
  386. this.viewer.scene.canvas.style.cursor = "pointer";
  387. } else {
  388. this.viewer.scene.canvas.style.cursor = "default";
  389. }
  390. this.labelLayer._labels.forEach((ele) => {
  391. if (ele.id === label?.id) {
  392. label.fillColor = Cesium.Color.YELLOW;
  393. label.outlineColor = Cesium.Color.BLACK;
  394. } else {
  395. ele.fillColor = Cesium.Color.fromCssColorString("#000");
  396. ele.outlineColor = Cesium.Color.WHITE;
  397. }
  398. });
  399. },
  400. // 根据经纬度获取周围一定范围城市基础信息
  401. getLocationData({ latitude, longitude }) {
  402. const lat = Cesium.Math.toDegrees(latitude);
  403. const lon = Cesium.Math.toDegrees(longitude);
  404. axios
  405. .get(
  406. `/ventusky/ventusky_location.json.php?lat=${lat}&lon=${lon}&zoom=${this.getZoomLevel()}`
  407. )
  408. .then((res) => {
  409. console.log(111, res.data);
  410. // https://api.waqi.info/feed/geo:35.3286804492;108.9025100708/?token=904a1bc6edf77c428347f2fe54cf663bcffaec21
  411. // res.data.city
  412. });
  413. },
  414. // 获取 zoom 级别
  415. getZoomLevel() {
  416. let zoomLevel = 0;
  417. const tilesToRender = this.viewer.scene.globe._surface._tilesToRender;
  418. if (tilesToRender.length !== 0) {
  419. zoomLevel = tilesToRender[0].level;
  420. }
  421. return zoomLevel - 1;
  422. },
  423. // 初始化性能监控
  424. initSystemInfo() {
  425. this.viewer.scene.debugShowFramesPerSecond = false;
  426. // 性能监控变量
  427. let lastFrameTime = performance.now();
  428. let frameCount = 0;
  429. let fps = 0;
  430. let frameTime = 0;
  431. // 获取帧率与响应时长
  432. this.viewer.scene.postRender.addEventListener(() => {
  433. const now = performance.now();
  434. const delta = now - lastFrameTime;
  435. frameCount++;
  436. // 每秒更新一次数据(避免更新太频繁)
  437. if (delta >= 1000) {
  438. fps = Math.round((frameCount * 1000) / delta);
  439. frameTime = delta / frameCount;
  440. // 更新显示
  441. this.fps = `${fps} FPS`;
  442. this.ms = `${frameTime.toFixed(1) + " ms"}`;
  443. // 重置计数器
  444. frameCount = 0;
  445. lastFrameTime = now;
  446. }
  447. });
  448. // 获取内存占用
  449. if (window.performance && performance.memory) {
  450. const jsHeapSize = performance.memory.usedJSHeapSize / 1048576;
  451. this.jsHeapSize = `${parseInt(jsHeapSize)} MB`;
  452. // const memory = performance.memory;
  453. // console.log("已分配堆内存:", memory.totalJSHeapSize / 1048576 + " MB");
  454. // console.log("已使用堆内存:", memory.usedJSHeapSize / 1048576 + " MB");
  455. // console.log("堆内存限制:", memory.jsHeapSizeLimit / 1048576 + " MB");
  456. }
  457. // 获取显卡信息
  458. const canvas = document.createElement("canvas");
  459. const gl = canvas.getContext("webgl");
  460. if (gl) {
  461. const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
  462. if (debugInfo) {
  463. const gVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
  464. const gRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
  465. // console.log("GPU厂商:", gVendor);
  466. // console.log("GPU型号:", gRenderer);
  467. this.gVendor = gVendor;
  468. this.gRenderer = gRenderer.split(",")?.[1]
  469. ? gRenderer.split(",")?.[1]
  470. : "";
  471. }
  472. }
  473. },
  474. getClickCloudOpacity(cartographic) {
  475. try {
  476. const level = this.calculateTileLevel(this.viewer);
  477. const tilingScheme = new Cesium.WebMercatorTilingScheme();
  478. // 确保 tilingScheme 有 positionToTileXY 方法
  479. if (!tilingScheme.positionToTileXY) {
  480. console.error("tilingScheme没有positionToTileXY方法");
  481. return;
  482. }
  483. if (!this.cloudLayer) {
  484. this.tagMsg = null;
  485. } else {
  486. this.tagMsg = "";
  487. }
  488. const tileXY = tilingScheme.positionToTileXY(cartographic, level);
  489. this.checkMode &&
  490. console.log(`瓦片坐标: 级别=${level}, X=${tileXY.x}, Y=${tileXY.y}`);
  491. const clickTileUrl = this.replaceTemplate(
  492. // mapSelect.url,
  493. "https://tile.openweathermap.org/map/clouds_new/{z}/{x}/{y}.png?appid=3b66d35579770393051599f8d518df4a",
  494. level,
  495. tileXY
  496. );
  497. this.checkMode && console.log(`用户点击位置瓦片url: ${clickTileUrl}`);
  498. // 存储当前瓦片信息
  499. const layer = this.viewer.imageryLayers.get(0);
  500. const provider = layer.imageryProvider;
  501. const currentTile = {
  502. x: tileXY.x,
  503. y: tileXY.y,
  504. level: level,
  505. rectangle: tilingScheme.tileXYToRectangle(tileXY.x, tileXY.y, level),
  506. size: {
  507. width: provider.tileWidth || 256,
  508. height: provider.tileHeight || 256,
  509. },
  510. };
  511. // 计算并显示在瓦片内的位置
  512. const clickPos = this.calculateTilePosition(cartographic, currentTile);
  513. if (this.cloudLayer) {
  514. this.getTileImageOpacity(
  515. clickTileUrl,
  516. clickPos.pixelX,
  517. clickPos.pixelY
  518. ).then((imgSource) => {
  519. const {
  520. rawAlpha, // 原始透明度值 (0-255)
  521. alphaPercentage, // 透明度百分比
  522. whiteScore, // 白色程度得分 (0-100)
  523. } = imgSource;
  524. this.checkMode && console.log(111, alphaPercentage);
  525. this.tagMsg = `${alphaPercentage * 2}%`;
  526. });
  527. }
  528. if (this.checkMode) {
  529. const tileRectangle = tilingScheme.tileXYToRectangle(
  530. tileXY.x,
  531. tileXY.y,
  532. level
  533. );
  534. this.highlightTile(viewer, tileRectangle);
  535. }
  536. } catch (error) {
  537. console.error("获取瓦片时出错:", error);
  538. }
  539. },
  540. // 计算瓦片级别的辅助函数
  541. calculateTileLevel(viewer) {
  542. // 方法1:根据相机高度估算
  543. const height = viewer.camera.positionCartographic.height;
  544. if (!height) return 12; // 默认值
  545. // 高度与级别的近似关系(根据实际需求调整)
  546. const level = Math.floor(20 - Math.log(height / 1000) / Math.log(2));
  547. return Math.max(0, Math.min(18, level)); // 限制在0-18级之间
  548. // 方法2:使用当前视图的细节层次
  549. // return viewer.scene.globe.maximumScreenSpaceError;
  550. },
  551. // 高亮显示瓦片的辅助函数
  552. highlightTile(viewer, rectangle) {
  553. // 移除之前的高亮
  554. viewer.entities.removeById("highlighted-tile");
  555. // 添加新的高亮
  556. viewer.entities.add({
  557. id: "highlighted-tile",
  558. rectangle: {
  559. coordinates: rectangle,
  560. material: Cesium.Color.RED.withAlpha(0.3),
  561. outline: true,
  562. outlineColor: Cesium.Color.RED,
  563. outlineWidth: 2,
  564. },
  565. });
  566. },
  567. // 计算在瓦片内的位置
  568. calculateTilePosition(cartographic, tile) {
  569. const rect = tile.rectangle;
  570. const size = tile.size;
  571. // 计算在瓦片内的归一化位置
  572. const lonNormalized =
  573. (cartographic.longitude - rect.west) / (rect.east - rect.west);
  574. const latNormalized =
  575. (cartographic.latitude - rect.south) / (rect.north - rect.south);
  576. // 转换为像素坐标(原点在左上角)
  577. const pixelX = Math.floor(lonNormalized * size.width);
  578. const pixelY = Math.floor((1 - latNormalized) * size.height); // 翻转Y轴
  579. this.checkMode && console.log(`left:${pixelX},top:${pixelY}`);
  580. return { pixelX, pixelY };
  581. },
  582. // canvas 获取地图瓦片颜色信息
  583. async getTileImageOpacity(imageUrl, left, top) {
  584. // 1. 创建临时图像加载网络图片
  585. const img = new Image();
  586. img.crossOrigin = "Anonymous"; // 解决跨域问题
  587. img.src = imageUrl;
  588. // 2. 图片加载完成后处理
  589. await new Promise((resolve) => (img.onload = resolve));
  590. // 3. 创建Canvas并绘制图像
  591. const canvas = document.createElement("canvas");
  592. canvas.width = img.width;
  593. canvas.height = img.height;
  594. const ctx = canvas.getContext("2d");
  595. ctx.drawImage(img, 0, 0);
  596. // 4. 获取鼠标点击位置的像素数据
  597. const pixelData = ctx.getImageData(left, top, 1, 1).data;
  598. const [r, g, b, alpha] = pixelData;
  599. // 5. 计算白色程度(RGB接近255的程度)
  600. const whiteRatio = (r + g + b) / (3 * 255); // RGB平均值归一化
  601. const whiteScore = Math.round(whiteRatio * 100); // 映射到0-100
  602. // 6. 返回结果
  603. return {
  604. rawAlpha: alpha, // 原始透明度值 (0-255)
  605. alphaPercentage: Math.round((alpha / 255) * 100), // 透明度百分比
  606. whiteScore: whiteScore, // 白色程度得分 (0-100)
  607. };
  608. },
  609. // 替换底图 xyz 值为鼠标点击位置的值并返回
  610. replaceTemplate(str, level, tileXY) {
  611. return str.replace(/\{([xyz])\}/g, (match, key) => {
  612. switch (key) {
  613. case "x":
  614. return tileXY.x;
  615. case "y":
  616. return tileXY.y;
  617. case "z":
  618. return level;
  619. default:
  620. return match; // 未匹配时返回原内容(理论上不会执行)
  621. }
  622. });
  623. },
  624. // 设置地球底图
  625. setMapImageryProvider() {
  626. if (this.imageryProvider) {
  627. this.viewer.imageryLayers.remove(this.imageryProvider);
  628. this.imageryProvider = null;
  629. }
  630. const imageryProvider = this.basicMapList.find((ele) => {
  631. return ele.id === this.basicMapId;
  632. });
  633. this.imageryProvider = new Cesium.UrlTemplateImageryProvider(
  634. imageryProvider
  635. );
  636. // 添加底图
  637. this.viewer.imageryLayers.addImageryProvider(this.imageryProvider);
  638. },
  639. // 初始化 geoJson 数据
  640. async initGeoJsonData() {
  641. // 创建GeoJSON数据源
  642. await new Cesium.GeoJsonDataSource.load(basicGeoJson, {
  643. stroke: Cesium.Color.GRAY, // 边界线颜色
  644. fill: Cesium.Color.BLACK.withAlpha(0), // 填充颜色
  645. strokeWidth: 1, // 边界线宽度
  646. markerSymbol: "?", // 点要素的符号
  647. clampToGround: true, // 贴地
  648. }).then((dataSource) => {
  649. // 添加到视图
  650. this.viewer.dataSources.add(dataSource);
  651. var entities = dataSource.entities.values;
  652. for (let i = 0; i < entities.length; i++) {
  653. let entity = entities[i];
  654. entity.polygon.hierarchy.getValue(Cesium.JulianDate.now()).positions;
  655. //单独设置线条样式
  656. var positions = entity.polygon.hierarchy._value.positions;
  657. entity.polyline = {
  658. positions: positions,
  659. width: 1,
  660. outline: false,
  661. };
  662. }
  663. // 添加中文标签图层
  664. const labelLayer = new Cesium.LabelCollection();
  665. this.viewer.scene.primitives.add(labelLayer);
  666. const cities = [];
  667. basicGeoJson?.features?.forEach((ele) => {
  668. if (Array.isArray(ele.properties.centroid)) {
  669. const name = ele.properties.name;
  670. const lon = ele.properties.centroid[0];
  671. const lat = ele.properties.centroid[1];
  672. cities.push({ name, lon, lat });
  673. }
  674. });
  675. cities.forEach((city, index) => {
  676. labelLayer.add({
  677. id: index,
  678. name: "cityLabel",
  679. position: Cesium.Cartesian3.fromDegrees(city.lon, city.lat, 10),
  680. text: city.name,
  681. font: 'bold 14px "Microsoft YaHei", sans-serif',
  682. fillColor: Cesium.Color.fromCssColorString("#000"),
  683. outlineColor: Cesium.Color.WHITE,
  684. outlineWidth: 2,
  685. style: Cesium.LabelStyle.FILL_AND_OUTLINE,
  686. pixelOffset: new Cesium.Cartesian2(0, 0), // 设置为0
  687. horizontalOrigin: Cesium.HorizontalOrigin.CENTER, // 水平居中
  688. verticalOrigin: Cesium.VerticalOrigin.CENTER, // 垂直居中
  689. });
  690. });
  691. this.labelLayer = labelLayer;
  692. this.resetViewport();
  693. });
  694. },
  695. // 重置视角
  696. resetViewport(height = 0) {
  697. // 设置初始视图为宁夏
  698. const that = this;
  699. this.viewer.camera.flyTo({
  700. destination: Cesium.Cartesian3.fromDegrees(
  701. 106.169866,
  702. 38.46637,
  703. height || 8000000
  704. ),
  705. orientation: {
  706. heading: Cesium.Math.toRadians(0),
  707. pitch: Cesium.Math.toRadians(-90),
  708. roll: 0.0,
  709. },
  710. duration: 1.0,
  711. complete() {
  712. // 为什么要加这个?因为破库地球没完全加载完成时如果执行了监听鼠标滑动事件会光速报错滑跪
  713. if (!that.loadDone) {
  714. that.initEventInputAction();
  715. that.loadDone = true;
  716. }
  717. },
  718. });
  719. },
  720. // 添加风场图
  721. showWindLayer() {
  722. if (!this.windLayer) {
  723. this.resetViewport(20000000);
  724. this.windLayer = new Windy(windLineJson, this.viewer);
  725. this.windLayerTimmer = setInterval(() => {
  726. this.windLayer.animate();
  727. }, 200);
  728. }
  729. },
  730. // 将风向角度数据转换为矢量坐标
  731. windToVector(speed, direction) {
  732. // 转换为弧度(气象角度:0°=北风,90°=东风)
  733. const rad = Cesium.Math.toRadians(direction);
  734. // 计算UV分量(U: 东西方向,V: 南北方向)
  735. return {
  736. u: -speed * Math.sin(rad), // 东正西负
  737. v: -speed * Math.cos(rad), // 北正南负
  738. };
  739. },
  740. // 获取当前地图瓦片级别(暂时没用到怀疑有BUG)
  741. getTileLevel() {
  742. let tiles = new Set();
  743. let tilesToRender = this.viewer.scene.globe._surface._tilesToRender;
  744. if (Cesium.defined(tilesToRender)) {
  745. for (let i = 0; i < tilesToRender.length; i++) {
  746. tiles.add(tilesToRender[i].level);
  747. }
  748. return [...tiles].sort((a, b) => {
  749. return b - a;
  750. })[0];
  751. }
  752. },
  753. // 显示云图
  754. showCloudLayer() {
  755. // 设置云图位置(示例:覆盖中国区域)
  756. // const rectangle = Cesium.Rectangle.fromDegrees(
  757. // 73.66, // 西经
  758. // 18.16, // 南纬
  759. // 135.05, // 东经
  760. // 53.55 // 北纬
  761. // );
  762. // const now = new Date();
  763. // const year = now.getFullYear();
  764. // const month = String(now.getMonth() + 1).padStart(2, "0");
  765. // const day = String(now.getDate()).padStart(2, "0");
  766. // const hour = String(Math.floor(now.getHours() / 3) * 3).padStart(2, "0"); // 每3小时更新
  767. // // 创建云图图层
  768. // const cloudLayer = this.viewer.imageryLayers.addImageryProvider(
  769. // new Cesium.UrlTemplateImageryProvider({
  770. // url: `https://data.ventusky.com/${year}/${month}/${day}/icon/whole_world/hour_${hour}/icon_oblacnost_${year}${month}${day}_${hour}.jpg`,
  771. // rectangle: Cesium.Rectangle.fromDegrees(-180, -90, 180, 90),
  772. // credit: "实时卫星云图",
  773. // })
  774. // );
  775. // 调用云层底图
  776. const cloudLayer = this.viewer.imageryLayers.addImageryProvider(
  777. new Cesium.UrlTemplateImageryProvider({
  778. url: "https://tile.openweathermap.org/map/clouds_new/{z}/{x}/{y}.png?appid=3b66d35579770393051599f8d518df4a",
  779. credit: "云层影像地图",
  780. })
  781. );
  782. // 设置蓝色调效果
  783. cloudLayer.alpha = 1; // 透明度
  784. cloudLayer.brightness = 1; // 亮度
  785. cloudLayer.contrast = 1; // 对比度
  786. this.cloudLayer = cloudLayer;
  787. },
  788. // 移除风场图
  789. removeWindLayer() {
  790. if (this.windLayer) {
  791. clearInterval(this.windLayerTimmer);
  792. this.windLayer.removeLines();
  793. this.viewer.imageryLayers.remove(this.windLayer);
  794. this.windLayer = null;
  795. }
  796. },
  797. // 移除卫星云图
  798. removeCloudLayer() {
  799. if (this.cloudLayer) {
  800. this.tagMsg = null;
  801. this.viewer.imageryLayers.remove(this.cloudLayer);
  802. this.cloudLayer = null;
  803. }
  804. },
  805. // 切换风场图显隐
  806. switchWindLayer() {
  807. if (this.windLayer) {
  808. this.removeWindLayer();
  809. } else {
  810. this.showWindLayer();
  811. }
  812. },
  813. // 切换卫星云图显隐
  814. switchCloudLayer() {
  815. // this.$router.push({
  816. // path: "/cloudLayer",
  817. // });
  818. if (this.cloudLayer) {
  819. this.removeCloudLayer();
  820. } else {
  821. this.showCloudLayer();
  822. }
  823. },
  824. switchRainLayer() {
  825. this.$router.push({
  826. path: "/rainLayer",
  827. });
  828. },
  829. switchTemperatureLayerr() {
  830. this.$router.push({
  831. path: "/temperatureLayer",
  832. });
  833. },
  834. switchTopographicMap() {
  835. this.$router.push({
  836. path: "/topographicMap",
  837. });
  838. },
  839. },
  840. };
  841. </script>
  842. <style lang="less" scoped>
  843. .mapBox {
  844. width: 100%;
  845. height: 100%;
  846. position: relative;
  847. box-sizing: content-box;
  848. position: relative;
  849. .menuBox {
  850. position: absolute;
  851. left: 0;
  852. top: 0;
  853. width: 100%;
  854. background: #fff;
  855. display: flex;
  856. justify-content: flex-end;
  857. align-items: center;
  858. padding: 10px;
  859. .el-button {
  860. margin: 0;
  861. }
  862. .item {
  863. font-size: 12px;
  864. margin-left: 10px;
  865. }
  866. &.switch {
  867. opacity: 0;
  868. transition: 0.2s;
  869. &:hover {
  870. opacity: 1;
  871. transition: 0.2s;
  872. }
  873. }
  874. }
  875. .tag {
  876. position: absolute;
  877. left: 0;
  878. top: 0;
  879. background: #fff;
  880. font-size: 12px;
  881. color: #000;
  882. padding: 4px 8px;
  883. border-radius: 28px;
  884. transform: translate(-50%, -50%);
  885. transition: 0.1s;
  886. pointer-events: none;
  887. }
  888. .devInfoBox {
  889. width: 130px;
  890. position: absolute;
  891. right: 0;
  892. bottom: 0;
  893. padding: 4px;
  894. font-size: 12px;
  895. color: #fff;
  896. background: rgba(0, 0, 0, 0.25);
  897. display: flex;
  898. flex-direction: column;
  899. justify-content: flex-start;
  900. align-items: flex-start;
  901. z-index: 500;
  902. user-select: none;
  903. .item {
  904. width: 100%;
  905. margin-top: 4px;
  906. overflow: hidden;
  907. text-overflow: ellipsis;
  908. white-space: nowrap;
  909. }
  910. }
  911. }
  912. </style>
  913. <style lang="less">
  914. .modelDialog {
  915. .el-dialog__body {
  916. height: 750px;
  917. .el-tabs {
  918. height: 100%;
  919. .el-tab-pane {
  920. width: 100%;
  921. height: 100%;
  922. }
  923. .el-tabs__content {
  924. height: 100%;
  925. }
  926. }
  927. }
  928. }
  929. </style>