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 在添加之后最多只调用一次。如果为 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 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 已触发,禁止点击事件穿透 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 = ` `; 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, 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"// 默认颜色 ]); }