Compare commits

...

19 Commits

Author SHA1 Message Date
hym 6f674873cf 更新矢量图层设置逻辑 2026-04-01 20:49:38 +08:00
hym e565cce907 更改 mapbox.accessToken 设置逻辑 2026-04-01 20:48:30 +08:00
hym b9e273fbcc 精灵图片相关静态资源 2025-12-25 16:49:41 +08:00
hym 610a47b4bd 增加加载精灵图片逻辑 2025-12-25 16:48:42 +08:00
hym 43a2bfaf57 搜索页面增加相关交互逻辑 2025-12-25 16:47:57 +08:00
hym ca9973fd9b 增加矢量图层相关交互逻辑 2025-12-25 16:46:58 +08:00
hym f2015af2b7 地图服务相关逻辑 2025-12-25 16:45:57 +08:00
hym 7978bd3a61 获取图层边界,用于初始化图层时指定边界,避免过多无效请求。 2025-12-25 16:45:28 +08:00
hym 7970910f6b 修改默认端口,避免跟本地 geoserver 服务端口冲突。 2025-12-25 16:43:21 +08:00
hym 9302be89c5 增加轨迹高亮控件逻辑 2025-11-25 21:11:26 +08:00
hym 20444e9243 启用 mapbox 地图控件,优化相关交互逻辑。 2025-11-25 21:09:38 +08:00
hym 51478c40c9 禁用所有旋转和俯仰角控制 2025-11-24 17:25:45 +08:00
hym 220da59e62 首页启用 mapbox 地图控件,优化相关交互逻辑。 2025-11-21 23:09:27 +08:00
hym 96585b087b 临时禁用 ESLint 规则 2025-11-21 23:03:20 +08:00
hym 2a193a9dff 增加矢量图层相关操作逻辑 2025-11-21 23:02:28 +08:00
hym 6658118fba 增加信息弹出等样式 2025-11-21 23:01:43 +08:00
hym ac105fd0c5 定义部分全局变量 2025-11-21 23:00:53 +08:00
hym e2e3dd6952 增加颜色生成器类 2025-11-21 22:55:02 +08:00
hym cd3774770e 引入 mapbox 地图控件 2025-11-21 22:49:28 +08:00
63 changed files with 1510 additions and 124 deletions

View File

