From ca9973fd9b233206fa53d55074107dc649af2452 Mon Sep 17 00:00:00 2001 From: hym Date: Thu, 25 Dec 2025 16:46:58 +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=E4=BA=A4=E4=BA=92=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 | 349 +++++++++++++++++++++++++++++--- 1 file changed, 323 insertions(+), 26 deletions(-) diff --git a/src/utils/vector-layer-utils.js b/src/utils/vector-layer-utils.js index 3696492..3efc311 100644 --- a/src/utils/vector-layer-utils.js +++ b/src/utils/vector-layer-utils.js @@ -3,28 +3,46 @@ 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') { +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) - 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 - }); + if (!source) { + 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: [`${Vue.config.baseUrl}/geoserver/gwc/service/tms/1.0.0/${workspace}:${layerId}@EPSG:900913@pbf/{z}/{x}/{y}.pbf`], + minzoom: 0, + maxzoom: 30, + bounds: bounds, + generateId: true + }); + } + else { + 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); + loadVectorFeature(layerId, feature, colors[i++], visibility, mapId, textField); }); } -export async function loadVectorFeature(layerId, feature, color, visibility, mapId) { +export async function loadVectorFeature(layerId, feature, color, visibility, mapId, textField = "name") { let sourceId = `vector-${layerId}`; // symbol // Slot 是 Mapbox 样式规范中的一个概念,它定义了图层在渲染过程中的位置和顺序。 @@ -32,7 +50,7 @@ export async function loadVectorFeature(layerId, feature, color, visibility, map let layout = { "symbol-placement": "point", "text-rotation-alignment": "auto", - "text-field": ["get", "name"], + "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"], @@ -47,14 +65,22 @@ export async function loadVectorFeature(layerId, feature, color, visibility, map "icon-allow-overlap": false, "icon-ignore-placement": false, "icon-image": "dot.png", - "icon-size": 1, + "icon-size": 0.5, "visibility": visibility }; let paint = { - "text-color": color, + // "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") { @@ -84,7 +110,9 @@ export async function loadVectorFeature(layerId, feature, color, visibility, map } 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, @@ -99,7 +127,13 @@ export async function loadVectorFeature(layerId, feature, color, visibility, map // 点击可交互的图层,显示相关信息 await setHighlight(`${sourceId}-${feature}`, sourceId, mapId); changeCursor(`${sourceId}-${feature}`, mapId); - await showTrackInfo(`${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; } @@ -110,17 +144,37 @@ export function setHighlight(layerId, sourceId, mapId = "home") { Vue.config.maps[mapId].on('click', layerId, (e) => { if (!e.features || e.features.length <= 0) return; - sourceLayer = e.features[0].sourceLayer; + + // // 查询点击点周围的所有要素(使用边界框) + // 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: e.features[0].id + id: feature.id }); if (clickedLineId) { Vue.config.maps[mapId].setFeatureState({ source: sourceId, sourceLayer: sourceLayer, id: clickedLineId }, { click: false }); } - clickedLineId = e.features[0].id; + clickedLineId = feature.id; if (state.click === true) { // 再次单击时取消选择 Vue.config.maps[mapId].setFeatureState({ source: sourceId, sourceLayer: sourceLayer, id: clickedLineId }, { click: false }); @@ -183,13 +237,111 @@ export function changeCursor(layerId, mapId = "home", cursor = "pointer") { }); } +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 = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `; + + 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 = ` + + + + + + + + + + + + + + + + + + + + + + + + + + `; + + 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 已触发,禁止点击事件穿透 - e.preventDefault(); + if (typeof e.preventDefault === "function") + e.preventDefault(); let coordinate = e.lngLat; let match = layerId.match(/vector-(.*)-[(symbol)|(line)]/); @@ -228,10 +380,155 @@ export function showTrackInfo(layerId, mapId = "home") { } 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]); + 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' + }, + '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, 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': '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', 'sample_name'], + '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"// 默认颜色 + ]); } \ No newline at end of file