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