Bladeren bron

增加河流和流域分区的矢量瓦片,优化点击要素弹框的功能

WQQ 2 dagen geleden
bovenliggende
commit
d6f7152eea

+ 199 - 5
RuoYi-Vue3/src/supermap-cesium-module/components/layer/mvtlayer-style/mvtlayer-style.js

@@ -4,6 +4,7 @@ import { watch, ref, reactive, toRefs, onBeforeUnmount, onMounted } from "vue";
 import resource from '../../../js/local/lang.js'  //语言资源
 import { storeState, actions } from '../../../js/store/store.js'   //简单局部状态管理
 import tool from "../../../js/tool/tool.js";
+import { queryMvtTile } from "../../../js/common/mvtClickHandler.js";  // MVT 瓦片 HTTP 查询
 
 function mvtlayerStyle(props) {
 
@@ -155,18 +156,57 @@ function mvtlayerStyle(props) {
             selectedLayer.setFilter(selectedChildLayer.id, filter)
     }
 
+    /**
+     * 屏幕坐标转经纬度
+     */
+    function screenToLonLat(position) {
+        const ray = viewer.camera.getPickRay(position);
+        const cartesian = viewer.scene.globe.pick(ray, viewer.scene);
+        if (!cartesian) return null;
+        const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
+        return {
+            lon: Cesium.Math.toDegrees(cartographic.longitude),
+            lat: Cesium.Math.toDegrees(cartographic.latitude)
+        };
+    }
+
     // 点击显示属性
     function clickShowProperty(e) {
         let selectedEntity = viewer.scene.pick(e.message.position) || viewer.selectedEntity;
         if (!Cesium.defined(selectedEntity) || !Cesium.defined(selectedEntity.pickResult)) {
             state.bubbleShow = false;
+            // MVT 瓦片依赖 scene.pick() 首次点击无法拾取(瓦片尚未渲染到 pick buffer),
+            // 降级为 HTTP 瓦片查询,确保一次点击即显示气泡
+            const mvtList = viewer.scene._vectorTileMaps?._layerQueue;
+            if (mvtList && mvtList.length > 0 && selectedLayer) {
+                const lonLat = screenToLonLat(e.message.position);
+                if (lonLat) {
+                    const tileBaseUrl = selectedLayer._tileBaseUrl;
+                    if (tileBaseUrl) {
+                        const tileUrl = tileBaseUrl.endsWith('/')
+                            ? tileBaseUrl + 'tiles/{z}/{x}/{y}.mvt'
+                            : tileBaseUrl + '/tiles/{z}/{x}/{y}.mvt';
+                        queryMvtTile(lonLat.lon, lonLat.lat, tileUrl).then(result => {
+                            if (result && result.properties) {
+                                showMvtHttpBubble(result, e.message.position);
+                            }
+                        });
+                    }
+                }
+            }
             return;
         }
-        let cha = document.body.clientWidth - e.message.position.x;
+        showMvtBubble(selectedEntity, e.message.position);
+    }
+
+    // 显示 MVT 属性气泡(从 scene.pick 获取的数据)
+    function showMvtBubble(selectedEntity, position) {
+        if (!Cesium.defined(selectedEntity) || !Cesium.defined(selectedEntity.pickResult)) return;
+        let cha = document.body.clientWidth - position.x;
         if (cha >= 300)
-            bubble.value.style.left = (e.message.position.x) + 'px';
-        else bubble.value.style.left = (e.message.position.x - 250) + 'px';
-        bubble.value.style.top = (e.message.position.y - 220) + 'px';
+            bubble.value.style.left = (position.x) + 'px';
+        else bubble.value.style.left = (position.x - 250) + 'px';
+        bubble.value.style.top = (position.y - 220) + 'px';
         state.bubbleShow = true;
         let obj = null;
         for (var key in selectedEntity.pickResult) {
@@ -178,12 +218,161 @@ function mvtlayerStyle(props) {
         console.log(feature)
     }
 
+    // 显示 MVT 属性气泡(从 HTTP 瓦片查询获取的数据)
+    function showMvtHttpBubble(result, position) {
+        const props = result.properties;
+        const geometry = result.geometry;
+        
+        // 1. 创建高亮图形
+        createHttpHighlight(geometry);
+        
+        // 2. 显示气泡
+        let cha = document.body.clientWidth - position.x;
+        if (cha >= 300)
+            bubble.value.style.left = (position.x) + 'px';
+        else bubble.value.style.left = (position.x - 250) + 'px';
+        bubble.value.style.top = (position.y - 220) + 'px';
+        state.bubbleShow = true;
+        // 将属性对象转为类 feature 结构供已有逻辑使用
+        const feature = {
+            properties: props,
+            ...props
+        };
+        state.clickGetFeature = feature;
+        console.log('[MVT-HTTP] 气泡显示:', props);
+    }
+
+    // HTTP 查询时创建高亮图形
+    let httpHighlightEntity = null;
+    
+    function createHttpHighlight(geometry) {
+        // 清除之前的高亮
+        if (httpHighlightEntity) {
+            viewer.entities.remove(httpHighlightEntity);
+            httpHighlightEntity = null;
+        }
+        
+        if (!geometry || !geometry.type) return;
+        
+        // 根据几何类型创建不同的高亮
+        const type = geometry.type;
+        
+        if (type === 'Point') {
+            // 点要素:创建高亮圆点
+            const coord = geometry.coordinates;
+            if (!coord || coord.length < 2) return;
+            
+            httpHighlightEntity = viewer.entities.add({
+                position: Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 0),
+                point: {
+                    pixelSize: 10,
+                    color: Cesium.Color.YELLOW,
+                    outlineColor: Cesium.Color.BLACK,
+                    outlineWidth: 2
+                }
+            });
+        } else if (type === 'MultiPoint') {
+            // 多点要素:创建高亮圆点(取第一个点)
+            const coord = geometry.coordinates[0];
+            if (!coord || coord.length < 2) return;
+            
+            httpHighlightEntity = viewer.entities.add({
+                position: Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 0),
+                point: {
+                    pixelSize: 10,
+                    color: Cesium.Color.YELLOW,
+                    outlineColor: Cesium.Color.BLACK,
+                    outlineWidth: 2
+                }
+            });
+        } else if (type === 'LineString') {
+            // 线要素:创建高亮折线
+            const coordinates = geometry.coordinates;
+            if (!coordinates || coordinates.length < 2) return;
+            
+            const positions = coordinates.map(coord => 
+                Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 0)
+            );
+            
+            httpHighlightEntity = viewer.entities.add({
+                polyline: {
+                    positions: positions,
+                    width: 5,
+                    material: Cesium.Color.YELLOW,
+                    outlineColor: Cesium.Color.BLACK,
+                    outlineWidth: 2
+                }
+            });
+        } else if (type === 'MultiLineString') {
+            // 多线要素:创建高亮折线(取第一条线)
+            const coordinates = geometry.coordinates[0];
+            if (!coordinates || coordinates.length < 2) return;
+            
+            const positions = coordinates.map(coord => 
+                Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 0)
+            );
+            
+            httpHighlightEntity = viewer.entities.add({
+                polyline: {
+                    positions: positions,
+                    width: 5,
+                    material: Cesium.Color.YELLOW,
+                    outlineColor: Cesium.Color.BLACK,
+                    outlineWidth: 2
+                }
+            });
+        } else if (type === 'Polygon') {
+            // 多边形要素:创建高亮多边形
+            const coordinates = geometry.coordinates;
+            if (!coordinates || !coordinates[0]) return;
+            
+            const positions = coordinates[0].map(coord => 
+                Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 0)
+            );
+            
+            httpHighlightEntity = viewer.entities.add({
+                polygon: {
+                    hierarchy: new Cesium.PolygonHierarchy(positions),
+                    material: Cesium.Color.YELLOW.withAlpha(0.3),
+                    outline: true,
+                    outlineColor: Cesium.Color.YELLOW,
+                    outlineWidth: 3
+                }
+            });
+        } else if (type === 'MultiPolygon') {
+            // 多多边形要素:创建高亮多边形(取第一个)
+            const coordinates = geometry.coordinates[0];
+            if (!coordinates || !coordinates[0]) return;
+            
+            const positions = coordinates[0].map(coord => 
+                Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 0)
+            );
+            
+            httpHighlightEntity = viewer.entities.add({
+                polygon: {
+                    hierarchy: new Cesium.PolygonHierarchy(positions),
+                    material: Cesium.Color.YELLOW.withAlpha(0.3),
+                    outline: true,
+                    outlineColor: Cesium.Color.YELLOW,
+                    outlineWidth: 3
+                }
+            });
+        }
+        
+        console.log(`[MVT-HTTP] 创建${type}高亮图形`);
+    }
+
     // 删除查询函数
     function clearSearch() {
         // viewer.scene.removeVectorTilesMap(mvtLayers[index].name);
         if (!highlightLayer) return;
         selectedLayer.removeLayer(highlightLayer.id);
         highlightLayer = null;
+        // 清除 HTTP 查询创建的高亮
+        if (httpHighlightEntity) {
+            viewer.entities.remove(httpHighlightEntity);
+            httpHighlightEntity = null;
+        }
     };
 
     //删除过滤
