mvtClickHandler.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /**
  2. * MVT 要素属性查询工具
  3. *
  4. * 由于数据服务(REST)的空间查询对 ShapeFile 数据源失效,
  5. * 改为直接从 MVT 瓦片(PBF)中解析要素属性。
  6. *
  7. * 流程:
  8. * 1. 点击位置 → WGS84 经纬度
  9. * 2. 经纬度 → 瓦片坐标 (z, x, y)
  10. * 3. 下载 .mvt 瓦片
  11. * 4. 解析 PBF,提取要素
  12. * 5. 点-面包含检测 → 获取属性
  13. */
  14. // 动态导入 PBF/MVT 解析库(pbf v5 导出 PbfReader,@mapbox/vector-tile 在 new Pbf() 时传入 Reader)
  15. import { PbfReader as Pbf } from 'pbf';
  16. import { VectorTile } from '@mapbox/vector-tile';
  17. class MvtClickHandler {
  18. constructor(vueInstance, options = {}) {
  19. this.vueInstance = vueInstance;
  20. // MVT 瓦片服务地址(从配置获取,默认使用土壤图层)
  21. this.tileUrlTemplate = options.tileUrl || 'http://localhost:8090/iserver/services/fj_soli_vector_tile/restjsr/v1/vectortile/maps/soil/tiles/{z}/{x}/{y}.mvt';
  22. // 备用:数据服务查询(已确认空间查询不生效,作为最后降级)
  23. this.serviceRootUrl = options.serviceRoot || '';
  24. }
  25. addMvtClickHandler() {}
  26. /**
  27. * 根据经纬度查询要素属性
  28. */
  29. async handleClick(lon, lat, screenPos) {
  30. console.log(`\n[MVT] 开始查询: 经度=${lon.toFixed(6)}, 纬度=${lat.toFixed(6)}`);
  31. // 方案1:从 MVT 瓦片解析(主要方案)
  32. const tileResult = await this._queryFromTile(lon, lat);
  33. if (tileResult && Object.keys(tileResult).length > 0) {
  34. console.log("[MVT] === 瓦片查询成功 ===", tileResult);
  35. this.showPopup({
  36. title: tileResult.type || tileResult.TURANG_NAME || "土壤要素",
  37. attributes: tileResult,
  38. position: screenPos
  39. });
  40. return;
  41. }
  42. console.log("[MVT] 瓦片未命中,该点无土壤要素数据");
  43. this.setPopupVisible(false);
  44. }
  45. /**
  46. * 从 MVT 瓦片查询要素属性
  47. */
  48. async _queryFromTile(lon, lat) {
  49. try {
  50. // 先在 zoom=14 查询(细节适中),没命中再降级
  51. const zoomLevels = [14, 13, 12, 11, 10];
  52. for (const z of zoomLevels) {
  53. const tileX = this._lonToTile(lon, z);
  54. const tileY = this._latToTile(lat, z);
  55. const tileUrl = this.tileUrlTemplate
  56. .replace('{z}', z)
  57. .replace('{x}', tileX)
  58. .replace('{y}', tileY);
  59. // 下载瓦片
  60. const response = await fetch(tileUrl);
  61. if (!response.ok) continue;
  62. const arrayBuffer = await response.arrayBuffer();
  63. if (!arrayBuffer || arrayBuffer.byteLength === 0) continue;
  64. // 解析 PBF
  65. const tile = new VectorTile(new Pbf(arrayBuffer));
  66. console.log(`[MVT] zoom=${z} 瓦片(${tileX},${tileY}) 图层:`, Object.keys(tile.layers));
  67. // 遍历所有图层找匹配要素
  68. for (const layerName of Object.keys(tile.layers)) {
  69. const layer = tile.layers[layerName];
  70. console.log(`[MVT] 图层 "${layerName}": ${layer.length} 个要素`);
  71. // 获取瓦片范围(WGS84)
  72. const tileBounds = this._getTileBounds(z, tileX, tileY);
  73. for (let i = 0; i < layer.length; i++) {
  74. const feature = layer.feature(i);
  75. // 只有当要素是面类型时才检查包含关系
  76. if (feature.type !== 3) continue; // 3 = Polygon, 4 = MultiPolygon
  77. // 将要素坐标从瓦片坐标转为 WGS84
  78. const geoJsonGeom = this._tileToGeoJSON(feature, tileBounds, layer.extent);
  79. // 检查点是否在面内
  80. if (this._pointInPolygon(lon, lat, geoJsonGeom.coordinates)) {
  81. console.log(`[MVT] 命中! zoom=${z}, 图层=${layerName}, 要素索引=${i}`);
  82. // 提取属性
  83. const props = {};
  84. const keys = Object.keys(feature.properties || {});
  85. // 优先取已知字段
  86. const knownFields = ['FID_', 'SOIL_', 'SOIL_ID', 'DL', 'type', 'SmID', 'SMID'];
  87. const allFields = [...new Set([...knownFields, ...keys])];
  88. allFields.forEach(key => {
  89. if (feature.properties[key] !== undefined) {
  90. props[key] = feature.properties[key];
  91. }
  92. });
  93. console.log(`[MVT] 属性:`, props);
  94. return props;
  95. }
  96. }
  97. }
  98. }
  99. return null;
  100. } catch (err) {
  101. console.error("[MVT] 瓦片查询异常:", err.message);
  102. return null;
  103. }
  104. }
  105. // ==================== 坐标转换工具 ====================
  106. /**
  107. * 经度 → 瓦片 X 坐标
  108. */
  109. _lonToTile(lon, zoom) {
  110. return Math.floor((lon + 180) / 360 * Math.pow(2, zoom));
  111. }
  112. /**
  113. * 纬度 → 瓦片 Y 坐标
  114. */
  115. _latToTile(lat, zoom) {
  116. 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));
  117. }
  118. /**
  119. * 获取瓦片的 WGS84 范围
  120. */
  121. _getTileBounds(z, x, y) {
  122. const n = Math.pow(2, z);
  123. const west = x / n * 360 - 180;
  124. const east = (x + 1) / n * 360 - 180;
  125. const north = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) * 180 / Math.PI;
  126. const south = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n))) * 180 / Math.PI;
  127. return { west, east, north, south };
  128. }
  129. /**
  130. * 将 MVT 要素的瓦片坐标转为 WGS84 坐标
  131. */
  132. _tileToGeoJSON(feature, bounds, extent) {
  133. extent = extent || 4096;
  134. const scaleX = (bounds.east - bounds.west) / extent;
  135. const scaleY = (bounds.south - bounds.north) / extent;
  136. const transformCoords = (coords) => {
  137. return coords.map(ring => {
  138. return ring.map(([x, y]) => [
  139. bounds.west + x * scaleX,
  140. bounds.north + y * scaleY // MVT y 轴向下,WGS84 y 轴向上
  141. ]);
  142. });
  143. };
  144. // 从 feature 获取几何(MVT 是 Geometry类型,需要处理)
  145. let geom;
  146. try {
  147. // feature.loadGeometry() 返回 [Ring, Ring, ...]
  148. // 每个 Ring 是 [{x, y}, ...]
  149. const rawGeom = feature.loadGeometry();
  150. if (feature.type === 3) {
  151. // Polygon: 第一个 ring 是外环,后续是内环
  152. geom = {
  153. type: 'Polygon',
  154. coordinates: transformCoords(rawGeom.map(ring =>
  155. ring.map(p => [p.x, p.y])
  156. ))
  157. };
  158. } else if (feature.type === 4) {
  159. // MultiPolygon 需要分组
  160. // 简单处理:把所有 rings 当做一个 polygon
  161. geom = {
  162. type: 'Polygon',
  163. coordinates: transformCoords(rawGeom.map(ring =>
  164. ring.map(p => [p.x, p.y])
  165. ))
  166. };
  167. }
  168. } catch (e) {
  169. return null;
  170. }
  171. return geom;
  172. }
  173. /**
  174. * 射线法判断点是否在多边形内
  175. */
  176. _pointInPolygon(lon, lat, coordinates) {
  177. if (!coordinates || !coordinates.length) return false;
  178. // 检查外环
  179. const ring = coordinates[0];
  180. if (!ring || ring.length < 3) return false;
  181. let inside = false;
  182. for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
  183. const xi = ring[i][0], yi = ring[i][1];
  184. const xj = ring[j][0], yj = ring[j][1];
  185. if ((yi > lat) !== (yj > lat) &&
  186. lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) {
  187. inside = !inside;
  188. }
  189. }
  190. return inside;
  191. }
  192. // ==================== 弹窗控制 ====================
  193. setPopupVisible(flag) {
  194. const target = this.vueInstance.mvtPopupVisible;
  195. if (typeof target === "object" && target.value !== undefined) {
  196. target.value = flag;
  197. } else {
  198. this.vueInstance.mvtPopupVisible = flag;
  199. }
  200. }
  201. showPopup(params) {
  202. const setVal = (key, val) => {
  203. const prop = this.vueInstance[key];
  204. if (typeof prop === "object" && prop && prop.value !== undefined) {
  205. prop.value = val;
  206. } else {
  207. this.vueInstance[key] = val;
  208. }
  209. };
  210. setVal("mvtPopupTitle", params.title);
  211. setVal("mvtPopupAttributes", params.attributes);
  212. setVal("mvtPopupPosition", params.position);
  213. this.setPopupVisible(true);
  214. }
  215. }
  216. export default MvtClickHandler;