@ -10,6 +10,8 @@ module.exports = {
es6: true
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// 完全禁用 ESLint
lintOnSave: false,
// add your custom rules here
// it is base on https://github.com/vuejs/eslint-config-vue

View File

@ -39,7 +39,7 @@ module.exports = {
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
port: 8181, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,

View File

@ -12,6 +12,12 @@
<link rel="stylesheet" href="./static/css/reset.css">
<link rel="stylesheet" href="./static/js/Cesium/Widgets/widgets.css">
<script type="text/javascript" src="./static/js/Cesium/Cesium.js"></script>
<!-- 引入 Mapbox GL JS -->
<link href="./static/css/mapbox-gl-v3.16.0.css" rel="stylesheet">
<script src="./static/js/mapbox-gl-v3.16.0.js"></script>
<!-- <script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&type=webgl&ak=AwIb6rcMDySo2AKQ2hVGIjmFCL4J1oJK"></script>-->
<!-- <script src="https://cdn.jsdelivr.net/npm/echarts-gl/dist/echarts-gl.min.js"></script> -->
<!-- <script type="text/javascript" src="http://maps.google.com/maps/api/js?key=AIzaSyD9kf3RTO2HwggOQ1_fbZawfiKzyBNPXeY"></script> -->

View File

@ -15,6 +15,7 @@ import axios from 'axios'
import qs from 'qs'
import store from './store/index'
import { extractWMTSBoundsInfo } from '@/utils/mapbox-utils'
window.sessionStorage.setItem('webInfo', 'ASOS')
Vue.use(ElementUI, { locale: Zhlocale })
@ -31,3 +32,22 @@ new Vue({
components: { App },
template: '<App/>'
})
// 定义部分全局变量
Vue.config.maps = {};
Vue.config.firstRender = true;
Vue.config.boundsInfoList = [];
Vue.config.baseUrl = process.env.NODE_ENV === 'development' ? 'http://124.222.8.13:9801' : `${window.location.protocol}//${window.location.hostname}:${window.location.port}`
Vue.config.wwwrootBaseUrl = "_content/IDSSE.OceanExplorer.Shared";
Vue.config.sprites = ["go.png", "ship.png", "ship1.png", "ship2.png", "ship3.png", "fdz.png", "shys.png", "auv.png", "aircraft.png", "aircraft-r.png", "arrow.png", "dot.png", "dot_red.png", "default.png", "info-dark.png", "warning-dark.png", "warning-red.png", "slow_sog.png", "stop_sog.png", "海底主基站.png", "石油平台.png", "岸基站.png", "circle-red.png", "circle-green.png", "circle-blue.png", "circle-yellow.png", "circle-purple.png", "circle-black.png", "circle-white.png"];
if (Vue.config.firstRender) {
Vue.config.firstRender = !Vue.config.firstRender;
fetch(`${Vue.config.baseUrl}/geoserver/gwc/service/wmts?service=WMTS&acceptVersions=1.0.0&request=GetCapabilities`)
.then(response => response.text())
.then(async (xmlContent) => {
Vue.config.boundsInfoList = await extractWMTSBoundsInfo(xmlContent);
});
}
window.mapboxgl.accessToken = 'pk.eyJ1IjoicWRod2kiLCJhIjoiY2xiaXppcjEyMGNhMjNwbzVrcHl4d20zOSJ9.0ETMJM3-zllFh2GRrRVUZg'

View File

@ -1779,4 +1779,64 @@ header{
}
}
.mapboxgl-ctrl-logo {
display: none !important;
}
.popup-table {
border-spacing: 0;
font-family: 'Microsoft YaHei';
color: #006400;
/*border-collapse: collapse;
empty-cells: show;*/
table-layout: fixed;
word-break: break-all;
word-wrap: break-word;
border-collapse: collapse;
/* min-width:300px;
max-width:400px;*/
width: auto;
}
.popup-table caption {
font: italic 85%/1 arial, sans-serif;
padding: 1em 0;
text-align: center;
}
.popup-table td,
.popup-table th {
border: 1px solid rgba(0, 100, 255, 0.2);
border-width: 1px 1px 1px 1px;
font-size: inherit;
padding: 3px;
overflow: visible;
/*white-space: normal;*/
}
.popup-table thead {
font-family: 'Microsoft YaHei';
background-color: rgba(0, 100, 255, 0.2);
text-align: center;
vertical-align: bottom;
}
.popup-table td {
background-color: transparent;
}
.popup-table-odd td {
background-color: #f2f2f2;
}
.popup-table-td-left td {
text-align: left;
}
.popup-table-td-center td {
text-align: center;
}
.popup-table-td-right td {
text-align: right;
}

View File

@ -0,0 +1,185 @@
export class ColorGenerator {
// 生成适合地图显示的区分度高的颜色
static generateDistinctColors(count) {
const colors = [];
for (let i = 0; i < count; i++) {
// 使用黄金角分布算法,确保颜色分布均匀
const hue = (i * 137.5) % 360; // 黄金角约等于 137.5 度
const saturation = 70 + Math.random() * 20; // 70-90% 饱和度
const lightness = 45 + Math.random() * 20; // 45-65% 亮度
const hsl = { h: hue, s: saturation, l: lightness };
colors.push(HSLColorGenerator.toCSS(hsl));
}
return colors;
}
// 生成分类数据颜色(确保相邻类别颜色差异明显)
static generateCategoricalColors(count, startHue = 0) {
const hueStep = 360 / count;
const colors = [];
for (let i = 0; i < count; i++) {
const hue = (startHue + i * hueStep) % 360;
const hsl = {
h: hue,
s: 80 + Math.random() * 15, // 80-95% 饱和度
l: 50 + Math.random() * 15 // 50-65% 亮度
};
colors.push(HSLColorGenerator.toCSS(hsl));
}
return colors;
}
// 生成连续数据颜色(渐变)
static generateSequentialColors(
count,
startColor = '#f7fbff',
endColor = '#08306b'
) {
const colors = [];
// 解析起始颜色和结束颜色
const startRGB = this.parseHexColor(startColor);
const endRGB = this.parseHexColor(endColor);
for (let i = 0; i < count; i++) {
const ratio = i / (count - 1);
const r = Math.round(startRGB.r + (endRGB.r - startRGB.r) * ratio);
const g = Math.round(startRGB.g + (endRGB.g - startRGB.g) * ratio);
const b = Math.round(startRGB.b + (endRGB.b - startRGB.b) * ratio);
colors.push(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`);
}
return colors;
}
// 生成发散颜色(用于有正负值的数据)
static generateDivergingColors(
count,
negativeColor = '#d73027',
neutralColor = '#ffffbf',
positiveColor = '#1a9850'
) {
const colors = [];
const midPoint = Math.floor(count / 2);
// 负值部分
const negRGB = this.parseHexColor(negativeColor);
const neutralRGB = this.parseHexColor(neutralColor);
for (let i = 0; i < midPoint; i++) {
const ratio = i / midPoint;
const r = Math.round(negRGB.r + (neutralRGB.r - negRGB.r) * ratio);
const g = Math.round(negRGB.g + (neutralRGB.g - negRGB.g) * ratio);
const b = Math.round(negRGB.b + (neutralRGB.b - negRGB.b) * ratio);
colors.push(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`);
}
// 正值部分
const positiveRGB = this.parseHexColor(positiveColor);
for (let i = midPoint; i < count; i++) {
const ratio = (i - midPoint) / (count - midPoint - 1);
const r = Math.round(neutralRGB.r + (positiveRGB.r - neutralRGB.r) * ratio);
const g = Math.round(neutralRGB.g + (positiveRGB.g - neutralRGB.g) * ratio);
const b = Math.round(neutralRGB.b + (positiveRGB.b - neutralRGB.b) * ratio);
colors.push(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`);
}
return colors;
}
// 解析十六进制颜色
static parseHexColor(hex) {
hex = hex.replace('#', '');
if (hex.length === 3) {
hex = hex.split('').map(c => c + c).join('');
}
return {
r: parseInt(hex.substring(0, 2), 16),
g: parseInt(hex.substring(2, 4), 16),
b: parseInt(hex.substring(4, 6), 16)
};
}
}
export class HSLColorGenerator {
// 随机 HSL 颜色
static randomHSL() {
return {
h: Math.floor(Math.random() * 360),
s: Math.floor(Math.random() * 101),
l: Math.floor(Math.random() * 101)
};
}
// 转换为 CSS 字符串
static toCSS(color) {
if (color.alpha !== undefined) {
return `hsla(${color.h}, ${color.s}%, ${color.l}%, ${color.alpha})`;
}
return `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
}
// 生成鲜艳的颜色(高饱和度)
static randomVibrantHSL(minSaturation = 80, minLightness = 40) {
return {
h: Math.floor(Math.random() * 360),
s: minSaturation + Math.floor(Math.random() * (101 - minSaturation)),
l: minLightness + Math.floor(Math.random() * (61 - minLightness))
};
}
// 生成柔和的颜色(低饱和度)
static randomPastelHSL(maxSaturation = 40, minLightness = 70) {
return {
h: Math.floor(Math.random() * 360),
s: Math.floor(Math.random() * (maxSaturation + 1)),
l: minLightness + Math.floor(Math.random() * (101 - minLightness))
};
}
// 转换为 RGB
static toRGB(hsl) {
const h = hsl.h / 360;
const s = hsl.s / 100;
const l = hsl.l / 100;
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
alpha: hsl.alpha
};
}
}

236
src/utils/mapbox-utils.js Normal file
View File

@ -0,0 +1,236 @@
import Vue from 'vue'
/**
* 获取所有图层信息
* @param {mapboxgl.Map} map - Mapbox 地图实例
* @returns {Array} 图层信息数组
*/
export function getAllLayers(map) {
if (!map || !map.getStyle()) {
return [];
}
const style = map.getStyle();
return style.layers || [];
}
/**
* 安全地设置图层属性
* @param {mapboxgl.Map} map - Mapbox 地图实例
* @param {string} layerId - 图层ID
* @param {string} property - 属性名
* @param {any} value - 属性值
* @returns {boolean}
*/
export function safeSetPaintProperty(map, layerId, property, value) {
if (!map.getLayer(layerId)) {
console.warn(`图层 ${layerId} 不存在,无法设置属性`);
return false;
}
try {
map.setPaintProperty(layerId, property, value);
return true;
} catch (error) {
console.error(`设置图层 ${layerId} 属性 ${property} 失败:`, error);
return false;
}
}
export function handleWheel(mapId = "home") {
document.addEventListener("keydown", () => {
// 允许鼠标滚轮缩放
Vue.config.maps[mapId].scrollZoom.enable();
}, { once: false, passive: false });
document.addEventListener("keyup", () => {
// 禁止鼠标滚轮缩放
Vue.config.maps[mapId].scrollZoom.disable();
}, { once: false, passive: false });
}
export function initSprites(mapId = "home") {//../../../static/images/logo.png
for (const sprite of Vue.config.sprites) {
// Vue.config.maps[mapId].loadImage(`${Vue.config.baseUrl}/oe/${Vue.config.wwwrootBaseUrl}/sprites/${sprite}`, function (error, image) {
Vue.config.maps[mapId].loadImage(`./static/sprites/${sprite}`, function (error, image) {
if (error) throw error;
if (!Vue.config.maps[mapId].hasImage(sprite))
// 为图像启用 sdf { sdf: true },启用的 sdf 的图像,在 Symbol 图层中就能够通过 icon-color 属性来重新渲染图像的颜色。
Vue.config.maps[mapId].addImage(sprite, image, { sdf: false });
});
}
}
export async function extractWMTSBoundsInfo(xmlContent) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlContent, "text/xml");
const featureTypes = xmlDoc.getElementsByTagName("Layer");
const result = [];
for (let i = 0; i < featureTypes.length; i++) {
const featureType = featureTypes[i];
// 提取 Name
const nameElement = featureType.getElementsByTagName("ows:Identifier")[0];
const name = nameElement ? nameElement.textContent || "" : "";
// 提取 WGS84BoundingBox
const wgs84BoundingBox = featureType.getElementsByTagName("ows:WGS84BoundingBox")[0];
if (wgs84BoundingBox) {
const lowerCorner = wgs84BoundingBox.getElementsByTagName("ows:LowerCorner")[0].textContent;
const upperCorner = wgs84BoundingBox.getElementsByTagName("ows:UpperCorner")[0].textContent;
if (name) {
result.push({
layerName: name,
bounds: [Number(lowerCorner.split(" ")[0]), Number(lowerCorner.split(" ")[1]), Number(upperCorner.split(" ")[0]), Number(upperCorner.split(" ")[1])]
});
}
}
}
return result;
}
/**
* 触发指定图层的点击事件
* @param {mapboxgl.Map} map - Mapbox地图实例
* @param {string} layerId - 要触发的图层ID
* @param {Array} lnglat - 经纬度 [经度, 纬度]
* @param {Object} options - 附加选项
*/
export async function triggerLayerClick(mapId, sourceLayerId, layerId, lnglat, options = {}) {
if (!Vue.config.maps[mapId] || !layerId || !lnglat) {
console.error('缺少必要参数');
return null;
}
const [lng, lat] = Array.isArray(lnglat) ? lnglat : [lnglat.lng, lnglat.lat];
// 1. 确保地图和图层存在
if (!Vue.config.maps[mapId].getLayer(layerId)) {
console.error(`图层 ${layerId} 不存在`);
return null;
}
// 2. 将经纬度转换为屏幕坐标
const point = Vue.config.maps[mapId].project([lng, lat]);
// 3. 查询该点/源下的图层要素
const bbox = [
[point.x - 1, point.y - 1],
[point.x + 1, point.y + 1]
];
const map = Vue.config.maps[mapId];
console.log("layerId: ", layerId, " sourceLayerId: ", sourceLayerId, " options: ", options);
// 尝试从图层获取 source id一些图层的 source 名称与 layerId 不同)
const layerObj = map.getLayer(layerId);
const sourceId = (layerObj && layerObj.source) ? layerObj.source : layerId;
const source = map.getSource(sourceId);
// 在浏览器控制台或 triggerLayerClick 中运行(替换 sourceId/sourceLayerName
const feats = map.querySourceFeatures(sourceId, { sourceLayer: "sample_station_TS-46-1_Ship_CTDI_SY" }) || [];
console.log(sourceId,">>>>>>>>>>>>>>>>>>>>sample_station_TS-46-1_Ship_CTDI_SY",' ======sample of features:', feats.slice(0, 5));
console.log('props keys:', feats.slice(0, 5).map(f => Object.keys(f.properties || {})));
let relatedTracks = map.querySourceFeatures(sourceId, {
sourceLayer: sourceId,
// 支持的过滤条件:==, !=, >, >=, <, <=, in, !in, all, any, none, has, !has
filter: ['==', ['get', 'sample_name'], options.sample_name]
});
console.log("relatedTracks: ", relatedTracks);
// 打印样式中与该 source 关联的所有 source-layer便于调试 vector tiles 的 layer 名称)
try {
const styleLayers = map.getStyle().layers || [];
const sourceLayerNames = new Set();
styleLayers.forEach(l => {
if (l.source === sourceId && l['source-layer']) sourceLayerNames.add(l['source-layer']);
});
console.log('candidate source-layer names for', sourceId, Array.from(sourceLayerNames));
} catch (err) {
console.warn('failed to enumerate style layers for debugging:', err);
}
// 使用表达式 ['==', ['get', prop], value] 来做精确匹配
const propName = 'sample_name';
const propValue = options.sample_name;
const filterExpr = ['==', ['get', propName], propValue];
// const filterExpr = ['==', propName, propValue];
let features = [];
try {
if (source) {
// 如果 source 尚未加载完,等待一次 sourcedata有时 vector tiles 尚未请求完成)
if (!map.isSourceLoaded(sourceId)) {
console.log('source not loaded yet, waiting for sourcedata:', sourceId);
await new Promise((resolve) => {
const onData = (e) => {
if (e && e.sourceId === sourceId && map.isSourceLoaded(sourceId)) {
map.off('sourcedata', onData);
resolve();
}
};
// 超时保护2秒后继续
const timeout = setTimeout(() => {
map.off('sourcedata', onData);
resolve();
}, 2000);
map.on('sourcedata', onData);
});
}
console.log("sourceId: ", sourceId, " sourceLayerId: ", sourceLayerId, " options: ", options);
// 优先使用 querySourceFeatures对 vector/geojson 源性能更好)
features = map.querySourceFeatures(sourceId, { sourceLayer: sourceLayerId, filter: filterExpr }) || [];
}
} catch (err) {
console.warn('querySourceFeatures failed:', err);
}
// 如果通过 source 查询不到再退回到渲染层查询rendered features作为兜底
if (!features || features.length === 0) {
try {
features = map.queryRenderedFeatures(bbox, { layers: [layerId], filter: filterExpr }) || [];
} catch (err) {
console.warn('queryRenderedFeatures fallback failed:', err);
features = [];
}
}
console.log('resolved sourceId:', sourceId, 'found features:', features.length);
// 4. 创建事件对象
const eventObject = {
lngLat: { lng, lat },
point: point,
features: features,
options: options,
originalEvent: createSimulatedEvent(point),
target: Vue.config.maps[mapId].getCanvas(),
type: 'click',
layerId: layerId,
timestamp: Date.now()
};
// 5. 触发图层特定的点击事件
Vue.config.maps[mapId].fire('click', eventObject, { layerId: layerId });
return {
event: eventObject,
features: features,
success: true
};
}
function createSimulatedEvent(point) {
return {
type: 'click',
clientX: point.x,
clientY: point.y,
preventDefault: () => { },
stopPropagation: () => { }
};
}

