| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- /**
- * 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;
|