|
|
// 全局变量
|
|
|
let currentModule = 'data-reception';
|
|
|
let dataStreamInterval;
|
|
|
let timeUpdateInterval;
|
|
|
let processingTaskInterval;
|
|
|
|
|
|
// DOM加载完成后初始化
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
initializeApp();
|
|
|
});
|
|
|
|
|
|
// 应用初始化
|
|
|
function initializeApp() {
|
|
|
setupNavigation();
|
|
|
setupTimeDisplay();
|
|
|
setupDataStream();
|
|
|
setupProcessingNodes();
|
|
|
setupVisualizationControls();
|
|
|
setupDataPoints();
|
|
|
|
|
|
console.log('海洋数据系统DEMO已启动');
|
|
|
}
|
|
|
|
|
|
// 设置导航功能
|
|
|
function setupNavigation() {
|
|
|
const navItems = document.querySelectorAll('.nav-item');
|
|
|
const modules = document.querySelectorAll('.module');
|
|
|
|
|
|
navItems.forEach(item => {
|
|
|
item.addEventListener('click', function() {
|
|
|
const targetModule = this.getAttribute('data-module');
|
|
|
|
|
|
// 更新导航状态
|
|
|
navItems.forEach(nav => nav.classList.remove('active'));
|
|
|
this.classList.add('active');
|
|
|
|
|
|
// 切换模块显示
|
|
|
modules.forEach(module => module.classList.remove('active'));
|
|
|
document.getElementById(targetModule).classList.add('active');
|
|
|
|
|
|
currentModule = targetModule;
|
|
|
|
|
|
// 根据模块执行特定初始化
|
|
|
switch(targetModule) {
|
|
|
case 'data-reception':
|
|
|
startDataReception();
|
|
|
break;
|
|
|
case 'data-processing':
|
|
|
startProcessingMonitor();
|
|
|
break;
|
|
|
case 'data-visualization':
|
|
|
startVisualization();
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
console.log(`切换到模块: ${targetModule}`);
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 设置时间显示
|
|
|
function setupTimeDisplay() {
|
|
|
const timeDisplay = document.getElementById('currentTime');
|
|
|
|
|
|
function updateTime() {
|
|
|
const now = new Date();
|
|
|
const timeString = now.toLocaleTimeString('zh-CN', {
|
|
|
hour12: false,
|
|
|
hour: '2-digit',
|
|
|
minute: '2-digit',
|
|
|
second: '2-digit'
|
|
|
});
|
|
|
timeDisplay.textContent = timeString;
|
|
|
}
|
|
|
|
|
|
updateTime();
|
|
|
timeUpdateInterval = setInterval(updateTime, 1000);
|
|
|
}
|
|
|
|
|
|
// 设置数据流显示
|
|
|
// 数据流管理类
|
|
|
class DataStreamManager {
|
|
|
constructor() {
|
|
|
this.container = null;
|
|
|
this.interval = null;
|
|
|
this.maxItems = 12;
|
|
|
this.updateFrequency = 500; // 1秒更新一次
|
|
|
this.isRunning = false;
|
|
|
}
|
|
|
|
|
|
init() {
|
|
|
this.container = document.querySelector('.data-stream');
|
|
|
if (!this.container) {
|
|
|
console.error('数据流容器未找到');
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
// 清空容器
|
|
|
this.container.innerHTML = '';
|
|
|
|
|
|
// 初始化几个数据项
|
|
|
this.generateInitialData();
|
|
|
|
|
|
// 开始定期更新
|
|
|
this.start();
|
|
|
|
|
|
console.log('数据流管理器初始化完成');
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
generateInitialData() {
|
|
|
// 生成3-5个初始数据项
|
|
|
const initialCount = Math.floor(Math.random() * 3) + 3;
|
|
|
for (let i = 0; i < initialCount; i++) {
|
|
|
setTimeout(() => {
|
|
|
this.addDataItem();
|
|
|
}, i * 300);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
addDataItem() {
|
|
|
if (!this.container) return;
|
|
|
|
|
|
// 随机选择设备
|
|
|
const device = oceanDevices[Math.floor(Math.random() * oceanDevices.length)];
|
|
|
const param = device.parameters[Math.floor(Math.random() * device.parameters.length)];
|
|
|
const value = this.generateValue(param);
|
|
|
|
|
|
const timestamp = new Date().toLocaleTimeString('zh-CN', {
|
|
|
hour12: false,
|
|
|
hour: '2-digit',
|
|
|
minute: '2-digit',
|
|
|
second: '2-digit'
|
|
|
});
|
|
|
|
|
|
// 创建数据项
|
|
|
const item = this.createDataItem(device, param, value, timestamp);
|
|
|
|
|
|
// 添加到容器顶部
|
|
|
this.container.insertBefore(item, this.container.firstChild);
|
|
|
|
|
|
// 限制数量
|
|
|
this.limitItems();
|
|
|
|
|
|
// 触发进入动画
|
|
|
requestAnimationFrame(() => {
|
|
|
item.style.opacity = '1';
|
|
|
item.style.transform = 'translateX(0)';
|
|
|
});
|
|
|
}
|
|
|
|
|
|
createDataItem(device, param, value, timestamp) {
|
|
|
const item = document.createElement('div');
|
|
|
item.className = `stream-item ${this.getDeviceClass(device.type)}`;
|
|
|
|
|
|
// 参数名称映射
|
|
|
const paramNames = {
|
|
|
'time': '时间', 'lng_lat': '经纬度', 'temp': '温度', 'cond': '电导率',
|
|
|
'depth': '深度', 'salinity': '盐度', 'flow_velocity': '流速', 'flow_direction': '流向',
|
|
|
'gravity': '重力', 'magnetic_field_1': '磁场强度1', 'magnetic_field_2': '磁场强度2',
|
|
|
'wave_height_max': '最大波高', 'wave_height_effective': '有效波高', 'cycle_max': '最大周期',
|
|
|
'wind_speed': '风速', 'wind_direction': '风向', 'air_pressure': '气压',
|
|
|
'humidity': '湿度', 'visibility': '能见度', 'cloud_height_1': '云高1',
|
|
|
'irradiance': '辐照度', 'turbidity': '浊度', 'chlorophyll': '叶绿素'
|
|
|
};
|
|
|
|
|
|
item.innerHTML = `
|
|
|
<div class="stream-device-info">
|
|
|
<div class="stream-device-icon">${this.getDeviceIcon(device.type)}</div>
|
|
|
<div class="stream-device-details">
|
|
|
<div class="stream-device-name">${device.name}</div>
|
|
|
<div class="stream-parameter">${paramNames[param] || param}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stream-value-section">
|
|
|
<div class="stream-value" style="color: ${device.color}">${value}</div>
|
|
|
<div class="stream-timestamp">${timestamp}</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
// 添加点击事件
|
|
|
item.addEventListener('click', () => {
|
|
|
this.showItemDetails(device, param, value, timestamp);
|
|
|
});
|
|
|
|
|
|
return item;
|
|
|
}
|
|
|
|
|
|
limitItems() {
|
|
|
const items = this.container.querySelectorAll('.stream-item');
|
|
|
if (items.length > this.maxItems) {
|
|
|
const excess = items.length - this.maxItems;
|
|
|
for (let i = 0; i < excess; i++) {
|
|
|
const lastItem = items[items.length - 1 - i];
|
|
|
lastItem.classList.add('removing');
|
|
|
setTimeout(() => {
|
|
|
if (lastItem.parentNode) {
|
|
|
lastItem.remove();
|
|
|
}
|
|
|
}, 300);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
generateValue(param) {
|
|
|
const valueRanges = {
|
|
|
'temp': () => (Math.random() * 30 + 5).toFixed(1) + '°C',
|
|
|
'depth': () => (Math.random() * 6000 + 10).toFixed(0) + 'm',
|
|
|
'flow_velocity': () => (Math.random() * 2 + 0.1).toFixed(2) + 'm/s',
|
|
|
'wind_speed': () => (Math.random() * 20 + 2).toFixed(1) + 'm/s',
|
|
|
'air_pressure': () => (Math.random() * 50 + 1000).toFixed(1) + 'hPa',
|
|
|
'humidity': () => (Math.random() * 40 + 40).toFixed(0) + '%',
|
|
|
'wave_height_max': () => (Math.random() * 8 + 0.5).toFixed(1) + 'm',
|
|
|
'salinity': () => (Math.random() * 5 + 30).toFixed(2) + 'psu',
|
|
|
'gravity': () => (Math.random() * 0.1 + 9.8).toFixed(3) + 'm/s²',
|
|
|
'visibility': () => (Math.random() * 20 + 5).toFixed(1) + 'km',
|
|
|
'cloud_height_1': () => (Math.random() * 3000 + 500).toFixed(0) + 'm'
|
|
|
};
|
|
|
|
|
|
return valueRanges[param] ? valueRanges[param]() : (Math.random() * 100).toFixed(2);
|
|
|
}
|
|
|
|
|
|
getDeviceClass(type) {
|
|
|
const classMap = {
|
|
|
'CTD': 'device-ctd', 'ADCP_38K': 'device-adcp', 'ADCP_150K': 'device-adcp',
|
|
|
'GRAVITY': 'device-gravity', 'MAGNETIC': 'device-magnetic', 'WAVE_GAUGE': 'device-wave',
|
|
|
'DEPTH_SOUNDER': 'device-depth', 'WAVE_RADAR': 'device-radar', 'MULTI_SENSOR': 'device-multi',
|
|
|
'WAVE_BUOY': 'device-buoy', 'BEIDOU': 'device-beidou', 'WIND_PROFILER': 'device-wind',
|
|
|
'WEATHER_STATION': 'device-weather', 'SKY_SCANNER': 'device-sky', 'TURBULENCE': 'device-turbulence',
|
|
|
'LIDAR_WIND': 'device-lidar', 'VISIBILITY': 'device-visibility', 'CLOUD_HEIGHT': 'device-cloud',
|
|
|
'NAVIGATION': 'device-navigation'
|
|
|
};
|
|
|
return classMap[type] || 'device-default';
|
|
|
}
|
|
|
|
|
|
getDeviceIcon(type) {
|
|
|
const iconMap = {
|
|
|
'CTD': '🌊', 'ADCP_38K': '🌀', 'ADCP_150K': '🌀', 'GRAVITY': '⚖️', 'MAGNETIC': '🧲',
|
|
|
'WAVE_GAUGE': '〰️', 'DEPTH_SOUNDER': '📏', 'WAVE_RADAR': '📡', 'MULTI_SENSOR': '🔬',
|
|
|
'WAVE_BUOY': '🛟', 'BEIDOU': '🛰️', 'WIND_PROFILER': '💨', 'WEATHER_STATION': '🌤️',
|
|
|
'SKY_SCANNER': '🌌', 'TURBULENCE': '🌪️', 'LIDAR_WIND': '🔦', 'VISIBILITY': '👁️',
|
|
|
'CLOUD_HEIGHT': '☁️', 'NAVIGATION': '🧭'
|
|
|
};
|
|
|
return iconMap[type] || '📊';
|
|
|
}
|
|
|
|
|
|
showItemDetails(device, param, value, timestamp) {
|
|
|
// 创建模态框
|
|
|
const modal = document.createElement('div');
|
|
|
modal.className = 'stream-detail-modal';
|
|
|
modal.style.cssText = `
|
|
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
|
background: rgba(0, 0, 0, 0.8); display: flex; align-items: center;
|
|
|
justify-content: center; z-index: 1000; backdrop-filter: blur(5px);
|
|
|
`;
|
|
|
|
|
|
const content = document.createElement('div');
|
|
|
content.style.cssText = `
|
|
|
background: linear-gradient(135deg, #1e3a8a 0%, #0f172a 100%);
|
|
|
border-radius: 16px; padding: 24px; max-width: 400px; width: 90%;
|
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
|
|
|
`;
|
|
|
|
|
|
content.innerHTML = `
|
|
|
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
|
|
<div style="font-size: 24px;">${this.getDeviceIcon(device.type)}</div>
|
|
|
<div>
|
|
|
<h3 style="margin: 0; color: white; font-size: 1.2rem;">${device.name}</h3>
|
|
|
<p style="margin: 4px 0 0 0; color: #94a3b8; font-size: 0.9rem;">设备ID: ${device.id}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="background: rgba(255, 255, 255, 0.05); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
|
<span style="color: #94a3b8;">参数</span>
|
|
|
<span style="color: white; font-weight: 600;">${param}</span>
|
|
|
</div>
|
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
|
<span style="color: #94a3b8;">数值</span>
|
|
|
<span style="color: ${device.color}; font-weight: 700; font-family: monospace;">${value}</span>
|
|
|
</div>
|
|
|
<div style="display: flex; justify-content: space-between;">
|
|
|
<span style="color: #94a3b8;">时间</span>
|
|
|
<span style="color: white; font-family: monospace;">${timestamp}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<button onclick="this.closest('.stream-detail-modal').remove()"
|
|
|
style="width: 100%; padding: 12px; background: ${device.color}; color: white;
|
|
|
border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">
|
|
|
关闭
|
|
|
</button>
|
|
|
`;
|
|
|
|
|
|
modal.appendChild(content);
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
// 点击背景关闭
|
|
|
modal.addEventListener('click', (e) => {
|
|
|
if (e.target === modal) modal.remove();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
start() {
|
|
|
if (this.isRunning) return;
|
|
|
|
|
|
this.isRunning = true;
|
|
|
this.interval = setInterval(() => {
|
|
|
this.addDataItem();
|
|
|
}, this.updateFrequency);
|
|
|
|
|
|
console.log('数据流开始运行');
|
|
|
}
|
|
|
|
|
|
stop() {
|
|
|
if (this.interval) {
|
|
|
clearInterval(this.interval);
|
|
|
this.interval = null;
|
|
|
}
|
|
|
this.isRunning = false;
|
|
|
console.log('数据流已停止');
|
|
|
}
|
|
|
|
|
|
restart() {
|
|
|
this.stop();
|
|
|
setTimeout(() => {
|
|
|
this.start();
|
|
|
}, 100);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 创建全局数据流管理器实例
|
|
|
const dataStreamManager = new DataStreamManager();
|
|
|
|
|
|
// 重新定义setupDataStream函数
|
|
|
function setupDataStream() {
|
|
|
return dataStreamManager.init();
|
|
|
}
|
|
|
|
|
|
// 设置数据点交互
|
|
|
function setupDataPoints() {
|
|
|
const dataPoints = document.querySelectorAll('.data-point');
|
|
|
const sourceDetails = document.getElementById('sourceDetails');
|
|
|
|
|
|
const sourceInfo = {
|
|
|
'浮标-001': {
|
|
|
location: '北纬 25.2°, 东经 120.5°',
|
|
|
status: '在线',
|
|
|
lastUpdate: '2分钟前',
|
|
|
type: '海洋浮标'
|
|
|
},
|
|
|
'卫星-A': {
|
|
|
location: '轨道高度 705km',
|
|
|
status: '在线',
|
|
|
lastUpdate: '1分钟前',
|
|
|
type: '海洋观测卫星'
|
|
|
},
|
|
|
'监测站-B': {
|
|
|
location: '北纬 22.8°, 东经 118.3°',
|
|
|
status: '在线',
|
|
|
lastUpdate: '30秒前',
|
|
|
type: '海岸监测站'
|
|
|
},
|
|
|
'船舶-C': {
|
|
|
location: '北纬 24.1°, 东经 119.7°',
|
|
|
status: '在线',
|
|
|
lastUpdate: '5分钟前',
|
|
|
type: '科考船'
|
|
|
}
|
|
|
};
|
|
|
|
|
|
dataPoints.forEach(point => {
|
|
|
point.addEventListener('click', function() {
|
|
|
const sourceName = this.getAttribute('data-source');
|
|
|
const info = sourceInfo[sourceName];
|
|
|
|
|
|
if (info && sourceDetails) {
|
|
|
sourceDetails.innerHTML = `
|
|
|
<div class="detail-item">
|
|
|
<span class="detail-label">数据源:</span>
|
|
|
<span class="detail-value">${sourceName}</span>
|
|
|
</div>
|
|
|
<div class="detail-item">
|
|
|
<span class="detail-label">类型:</span>
|
|
|
<span class="detail-value">${info.type}</span>
|
|
|
</div>
|
|
|
<div class="detail-item">
|
|
|
<span class="detail-label">位置:</span>
|
|
|
<span class="detail-value">${info.location}</span>
|
|
|
</div>
|
|
|
<div class="detail-item">
|
|
|
<span class="detail-label">状态:</span>
|
|
|
<span class="detail-value status-online">${info.status}</span>
|
|
|
</div>
|
|
|
<div class="detail-item">
|
|
|
<span class="detail-label">最后更新:</span>
|
|
|
<span class="detail-value">${info.lastUpdate}</span>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
// 高亮效果
|
|
|
dataPoints.forEach(p => p.classList.remove('highlighted'));
|
|
|
this.classList.add('highlighted');
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 设置处理节点交互
|
|
|
function setupProcessingNodes() {
|
|
|
const nodes = document.querySelectorAll('.pipeline-node');
|
|
|
const nodeTitle = document.getElementById('nodeTitle');
|
|
|
const nodeDetails = document.getElementById('nodeDetails');
|
|
|
|
|
|
const nodeInfo = {
|
|
|
'input': {
|
|
|
title: '原始数据输入',
|
|
|
details: `
|
|
|
<div class="node-info">
|
|
|
<p><strong>功能:</strong> 接收来自各种数据源的原始海洋数据</p>
|
|
|
<p><strong>数据类型:</strong> 温度、盐度、叶绿素、波高等</p>
|
|
|
<p><strong>当前状态:</strong> <span style="color: #10b981;">正常运行</span></p>
|
|
|
<p><strong>处理速率:</strong> 15.2 MB/s</p>
|
|
|
</div>
|
|
|
`
|
|
|
},
|
|
|
'clean': {
|
|
|
title: '数据清洗',
|
|
|
details: `
|
|
|
<div class="node-info">
|
|
|
<p><strong>功能:</strong> 去除异常值、填补缺失数据</p>
|
|
|
<p><strong>质量指标:</strong></p>
|
|
|
<ul>
|
|
|
<li>数据完整性: 98.5%</li>
|
|
|
<li>异常值检出率: 2.1%</li>
|
|
|
<li>处理成功率: 99.8%</li>
|
|
|
</ul>
|
|
|
<p><strong>算法:</strong> 3σ准则 + 卡尔曼滤波</p>
|
|
|
</div>
|
|
|
`
|
|
|
},
|
|
|
'format': {
|
|
|
title: '数据格式化',
|
|
|
details: `
|
|
|
<div class="node-info">
|
|
|
<p><strong>功能:</strong> 统一数据格式和坐标系统</p>
|
|
|
<p><strong>输出格式:</strong> NetCDF-4</p>
|
|
|
<p><strong>坐标系:</strong> WGS84</p>
|
|
|
<p><strong>时间标准:</strong> UTC</p>
|
|
|
<p><strong>处理延迟:</strong> < 5秒</p>
|
|
|
</div>
|
|
|
`
|
|
|
},
|
|
|
'interpolation': {
|
|
|
title: '三维插值',
|
|
|
details: `
|
|
|
<div class="node-info">
|
|
|
<p><strong>功能:</strong> 生成规则网格的三维温度场</p>
|
|
|
<p><strong>插值方法:</strong> 克里金插值</p>
|
|
|
<p><strong>质量评估:</strong></p>
|
|
|
<ul>
|
|
|
<li>相关系数: 0.95</li>
|
|
|
<li>RMSE: 0.2°C</li>
|
|
|
<li>标准差: 0.15</li>
|
|
|
</ul>
|
|
|
<p><strong>网格分辨率:</strong> 0.1° × 0.1°</p>
|
|
|
</div>
|
|
|
`
|
|
|
},
|
|
|
'analysis': {
|
|
|
title: '叶绿素反演',
|
|
|
details: `
|
|
|
<div class="node-info">
|
|
|
<p><strong>功能:</strong> 基于多光谱数据反演叶绿素浓度</p>
|
|
|
<p><strong>算法:</strong> OC4v6 + 神经网络</p>
|
|
|
<p><strong>波段:</strong> 443, 490, 510, 555 nm</p>
|
|
|
<p><strong>精度:</strong> ±0.3 mg/m³</p>
|
|
|
<p><strong>覆盖范围:</strong> 全球海域</p>
|
|
|
</div>
|
|
|
`
|
|
|
},
|
|
|
'output': {
|
|
|
title: '产品数据库',
|
|
|
details: `
|
|
|
<div class="node-info">
|
|
|
<p><strong>功能:</strong> 存储最终处理产品</p>
|
|
|
<p><strong>存储格式:</strong> HDF5 + 元数据</p>
|
|
|
<p><strong>数据产品:</strong></p>
|
|
|
<ul>
|
|
|
<li>海表温度场</li>
|
|
|
<li>叶绿素分布图</li>
|
|
|
<li>三维温盐结构</li>
|
|
|
</ul>
|
|
|
<p><strong>更新频率:</strong> 每小时</p>
|
|
|
</div>
|
|
|
`
|
|
|
}
|
|
|
};
|
|
|
|
|
|
nodes.forEach(node => {
|
|
|
node.addEventListener('click', function() {
|
|
|
const step = this.getAttribute('data-step');
|
|
|
const info = nodeInfo[step];
|
|
|
|
|
|
if (info && nodeTitle && nodeDetails) {
|
|
|
nodeTitle.textContent = info.title;
|
|
|
nodeDetails.innerHTML = info.details;
|
|
|
}
|
|
|
|
|
|
// 高亮效果
|
|
|
nodes.forEach(n => n.classList.remove('highlighted'));
|
|
|
this.classList.add('highlighted');
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 设置可视化控件
|
|
|
function setupVisualizationControls() {
|
|
|
const playButton = document.querySelector('#data-visualization .btn-primary');
|
|
|
const dataSelect = document.querySelector('#data-visualization .control-select');
|
|
|
|
|
|
if (playButton) {
|
|
|
playButton.addEventListener('click', function() {
|
|
|
const icon = this.querySelector('i');
|
|
|
const text = this.querySelector('span') || this.childNodes[this.childNodes.length - 1];
|
|
|
|
|
|
if (icon.classList.contains('fa-play')) {
|
|
|
icon.classList.remove('fa-play');
|
|
|
icon.classList.add('fa-pause');
|
|
|
if (text) text.textContent = ' 暂停动画';
|
|
|
startTemperatureAnimation();
|
|
|
} else {
|
|
|
icon.classList.remove('fa-pause');
|
|
|
icon.classList.add('fa-play');
|
|
|
if (text) text.textContent = ' 播放动画';
|
|
|
stopTemperatureAnimation();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (dataSelect) {
|
|
|
dataSelect.addEventListener('change', function() {
|
|
|
updateVisualizationData(this.value);
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 启动数据接收
|
|
|
function startDataReception() {
|
|
|
console.log('数据接收模块已激活');
|
|
|
// 重新启动数据流(如果已停止)
|
|
|
if (!dataStreamInterval) {
|
|
|
setupDataStream();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 启动处理监控
|
|
|
function startProcessingMonitor() {
|
|
|
console.log('数据处理模块已激活');
|
|
|
startProcessingTask();
|
|
|
}
|
|
|
|
|
|
// 启动可视化
|
|
|
function startVisualization() {
|
|
|
console.log('数据可视化模块已激活');
|
|
|
updateMetrics();
|
|
|
}
|
|
|
|
|
|
// 启动处理任务演示
|
|
|
function startProcessingTask() {
|
|
|
const nodes = document.querySelectorAll('.pipeline-node');
|
|
|
const logContent = document.getElementById('logContent');
|
|
|
|
|
|
function addLogEntry(message, type = 'info') {
|
|
|
if (!logContent) return;
|
|
|
|
|
|
const now = new Date();
|
|
|
const timeString = now.toLocaleTimeString('zh-CN', {
|
|
|
hour12: false,
|
|
|
hour: '2-digit',
|
|
|
minute: '2-digit',
|
|
|
second: '2-digit'
|
|
|
});
|
|
|
|
|
|
const logEntry = document.createElement('div');
|
|
|
logEntry.className = `log-entry ${type}`;
|
|
|
logEntry.textContent = `[${timeString}] ${message}`;
|
|
|
|
|
|
logContent.insertBefore(logEntry, logContent.firstChild);
|
|
|
|
|
|
// 限制日志条数
|
|
|
while (logContent.children.length > 20) {
|
|
|
logContent.removeChild(logContent.lastChild);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function simulateProcessing() {
|
|
|
const messages = [
|
|
|
{ msg: '数据块 #A7B2 处理完成', type: 'info' },
|
|
|
{ msg: '三维插值算法执行成功', type: 'info' },
|
|
|
{ msg: '叶绿素反演计算完成', type: 'info' },
|
|
|
{ msg: '警告: 数据源 浮标-003 信号弱', type: 'warning' },
|
|
|
{ msg: '产品数据已存储到数据库', type: 'info' },
|
|
|
{ msg: '质量检验通过率: 98.5%', type: 'info' }
|
|
|
];
|
|
|
|
|
|
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
|
|
|
addLogEntry(randomMessage.msg, randomMessage.type);
|
|
|
}
|
|
|
|
|
|
// 定期添加日志
|
|
|
processingTaskInterval = setInterval(simulateProcessing, 5000);
|
|
|
|
|
|
// 初始日志
|
|
|
setTimeout(() => addLogEntry('南海温度场与叶绿素产品生成任务启动'), 1000);
|
|
|
}
|
|
|
|
|
|
// 温度动画控制
|
|
|
let temperatureAnimationInterval;
|
|
|
|
|
|
function startTemperatureAnimation() {
|
|
|
const temperatureZones = document.querySelectorAll('.temperature-zone');
|
|
|
|
|
|
temperatureAnimationInterval = setInterval(() => {
|
|
|
temperatureZones.forEach(zone => {
|
|
|
// 随机改变温度区域的大小和位置
|
|
|
const newTop = Math.random() * 70 + 10;
|
|
|
const newLeft = Math.random() * 70 + 10;
|
|
|
const newWidth = Math.random() * 20 + 15;
|
|
|
const newHeight = Math.random() * 15 + 10;
|
|
|
|
|
|
zone.style.top = newTop + '%';
|
|
|
zone.style.left = newLeft + '%';
|
|
|
zone.style.width = newWidth + '%';
|
|
|
zone.style.height = newHeight + '%';
|
|
|
});
|
|
|
}, 2000);
|
|
|
}
|
|
|
|
|
|
function stopTemperatureAnimation() {
|
|
|
if (temperatureAnimationInterval) {
|
|
|
clearInterval(temperatureAnimationInterval);
|
|
|
temperatureAnimationInterval = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 更新可视化数据
|
|
|
function updateVisualizationData(dataType) {
|
|
|
const metrics = document.querySelectorAll('.metric-value');
|
|
|
|
|
|
const dataValues = {
|
|
|
'温度数据': ['24.5°C', '2.8m', '3'],
|
|
|
'盐度数据': ['34.2‰', '1.9m', '2'],
|
|
|
'叶绿素数据': ['0.8mg/m³', '2.1m', '4']
|
|
|
};
|
|
|
|
|
|
const values = dataValues[dataType] || dataValues['温度数据'];
|
|
|
|
|
|
metrics.forEach((metric, index) => {
|
|
|
if (values[index]) {
|
|
|
metric.textContent = values[index];
|
|
|
}
|
|
|
});
|
|
|
|
|
|
console.log(`切换到${dataType}显示`);
|
|
|
}
|
|
|
|
|
|
// 更新指标数据
|
|
|
function updateMetrics() {
|
|
|
const statValues = document.querySelectorAll('.stat-value');
|
|
|
|
|
|
// 模拟数据更新
|
|
|
setInterval(() => {
|
|
|
statValues.forEach(stat => {
|
|
|
const currentText = stat.textContent;
|
|
|
if (currentText.includes('MB/s')) {
|
|
|
const newValue = (Math.random() * 10 + 10).toFixed(1);
|
|
|
stat.textContent = newValue + ' MB/s';
|
|
|
} else if (currentText.includes('°C')) {
|
|
|
const newValue = (Math.random() * 5 + 22).toFixed(1);
|
|
|
stat.textContent = newValue + '°C';
|
|
|
} else if (!isNaN(parseInt(currentText))) {
|
|
|
const baseValue = parseInt(currentText);
|
|
|
const variation = Math.floor(Math.random() * 10 - 5);
|
|
|
stat.textContent = Math.max(0, baseValue + variation);
|
|
|
}
|
|
|
});
|
|
|
}, 10000);
|
|
|
}
|
|
|
|
|
|
// 添加CSS类用于高亮效果
|
|
|
const style = document.createElement('style');
|
|
|
style.textContent = `
|
|
|
.data-point.highlighted {
|
|
|
transform: scale(1.5);
|
|
|
box-shadow: 0 0 20px var(--accent-green);
|
|
|
z-index: 20;
|
|
|
}
|
|
|
|
|
|
.pipeline-node.highlighted {
|
|
|
transform: translateY(-5px) scale(1.05);
|
|
|
box-shadow: 0 15px 40px rgba(0, 212, 255, 0.4);
|
|
|
border-color: var(--accent-blue);
|
|
|
}
|
|
|
|
|
|
.node-info {
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.node-info ul {
|
|
|
margin: 0.5rem 0;
|
|
|
padding-left: 1rem;
|
|
|
}
|
|
|
|
|
|
.node-info li {
|
|
|
margin: 0.25rem 0;
|
|
|
color: var(--text-secondary);
|
|
|
}
|
|
|
|
|
|
.node-info strong {
|
|
|
color: var(--accent-blue);
|
|
|
}
|
|
|
`;
|
|
|
document.head.appendChild(style);
|
|
|
|
|
|
// 清理函数
|
|
|
function cleanup() {
|
|
|
if (dataStreamInterval) clearInterval(dataStreamInterval);
|
|
|
if (timeUpdateInterval) clearInterval(timeUpdateInterval);
|
|
|
if (processingTaskInterval) clearInterval(processingTaskInterval);
|
|
|
if (temperatureAnimationInterval) clearInterval(temperatureAnimationInterval);
|
|
|
}
|
|
|
|
|
|
// 页面卸载时清理
|
|
|
window.addEventListener('beforeunload', cleanup);
|
|
|
|
|
|
// 导出主要函数供调试使用
|
|
|
window.OceanDemo = {
|
|
|
switchModule: function(moduleName) {
|
|
|
const navItem = document.querySelector(`[data-module="${moduleName}"]`);
|
|
|
if (navItem) navItem.click();
|
|
|
},
|
|
|
startProcessingDemo: startProcessingTask,
|
|
|
updateVisualization: updateVisualizationData,
|
|
|
cleanup: cleanup
|
|
|
};
|
|
|
|
|
|
console.log('海洋数据系统DEMO脚本加载完成');
|
|
|
|
|
|
// 真实海洋设备数据配置
|
|
|
const oceanDevices = [
|
|
|
{
|
|
|
id: 'ctd_001',
|
|
|
name: 'CTD温盐深仪',
|
|
|
type: 'CTD',
|
|
|
position: { lat: 35.2, lng: 120.5 },
|
|
|
parameters: ['time', 'lng_lat', 'temp', 'cond', 'depth'],
|
|
|
status: 'online',
|
|
|
color: '#00ff88'
|
|
|
},
|
|
|
{
|
|
|
id: 'adcp_38k_001',
|
|
|
name: '船载ADCP系统(38k)',
|
|
|
type: 'ADCP_38K',
|
|
|
position: { lat: 36.1, lng: 121.2 },
|
|
|
parameters: ['data_time', 'lng_lat', 'depth', 'flow_velocity', 'flow_direction', 'flow_velocity_e', 'flow_velocity_n', 'flow_velocity_v'],
|
|
|
status: 'online',
|
|
|
color: '#0088ff'
|
|
|
},
|
|
|
{
|
|
|
id: 'adcp_150k_001',
|
|
|
name: '船载ADCP系统(150k)',
|
|
|
type: 'ADCP_150K',
|
|
|
position: { lat: 34.8, lng: 119.8 },
|
|
|
parameters: ['data_time', 'lng_lat', 'depth', 'flow_velocity', 'flow_direction', 'flow_velocity_e', 'flow_velocity_n', 'flow_velocity_v'],
|
|
|
status: 'online',
|
|
|
color: '#0066ff'
|
|
|
},
|
|
|
{
|
|
|
id: 'gravity_001',
|
|
|
name: '重力仪',
|
|
|
type: 'GRAVITY',
|
|
|
position: { lat: 35.5, lng: 120.8 },
|
|
|
parameters: ['data_time', 'lng_lat', 'gravity'],
|
|
|
status: 'online',
|
|
|
color: '#ff6600'
|
|
|
},
|
|
|
{
|
|
|
id: 'magnetic_001',
|
|
|
name: '磁力仪',
|
|
|
type: 'MAGNETIC',
|
|
|
position: { lat: 36.3, lng: 121.5 },
|
|
|
parameters: ['data_time', 'lng_lat', 'magnetic_field_1', 'magnetic_field_2'],
|
|
|
status: 'online',
|
|
|
color: '#ff0066'
|
|
|
},
|
|
|
{
|
|
|
id: 'wave_gauge_001',
|
|
|
name: '波潮仪',
|
|
|
type: 'WAVE_GAUGE',
|
|
|
position: { lat: 35.8, lng: 120.2 },
|
|
|
parameters: ['data_time', 'lng_lat', 'wave_height_max', 'cycle_max', 'wave_height_1_10', 'cycle_1_10', 'wave_height_effective', 'cycle_effective', 'wave_height_average', 'cycle_average'],
|
|
|
status: 'online',
|
|
|
color: '#00ffff'
|
|
|
},
|
|
|
{
|
|
|
id: 'depth_sounder_001',
|
|
|
name: '6000米测深仪',
|
|
|
type: 'DEPTH_SOUNDER',
|
|
|
position: { lat: 34.5, lng: 119.5 },
|
|
|
parameters: ['data_time', 'lng_lat', 'depth_f', 'depth_m'],
|
|
|
status: 'online',
|
|
|
color: '#8800ff'
|
|
|
},
|
|
|
{
|
|
|
id: 'wave_radar_001',
|
|
|
name: '测波雷达',
|
|
|
type: 'WAVE_RADAR',
|
|
|
position: { lat: 36.5, lng: 122.1 },
|
|
|
parameters: ['data_time', 'lng_lat', 'wave_height_effective', 'cycle_tm2', 'direction_peak', 'cycle_peak', 'wavelength_peak', 'direction_swell_primary', 'cycle_swell_primary', 'wavelength_swell_primary', 'direction_wind_wave', 'cycle_wind_wave', 'wavelength_wind_wave', 'current_direction', 'current_velocity', 'encounter_current_direction', 'encounter_current_velocity'],
|
|
|
status: 'online',
|
|
|
color: '#ffff00'
|
|
|
},
|
|
|
{
|
|
|
id: 'multi_sensor_001',
|
|
|
name: '表层多要素自动测定系统',
|
|
|
type: 'MULTI_SENSOR',
|
|
|
position: { lat: 35.1, lng: 119.9 },
|
|
|
parameters: ['data_time', 'lng_lat', 'temp', 'cond', 'salinity', 'chlorophyll', 'cdom', 'turbidity'],
|
|
|
status: 'online',
|
|
|
color: '#00ff00'
|
|
|
},
|
|
|
{
|
|
|
id: 'wave_buoy_001',
|
|
|
name: '小型波浪浮标',
|
|
|
type: 'WAVE_BUOY',
|
|
|
position: { lat: 34.9, lng: 120.7 },
|
|
|
parameters: ['time', 'lng_lat', 'wave_height_average', 'cycle_average', 'wave_height_max', 'cycle_max', 'wave_height_1_10', 'cycle_1_10', 'wave_height_effective'],
|
|
|
status: 'online',
|
|
|
color: '#ff8800'
|
|
|
},
|
|
|
{
|
|
|
id: 'beidou_001',
|
|
|
name: '北斗探测系统',
|
|
|
type: 'BEIDOU',
|
|
|
position: { lat: 36.0, lng: 121.8 },
|
|
|
parameters: ['data_time', 'lng_lat', 'temp', 'relative_humidity', 'air_pressure', 'wind_speed', 'height', 'wind_direction'],
|
|
|
status: 'online',
|
|
|
color: '#ff4400'
|
|
|
},
|
|
|
{
|
|
|
id: 'wind_profiler_001',
|
|
|
name: '舰载风廓线雷达',
|
|
|
type: 'WIND_PROFILER',
|
|
|
position: { lat: 35.7, lng: 121.0 },
|
|
|
parameters: ['time', 'lng_lat', 'height', 'wind_direction', 'wind_speed', 'wind_speed_v', 'reliability_l', 'reliability_v', 'cn2'],
|
|
|
status: 'online',
|
|
|
color: '#4400ff'
|
|
|
},
|
|
|
{
|
|
|
id: 'weather_station_001',
|
|
|
name: '船载自动气象站',
|
|
|
type: 'WEATHER_STATION',
|
|
|
position: { lat: 34.7, lng: 119.3 },
|
|
|
parameters: ['data_time', 'lng_lat', 'wind_speed_realtime', 'wind_direction_realtime', 'wind_speed_average', 'wind_direction_average', 'wind_speed_max', 'wind_direction_max', 'time_wind_speed_max', 'temperature', 'humidity', 'pressure', 'rainfall_minute', 'rainfall_hour', 'rainfall_day', 'status_sensor'],
|
|
|
status: 'online',
|
|
|
color: '#88ff00'
|
|
|
},
|
|
|
{
|
|
|
id: 'sky_scanner_001',
|
|
|
name: '全天空背景扫描仪',
|
|
|
type: 'SKY_SCANNER',
|
|
|
position: { lat: 36.2, lng: 120.3 },
|
|
|
parameters: ['data_time', 'lng_lat', 'irradiance', 'radiance'],
|
|
|
status: 'online',
|
|
|
color: '#ff0088'
|
|
|
},
|
|
|
{
|
|
|
id: 'turbulence_001',
|
|
|
name: '大气湍流谱仪',
|
|
|
type: 'TURBULENCE',
|
|
|
position: { lat: 35.4, lng: 121.7 },
|
|
|
parameters: ['data_time', 'lng_lat', 'temp', 'air_pressure', 'cn2'],
|
|
|
status: 'online',
|
|
|
color: '#0044ff'
|
|
|
},
|
|
|
{
|
|
|
id: 'lidar_wind_001',
|
|
|
name: '激光测风雷达',
|
|
|
type: 'LIDAR_WIND',
|
|
|
position: { lat: 34.6, lng: 120.1 },
|
|
|
parameters: ['data_time', 'lng_lat', 'height', 'wind_speed_l', 'wind_direction_l', 'wind_speed_v'],
|
|
|
status: 'online',
|
|
|
color: '#ff6600'
|
|
|
},
|
|
|
{
|
|
|
id: 'visibility_001',
|
|
|
name: '天气现象及能见度仪',
|
|
|
type: 'VISIBILITY',
|
|
|
position: { lat: 35.9, lng: 119.6 },
|
|
|
parameters: ['data_time', 'lng_lat', 'height', 'visibility', 'azimuth_angle', 'elevation'],
|
|
|
status: 'online',
|
|
|
color: '#66ff00'
|
|
|
},
|
|
|
{
|
|
|
id: 'cloud_height_001',
|
|
|
name: '激光云高仪',
|
|
|
type: 'CLOUD_HEIGHT',
|
|
|
position: { lat: 36.4, lng: 120.9 },
|
|
|
parameters: ['data_time', 'lng_lat', 'cloud_height_1', 'cloud_height_2', 'cloud_height_3', 'cloud_height_4', 'cloud_height_5', 'cloud_thickness_1', 'cloud_thickness_2', 'cloud_thickness_3', 'cloud_thickness_4', 'cloud_thickness_5'],
|
|
|
status: 'online',
|
|
|
color: '#00aaff'
|
|
|
},
|
|
|
{
|
|
|
id: 'navigation_001',
|
|
|
name: '船舶航行数据',
|
|
|
type: 'NAVIGATION',
|
|
|
position: { lat: 35.3, lng: 121.3 },
|
|
|
parameters: ['POS惯导', 'GNSS', '计程仪', '电罗经', '测深仪'],
|
|
|
status: 'online',
|
|
|
color: '#aa00ff'
|
|
|
}
|
|
|
];
|
|
|
|
|
|
function initDataReception() {
|
|
|
const earthContainer = document.getElementById('earth-container');
|
|
|
if (!earthContainer) return;
|
|
|
|
|
|
// 清空容器
|
|
|
earthContainer.innerHTML = '';
|
|
|
|
|
|
// 创建地球
|
|
|
const earth = document.createElement('div');
|
|
|
earth.className = 'earth';
|
|
|
earthContainer.appendChild(earth);
|
|
|
|
|
|
// 添加真实设备数据源点
|
|
|
oceanDevices.forEach((device, index) => {
|
|
|
const point = document.createElement('div');
|
|
|
point.className = 'data-source-point';
|
|
|
point.style.backgroundColor = device.color;
|
|
|
point.style.boxShadow = `0 0 20px ${device.color}`;
|
|
|
|
|
|
// 根据经纬度计算位置(简化的球面投影)
|
|
|
const x = 50 + (device.position.lng - 120) * 8; // 相对于120度经线
|
|
|
const y = 50 - (device.position.lat - 35) * 8; // 相对于35度纬线
|
|
|
|
|
|
point.style.left = `${Math.max(10, Math.min(90, x))}%`;
|
|
|
point.style.top = `${Math.max(10, Math.min(90, y))}%`;
|
|
|
|
|
|
point.setAttribute('data-device-id', device.id);
|
|
|
point.setAttribute('data-device-name', device.name);
|
|
|
point.setAttribute('data-device-type', device.type);
|
|
|
|
|
|
// 添加点击事件
|
|
|
point.addEventListener('click', () => showDeviceDetails(device));
|
|
|
|
|
|
earthContainer.appendChild(point);
|
|
|
|
|
|
// 创建数据流线
|
|
|
const streamLine = document.createElement('div');
|
|
|
streamLine.className = 'data-stream-line';
|
|
|
streamLine.style.background = `linear-gradient(45deg, transparent, ${device.color}, transparent)`;
|
|
|
streamLine.style.left = point.style.left;
|
|
|
streamLine.style.top = point.style.top;
|
|
|
streamLine.style.animationDelay = `${index * 0.2}s`;
|
|
|
|
|
|
earthContainer.appendChild(streamLine);
|
|
|
});
|
|
|
|
|
|
// 更新统计信息
|
|
|
updateReceptionStats();
|
|
|
}
|
|
|
|
|
|
function updateReceptionStats() {
|
|
|
const totalDevices = oceanDevices.length;
|
|
|
const onlineDevices = oceanDevices.filter(d => d.status === 'online').length;
|
|
|
const totalParams = oceanDevices.reduce((sum, device) => sum + device.parameters.length, 0);
|
|
|
|
|
|
// 更新统计显示
|
|
|
const statsElements = {
|
|
|
'total-received': `${(Math.random() * 50000 + 150000).toFixed(0)}`,
|
|
|
'reception-rate': `${(Math.random() * 200 + 800).toFixed(0)} 包/秒`,
|
|
|
'online-sources': `${onlineDevices}/${totalDevices}`,
|
|
|
'data-types': `${totalParams} 种参数`
|
|
|
};
|
|
|
|
|
|
Object.entries(statsElements).forEach(([id, value]) => {
|
|
|
const element = document.getElementById(id);
|
|
|
if (element) element.textContent = value;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 底部数据流面板管理
|
|
|
class BottomDataStreamManager {
|
|
|
constructor() {
|
|
|
this.isCollapsed = false;
|
|
|
this.dataStreamManager = new DataStreamManager();
|
|
|
this.init();
|
|
|
}
|
|
|
|
|
|
init() {
|
|
|
this.setupEventListeners();
|
|
|
this.startDataStream();
|
|
|
}
|
|
|
|
|
|
setupEventListeners() {
|
|
|
const streamHeader = document.querySelector('.stream-header');
|
|
|
const streamToggle = document.querySelector('.stream-toggle');
|
|
|
|
|
|
if (streamHeader) {
|
|
|
streamHeader.addEventListener('click', () => this.togglePanel());
|
|
|
}
|
|
|
|
|
|
if (streamToggle) {
|
|
|
streamToggle.addEventListener('click', (e) => {
|
|
|
e.stopPropagation();
|
|
|
this.togglePanel();
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
togglePanel() {
|
|
|
const panel = document.querySelector('.bottom-data-stream');
|
|
|
if (panel) {
|
|
|
this.isCollapsed = !this.isCollapsed;
|
|
|
panel.classList.toggle('collapsed', this.isCollapsed);
|
|
|
|
|
|
// 更新按钮文本
|
|
|
const toggleText = document.querySelector('.stream-toggle span');
|
|
|
if (toggleText) {
|
|
|
toggleText.textContent = this.isCollapsed ? '展开' : '收起';
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
startDataStream() {
|
|
|
// 启动数据流管理器
|
|
|
this.dataStreamManager.start();
|
|
|
|
|
|
// 每1秒添加新数据 - 加快生成速度
|
|
|
setInterval(() => {
|
|
|
this.addNewDataItem();
|
|
|
}, 1000);
|
|
|
}
|
|
|
|
|
|
addNewDataItem() {
|
|
|
const container = document.querySelector('.data-stream.horizontal');
|
|
|
if (!container) return;
|
|
|
|
|
|
const newItem = this.dataStreamManager.generateDataItem();
|
|
|
const itemElement = this.createStreamItemElement(newItem);
|
|
|
|
|
|
// 为新卡片添加特殊类名
|
|
|
itemElement.classList.add('new-item');
|
|
|
|
|
|
// 为现有卡片添加推动效果
|
|
|
const existingItems = container.querySelectorAll('.stream-item');
|
|
|
existingItems.forEach((item, index) => {
|
|
|
if (index < 5) { // 对前5个卡片添加推动效果
|
|
|
// 先移除可能存在的重置类
|
|
|
item.classList.remove('reset-position');
|
|
|
// 添加推动效果
|
|
|
setTimeout(() => {
|
|
|
item.classList.add('push-right');
|
|
|
}, 50); // 小延迟确保DOM更新
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 添加到容器开头
|
|
|
container.insertBefore(itemElement, container.firstChild);
|
|
|
|
|
|
// 延迟重置卡片位置,创建流动效果
|
|
|
setTimeout(() => {
|
|
|
const allItems = container.querySelectorAll('.stream-item');
|
|
|
allItems.forEach((item, index) => {
|
|
|
if (item.classList.contains('push-right')) {
|
|
|
item.classList.remove('push-right');
|
|
|
item.classList.add('reset-position');
|
|
|
}
|
|
|
});
|
|
|
}, 100);
|
|
|
|
|
|
// 限制显示的数据项数量,为超出的卡片添加淡出动画
|
|
|
setTimeout(() => {
|
|
|
const items = container.querySelectorAll('.stream-item');
|
|
|
if (items.length > 20) {
|
|
|
const itemToRemove = items[items.length - 1];
|
|
|
itemToRemove.classList.add('fade-out');
|
|
|
|
|
|
// 动画完成后移除元素
|
|
|
setTimeout(() => {
|
|
|
if (itemToRemove.parentNode) {
|
|
|
itemToRemove.remove();
|
|
|
}
|
|
|
}, 500);
|
|
|
}
|
|
|
}, 200);
|
|
|
|
|
|
// 清理类名
|
|
|
setTimeout(() => {
|
|
|
itemElement.classList.remove('new-item');
|
|
|
const allItems = container.querySelectorAll('.stream-item');
|
|
|
allItems.forEach(item => {
|
|
|
item.classList.remove('reset-position');
|
|
|
});
|
|
|
}, 800);
|
|
|
|
|
|
// 自动滚动到最新数据
|
|
|
container.scrollLeft = 0;
|
|
|
}
|
|
|
|
|
|
createStreamItemElement(data) {
|
|
|
const item = document.createElement('div');
|
|
|
item.className = 'stream-item';
|
|
|
|
|
|
// 设备类型颜色
|
|
|
const deviceColors = {
|
|
|
'temperature': { bg: 'rgba(239, 68, 68, 0.2)', border: '#ef4444', icon: '🌡️' },
|
|
|
'pressure': { bg: 'rgba(59, 130, 246, 0.2)', border: '#3b82f6', icon: '📊' },
|
|
|
'salinity': { bg: 'rgba(34, 197, 94, 0.2)', border: '#22c55e', icon: '🧂' },
|
|
|
'ph': { bg: 'rgba(168, 85, 247, 0.2)', border: '#a855f7', icon: '⚗️' },
|
|
|
'oxygen': { bg: 'rgba(6, 182, 212, 0.2)', border: '#06b6d4', icon: '💨' },
|
|
|
'turbidity': { bg: 'rgba(245, 158, 11, 0.2)', border: '#f59e0b', icon: '🌊' }
|
|
|
};
|
|
|
|
|
|
const colorInfo = deviceColors[data.type] || deviceColors['temperature'];
|
|
|
|
|
|
item.innerHTML = `
|
|
|
<div class="stream-device-info">
|
|
|
<div class="stream-device-icon" style="background: ${colorInfo.bg}; border-color: ${colorInfo.border}; color: ${colorInfo.border};">
|
|
|
${colorInfo.icon}
|
|
|
</div>
|
|
|
<div class="stream-device-details">
|
|
|
<div class="stream-device-name">${data.deviceName}</div>
|
|
|
<div class="stream-parameter">${data.parameter}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stream-value-section">
|
|
|
<div class="stream-value" style="color: ${colorInfo.border};">${data.value}</div>
|
|
|
<div class="stream-timestamp">${data.timestamp}</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
return item;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 初始化底部数据流面板
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
// 检查是否存在底部数据流面板
|
|
|
if (document.querySelector('.bottom-data-stream')) {
|
|
|
new BottomDataStreamManager();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
function showDeviceDetails(device) {
|
|
|
// 创建设备详情弹窗
|
|
|
const modal = document.createElement('div');
|
|
|
modal.className = 'device-modal';
|
|
|
modal.innerHTML = `
|
|
|
<div class="device-modal-content">
|
|
|
<div class="device-modal-header">
|
|
|
<h3>${device.name}</h3>
|
|
|
<span class="device-modal-close">×</span>
|
|
|
</div>
|
|
|
<div class="device-modal-body">
|
|
|
<div class="device-info">
|
|
|
<p><strong>设备ID:</strong> ${device.id}</p>
|
|
|
<p><strong>设备类型:</strong> ${device.type}</p>
|
|
|
<p><strong>位置:</strong> ${device.position.lat}°N, ${device.position.lng}°E</p>
|
|
|
<p><strong>状态:</strong> <span class="status-${device.status}">${device.status === 'online' ? '在线' : '离线'}</span></p>
|
|
|
</div>
|
|
|
<div class="device-parameters">
|
|
|
<h4>测量参数:</h4>
|
|
|
<div class="parameters-grid">
|
|
|
${device.parameters.map(param => `<span class="parameter-tag">${param}</span>`).join('')}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="device-stats">
|
|
|
<h4>实时数据:</h4>
|
|
|
<div class="stats-grid">
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-label">数据包数:</span>
|
|
|
<span class="stat-value">${Math.floor(Math.random() * 1000 + 500)}</span>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-label">传输速率:</span>
|
|
|
<span class="stat-value">${Math.floor(Math.random() * 50 + 20)} KB/s</span>
|
|
|
</div>
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-label">数据质量:</span>
|
|
|
<span class="stat-value">${(Math.random() * 10 + 90).toFixed(1)}%</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
// 关闭弹窗事件
|
|
|
const closeBtn = modal.querySelector('.device-modal-close');
|
|
|
closeBtn.addEventListener('click', () => {
|
|
|
document.body.removeChild(modal);
|
|
|
});
|
|
|
|
|
|
modal.addEventListener('click', (e) => {
|
|
|
if (e.target === modal) {
|
|
|
document.body.removeChild(modal);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 更新数据流显示函数
|
|
|
function updateDataStream() {
|
|
|
const streamContainer = document.querySelector('.data-stream');
|
|
|
if (!streamContainer) return;
|
|
|
|
|
|
// 随机选择一个设备
|
|
|
const device = oceanDevices[Math.floor(Math.random() * oceanDevices.length)];
|
|
|
const param = device.parameters[Math.floor(Math.random() * device.parameters.length)];
|
|
|
const value = generateRandomValue(param);
|
|
|
|
|
|
const now = new Date();
|
|
|
const timestamp = now.toLocaleTimeString('zh-CN', {
|
|
|
hour12: false,
|
|
|
hour: '2-digit',
|
|
|
minute: '2-digit',
|
|
|
second: '2-digit'
|
|
|
});
|
|
|
|
|
|
// 获取参数的中文名称
|
|
|
const paramNames = {
|
|
|
'time': '时间',
|
|
|
'lng_lat': '经纬度',
|
|
|
'temp': '温度',
|
|
|
'cond': '电导率',
|
|
|
'depth': '深度',
|
|
|
'salinity': '盐度',
|
|
|
'flow_velocity': '流速',
|
|
|
'flow_direction': '流向',
|
|
|
'gravity': '重力',
|
|
|
'magnetic_field_1': '磁场强度1',
|
|
|
'magnetic_field_2': '磁场强度2',
|
|
|
'wave_height_max': '最大波高',
|
|
|
'wave_height_effective': '有效波高',
|
|
|
'cycle_max': '最大周期',
|
|
|
'wind_speed': '风速',
|
|
|
'wind_direction': '风向',
|
|
|
'air_pressure': '气压',
|
|
|
'humidity': '湿度',
|
|
|
'visibility': '能见度',
|
|
|
'cloud_height_1': '云高1',
|
|
|
'irradiance': '辐照度',
|
|
|
'turbidity': '浊度',
|
|
|
'chlorophyll': '叶绿素'
|
|
|
};
|
|
|
|
|
|
// 创建数据流项目
|
|
|
const streamItem = document.createElement('div');
|
|
|
streamItem.className = `stream-item ${getDeviceTypeClass(device.type)}`;
|
|
|
|
|
|
streamItem.innerHTML = `
|
|
|
<div class="stream-device-info">
|
|
|
<div class="stream-device-icon">${deviceTypeIcons[device.type] || '📊'}</div>
|
|
|
<div class="stream-device-details">
|
|
|
<div class="stream-device-name">${device.name}</div>
|
|
|
<div class="stream-parameter">${paramNames[param] || param}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stream-value-section">
|
|
|
<div class="stream-value" style="color: ${device.color}">${value}</div>
|
|
|
<div class="stream-timestamp">${timestamp}</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
// 添加点击事件
|
|
|
streamItem.addEventListener('click', () => {
|
|
|
showStreamItemDetails(device, param, value, timestamp);
|
|
|
});
|
|
|
|
|
|
// 添加到流中
|
|
|
streamContainer.insertBefore(streamItem, streamContainer.firstChild);
|
|
|
|
|
|
// 限制显示的项目数量
|
|
|
const items = streamContainer.querySelectorAll('.stream-item');
|
|
|
if (items.length > 12) {
|
|
|
const lastItem = items[items.length - 1];
|
|
|
lastItem.classList.add('removing');
|
|
|
setTimeout(() => {
|
|
|
if (lastItem.parentNode) {
|
|
|
lastItem.remove();
|
|
|
}
|
|
|
}, 300);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 显示数据流项目详细信息
|
|
|
function showStreamItemDetails(device, param, value, timestamp) {
|
|
|
const modal = document.createElement('div');
|
|
|
modal.className = 'stream-detail-modal';
|
|
|
modal.style.cssText = `
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
z-index: 1000;
|
|
|
backdrop-filter: blur(5px);
|
|
|
`;
|
|
|
|
|
|
const content = document.createElement('div');
|
|
|
content.style.cssText = `
|
|
|
background: linear-gradient(135deg, #1e3a8a 0%, #0f172a 100%);
|
|
|
border-radius: 16px;
|
|
|
padding: 24px;
|
|
|
max-width: 400px;
|
|
|
width: 90%;
|
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
|
|
|
`;
|
|
|
|
|
|
content.innerHTML = `
|
|
|
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
|
|
<div style="font-size: 24px;">${deviceTypeIcons[device.type] || '📊'}</div>
|
|
|
<div>
|
|
|
<h3 style="margin: 0; color: white; font-size: 1.2rem;">${device.name}</h3>
|
|
|
<p style="margin: 4px 0 0 0; color: #94a3b8; font-size: 0.9rem;">设备ID: ${device.id}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="background: rgba(255, 255, 255, 0.05); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
|
<span style="color: #94a3b8;">参数</span>
|
|
|
<span style="color: white; font-weight: 600;">${param}</span>
|
|
|
</div>
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
|
<span style="color: #94a3b8;">数值</span>
|
|
|
<span style="color: ${device.color}; font-weight: 700; font-family: 'Courier New', monospace;">${value}</span>
|
|
|
</div>
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
<span style="color: #94a3b8;">时间</span>
|
|
|
<span style="color: white; font-family: 'Courier New', monospace;">${timestamp}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
|
|
<span style="color: #94a3b8;">位置</span>
|
|
|
<span style="color: white; font-family: 'Courier New', monospace;">${device.position.lat.toFixed(2)}°N, ${device.position.lng.toFixed(2)}°E</span>
|
|
|
</div>
|
|
|
<button onclick="this.closest('.stream-detail-modal').remove()"
|
|
|
style="width: 100%; padding: 12px; background: ${device.color}; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: all 0.3s ease;">
|
|
|
关闭
|
|
|
</button>
|
|
|
`;
|
|
|
|
|
|
modal.appendChild(content);
|
|
|
document.body.appendChild(modal);
|
|
|
|
|
|
// 点击背景关闭
|
|
|
modal.addEventListener('click', (e) => {
|
|
|
if (e.target === modal) {
|
|
|
modal.remove();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 更新数据源点渲染函数
|
|
|
function renderDataSourcePoints() {
|
|
|
const earthSphere = document.querySelector('.earth-sphere');
|
|
|
if (!earthSphere) return;
|
|
|
|
|
|
// 清除现有的数据点
|
|
|
earthSphere.querySelectorAll('.data-source-point').forEach(point => point.remove());
|
|
|
earthSphere.querySelectorAll('.data-stream-line').forEach(line => line.remove());
|
|
|
|
|
|
oceanDevices.forEach((device, index) => {
|
|
|
// 创建数据源点
|
|
|
const point = document.createElement('div');
|
|
|
point.className = 'data-source-point';
|
|
|
point.style.backgroundColor = deviceTypeColors[device.type] || '#00d4ff';
|
|
|
point.style.color = deviceTypeColors[device.type] || '#00d4ff';
|
|
|
|
|
|
// 根据经纬度计算位置(简化的球面投影)
|
|
|
const x = ((device.longitude + 180) / 360) * 100;
|
|
|
const y = ((90 - device.latitude) / 180) * 100;
|
|
|
|
|
|
point.style.left = `${x}%`;
|
|
|
point.style.top = `${y}%`;
|
|
|
|
|
|
// 添加点击事件
|
|
|
point.addEventListener('click', () => showDeviceDetails(device));
|
|
|
|
|
|
// 添加悬停提示
|
|
|
point.title = `${device.name} (${device.type})`;
|
|
|
|
|
|
earthSphere.appendChild(point);
|
|
|
|
|
|
// 创建数据流线
|
|
|
if (Math.random() < 0.3) { // 30%的概率显示数据流
|
|
|
const streamLine = document.createElement('div');
|
|
|
streamLine.className = 'data-stream-line';
|
|
|
streamLine.style.background = `linear-gradient(to top, ${deviceTypeColors[device.type] || '#00d4ff'}, transparent)`;
|
|
|
streamLine.style.left = `${x}%`;
|
|
|
streamLine.style.top = `${y}%`;
|
|
|
streamLine.style.animationDelay = `${Math.random() * 3}s`;
|
|
|
|
|
|
earthSphere.appendChild(streamLine);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 更新设备详情显示函数
|
|
|
function showDeviceDetails(device) {
|
|
|
// 创建模态框
|
|
|
const modal = document.createElement('div');
|
|
|
modal.className = 'device-modal';
|
|
|
|
|
|
modal.innerHTML = `
|
|
|
<div class="device-modal-content">
|
|
|
<div class="device-modal-header">
|
|
|
<h3>${deviceTypeIcons[device.type] || '📊'} ${device.name}</h3>
|
|
|
<span class="device-modal-close">×</span>
|
|
|
</div>
|
|
|
<div class="device-info">
|
|
|
<p><strong>设备类型:</strong> ${device.type}</p>
|
|
|
<p><strong>位置:</strong> ${device.latitude.toFixed(4)}°N, ${device.longitude.toFixed(4)}°E</p>
|
|
|
<p><strong>状态:</strong> <span class="${device.status === 'online' ? 'status-online' : 'status-offline'}">${device.status === 'online' ? '在线' : '离线'}</span></p>
|
|
|
<p><strong>设备ID:</strong> ${device.id}</p>
|
|
|
</div>
|
|
|
<div class="device-parameters">
|
|
|
<h4>监测参数</h4>
|
|
|
<div class="parameters-grid">
|
|
|
${device.parameters.map(param => `<span class="parameter-tag">${param}</span>`).join('')}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="device-stats">
|
|
|
<h4>实时数据</h4>
|
|
|
<div class="stats-grid">
|
|
|
${device.parameters.slice(0, 6).map(param => `
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-label">${param}</span>
|
|
|
<span class="stat-value">${generateRandomValue(param)}</span>
|
|
|
</div>
|
|
|
`).join('')}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
// 添加关闭事件
|
|
|
modal.querySelector('.device-modal-close').addEventListener('click', () => {
|
|
|
modal.remove();
|
|
|
});
|
|
|
|
|
|
modal.addEventListener('click', (e) => {
|
|
|
if (e.target === modal) {
|
|
|
modal.remove();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
document.body.appendChild(modal);
|
|
|
}
|
|
|
|
|
|
function generateRandomValue(param) {
|
|
|
const valueMap = {
|
|
|
'temp': `${(Math.random() * 30 + 5).toFixed(2)}°C`,
|
|
|
'temperature': `${(Math.random() * 30 + 5).toFixed(2)}°C`,
|
|
|
'depth': `${(Math.random() * 6000).toFixed(1)}m`,
|
|
|
'wind_speed': `${(Math.random() * 20 + 2).toFixed(1)}m/s`,
|
|
|
'wave_height_max': `${(Math.random() * 8 + 1).toFixed(2)}m`,
|
|
|
'pressure': `${(Math.random() * 50 + 1000).toFixed(1)}hPa`,
|
|
|
'humidity': `${(Math.random() * 40 + 40).toFixed(1)}%`,
|
|
|
'salinity': `${(Math.random() * 5 + 30).toFixed(2)}‰`,
|
|
|
'flow_velocity': `${(Math.random() * 3).toFixed(2)}m/s`,
|
|
|
'gravity': `${(Math.random() * 0.1 + 9.8).toFixed(4)}m/s²`,
|
|
|
'visibility': `${(Math.random() * 20 + 5).toFixed(1)}km`,
|
|
|
'irradiance': `${(Math.random() * 1000 + 200).toFixed(1)}W/m²`
|
|
|
};
|
|
|
|
|
|
// 检查参数名是否包含特定关键词
|
|
|
for (const [key, value] of Object.entries(valueMap)) {
|
|
|
if (param.includes(key)) {
|
|
|
return value;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 默认返回数值
|
|
|
return `${(Math.random() * 100).toFixed(2)}`;
|
|
|
}
|
|
|
|
|
|
// 数据处理模块
|
|
|
function initDataProcessing() {
|
|
|
const pipelineContainer = document.querySelector('.pipeline-container');
|
|
|
if (!pipelineContainer) return;
|
|
|
|
|
|
// 清空现有内容
|
|
|
pipelineContainer.innerHTML = '';
|
|
|
|
|
|
// 创建处理流水线
|
|
|
const pipelineFlow = document.createElement('div');
|
|
|
pipelineFlow.className = 'pipeline-flow';
|
|
|
|
|
|
// 定义处理节点(基于真实海洋数据处理流程)
|
|
|
const processingNodes = [
|
|
|
{
|
|
|
id: 'data-ingestion',
|
|
|
type: 'input-node',
|
|
|
icon: 'fas fa-download',
|
|
|
title: '数据接收',
|
|
|
description: '接收来自19种海洋观测设备的原始数据'
|
|
|
},
|
|
|
{
|
|
|
id: 'data-validation',
|
|
|
type: 'process-node',
|
|
|
icon: 'fas fa-check-circle',
|
|
|
title: '数据验证',
|
|
|
description: '验证CTD、ADCP、重力仪等设备数据的完整性和准确性'
|
|
|
},
|
|
|
{
|
|
|
id: 'quality-control',
|
|
|
type: 'process-node',
|
|
|
icon: 'fas fa-filter',
|
|
|
title: '质量控制',
|
|
|
description: '对温度、盐度、流速等参数进行质量检查和异常值过滤'
|
|
|
},
|
|
|
{
|
|
|
id: 'data-calibration',
|
|
|
type: 'compute-node',
|
|
|
icon: 'fas fa-cogs',
|
|
|
title: '数据校准',
|
|
|
description: '校准传感器漂移,统一时间基准和坐标系统'
|
|
|
},
|
|
|
{
|
|
|
id: 'oceanographic-analysis',
|
|
|
type: 'compute-node',
|
|
|
icon: 'fas fa-chart-line',
|
|
|
title: '海洋学分析',
|
|
|
description: '计算海水密度、声速、地转流等海洋学参数'
|
|
|
},
|
|
|
{
|
|
|
id: 'data-fusion',
|
|
|
type: 'compute-node',
|
|
|
icon: 'fas fa-layer-group',
|
|
|
title: '数据融合',
|
|
|
description: '融合多源观测数据,生成综合海洋环境场'
|
|
|
},
|
|
|
{
|
|
|
id: 'data-output',
|
|
|
type: 'output-node',
|
|
|
icon: 'fas fa-upload',
|
|
|
title: '数据输出',
|
|
|
description: '输出标准格式的海洋数据产品和可视化结果'
|
|
|
}
|
|
|
];
|
|
|
|
|
|
// 创建节点元素
|
|
|
processingNodes.forEach((node, index) => {
|
|
|
const nodeElement = document.createElement('div');
|
|
|
nodeElement.className = `pipeline-node ${node.type}`;
|
|
|
nodeElement.id = node.id;
|
|
|
nodeElement.innerHTML = `
|
|
|
<i class="${node.icon}"></i>
|
|
|
<span>${node.title}</span>
|
|
|
<div class="node-pulse"></div>
|
|
|
`;
|
|
|
|
|
|
// 添加点击事件
|
|
|
nodeElement.addEventListener('click', () => showNodeDetails(node));
|
|
|
|
|
|
pipelineFlow.appendChild(nodeElement);
|
|
|
|
|
|
// 添加连接线(除了最后一个节点)
|
|
|
if (index < processingNodes.length - 1) {
|
|
|
const connection = document.createElement('div');
|
|
|
connection.className = 'pipeline-connection';
|
|
|
connection.style.left = `${(index + 1) * 150 - 75}px`;
|
|
|
connection.style.width = '150px';
|
|
|
pipelineFlow.appendChild(connection);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
pipelineContainer.appendChild(pipelineFlow);
|
|
|
|
|
|
// 更新处理统计
|
|
|
updateProcessingStats();
|
|
|
|
|
|
// 启动处理监控
|
|
|
startProcessingMonitor();
|
|
|
}
|
|
|
|
|
|
// 显示节点详情
|
|
|
function showNodeDetails(node) {
|
|
|
const detailsPanel = document.querySelector('.node-details');
|
|
|
if (!detailsPanel) return;
|
|
|
|
|
|
// 根据节点类型显示不同的详细信息
|
|
|
let detailsContent = '';
|
|
|
|
|
|
switch(node.id) {
|
|
|
case 'data-ingestion':
|
|
|
detailsContent = `
|
|
|
<h4>数据接收节点</h4>
|
|
|
<p>${node.description}</p>
|
|
|
<ul>
|
|
|
<li>CTD数据:温度、盐度、深度</li>
|
|
|
<li>ADCP数据:流速、流向</li>
|
|
|
<li>重力仪:重力异常</li>
|
|
|
<li>磁力仪:磁场强度</li>
|
|
|
<li>气象数据:风速、气压、湿度</li>
|
|
|
<li>波浪数据:波高、周期、方向</li>
|
|
|
</ul>
|
|
|
<p><strong>处理速度:</strong> 1000条/秒</p>
|
|
|
<p><strong>数据格式:</strong> NetCDF, ASCII, Binary</p>
|
|
|
`;
|
|
|
break;
|
|
|
case 'data-validation':
|
|
|
detailsContent = `
|
|
|
<h4>数据验证节点</h4>
|
|
|
<p>${node.description}</p>
|
|
|
<ul>
|
|
|
<li>时间戳连续性检查</li>
|
|
|
<li>地理坐标合理性验证</li>
|
|
|
<li>物理参数范围检查</li>
|
|
|
<li>数据完整性验证</li>
|
|
|
</ul>
|
|
|
<p><strong>验证规则:</strong> 127项</p>
|
|
|
<p><strong>通过率:</strong> 98.5%</p>
|
|
|
`;
|
|
|
break;
|
|
|
case 'quality-control':
|
|
|
detailsContent = `
|
|
|
<h4>质量控制节点</h4>
|
|
|
<p>${node.description}</p>
|
|
|
<ul>
|
|
|
<li>统计异常值检测</li>
|
|
|
<li>物理一致性检查</li>
|
|
|
<li>时间序列平滑</li>
|
|
|
<li>空间插值质量评估</li>
|
|
|
</ul>
|
|
|
<p><strong>QC标准:</strong> IOC/UNESCO</p>
|
|
|
<p><strong>处理算法:</strong> 3σ准则, Hampel滤波</p>
|
|
|
`;
|
|
|
break;
|
|
|
case 'data-calibration':
|
|
|
detailsContent = `
|
|
|
<h4>数据校准节点</h4>
|
|
|
<p>${node.description}</p>
|
|
|
<ul>
|
|
|
<li>传感器漂移校正</li>
|
|
|
<li>时间同步校准</li>
|
|
|
<li>坐标系统转换</li>
|
|
|
<li>单位标准化</li>
|
|
|
</ul>
|
|
|
<p><strong>校准精度:</strong> ±0.001°C (温度)</p>
|
|
|
<p><strong>时间精度:</strong> ±1ms</p>
|
|
|
`;
|
|
|
break;
|
|
|
case 'oceanographic-analysis':
|
|
|
detailsContent = `
|
|
|
<h4>海洋学分析节点</h4>
|
|
|
<p>${node.description}</p>
|
|
|
<ul>
|
|
|
<li>海水状态方程计算</li>
|
|
|
<li>地转流计算</li>
|
|
|
<li>混合层深度估算</li>
|
|
|
<li>海洋锋面识别</li>
|
|
|
</ul>
|
|
|
<p><strong>计算模型:</strong> TEOS-10</p>
|
|
|
<p><strong>分析算法:</strong> 动力高度法, 梯度检测</p>
|
|
|
`;
|
|
|
break;
|
|
|
case 'data-fusion':
|
|
|
detailsContent = `
|
|
|
<h4>数据融合节点</h4>
|
|
|
<p>${node.description}</p>
|
|
|
<ul>
|
|
|
<li>多源数据时空配准</li>
|
|
|
<li>最优插值算法</li>
|
|
|
<li>卡尔曼滤波融合</li>
|
|
|
<li>不确定性评估</li>
|
|
|
</ul>
|
|
|
<p><strong>融合方法:</strong> 变分同化</p>
|
|
|
<p><strong>网格分辨率:</strong> 0.1° × 0.1°</p>
|
|
|
`;
|
|
|
break;
|
|
|
case 'data-output':
|
|
|
detailsContent = `
|
|
|
<h4>数据输出节点</h4>
|
|
|
<p>${node.description}</p>
|
|
|
<ul>
|
|
|
<li>标准格式转换</li>
|
|
|
<li>元数据生成</li>
|
|
|
<li>可视化产品制作</li>
|
|
|
<li>质量报告生成</li>
|
|
|
</ul>
|
|
|
<p><strong>输出格式:</strong> NetCDF-4, HDF5, GeoTIFF</p>
|
|
|
<p><strong>产品类型:</strong> L1, L2, L3级数据产品</p>
|
|
|
`;
|
|
|
break;
|
|
|
default:
|
|
|
detailsContent = `<h4>${node.title}</h4><p>${node.description}</p>`;
|
|
|
}
|
|
|
|
|
|
detailsPanel.innerHTML = detailsContent;
|
|
|
}
|
|
|
|
|
|
// 更新处理统计
|
|
|
function updateProcessingStats() {
|
|
|
const stats = [
|
|
|
{ label: '处理任务', value: Math.floor(Math.random() * 50) + 150, id: 'processing-tasks' },
|
|
|
{ label: '完成任务', value: Math.floor(Math.random() * 30) + 120, id: 'completed-tasks' },
|
|
|
{ label: '等待队列', value: Math.floor(Math.random() * 20) + 10, id: 'pending-tasks' },
|
|
|
{ label: '错误任务', value: Math.floor(Math.random() * 5) + 2, id: 'error-tasks', error: true }
|
|
|
];
|
|
|
|
|
|
stats.forEach(stat => {
|
|
|
const element = document.getElementById(stat.id);
|
|
|
if (element) {
|
|
|
const numberElement = element.querySelector('.stat-number');
|
|
|
if (numberElement) {
|
|
|
numberElement.textContent = stat.value;
|
|
|
if (stat.error) {
|
|
|
element.classList.add('error');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 更新资源监控
|
|
|
updateResourceMonitor();
|
|
|
}
|
|
|
|
|
|
// 更新资源监控
|
|
|
function updateResourceMonitor() {
|
|
|
const resources = [
|
|
|
{ id: 'cpu-usage', value: Math.floor(Math.random() * 30) + 45 },
|
|
|
{ id: 'memory-usage', value: Math.floor(Math.random() * 20) + 60 },
|
|
|
{ id: 'storage-usage', value: Math.floor(Math.random() * 15) + 35 }
|
|
|
];
|
|
|
|
|
|
resources.forEach(resource => {
|
|
|
const element = document.getElementById(resource.id);
|
|
|
if (element) {
|
|
|
const progressRing = element.querySelector('.progress-ring');
|
|
|
const progressValue = element.querySelector('.progress-value');
|
|
|
|
|
|
if (progressRing && progressValue) {
|
|
|
// 更新环形进度条
|
|
|
const percentage = resource.value;
|
|
|
progressRing.style.background = `conic-gradient(var(--accent-blue) ${percentage * 3.6}deg, rgba(255, 255, 255, 0.1) ${percentage * 3.6}deg)`;
|
|
|
progressValue.textContent = `${percentage}%`;
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 启动处理监控
|
|
|
function startProcessingMonitor() {
|
|
|
// 模拟处理日志
|
|
|
const logContent = document.querySelector('.log-content');
|
|
|
if (!logContent) return;
|
|
|
|
|
|
const logMessages = [
|
|
|
{ type: 'info', message: '[INFO] 开始处理CTD数据批次 #2024-001' },
|
|
|
{ type: 'info', message: '[INFO] 数据验证完成,通过率: 98.7%' },
|
|
|
{ type: 'warning', message: '[WARN] ADCP设备 #03 数据延迟 5分钟' },
|
|
|
{ type: 'info', message: '[INFO] 质量控制完成,标记异常值 12个' },
|
|
|
{ type: 'info', message: '[INFO] 海洋学参数计算完成' },
|
|
|
{ type: 'error', message: '[ERROR] 重力仪数据校准失败,重试中...' },
|
|
|
{ type: 'info', message: '[INFO] 数据融合完成,生成L3级产品' },
|
|
|
{ type: 'info', message: '[INFO] 输出NetCDF文件: ocean_data_20240115.nc' }
|
|
|
];
|
|
|
|
|
|
let logIndex = 0;
|
|
|
const addLogEntry = () => {
|
|
|
if (logIndex < logMessages.length) {
|
|
|
const log = logMessages[logIndex];
|
|
|
const logEntry = document.createElement('div');
|
|
|
logEntry.className = `log-entry ${log.type}`;
|
|
|
logEntry.textContent = `${new Date().toLocaleTimeString()} ${log.message}`;
|
|
|
|
|
|
logContent.appendChild(logEntry);
|
|
|
logContent.scrollTop = logContent.scrollHeight;
|
|
|
|
|
|
logIndex++;
|
|
|
setTimeout(addLogEntry, Math.random() * 3000 + 2000);
|
|
|
} else {
|
|
|
// 重置日志
|
|
|
setTimeout(() => {
|
|
|
logContent.innerHTML = '';
|
|
|
logIndex = 0;
|
|
|
addLogEntry();
|
|
|
}, 5000);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
addLogEntry();
|
|
|
}
|
|
|
|
|
|
// 设备类型颜色映射
|
|
|
const deviceTypeColors = {
|
|
|
'CTD': '#00ff88',
|
|
|
'ADCP': '#0088ff',
|
|
|
'Gravity': '#ff6600',
|
|
|
'Magnetic': '#ff0066',
|
|
|
'Wave': '#00ffff',
|
|
|
'Depth': '#8800ff',
|
|
|
'Radar': '#ffff00',
|
|
|
'Multi': '#00ff00',
|
|
|
'Buoy': '#ff8800',
|
|
|
'Beidou': '#ff4400',
|
|
|
'Wind': '#4400ff',
|
|
|
'Weather': '#88ff00',
|
|
|
'Sky': '#ff0088',
|
|
|
'Turbulence': '#0044ff',
|
|
|
'Lidar': '#ff6600',
|
|
|
'Visibility': '#66ff00',
|
|
|
'Cloud': '#00aaff',
|
|
|
'Navigation': '#aa00ff'
|
|
|
};
|
|
|
|
|
|
// 设备类型图标映射
|
|
|
const deviceTypeIcons = {
|
|
|
'CTD': '🌊',
|
|
|
'ADCP': '🌀',
|
|
|
'Gravity': '⚖️',
|
|
|
'Magnetic': '🧲',
|
|
|
'Wave': '〰️',
|
|
|
'Depth': '📏',
|
|
|
'Radar': '📡',
|
|
|
'Multi': '🔬',
|
|
|
'Buoy': '🛟',
|
|
|
'Beidou': '🛰️',
|
|
|
'Wind': '💨',
|
|
|
'Weather': '🌤️',
|
|
|
'Sky': '🌌',
|
|
|
'Turbulence': '🌪️',
|
|
|
'Lidar': '🔦',
|
|
|
'Visibility': '👁️',
|
|
|
'Cloud': '☁️',
|
|
|
'Navigation': '🧭'
|
|
|
};
|
|
|
|
|
|
// 获取设备类型的CSS类名
|
|
|
function getDeviceTypeClass(deviceType) {
|
|
|
const typeMap = {
|
|
|
'CTD': 'ctd',
|
|
|
'ADCP': 'adcp',
|
|
|
'Gravity': 'gravity',
|
|
|
'Magnetic': 'magnetic',
|
|
|
'Wave': 'wave',
|
|
|
'Depth': 'depth',
|
|
|
'Radar': 'radar',
|
|
|
'Multi': 'multi',
|
|
|
'Buoy': 'buoy',
|
|
|
'Beidou': 'beidou',
|
|
|
'Wind': 'wind',
|
|
|
'Weather': 'weather',
|
|
|
'Sky': 'sky',
|
|
|
'Turbulence': 'turbulence',
|
|
|
'Lidar': 'lidar',
|
|
|
'Visibility': 'visibility',
|
|
|
'Cloud': 'cloud',
|
|
|
'Navigation': 'navigation'
|
|
|
};
|
|
|
return `device-${typeMap[deviceType] || 'default'}`;
|
|
|
}
|
|
|
|
|
|
// 更新数据流显示函数
|
|
|
function updateDataStream() {
|
|
|
const streamContainer = document.querySelector('.data-stream');
|
|
|
if (!streamContainer) return;
|
|
|
|
|
|
// 随机选择一个设备
|
|
|
const device = oceanDevices[Math.floor(Math.random() * oceanDevices.length)];
|
|
|
const param = device.parameters[Math.floor(Math.random() * device.parameters.length)];
|
|
|
const value = generateRandomValue(param);
|
|
|
|
|
|
const time = new Date().toLocaleTimeString();
|
|
|
|
|
|
// 创建数据流项目
|
|
|
const streamItem = document.createElement('div');
|
|
|
streamItem.className = `stream-item ${getDeviceTypeClass(device.type)}`;
|
|
|
|
|
|
streamItem.innerHTML = `
|
|
|
<div class="stream-time">${time}</div>
|
|
|
<div class="stream-device">
|
|
|
<span class="device-icon">${deviceTypeIcons[device.type] || '📊'}</span>
|
|
|
${device.name}
|
|
|
</div>
|
|
|
<div class="stream-param">${param}</div>
|
|
|
<div class="stream-value" style="color: ${deviceTypeColors[device.type] || '#00d4ff'}">${value}</div>
|
|
|
`;
|
|
|
|
|
|
// 添加到流中
|
|
|
streamContainer.insertBefore(streamItem, streamContainer.firstChild);
|
|
|
|
|
|
// 限制显示的项目数量
|
|
|
const items = streamContainer.querySelectorAll('.stream-item');
|
|
|
if (items.length > 10) {
|
|
|
items[items.length - 1].remove();
|
|
|
}
|
|
|
|
|
|
// 添加进入动画
|
|
|
streamItem.style.opacity = '0';
|
|
|
streamItem.style.transform = 'translateX(-20px)';
|
|
|
setTimeout(() => {
|
|
|
streamItem.style.transition = 'all 0.3s ease';
|
|
|
streamItem.style.opacity = '1';
|
|
|
streamItem.style.transform = 'translateX(0)';
|
|
|
}, 50);
|
|
|
}
|
|
|
|
|
|
// 更新数据源点渲染函数
|
|
|
function renderDataSourcePoints() {
|
|
|
const earthSphere = document.querySelector('.earth-sphere');
|
|
|
if (!earthSphere) return;
|
|
|
|
|
|
// 清除现有的数据点
|
|
|
earthSphere.querySelectorAll('.data-source-point').forEach(point => point.remove());
|
|
|
earthSphere.querySelectorAll('.data-stream-line').forEach(line => line.remove());
|
|
|
|
|
|
oceanDevices.forEach((device, index) => {
|
|
|
// 创建数据源点
|
|
|
const point = document.createElement('div');
|
|
|
point.className = 'data-source-point';
|
|
|
point.style.backgroundColor = deviceTypeColors[device.type] || '#00d4ff';
|
|
|
point.style.color = deviceTypeColors[device.type] || '#00d4ff';
|
|
|
|
|
|
// 根据经纬度计算位置(简化的球面投影)
|
|
|
const x = ((device.longitude + 180) / 360) * 100;
|
|
|
const y = ((90 - device.latitude) / 180) * 100;
|
|
|
|
|
|
point.style.left = `${x}%`;
|
|
|
point.style.top = `${y}%`;
|
|
|
|
|
|
// 添加点击事件
|
|
|
point.addEventListener('click', () => showDeviceDetails(device));
|
|
|
|
|
|
// 添加悬停提示
|
|
|
point.title = `${device.name} (${device.type})`;
|
|
|
|
|
|
earthSphere.appendChild(point);
|
|
|
|
|
|
// 创建数据流线
|
|
|
if (Math.random() < 0.3) { // 30%的概率显示数据流
|
|
|
const streamLine = document.createElement('div');
|
|
|
streamLine.className = 'data-stream-line';
|
|
|
streamLine.style.background = `linear-gradient(to top, ${deviceTypeColors[device.type] || '#00d4ff'}, transparent)`;
|
|
|
streamLine.style.left = `${x}%`;
|
|
|
streamLine.style.top = `${y}%`;
|
|
|
streamLine.style.animationDelay = `${Math.random() * 3}s`;
|
|
|
|
|
|
earthSphere.appendChild(streamLine);
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 更新设备详情显示函数
|
|
|
function showDeviceDetails(device) {
|
|
|
// 创建模态框
|
|
|
const modal = document.createElement('div');
|
|
|
modal.className = 'device-modal';
|
|
|
|
|
|
modal.innerHTML = `
|
|
|
<div class="device-modal-content">
|
|
|
<div class="device-modal-header">
|
|
|
<h3>${deviceTypeIcons[device.type] || '📊'} ${device.name}</h3>
|
|
|
<span class="device-modal-close">×</span>
|
|
|
</div>
|
|
|
<div class="device-info">
|
|
|
<p><strong>设备类型:</strong> ${device.type}</p>
|
|
|
<p><strong>位置:</strong> ${device.latitude.toFixed(4)}°N, ${device.longitude.toFixed(4)}°E</p>
|
|
|
<p><strong>状态:</strong> <span class="${device.status === 'online' ? 'status-online' : 'status-offline'}">${device.status === 'online' ? '在线' : '离线'}</span></p>
|
|
|
<p><strong>设备ID:</strong> ${device.id}</p>
|
|
|
</div>
|
|
|
<div class="device-parameters">
|
|
|
<h4>监测参数</h4>
|
|
|
<div class="parameters-grid">
|
|
|
${device.parameters.map(param => `<span class="parameter-tag">${param}</span>`).join('')}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="device-stats">
|
|
|
<h4>实时数据</h4>
|
|
|
<div class="stats-grid">
|
|
|
${device.parameters.slice(0, 6).map(param => `
|
|
|
<div class="stat-item">
|
|
|
<span class="stat-label">${param}</span>
|
|
|
<span class="stat-value">${generateRandomValue(param)}</span>
|
|
|
</div>
|
|
|
`).join('')}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
// 添加关闭事件
|
|
|
modal.querySelector('.device-modal-close').addEventListener('click', () => {
|
|
|
modal.remove();
|
|
|
});
|
|
|
|
|
|
modal.addEventListener('click', (e) => {
|
|
|
if (e.target === modal) {
|
|
|
modal.remove();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
document.body.appendChild(modal);
|
|
|
} |