增加矢量图层相关交互逻辑
This commit is contained in:
parent
f2015af2b7
commit
ca9973fd9b
|
|
@ -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 = `
|
||||
<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 已触发,禁止点击事件穿透
|
||||
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"// 默认颜色
|
||||
]);
|
||||
}
|
||||
Loading…
Reference in New Issue