From 2a193a9dff0dc93c7b839637740079e2c981db25 Mon Sep 17 00:00:00 2001 From: hym Date: Fri, 21 Nov 2025 23:02:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=9F=A2=E9=87=8F=E5=9B=BE?= =?UTF-8?q?=E5=B1=82=E7=9B=B8=E5=85=B3=E6=93=8D=E4=BD=9C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/vector-layer-utils.js | 237 ++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 src/utils/vector-layer-utils.js diff --git a/src/utils/vector-layer-utils.js b/src/utils/vector-layer-utils.js new file mode 100644 index 0000000..3696492 --- /dev/null +++ b/src/utils/vector-layer-utils.js @@ -0,0 +1,237 @@ +import Vue from 'vue' +import { regInfo } from '@/api/dataSearch' + +let sourceLayer = undefined; +let clickedLineIds = []; + +export function loadVectorLayer(layerId, features = ["line", "symbol"], colors = ["#FEFEFE", "#FF0000"], visibility = "visible", mapId = "home", workspace = 'dsds') { + // 添加数据源 + const sourceId = `vector-${layerId}`; + let source = Vue.config.maps[mapId].getSource(sourceId); + if (!source) + Vue.config.maps[mapId].addSource(sourceId, { + type: 'vector', + scheme: 'tms', + tiles: [`${Vue.config.baseUrl}/geoserver/gwc/service/tms/1.0.0/${workspace}:${layerId}@EPSG:900913@pbf/{z}/{x}/{y}.pbf`], + minzoom: 0, + maxzoom: 30, + generateId: true + }); + + let i = 0; + features.forEach((feature) => { + loadVectorFeature(layerId, feature, colors[i++], visibility, mapId); + }); +} + +export async function loadVectorFeature(layerId, feature, color, visibility, mapId) { + let sourceId = `vector-${layerId}`; + // symbol + // Slot 是 Mapbox 样式规范中的一个概念,它定义了图层在渲染过程中的位置和顺序。 + let slot = "foreground"; + let layout = { + "symbol-placement": "point", + "text-rotation-alignment": "auto", + "text-field": ["get", "name"], + "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": 1, + "visibility": visibility + }; + let paint = { + "text-color": color, + "text-halo-color": "#FFFFFF", + "text-halo-width": 0.1, + "icon-opacity": 1, + // slot: "middle" + }; + if (feature === "line") { + slot = "top"; + layout = { + "visibility": visibility + }; + 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}`); + if (!layer) { + 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); + 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; + sourceLayer = e.features[0].sourceLayer; + // 获取当前单击的线的选择状态 + let state = Vue.config.maps[mapId].getFeatureState({ + source: sourceId, + sourceLayer: sourceLayer, + id: e.features[0].id + }); + if (clickedLineId) { + Vue.config.maps[mapId].setFeatureState({ source: sourceId, sourceLayer: sourceLayer, id: clickedLineId }, { click: false }); + } + clickedLineId = e.features[0].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 在添加之后最多只调用一次。如果为 true,listener 会在其被调用之后自动移除。 + // }, 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 showTrackInfo(layerId, mapId = "home") { + Vue.config.maps[mapId].on("click", layerId, async (e) => { + // 禁止点击事件穿透 - 判断同一个 event 是否已经触发,用于避免图层内标记点注册的点击事件重复触发(点位分布密集时容易出现) + if (e.defaultPrevented) + return; + // 标记该 event 已触发,禁止点击事件穿透 + 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 = ` + + + + + + + + + + + + + + + + + + + + + + `; + + showPopupDetails(mapId, coordinate, description, "420px"); + }) + }); +} + +export function showPopupDetails(mapId, coordinate, description, maxWidth = "300px", closeButton = true, closeOnClick = true, options = {}) { + new window.mapboxgl.Popup({ offset: {}, className: "my-class", closeButton: closeButton, closeOnClick: closeOnClick }) + .setLngLat(coordinate) + .setHTML(description) + .setOffset(10) + .setMaxWidth(maxWidth) + .addTo(Vue.config.maps[mapId]); +} \ No newline at end of file