DSDSWeb/src/utils/vector-layer-utils.js

538 lines
21 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 { regInfo } from '@/api/dataSearch'
let sourceLayer = undefined;
let clickedLineIds = [];
// 单例 Popup用于确保地图上只有一个弹框实例
let sharedPopup = null;
export function loadVectorLayer(layerId, features = ["line", "symbol"], colors = ["#FEFEFE", "#FF0000"], visibility = "visible", mapId = "home", workspace = 'dsds', textField = "name") {
// 添加数据源
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, colors[i++], visibility, mapId, textField);
});
}
export async function loadVectorFeature(layerId, feature, color, visibility, mapId, textField = "name") {
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'
});
}
// 点击可交互的图层,显示相关信息
await setHighlight(`${sourceId}-${feature}`, sourceId, mapId);
changeCursor(`${sourceId}-${feature}`, mapId);
if (layerId.includes("sample_line"))
await showSampleLineInfo(`${sourceId}-${feature}`, mapId);
if (layerId.includes("sample_station"))
await showSampleStationInfo(`${sourceId}-${feature}`, mapId);
else
await showTrackInfo(`${sourceId}-${feature}`, mapId);
return sourceId;
}
export function setHighlight(layerId, sourceId, mapId = "home") {
let hoveredLineId = null;
let clickedLineId = null;
Vue.config.maps[mapId].on('click', layerId, (e) => {
if (!e.features || e.features.length <= 0)
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);
// console.log(e.features.length, e.features);
// console.log("1 e: ", e);
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 });
// 显示信息框
// if (sourceId === 'GMSL Cable Data')
// document.getElementById("cable-overlay")!.style.display = 'block';
// 单击时加入选择列表(用于单击 ESC 键时取消选择状态)
clickedLineIds.push(clickedLineId);
// document.removeEventListener("keydown", removeHighlight, true);
document.addEventListener("keydown", (e) => {
removeHighlight(e, sourceId);
}, { once: true }); // once一个布尔值表示 listener 在添加之后最多只调用一次。如果为 truelistener 会在其被调用之后自动移除。
// }, true);
}
});
// 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) => {
if (e.features.length <= 0)
return;
sourceLayer = e.features[0].sourceLayer;
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 function changeCursor(layerId, mapId = "home", 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 = "home") {
Vue.config.maps[mapId].on("click", layerId, async (e) => {
// 禁止点击事件穿透 - 判断同一个 event 是否已经触发,用于避免图层内标记点注册的点击事件重复触发(点位分布密集时容易出现)
if (e.defaultPrevented)
return;
// 标记该 event 已触发,禁止点击事件穿透
if (typeof e.preventDefault === "function")
e.preventDefault();
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, e.lngLat, description, "450px");
});
}
export function showSampleStationInfo(layerId, mapId = "home") {
Vue.config.maps[mapId].on("click", layerId, async (e) => {
// 禁止点击事件穿透 - 判断同一个 event 是否已经触发,用于避免图层内标记点注册的点击事件重复触发(点位分布密集时容易出现)
if (e.defaultPrevented)
return;
// 标记该 event 已触发,禁止点击事件穿透
// if (typeof e.preventDefault === "function")
// e.preventDefault();
// console.log(e.features.length, e.features);
// console.log("2 e: ", e);
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, feature.geometry.coordinates, description, "450px");
});
}
export function showTrackInfo(layerId, mapId = "home") {
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(mapId, coordinate, description, "420px");
})
});
}
export function showPopupDetails(mapId, coordinate, description, maxWidth = "300px", closeButton = true, closeOnClick = true, options = {}) {
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) {
try {
sharedPopup.setLngLat(coordinate)
.setHTML(description)
.setOffset(10)
.setMaxWidth(maxWidth);
} catch (err) {
try {
// 万一现有 popup 状态异常,移除并重新创建
sharedPopup.remove();
sharedPopup = null;
} catch (e) {
console.error("移除异常 popup 失败:", e);
}
sharedPopup = new window.mapboxgl.Popup(popupOptions)
.setLngLat(coordinate)
.setHTML(description)
.setOffset(10)
.setMaxWidth(maxWidth)
.addTo(map);
sharedPopup.on('close', () => { sharedPopup = null; });
}
} else {
sharedPopup = new window.mapboxgl.Popup(popupOptions)
.setLngLat(coordinate)
.setHTML(description)
.setOffset(10)
.setMaxWidth(maxWidth)
.addTo(map);
sharedPopup.on('close', () => { sharedPopup = null; });
}
}
export async function loadJsonLineFeature(layerId, color, geojson, mapId) {
// console.log("layerId: ",layerId);
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 => {
console.log("AAAAA: ", f);
Vue.config.maps[mapId].setFeatureState({ source: sourceId, id: f.id }, { highlight: false });
});
// 标记匹配要素
Vue.config.maps[mapId].querySourceFeatures(sourceId).forEach(f => {
console.log("BBBBB: ", 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"// 默认颜色
]);
}