754 lines
30 KiB
JavaScript
754 lines
30 KiB
JavaScript
import Vue from 'vue'
|
||
import { regInfo } from '@/api/dataSearch'
|
||
|
||
let sourceLayer = undefined;
|
||
let clickedLineIds = [];
|
||
|
||
// 存储可复用的每个地图对象的唯一 Popup 实例(key 为 mapId),以避免频繁创建/销毁 Popup 导致的性能问题和潜在内存泄漏
|
||
let sharedPopup = {};
|
||
// 存储上一条点击的线
|
||
let lastLineId;
|
||
|
||
// 默认的线宽表达式
|
||
export let lineWidthExpression = [
|
||
"case",
|
||
["boolean", ["feature-state", "hover"], false],
|
||
4,
|
||
["boolean", ["feature-state", "click"], false],
|
||
2,
|
||
1.8
|
||
];
|
||
|
||
export function setMapLayoutProperty(mapId, visibility = "none") {
|
||
// 设置所有图层的显示/隐藏状态(根据图层类型或名称过滤底图图层,避免误操作)
|
||
const layers = Vue.config.maps[mapId].getStyle().layers;
|
||
layers.forEach(layer => {
|
||
// 跳过底图
|
||
if (layer.id !== '影像地图' && layer.type !== 'raster') {
|
||
Vue.config.maps[mapId].setLayoutProperty(layer.id, 'visibility', visibility);
|
||
}
|
||
});
|
||
}
|
||
|
||
export function setMapPaintProperty({ mapId, property, value }) {
|
||
const layers = Vue.config.maps[mapId].getStyle().layers;
|
||
layers.forEach(layer => {
|
||
// 跳过底图和非测线图层
|
||
if (layer.id !== '影像地图' && layer.type !== 'raster' && layer.id.endsWith("-line")) {
|
||
Vue.config.maps[mapId].setPaintProperty(layer.id, property, lineWidthExpression);
|
||
}
|
||
});
|
||
// Vue.config.maps[mapId].setPaintProperty(layerId, "line-width", 14);
|
||
}
|
||
|
||
export function loadVectorLayer({ layerId, features = ["line", "symbol"], colors = ["#FEFEFE", "#FF0000"], visibility = "visible", mapId = "homeMap", workspace = 'dsds', textField = "name", callbackOnClick }) {
|
||
// 添加数据源
|
||
const sourceId = `vector-${layerId}`;
|
||
let source = Vue.config.maps[mapId].getSource(sourceId);
|
||
if (!source) {
|
||
// request vector tiles with extra buffer to avoid geometry clipping at tile edges
|
||
const tilesUrl = `${Vue.config.baseUrl}/geoserver/gwc/service/tms/1.0.0/${workspace}:${layerId}@EPSG:900913@pbf/{z}/{x}/{y}.pbf?buffer=64`;
|
||
let bb = Vue.config.boundsInfoList.find(x => x.layerName.toLowerCase() == `${workspace}:${layerId}`.toLocaleLowerCase());
|
||
let bounds = bb ? bb.bounds : null;
|
||
if (bounds) {
|
||
Vue.config.maps[mapId].addSource(sourceId, {
|
||
type: 'vector',
|
||
scheme: 'tms',
|
||
tiles: [tilesUrl],
|
||
minzoom: 0,
|
||
maxzoom: 30,
|
||
bounds: bounds,
|
||
generateId: true
|
||
});
|
||
}
|
||
else {
|
||
Vue.config.maps[mapId].addSource(sourceId, {
|
||
type: 'vector',
|
||
scheme: 'tms',
|
||
tiles: [tilesUrl],
|
||
minzoom: 0,
|
||
maxzoom: 30,
|
||
generateId: true
|
||
});
|
||
}
|
||
}
|
||
|
||
let i = 0;
|
||
features.forEach((feature) => {
|
||
loadVectorFeature({ layerId, feature, color: colors[i++], visibility, mapId, textField, callbackOnClick });
|
||
});
|
||
}
|
||
|
||
export async function loadVectorFeature({ layerId, feature, color, visibility, mapId, textField = "name", callbackOnClick }) {
|
||
let sourceId = `vector-${layerId}`;
|
||
// symbol
|
||
// Slot 是 Mapbox 样式规范中的一个概念,它定义了图层在渲染过程中的位置和顺序。
|
||
let slot = "foreground";
|
||
let layout = {
|
||
"symbol-placement": "point",
|
||
"text-rotation-alignment": "auto",
|
||
"text-field": ["get", textField],
|
||
"text-font": ["literal", ["Arial Unicode MS Regular", "DIN Offc Pro Italic", "Open Sans Regular"]],
|
||
"text-size": 12,
|
||
"text-variable-anchor": ["top", "bottom", "left", "right"],
|
||
"text-radial-offset": 1,
|
||
"text-justify": "auto",
|
||
"text-optional": true,
|
||
"text-allow-overlap": false,
|
||
"text-ignore-placement": false,
|
||
'symbol-spacing': 1000, // 像素间距,增大值可减少标注密度
|
||
// 'text-max-angle': 300, // 角度越大,标注越少
|
||
"icon-optional": true,
|
||
"icon-allow-overlap": false,
|
||
"icon-ignore-placement": false,
|
||
"icon-image": "dot.png",
|
||
"icon-size": 0.5,
|
||
"visibility": visibility
|
||
};
|
||
let paint = {
|
||
// "text-color": color,
|
||
"text-halo-color": "#FFFFFF",
|
||
"text-halo-width": 0.1,
|
||
"icon-opacity": 1,
|
||
"text-color": [
|
||
"case",
|
||
["boolean", ["feature-state", "click"], false],
|
||
"red",
|
||
["boolean", ["feature-state", "hover"], false],
|
||
"yellow",
|
||
color
|
||
],
|
||
// slot: "middle"
|
||
};
|
||
if (feature === "line") {
|
||
slot = "top";
|
||
layout = {
|
||
"visibility": visibility,
|
||
// "line-join": "round",
|
||
// "line-cap": "round"
|
||
};
|
||
paint = {
|
||
"line-opacity": 1,
|
||
"line-width": [
|
||
"case",
|
||
["boolean", ["feature-state", "hover"], false],
|
||
4,
|
||
["boolean", ["feature-state", "click"], false],
|
||
2,
|
||
1.8
|
||
],
|
||
"line-color": [
|
||
"case",
|
||
["boolean", ["feature-state", "click"], false],
|
||
"red",
|
||
["boolean", ["feature-state", "hover"], false],
|
||
"yellow",
|
||
color
|
||
]
|
||
}
|
||
}
|
||
|
||
let layer = Vue.config.maps[mapId].getLayer(`${sourceId}-${feature}`);
|
||
// console.log(">>>>>>>>>>>>>>>>>>source-layer: ", layerId);
|
||
if (!layer) {
|
||
// console.log("Adding vector layer:", `${sourceId}-${feature}`);
|
||
Vue.config.maps[mapId].addLayer({
|
||
id: `${sourceId}-${feature}`,
|
||
'type': feature,
|
||
'source': sourceId,
|
||
'source-layer': layerId,
|
||
'layout': layout,
|
||
'paint': paint,
|
||
// 'slot': 'foreground'
|
||
});
|
||
}
|
||
else {
|
||
// 图层已存在则设置为可见
|
||
Vue.config.maps[mapId].setLayoutProperty(`${sourceId}-${feature}`, "visibility", "visible");
|
||
}
|
||
|
||
// 点击可交互的图层,显示相关信息
|
||
await setHighlight(`${sourceId}-${feature}`, sourceId, mapId);
|
||
changeCursor(`${sourceId}-${feature}`, mapId);
|
||
|
||
if (layerId.includes("sample_line"))
|
||
await showSampleLineInfo(`${sourceId}-${feature}`, mapId, callbackOnClick);
|
||
if (layerId.includes("sample_station"))
|
||
await showSampleStationInfo(`${sourceId}-${feature}`, mapId, callbackOnClick);
|
||
else if (sourceId.includes("-SY") || sourceId.includes("-FDZ")) {
|
||
await showDiveInfo(`${sourceId}-${feature}`, mapId);
|
||
console.log(mapId, `${sourceId}-${feature}`);
|
||
}
|
||
else
|
||
await showTrackInfo(`${sourceId}-${feature}`, mapId);
|
||
|
||
return sourceId;
|
||
}
|
||
|
||
export function setHighlight(layerId, sourceId, mapId = "homeMap") {
|
||
let hoveredLineId = null;
|
||
let clickedLineId = null;
|
||
Vue.config.maps[mapId].on('click', layerId, async (e) => {
|
||
sourceLayer = e.features[0].sourceLayer;
|
||
// 排除没有 sourceLayer 的情况(如点击事件中 features 可能来自多个图层,某些图层缺乏 sourceLayer 属性会导致后续 setFeatureState 报错)
|
||
if (!sourceLayer)
|
||
return;
|
||
|
||
// // 查询点击点周围的所有要素(使用边界框)
|
||
// const bbox = [
|
||
// [e.point.x - 10, e.point.y - 10], // 左上
|
||
// [e.point.x + 10, e.point.y + 10] // 右下
|
||
// ];
|
||
// // 查询所有要素
|
||
// const allFeatures = Vue.config.maps[mapId].queryRenderedFeatures(bbox);
|
||
// console.log("allFeatures: ", allFeatures);
|
||
// // // 按图层分组
|
||
// // const featuresByLayer = groupFeaturesByLayer(allFeatures);
|
||
// // console.log('点击位置附近的所有要素:', featuresByLayer);
|
||
|
||
let feature = e.features[0];
|
||
if (e.features.length > 1 && e.options)
|
||
feature = e.features.filter(f => f.properties.sample_name == e.options.sample_name)[0];
|
||
|
||
sourceLayer = feature.sourceLayer;
|
||
// 获取当前单击的线的选择状态
|
||
let state = Vue.config.maps[mapId].getFeatureState({
|
||
source: sourceId,
|
||
sourceLayer: sourceLayer,
|
||
id: feature.id
|
||
});
|
||
if (clickedLineId) {
|
||
Vue.config.maps[mapId].setFeatureState({ source: sourceId, sourceLayer: sourceLayer, id: clickedLineId }, { click: false });
|
||
}
|
||
clickedLineId = feature.id;
|
||
if (state.click === true) {
|
||
// 再次单击时取消选择
|
||
Vue.config.maps[mapId].setFeatureState({ source: sourceId, sourceLayer: sourceLayer, id: clickedLineId }, { click: false });
|
||
// 隐藏信息框
|
||
// document.getElementById("cable-overlay")!.style.display = 'none';
|
||
// 过滤掉当前单击的线
|
||
clickedLineIds = clickedLineIds.filter((item) => item !== clickedLineId);
|
||
} else {
|
||
Vue.config.maps[mapId].setFeatureState({ source: sourceId, sourceLayer: sourceLayer, id: clickedLineId }, { click: true });
|
||
// 单击时加入选择列表(用于单击 ESC 键时取消选择状态)
|
||
clickedLineIds.push(clickedLineId);
|
||
// document.removeEventListener("keydown", removeHighlight, true);
|
||
// document.addEventListener("keydown", (e) => {
|
||
// removeHighlight(e, sourceId);
|
||
// }, { once: true }); // once:一个布尔值,表示 listener 在添加之后最多只调用一次。如果为 true,listener 会在其被调用之后自动移除。
|
||
}
|
||
});
|
||
|
||
// When the user moves their mouse over the state-fill layer, we'll update the feature state for the feature under the mouse.
|
||
Vue.config.maps[mapId].on('mousemove', layerId, (e) => {
|
||
sourceLayer = e.features[0].sourceLayer;
|
||
// 排除没有 sourceLayer 的情况(如点击事件中 features 可能来自多个图层,某些图层缺乏 sourceLayer 属性会导致后续 setFeatureState 报错)
|
||
if (!sourceLayer)
|
||
return;
|
||
if (hoveredLineId) {
|
||
Vue.config.maps[mapId].setFeatureState(
|
||
{ source: sourceId, sourceLayer: sourceLayer, id: hoveredLineId },
|
||
{ hover: false }
|
||
);
|
||
}
|
||
hoveredLineId = e.features[0].id;
|
||
Vue.config.maps[mapId].setFeatureState(
|
||
{ source: sourceId, sourceLayer: sourceLayer, id: hoveredLineId },
|
||
{ hover: true }
|
||
);
|
||
});
|
||
|
||
// When the mouse leaves the state-fill layer, update the feature state of the previously hovered feature.
|
||
Vue.config.maps[mapId].on('mouseleave', layerId, () => {
|
||
if (hoveredLineId && sourceLayer) {
|
||
Vue.config.maps[mapId].setFeatureState(
|
||
{ source: sourceId, sourceLayer: sourceLayer, id: hoveredLineId },
|
||
{ hover: false }
|
||
);
|
||
}
|
||
hoveredLineId = null;
|
||
});
|
||
}
|
||
|
||
export async function showUnitReport(mapId, unit, coordinate, callbackOnClose = null) {
|
||
const result = await (await fetch(`${Vue.config.baseUrl}/ds/report/unit/total.htm?unit=${unit}`)).json();
|
||
const unitReports = result.array || [];
|
||
if (!unitReports.length) {
|
||
alert("未查询到相关单位的统计信息");
|
||
return;
|
||
}
|
||
|
||
const description = `
|
||
<table class="popup-table" style="min-width:280px;">
|
||
<thead>
|
||
<tr>
|
||
<td colspan="2">单位参航信息</td>
|
||
</tr>
|
||
</thead>
|
||
<tr>
|
||
<td colspan="2">单位名称: <b>${unitReports[0].unit_name}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>参航次数: <b>${unitReports[0].voyage_total || "/"}</b></td>
|
||
<td>参航天数: <b>${unitReports[0].voyage_days || "/"}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>参航人数: <b>${unitReports[0].member_total || "/"}</b></td>
|
||
<td>下潜次数: <b>${unitReports[0].drving_total || "/"}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>首潜人数: <b>${unitReports[0].f_drving_total || "/"}</b></td>
|
||
<td>首次参航人数: <b>${unitReports[0].f_voyage_total || "/"}</b></td>
|
||
</tr>
|
||
</table>`;
|
||
|
||
showPopupDetails({ mapId, coordinate, description, maxWidth: "300px", callbackOnClose: callbackOnClose });
|
||
}
|
||
|
||
export function changeCursor(layerId, mapId = "homeMap", cursor = "pointer") {
|
||
Vue.config.maps[mapId].on("mouseenter", layerId, () => {
|
||
Vue.config.maps[mapId].getCanvas().style.cursor = cursor;
|
||
});
|
||
|
||
Vue.config.maps[mapId].on("mouseleave", layerId, () => {
|
||
Vue.config.maps[mapId].getCanvas().style.cursor = "";
|
||
});
|
||
}
|
||
|
||
export function showSampleLineInfo(layerId, mapId = "homeMap", callbackOnClick = null) {
|
||
Vue.config.maps[mapId].on("click", layerId, async (e) => {
|
||
// 在左侧列表中高亮对应项
|
||
if (callbackOnClick && typeof callbackOnClick === "function") {
|
||
callbackOnClick(e.features[0]);
|
||
}
|
||
|
||
// 禁止点击事件穿透 - 判断同一个 event 是否已经触发,用于避免图层内标记点注册的点击事件重复触发(点位分布密集时容易出现)
|
||
if (e.defaultPrevented)
|
||
return;
|
||
// 标记该 event 已触发,禁止点击事件穿透
|
||
if (typeof e.preventDefault === "function")
|
||
e.preventDefault();
|
||
// console.log("showSampleLineInfo: ", layerId, e.features[0]);
|
||
|
||
const description = `
|
||
<table class="popup-table" style="min-width:380px;">
|
||
<thead>
|
||
<tr>
|
||
<td colspan="2">测线信息</td>
|
||
</tr>
|
||
</thead>
|
||
<tr>
|
||
<td>数据集名称: <b>${e.features[0].properties.dataset_name}</b></td>
|
||
<td>数据样品编号: <b>${e.features[0].properties.sample_name}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>赞助机构: <b>${e.features[0].properties.funding_org}</b></td>
|
||
<td>公开期限: <b>${e.features[0].properties.publish_rule}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>开始经度: <b>${e.features[0].properties.start_lon}°</b></td>
|
||
<td>结束经度: <b>${e.features[0].properties.end_lon}°</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>开始纬度: <b>${e.features[0].properties.start_lat}°</b></td>
|
||
<td>结束纬度: <b>${e.features[0].properties.end_lat}°</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>开始时间: <b>${e.features[0].properties.start_time}</b></td>
|
||
<td>结束时间: <b>${e.features[0].properties.end_time}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>测线深度: <b>${e.features[0].properties.line_deep}m</b></td>
|
||
<td>数据状态: <b>${e.features[0].properties.sample_status}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2">备注信息: <b>${e.features[0].properties.sample_remark}</b></td>
|
||
</tr>
|
||
</table>`;
|
||
|
||
showPopupDetails({ mapId, coordinate: e.lngLat, description, maxWidth: "450px" });
|
||
});
|
||
}
|
||
|
||
export function showSampleStationInfo(layerId, mapId = "homeMap", callbackOnClick = null) {
|
||
Vue.config.maps[mapId].on("click", layerId, async (e) => {
|
||
// 在左侧列表中高亮对应项
|
||
if (callbackOnClick && typeof callbackOnClick === "function") {
|
||
callbackOnClick(e.features[0]);
|
||
}
|
||
|
||
// 禁止点击事件穿透 - 判断同一个 event 是否已经触发,用于避免图层内标记点注册的点击事件重复触发(点位分布密集时容易出现)
|
||
if (e.defaultPrevented)
|
||
return;
|
||
// 标记该 event 已触发,禁止点击事件穿透
|
||
// if (typeof e.preventDefault === "function")
|
||
// e.preventDefault();
|
||
|
||
let feature = e.features[0];
|
||
if (e.features.length > 1)
|
||
feature = e.features.filter(f => f.properties.sample_name == e.options.sample_name)[0];
|
||
|
||
const description = `
|
||
<table class="popup-table" style="min-width:380px;">
|
||
<thead>
|
||
<tr>
|
||
<td colspan="2">站位信息</td>
|
||
</tr>
|
||
</thead>
|
||
<tr>
|
||
<td>数据集名称: <b>${feature.properties.dataset_name}</b></td>
|
||
<td>样品编号: <b>${feature.properties.sample_name}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>赞助机构: <b>${feature.properties.funding_org}</b></td>
|
||
<td>公开期限: <b>${feature.properties.publish_rule}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>站位经度: <b>${feature.properties.sampling_lon}°</b></td>
|
||
<td>站位纬度: <b>${feature.properties.sampling_lat}°</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>采样时间: <b>${feature.properties.sampling_time}</b></td>
|
||
<td>采样深度: <b>${feature.properties.sampling_deep}m</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2">备注信息: <b>${feature.properties.sample_remark}</b></td>
|
||
</tr>
|
||
</table>`;
|
||
|
||
showPopupDetails({ mapId, coordinate: feature.geometry.coordinates, description, maxWidth: "450px" });
|
||
});
|
||
}
|
||
|
||
export function showHighlightSampleInfo({ mapId, job_type, sample, layerId, sourceLayerId, callbackOnClick }) {
|
||
let description = "";
|
||
let coordinate;
|
||
if (job_type == "测线作业") {
|
||
// // 恢复其它测线的线宽
|
||
// // setMapPaintProperty({ mapId, property: "line-width", value: lineWidthExpression });
|
||
// // 恢复上一条高亮测线的宽度
|
||
// Vue.config.maps[mapId].setPaintProperty(lastLineId, "line-width", lineWidthExpression);
|
||
// // 高亮当前选择的测线
|
||
// Vue.config.maps[mapId].setPaintProperty(layerId, "line-width", 5);
|
||
|
||
// if (lastLineId)
|
||
// Vue.config.maps[mapId].setFeatureState(
|
||
// { sourceId: sourceLayerId, id: lastLineId },
|
||
// { hover: false }
|
||
// );
|
||
// Vue.config.maps[mapId].setFeatureState(
|
||
// { sourceId: sourceLayerId, id: layerId },
|
||
// { hover: true }
|
||
// );
|
||
|
||
// if (lastLineId)
|
||
// Vue.config.maps[mapId].setPaintProperty(lastLineId, 'line-color', '#FFFFFF')
|
||
// Vue.config.maps[mapId].setPaintProperty(layerId, 'line-color', '#FFFF00')
|
||
// lastLineId = layerId;
|
||
|
||
coordinate = [(Number(sample.start_lon) + Number(sample.end_lon)) / 2, (Number(sample.start_lat) + Number(sample.end_lat)) / 2];
|
||
description = `
|
||
<table class="popup-table" style="min-width:380px;">
|
||
<thead>
|
||
<tr>
|
||
<td colspan="2">测线信息</td>
|
||
</tr>
|
||
</thead>
|
||
<tr>
|
||
<td>数据集名称: <b>${sample.dataset_name}</b></td>
|
||
<td>数据样品编号: <b>${sample.sample_name}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>赞助机构: <b>${sample.funding_org}</b></td>
|
||
<td>公开期限: <b>${sample.publish_rule}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>开始经度: <b>${sample.start_lon}°</b></td>
|
||
<td>结束经度: <b>${sample.end_lon}°</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>开始纬度: <b>${sample.start_lat}°</b></td>
|
||
<td>结束纬度: <b>${sample.end_lat}°</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>开始时间: <b>${sample.start_time}</b></td>
|
||
<td>结束时间: <b>${sample.end_time}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>测线深度: <b>${sample.line_deep}m</b></td>
|
||
<td>数据状态: <b>${sample.sample_status}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2">备注信息: <b>${sample.sample_remark}</b></td>
|
||
</tr>
|
||
</table>`;
|
||
}
|
||
else if (job_type == "站位作业") {
|
||
coordinate = [sample.sampling_lon, sample.sampling_lat];
|
||
description = `
|
||
<table class="popup-table" style="min-width:380px;">
|
||
<thead>
|
||
<tr>
|
||
<td colspan="2">站位信息</td>
|
||
</tr>
|
||
</thead>
|
||
<tr>
|
||
<td>数据集名称: <b>${sample.dataset_name}</b></td>
|
||
<td>样品编号: <b>${sample.sample_name}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>赞助机构: <b>${sample.funding_org}</b></td>
|
||
<td>公开期限: <b>${sample.publish_rule}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>站位经度: <b>${sample.sampling_lon}°</b></td>
|
||
<td>站位纬度: <b>${sample.sampling_lat}°</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>采样时间: <b>${sample.sampling_time}</b></td>
|
||
<td>采样深度: <b>${sample.sampling_deep}m</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2">备注信息: <b>${sample.sample_remark}</b></td>
|
||
</tr>
|
||
</table>`;
|
||
}
|
||
showPopupDetails({ mapId, coordinate, description, maxWidth: "450px", callbackOnClose: callbackOnClick });
|
||
}
|
||
|
||
export function showTrackInfo(layerId, mapId = "homeMap") {
|
||
Vue.config.maps[mapId].on("click", layerId, async (e) => {
|
||
// 禁止点击事件穿透 - 判断同一个 event 是否已经触发,用于避免图层内标记点注册的点击事件重复触发(点位分布密集时容易出现)
|
||
if (e.defaultPrevented)
|
||
return;
|
||
// 标记该 event 已触发,禁止点击事件穿透
|
||
if (typeof e.preventDefault === "function")
|
||
e.preventDefault();
|
||
|
||
let coordinate = e.lngLat;
|
||
let match = layerId.match(/vector-(.*)-[(symbol)|(line)]/);
|
||
if (!match)
|
||
return;
|
||
const voyageId = match[1];
|
||
regInfo({ voyage_name: voyageId }).then(res => {
|
||
const info = res.map.voyage;
|
||
const description = `
|
||
<table class="popup-table" style="min-width:380px;">
|
||
<thead>
|
||
<tr>
|
||
<td colspan="2"><a style="color:#0000FF" target="_balnk" title="点击查看航次详情" href="${Vue.config.baseUrl}/ds/#/container/dataDetail?voyage_name=${info.voyage_name}">航次信息</a></td>
|
||
</tr>
|
||
</thead>
|
||
<tr>
|
||
<td>航次编号: <b>${info.voyage_name}</b></td>
|
||
<td>航次首席: <b>${info.voyage_officer}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>科考船舶: <b>${info.ship_name}</b></td>
|
||
<td>资助机构: <b>${info.funding_org}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td>离港日期: <b>${info.voyage_start_date}</b></td>
|
||
<td>返港日期: <b>${info.voyage_end_date}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2">航次概况: <b>${info.voyage_remark}</b></td>
|
||
</tr>
|
||
</table>`;
|
||
|
||
showPopupDetails({ coordinate, mapId, description, maxWidth: "420px" });
|
||
})
|
||
});
|
||
}
|
||
|
||
export function showDiveInfo(layerId, mapId = "homeMap") {
|
||
Vue.config.maps[mapId].on("click", layerId, async (e) => {
|
||
// 禁止点击事件穿透 - 判断同一个 event 是否已经触发,用于避免图层内标记点注册的点击事件重复触发(点位分布密集时容易出现)
|
||
if (e.defaultPrevented)
|
||
return;
|
||
// 标记该 event 已触发,禁止点击事件穿透
|
||
if (typeof e.preventDefault === "function")
|
||
e.preventDefault();
|
||
|
||
const feature = e.features[0];
|
||
let coordinate = e.lngLat;
|
||
const description = `
|
||
<table class="popup-table" style="min-width:280px;">
|
||
<thead>
|
||
<tr>
|
||
<td colspan="2">潜次信息</td>
|
||
</tr>
|
||
</thead>
|
||
<tr>
|
||
<td colspan="2">潜次编号: <b>${feature.properties.name}</b></td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="2">created_at: <b>${feature.properties.created_at}</b></td>
|
||
</tr>
|
||
</table>`;
|
||
|
||
showPopupDetails({ mapId, coordinate, description, maxWidth: "300px" });
|
||
})
|
||
}
|
||
|
||
export function showPopupDetails({ mapId, coordinate, description, maxWidth = "300px", closeButton = true, closeOnClick = true, options = {}, callbackOnClose }) {
|
||
const map = Vue.config.maps[mapId];
|
||
if (!map)
|
||
return;
|
||
|
||
const popupOptions = Object.assign({ offset: {}, className: "my-class", closeButton: closeButton, closeOnClick: closeOnClick }, options);
|
||
|
||
// 如果已有共享 popup,复用并更新位置/内容;否则创建新的并绑定 close 事件以便释放引用
|
||
if (sharedPopup[mapId]) {
|
||
try {
|
||
sharedPopup[mapId].setLngLat(coordinate)
|
||
.setHTML(description)
|
||
.setOffset(10)
|
||
.setMaxWidth(maxWidth)
|
||
.addTo(map);
|
||
} catch (err) {
|
||
console.error("设置 popup 失败:", err);
|
||
try {
|
||
// 万一现有 popup 状态异常,移除并重新创建
|
||
sharedPopup[mapId].remove();
|
||
sharedPopup[mapId] = null;
|
||
} catch (e) {
|
||
console.error("移除异常 popup 失败:", e);
|
||
}
|
||
sharedPopup[mapId] = new window.mapboxgl.Popup(popupOptions)
|
||
.setLngLat(coordinate)
|
||
.setHTML(description)
|
||
.setOffset(10)
|
||
.setMaxWidth(maxWidth)
|
||
.addTo(map);
|
||
sharedPopup[mapId].on('close', () => {
|
||
// 关闭弹窗时执行回调(如有)
|
||
if (callbackOnClose && typeof callbackOnClose === "function") {
|
||
callbackOnClose();
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
sharedPopup[mapId] = new window.mapboxgl.Popup(popupOptions)
|
||
.setLngLat(coordinate)
|
||
.setHTML(description)
|
||
.setOffset(10)
|
||
.setMaxWidth(maxWidth)
|
||
.addTo(map);
|
||
sharedPopup[mapId].on('close', () => {
|
||
// 关闭弹窗时执行回调(如有)
|
||
if (callbackOnClose && typeof callbackOnClose === "function") {
|
||
callbackOnClose();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
export async function loadJsonLineFeature(layerId, color, geojson, mapId) {
|
||
let source = Vue.config.maps[mapId].getSource(layerId);
|
||
if (!source) {
|
||
Vue.config.maps[mapId].addSource(layerId, {
|
||
"type": "geojson",
|
||
"data": geojson
|
||
});
|
||
}
|
||
else {
|
||
source.setData(geojson);
|
||
}
|
||
|
||
let layer = Vue.config.maps[mapId].getLayer(layerId);
|
||
if (layer)
|
||
Vue.config.maps[mapId].setLayoutProperty(layerId, "visibility", "visible");
|
||
else {
|
||
Vue.config.maps[mapId].addLayer({
|
||
'id': layerId,
|
||
'type': 'line',
|
||
'source': layerId,
|
||
'layout': {
|
||
'visibility': 'visible',
|
||
'line-join': 'round',
|
||
'line-cap': 'round'
|
||
},
|
||
'paint': {
|
||
'line-color': color,
|
||
'line-width': 1
|
||
}
|
||
});
|
||
|
||
// 点击可交互的图层,显示相关信息
|
||
await setHighlight(layerId, layerId, mapId);
|
||
changeCursor(layerId, mapId);
|
||
// await showTrackInfo(`${sourceId}-${feature}`, mapId);
|
||
}
|
||
}
|
||
|
||
export async function loadJsonPointFeature(layerId, color, geojson, text_field, mapId) {
|
||
let source = Vue.config.maps[mapId].getSource(layerId);
|
||
if (!source) {
|
||
Vue.config.maps[mapId].addSource(layerId, {
|
||
"type": "geojson",
|
||
"data": geojson
|
||
});
|
||
}
|
||
else {
|
||
source.setData(geojson);
|
||
}
|
||
|
||
let layer = Vue.config.maps[mapId].getLayer(layerId);
|
||
if (layer)
|
||
Vue.config.maps[mapId].setLayoutProperty(layerId, "visibility", "visible");
|
||
else {
|
||
Vue.config.maps[mapId].addLayer({
|
||
'id': layerId,
|
||
'type': 'symbol',
|
||
'source': layerId,
|
||
'layout': {
|
||
'icon-image': 'dot.png',
|
||
'icon-size': 0.5,
|
||
// get the title name from the source's "title" property
|
||
'text-field': ['get', text_field],
|
||
'text-font': [
|
||
'Open Sans Semibold',
|
||
'Arial Unicode MS Bold'
|
||
],
|
||
"text-size": 10,
|
||
'text-offset': [0, 1.25],
|
||
'text-anchor': 'top',
|
||
'visibility': 'visible'
|
||
},
|
||
'paint': {
|
||
"text-color": color,
|
||
"text-halo-color": "#FFFFFF",
|
||
"text-halo-width": 0.1,
|
||
"icon-opacity": 1,
|
||
}
|
||
});
|
||
|
||
// 点击可交互的图层,显示相关信息
|
||
await setHighlight(layerId, layerId, mapId);
|
||
changeCursor(layerId, mapId);
|
||
// await showTrackInfo(`${sourceId}-${feature}`, mapId);
|
||
}
|
||
}
|
||
|
||
export async function highlightFeaturesByProperty(mapId, sourceId, layerId, propertyName = "sample_name", targetValue = "TARGET_VALUE") {
|
||
// 清空所有高亮状态(可选)
|
||
Vue.config.maps[mapId].querySourceFeatures(sourceId).forEach(f => {
|
||
Vue.config.maps[mapId].setFeatureState({ source: sourceId, id: f.id }, { highlight: false });
|
||
});
|
||
|
||
// 标记匹配要素
|
||
Vue.config.maps[mapId].querySourceFeatures(sourceId).forEach(f => {
|
||
if (f.properties && f.properties[propertyName] === targetValue) {
|
||
Vue.config.maps[mapId].setFeatureState({ source: sourceId, id: f.id }, { highlight: true });
|
||
}
|
||
});
|
||
|
||
// 图层 paint 使用 feature-state
|
||
Vue.config.maps[mapId].setPaintProperty(layerId, 'text-color', [
|
||
'case',
|
||
['boolean', ['feature-state', 'highlight'], false],
|
||
'#FFFFFF', // 高亮颜色
|
||
"#FF0000"// 默认颜色
|
||
]);
|
||
} |