2025-12-25 16:45:57 +08:00
|
|
|
|
import Vue from 'vue'
|
2026-04-02 18:04:40 +08:00
|
|
|
|
import { loadVectorLayer } from '@/utils/vector-layer-utils'
|
2025-12-25 16:45:57 +08:00
|
|
|
|
|
2026-04-02 14:29:34 +08:00
|
|
|
|
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,
|
2026-04-02 18:04:40 +08:00
|
|
|
|
// 旋转和俯仰角控制
|
|
|
|
|
|
dragRotate: options.dragRotate || false,
|
|
|
|
|
|
touchZoomRotate: options.touchZoomRotate || false,
|
|
|
|
|
|
pitchWithRotate: options.pitchWithRotate || false,
|
2026-04-02 14:29:34 +08:00
|
|
|
|
// 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', () => {
|
|
|
|
|
|
// 加载海岸线
|
2026-04-02 18:04:40 +08:00
|
|
|
|
if (options.coastlineOn || false)
|
|
|
|
|
|
loadVectorLayer("GSHHS_f_L1", ["line"], ["#5c71c3"], "visible", mapId, 'ougp');
|
2026-04-02 14:29:34 +08:00
|
|
|
|
// 加载等深线
|
2026-04-02 18:04:40 +08:00
|
|
|
|
if (options.contourOn || false) {
|
|
|
|
|
|
loadVectorLayer("contour-4500m", ["line"], ["#FEFEFE"], "visible", mapId, 'ougp');
|
|
|
|
|
|
loadVectorLayer("contour-6000m", ["line"], ["#FEFEFE"], "visible", mapId, 'ougp');
|
|
|
|
|
|
}
|
2026-04-02 14:29:34 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 16:45:57 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有图层信息
|
|
|
|
|
|
* @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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 18:04:40 +08:00
|
|
|
|
export function handleWheel(mapId = "homeMap") {
|
2025-12-25 16:45:57 +08:00
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 18:04:40 +08:00
|
|
|
|
export function initSprites(mapId = "homeMap") {//../../../static/images/logo.png
|
2025-12-25 16:45:57 +08:00
|
|
|
|
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" }) || [];
|
2026-04-02 14:29:34 +08:00
|
|
|
|
console.log(sourceId, ">>>>>>>>>>>>>>>>>>>>sample_station_TS-46-1_Ship_CTDI_SY", ' ======sample of features:', feats.slice(0, 5));
|
2025-12-25 16:45:57 +08:00
|
|
|
|
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: () => { }
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|