diff --git a/src/utils/mapbox-utils.js b/src/utils/mapbox-utils.js new file mode 100644 index 0000000..d08e2c0 --- /dev/null +++ b/src/utils/mapbox-utils.js @@ -0,0 +1,236 @@ +import Vue from 'vue' + +/** + * 获取所有图层信息 + * @param {mapboxgl.Map} map - Mapbox 地图实例 + * @returns {Array} 图层信息数组 + */ +export function getAllLayers(map) { + if (!map || !map.getStyle()) { + return []; + } + + const style = map.getStyle(); + return style.layers || []; +} + +/** + * 安全地设置图层属性 + * @param {mapboxgl.Map} map - Mapbox 地图实例 + * @param {string} layerId - 图层ID + * @param {string} property - 属性名 + * @param {any} value - 属性值 + * @returns {boolean} + */ +export function safeSetPaintProperty(map, layerId, property, value) { + if (!map.getLayer(layerId)) { + console.warn(`图层 ${layerId} 不存在,无法设置属性`); + return false; + } + + try { + map.setPaintProperty(layerId, property, value); + return true; + } catch (error) { + console.error(`设置图层 ${layerId} 属性 ${property} 失败:`, error); + return false; + } +} + +export function handleWheel(mapId = "home") { + document.addEventListener("keydown", () => { + // 允许鼠标滚轮缩放 + Vue.config.maps[mapId].scrollZoom.enable(); + }, { once: false, passive: false }); + document.addEventListener("keyup", () => { + // 禁止鼠标滚轮缩放 + Vue.config.maps[mapId].scrollZoom.disable(); + }, { once: false, passive: false }); +} + +export function initSprites(mapId = "home") {//../../../static/images/logo.png + for (const sprite of Vue.config.sprites) { + // Vue.config.maps[mapId].loadImage(`${Vue.config.baseUrl}/oe/${Vue.config.wwwrootBaseUrl}/sprites/${sprite}`, function (error, image) { + Vue.config.maps[mapId].loadImage(`./static/sprites/${sprite}`, function (error, image) { + if (error) throw error; + if (!Vue.config.maps[mapId].hasImage(sprite)) + // 为图像启用 sdf: { sdf: true },启用的 sdf 的图像,在 Symbol 图层中就能够通过 icon-color 属性来重新渲染图像的颜色。 + Vue.config.maps[mapId].addImage(sprite, image, { sdf: false }); + }); + } +} + +export async function extractWMTSBoundsInfo(xmlContent) { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlContent, "text/xml"); + + const featureTypes = xmlDoc.getElementsByTagName("Layer"); + const result = []; + + for (let i = 0; i < featureTypes.length; i++) { + const featureType = featureTypes[i]; + + // 提取 Name + const nameElement = featureType.getElementsByTagName("ows:Identifier")[0]; + const name = nameElement ? nameElement.textContent || "" : ""; + + // 提取 WGS84BoundingBox + const wgs84BoundingBox = featureType.getElementsByTagName("ows:WGS84BoundingBox")[0]; + + if (wgs84BoundingBox) { + const lowerCorner = wgs84BoundingBox.getElementsByTagName("ows:LowerCorner")[0].textContent; + const upperCorner = wgs84BoundingBox.getElementsByTagName("ows:UpperCorner")[0].textContent; + if (name) { + result.push({ + layerName: name, + bounds: [Number(lowerCorner.split(" ")[0]), Number(lowerCorner.split(" ")[1]), Number(upperCorner.split(" ")[0]), Number(upperCorner.split(" ")[1])] + }); + } + } + } + + return result; +} + +/** + * 触发指定图层的点击事件 + * @param {mapboxgl.Map} map - Mapbox地图实例 + * @param {string} layerId - 要触发的图层ID + * @param {Array} lnglat - 经纬度 [经度, 纬度] + * @param {Object} options - 附加选项 + */ +export async function triggerLayerClick(mapId, sourceLayerId, layerId, lnglat, options = {}) { + if (!Vue.config.maps[mapId] || !layerId || !lnglat) { + console.error('缺少必要参数'); + return null; + } + + const [lng, lat] = Array.isArray(lnglat) ? lnglat : [lnglat.lng, lnglat.lat]; + + // 1. 确保地图和图层存在 + if (!Vue.config.maps[mapId].getLayer(layerId)) { + console.error(`图层 ${layerId} 不存在`); + return null; + } + + // 2. 将经纬度转换为屏幕坐标 + const point = Vue.config.maps[mapId].project([lng, lat]); + + // 3. 查询该点/源下的图层要素 + const bbox = [ + [point.x - 1, point.y - 1], + [point.x + 1, point.y + 1] + ]; + + const map = Vue.config.maps[mapId]; + console.log("layerId: ", layerId, " sourceLayerId: ", sourceLayerId, " options: ", options); + + // 尝试从图层获取 source id(一些图层的 source 名称与 layerId 不同) + const layerObj = map.getLayer(layerId); + const sourceId = (layerObj && layerObj.source) ? layerObj.source : layerId; + const source = map.getSource(sourceId); + + // 在浏览器控制台或 triggerLayerClick 中运行(替换 sourceId/sourceLayerName) + const feats = map.querySourceFeatures(sourceId, { sourceLayer: "sample_station_TS-46-1_Ship_CTDI_SY" }) || []; + console.log(sourceId,">>>>>>>>>>>>>>>>>>>>sample_station_TS-46-1_Ship_CTDI_SY",' ======sample of features:', feats.slice(0, 5)); + console.log('props keys:', feats.slice(0, 5).map(f => Object.keys(f.properties || {}))); + + let relatedTracks = map.querySourceFeatures(sourceId, { + sourceLayer: sourceId, + // 支持的过滤条件:==, !=, >, >=, <, <=, in, !in, all, any, none, has, !has + filter: ['==', ['get', 'sample_name'], options.sample_name] + }); + console.log("relatedTracks: ", relatedTracks); + + // 打印样式中与该 source 关联的所有 source-layer(便于调试 vector tiles 的 layer 名称) + try { + const styleLayers = map.getStyle().layers || []; + const sourceLayerNames = new Set(); + styleLayers.forEach(l => { + if (l.source === sourceId && l['source-layer']) sourceLayerNames.add(l['source-layer']); + }); + console.log('candidate source-layer names for', sourceId, Array.from(sourceLayerNames)); + } catch (err) { + console.warn('failed to enumerate style layers for debugging:', err); + } + + // 使用表达式 ['==', ['get', prop], value] 来做精确匹配 + const propName = 'sample_name'; + const propValue = options.sample_name; + const filterExpr = ['==', ['get', propName], propValue]; + // const filterExpr = ['==', propName, propValue]; + + let features = []; + try { + if (source) { + // 如果 source 尚未加载完,等待一次 sourcedata(有时 vector tiles 尚未请求完成) + if (!map.isSourceLoaded(sourceId)) { + console.log('source not loaded yet, waiting for sourcedata:', sourceId); + await new Promise((resolve) => { + const onData = (e) => { + if (e && e.sourceId === sourceId && map.isSourceLoaded(sourceId)) { + map.off('sourcedata', onData); + resolve(); + } + }; + // 超时保护,2秒后继续 + const timeout = setTimeout(() => { + map.off('sourcedata', onData); + resolve(); + }, 2000); + map.on('sourcedata', onData); + }); + } + + console.log("sourceId: ", sourceId, " sourceLayerId: ", sourceLayerId, " options: ", options); + // 优先使用 querySourceFeatures(对 vector/geojson 源性能更好) + features = map.querySourceFeatures(sourceId, { sourceLayer: sourceLayerId, filter: filterExpr }) || []; + } + } catch (err) { + console.warn('querySourceFeatures failed:', err); + } + + // 如果通过 source 查询不到,再退回到渲染层查询(rendered features)作为兜底 + if (!features || features.length === 0) { + try { + features = map.queryRenderedFeatures(bbox, { layers: [layerId], filter: filterExpr }) || []; + } catch (err) { + console.warn('queryRenderedFeatures fallback failed:', err); + features = []; + } + } + + console.log('resolved sourceId:', sourceId, 'found features:', features.length); + + // 4. 创建事件对象 + const eventObject = { + lngLat: { lng, lat }, + point: point, + features: features, + options: options, + originalEvent: createSimulatedEvent(point), + target: Vue.config.maps[mapId].getCanvas(), + type: 'click', + layerId: layerId, + timestamp: Date.now() + }; + + // 5. 触发图层特定的点击事件 + Vue.config.maps[mapId].fire('click', eventObject, { layerId: layerId }); + + return { + event: eventObject, + features: features, + success: true + }; +} + +function createSimulatedEvent(point) { + return { + type: 'click', + clientX: point.x, + clientY: point.y, + preventDefault: () => { }, + stopPropagation: () => { } + }; +} \ No newline at end of file