|
@@ -16,216 +16,412 @@
|
|
|
import { PbfReader as Pbf } from 'pbf';
|
|
import { PbfReader as Pbf } from 'pbf';
|
|
|
import { VectorTile } from '@mapbox/vector-tile';
|
|
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);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ==================== 弹窗控制 ====================
|
|
// ==================== 弹窗控制 ====================
|