DSDSWeb/src/utils/mapbox-utils.js

331 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Vue from 'vue'
import { loadVectorLayer } from '@/utils/vector-layer-utils'
export function initMapbox(mapId, options = {}) {
const map = new window.mapboxgl.Map({
container: mapId,
// style: 'mapbox://styles/mapbox/standard',
style: {
"version": 8,
"name": "default_style",
// mapbox地图使用的图标
"sprite": `${Vue.config.baseUrl}/oe/${Vue.config.wwwrootBaseUrl}/sprites/sprite`,
// 使用官方标注字体(需联网)
"glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf",
// 自部署标注字体无需联网glyphs 前有两个斜杠时可能导致404
// "glyphs": `${Vue.config.baseUrl}/oe/${Vue.config.wwwrootBaseUrl}/glyphs/{fontstack}/{range}.pbf`,
"sources": {
"影像地图": {
"type": "raster",
"tiles": [
Vue.config.tiles[options.tileKey || "gaode"]
],
"tileSize": 256,
// "minzoom": 0,
// "maxzoom": baseMapMaxZoom
}
},
"layers": [
{
"id": "影像地图",
"type": "raster",
"source": "影像地图",
"layout": { visibility: "visible" },
// "minzoom": 0,
// "maxzoom": baseMapMaxZoom
}
]
},
config: {
basemap: {
theme: 'monochrome',
lightPreset: 'night'
}
},
zoom: options.zoom || 5,
center: options.center || [112, 20],
minZoom: options.minZoom || 2,
maxZoom: options.maxZoom || 29,
trackResize: true,
scrollZoom: false, // 禁用滚轮缩放
// 地图默认字体
localIdeographFontFamily: "Microsoft YoHei, 'Noto Sans', 'Noto Sans CJK SC', sans-serif",
pitch: 0,
bearing: 0,
// 旋转和俯仰角控制
dragRotate: options.dragRotate || false,
touchZoomRotate: options.touchZoomRotate || false,
pitchWithRotate: options.pitchWithRotate || false,
// maxBounds: [[-180, -90], [180, 90]],// Set the map's geographical boundaries.
antialias: true,
attributionControl: false,
boxZoom: true,//如果为true则启用了“缩放框”交互按住shift并在地图上拖框放大
preserveDrawingBuffer: false,//如果为true地图的canvas可以使用导出到png通过map.getCanvas().toDataURL()。默认为false会提高地图性能。
// projection: 'equirectangular', // 设置投影方式经纬度直投解决缩放级别大于26时瓦片消失的问题
});
Vue.config.maps[mapId] = map;
// 确保窗口大小改变时,地图能正确铺满容器
window.addEventListener('resize', (event) => {
map.resize();
// // maps.forEach((key: string, value: any) => {
// // console.log(`${key}: ${value}`);
// // });
// for (let [key, value] of Object.entries(maps)) {
// // value.resize();
// }
// Object.values(maps).forEach((map) => {
// map.resize();
// });
});
// handleWheel("voyageMap_ship");
Vue.config.maps[mapId].scrollZoom.enable();
initSprites(mapId)
map.on('load', () => {
// 加载海岸线
if (options.coastlineOn || false)
loadVectorLayer("GSHHS_f_L1", ["line"], ["#5c71c3"], "visible", mapId, 'ougp');
// 加载等深线
if (options.contourOn || false) {
loadVectorLayer("contour-4500m", ["line"], ["#FEFEFE"], "visible", mapId, 'ougp');
loadVectorLayer("contour-6000m", ["line"], ["#FEFEFE"], "visible", mapId, 'ougp');
}
});
}
/**
* 获取所有图层信息
* @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 = "homeMap") {
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 = "homeMap") {//../../../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: () => { }
};
}