@@ -205,7 +394,12 @@ function mvtlayerStyle(props) {
 
     //关闭气泡
     function closeBubble() {
-        state.bubbleShow = false
+        state.bubbleShow = false;
+        // 关闭气泡时也清除高亮
+        if (httpHighlightEntity) {
+            viewer.entities.remove(httpHighlightEntity);
+            httpHighlightEntity = null;
+        }
     }
     //悬停气泡
     function dockBubble(val) {

+ 19 - 28
RuoYi-Vue3/src/supermap-cesium-module/config/server_config.js

@@ -1,21 +1,16 @@
 export default [
   {
     id: "vectorData",
-    name: "矢量数据",
+    name: "基础地理实体",
     children: [
       {
-        type: "RESTFEATURE",
+        type: "MVT",
         thumbnail: "/img/componentsImg/mvt.png",
         proxiedUrl:
-          "http://localhost:8090/iserver/services/data-vec_workspace/rest/data/datasources/vec_data/datasets/building",
+          "http://localhost:8090/iserver/services/fj_building_vector_tile/restjsr/v1/vectortile/maps/building",
         name: "建筑",
+        layers: [{ type: "MVT", layerName: "建筑" }],
         state: 0,
-        style: {
-          fillColor: "#00FF00",
-          fillOpacity: 0.4,
-          strokeColor: "#0055FF",
-          strokeWidth: 2,
-        },
       },
       {
         type: "MVT",
@@ -25,37 +20,33 @@ export default [
         name: "土壤",
         layers: [{ type: "MVT", layerName: "土壤" }],
         // 新增:绑定该MVT对应的RESTFEATURE数据服务地址
-        dataSourceUrl:
-          "http://192.168.0.103:8090/iserver/services/data-shapefile-turang2/rest/data/datasources/fujian-turang/datasets/fujian-turang",
+        // dataSourceUrl:
+        //   "http://192.168.0.103:8090/iserver/services/data-shapefile-turang2/rest/data/datasources/fujian-turang/datasets/fujian-turang",
         state: 0,
       },
       {
-        type: "RESTFEATURE",
+        type: "MVT",
         thumbnail: "/img/componentsImg/mvt.png",
         proxiedUrl:
-          "http://localhost:8090/iserver/services/data-vec_workspace/rest/data/datasources/vec_data/datasets/shuixi",
-        name: "水系",
+          "http://localhost:8090/iserver/services/fj_river_vector_tile/restjsr/v1/vectortile/maps/river",
+        name: "河流",
+        layers: [{ type: "MVT", layerName: "水系" }],
         state: 0,
         style: {
-          fillColor: "#00BFFF",
-          fillOpacity: 0.5,
-          strokeColor: "#0066CC",
-          strokeWidth: 2,
-        },
+          fillColor: "#0066CC",
+          fillOpacity: 0.7,
+          strokeColor: "#003399",
+          strokeWidth: 1
+        }
       },
       {
-        type: "RESTFEATURE",
+        type: "MVT",
         thumbnail: "/img/componentsImg/mvt.png",
         proxiedUrl:
-          "http://localhost:8090/iserver/services/data-vec_workspace/rest/data/datasources/vec_data/datasets/liuyu",
-        name: "流域",
+          "http://localhost:8090/iserver/services/fj_liuyu_vector_tile/restjsr/v1/vectortile/maps/liuyu",
+        name: "流域分区",
+        layers: [{ type: "MVT", layerName: "流域" }],
         state: 0,
-        style: {
-          fillColor: "#98FB98",
-          fillOpacity: 0.3,
-          strokeColor: "#228B22",
-          strokeWidth: 2,
-        },
       },
     ],
   },

+ 66 - 1
RuoYi-Vue3/src/supermap-cesium-module/js/common/layerManagement.js

@@ -282,12 +282,69 @@ function flyToChinaRegion() {
     });
 }
 
+// 为MVT图层应用样式
+function applyMvtStyle(mvtMap, style) {
+    try {
+        console.log('===== applyMvtStyle 开始 =====');
+        console.log('样式配置:', style);
+        
+        // 尝试多种方式应用样式
+        if (mvtMap._inner && mvtMap._inner._styleCallback) {
+            console.log('尝试通过 _styleCallback 应用样式');
+            mvtMap._inner._styleCallback = function(feature, layer) {
+                const fillColor = Cesium.Color.fromCssColorString(style.fillColor || '#0066CC').withAlpha(style.fillOpacity || 0.7);
+                const strokeColor = Cesium.Color.fromCssColorString(style.strokeColor || '#003399');
+                const strokeWidth = style.strokeWidth || 1;
+                
+                return {
+                    fillColor: fillColor,
+                    strokeColor: strokeColor,
+                    strokeWidth: strokeWidth,
+                    fill: true,
+                    stroke: true
+                };
+            };
+        }
+        
+        // 尝试通过 imageryProvider 的 style 属性
+        if (mvtMap._provider && mvtMap._provider.style) {
+            console.log('尝试通过 _provider.style 应用样式');
+            mvtMap._provider.style = new Cesium.Cesium3DTileStyle({
+                color: `color(${style.fillColor || '#0066CC'})`,
+                opacity: style.fillOpacity || 0.7
+            });
+        }
+        
+        // 尝试通过更新瓦片图层的样式
+        if (mvtMap.updateStyle) {
+            console.log('尝试通过 updateStyle 方法应用样式');
+            mvtMap.updateStyle({
+                fillColor: style.fillColor,
+                fillOpacity: style.fillOpacity,
+                strokeColor: style.strokeColor,
+                strokeWidth: style.strokeWidth
+            });
+        }
+        
+        console.log('===== applyMvtStyle 完成 =====');
+    } catch (e) {
+        console.error('applyMvtStyle异常:', e);
+    }
+}
+
 // 添加mvt
-function addMvtLayer(LayerURL, name, callback) {
+function addMvtLayer(LayerURL, name, style, callback) {
+    // 处理参数顺序兼容:如果第三个参数是函数,则作为callback
+    if (typeof style === 'function') {
+        callback = style;
+        style = null;
+    }
+    
     try {
         console.log('===== addMvtLayer 开始 =====');
         console.log('URL:', LayerURL);
         console.log('名称:', name);
+        console.log('样式:', style);
 
         let mvtMap = viewer.scene.addVectorTilesMap({
             url: LayerURL,
@@ -296,6 +353,9 @@ function addMvtLayer(LayerURL, name, callback) {
             viewer: viewer
         });
 
+        // 保存瓦片URL到图层对象,供后续点击查询复用
+        mvtMap._tileBaseUrl = LayerURL;
+
         console.log('MVT地图对象:', mvtMap);
         console.log('MVT地图对象属性:', Object.keys(mvtMap));
         console.log('场景中的矢量瓦片地图:', viewer.scene.vectorTilesMaps);
@@ -358,6 +418,11 @@ function addMvtLayer(LayerURL, name, callback) {
                 console.log('_provider属性:', Object.keys(mvtMap._provider));
             }
 
+            // 如果有样式配置,尝试应用样式
+            if (style) {
+                applyMvtStyle(mvtMap, style);
+            }
+
             actions.setChangeLayers();
             callback(mvtMap)
         }, function(error) {

+ 377 - 181
RuoYi-Vue3/src/supermap-cesium-module/js/common/mvtClickHandler.js

@@ -16,216 +16,412 @@
 import { PbfReader as Pbf } from 'pbf';
 import { VectorTile } from '@mapbox/vector-tile';
 
-class MvtClickHandler {
-  constructor(vueInstance, options = {}) {
-    this.vueInstance = vueInstance;
+// ==================== 独立工具函数(供外部复用) ====================
 
-    // MVT 瓦片服务地址(从配置获取,默认使用土壤图层)
-    this.tileUrlTemplate = options.tileUrl || 'http://localhost:8090/iserver/services/fj_soli_vector_tile/restjsr/v1/vectortile/maps/soil/tiles/{z}/{x}/{y}.mvt';
+/**
+ * 经度 → 瓦片 X 坐标
+ */
+export function lonToTile(lon, zoom) {
+  return Math.floor((lon + 180) / 360 * Math.pow(2, zoom));
+}
 
-    // 备用:数据服务查询(已确认空间查询不生效,作为最后降级)
-    this.serviceRootUrl = options.serviceRoot || '';
+/**
+ * 纬度 → 瓦片 Y 坐标
+ */
+export function latToTile(lat, zoom) {
+  return Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom));
+}
+
+/**
+ * 获取瓦片的 WGS84 范围
+ */
+export function getTileBounds(z, x, y) {
+  const n = Math.pow(2, z);
+  const west = x / n * 360 - 180;
+  const east = (x + 1) / n * 360 - 180;
+  const north = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI;
+  const south = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n))) * 180 / Math.PI;
+  return { west, east, north, south };
+}
+
+/**
+ * 将 MVT 要素的瓦片坐标转为 WGS84 坐标
+ */
+export function tileToGeoJSON(feature, bounds, extent) {
+  extent = extent || 4096;
+  const scaleX = (bounds.east - bounds.west) / extent;
+  const scaleY = (bounds.south - bounds.north) / extent;
+
+  const transformCoords = (coords) => {
+    return coords.map(ring => {
+      return ring.map(([x, y]) => [
+        bounds.west + x * scaleX,
+        bounds.north + y * scaleY  // MVT y 轴向下,WGS84 y 轴向上
+      ]);
+    });
+  };
+
+  // 单点坐标:将 [x, y] 瓦片坐标转为 WGS84
+  const transformPoint = ([x, y]) => [
+    bounds.west + x * scaleX,
+    bounds.north + y * scaleY
+  ];
+
+  let geom;
+  try {
+    const rawGeom = feature.loadGeometry();
+
+    // MVT 要素类型: 1-点, 2-线, 3-多边形, 4-多点, 5-多线, 6-多多边形
+    switch (feature.type) {
+      case 1: { // 点
+        const pt = rawGeom.map(p => [p.x, p.y])[0];
+        geom = {
+          type: 'Point',
+          coordinates: pt ? transformPoint(pt) : null
+        };
+        break;
+      }
+      case 2: { // 线
+        geom = {
+          type: 'LineString',
+          coordinates: rawGeom.map(ring => ring.map(p => transformPoint([p.x, p.y])))
+        };
+        break;
+      }
+      case 3: { // 多边形
+        geom = {
+          type: 'Polygon',
+          coordinates: transformCoords(rawGeom.map(ring => ring.map(p => [p.x, p.y])))
+        };
+        break;
+      }
+      case 4: { // 多点
+        geom = {
+          type: 'MultiPoint',
+          coordinates: rawGeom.map(p => transformPoint([p.x, p.y]))
+        };
+        break;
+      }
+      case 5: { // 多线
+        geom = {
+          type: 'MultiLineString',
+          coordinates: rawGeom.map(ring => ring.map(p => transformPoint([p.x, p.y])))
+        };
+        break;
+      }
+      case 6: { // 多多边形
+        geom = {
+          type: 'MultiPolygon',
+          coordinates: transformCoords(rawGeom.map(ring => ring.map(p => [p.x, p.y])))
+        };
+        break;
+      }
+    }
+  } catch (e) {
+    console.error('[MVT] 几何转换失败:', e.message);
+    return null;
   }
 
-  addMvtClickHandler() {}
+  return geom;
+}
 
-  /**
-   * 根据经纬度查询要素属性
-   */
-  async handleClick(lon, lat, screenPos) {
-    console.log(`\n[MVT] 开始查询: 经度=${lon.toFixed(6)}, 纬度=${lat.toFixed(6)}`);
+/**
+ * 射线法判断点是否在多边形内
+ */
+export function pointInPolygon(lon, lat, coordinates) {
+  if (!coordinates || !coordinates.length) return false;
 
-    // 方案1:从 MVT 瓦片解析(主要方案)
-    const tileResult = await this._queryFromTile(lon, lat);
-    if (tileResult && Object.keys(tileResult).length > 0) {
-      console.log("[MVT] === 瓦片查询成功 ===", tileResult);
-      this.showPopup({
-        title: tileResult.type || tileResult.TURANG_NAME || "土壤要素",
-        attributes: tileResult,
-        position: screenPos
-      });
-      return;
+  const ring = coordinates[0];
+  if (!ring || ring.length < 3) return false;
+
+  let inside = false;
+  for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
+    const xi = ring[i][0], yi = ring[i][1];
+    const xj = ring[j][0], yj = ring[j][1];
+
+    if ((yi > lat) !== (yj > lat) &&
+        lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) {
+      inside = !inside;
     }
+  }
 
-    console.log("[MVT] 瓦片未命中,该点无土壤要素数据");
-    this.setPopupVisible(false);
+  return inside;
+}
+
+/**
+ * 点要素检测:判断点击点是否接近点要素(用于点和多点类型)
+ * @param {number} lon - 点击经度
+ * @param {number} lat - 点击纬度
+ * @param {Array} coordinates - 点坐标
+ * @param {number} threshold - 阈值(度),默认约 10 米
+ */
+export function pointNearPoint(lon, lat, coordinates, threshold = 0.0001) {
+  if (!coordinates) return false;
+  
+  // 单点坐标 [x, y] 格式
+  if (typeof coordinates[0] === 'number') {
+    const distance = Math.sqrt(Math.pow(lon - coordinates[0], 2) + Math.pow(lat - coordinates[1], 2));
+    return distance < threshold;
+  }
+  
+  // 多点坐标 [[x1,y1], [x2,y2], ...] 格式
+  for (const point of coordinates) {
+    if (!Array.isArray(point) || point.length < 2) continue;
+    const px = point[0];
+    const py = point[1];
+    const distance = Math.sqrt(Math.pow(lon - px, 2) + Math.pow(lat - py, 2));
+    if (distance < threshold) {
+      return true;
+    }
   }
+  return false;
+}
 
-  /**
-   * 从 MVT 瓦片查询要素属性
-   */
-  async _queryFromTile(lon, lat) {
-    try {
-      // 先在 zoom=14 查询(细节适中),没命中再降级
-      const zoomLevels = [14, 13, 12, 11, 10];
-      
-      for (const z of zoomLevels) {
-        const tileX = this._lonToTile(lon, z);
-        const tileY = this._latToTile(lat, z);
-        
-        const tileUrl = this.tileUrlTemplate
-          .replace('{z}', z)
-          .replace('{x}', tileX)
-          .replace('{y}', tileY);
-        
-        // 下载瓦片
-        const response = await fetch(tileUrl);
-        if (!response.ok) continue;
-        
-        const arrayBuffer = await response.arrayBuffer();
-        if (!arrayBuffer || arrayBuffer.byteLength === 0) continue;
-        
-        // 解析 PBF
-        const tile = new VectorTile(new Pbf(arrayBuffer));
-        console.log(`[MVT] zoom=${z} 瓦片(${tileX},${tileY}) 图层:`, Object.keys(tile.layers));
-        
-        // 遍历所有图层找匹配要素
-        for (const layerName of Object.keys(tile.layers)) {
-          const layer = tile.layers[layerName];
-          console.log(`[MVT] 图层 "${layerName}": ${layer.length} 个要素`);
-          
-          // 获取瓦片范围(WGS84)
-          const tileBounds = this._getTileBounds(z, tileX, tileY);
-          
-          for (let i = 0; i < layer.length; i++) {
-            const feature = layer.feature(i);
-            
-            // 只有当要素是面类型时才检查包含关系
-            if (feature.type !== 3) continue; // 3 = Polygon, 4 = MultiPolygon
-            
-            // 将要素坐标从瓦片坐标转为 WGS84
-            const geoJsonGeom = this._tileToGeoJSON(feature, tileBounds, layer.extent);
-            
-            // 检查点是否在面内
-            if (this._pointInPolygon(lon, lat, geoJsonGeom.coordinates)) {
-              console.log(`[MVT] 命中! zoom=${z}, 图层=${layerName}, 要素索引=${i}`);
-              
-              // 提取属性
-              const props = {};
-              const keys = Object.keys(feature.properties || {});
-              // 优先取已知字段
-              const knownFields = ['FID_', 'SOIL_', 'SOIL_ID', 'DL', 'type', 'SmID', 'SMID'];
-              const allFields = [...new Set([...knownFields, ...keys])];
-              
-              allFields.forEach(key => {
-                if (feature.properties[key] !== undefined) {
-                  props[key] = feature.properties[key];
-                }
-              });
-              
-              console.log(`[MVT] 属性:`, props);
-              return props;
-            }
-          }
-        }
+/**
+ * 线要素检测:判断点击点是否接近线要素(用于线和多线类型)
+ * @param {number} lon - 点击经度
+ * @param {number} lat - 点击纬度
+ * @param {Array} coordinates - 线坐标数组
+ * @param {number} threshold - 阈值(度),默认约 10 米
+ */
+export function pointNearLine(lon, lat, coordinates, threshold = 0.0001) {
+  if (!coordinates || !coordinates.length) return false;
+
+  for (const line of coordinates) {
+    if (!line || line.length < 2) continue;
+
+    for (let i = 0; i < line.length - 1; i++) {
+      const x1 = line[i][0];
+      const y1 = line[i][1];
+      const x2 = line[i + 1][0];
+      const y2 = line[i + 1][1];
+
+      const dist = pointToLineDistance(lon, lat, x1, y1, x2, y2);
+      if (dist < threshold) {
+        return true;
       }
-      
-      return null;
-    } catch (err) {
-      console.error("[MVT] 瓦片查询异常:", err.message);
-      return null;
     }
   }
+  return false;
+}
 
-  // ==================== 坐标转换工具 ====================
+/**
+ * 计算点到线段的距离
+ */
+function pointToLineDistance(px, py, x1, y1, x2, y2) {
+  const A = px - x1;
+  const B = py - y1;
+  const C = x2 - x1;
+  const D = y2 - y1;
 
-  /**
-   * 经度 → 瓦片 X 坐标
-   */
-  _lonToTile(lon, zoom) {
-    return Math.floor((lon + 180) / 360 * Math.pow(2, zoom));
-  }
+  const dot = A * C + B * D;
+  const lenSq = C * C + D * D;
+  let param = -1;
 
-  /**
-   * 纬度 → 瓦片 Y 坐标
-   */
-  _latToTile(lat, zoom) {
-    return Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom));
+  if (lenSq !== 0) {
+    param = dot / lenSq;
   }
 
-  /**
-   * 获取瓦片的 WGS84 范围
-   */
-  _getTileBounds(z, x, y) {
-    const n = Math.pow(2, z);
-    const west = x / n * 360 - 180;
-    const east = (x + 1) / n * 360 - 180;
-    const north = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI;
-    const south = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n))) * 180 / Math.PI;
-    return { west, east, north, south };
+  let xx, yy;
+
+  if (param < 0) {
+    xx = x1;
+    yy = y1;
+  } else if (param > 1) {
+    xx = x2;
+    yy = y2;
+  } else {
+    xx = x1 + param * C;
+    yy = y1 + param * D;
   }
 
-  /**
-   * 将 MVT 要素的瓦片坐标转为 WGS84 坐标
-   */
-  _tileToGeoJSON(feature, bounds, extent) {
-    extent = extent || 4096;
-    const scaleX = (bounds.east - bounds.west) / extent;
-    const scaleY = (bounds.south - bounds.north) / extent;
-    
-    const transformCoords = (coords) => {
-      return coords.map(ring => {
-        return ring.map(([x, y]) => [
-          bounds.west + x * scaleX,
-          bounds.north + y * scaleY  // MVT y 轴向下,WGS84 y 轴向上
-        ]);
-      });
-    };
-    
-    // 从 feature 获取几何(MVT 是 Geometry类型,需要处理)
-    let geom;
-    try {
-      // feature.loadGeometry() 返回 [Ring, Ring, ...]
-      // 每个 Ring 是 [{x, y}, ...]
-      const rawGeom = feature.loadGeometry();
-      
-      if (feature.type === 3) {
-        // Polygon: 第一个 ring 是外环,后续是内环
-        geom = {
-          type: 'Polygon',
-          coordinates: transformCoords(rawGeom.map(ring => 
-            ring.map(p => [p.x, p.y])
-          ))
-        };
-      } else if (feature.type === 4) {
-        // MultiPolygon 需要分组
-        // 简单处理:把所有 rings 当做一个 polygon
-        geom = {
-          type: 'Polygon',
-          coordinates: transformCoords(rawGeom.map(ring => 
-            ring.map(p => [p.x, p.y])
-          ))
-        };
+  const dx = px - xx;
+  const dy = py - yy;
+  return Math.sqrt(dx * dx + dy * dy);
+}
+
+/**
+ * 从 MVT 瓦片查询指定经纬度处的要素属性和几何
+ * @param {number} lon - 经度
+ * @param {number} lat - 纬度
+ * @param {string} tileUrlTemplate - 瓦片 URL 模板,含 {z}/{x}/{y} 占位符
+ * @param {number[]} [zoomLevels] - 查询的缩放级别,默认 [14,13,12,11,10]
+ * @returns {Promise<Object|null>} 包含属性和几何的对象,未命中返回 null
+ */
+export async function queryMvtTile(lon, lat, tileUrlTemplate, zoomLevels = [14, 13, 12, 11, 10]) {
+  try {
+    for (const z of zoomLevels) {
+      const tileX = lonToTile(lon, z);
+      const tileY = latToTile(lat, z);
+
+      const tileUrl = tileUrlTemplate
+        .replace('{z}', z)
+        .replace('{x}', tileX)
+        .replace('{y}', tileY);
+
+      console.log(`[MVT-DEBUG] 请求瓦片 zoom=${z}, x=${tileX}, y=${tileY}, url=${tileUrl}`);
+
+      let response;
+      try {
+        response = await fetch(tileUrl);
+      } catch (fetchErr) {
+        console.warn(`[MVT-DEBUG] fetch失败 zoom=${z}:`, fetchErr.message);
+        continue;
+      }
+
+      console.log(`[MVT-DEBUG] 响应状态: ${response.status} ${response.statusText}`);
+
+      if (!response.ok) {
+        // 试试不同扩展名
+        if (tileUrl.endsWith('.mvt')) {
+          const altUrl = tileUrl.replace('.mvt', '.pbf');
+          console.log(`[MVT-DEBUG] .mvt失败,重试.pbf: ${altUrl}`);
+          try {
+            response = await fetch(altUrl);
+            console.log(`[MVT-DEBUG] .pbf响应状态: ${response.status}`);
+            if (!response.ok) continue;
+          } catch {
+            continue;
+          }
+        } else {
+          continue;
+        }
+      }
+
+      const arrayBuffer = await response.arrayBuffer();
+      console.log(`[MVT-DEBUG] 瓦片数据大小: ${arrayBuffer.byteLength} 字节`);
+
+      if (!arrayBuffer || arrayBuffer.byteLength === 0) continue;
+
+      let tile;
+      try {
+        tile = new VectorTile(new Pbf(arrayBuffer));
+      } catch (parseErr) {
+        console.warn(`[MVT-DEBUG] PBF解析失败 zoom=${z}:`, parseErr.message);
+        continue;
+      }
+
+      const layerNames = Object.keys(tile.layers);
+      console.log(`[MVT-DEBUG] 瓦片中图层:`, layerNames);
+
+      if (layerNames.length === 0) continue;
+
+      for (const layerName of layerNames) {
+        const layer = tile.layers[layerName];
+        console.log(`[MVT-DEBUG] 图层"${layerName}": ${layer.length} 个要素`);
+
+        const tileBounds = getTileBounds(z, tileX, tileY);
+        console.log(`[MVT-DEBUG] 瓦片范围:`, tileBounds);
+
+        for (let i = 0; i < layer.length; i++) {
+          const feature = layer.feature(i);
+
+          // 支持所有要素类型:1-点, 2-线, 3-多边形, 4-多点, 5-多线, 6-多多边形
+          if (feature.type < 1 || feature.type > 6) continue;
+
+          const geoJsonGeom = tileToGeoJSON(feature, tileBounds, layer.extent);
+          if (!geoJsonGeom) continue;
+
+          // 根据要素类型使用不同的检测方法
+          let isHit = false;
+          if (feature.type === 1 || feature.type === 4) {
+            // 点要素:直接比较坐标
+            isHit = pointNearPoint(lon, lat, geoJsonGeom.coordinates);
+          } else if (feature.type === 2 || feature.type === 5) {
+            // 线要素:检测点到线的距离
+            isHit = pointNearLine(lon, lat, geoJsonGeom.coordinates);
+          } else if (feature.type === 3 || feature.type === 6) {
+            // 多边形要素:点在多边形内检测
+            isHit = pointInPolygon(lon, lat, geoJsonGeom.coordinates);
+          }
+
+          if (isHit) {
+            console.log(`[MVT-DEBUG] 命中要素 #${i}, 类型=${feature.type}`);
+            const props = {};
+            const keys = Object.keys(feature.properties || {});
+            const knownFields = ['FID_', 'SOIL_', 'SOIL_ID', 'DL', 'type', 'SmID', 'SMID', 'NAME', 'name'];
+            const allFields = [...new Set([...knownFields, ...keys])];
+
+            allFields.forEach(key => {
+              if (feature.properties[key] !== undefined) {
+                props[key] = feature.properties[key];
+              }
+            });
+
+            // 返回属性和几何信息,几何用于高亮显示
+            return {
+              properties: props,
+              geometry: geoJsonGeom,
+              zoom: z
+            };
+          }
+        }
       }
-    } catch (e) {
-      return null;
     }
-    
-    return geom;
+
+    console.log("[MVT-DEBUG] 所有缩放级别都未命中");
+    return null;
+  } catch (err) {
+    console.error("[MVT] 瓦片查询异常:", err.message);
+    return null;
   }
+}
+
+// ==================== 类定义(供 aside.vue 使用) ====================
+
+class MvtClickHandler {
+  constructor(vueInstance, options = {}) {
+    this.vueInstance = vueInstance;
+
+    // MVT 瓦片服务地址(从配置获取,默认使用土壤图层)
+    this.tileUrlTemplate = options.tileUrl || 'http://localhost:8090/iserver/services/fj_soli_vector_tile/restjsr/v1/vectortile/maps/soil/tiles/{z}/{x}/{y}.mvt';
+
+    // 备用:数据服务查询(已确认空间查询不生效,作为最后降级)
+    this.serviceRootUrl = options.serviceRoot || '';
+  }
+
+  addMvtClickHandler() {}
 
   /**
-   * 射线法判断点是否在多边形内
+   * 根据经纬度查询要素属性(支持多图层查询)
+   * @param {number} lon - 经度
+   * @param {number} lat - 纬度
+   * @param {Object} screenPos - 屏幕坐标
+   * @param {string[]} tileUrls - 可选的瓦片URL列表,若提供则遍历查询
    */
-  _pointInPolygon(lon, lat, coordinates) {
-    if (!coordinates || !coordinates.length) return false;
-    
-    // 检查外环
-    const ring = coordinates[0];
-    if (!ring || ring.length < 3) return false;
-    
-    let inside = false;
-    for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
-      const xi = ring[i][0], yi = ring[i][1];
-      const xj = ring[j][0], yj = ring[j][1];
-      
-      if ((yi > lat) !== (yj > lat) &&
-          lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) {
-        inside = !inside;
+  async handleClick(lon, lat, screenPos, tileUrls = []) {
+    console.log(`\n[MVT] 开始查询: 经度=${lon.toFixed(6)}, 纬度=${lat.toFixed(6)}`);
+
+    // 构建查询的瓦片URL列表:优先使用传入的列表,否则使用默认的
+    const urlsToQuery = tileUrls.length > 0 ? tileUrls : [this.tileUrlTemplate];
+    console.log('[MVT] 查询图层数量:', urlsToQuery.length);
+
+    // 方案1:从 MVT 瓦片解析(主要方案)- 遍历所有图层
+    let tileResult = null;
+    for (const tileUrl of urlsToQuery) {
+      console.log('[MVT] 查询图层:', tileUrl);
+      tileResult = await queryMvtTile(lon, lat, tileUrl);
+      if (tileResult && tileResult.properties && Object.keys(tileResult.properties).length > 0) {
+        console.log("[MVT] === 瓦片查询成功 ===", tileResult);
+        break;
       }
     }
     
-    return inside;
+    // tileResult 结构: {properties, geometry, zoom}
+    if (tileResult && tileResult.properties && Object.keys(tileResult.properties).length > 0) {
+      const props = tileResult.properties;
+      // 动态提取标题:优先使用 type、NAME、name、TURANG_NAME 等字段,最后用通用名称
+      const title = props.type || props.NAME || props.name || props.TURANG_NAME || props.building || props.RIVER_NAME || props.BASIN_NAME || "矢量要素";
+      this.showPopup({
+        title: title,
+        attributes: props,
+        position: screenPos
+      });
+      return;
+    }
+
+    console.log("[MVT] 瓦片未命中,该点无矢量要素数据");
+    this.setPopupVisible(false);
   }
 
   // ==================== 弹窗控制 ====================

+ 38 - 9
RuoYi-Vue3/src/supermap-cesium-module/js/common/waterLayer.js

@@ -111,12 +111,36 @@ function loadWaterLayer(url, name, callback) {
                     }
                     
                     let terrainHeight = 0;
-                    try {
-                        const sampledPositions = await Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, cartographics);
-                        const totalHeight = sampledPositions.reduce((sum, pos) => sum + pos.height, 0);
-                        terrainHeight = totalHeight / sampledPositions.length;
-                    } catch (e) {
-                        console.warn('地形采样失败,使用默认高度:', e);
+                    
+                    // 检查地形服务是否有效
+                    const isTerrainAvailable = viewer.terrainProvider && 
+                        !(viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider);
+                    
+                    if (!isTerrainAvailable) {
+                        console.warn('未加载真实地形服务,使用默认水面高度');
+                        terrainHeight = 0;
+                    } else {
+                        try {
+                            // 等待地形服务就绪
+                            if (viewer.terrainProvider.readyPromise) {
+                                await viewer.terrainProvider.readyPromise;
+                            }
+                            
+                            // 执行地形采样
+                            const sampledPositions = await Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, cartographics);
+                            const validHeights = sampledPositions.filter(pos => pos && isFinite(pos.height));
+                            
+                            if (validHeights.length > 0) {
+                                const totalHeight = validHeights.reduce((sum, pos) => sum + pos.height, 0);
+                                terrainHeight = totalHeight / validHeights.length;
+                            } else {
+                                console.warn('地形采样结果中没有有效高度');
+                                terrainHeight = 0;
+                            }
+                        } catch (e) {
+                            console.warn('地形采样失败,使用默认高度:', e);
+                            terrainHeight = 0;
+                        }
                     }
                     
                     // 验证地形高度是否有效
@@ -125,12 +149,17 @@ function loadWaterLayer(url, name, callback) {
                         terrainHeight = 0;
                     }
                     
-                    const waterHeight = terrainHeight - 22;
+                    // 水面高度 = 地形高度 + 水面偏移量
+                    // 使用可配置的水面偏移量,负值表示水面低于地形
+                    const waterOffset = -22; // 水面低于地形20米
+                    const waterHeight = terrainHeight + waterOffset;
+                    
+                    console.log(`水面高度计算: 地形高度=${terrainHeight.toFixed(2)}m, 水面偏移=${waterOffset}m, 最终水面高度=${waterHeight.toFixed(2)}m`);
                     
                     // 验证水面高度是否有效
                     if (!isFinite(waterHeight) || isNaN(waterHeight)) {
-                        console.warn('水面高度无效,使用默认值 -22');
-                        waterHeight = -22;
+                        console.warn('水面高度无效,使用默认值 0');
+                        waterHeight = 0;
                     }
                     
                     // 限制水面高度范围,避免极端值

+ 55 - 17
RuoYi-Vue3/src/supermap-cesium-module/views/layout/aside.vue

@@ -1230,7 +1230,7 @@ export default {
         if (obj.type === 'MVT') {
           // MVT 类型使用矢量瓦片加载
           console.log('使用MVT方式加载');
-          layerManagement.addMvtLayer(obj.proxiedUrl, obj.name, (mvtLayer) => {
+          layerManagement.addMvtLayer(obj.proxiedUrl, obj.name, obj.style, (mvtLayer) => {
             if (mvtLayer) {
               obj.state = 1;
               window.store.actions.setChangeLayers();
@@ -1238,8 +1238,8 @@ export default {
               console.log(`${obj.name} MVT加载成功`);
               ElMessage.success(`${obj.name} 加载成功`);
               
-              // 添加MVT图层点击事件处理
-              this.addMvtClickHandler();
+              // 添加MVT图层点击事件处理,传入当前图层的瓦片URL
+              this.addMvtClickHandler(obj.proxiedUrl);
             } else {
               ElMessage.error(`加载 ${obj.name} 失败`);
             }
@@ -1381,9 +1381,26 @@ export default {
     },
     
     // 添加MVT图层点击事件处理
-    addMvtClickHandler() {
-      console.log('aside.vue addMvtClickHandler 被调用');
+    addMvtClickHandler(tileBaseUrl) {
+      console.log('aside.vue addMvtClickHandler 被调用', tileBaseUrl);
       
+      // 初始化 MVT 图层列表(支持多图层)
+      if (!this._mvtTileUrls) {
+        this._mvtTileUrls = [];
+      }
+      
+      if (tileBaseUrl) {
+        const tileUrl = tileBaseUrl.endsWith('/') 
+          ? tileBaseUrl + 'tiles/{z}/{x}/{y}.mvt'
+          : tileBaseUrl + '/tiles/{z}/{x}/{y}.mvt';
+        
+        // 避免重复添加相同的瓦片URL
+        if (!this._mvtTileUrls.includes(tileUrl)) {
+          this._mvtTileUrls.push(tileUrl);
+        }
+      }
+      
+      // 初始化点击处理器(单例)
       if (!this.mvtClickHandler) {
         this.mvtClickHandler = new MvtClickHandler(this);
       }
@@ -1481,11 +1498,29 @@ export default {
           this.saveLoadedServices();
           break;
         case "vectorData":
-          layerManagement.layersDelete("GEOJSON", obj.name, () => {
+          // 根据矢量数据类型选择卸载方式:MVT用removeVectorTilesMap,RESTFEATURE用dataSources
+          const vectorDeleteType = obj.type === 'MVT' ? 'MVT' : 'GEOJSON';
+          layerManagement.layersDelete(vectorDeleteType, obj.name, () => {
             obj.state = 0;
             window.store.actions.setChangeLayers();
             this.saveLoadedServices();
           });
+          // MVT 卸载后从图层列表中移除,并更新标记
+          if (obj.type === 'MVT') {
+            // 从 _mvtTileUrls 中移除当前图层的 URL
+            if (this._mvtTileUrls && obj.proxiedUrl) {
+              const tileUrl = obj.proxiedUrl.endsWith('/') 
+                ? obj.proxiedUrl + 'tiles/{z}/{x}/{y}.mvt'
+                : obj.proxiedUrl + '/tiles/{z}/{x}/{y}.mvt';
+              const index = this._mvtTileUrls.indexOf(tileUrl);
+              if (index > -1) {
+                this._mvtTileUrls.splice(index, 1);
+              }
+            }
+            // 只有当所有 MVT 图层都卸载时,才清除标记
+            this._hasMvtLayer = this._mvtTileUrls && this._mvtTileUrls.length > 0;
+            this.mvtPopupVisible = false;
+          }
           break;
       }
     },
@@ -1616,8 +1651,8 @@ export default {
               if (flyTo) {
                 viewer.flyTo(mvtlayer, { duration: 2 });
               }
-              // 添加MVT图层点击事件处理
-              this.addMvtClickHandler();
+              // 添加MVT图层点击事件处理,传入当前图层的瓦片URL
+              this.addMvtClickHandler(url);
             });
             break;
           case 'SHP':
@@ -2084,9 +2119,9 @@ export default {
           }
         }
 
-        // 恢复底图
+        // 恢复底图 (server_config[2] 是在线底图)
         if (config.baseLayerType) {
-          const baseLayer = this.server_config[1].children.find(
+          const baseLayer = this.server_config[2].children.find(
             layer => layer.type === config.baseLayerType
           );
           if (baseLayer) {
@@ -2098,12 +2133,13 @@ export default {
           }
         }
 
-        // 恢复地形
+        // 恢复地形 (server_config[3] 是在线地形)
         if (config.terrainLayerType) {
-          const terrainLayer = this.server_config[2].children.find(
+          const terrainLayer = this.server_config[3].children.find(
             layer => layer.type === config.terrainLayerType
           );
           if (terrainLayer) {
+            console.log('恢复地形:', terrainLayer.name);
             await new Promise(resolve => {
               this.addOnlineTerrain(terrainLayer);
               setTimeout(resolve, 500);
@@ -2111,10 +2147,10 @@ export default {
           }
         }
 
-        // 恢复公共服务
+        // 恢复公共服务 (server_config[1] 是公共服务)
         if (loadedServices && loadedServices.length > 0) {
           for (const webService of loadedServices) {
-            const service = this.server_config[0].children.find(
+            const service = this.server_config[1].children.find(
               s => s.name === webService.name
             );
             if (service) {
@@ -2234,8 +2270,9 @@ export default {
         console.log('全局点击 - picked:', picked);
         
         // ========== MVT 矢量瓦片点击检测 ==========
-        // MVT 无法被 scene.pick() 拾取,无论 picked 为何值都尝试查询
-        if (this._hasMvtLayer && this.mvtClickHandler) {
+        // MVT 无法被 scene.pick() 拾取,只在未拾取到任何实体时才尝试 MVT 查询,
+        // 避免与 GeoJSON 要素、模型等其他实体的点击事件冲突
+        if (this._hasMvtLayer && this.mvtClickHandler && !Cesium.defined(picked)) {
           // 获取点击位置经纬度
           const ray = window.viewer.camera.getPickRay(movement.position);
           const cartesian = window.viewer.scene.globe.pick(ray, window.viewer.scene);
@@ -2245,10 +2282,11 @@ export default {
             const lon = Cesium.Math.toDegrees(cartographic.longitude);
             const lat = Cesium.Math.toDegrees(cartographic.latitude);
             
+            // 遍历所有已加载的 MVT 图层进行查询
             await this.mvtClickHandler.handleClick(lon, lat, {
               x: movement.position.x,
               y: movement.position.y
-            });
+            }, this._mvtTileUrls || []);
           }
         }