Cesium.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. <template>
  2. <div id="cesiumContainer" style="height: 100%;width: 100%;"></div>
  3. <!-- 自定义弹框组件 -->
  4. <div v-if="selectedPoint" class="custom-popup" :style="{
  5. left: `${popupPosition.x}px`,
  6. top: `${popupPosition.y}px`
  7. }">
  8. <div class="popup-content">
  9. <h3>{{ selectedPoint.STNM || '未知点' }}</h3>
  10. <p><strong>经度:</strong> {{ selectedPoint.LGTD }}</p>
  11. <p><strong>纬度:</strong> {{ selectedPoint.LTTD }}</p>
  12. <div class="popup-arrow"></div>
  13. </div>
  14. </div>
  15. </template>
  16. <script setup>
  17. import { ref, onMounted, onUnmounted } from 'vue'
  18. import * as Cesium from 'cesium';
  19. import "cesium/Build/CesiumUnminified/Widgets/widgets.css";
  20. import JYLData from '@/assets/Data/THJYL.json'
  21. const TDTTK = "d9e7aa2ad204aba6aeedea6f5ab48ed9";
  22. const selectedPoint = ref(null);
  23. const popupPosition = ref({ x: 0, y: 0 });
  24. let handler = null;
  25. let viewer = null;
  26. // 计算页面缩放比例(与autofit配置一致)
  27. const getScaleRatio = () => {
  28. const designWidth = 1920;
  29. const designHeight = 1080;
  30. return Math.min(window.innerWidth / designWidth, window.innerHeight / designHeight);
  31. };
  32. onMounted(async () => {
  33. viewer = new Cesium.Viewer('cesiumContainer', {
  34. timeline: false,
  35. baseLayer: false,
  36. geocoder: false,
  37. homeButton: false,
  38. sceneModePicker: false,
  39. navigationHelpButton: false,
  40. animation: false,
  41. fullscreenButton: false,
  42. vrButton: false,
  43. selectionIndicator: false,
  44. infoBox: false,
  45. //加载地形效果
  46. terrainProvider: await Cesium.createWorldTerrainAsync({
  47. url: 'http://10.8.11.98:9003/terrain/GlobeDEM/layer.json', // 注意修正URL中的locallhost为localhost
  48. requestVertexNormals: true,
  49. // requestWaterMask: true,
  50. })
  51. });
  52. try {
  53. // 1. 请求并解析TMS元数据XML
  54. const xmlUrl = 'http://10.8.11.98:9003/image/tms/HTHDOM/tilemapresource.xml';
  55. const response = await fetch(xmlUrl);
  56. const xmlText = await response.text();
  57. const parser = new DOMParser();
  58. const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
  59. // 2. 从XML中提取关键参数
  60. const tileFormat = xmlDoc.querySelector('TileFormat').getAttribute('extension'); // 如"png"
  61. const maxLevel = xmlDoc.querySelectorAll('TileSet').length - 1; // 最大层级(假设层级从0开始)
  62. const srs = xmlDoc.querySelector('SRS').textContent; // 如"EPSG:3857"
  63. // 3. 加载TMS影像
  64. const tmsImagery = new Cesium.UrlTemplateImageryProvider({
  65. url: `http://10.8.11.98:9003/image/tms/HTHDOM/{z}/{x}/{y}.${tileFormat}`,
  66. tileWidth: 256, // 从XML的TileFormat中获取width
  67. tileHeight: 256, // 从XML的TileFormat中获取height
  68. maximumLevel: maxLevel,
  69. projection: srs === 'EPSG:3857' ? Cesium.WebMercatorProjection : Cesium.GeographicProjection,
  70. // 核心:TMS行号反转(解决上下颠倒)
  71. urlTemplateFunction: (x, y, level) => {
  72. const tmsY = Math.pow(2, level) - 1 - y; // 反转行号
  73. return `http://10.8.11.98:9003/image/tms/HTHDOM//${level}/${x}/${tmsY}.${tileFormat}`;
  74. }
  75. });
  76. viewer.imageryLayers.addImageryProvider(tmsImagery);
  77. console.log('TMS影像加载成功');
  78. }
  79. catch (error) {
  80. console.error('加载TMS影像失败:', error);
  81. // 检查XML URL是否正确(可能拼写错误,如localhost是否少写字母)
  82. if (error.message.includes('404')) {
  83. console.warn('请确认XML路径正确:', xmlUrl);
  84. }
  85. }
  86. // 多块倾斜摄影瓦片集的URL列表(根据实际路径修改)
  87. const tilesetUrls = [
  88. "http://localhost:9003/model/TSQ1234/tileset.json",
  89. "http://localhost:9003/model/SY123/tileset.json",
  90. "http://10.8.11.98:9003/model/tvhy8PnM8/tileset.json",
  91. "http://10.8.11.98:9003/model/t9or0ZtaT/tileset.json"
  92. ];
  93. // 存储加载成功的瓦片集(可选,用于后续管理)
  94. const loadedTilesets = [];
  95. // 批量加载瓦片集(不调整视角)
  96. async function loadMultipleTilesets() {
  97. try {
  98. // 遍历URL列表,并行加载所有瓦片集
  99. const promises = tilesetUrls.map(async (url, index) => {
  100. try {
  101. const tileset = await Cesium.Cesium3DTileset.fromUrl(url, {
  102. maximumScreenSpaceError: 32, // 细节层级控制
  103. dynamicScreenSpaceError: true,
  104. skipLevelOfDetail: true,
  105. maximumConcurrentRequests: 5,
  106. tileCacheSize: 100
  107. });
  108. // 添加到场景
  109. viewer.scene.primitives.add(tileset);
  110. loadedTilesets.push(tileset);
  111. console.log(`第${index + 1}块瓦片集加载完成`);
  112. return tileset;
  113. } catch (error) {
  114. console.error(`第${index + 1}块瓦片集加载失败:`, error);
  115. return null; // 单块失败不影响其他块
  116. }
  117. });
  118. // 等待所有瓦片集加载(无论成功与否)
  119. await Promise.all(promises);
  120. console.log("所有瓦片集加载操作已完成");
  121. } catch (error) {
  122. console.error("批量加载逻辑出错:", error);
  123. }
  124. }
  125. // 执行加载(加载后保持原有视口)
  126. loadMultipleTilesets();
  127. // 定义距离显示条件和缩放属性(保持不变)
  128. const distanceDisplayCondition = new Cesium.DistanceDisplayCondition(0, 10000000);
  129. const pointNearFarScalar = new Cesium.NearFarScalar(10000, 1.0, 1000000, 0.3);
  130. const labelNearFarScalar = new Cesium.NearFarScalar(10000, 1.0, 400000, 0);
  131. // 存储实体与数据的映射(保持不变)
  132. const entityDataMap = new Map();
  133. JYLData.forEach((item) => {
  134. const position = Cesium.Cartesian3.fromDegrees(
  135. parseFloat(item.LGTD),
  136. parseFloat(item.LTTD)
  137. );
  138. const entity = viewer.entities.add({
  139. position: position,
  140. billboard: {
  141. image: '/src/assets/icon/blue.png',
  142. scale: 0.4,
  143. color: Cesium.Color.YELLOW,
  144. horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
  145. verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
  146. distanceDisplayCondition: distanceDisplayCondition,
  147. scaleByDistance: pointNearFarScalar
  148. },
  149. label: {
  150. text: item.STNM || '未知点',
  151. font: '25px 微软雅黑',
  152. fillColor: Cesium.Color.WHITE,
  153. backgroundColor: new Cesium.Color(0.1, 0.1, 0.1, 0.7),
  154. backgroundPadding: new Cesium.Cartesian2(8, 4),
  155. showBackground: true,
  156. cornerRadius: 4,
  157. outlineColor: Cesium.Color.BLACK,
  158. outlineWidth: 1,
  159. horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
  160. verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
  161. pixelOffset: new Cesium.Cartesian2(0, -32),
  162. scale: 1.0,
  163. disableDepthTestDistance: Number.POSITIVE_INFINITY,
  164. distanceDisplayCondition: distanceDisplayCondition,
  165. scaleByDistance: labelNearFarScalar
  166. },
  167. id: `point-${item.STCD || item.LGTD + '-' + item.LTTD}`,
  168. properties: {
  169. data: item
  170. }
  171. });
  172. entityDataMap.set(entity.id, item);
  173. });
  174. // 天地图图层(保持不变)
  175. const tdtLayer = new Cesium.WebMapTileServiceImageryProvider({
  176. url: `https://t0.tianditu.com/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={TileMatrix}&TILEROW={TileRow}&TILECOL={TileCol}&tk=${TDTTK}`,
  177. layer: "tdt",
  178. style: "default",
  179. format: "image/jpeg",
  180. tileMatrixSetID: "w",
  181. maximumLevel: 16,
  182. show: true,
  183. });
  184. viewer.imageryLayers.addImageryProvider(tdtLayer);
  185. const tdtAnnotionLayer = new Cesium.WebMapTileServiceImageryProvider({
  186. url: `http://t0.tianditu.com/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={TileMatrix}&TILEROW={TileRow}&TILECOL={TileCol}&tk=${TDTTK}`,
  187. layer: "tdtAnno",
  188. style: "default",
  189. format: "image/jpeg",
  190. tileMatrixSetID: "w",
  191. maximumLevel: 18,
  192. show: false,
  193. });
  194. viewer.imageryLayers.addImageryProvider(tdtAnnotionLayer);
  195. // 初始化视图(保持不变)
  196. viewer.cesiumWidget.creditContainer.style.display = "none";
  197. viewer.camera.setView({
  198. destination: Cesium.Cartesian3.fromDegrees(120.169103, 31.226174, 500000),
  199. orientation: {
  200. heading: Cesium.Math.toRadians(0),
  201. pitch: Cesium.Math.toRadians(-90),
  202. },
  203. });
  204. // 点击事件处理(核心修改)
  205. handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
  206. handler.setInputAction((click) => {
  207. const scaleRatio = getScaleRatio();
  208. const correctedX = click.position.x / scaleRatio;
  209. const correctedY = click.position.y / scaleRatio;
  210. const pickedObject = viewer.scene.pick(new Cesium.Cartesian2(correctedX, correctedY));
  211. if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
  212. const entityId = pickedObject.id.id;
  213. const data = entityDataMap.get(entityId) || pickedObject.id.properties?.data?.getValue();
  214. if (data) {
  215. selectedPoint.value = data;
  216. // 关键:计算图标屏幕坐标,固定显示在图标上方
  217. const entityPosition = viewer.scene.cartesianToCanvasCoordinates(pickedObject.id.position._value);
  218. if (entityPosition) {
  219. popupPosition.value = {
  220. // 水平居中对齐图标
  221. x: (entityPosition.x / scaleRatio) - 100,
  222. // 固定显示在图标上方(距离图标底部10px)
  223. y: (entityPosition.y / scaleRatio) - 130 // 130 = 弹框高度 + 间距
  224. };
  225. }
  226. viewer.flyTo(pickedObject.id, {
  227. offset: new Cesium.HeadingPitchRange(0, -0.5, 1500)
  228. });
  229. }
  230. } else {
  231. selectedPoint.value = null;
  232. }
  233. }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  234. // 清理函数(移除鼠标移动监听)
  235. onUnmounted(() => {
  236. if (handler) {
  237. handler.destroy();
  238. handler = null;
  239. }
  240. if (viewer && !viewer.isDestroyed()) {
  241. viewer.destroy();
  242. }
  243. });
  244. });
  245. </script>
  246. <style scoped>
  247. .custom-popup {
  248. position: absolute;
  249. z-index: 1000;
  250. display: block;
  251. /* 始终显示(由v-if控制显隐) */
  252. pointer-events: none;
  253. }
  254. .popup-content {
  255. background-color: white;
  256. border-radius: 5px;
  257. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  258. padding: 10px 15px;
  259. width: 200px;
  260. pointer-events: all;
  261. }
  262. .popup-arrow {
  263. position: absolute;
  264. bottom: -10px;
  265. /* 指向图标 */
  266. left: 50%;
  267. transform: translateX(-50%);
  268. width: 0;
  269. height: 0;
  270. border-left: 10px solid transparent;
  271. border-right: 10px solid transparent;
  272. border-top: 10px solid white;
  273. }
  274. /* 其他样式保持不变 */
  275. .popup-content h3 {
  276. margin-top: 0;
  277. margin-bottom: 8px;
  278. color: #333;
  279. }
  280. .popup-content p {
  281. margin: 5px 0;
  282. color: #666;
  283. font-size: 14px;
  284. }
  285. </style>