/** * MVT 要素属性查询工具 * * 由于数据服务(REST)的空间查询对 ShapeFile 数据源失效, * 改为直接从 MVT 瓦片(PBF)中解析要素属性。 * * 流程: * 1. 点击位置 → WGS84 经纬度 * 2. 经纬度 → 瓦片坐标 (z, x, y) * 3. 下载 .mvt 瓦片 * 4. 解析 PBF,提取要素 * 5. 点-面包含检测 → 获取属性 */ // 动态导入 PBF/MVT 解析库(pbf v5 导出 PbfReader,@mapbox/vector-tile 在 new Pbf() 时传入 Reader) 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'; // 备用:数据服务查询(已确认空间查询不生效,作为最后降级) this.serviceRootUrl = options.serviceRoot || ''; } addMvtClickHandler() {} /** * 根据经纬度查询要素属性 */ async handleClick(lon, lat, screenPos) { console.log(`\n[MVT] 开始查询: 经度=${lon.toFixed(6)}, 纬度=${lat.toFixed(6)}`); // 方案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; } console.log("[MVT] 瓦片未命中,该点无土壤要素数据"); this.setPopupVisible(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; } } } } return null; } catch (err) { console.error("[MVT] 瓦片查询异常:", err.message); return null; } } // ==================== 坐标转换工具 ==================== /** * 经度 → 瓦片 X 坐标 */ _lonToTile(lon, zoom) { return Math.floor((lon + 180) / 360 * Math.pow(2, zoom)); } /** * 纬度 → 瓦片 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)); } /** * 获取瓦片的 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 }; } /** * 将 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]) )) }; } } catch (e) { return null; } return geom; } /** * 射线法判断点是否在多边形内 */ _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; } } return inside; } // ==================== 弹窗控制 ==================== setPopupVisible(flag) { const target = this.vueInstance.mvtPopupVisible; if (typeof target === "object" && target.value !== undefined) { target.value = flag; } else { this.vueInstance.mvtPopupVisible = flag; } } showPopup(params) { const setVal = (key, val) => { const prop = this.vueInstance[key]; if (typeof prop === "object" && prop && prop.value !== undefined) { prop.value = val; } else { this.vueInstance[key] = val; } }; setVal("mvtPopupTitle", params.title); setVal("mvtPopupAttributes", params.attributes); setVal("mvtPopupPosition", params.position); this.setPopupVisible(true); } } export default MvtClickHandler;