View File

@ -0,0 +1,540 @@
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 在添加之后最多只调用一次。如果为 truelistener 会在其被调用之后自动移除。
// }, 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 = `
<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 已触发,禁止点击事件穿透
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 = `
<table class="popup-table" style="min-width:380px;">
<thead>
<tr>
<td colspan="2"><a style="color:#0000FF" target="_balnk" title="点击查看航次详情" href="${Vue.config.baseUrl}/ds/#/container/dataDetail?voyage_name=${info.voyage_name}">航次信息</a></td>
</tr>
</thead>
<tr>
<td>航次编号: <b>${info.voyage_name}</b></td>
<td>航次首席: <b>${info.voyage_officer}</b></td>
</tr>
<tr>
<td>科考船舶: <b>${info.ship_name}</b></td>
<td>资助机构: <b>${info.funding_org}</b></td>
</tr>
<tr>
<td>离港日期: <b>${info.voyage_start_date}</b></td>
<td>返港日期: <b>${info.voyage_end_date}</b></td>
</tr>
<tr>
<td colspan="2">航次概况: <b>${info.voyage_remark}</b></td>
</tr>
</table>`;
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"// 默认颜色
]);
}

View File

@ -29,18 +29,22 @@
<div v-for="(item, index) in dataList" :key="index" :id="item.tsy_id" class="left4_1">
<div class="left4_top">
<p class="rule-text">编号规则:航次号_潜次号_设备类型_数据样品类型</p>
<p @click="openList(item, 'list')" style="font-weight: 400;font-size: 14px;cursor: pointer;color: #409eff" >{{ item.dataset_name }} <span class="mileage">{{ item.file_total }}</span></p>
<p @click="openList(item, 'list')"
style="font-weight: 400;font-size: 14px;cursor: pointer;color: #409eff">{{ item.dataset_name }} <span
class="mileage">{{ item.file_total }}</span></p>
<span class="org">{{ item.create_time }}</span>
</div>
<ul v-show="currenId == item.tsy_id && fold">
<li :class="sample.tsy_id == currenSample ? 'bg-color' : ''" class="sample" :id="sample.tsy_id"
v-for="(sample, i) in item.sampleArray" :key="i" @click.stop="onSample(sample)">{{ sample.sample_name }}
v-for="(sample, i) in item.sampleArray" :key="i" @click.stop="onSample(sample, item.job_type)">{{
sample.sample_name }}
</li>
</ul>
<div class="left4_list">
<div class="list1">
<p>航次</p>
<span class="hover-color" style="color: #409eff" @click="turnDeatilPage(item)">{{ item.voyage_name }}</span>
<span class="hover-color" style="color: #409eff" @click="turnDeatilPage(item)">{{ item.voyage_name
}}</span>
</div>
<div class="list1">
<p>平台类型</p>
@ -82,7 +86,8 @@
<div class="left4_top">
<p :style="currentVoyage === item.tsy_id ? 'color: #409eff' : ''">
<span style="color: #409eff" @click="turnDeatilPage(item)">{{ item.voyage_name }}</span>
<span class="mileage">{{ item.voyage_mileage }}海里</span></p>
<span class="mileage">{{ item.voyage_mileage }}海里</span>
</p>
<span class="org">{{ item.funding_org }}</span>
</div>
<div class="left4_list">
@ -115,11 +120,15 @@
<span>{{ item.serect_status == 'Active' ? '是' : '否' }}</span>
</div>
<div class="btns">
<el-button v-if="!item.highlight" icon="el-icon-s-opportunity" circle @click="addHighlightLayer(item.voyage_name, index)" />
<el-button v-if="item.highlight" type="primary" icon="el-icon-s-opportunity" circle @click="clearHighlight" />
<el-button v-if="!item.show" icon="el-icon-view" circle @click="addSingleLineImg(item.voyage_name, index)" />
<el-button v-if="item.show" type="primary" icon="el-icon-view" circle @click="removeSingleLineImg(item.voyage_name, index)" />
<el-button icon="el-icon-location-information" circle />
<el-button v-if="!item.highlight" icon="el-icon-s-opportunity" circle
@click="addHighlightLayer(item.voyage_name, index)" />
<el-button v-if="item.highlight" type="primary" icon="el-icon-s-opportunity" circle
@click="clearHighlight(item.voyage_name, index)" />
<el-button v-if="!item.show" icon="el-icon-view" circle
@click="addSingleLineImg(item.voyage_name, index)" />
<el-button v-if="item.show" type="primary" icon="el-icon-view" circle
@click="removeSingleLineImg(item.voyage_name, index)" />
<!-- <el-button icon="el-icon-location-information" circle /> -->
</div>
</div>
</div>
@ -139,58 +148,46 @@
</div>
</div>
<!-- 航次名称弹窗-->
<el-dialog
title="请选择航次名称"
:visible.sync="dialog1"
width="1000px"
>
<el-table ref="tableData1" border highlight-current-row @current-change="handleCurrentChange1" max-height="480" :data="tableData1" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }">
<el-dialog title="请选择航次名称" :visible.sync="dialog1" width="1000px">
<el-table ref="tableData1" border highlight-current-row @current-change="handleCurrentChange1" max-height="480"
:data="tableData1" :header-cell-style="{ background: '#dddfe6', color: '#555960', textAlign: 'center' }"
:cell-style="{ textAlign: 'center' }">
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="name" label="航次名称" />
</el-table>
</el-dialog>
<!-- 平台类型弹窗-->
<el-dialog
title="请选择平台类型"
:visible.sync="dialog2"
width="1000px"
>
<el-table ref="tableData2" border @current-change="handleCurrentChange2" :data="tableData2" max-height="480" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }">
<el-dialog title="请选择平台类型" :visible.sync="dialog2" width="1000px">
<el-table ref="tableData2" border @current-change="handleCurrentChange2" :data="tableData2" max-height="480"
:header-cell-style="{ background: '#dddfe6', color: '#555960', textAlign: 'center' }"
:cell-style="{ textAlign: 'center' }">
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="name" label="平台类型名称" />
<el-table-column prop="value" label="英文类型名称" />
</el-table>
</el-dialog>
<!-- 设备类型弹窗-->
<el-dialog
title="请选择设备类型"
:visible.sync="dialog3"
width="1000px"
>
<el-table ref="tableData3" border @current-change="handleCurrentChange3" :data="tableData3" max-height="480" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }">
<el-dialog title="请选择设备类型" :visible.sync="dialog3" width="1000px">
<el-table ref="tableData3" border @current-change="handleCurrentChange3" :data="tableData3" max-height="480"
:header-cell-style="{ background: '#dddfe6', color: '#555960', textAlign: 'center' }"
:cell-style="{ textAlign: 'center' }">
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="name" label="设备类型名称" />
<el-table-column prop="value" label="英文名称" />
</el-table>
</el-dialog>
<!-- 数据样品类型弹窗-->
<el-dialog
title="请选择数据样品类型"
:visible.sync="dialog4"
width="1000px"
>
<el-table ref="tableData4" border @current-change="handleCurrentChange4" :data="tableData4" max-height="480" :header-cell-style="{ background: '#dddfe6',color: '#555960',textAlign: 'center' }" :cell-style="{ textAlign: 'center' }">
<el-dialog title="请选择数据样品类型" :visible.sync="dialog4" width="1000px">
<el-table ref="tableData4" border @current-change="handleCurrentChange4" :data="tableData4" max-height="480"
:header-cell-style="{ background: '#dddfe6', color: '#555960', textAlign: 'center' }"
:cell-style="{ textAlign: 'center' }">
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="name" label="数据样品类型类型名称" />
<el-table-column prop="value" label="英文名称" />
</el-table>
</el-dialog>
<!-- 航次信息弹窗-->
<el-dialog
title="航次信息"
:visible.sync="dialog5"
width="800px"
>
<el-dialog title="航次信息" :visible.sync="dialog5" width="800px">
<div class="detail_b">
<h3 style="padding-bottom: 10px;">{{ voyageInfo.voyage_name }}</h3>
<p>{{ voyageInfo.voyage_remark }}</p>
@ -240,6 +237,10 @@ import Cookies from 'js-cookie'
import {
setImageryViewModels
} from '@/utils/common'
import Vue from 'vue'
import { loadVectorLayer, loadJsonLineFeature, loadJsonPointFeature, highlightFeaturesByProperty } from '@/utils/vector-layer-utils'
import { ColorGenerator } from '@/utils/color-generator';
import { handleWheel, initSprites, triggerLayerClick } from "@/utils/mapbox-utils";
const Cesium = window.Cesium
export default {
@ -248,6 +249,7 @@ export default {
return {
show: false,
viewer: null,
filter: null,
color: [Cesium.Color.RED, Cesium.Color.GREEN, Cesium.Color.BLUE, Cesium.Color.CHARTREUSE, Cesium.Color.DARKBLUE],
searchType: [
{
@ -362,7 +364,8 @@ export default {
},
mounted() {
this.imageryViewModels = setImageryViewModels(this.CesiumHostAddr)
this.initCesium()
this.initMapbox()
// this.initCesium()
},
methods: {
//
@ -452,21 +455,35 @@ export default {
const arr = res.page.records
if (this.radio === '航次') {
if (arr.length) {
let i = 0;
const colors = ColorGenerator.generateDivergingColors(arr.length);
arr.forEach(item => {
item.highlight = false
item.show = true
this.addSingleLineImg(item.voyage_name, null)
// this.addSingleLineImg(item.voyage_name, null)
//
loadVectorLayer(item.voyage_name, ["line", "symbol"], [colors[i], colors[i]], "visible", "dataSearch", "dsds");
i++;
})
}
this.dataList = arr
}
if (this.radio === '数据样品集') {
try {
this.getDataPoints(arr)
}
catch (err) {
console.log(err);
}
}
})
},
//
addHighlightLayer(voyageName, i) {
Vue.config.maps["dataSearch"].setPaintProperty(`vector-${voyageName}-line`, "line-width", 4);
this.dataList[i].highlight = true;
return;
this.clearHighlight()
const highlightLayer = new Cesium.WebMapTileServiceImageryProvider({
url: `${this.CesiumHostAddr}geoserver/gwc/service/wmts/rest/dsds:${voyageName}-highlight/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png`,
@ -481,7 +498,19 @@ export default {
this.dataList[i].highlight = true;
},
//
clearHighlight() {
clearHighlight(voyageName, i) {
let lineWidthExpression = [
"case",
["boolean", ["feature-state", "hover"], false],
4,
["boolean", ["feature-state", "click"], false],
2,
1.8
];
Vue.config.maps["dataSearch"].setPaintProperty(`vector-${voyageName}-line`, "line-width", lineWidthExpression);
this.dataList[i].highlight = false;
return;
this.dataList.map(item => { item.highlight = false })
//
if (this.highlightLayer) {
@ -491,7 +520,13 @@ export default {
},
//
addSingleLineImg(voyageName, i) {
if (i !== null) {
this.dataList[i].show = true;
}
//
Vue.config.maps["dataSearch"].setLayoutProperty(`vector-${voyageName}-symbol`, "visibility", "visible");
Vue.config.maps["dataSearch"].setLayoutProperty(`vector-${voyageName}-line`, "visibility", "visible");
return;
//
if (this.trackLayerMap[voyageName]) {
console.warn(`航次 ${voyageName} 的轨迹图层已存在`);
@ -518,6 +553,16 @@ export default {
//
removeSingleLineImg(voyageName, i) {
if (i !== null) {
this.dataList[i].show = false;
}
//
// let visibility = Vue.config.maps["dataSearch"].getLayoutProperty(`vector-${voyageName}-symbol`, "visibility");
// visibility = visibility == "visible" ? "none" : "visible";
Vue.config.maps["dataSearch"].setLayoutProperty(`vector-${voyageName}-symbol`, "visibility", "none");
Vue.config.maps["dataSearch"].setLayoutProperty(`vector-${voyageName}-line`, "visibility", "none");
return;
const layerInstance = this.trackLayerMap[voyageName];
if (layerInstance) {
// CesiumimageryLayers
@ -581,7 +626,7 @@ export default {
},
//
clearAll() {
this.clearHighlight()
// this.clearHighlight()
if (this.trackLayerMap.length) {
this.trackLayerMap.forEach(layer => {
this.viewer.imageryLayers.remove(layer);
@ -597,9 +642,20 @@ export default {
//
getDataPoints(arr) {
if (!arr.length) return false
let i = 0;
const colors = ColorGenerator.generateDivergingColors(arr.length);
arr.forEach(item => {
this.addSingleLineImg(item.voyage_name, null)
this.divingImage(item)
// 线/
if (item.job_type == "测线作业") {
loadVectorLayer(`sample_line_${item.dataset_name}`, ["line"], ["#FFFFFF", "#FFFFFF"], "visible", "dataSearch", "dsds", "sample_name");
} else {
loadVectorLayer(`sample_station_${item.dataset_name}`, ["symbol"], ["#FFFFFF", "#FFFFFF"], "visible", "dataSearch", "dsds", "sample_name");
}
i++;
// this.addSingleLineImg(item.voyage_name, null)
// this.dataList[i].show = true;
// this.divingImage(item)
})
const voyageList = arr
voyageList.map(item => {
@ -609,11 +665,11 @@ export default {
dataset_id: item.tsy_id
}).then(res => {
item.sampleArray = res.array
if (res.array.length) {
res.array.forEach(sample => {
this.sampleLineImage(sample)
})
}
// if (res.array.length) {
// res.array.forEach(sample => {
// this.sampleLineImage(sample)
// })
// }
})
}
if (item.job_type === '站位作业') {
@ -623,16 +679,105 @@ export default {
}).then(res => {
item.sampleArray = res.array
//
if (res.array.length) {
res.array.forEach(sample => {
this.sampleLineImage(sample)
})
}
// if (res.array.length) {
// res.array.forEach(sample => {
// this.sampleLineImage(sample)
// });
// }
})
}
})
this.dataList = voyageList
console.log(this.dataList)
},
// mapbox
initMapbox() {
const map = new window.mapboxgl.Map({
container: 'cesiumContainerData',
// style: 'mapbox://styles/mapbox/standard',
style: {
"version": 8,
"name": "default_style",
// mapbox使
"sprite": `${Vue.config.baseUrl}/oe/${Vue.config.wwwrootBaseUrl}/sprites/sprite`,
// 使
"glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf",
// glyphs 404
// "glyphs": `${Vue.config.baseUrl}/oe/${Vue.config.wwwrootBaseUrl}/glyphs/{fontstack}/{range}.pbf`,
"sources": {
"影像地图": {
"type": "raster",
"tiles": [`${Vue.config.baseUrl}/geoserver/gwc/service/wmts/rest/ougp:world/EPSG:900913/EPSG:900913:{z}/{y}/{x}?format=image/jpeg`],
"tileSize": 256,
// "minzoom": 0,
// "maxzoom": baseMapMaxZoom
}
},
"layers": [
{
"id": "影像地图",
"type": "raster",
"source": "影像地图",
"layout": { visibility: "visible" },
// "minzoom": 0,
// "maxzoom": baseMapMaxZoom
}
]
},
config: {
basemap: {
theme: 'monochrome',
lightPreset: 'night'
}
},
zoom: 5,
center: [113, 15.5],
minZoom: 2,
maxZoom: 29,
trackResize: true,
scrollZoom: false, //
//
localIdeographFontFamily: "Microsoft YoHei, 'Noto Sans', 'Noto Sans CJK SC', sans-serif",
pitch: 0,
bearing: 0,
// //
// dragRotate: false,
// touchZoomRotate: false,
// pitchWithRotate: false,
// maxBounds: [[-180, -90], [180, 90]],// Set the map's geographical boundaries.
antialias: true,
attributionControl: false,
boxZoom: true,//trueshift
preserveDrawingBuffer: false,//truecanvas使pngmap.getCanvas().toDataURL()false
projection: 'equirectangular', // 26
});
Vue.config.maps["dataSearch"] = map;
//
window.addEventListener('resize', (event) => {
map.resize();
// // maps.forEach((key: string, value: any) => {
// // console.log(`${key}: ${value}`);
// // });
// for (let [key, value] of Object.entries(maps)) {
// // value.resize();
// }
// Object.values(maps).forEach((map) => {
// map.resize();
// });
});
// handleWheel("dataSearch");
Vue.config.maps["dataSearch"].scrollZoom.enable();
initSprites("dataSearch")
map.on('load', () => {
// 线
// loadVectorLayer("GSHHS_f_L1", ["line"], ["#5c71c3"], "visible", "dataSearch", 'ougp');
// 线
// loadVectorLayer("contour-4500m", ["line"], ["#FEFEFE"], "visible", "dataSearch", 'ougp');
// loadVectorLayer("contour-6000m", ["line"], ["#FEFEFE"], "visible", "dataSearch", 'ougp');
});
},
// cesium
@ -711,7 +856,7 @@ export default {
// 6000线
const contour6000 = new Cesium.WebMapTileServiceImageryProvider(
{
url: this.CesiumHostAddr + 'geoserver/gwc/service/wmts/rest/ougp:contour6000/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png',
url: Vue.config.baseUrl + 'geoserver/gwc/service/wmts/rest/ougp:contour6000/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png',
layer: 'ougp:contour6000',
style: 'default',
format: 'image/png',
@ -738,9 +883,17 @@ export default {
},
shousuo() {
this.show = true
// resize
setTimeout(() => {
Vue.config.maps["dataSearch"].resize();
}, 10);
},
toshow() {
this.show = false
// resize
setTimeout(() => {
Vue.config.maps["dataSearch"].resize();
}, 10);
},
//
openList(item, type) {
@ -765,8 +918,34 @@ export default {
}
},
//
onSample(sample) {
onSample(sample, job_type) {
this.currenSample = sample.tsy_id
if (job_type == "测线作业" && (!sample.start_lon || !sample.start_lat || !sample.end_lon || !sample.end_lat) || job_type == "站位作业" && (!sample.sampling_lon || !sample.sampling_lat)) {
alert('该样品无坐标信息,无法定位!');
return;
}
let lon = 0;
let lat = 0;
if (job_type == "测线作业") {
// lon = Number(sample.start_lon);
// lat = Number(sample.start_lat);
lon = (Number(sample.start_lon) + Number(sample.end_lon)) / 2;
lat = (Number(sample.start_lat) + Number(sample.end_lat)) / 2;
}
if (job_type == "站位作业") {
lon = Number(sample.sampling_lon);
lat = Number(sample.sampling_lat);
}
Vue.config.maps["dataSearch"].setZoom(10);
Vue.config.maps["dataSearch"].flyTo({
center: [lon, lat],
speed: 10
});
let layerId = `vector-${job_type == "测线作业" ? "sample_line" : "sample_station"}_${sample.dataset_name}-${job_type == "测线作业" ? "line" : "symbol"}`;
let sourceLayerId = `${job_type == "测线作业" ? "sample_line" : "sample_station"}_${sample.dataset_name}`;
// vector-sample_station_TS-46-1_FDZ377_Slurp_SWYP-symbolvector-sample_line_TS2-29-1_SY612_GQXJ_PJZP-line
// highlightFeaturesByProperty("dataSearch", `${job_type == "线" ? "sample_line" : "sample_station"}_${sample.dataset_name}`, layerId, "sample_name", sample.sample_name);
triggerLayerClick("dataSearch", sourceLayerId, layerId, [lon, lat], { "sample_name": sample.sample_name });
},
goHome() {
this.$router.push({ name: 'home' })
@ -782,6 +961,18 @@ export default {
</script>
<style scoped lang="scss">
// #cesiumContainerData {
// // position: absolute;
// left: 0px;
// top: 0px;
// bottom: 0px;
// right: 0px;
// padding: 0px;
// margin: 0px;
// width: 100%;
// height: 100vh;
// }
.shuju {
width: 100%;
height: 100%;

View File

@ -65,35 +65,19 @@
<div class="guiji_2">
<div id="cesiumContainer" ref="cesiumContainer"
onmouseleave="document.getElementById('mouseWheelTip').style.display = 'none'"
style="height: 100%;width:100%;">
onmouseenter="document.getElementById('mouseWheelTip').style.display = ''" style="height: 100%;width:100%;">
<div class="son" style="display: none;">弹框插槽</div>
<div id="mouseWheelTip" style="display: none;">地图缩放按下 Ctrl + 鼠标滚轮</div>
<el-popover
placement="left-start"
title="航次列表"
width="300"
trigger="hover"
>
<el-popover placement="left-start" title="航次列表" width="300" trigger="hover">
<div class="contour-line">
<span>等深线</span>
<el-button
size="mini"
v-for="depth in depthOptions"
:key="depth"
:type="selectedDepths.includes(depth) ? 'primary' : ''"
@click="toggleDepth(depth)"
>
<el-button size="mini" v-for="depth in depthOptions" :key="depth"
:type="selectedDepths.includes(depth) ? 'primary' : ''" @click="toggleDepth(depth)">
{{ depth }}
</el-button>
</div>
<el-table
ref="multipleTable"
:data="voyageList"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
highlight-current-row
:current-row-key="currentVoyage"
>
<el-table ref="multipleTable" :data="voyageList" @selection-change="handleSelectionChange"
@row-click="handleRowClick" highlight-current-row :current-row-key="currentVoyage">
<el-table-column type="selection" width="50" />
<el-table-column property="voyage_name" label="航次" />
</el-table>
@ -245,6 +229,7 @@
</template>
<script>
import Vue from 'vue'
import Footer from '../components/public/Footer'
import { getVoyage, statisticsNums } from '../api/home'
import { swiper, swiperSlide } from 'vue-awesome-swiper'
@ -253,6 +238,9 @@ import { sampleDatasetList, voyageRegList } from '@/api/dataTransfer'
import {
modifyCesiumMouseWheel, setImageryViewModels
} from '@/utils/common'
import { loadVectorLayer } from '@/utils/vector-layer-utils'
import { ColorGenerator } from '@/utils/color-generator';
import { handleWheel, initSprites } from "@/utils/mapbox-utils";
const Cesium = window.Cesium
@ -336,7 +324,7 @@ export default {
},
mounted() {
this.imageryViewModels = setImageryViewModels(this.CesiumHostAddr)
this.initCesium()
this.initMapbox()
},
methods: {
//
@ -345,7 +333,16 @@ export default {
start: 0,
limit: 10
}).then(res => {
this.voyageList = res.page.records
this.voyageList = res.page.records;
let i = 0;
const colors = ColorGenerator.generateDivergingColors(this.voyageList.length);
this.voyageList.forEach(element => {
//
loadVectorLayer(element.voyage_name, ["line", "symbol"], [colors[i], colors[i]], "visible", "home", "dsds");
i++;
});
this.$nextTick(() => {
//
this.$refs.multipleTable.toggleAllSelection()
@ -353,6 +350,81 @@ export default {
})
},
// mapbox
initMapbox() {
const map = new window.mapboxgl.Map({
container: 'cesiumContainer',
// style: 'mapbox://styles/mapbox/standard',
style: {
"version": 8,
"name": "default_style",
// mapbox使
"sprite": `${Vue.config.baseUrl}/oe/${Vue.config.wwwrootBaseUrl}/sprites/sprite`,
// 使
"glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf",
// glyphs 404
// "glyphs": `${Vue.config.baseUrl}/oe/${Vue.config.wwwrootBaseUrl}/glyphs/{fontstack}/{range}.pbf`,
"sources": {
"影像地图": {
"type": "raster",
"tiles": ["https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"],
"tileSize": 256,
// "minzoom": 0,
// "maxzoom": baseMapMaxZoom
}
},
"layers": [
{
"id": "影像地图",
"type": "raster",
"source": "影像地图",
"layout": { visibility: "visible" },
// "minzoom": 0,
// "maxzoom": baseMapMaxZoom
}
]
},
config: {
basemap: {
theme: 'monochrome',
lightPreset: 'night'
}
},
zoom: 5,
center: [113, 15.5],
minZoom: 2,
maxZoom: 29,
trackResize: true,
scrollZoom: false, //
//
localIdeographFontFamily: "Microsoft YoHei, 'Noto Sans', 'Noto Sans CJK SC', sans-serif",
pitch: 0, // 0
bearing: 0, // 0
//
dragRotate: false,
touchZoomRotate: false,
pitchWithRotate: false,
// maxBounds: [[-180, -90], [180, 90]],// Set the map's geographical boundaries.
antialias: true,
attributionControl: false,
boxZoom: true,//trueshift
preserveDrawingBuffer: false,//truecanvas使pngmap.getCanvas().toDataURL()false
projection: 'equirectangular', // 26
});
Vue.config.maps["home"] = map;
handleWheel("home");
initSprites("home")
map.on('load', () => {
// 线
// loadVectorLayer("GSHHS_f_L1", ["line"], ["#FEFEFE"], "home", 'ougp');
// 线
loadVectorLayer("contour-4500m", ["line"], ["#FEFEFE"], "none", "home", 'ougp');
loadVectorLayer("contour-6000m", ["line"], ["#FEFEFE"], "none", "home", 'ougp');
});
},
// cesium
initCesium() {
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkY2Y5NWY2Ni1mODQ1LTQ3YTUtYjc4Zi1jYzhjMGI2YzcxMWYiLCJpZCI6MTI5ODMwLCJpYXQiOjE2Nzk0Njk5NTd9.DTH54ioOH-HLqeNIetBe9hFyrPOX2Vp1AQmZzw8TIZ4'
@ -432,6 +504,14 @@ export default {
},
//
handleRowClick(row, column, event) {
// /
let visibility = Vue.config.maps["home"].getLayoutProperty(`vector-${row.voyage_name}-symbol`, "visibility");
visibility = visibility == "visible" ? "none" : "visible";
Vue.config.maps["home"].setLayoutProperty(`vector-${row.voyage_name}-symbol`, "visibility", visibility);
Vue.config.maps["home"].setLayoutProperty(`vector-${row.voyage_name}-line`, "visibility", visibility);
this.$refs.multipleTable.toggleRowSelection(row);
return;
//
if (this.currentVoyage === row.voyage_name) {
this.clearHighlight();
@ -492,7 +572,9 @@ export default {
//
voyagesToRemove.forEach(voyage => {
this.removeSingleLineImg(voyage);
// this.removeSingleLineImg(voyage);
Vue.config.maps["home"].setLayoutProperty(`vector-${voyage}-symbol`, "visibility", "none");
Vue.config.maps["home"].setLayoutProperty(`vector-${voyage}-line`, "visibility", "none");
});
//
@ -500,7 +582,9 @@ export default {
// val
const correspondingItem = val.find(item => item.voyage_name === voyage);
if (correspondingItem) {
this.addSingleLineImg(correspondingItem);
// this.addSingleLineImg(correspondingItem);
Vue.config.maps["home"].setLayoutProperty(`vector-${voyage}-symbol`, "visibility", "visible");
Vue.config.maps["home"].setLayoutProperty(`vector-${voyage}-line`, "visibility", "visible");
}
});
@ -517,7 +601,6 @@ export default {
return;
}
//
const trackLine = new Cesium.WebMapTileServiceImageryProvider({
url: `${this.CesiumHostAddr}geoserver/gwc/service/wmts/rest/dsds:${voyageName}/{TileMatrixSet}/{TileMatrixSet}:{TileMatrix}/{TileRow}/{TileCol}?format=image/png`,
layer: `dsds:${voyageName}`,
@ -552,10 +635,12 @@ export default {
const index = this.selectedDepths.indexOf(depth);
if (index > -1) {
this.selectedDepths.splice(index, 1); //
this.removeSingleContourLine(depth); // 线
// this.removeSingleContourLine(depth); // 线
Vue.config.maps["home"].setLayoutProperty(`vector-contour-${depth}m-line`, "visibility", "none");
} else {
this.selectedDepths.push(depth); //
this.addSingleContourLine(depth); // 线
// this.addSingleContourLine(depth); // 线
Vue.config.maps["home"].setLayoutProperty(`vector-contour-${depth}m-line`, "visibility", "visible");
}
},
// 线

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
static/sprites/Able.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
static/sprites/Bowditch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
static/sprites/Henson.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
static/sprites/Loyal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
static/sprites/aircraft.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
static/sprites/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

BIN
static/sprites/auv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

BIN
static/sprites/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
static/sprites/dot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

BIN
static/sprites/dot_red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
static/sprites/fdz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
static/sprites/go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

File diff suppressed because one or more lines are too long

BIN
static/sprites/naivemap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
static/sprites/ship.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/sprites/ship1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/sprites/ship11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/sprites/ship2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/sprites/ship3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/sprites/shipdt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/sprites/shys.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
static/sprites/slow_sog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

File diff suppressed because one or more lines are too long

BIN
static/sprites/sprite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
static/sprites/stop_sog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

BIN
static/sprites/szy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB