You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3880 lines
147 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>海底采矿智能态势操作系统</title>
<script src="./model/chart.umd.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> -->
<!-- 替换<head>中的leaflet.css引用 -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<!-- <link rel="stylesheet" href="js/leaflet.css" /> -->
<script src="js/underscore-min.js"></script>
<script src="js/backbone-min.js"></script>
<script src="js/topojson.min.js"></script>
<script src="js/leaflet.js"></script>
<script src="js/leaflet-vector-scalar.js"></script>
<script src="model/seaCurrentData.js"></script>
<!-- <script src="js/leafletvectorscalar.js"></script> -->
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Arial, sans-serif;
}
body {
background: url('/assets/img/bg.png') no-repeat;
background-size: 100% 100%;
color: #e0f0ff;
overflow: hidden;
height: 100vh;
}
/* 顶部标题栏样式 - 与indexScenario.html保持一致 */
.topDiv {
width: 100%;
height: 8vh;
background: url('/assets/img/topBg.png') top no-repeat;
background-size: 100% 115%;
display: flex;
justify-content: center;
z-index: 990;
position: relative;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.topDiv .center {
font-family: PangMenZhengDao, 'Source Han Sans CN';
font-weight: 400;
font-size: 4.1vh;
color: #ffffff;
align-self: center;
margin-top: -40px;
}
.topDiv .left {
position: absolute;
top: 4vh;
left: 3vw;
color: #ffffff;
}
.topDiv .left span:nth-child(1) {
font-weight: bold;
font-size: 0.8vw;
margin-right: 0.5vh;
}
.topDiv .left span:nth-child(2) {
font-weight: bold;
font-size: 1.2vw;
}
.topDiv .right {
position: absolute;
top: 3vh;
right: 0;
color: #ffffff;
display: flex;
align-items: center;
}
.topDiv .right img:nth-child(1) {}
.topDiv .right span {
font-weight: bold;
font-size: 1.2vw;
}
.topDiv .icon-home {
position: absolute;
top: 1vh;
right: 8vw;
width: 4vh;
cursor: pointer;
}
.container {
display: grid;
grid-template-rows: 60px 1fr 180px;
grid-template-columns: 300px 1fr 320px;
height: calc(100vh - 8vh - 20px);
gap: 10px;
padding: 10px;
overflow: auto;
}
/* 顶部导航栏 */
.header {
grid-column: 1 / 4;
background: rgba(12, 42, 73, 0.8);
border-radius: 8px;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(64, 156, 255, 0.3);
/* backdrop-filter: blur(10px); */
}
.system-title {
font-size: 1.4rem;
font-weight: 600;
color: #4fc3f7;
text-shadow: 0 0 8px rgba(79, 195, 247, 0.7);
}
.global-info {
display: flex;
gap: 30px;
}
.info-item {
display: flex;
flex-direction: column;
align-items: center;
}
.info-label {
font-size: 0.8rem;
color: #a0d2ff;
}
.info-value {
font-size: 1.1rem;
font-weight: 600;
color: #4fc3f7;
text-shadow: 0 0 8px rgba(79, 195, 247, 0.7);
}
.alerts {
display: flex;
align-items: center;
gap: 10px;
}
.alert-indicator {
position: relative;
cursor: pointer;
}
.alert-badge {
position: absolute;
top: -8px;
right: -8px;
background-color: #f44336;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
}
/* 左侧面板 - 地质信息 */
.left-panel {
background: rgba(12, 42, 73, 0.1);
/* backdrop-filter: blur(10px); */
border-radius: 8px;
display: flex;
flex-direction: column;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(64, 156, 255, 0.3);
overflow: hidden;
}
.panel-header {
padding: 15px;
background: url('/assets/img/common/homeTitle2.png') no-repeat;
background-position: center;
background-size: 108% 120%;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px 8px 0 0 !important;
border-bottom: 1px solid rgba(64, 156, 255, 0.3);
font-weight: 600;
color: #4fc3f7;
font-family: 'YouSheBiaoTiHei', 'Microsoft YaHei';
font-size: 1.2rem;
}
.panel-tabs {
display: flex;
border-bottom: 1px solid rgba(64, 156, 255, 0.3);
}
.tab {
flex: 1;
text-align: center;
padding: 10px;
cursor: pointer;
transition: background-color 0.3s;
color: #a0d2ff;
}
.tab.active {
background: rgba(40, 80, 130, 0.7);
border-bottom: 2px solid #4fc3f7;
color: #4fc3f7;
}
.tab-content {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
.resource-map {
height: 200px;
background: linear-gradient(135deg, #0a3d62, #1a5276);
border-radius: 6px;
margin-bottom: 15px;
position: relative;
overflow: hidden;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.resource-legend {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 0.8rem;
color: #a0d2ff;
}
.profile-chart {
height: 200px;
background: rgba(25, 55, 95, 0.6);
border-radius: 6px;
margin-bottom: 15px;
position: relative;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.metal-analysis {
height: 150px;
background: rgba(25, 55, 95, 0.6);
border-radius: 6px;
margin-bottom: 15px;
position: relative;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.stat-card {
background: rgba(25, 55, 95, 0.6);
border-radius: 6px;
padding: 10px;
transition: all 0.3s;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.stat-card:hover {
background: rgba(40, 80, 130, 0.7);
transform: translateY(-2px);
}
.stat-value {
font-size: 1.5rem;
font-weight: 600;
margin: 5px 0;
color: #4fc3f7;
text-shadow: 0 0 8px rgba(79, 195, 247, 0.7);
}
.stat-label {
font-size: 0.8rem;
color: #a0d2ff;
}
/* 中央主视图 - 实时数据分析 */
/* 中央主视图 - 实时数据分析 */
.main-view {
background: rgba(12, 42, 73, 0.1);
/* backdrop-filter: blur(10px); */
border-radius: 8px;
position: relative;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(64, 156, 255, 0.3);
overflow: hidden;
display: flex;
flex-direction: column;
}
.view-header {
padding: 15px;
background: url('/assets/img/common/homeTitle2.png') no-repeat;
background-position: center;
background-size: 108% 120%;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px 8px 0 0 !important;
border-bottom: 1px solid rgba(64, 156, 255, 0.3);
font-weight: 600;
color: #4fc3f7;
font-family: 'YouSheBiaoTiHei', 'Microsoft YaHei';
font-size: 1.2rem;
}
.view-title {
font-size: 1.2rem;
font-weight: 600;
color: #4fc3f7;
}
.data-refresh {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.8rem;
color: #a0d2ff;
}
.refresh-btn {
background: none;
border: none;
color: #4fc3f7;
cursor: pointer;
font-size: 1rem;
}
.main-view .panel-tabs {
background: rgba(40, 80, 130, 0.7);
border-bottom: 1px solid rgba(64, 156, 255, 0.3);
padding: 0 15px;
display: flex;
}
.main-view .panel-tabs .tab {
flex: 1;
text-align: center;
padding: 10px;
cursor: pointer;
transition: background-color 0.3s;
color: #a0d2ff;
}
.main-view .panel-tabs .tab.active {
background: rgba(40, 80, 130, 0.7);
border-bottom: 2px solid #4fc3f7;
color: #4fc3f7;
}
.main-view .tab-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.main-view .tab-pane {
flex: 1;
display: none;
overflow: auto;
}
.main-view .tab-pane.active {
display: block;
}
.central-data-content {
flex: 1;
display: grid;
grid-template-rows: 1fr 1fr;
grid-template-columns: 1fr 1fr;
gap: 15px;
padding: 15px;
}
.data-card {
background: rgba(25, 55, 95, 0.6);
border-radius: 8px;
padding: 15px;
display: flex;
flex-direction: column;
border: 1px solid rgba(64, 156, 255, 0.3);
transition: all 0.3s;
overflow: hidden;
}
.data-card:hover {
background: rgba(40, 80, 130, 0.7);
transform: translateY(-2px);
}
.data-card-header {
padding: 10px;
background: url('/assets/img/common/homeTitle2.png') no-repeat;
background-position: center;
background-size: 108% 120%;
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px 6px 0 0 !important;
border-bottom: 1px solid rgba(64, 156, 255, 0.3);
font-weight: 600;
color: #4fc3f7;
font-family: 'YouSheBiaoTiHei', 'Microsoft YaHei';
font-size: 1rem;
margin: -15px -15px 10px -15px;
}
.data-card-title {
font-size: 1rem;
font-weight: 600;
color: #a0d2ff;
}
.data-card-value {
font-size: 1.8rem;
font-weight: 700;
margin: 10px 0;
text-align: center;
color: #4fc3f7;
text-shadow: 0 0 8px rgba(79, 195, 247, 0.7);
}
.data-card-trend {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
font-size: 0.9rem;
}
.trend-up {
color: #66bb6a;
}
.trend-down {
color: #f44336;
}
.data-chart-container {
flex: 1;
position: relative;
margin-top: 10px;
}
.data-card-large {
grid-column: 1 / 3;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
background: rgba(25, 55, 95, 0.6);
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(64, 156, 255, 0.3);
transition: all 0.3s;
overflow: hidden;
}
.data-chart-full {
grid-column: 1 / 3;
height: 240px;
background: rgba(25, 55, 95, 0.6);
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(64, 156, 255, 0.3);
transition: all 0.3s;
overflow: hidden;
}
/* 右侧面板 - 设备状态 */
.right-panel {
background: rgba(12, 42, 73, 0.1);
/* backdrop-filter: blur(10px); */
border-radius: 8px;
display: flex;
flex-direction: column;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(64, 156, 255, 0.3);
overflow: hidden;
}
.equipment-info {
padding: 15px;
}
.equipment-name {
font-size: 1.2rem;
font-weight: 600;
color: #4fc3f7;
font-family: 'YouSheBiaoTiHei', 'Microsoft YaHei';
text-align: center;
padding: 15px;
background: url('/assets/img/common/homeTitle2.png') no-repeat;
background-position: center;
background-size: 108% 120%;
margin: -15px -15px 15px -15px;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid rgba(64, 156, 255, 0.3);
}
.status-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 20px;
}
.status-item {
background: rgba(25, 55, 95, 0.6);
border-radius: 6px;
padding: 10px;
transition: all 0.3s;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.status-item:hover {
background: rgba(40, 80, 130, 0.7);
transform: translateY(-2px);
}
.status-label {
font-size: 0.8rem;
color: #a0d2ff;
}
.status-value {
font-size: 1rem;
font-weight: 600;
color: #4fc3f7;
text-shadow: 0 0 8px rgba(79, 195, 247, 0.7);
}
.subsystem {
margin-bottom: 15px;
}
.subsystem-title {
font-size: 0.9rem;
margin-bottom: 8px;
color: #a0d2ff;
}
.gauge {
height: 8px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 4px;
margin-bottom: 5px;
overflow: hidden;
}
.gauge-fill {
height: 100%;
border-radius: 4px;
}
.gauge-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #a0d2ff;
}
.control-panel {
margin-top: auto;
padding: 15px;
border-top: 1px solid rgba(64, 156, 255, 0.3);
}
.control-buttons {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.btn {
flex: 1;
padding: 10px;
border: 1px solid rgba(64, 156, 255, 0.3);
border-radius: 4px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
background: rgba(12, 42, 73, 0.8);
color: #e0f0ff;
}
.btn:hover {
background: rgba(12, 42, 73, 1);
border-color: #4fc3f7;
}
.btn-primary {
background: linear-gradient(to right, #0277bd, #4fc3f7);
color: white;
}
.btn-danger {
background: linear-gradient(to right, #d32f2f, #f44336);
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.mode-selector {
display: flex;
background: rgba(12, 42, 73, 0.8);
border-radius: 4px;
overflow: hidden;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.mode-option {
flex: 1;
text-align: center;
padding: 8px;
cursor: pointer;
transition: background-color 0.3s;
color: #a0d2ff;
}
.mode-option.active {
background: rgba(40, 80, 130, 0.7);
color: #4fc3f7;
}
/* 底部面板 - 水文信息 */
.bottom-panel {
grid-column: 1 / 4;
background: rgba(12, 42, 73, 0.1);
border-radius: 8px;
display: grid;
grid-template-columns: 1fr 1fr 300px;
gap: 15px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(64, 156, 255, 0.3);
/* 增加整体高度 */
height: 400px;
}
.hydro-card {
background: rgba(25, 55, 95, 0.6);
border-radius: 6px;
padding: 15px;
transition: all 0.3s;
border: 1px solid rgba(64, 156, 255, 0.3);
min-height: 180px;
display: flex;
flex-direction: column;
}
.hydro-card:hover {
background: rgba(40, 80, 130, 0.7);
transform: translateY(-2px);
}
.hydro-title {
font-size: 1.2rem;
font-weight: 600;
color: #4fc3f7;
font-family: 'YouSheBiaoTiHei', 'Microsoft YaHei';
text-align: center;
padding: 10px 0;
background: url('/assets/img/common/homeTitle2.png') no-repeat;
background-position: center;
background-size: 100% 100%;
margin: -15px -15px 15px -15px;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid rgba(64, 156, 255, 0.3);
}
/* 增加洋流监测模块的高度 */
.current-visualization {
/* height: 120px; */
height: calc(100vh - 780px);
/* 从100px增加到120px */
background: rgba(12, 42, 73, 0.8);
border-radius: 6px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(64, 156, 255, 0.3);
}
/* 增加洋流态势图模块的高度 */
.current-map {
height: 120px;
/* 从100px增加到120px */
background: rgba(12, 42, 73, 0.8);
border-radius: 6px;
position: relative;
overflow: hidden;
border: 1px solid rgba(64, 156, 255, 0.3);
}
/* 增加实时数据流模块的高度 */
.data-log {
height: calc(100vh - 760px);
/* 从120px增加到140px */
overflow-y: auto;
background: rgba(12, 42, 73, 0.8);
border-radius: 6px;
padding: 10px;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 0.8rem;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.log-entry {
margin-bottom: 5px;
padding-bottom: 5px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.log-time {
color: #a0d2ff;
}
/* 通信面板 */
/* 通信面板样式 */
.comms-panel {
position: relative;
width: 100%;
height: 100%;
background: transparent;
box-shadow: none;
border: none;
backdrop-filter: none;
padding: 15px;
box-sizing: border-box;
}
.comms-title {
font-size: 1.2rem;
font-weight: 600;
color: #4fc3f7;
font-family: 'YouSheBiaoTiHei', 'Microsoft YaHei';
text-align: center;
padding: 15px;
background: url('/assets/img/common/homeTitle2.png') no-repeat;
background-position: center;
background-size: 108% 120%;
margin: -15px -15px 15px -15px;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid rgba(64, 156, 255, 0.3);
}
.connection-status {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.status-online {
background-color: #66bb6a;
box-shadow: 0 0 5px #66bb6a;
}
.comms-buttons {
display: flex;
gap: 5px;
margin-top: 15px;
}
.comms-btn {
flex: 1;
padding: 6px;
background: rgba(12, 42, 73, 0.8);
border: 1px solid rgba(64, 156, 255, 0.3);
border-radius: 4px;
color: #e0f0ff;
cursor: pointer;
font-size: 0.8rem;
transition: all 0.3s;
}
.comms-btn:hover {
background: rgba(12, 42, 73, 1);
border-color: #4fc3f7;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: rgba(12, 42, 73, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(106, 149, 201, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(106, 149, 201, 0.7);
}
/* 添加到现有样式中 */
.current-map {
height: 100px;
background: rgba(12, 42, 73, 0.8);
border-radius: 6px;
position: relative;
overflow: hidden;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.leaflet-container {
background: transparent !important;
height: calc(100vh - 760px);
border-radius: 6px;
}
.leaflet-control-attribution {
display: none;
}
.scalar-overlay {
border-radius: 6px;
}
/* 通信面板详细样式 */
.comms-overview {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
padding: 15px;
background: rgba(25, 55, 95, 0.6);
border-radius: 6px;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.comms-summary-item {
text-align: center;
}
.comms-summary-label {
font-size: 0.8rem;
color: #a0d2ff;
margin-bottom: 5px;
}
.comms-summary-value {
font-size: 1.2rem;
font-weight: 600;
color: #4fc3f7;
}
.connection-details {
margin-bottom: 20px;
}
.connection-status {
padding: 12px;
margin-bottom: 10px;
background: rgba(25, 55, 95, 0.4);
border-radius: 6px;
border: 1px solid rgba(64, 156, 255, 0.2);
transition: all 0.3s;
}
.connection-status:hover {
background: rgba(40, 80, 130, 0.7);
transform: translateY(-2px);
}
.comms-history {
padding: 15px;
background: rgba(25, 55, 95, 0.6);
border-radius: 6px;
border: 1px solid rgba(64, 156, 255, 0.3);
}
.comms-history>div:first-child {
font-weight: 600;
margin-bottom: 10px;
color: #4fc3f7;
}
/* 滚动条样式 */
.comms-history>div:last-child {
max-height: 120px;
overflow-y: auto;
}
.comms-history>div:last-child::-webkit-scrollbar {
width: 4px;
}
.comms-history>div:last-child::-webkit-scrollbar-track {
background: rgba(12, 42, 73, 0.3);
border-radius: 4px;
}
.comms-history>div:last-child::-webkit-scrollbar-thumb {
background: rgba(106, 149, 201, 0.5);
border-radius: 4px;
}
.comms-history>div:last-child::-webkit-scrollbar-thumb:hover {
background: rgba(106, 149, 201, 0.7);
}
/* 通信状态卡片响应式布局 */
.connection-details {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 20px;
}
/* 在小屏幕上调整为堆叠布局 */
@media (max-width: 1200px) {
.connection-details {
grid-template-columns: 1fr;
}
}
.connection-status {
padding: 15px;
background: rgba(25, 55, 95, 0.4);
border-radius: 6px;
border: 1px solid rgba(64, 156, 255, 0.2);
transition: all 0.3s;
}
.connection-status:hover {
background: rgba(40, 80, 130, 0.7);
transform: translateY(-2px);
}
</style>
<script>
if (!L.DomUtil.setTransform) {
L.DomUtil.setTransform = function (el, offset, scale) {
var pos = offset || new L.Point(0, 0)
el.style[L.DomUtil.TRANSFORM] =
(L.Browser.ie3d
? 'translate(' + pos.x + 'px,' + pos.y + 'px)'
: 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + (scale ? ' scale(' + scale + ')' : '')
}
} // -- support for both 0.0.7 and 1.0.0 rc2 leaflet
L.ScalarLayer = (L.Layer ? L.Layer : L.Class).extend({
options: {
displayValues: true,
displayOptions: {
velocityType: 'Scalar',
position: 'bottomleft',
emptyString: 'No Scalar data',
},
minValue: 193,
maxValue: 328,
colorScale: null,
data: null,
},
_map: null,
_canvasLayer: null,
_scalar: null,
_context: null,
_timer: 0,
_mouseControl: null,
initialize: function initialize(options) {
console.log('==>', options)
L.setOptions(this, options)
},
onAdd: function onAdd(map) {
// determine where to add the layer
this._paneName = this.options.paneName || 'scalarPane' // fall back to overlayPane for leaflet < 1
var pane = map._panes.overlayPane
if (map.getPane) {
// attempt to get pane first to preserve parent (createPane voids this)
pane = map.getPane(this._paneName)
if (!pane) {
pane = map.createPane(this._paneName)
}
} // create canvas, add to map pane
this._canvasLayer = L.canvasLayer({
pane: pane,
}).delegate(this)
this._canvasLayer.addTo(map)
this._map = map
// this._map.on('mousemove',this._onMouseMove,this);
},
onRemove: function onRemove(map) {
this._destroyScalar()
},
setData: function setData(data) {
this.options.data = data
if (this._scalar) {
this._scalar.setData(data)
this._clearAndRestart()
}
this.fire('load')
},
setOpacity: function setOpacity(opacity) {
console.log('this._canvasLayer', this._canvasLayer)
this._canvasLayer.setOpacity(opacity)
},
setOptions: function setOptions(options) {
this.options = Object.assign(this.options, options)
if (options.hasOwnProperty('displayOptions')) {
this.options.displayOptions = Object.assign(
this.options.displayOptions,
options.displayOptions,
)
this._initMouseHandler(true)
}
if (options.hasOwnProperty('data')) this.options.data = options.data
if (this._scalar) {
this._scalar.setOptions(options)
if (options.hasOwnProperty('data')) this._scalar.setData(options.data)
this._clearAndRestart()
} // this.fire("load");
},
vectorToSpeed: function (uMs, vMs) {
return Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2))
},
_onMouseMove: function (e) {
var self = this
var pos = self._map.containerPointToLatLng(L.point(e.containerPoint.x, e.containerPoint.y))
var gridValue = self._scalar.interpolatePoint(pos.lng, pos.lat)
var htmlOut = gridValue ? gridValue.toFixed(2) : null
console.log('当前的值是', htmlOut)
},
/*------------------------------------ PRIVATE ------------------------------------------*/
onDrawLayer: function onDrawLayer(overlay, params) {
var self = this
if (!this._scalar) {
this._initScalar(this)
return
}
if (!this.options.data) {
return
}
if (this._timer) clearTimeout(self._timer)
this._timer = setTimeout(function () {
self._startScalar()
}, 0) // showing velocity is delayed
},
_startScalar: function _startScalar() {
var bounds = this._map.getBounds()
var size = this._map.getSize() // bounds, width, height, extent
this._scalar.start(
[
[0, 0],
[size.x, size.y],
],
size.x,
size.y,
[
[bounds._southWest.lng, bounds._southWest.lat],
[bounds._northEast.lng, bounds._northEast.lat],
],
)
},
_initScalar: function _initScalar(self) {
// scalar object, copy options
var options = Object.assign(
{
canvas: self._canvasLayer._canvas,
map: this._map,
},
self.options,
)
this._scalar = new Scalar(options) // prepare context global var, start drawing
this._context = this._canvasLayer._canvas.getContext('2d')
this._canvasLayer._canvas.classList.add('scalar-overlay')
this._canvasLayer._canvas.classList.add('leaflet-zoom-hide')
this.onDrawLayer()
this._map.on('movestart', self._hideScalar)
this._map.on('moveend', self._clearAndRestart)
this._map.on('zoomstart', self._hideScalar)
this._map.on('zoomend', self._clearAndRestart)
this._map.on('resize', self._clearScalar)
this._initMouseHandler(false)
},
_initMouseHandler: function _initMouseHandler(voidPrevious) {
if (voidPrevious) {
this._map.removeControl(this._mouseControl)
this._mouseControl = false
}
if (!this._mouseControl && this.options.displayValues) {
var options = this.options.displayOptions || {}
options['leafletVelocity'] = this
this._mouseControl = L.control.velocity(options).addTo(this._map)
}
},
// 拖拽时 隐藏 windy canvas
_hideScalar: function _hideWind() {
console.log('viewset')
var canvas = document.querySelector('.scalar-overlay')
if (canvas) {
canvas.style.visibility = 'hidden'
}
},
_clearAndRestart: function _clearAndRestart() {
if (this._context) this._context.clearRect(0, 0, 3000, 3000)
if (this._scalar) this._startScalar()
setTimeout(function () {
var canvas = document.querySelector('.scalar-overlay')
if (canvas) {
canvas.style.visibility = 'visible'
}
}, 0)
},
_clearScalar: function _clearScalar() {
if (this._scalar) this._scalar.stop()
if (this._context) this._context.clearRect(0, 0, 3000, 3000)
},
_destroyScalar: function _destroyScalar() {
if (this._timer) clearTimeout(this._timer)
if (this._scalar) this._scalar.stop()
if (this._context) this._context.clearRect(0, 0, 3000, 3000)
if (this._mouseControl) this._map.removeControl(this._mouseControl)
this._mouseControl = null
this._scalar = null
this._map.removeLayer(this._canvasLayer)
},
})
L.scalarLayer = function (options) {
return new L.ScalarLayer(options)
}
var µ = (function () {
'use strict'
var τ = 2 * Math.PI
var H = 0.000036 // 0.0000360°φ ~= 4m
var DEFAULT_CONFIG = 'current/wind/surface/level/orthographic'
var TOPOLOGY = isMobile() ? '/data/earth-topo-mobile.json?v2' : '/data/earth-topo.json?v2'
/**
* @returns {Boolean} true if the specified value is truthy.
*/
function isTruthy(x) {
return !!x
}
/**
* @returns {Boolean} true if the specified value is not null and not undefined.
*/
function isValue(x) {
return x !== null && x !== undefined
}
/**
* @returns {Object} the first argument if not null and not undefined, otherwise the second argument.
*/
function coalesce(a, b) {
return isValue(a) ? a : b
}
/**
* @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo
* of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation.
*/
function floorMod(a, n) {
var f = a - n * Math.floor(a / n) // HACK: when a is extremely close to an n transition, f can be equal to n. This is bad because f must be
// within range [0, n). Check for this corner case. Example: a:=-1e-16, n:=10. What is the proper fix?
return f === n ? 0 : f
}
/**
* @returns {Number} distance between two points having the form [x, y].
*/
function distance(a, b) {
var Δx = b[0] - a[0]
var Δy = b[1] - a[1]
return Math.sqrt(Δx * Δx + Δy * Δy)
}
/**
* @returns {Number} the value x clamped to the range [low, high].
*/
function clamp(x, low, high) {
return Math.max(low, Math.min(x, high))
}
/**
* @returns {number} the fraction of the bounds [low, high] covered by the value x, after clamping x to the
* bounds. For example, given bounds=[10, 20], this method returns 1 for x>=20, 0.5 for x=15 and 0
* for x<=10.
*/
function proportion(x, low, high) {
return (µ.clamp(x, low, high) - low) / (high - low)
}
/**
* @returns {number} the value p within the range [0, 1], scaled to the range [low, high].
*/
function spread(p, low, high) {
return p * (high - low) + low
}
/**
* Pad number with leading zeros. Does not support fractional or negative numbers.
*/
function zeroPad(n, width) {
var s = n.toString()
var i = Math.max(width - s.length, 0)
return new Array(i + 1).join('0') + s
}
/**
* @returns {String} the specified string with the first letter capitalized.
*/
function capitalize(s) {
return s.length === 0 ? s : s.charAt(0).toUpperCase() + s.substr(1)
}
/**
* @returns {Boolean} true if agent is probably firefox. Don't really care if this is accurate.
*/
function isFF() {
return /firefox/i.test(navigator.userAgent)
}
/**
* @returns {Boolean} true if agent is probably a mobile device. Don't really care if this is accurate.
*/
function isMobile() {
return /android|blackberry|iemobile|ipad|iphone|ipod|opera mini|webos/i.test(
navigator.userAgent,
)
}
function isEmbeddedInIFrame() {
return window != window.top
}
function toUTCISO(date) {
return (
date.getUTCFullYear() +
'-' +
zeroPad(date.getUTCMonth() + 1, 2) +
'-' +
zeroPad(date.getUTCDate(), 2) +
' ' +
zeroPad(date.getUTCHours(), 2) +
':00'
)
}
function toLocalISO(date) {
return (
date.getFullYear() +
'-' +
zeroPad(date.getMonth() + 1, 2) +
'-' +
zeroPad(date.getDate(), 2) +
' ' +
zeroPad(date.getHours(), 2) +
':00'
)
}
/**
* @returns {String} the string yyyyfmmfdd as yyyytmmtdd, where f and t are the "from" and "to" delimiters. Either
* delimiter may be the empty string.
*/
function ymdRedelimit(ymd, fromDelimiter, toDelimiter) {
if (!fromDelimiter) {
return ymd.substr(0, 4) + toDelimiter + ymd.substr(4, 2) + toDelimiter + ymd.substr(6, 2)
}
var parts = ymd.substr(0, 10).split(fromDelimiter)
return [parts[0], parts[1], parts[2]].join(toDelimiter)
}
/**
* @returns {String} the UTC year, month, and day of the specified date in yyyyfmmfdd format, where f is the
* delimiter (and may be the empty string).
*/
function dateToUTCymd(date, delimiter) {
return ymdRedelimit(date.toISOString(), '-', delimiter || '')
}
function dateToConfig(date) {
return {
date: µ.dateToUTCymd(date, '/'),
hour: µ.zeroPad(date.getUTCHours(), 2) + '00',
}
}
/**
* @returns {Object} an object to perform logging, if/when the browser supports it.
*/
function log() {
function format(o) {
return o && o.stack ? o + '\n' + o.stack : o
}
return {
debug: function debug(s) {
if (console && console.log) console.log(format(s))
},
info: function info(s) {
if (console && console.info) console.info(format(s))
},
error: function error(e) {
if (console && console.error) console.error(format(e))
},
time: function time(s) {
if (console && console.time) console.time(format(s))
},
timeEnd: function timeEnd(s) {
if (console && console.timeEnd) console.timeEnd(format(s))
},
}
}
/**
* @returns {width: (Number), height: (Number)} an object that describes the size of the browser's current view.
*/
function view() {
var w = window
var d = document && document.documentElement
var b = document && document.getElementsByTagName('body')[0]
var x = w.innerWidth || d.clientWidth || b.clientWidth
var y = w.innerHeight || d.clientHeight || b.clientHeight
return {
width: x,
height: y,
}
}
/**
* Removes all children of the specified DOM element.
*/
function removeChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild)
}
}
/**
* @returns {Object} clears and returns the specified Canvas element's 2d context.
*/
function clearCanvas(canvas) {
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height)
return canvas
}
function colorInterpolator(start, end) {
var r = start[0],
g = start[1],
b = start[2]
var Δr = end[0] - r,
Δg = end[1] - g,
Δb = end[2] - b
return function (i, a) {
return [Math.floor(r + i * Δr), Math.floor(g + i * Δg), Math.floor(b + i * Δb), a]
}
}
/**
* Produces a color style in a rainbow-like trefoil color space. Not quite HSV, but produces a nice
* spectrum. See http://krazydad.com/tutorials/makecolors.php.
*
* @param hue the hue rotation in the range [0, 1]
* @param a the alpha value in the range [0, 255]
* @returns {Array} [r, g, b, a]
*/
function sinebowColor(hue, a) {
// Map hue [0, 1] to radians [0, 5/6τ]. Don't allow a full rotation because that keeps hue == 0 and
// hue == 1 from mapping to the same color.
var rad = (hue * τ * 5) / 6
rad *= 0.75 // increase frequency to 2/3 cycle per rad
var s = Math.sin(rad)
var c = Math.cos(rad)
var r = Math.floor(Math.max(0, -c) * 255)
var g = Math.floor(Math.max(s, 0) * 255)
var b = Math.floor(Math.max(c, 0, -s) * 255)
return [r, g, b, a]
}
var BOUNDARY = 0.45
var fadeToWhite = colorInterpolator(sinebowColor(1.0, 0), [255, 255, 255])
/**
* Interpolates a sinebow color where 0 <= i <= j, then fades to white where j < i <= 1.
*
* @param i number in the range [0, 1]
* @param a alpha value in range [0, 255]
* @returns {Array} [r, g, b, a]
*/
function extendedSinebowColor(i, a) {
return i <= BOUNDARY
? sinebowColor(i / BOUNDARY, a)
: fadeToWhite((i - BOUNDARY) / (1 - BOUNDARY), a)
}
function asColorStyle(r, g, b, a) {
return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + a + ')'
}
/**
* @returns {Array} of wind colors and a method, indexFor, that maps wind magnitude to an index on the color scale.
*/
function windIntensityColorScale(step, maxWind) {
var result = []
for (var j = 85; j <= 255; j += step) {
result.push(asColorStyle(j, j, j, 1.0))
}
result.indexFor = function (m) {
// map wind speed to a style
return Math.floor((Math.min(m, maxWind) / maxWind) * (result.length - 1))
}
return result
}
/**
* Creates a color scale composed of the specified segments. Segments is an array of two-element arrays of the
* form [value, color], where value is the point along the scale and color is the [r, g, b] color at that point.
* For example, the following creates a scale that smoothly transitions from red to green to blue along the
* points 0.5, 1.0, and 3.5:
*
* [ [ 0.5, [255, 0, 0] ],
* [ 1.0, [0, 255, 0] ],
* [ 3.5, [0, 0, 255] ] ]
*
* @param segments array of color segments
* @returns {Function} a function(point, alpha) that returns the color [r, g, b, alpha] for the given point.
*/
function segmentedColorScale(segments) {
var points = [],
interpolators = [],
ranges = []
for (var i = 0; i < segments.length - 1; i++) {
points.push(segments[i + 1][0])
interpolators.push(colorInterpolator(segments[i][1], segments[i + 1][1]))
ranges.push([segments[i][0], segments[i + 1][0]])
}
return function (point, alpha) {
var i
for (i = 0; i < points.length - 1; i++) {
if (point <= points[i]) {
break
}
}
var range = ranges[i]
return interpolators[i](µ.proportion(point, range[0], range[1]), alpha)
}
}
/**
* Returns a human readable string for the provided coordinates.
*/
function formatCoordinates(λ, φ) {
return (
Math.abs(φ).toFixed(2) +
'° ' +
(φ >= 0 ? 'N' : 'S') +
', ' +
Math.abs(λ).toFixed(2) +
'° ' +
(λ >= 0 ? 'E' : 'W')
)
}
/**
* Returns a human readable string for the provided scalar in the given units.
*/
function formatScalar(value, units) {
return units.conversion(value).toFixed(units.precision)
}
/**
* Returns a human readable string for the provided rectangular wind vector in the given units.
* See http://mst.nerc.ac.uk/wind_vect_convs.html.
*/
function formatVector(wind, units) {
var d = (Math.atan2(-wind[0], -wind[1]) / τ) * 360 // calculate into-the-wind cardinal degrees
var wd = Math.round(((d + 360) % 360) / 5) * 5 // shift [-180, 180] to [0, 360], and round to nearest 5.
return wd.toFixed(0) + '° @ ' + formatScalar(wind[2], units)
}
/**
* Returns a promise for a JSON resource (URL) fetched via XHR. If the load fails, the promise rejects with an
* object describing the reason: {status: http-status-code, message: http-status-text, resource:}.
*/
function loadJson(resource) { } // var d = when.defer();
// d3.json(resource, function(error, result) {
// return error ?
// !error.status ?
// d.reject({status: -1, message: "Cannot load resource: " + resource, resource: resource}) :
// d.reject({status: error.status, message: error.statusText, resource: resource}) :
// d.resolve(result);
// });
// return d.promise;
/**
* Returns the distortion introduced by the specified projection at the given point.
*
* This method uses finite difference estimates to calculate warping by adding a very small amount (h) to
* both the longitude and latitude to create two lines. These lines are then projected to pixel space, where
* they become diagonals of triangles that represent how much the projection warps longitude and latitude at
* that location.
*
* <pre>
* (λ, φ+h) (xλ, yλ)
* . .
* | ==> \
* | \ __. (xφ, yφ)
* (λ, φ) .____. (λ+h, φ) (x, y) .--
* </pre>
*
* See:
* Map Projections: A Working Manual, Snyder, John P: pubs.er.usgs.gov/publication/pp1395
* gis.stackexchange.com/questions/5068/how-to-create-an-accurate-tissot-indicatrix
* www.jasondavies.com/maps/tissot
*
* @returns {Array} array of scaled derivatives [dx/dλ, dy/dλ, dx/dφ, dy/dφ]
*/
function distortion(projection, λ, φ, x, y) {
var = λ < 0 ? H : -H
var = φ < 0 ? H : -H
var = projection([λ + , φ])
var = projection([λ, φ + ]) // Meridian scale factor (see Snyder, equation 4-3), where R = 1. This handles issue where length of 1° λ
// changes depending on φ. Without this, there is a pinching effect at the poles.
var k = Math.cos((φ / 360) * τ)
return [([0] - x) / / k, ([1] - y) / / k, ([0] - x) / , ([1] - y) / ]
}
/**
* Returns a new agent. An agent executes tasks and stores the result of the most recently completed task.
*
* A task is a value or promise, or a function that returns a value or promise. After submitting a task to
* an agent using the submit() method, the task is evaluated and its result becomes the agent's value,
* replacing the previous value. If a task is submitted to an agent while an earlier task is still in
* progress, the earlier task is cancelled and its result ignored. Evaluation of a task may even be skipped
* entirely if cancellation occurs early enough.
*
* Agents are Backbone.js Event emitters. When a submitted task is accepted for invocation by an agent, a
* "submit" event is emitted. This event has the agent as its sole argument. When a task finishes and
* the agent's value changes, an "update" event is emitted, providing (value, agent) as arguments. If a task
* fails by either throwing an exception or rejecting a promise, a "reject" event having arguments (err, agent)
* is emitted. If an event handler throws an error, an "error" event having arguments (err, agent) is emitted.
*
* The current task can be cancelled by invoking the agent.cancel() method, and the cancel status is available
* as the Boolean agent.cancel.requested key. Within the task callback, the "this" context is set to the agent,
* so a task can know to abort execution by checking the this.cancel.requested key. Similarly, a task can cancel
* itself by invoking this.cancel().
*
* Example pseudocode:
* <pre>
* var agent = newAgent();
* agent.on("update", function(value) {
* console.log("task completed: " + value); // same as agent.value()
* });
*
* function someLongAsynchronousProcess(x) { // x === "abc"
* var d = when.defer();
* // some long process that eventually calls: d.resolve(result)
* return d.promise;
* }
*
* agent.submit(someLongAsynchronousProcess, "abc");
* </pre>
*
* @param [initial] initial value of the agent, if any
* @returns {Object}
*/
function newAgent(initial) {
/**
* @returns {Function} a cancel function for a task.
*/
function cancelFactory() {
return function cancel() {
cancel.requested = true
return agent
}
}
/**
* Invokes the specified task.
* @param cancel the task's cancel function.
* @param taskAndArguments the [task-function-or-value, arg0, arg1, ...] array.
*/
function runTask(cancel, taskAndArguments) {
//
// function run(args) {
// return cancel.requested ? null : _.isFunction(task) ? task.apply(agent, args) : task;
// }
//
// function accept(result) {
// if (!cancel.requested) {
// value = result;
// agent.trigger("update", result, agent);
// }
// }
//
// function reject(err) {
// if (!cancel.requested) { // ANNOYANCE: when cancelled, this task's error is silently suppressed
// agent.trigger("reject", err, agent);
// }
// }
//
// function fail(err) {
// agent.trigger("fail", err, agent);
// }
//
// try {
// // When all arguments are resolved, invoke the task then either accept or reject the result.
// var task = taskAndArguments[0];
// when.all(_.rest(taskAndArguments)).then(run).then(accept, reject).done(undefined, fail);
// agent.trigger("submit", agent);
// } catch (err) {
// fail(err);
// }
}
var _value = initial
var runTask_debounced = _.debounce(runTask, 0) // ignore multiple simultaneous submissions--reduces noise
var agent = {
/**
* @returns {Object} this agent's current value.
*/
value: function value() {
return _value
},
/**
* Cancels this agent's most recently submitted task.
*/
cancel: cancelFactory(),
/**
* Submit a new task and arguments to invoke the task with. The task may return a promise for
* asynchronous tasks, and all arguments may be either values or promises. The previously submitted
* task, if any, is immediately cancelled.
* @returns this agent.
*/
submit: function submit(task, arg0, arg1, and_so_on) {
// immediately cancel the previous task
this.cancel() // schedule the new task and update the agent with its associated cancel function
runTask_debounced((this.cancel = cancelFactory()), arguments)
return this
},
}
return _.extend(agent, Backbone.Events)
}
/**
* Parses a URL hash fragment:
*
* example: "2013/11/14/0900Z/wind/isobaric/1000hPa/orthographic=26.50,-153.00,1430/overlay=off"
* output: {date: "2013/11/14", hour: "0900", param: "wind", surface: "isobaric", level: "1000hPa",
* projection: "orthographic", orientation: "26.50,-153.00,1430", overlayType: "off"}
*
* grammar:
* hash := ( "current" | yyyy / mm / dd / hhhh "Z" ) / param / surface / level [ / option [ / option ... ] ]
* option := type [ "=" number [ "," number [ ... ] ] ]
*
* @param hash the hash fragment.
* @param projectionNames the set of allowed projections.
* @param overlayTypes the set of allowed overlays.
* @returns {Object} the result of the parse.
*/
function parse(hash, projectionNames, overlayTypes) {
var option,
result = {} // 1 2 3 4 5 6 7 8 9
var tokens =
/^(current|(\d{4})\/(\d{1,2})\/(\d{1,2})\/(\d{3,4})Z)\/(\w+)\/(\w+)\/(\w+)([\/].+)?/.exec(
hash,
)
if (tokens) {
var date =
tokens[1] === 'current'
? 'current'
: tokens[2] + '/' + zeroPad(tokens[3], 2) + '/' + zeroPad(tokens[4], 2)
var hour = isValue(tokens[5]) ? zeroPad(tokens[5], 4) : ''
result = {
date: date,
// "current" or "yyyy/mm/dd"
hour: hour,
// "hhhh" or ""
param: tokens[6],
// non-empty alphanumeric _
surface: tokens[7],
// non-empty alphanumeric _
level: tokens[8],
// non-empty alphanumeric _
projection: 'orthographic',
orientation: '',
topology: TOPOLOGY,
overlayType: 'default',
showGridPoints: false,
}
coalesce(tokens[9], '')
.split('/')
.forEach(function (segment) {
if ((option = /^(\w+)(=([\d\-.,]*))?$/.exec(segment))) {
if (projectionNames.has(option[1])) {
result.projection = option[1] // non-empty alphanumeric _
result.orientation = coalesce(option[3], '') // comma delimited string of numbers, or ""
}
} else if ((option = /^overlay=(\w+)$/.exec(segment))) {
if (overlayTypes.has(option[1]) || option[1] === 'default') {
result.overlayType = option[1]
}
} else if ((option = /^grid=(\w+)$/.exec(segment))) {
if (option[1] === 'on') {
result.showGridPoints = true
}
}
})
}
return result
}
/**
* A Backbone.js Model that persists its attributes as a human readable URL hash fragment. Loading from and
* storing to the hash fragment is handled by the sync method.
*/
var Configuration = Backbone.Model.extend({
id: 0,
_ignoreNextHashChangeEvent: false,
_projectionNames: null,
_overlayTypes: null,
/**
* @returns {String} this configuration converted to a hash fragment.
*/
toHash: function toHash() {
var attr = this.attributes
var dir = attr.date === 'current' ? 'current' : attr.date + '/' + attr.hour + 'Z'
var proj = [attr.projection, attr.orientation].filter(isTruthy).join('=')
var ol =
!isValue(attr.overlayType) || attr.overlayType === 'default'
? ''
: 'overlay=' + attr.overlayType
var grid = attr.showGridPoints ? 'grid=on' : ''
return [dir, attr.param, attr.surface, attr.level, ol, proj, grid].filter(isTruthy).join('/')
},
/**
* Synchronizes between the configuration model and the hash fragment in the URL bar. Invocations
* caused by "hashchange" events must have the {trigger: "hashchange"} option specified.
*/
sync: function sync(method, model, options) {
switch (method) {
case 'read':
if (options.trigger === 'hashchange' && model._ignoreNextHashChangeEvent) {
model._ignoreNextHashChangeEvent = false
return
}
model.set(
parse(
window.location.hash.substr(1) || DEFAULT_CONFIG,
model._projectionNames,
model._overlayTypes,
),
)
break
case 'update':
// Ugh. Setting the hash fires a hashchange event during the next event loop turn. Ignore it.
model._ignoreNextHashChangeEvent = true
window.location.hash = model.toHash()
break
}
},
})
/**
* A Backbone.js Model to hold the page's configuration as a set of attributes: date, layer, projection,
* orientation, etc. Changes to the configuration fire events which the page's components react to. For
* example, configuration.save({projection: "orthographic"}) fires an event which causes the globe to be
* re-rendered with an orthographic projection.
*
* All configuration attributes are persisted in a human readable form to the page's hash fragment (and
* vice versa). This allows deep linking and back-button navigation.
*
* @returns {Configuration} Model to represent the hash fragment, using the specified set of allowed projections.
*/
function buildConfiguration(projectionNames, overlayTypes) {
var result = new Configuration()
result._projectionNames = projectionNames
result._overlayTypes = overlayTypes
return result
}
return {
isTruthy: isTruthy,
isValue: isValue,
coalesce: coalesce,
floorMod: floorMod,
distance: distance,
clamp: clamp,
proportion: proportion,
spread: spread,
zeroPad: zeroPad,
capitalize: capitalize,
isFF: isFF,
isMobile: isMobile,
isEmbeddedInIFrame: isEmbeddedInIFrame,
toUTCISO: toUTCISO,
toLocalISO: toLocalISO,
ymdRedelimit: ymdRedelimit,
dateToUTCymd: dateToUTCymd,
dateToConfig: dateToConfig,
log: log,
view: view,
removeChildren: removeChildren,
clearCanvas: clearCanvas,
sinebowColor: sinebowColor,
extendedSinebowColor: extendedSinebowColor,
windIntensityColorScale: windIntensityColorScale,
segmentedColorScale: segmentedColorScale,
formatCoordinates: formatCoordinates,
formatScalar: formatScalar,
formatVector: formatVector,
loadJson: loadJson,
distortion: distortion,
newAgent: newAgent,
parse: parse,
buildConfiguration: buildConfiguration,
}
})()
var Scalar = function Scalar(params) {
// var SCALE = {
// bounds: [-20, 20],
// gradient: µ.segmentedColorScale([
// [-20, [37, 4, 42]],
// [-15, [41, 10, 130]],
// [-12, [81, 40, 40]],
// [-10, [192, 37, 149]], // -40 C/F
// [-5, [70, 215, 215]], // 0 F
// [0, [21, 84, 187]], // 0 C
// [5, [24, 132, 14]], // just above 0 C
// [10, [247, 251, 59]],
// [12, [235, 167, 21]],
// [15, [230, 71, 39]],
// [20, [88, 27, 67]]
// ])
// }
var view = {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
}
// var OVERLAY_ALPHA = Math.floor(0.4 * 255)//百分之40透明度
var OVERLAY_ALPHA = Math.floor(0.6 * 255)
var TRANSPARENT_BLACK = [0, 0, 0, 0] //default canvas's grid data color
var MIN_VELOCITY_INTENSITY = params.minValue || 193 // velocity at which particle intensity is minimum (m/s)
var MAX_VELOCITY_INTENSITY = params.maxValue || 328 // velocity at which particle intensity is maximum (m/s)
// var OPACITY = 0.97 // 默认 最大值最小值范围及颜色区间
var OPACITY = 1 // 默认 最大值最小值范围及颜色区间
var COLORSCALE = params.colorScale || [
[193, [90, 86, 143]],
[206, [72, 104, 181]],
[219, [69, 151, 168]],
[233.15, [81, 180, 98]], // -40 C/F
[255.372, [106, 192, 82]], // 0 F
[273.15, [177, 209, 67]], // 0 C
[275.15, [215, 206, 60]], // just above 0 C
[291, [214, 172, 64]],
[298, [213, 137, 72]],
[311, [205, 94, 93]],
[328, [144, 28, 79]],
]
var SCALE = {
bounds: [MIN_VELOCITY_INTENSITY, MAX_VELOCITY_INTENSITY],
gradient: {}, // 计算色值及step
}
var setColorScale = function setColorScale() {
let n = parseFloat(((MAX_VELOCITY_INTENSITY - MIN_VELOCITY_INTENSITY) / 10).toFixed(4)),
m = MIN_VELOCITY_INTENSITY
for (let i = 0; i < COLORSCALE.length; i++) {
COLORSCALE[i][0] = m
m += n
}
return COLORSCALE
}
SCALE.gradient = µ.segmentedColorScale(setColorScale())
var NULL_WIND_VECTOR = [NaN, NaN, null] // singleton for no wind in the form: [u, v, magnitude]
var builder
var grid
var gridData = params.data
var date
var λ0, φ0, Δλ, Δφ, ni, nj
var setData = function setData(data) {
gridData = data
} // 计算色值及step
var setColorScale = function setColorScale() {
var max = SCALE.bounds[1],
min = SCALE.bounds[0]
var n = Math.ceil((max - min) / 10)
var m = min
for (var i = 0; i < COLORSCALE.length; i++) {
COLORSCALE[i].unshift(m)
m += n
}
return COLORSCALE
}
var setOptions = function setOptions(options) {
if (options.hasOwnProperty('opacity')) OPACITY = options.opacity
} // 双线性插入标量
var bilinearInterpolateScalar = function bilinearInterpolateScalar(x, y, g00, g10, g01, g11) {
var rx = 1 - x
var ry = 1 - y
return g00 * rx * ry + g10 * x * ry + g01 * rx * y + g11 * x * y
} // 标量初始化
var createScalarBuilder = function createScalarBuilder(res) {
let _data, _data1, _data2
if (res.length > 1) {
_data1 = res[0]
_data2 = res[1]
_data = _data1.data.map(function (v, i) {
// 向量中有 负值,需取绝对值
return (Math.abs(v) + Math.abs(_data2.data[i])) / 2
})
} else {
_data1 = res[0]
_data = _data1.data.map(function (v, i) {
// 向量中有 负值,需取绝对值
return Math.abs(v)
})
}
// console.log('===>builder',_data)
return {
header: res[0].header,
data: function data(i) {
return _data[i]
},
interpolate: bilinearInterpolateScalar,
}
}
var buildGrid = function buildGrid(data, callback) {
builder = createScalarBuilder(data)
var header = builder.header
λ0 = header.lo1
φ0 = header.la1 // the grid's origin (e.g., 0.0E, 90.0N)
Δλ = header.dx
Δφ = header.dy // distance between grid points (e.g., 2.5 deg lon, 2.5 deg lat)
ni = header.nx
nj = header.ny // number of grid points W-E and N-S (e.g., 144 x 73)
date = new Date(header.refTime)
date.setHours(date.getHours() + header.forecastTime) // Scan mode 0 assumed. Longitude increases from λ0, and latitude decreases from φ0.
// http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_table3-4.shtml
grid = []
var p = 0
var isContinuous = Math.floor(ni * Δλ) >= 360
for (var j = 0; j < nj; j++) {
var row = []
for (var i = 0; i < ni; i++, p++) {
row[i] = builder.data(p)
}
if (isContinuous) {
// For wrapped grids, duplicate first column as last column to simplify interpolation logic
row.push(row[0])
}
grid[j] = row
}
callback({
date: date,
interpolate: interpolate,
})
}
/**
* Get interpolated grid value from Lon/Lat position
* @param λ {Float} Longitude
* @param φ {Float} Latitude
* @returns {Object}
*/
var interpolate = function interpolate(λ, φ) {
if (!grid) return null
var i = floorMod(λ - λ0, 360) / Δλ // calculate longitude index in wrapped range [0, 360)
var j = (φ0 - φ) / Δφ // calculate latitude index in direction +90 to -90
var fi = Math.floor(i),
ci = fi + 1
var fj = Math.floor(j),
cj = fj + 1
var row
if ((row = grid[fj])) {
var g00 = row[fi]
var g10 = row[ci]
if (isValue(g00) && isValue(g10) && (row = grid[cj])) {
var g01 = row[fi]
var g11 = row[ci]
if (isValue(g01) && isValue(g11)) {
// All four points found, so interpolate the value.
return builder.interpolate(i - fi, j - fj, g00, g10, g01, g11)
}
}
}
return null
}
/**
* @returns {Boolean} true if the specified value is not null and not undefined.
*/
var isValue = function isValue(x) {
return x !== null && x !== undefined
}
/**
* @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo
* of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation.
*/
var floorMod = function floorMod(a, n) {
return a - n * Math.floor(a / n)
}
/**
* @returns {Number} the value x clamped to the range [low, high].
*/
var clamp = function clamp(x, range) {
return Math.max(range[0], Math.min(x, range[1]))
}
/**
* @returns {Boolean} true if agent is probably a mobile device. Don't really care if this is accurate.
*/
var isMobile = function isMobile() {
return /android|blackberry|iemobile|ipad|iphone|ipod|opera mini|webos/i.test(
navigator.userAgent,
)
} //
var createField = function createField(columns, bounds, callback) {
/**
* @returns {Array} wind vector [u, v, magnitude] at the point (x, y), or [NaN, NaN, null] if wind
* is undefined at that point.
*/
function field(x, y) {
var column = columns[Math.round(x)]
return (column && column[Math.round(y)]) || NULL_WIND_VECTOR
} // Frees the massive "columns" array for GC. Without this, the array is leaked (in Chrome) each time a new
// field is interpolated because the field closure's context is leaked, for reasons that defy explanation.
field.release = function () {
columns = []
}
field.randomize = function (o) {
// UNDONE: this method is terrible
var x, y
var safetyNet = 0
do {
x = Math.round(Math.floor(Math.random() * bounds.width) + bounds.x)
y = Math.round(Math.floor(Math.random() * bounds.height) + bounds.y)
} while (field(x, y)[2] === null && safetyNet++ < 30)
o.x = x
o.y = y
return o
} // console.log(bounds,field)
callback(bounds, field)
}
var buildBounds = function buildBounds(bounds, width, height) {
var upperLeft = bounds[0]
var lowerRight = bounds[1]
var x = Math.round(upperLeft[0]) //Math.max(Math.floor(upperLeft[0], 0), 0);
var y = Math.max(Math.floor(upperLeft[1], 0), 0)
var xMax = Math.min(Math.ceil(lowerRight[0], width), width - 1)
var yMax = Math.min(Math.ceil(lowerRight[1], height), height - 1)
return {
x: x,
y: y,
xMax: width,
yMax: yMax,
width: width,
height: height,
}
}
var deg2rad = function deg2rad(deg) {
return (deg / 180) * Math.PI
}
var invert = function invert(x, y, windy) {
var latlon = params.map.containerPointToLatLng(L.point(x, y))
return [latlon.lng, latlon.lat]
}
var project = function project(lat, lon, windy) {
var xy = params.map.latLngToContainerPoint(L.latLng(lat, lon))
return [xy.x, xy.y]
} //
var createMask = function createMask() {
// Create a detached canvas, ask the model to define the mask polygon, then fill with an opaque color.
var width = view.width,
height = view.height
var canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
var context = canvas.getContext('2d')
context.fillStyle = 'rgba(255, 0, 0, 1)'
context.fill()
var imageData = context.getImageData(0, 0, width, height)
var data = imageData.data // layout: [r, g, b, a, r, g, b, a, ...]
// console.log(data)
return {
imageData: imageData,
isVisible: function isVisible(x, y) {
var i = (y * width + x) * 4
return data[i + 3] > 0 // non-zero alpha means pixel is visible
},
set: function set(x, y, rgba) {
var i = (y * width + x) * 4
data[i] = rgba[0]
data[i + 1] = rgba[1]
data[i + 2] = rgba[2]
data[i + 3] = rgba[3]
return this
},
}
}
var interpolateField = function interpolateField(grid, bounds, extent, callback) {
var columns = []
var x = bounds.x
var mask = createMask()
function interpolateColumn(x) {
var column = []
for (var y = bounds.y; y <= bounds.yMax; y += 2) {
// if (mask.isVisible(x, y)){
var coord = invert(x, y)
var color = TRANSPARENT_BLACK
if (coord) {
var λ = coord[0],
φ = coord[1]
if (isFinite(λ)) {
var scalar = null
scalar = grid.interpolate(λ, φ)
if (isValue(scalar)) {
color = SCALE.gradient(scalar, OVERLAY_ALPHA)
}
}
}
mask
.set(x, y, color)
.set(x + 1, y, color)
.set(x, y + 1, color)
.set(x + 1, y + 1, color) // }
}
columns[x + 1] = columns[x] = column
}
; (function batchInterpolate() {
var start = Date.now()
while (x < bounds.width) {
interpolateColumn(x)
x += 2
if (Date.now() - start > 1000) {
//MAX_TASK_TIME) {
setTimeout(batchInterpolate, 25)
return
}
}
createField(columns, bounds, function () {
drawOverlay(mask)
})
})()
} // 绘制强度图层
var drawOverlay = function drawOverlay(field) {
if (!field) return
console.log('强度图层加载')
var canvas = document.querySelector('.scalar-overlay')
var ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, 3000, 3000) // field.imageData.data = data
ctx.putImageData(field.imageData, 0, 0)
console.log(field.imageData)
} // 开始绘制
var start = function start(bounds, width, height, extent) {
var mapBounds = {
south: deg2rad(extent[0][1]),
north: deg2rad(extent[1][1]),
east: deg2rad(extent[1][0]),
west: deg2rad(extent[0][0]),
width: width,
height: height,
}
stop() // build grid
buildGrid(gridData, function (grid) {
// interpolateField
interpolateField(
grid,
buildBounds(bounds, width, height),
mapBounds,
function (bounds, field) {
// animate the canvas with random points
// strength.field = field;
// animate(bounds, field);
},
)
})
}
var stop = function stop() {
if (scalar.field) scalar.field.release()
}
var scalar = {
params: params,
start: start,
stop: stop,
createField: createField,
interpolatePoint: interpolate,
setData: setData,
setOptions: setOptions,
}
return scalar
}
</script>
</head>
<body>
<!-- 顶部标题栏 - 与indexScenario.html保持一致 -->
<div class="topDiv">
<div class="left">
<span id="current-date">2025年10月17日</span>
<span id="current-time">15:30:15</span>
</div>
<div class="center">海底采矿智能态势操作系统</div>
<div class="right">
<img src="/common/images/logo_gt.png" alt="" />
</div>
</div>
<div class="container">
<!-- 顶部导航栏 -->
<div class="header">
<div class="system-title">"海神号"海底采矿作业指挥系统 - CCZ-001矿区</div>
<div class="global-info">
<div class="info-item">
<div class="info-label">当前深度</div>
<div class="info-value">4500 m</div>
</div>
<div class="info-item">
<div class="info-label">系统状态</div>
<div class="info-value" style="color: #66bb6a">正常</div>
</div>
<div class="info-item">
<div class="info-label">采矿速率</div>
<div class="info-value">120 吨/小时</div>
</div>
</div>
<div class="alerts">
<div class="alert-indicator">
<span>🔔</span>
<div class="alert-badge">2</div>
</div>
</div>
</div>
<!-- 左侧面板 - 地质信息 -->
<div class="left-panel">
<div class="panel-header">地质与资源信息</div>
<div class="panel-tabs">
<div class="tab active" data-tab="resources">资源分布</div>
<div class="tab" data-tab="geology">地质剖面</div>
<div class="tab" data-tab="stats">资源统计</div>
</div>
<div class="tab-content">
<!-- 替换原有的资源分布标签页内容 -->
<div class="tab-pane active" id="resources">
<div class="resource-map" id="resource-map">
<canvas id="resourceMapCanvas"></canvas>
</div>
<div class="resource-legend">
<span>低丰度</span>
<span>中丰度</span>
<span>高丰度</span>
</div>
<div class="stats-grid" style="margin-top: 15px;">
<div class="stat-card">
<div class="stat-label">多金属结核丰度</div>
<div class="stat-value">12.5 kg/m²</div>
</div>
<div class="stat-card">
<div class="stat-label">铜含量</div>
<div class="stat-value">1.2%</div>
</div>
<div class="stat-card">
<div class="stat-label">镍含量</div>
<div class="stat-value">0.8%</div>
</div>
<div class="stat-card">
<div class="stat-label">钴含量</div>
<div class="stat-value">0.3%</div>
</div>
<div class="stat-card">
<div class="stat-label">锰含量</div>
<div class="stat-value">22.4%</div>
</div>
<div class="stat-card">
<div class="stat-label">覆盖面积</div>
<div class="stat-value">75%</div>
</div>
</div>
</div>
<!-- 替换原有的地质剖面标签页内容 -->
<div class="tab-pane" id="geology">
<div class="profile-chart" id="profile-chart">
<canvas id="geologyChart"></canvas>
</div>
<div class="metal-analysis" id="metal-analysis">
<canvas id="metalAnalysisChart"></canvas>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">沉积物厚度</div>
<div class="stat-value">4.2 m</div>
</div>
<div class="stat-card">
<div class="stat-label">承载力</div>
<div class="stat-value">85 kPa</div>
</div>
<div class="stat-card">
<div class="stat-label">土质类型</div>
<div class="stat-value">硅质软泥</div>
</div>
<div class="stat-card">
<div class="stat-label">结核密度</div>
<div class="stat-value">中等</div>
</div>
</div>
</div>
<!-- 替换原有的资源统计标签页内容 -->
<div class="tab-pane" id="stats">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">总资源储量</div>
<div class="stat-value">2.8 百万吨</div>
</div>
<div class="stat-card">
<div class="stat-label">已采集量</div>
<div class="stat-value">124,560 吨</div>
</div>
<div class="stat-card">
<div class="stat-label">采集进度</div>
<div class="stat-value">18.5%</div>
</div>
<div class="stat-card">
<div class="stat-label">铜储量</div>
<div class="stat-value">33,600 吨</div>
</div>
<div class="stat-card">
<div class="stat-label">镍储量</div>
<div class="stat-value">22,400 吨</div>
</div>
<div class="stat-card">
<div class="stat-label">钴储量</div>
<div class="stat-value">8,400 吨</div>
</div>
<div class="stat-card">
<div class="stat-label">锰储量</div>
<div class="stat-value">627,200 吨</div>
</div>
<div class="stat-card">
<div class="stat-label">预计总开采期</div>
<div class="stat-value">15 年</div>
</div>
</div>
</div>
</div>
</div>
<!-- 修改中央主视图 - 实时数据分析部分 -->
<div class="main-view">
<div class="view-header">实时数据分析</div>
<!-- 添加tab切换 -->
<div class="panel-tabs"
style="padding: 0 15px; background: rgba(40, 80, 130, 0.3); border-bottom: 1px solid rgba(64, 156, 255, 0.3);">
<div class="tab active" data-tab="data-charts">数据图表</div>
<div class="tab" data-tab="comms-status">通信状态</div>
</div>
<div class="tab-content" style="flex: 1; overflow: hidden;">
<!-- 数据图表tab内容 -->
<div class="tab-pane active" id="data-charts"
style="height: 100%; display: flex; flex-direction: column;">
<div class="central-data-content" style="flex: 1;">
<!-- 采矿数据卡片 -->
<div class="data-card">
<div class="data-card-header">
<div class="data-card-title">采矿速率</div>
</div>
<div class="data-card-value" id="mining-rate-value">120 吨/小时</div>
<div class="data-chart-container">
<canvas id="mining-rate-chart"></canvas>
</div>
<div class="data-card-trend trend-up" id="mining-rate-trend">↑ 2.5%</div>
</div>
<!-- 系统健康度卡片 -->
<div class="data-card">
<div class="data-card-header">
<div class="data-card-title">系统健康度</div>
</div>
<div class="data-card-value" id="system-health-value">92.5%</div>
<div class="data-chart-container">
<canvas id="system-health-chart"></canvas>
</div>
<div class="data-card-trend trend-down" id="system-health-trend">↓ 1.2%</div>
</div>
<!-- 环境参数卡片 -->
<div class="data-card">
<div class="data-card-header">
<div class="data-card-title">环境参数</div>
</div>
<div class="data-card-value" id="environment-value">2.1°C</div>
<div class="data-chart-container">
<canvas id="environment-chart"></canvas>
</div>
<div class="data-card-trend" id="environment-trend">稳定</div>
</div>
<!-- 资源采集进度卡片 -->
<div class="data-card">
<div class="data-card-header">
<div class="data-card-title">采集进度</div>
</div>
<div class="data-card-value" id="progress-value">18.5%</div>
<div class="data-chart-container">
<canvas id="progress-chart"></canvas>
</div>
<div class="data-card-trend trend-up" id="progress-trend">↑ 0.8%</div>
</div>
<!-- 大型图表 - 采矿效率趋势 -->
<div class="data-chart-full">
<div class="data-card-header">
<div class="data-card-title">采矿效率趋势分析</div>
</div>
<div class="data-chart-container">
<canvas id="efficiency-trend-chart"></canvas>
</div>
</div>
</div>
</div>
<!-- 通信状态tab内容 -->
<!-- 修改通信状态tab内容增加更多详细信息 -->
<div class="tab-pane" id="comms-status" style="padding: 0; height: 100%;">
<div class="comms-panel"
style="position: relative; width: 100%; height: 100%; box-shadow: none; border: none;">
<div class="comms-title" style="color: #ffff;">通信状态</div>
<!-- 通信概览 -->
<div class="comms-overview"
style="display: flex; justify-content: space-between; margin-bottom: 20px; padding: 10px; background: rgba(25, 55, 95, 0.6); border-radius: 6px;">
<div class="comms-summary-item">
<div class="comms-summary-label">总连接数</div>
<div class="comms-summary-value">3</div>
</div>
<div class="comms-summary-item">
<div class="comms-summary-label">在线设备</div>
<div class="comms-summary-value" style="color: #66bb6a;">3</div>
</div>
<div class="comms-summary-item">
<div class="comms-summary-label">信号强度</div>
<div class="comms-summary-value">良好</div>
</div>
</div>
<!-- 连接详情 -->
<!-- 使用Flexbox实现一行三列布局 -->
<div class="connection-details" style="display: flex; gap: 15px; margin-bottom: 20px;">
<div class="connection-status" style="flex: 1;">
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<div class="status-dot status-online"></div>
<div style="margin-left: 8px; font-weight: 600;">与水面船通信</div>
<div style="margin-left: auto; font-size: 0.9rem; color: #66bb6a;">正常</div>
</div>
<div style="font-size: 0.85rem; color: #a0d2ff; margin-left: 18px;">
<div>延迟: 45ms</div>
<div>带宽: 10Mbps</div>
<div>最后更新: 14:30:12</div>
</div>
</div>
<div class="connection-status" style="flex: 1;">
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<div class="status-dot status-online"></div>
<div style="margin-left: 8px; font-weight: 600;">与集矿机通信</div>
<div style="margin-left: auto; font-size: 0.9rem; color: #66bb6a;">正常</div>
</div>
<div style="font-size: 0.85rem; color: #a0d2ff; margin-left: 18px;">
<div>延迟: 120ms</div>
<div>带宽: 5Mbps</div>
<div>最后更新: 14:30:14</div>
</div>
</div>
<div class="connection-status" style="flex: 1;">
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<div class="status-dot status-online"></div>
<div style="margin-left: 8px; font-weight: 600;">与ROV通信</div>
<div style="margin-left: auto; font-size: 0.9rem; color: #66bb6a;">正常</div>
</div>
<div style="font-size: 0.85rem; color: #a0d2ff; margin-left: 18px;">
<div>延迟: 85ms</div>
<div>带宽: 8Mbps</div>
<div>最后更新: 14:30:10</div>
</div>
</div>
</div>
<!-- 通信历史 -->
<div class="comms-history" style="margin-top: 20px;">
<div style="font-weight: 600; margin-bottom: 10px; color: #4fc3f7;">通信历史</div>
<div style="font-size: 0.85rem; max-height: 120px; overflow-y: auto;">
<div style="padding: 5px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<span style="color: #a0d2ff;">14:28:05</span>
<span style="color: #66bb6a;">[INFO]</span>
<span>与集矿机建立连接</span>
</div>
<div style="padding: 5px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<span style="color: #a0d2ff;">14:25:32</span>
<span style="color: #66bb6a;">[INFO]</span>
<span>ROV通信恢复</span>
</div>
<div style="padding: 5px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<span style="color: #a0d2ff;">14:20:45</span>
<span style="color: #ff9800;">[WARN]</span>
<span>水面船信号短暂波动</span>
</div>
<div style="padding: 5px 0;">
<span style="color: #a0d2ff;">14:18:30</span>
<span style="color: #66bb6a;">[INFO]</span>
<span>所有通信链路正常</span>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="comms-buttons" style="margin-top: 20px;">
<button class="comms-btn">指挥中心</button>
<button class="comms-btn">水面船</button>
<button class="comms-btn">ROV操作员</button>
<button class="comms-btn" style="background: rgba(33, 150, 243, 0.8);">通信诊断</button>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧面板 - 设备状态部分 -->
<div class="right-panel">
<div class="panel-header">设备状态与控制</div>
<div class="equipment-info">
<div class="equipment-name">集矿机 #01 - "开拓者号"</div>
<div class="status-grid">
<div class="status-item">
<div class="status-label">位置</div>
<div class="status-value">153.2°E, 12.5°N</div>
</div>
<div class="status-item">
<div class="status-label">深度</div>
<div class="status-value">4485 m</div>
</div>
<div class="status-item">
<div class="status-label">姿态</div>
<div class="status-value">俯仰: 2.1°, 横滚: 0.5°</div>
</div>
<div class="status-item">
<div class="status-label">速度</div>
<div class="status-value">0.8 km/h</div>
</div>
</div>
<div class="subsystem">
<div class="subsystem-title">动力系统</div>
<div class="gauge">
<div class="gauge-fill" style="width: 75%; background-color: #66bb6a;"></div>
</div>
<div class="gauge-label">
<span>电池电量</span>
<span>75%</span>
</div>
</div>
<div class="subsystem">
<div class="subsystem-title">采集系统</div>
<div class="gauge">
<div class="gauge-fill" style="width: 90%; background-color: #66bb6a;"></div>
</div>
<div class="gauge-label">
<span>采集头转速</span>
<span>90%</span>
</div>
</div>
<div class="subsystem">
<div class="subsystem-title">液压系统</div>
<div class="gauge">
<div class="gauge-fill" style="width: 65%; background-color: #ff9800;"></div>
</div>
<div class="gauge-label">
<span>系统压力</span>
<span>65%</span>
</div>
</div>
</div>
<div class="control-panel">
<div class="control-buttons">
<button class="btn btn-primary">启动采集</button>
<button class="btn btn-danger">紧急停止</button>
</div>
<div class="mode-selector">
<div class="mode-option active">自动路径</div>
<div class="mode-option">手动遥控</div>
</div>
</div>
</div>
<!-- 底部面板 - 水文信息 -->
<!-- 修改底部面板 - 水文信息部分 -->
<div class="bottom-panel">
<div class="hydro-card">
<div class="hydro-title">洋流监测</div>
<div class="current-visualization">
<img src="/img/洋流.png" style="width: 100%;">
</div>
<div class="status-value">流速: 0.35 m/s | 方向: 125°</div>
</div>
<div class="hydro-card">
<div class="hydro-title">洋流态势图</div>
<div class="current-map" id="current-map"></div>
</div>
<div class="hydro-card">
<div class="hydro-title">实时数据流</div>
<div class="data-log">
<div class="log-entry">
<span class="log-time">14:28:05</span> 集矿机 #01启动自动路径跟踪模式
</div>
<div class="log-entry">
<span class="log-time">14:25:32</span> ROV #02检测到生物群落已自动避让
</div>
<div class="log-entry">
<span class="log-time">14:22:18</span> 集矿机 #01采集速率稳定在120吨/小时
</div>
<div class="log-entry">
<span class="log-time">14:20:45</span> 系统:检测到洋流变化,已自动调整
</div>
<div class="log-entry">
<span class="log-time">14:18:30</span> 水面船:收到矿石提升请求,准备启动
</div>
<div class="log-entry">
<span class="log-time">14:15:12</span> 集矿机 #01完成第45号采集区块
</div>
</div>
</div>
</div>
</div>
<!-- 替换现有的标签页切换功能 -->
<script>
// 存储图表实例
let miningRateChart, systemHealthChart, environmentChart, progressChart, efficiencyChart;
let chartInitialized = false;
// 初始化实时数据更新
function initRealTimeData() {
// 初始化图表
initCharts();
// 每3秒更新一次数据
setInterval(updateRealTimeData, 3000);
}
// 销毁已存在的图表
function destroyChart(chart) {
if (chart) {
chart.destroy();
return null;
}
return chart;
}
// 初始化图表
function initCharts() {
// 如果已经初始化过,先销毁所有图表
if (chartInitialized) {
miningRateChart = destroyChart(miningRateChart);
systemHealthChart = destroyChart(systemHealthChart);
environmentChart = destroyChart(environmentChart);
progressChart = destroyChart(progressChart);
efficiencyChart = destroyChart(efficiencyChart);
}
const now = new Date();
const timeLabels = [];
for (let i = 6; i >= 0; i--) {
const time = new Date(now);
time.setMinutes(time.getMinutes() - i * 5);
timeLabels.push(`${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`);
}
// 采矿速率图表
const miningRateCtx = document.getElementById('mining-rate-chart').getContext('2d');
miningRateChart = new Chart(miningRateCtx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [{
label: '采矿速率 (吨/小时)',
data: [118, 119, 120, 121, 120, 122, 120],
borderColor: '#4fc3f7',
backgroundColor: 'rgba(79, 195, 247, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
color: '#a0d2ff',
maxRotation: 0,
autoSkip: false,
font: {
size: 10
}
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff',
callback: function (value) {
return value + ' 吨';
}
}
}
}
}
});
// 系统健康度图表
const systemHealthCtx = document.getElementById('system-health-chart').getContext('2d');
systemHealthChart = new Chart(systemHealthCtx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [{
label: '系统健康度 (%)',
data: [93.2, 93.0, 92.8, 92.5, 92.7, 92.6, 92.5],
borderColor: '#66bb6a',
backgroundColor: 'rgba(102, 187, 106, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
color: '#a0d2ff',
maxRotation: 0,
autoSkip: false,
font: {
size: 10
}
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff',
callback: function (value) {
return value + '%';
}
}
}
}
}
});
// 环境参数图表
const environmentCtx = document.getElementById('environment-chart').getContext('2d');
environmentChart = new Chart(environmentCtx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [{
label: '温度 (°C)',
data: [2.0, 2.1, 2.1, 2.0, 2.1, 2.1, 2.1],
borderColor: '#ff9800',
backgroundColor: 'rgba(255, 152, 0, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
color: '#a0d2ff',
maxRotation: 0,
autoSkip: false,
font: {
size: 10
}
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff',
callback: function (value) {
return value + '°C';
}
}
}
}
}
});
// 采集进度图表
const progressCtx = document.getElementById('progress-chart').getContext('2d');
progressChart = new Chart(progressCtx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [{
label: '采集进度 (%)',
data: [18.2, 18.3, 18.3, 18.4, 18.4, 18.5, 18.5],
borderColor: '#ba68c8',
backgroundColor: 'rgba(186, 104, 200, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
color: '#a0d2ff',
maxRotation: 0,
autoSkip: false,
font: {
size: 10
}
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff',
callback: function (value) {
return value + '%';
}
}
}
}
}
});
// 采矿效率趋势图表
const efficiencyLabels = [];
for (let i = 6; i >= 0; i--) {
const time = new Date(now);
time.setHours(time.getHours() - i * 2);
efficiencyLabels.push(`${time.getHours().toString().padStart(2, '0')}:00`);
}
const efficiencyCtx = document.getElementById('efficiency-trend-chart').getContext('2d');
efficiencyChart = new Chart(efficiencyCtx, {
type: 'line',
data: {
labels: efficiencyLabels,
datasets: [{
label: '采矿效率 (吨/小时)',
data: [115, 117, 119, 120, 122, 121, 120],
borderColor: '#4fc3f7',
backgroundColor: 'rgba(79, 195, 247, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff',
callback: function (value) {
return value + ' 吨';
}
}
}
}
}
});
chartInitialized = true;
}
// 更新实时数据
function updateRealTimeData() {
const now = new Date();
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
// 生成随机变化值
const miningRateChange = (Math.random() - 0.5) * 4;
const systemHealthChange = (Math.random() - 0.5) * 0.5;
const environmentChange = (Math.random() - 0.5) * 0.2;
const progressChange = (Math.random() - 0.3) * 0.2;
// 更新图表数据
updateChartData(miningRateChart, miningRateChange, 110, 130);
updateChartData(systemHealthChart, systemHealthChange, 90, 95);
updateChartData(environmentChart, environmentChange, 1.5, 3.0);
updateChartData(progressChart, progressChange, 15, 25);
updateChartData(efficiencyChart, miningRateChange * 0.1, 110, 130);
// 更新显示值和趋势
updateCardValues(miningRateChange, systemHealthChange, environmentChange, progressChange);
}
// 更新图表数据
function updateChartData(chart, change, min, max) {
if (!chart) return;
const data = chart.data.datasets[0].data;
const lastValue = data[data.length - 1];
const newValue = Math.min(max, Math.max(min, lastValue + change));
// 移除第一个数据点,添加新数据点
data.shift();
data.push(newValue);
// 更新标签
const labels = chart.data.labels;
labels.shift();
const now = new Date();
labels.push(`${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`);
chart.update();
}
// 更新卡片显示值
function updateCardValues(miningRateChange, systemHealthChange, environmentChange, progressChange) {
// 采矿速率
const miningRateElement = document.getElementById('mining-rate-value');
const miningRateTrendElement = document.getElementById('mining-rate-trend');
const currentMiningRate = parseFloat(miningRateElement.textContent);
const newMiningRate = Math.max(110, Math.min(130, currentMiningRate + miningRateChange));
miningRateElement.textContent = newMiningRate.toFixed(1) + ' 吨/小时';
miningRateTrendElement.textContent = miningRateChange >= 0 ? `${Math.abs(miningRateChange).toFixed(1)}%` : `${Math.abs(miningRateChange).toFixed(1)}%`;
miningRateTrendElement.className = miningRateChange >= 0 ? 'data-card-trend trend-up' : 'data-card-trend trend-down';
// 系统健康度
const systemHealthElement = document.getElementById('system-health-value');
const systemHealthTrendElement = document.getElementById('system-health-trend');
const currentHealth = parseFloat(systemHealthElement.textContent);
const newHealth = Math.max(90, Math.min(95, currentHealth + systemHealthChange));
systemHealthElement.textContent = newHealth.toFixed(1) + '%';
systemHealthTrendElement.textContent = systemHealthChange >= 0 ? `${Math.abs(systemHealthChange).toFixed(1)}%` : `${Math.abs(systemHealthChange).toFixed(1)}%`;
systemHealthTrendElement.className = systemHealthChange >= 0 ? 'data-card-trend trend-up' : 'data-card-trend trend-down';
// 环境参数
const environmentElement = document.getElementById('environment-value');
const environmentTrendElement = document.getElementById('environment-trend');
const currentEnvironment = parseFloat(environmentElement.textContent);
const newEnvironment = Math.max(1.5, Math.min(3.0, currentEnvironment + environmentChange));
environmentElement.textContent = newEnvironment.toFixed(1) + '°C';
environmentTrendElement.textContent = Math.abs(environmentChange) < 0.05 ? '稳定' : (environmentChange >= 0 ? `${Math.abs(environmentChange).toFixed(1)}°C` : `${Math.abs(environmentChange).toFixed(1)}°C`);
environmentTrendElement.className = Math.abs(environmentChange) < 0.05 ? 'data-card-trend' : (environmentChange >= 0 ? 'data-card-trend trend-up' : 'data-card-trend trend-down');
// 采集进度
const progressElement = document.getElementById('progress-value');
const progressTrendElement = document.getElementById('progress-trend');
const currentProgress = parseFloat(progressElement.textContent);
const newProgress = Math.max(15, Math.min(25, currentProgress + progressChange));
progressElement.textContent = newProgress.toFixed(1) + '%';
progressTrendElement.textContent = progressChange >= 0 ? `${Math.abs(progressChange).toFixed(1)}%` : `${Math.abs(progressChange).toFixed(1)}%`;
progressTrendElement.className = progressChange >= 0 ? 'data-card-trend trend-up' : 'data-card-trend trend-down';
}
// 页面加载完成后初始化实时数据
document.addEventListener('DOMContentLoaded', function () {
// 确保在DOM完全加载后再初始化
setTimeout(function () {
initRealTimeData();
}, 100);
});
// 左侧面板标签页切换功能
function initLeftPanelTabs() {
document.querySelectorAll('.left-panel .tab').forEach(tab => {
tab.addEventListener('click', function () {
// 只影响左侧面板的标签和内容
document.querySelectorAll('.left-panel .tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.left-panel .tab-pane').forEach(p => p.classList.remove('active'));
this.classList.add('active');
const tabId = this.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
});
});
}
// 中央面板标签页切换功能
function initMainViewTabs() {
document.querySelectorAll('.main-view .tab').forEach(tab => {
tab.addEventListener('click', function () {
// 只影响中央面板的标签和内容
document.querySelectorAll('.main-view .tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.main-view .tab-pane').forEach(p => p.classList.remove('active'));
this.classList.add('active');
const tabId = this.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
});
});
}
// 模式选择器切换
document.querySelectorAll('.mode-option').forEach(option => {
option.addEventListener('click', function () {
document.querySelectorAll('.mode-option').forEach(o => o.classList.remove('active'));
this.classList.add('active');
});
});
// 初始化图表
document.addEventListener('DOMContentLoaded', function () {
// // 初始化时间并每秒更新
// updateTime();
// setInterval(updateTime, 1000);
// 初始化标签页切换功能
initLeftPanelTabs();
initMainViewTabs();
// 采矿速率图表
const miningRateCtx = document.getElementById('mining-rate-chart').getContext('2d');
new Chart(miningRateCtx, {
type: 'line',
data: {
labels: ['14:00', '14:05', '14:10', '14:15', '14:20', '14:25', '14:30'],
datasets: [{
label: '采矿速率 (吨/小时)',
data: [115, 118, 122, 119, 121, 120, 120],
borderColor: '#4fc3f7',
backgroundColor: 'rgba(79, 195, 247, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
}
}
}
});
// 系统健康度图表
const systemHealthCtx = document.getElementById('system-health-chart').getContext('2d');
new Chart(systemHealthCtx, {
type: 'line',
data: {
labels: ['14:00', '14:05', '14:10', '14:15', '14:20', '14:25', '14:30'],
datasets: [{
label: '系统健康度 (%)',
data: [94, 93.5, 93, 92.8, 92.5, 92.5, 92.5],
borderColor: '#66bb6a',
backgroundColor: 'rgba(102, 187, 106, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
}
}
}
});
// 环境参数图表
const environmentCtx = document.getElementById('environment-chart').getContext('2d');
new Chart(environmentCtx, {
type: 'line',
data: {
labels: ['14:00', '14:05', '14:10', '14:15', '14:20', '14:25', '14:30'],
datasets: [{
label: '温度 (°C)',
data: [2.0, 2.1, 2.1, 2.1, 2.1, 2.1, 2.1],
borderColor: '#ff9800',
backgroundColor: 'rgba(255, 152, 0, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
}
}
}
});
// 采集进度图表
const progressCtx = document.getElementById('progress-chart').getContext('2d');
new Chart(progressCtx, {
type: 'line',
data: {
labels: ['14:00', '14:05', '14:10', '14:15', '14:20', '14:25', '14:30'],
datasets: [{
label: '采集进度 (%)',
data: [17.8, 17.9, 18.0, 18.1, 18.3, 18.4, 18.5],
borderColor: '#ba68c8',
backgroundColor: 'rgba(186, 104, 200, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
}
}
}
});
// 采矿效率趋势图表
const efficiencyCtx = document.getElementById('efficiency-trend-chart').getContext('2d');
new Chart(efficiencyCtx, {
type: 'line',
data: {
labels: ['08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00'],
datasets: [{
label: '采矿效率 (吨/小时)',
data: [110, 115, 118, 120, 122, 121, 119],
borderColor: '#4fc3f7',
backgroundColor: 'rgba(79, 195, 247, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
}
}
}
});
// 资源分布图
const resourceMapCtx = document.getElementById('resourceMapCanvas').getContext('2d');
new Chart(resourceMapCtx, {
type: 'bubble',
data: {
datasets: [{
label: '资源分布',
data: [{
x: 10,
y: 20,
r: 15
}, {
x: 25,
y: 30,
r: 10
}, {
x: 40,
y: 15,
r: 20
}, {
x: 55,
y: 25,
r: 12
}, {
x: 70,
y: 35,
r: 18
}],
backgroundColor: 'rgba(79, 195, 247, 0.5)',
borderColor: '#4fc3f7',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
min: 0,
max: 100,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
},
y: {
min: 0,
max: 100,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
}
}
}
});
// 地质剖面图
const geologyCtx = document.getElementById('geologyChart').getContext('2d');
new Chart(geologyCtx, {
type: 'line',
data: {
labels: ['0km', '5km', '10km', '15km', '20km'],
datasets: [{
label: '海床高度',
data: [4500, 4485, 4490, 4480, 4495],
borderColor: '#4fc3f7',
backgroundColor: 'rgba(79, 195, 247, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
},
y: {
reverse: true,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
}
}
}
});
// 金属含量分析图
const metalCtx = document.getElementById('metalAnalysisChart').getContext('2d');
new Chart(metalCtx, {
type: 'bar',
data: {
labels: ['Cu', 'Ni', 'Co', 'Mn'],
datasets: [{
label: '金属含量 (%)',
data: [1.2, 0.8, 0.3, 22.4],
backgroundColor: [
'rgba(79, 195, 247, 0.7)',
'rgba(102, 187, 106, 0.7)',
'rgba(255, 152, 0, 0.7)',
'rgba(186, 104, 200, 0.7)'
],
borderColor: [
'#4fc3f7',
'#66bb6a',
'#ff9800',
'#ba68c8'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#a0d2ff'
}
}
}
}
});
// 初始化洋流态势图
setTimeout(initCurrentMap, 1000);
});
// 初始化洋流态势图
function initCurrentMap() {
// 确保元素存在
const mapContainer = document.getElementById('current-map');
if (!mapContainer) return;
// 清空容器
mapContainer.innerHTML = '';
// 创建leaflet地图 121.62,38.18
const map = L.map('current-map', {
center: [38.18, 121.62],
zoom: 7,
minZoom: 4,
maxZoom: 10,
zoomControl: false,
attributionControl: false,
dragging: false,
touchZoom: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
keyboard: false
});
// 设置地图边界
map.setMaxBounds([[15, 104], [42, 131]]);
L.tileLayer(
'http://t{s}.tianditu.com/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=489fa3b53cd351877dcf69e9ed899a04',
//'http://t{s}.tianditu.gov.cn/DataServer?T=ter_w&x={x}&y={y}&l={z}&tk=cd7516c53e2e5bee9bad989b63db6ce4',
{ attribution: '天地图地形', subdomains: [0, 1, 2, 3, 4, 5, 6, 7] },
).addTo(map);
// // 添加一个空图层作为背景
// L.tileLayer('', {
// opacity: 0
// }).addTo(map);
// 加载洋流数据
loadCurrentData(map);
}
function loadCurrentData(map) {
try {
// 解析seacurrentdata.js中的数据
if (typeof seaCurrentData !== 'undefined' && seaCurrentData.data) {
// 解析数据
const parsedData = JSON.parse(seaCurrentData.data);
const rawDataArray = parsedData.dataJson;
// 验证数据是否存在且格式正确
if (rawDataArray && rawDataArray.length >= 2) {
const u = rawDataArray[0];
const v = rawDataArray[1];
// 验证数据组件是否存在且有data和header属性
if (u && v && Array.isArray(u.data) && Array.isArray(v.data) && u.header && v.header) {
// 转换为leaflet-velocity需要的格式
const velocityData = [
{
header: {
lo1: u.header.lo1,
la1: u.header.la1,
dx: u.header.dx,
dy: u.header.dy,
nx: u.header.nx,
ny: u.header.ny,
parameterCategory: 2,
parameterNumber: 2,
refTime: Date.now(),
},
data: u.data,
},
{
header: {
lo1: v.header.lo1,
la1: v.header.la1,
dx: v.header.dx,
dy: v.header.dy,
nx: v.header.nx,
ny: v.header.ny,
parameterCategory: 2,
parameterNumber: 3,
refTime: Date.now(),
},
data: v.data,
},
];
// 创建velocity图层配置
const options = {
displayValues: false,
displayOptions: {
velocityType: '洋流',
position: 'bottomleft',
emptyString: '无洋流数据',
},
maxVelocity: 1.5,
velocityScale: 0.04,
colorScale: [
"rgba(36,104, 180, 0.8)",
"rgba(60,157, 194, 0.8)",
"rgba(128,205,193, 0.8)",
"rgba(151,218,168, 0.8)",
"rgba(198,231,181, 0.8)",
"rgba(238,247,217, 0.8)",
"rgba(255,238,159, 0.8)",
"rgba(252,217,125, 0.8)",
"rgba(255,182,100, 0.8)",
"rgba(252,150,75, 0.8)",
"rgba(250,112,52, 0.8)"
],
lineWidth: 3,
particleMultiplier: 1 / 100,
opacity: 0.9,
frameRate: 200,
animated: true,
};
const velocityLayer = L.velocityLayer({
...options,
data: velocityData,
});
velocityLayer.addTo(map);
console.log('洋流图层已添加到地图');
// 添加标量图层
let config = {};
config = { ...config, minValue: 0.001, maxValue: 2 };
// 使用正确的ScalarLayer实现
const intensityLayer = L.scalarLayer({
data: rawDataArray,
displayValues: false,
displayOptions: {
velocityType: '',
position: '',
emptyString: '',
},
paneName: 'scalarPane',
...config,
}).addTo(map);
return;
} else {
console.error('洋流数据组件格式不正确缺少data或header属性');
}
} else {
console.error('洋流数据格式不正确,缺少足够的数据组件');
}
} else {
console.error('未找到洋流数据');
}
// 如果没有数据或数据格式错误,则创建示例粒子动画
createSampleParticles(map);
} catch (e) {
console.error('加载洋流数据出错:', e);
// 出错时创建示例粒子动画
createSampleParticles(map);
}
}
// 修复createSampleParticles函数中的问题
function createSampleParticles(map) {
// 创建canvas图层用于绘制粒子
const particleLayer = L.canvasLayer().delegate(this);
particleLayer.onDrawLayer = function () {
const canvas = particleLayer._canvas;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// 清除画布
ctx.clearRect(0, 0, width, height);
// 设置粒子样式
ctx.strokeStyle = 'rgba(79, 195, 247, 0.7)';
ctx.lineWidth = 2;
// 绘制流动的粒子效果
const time = Date.now() / 1000;
for (let i = 0; i < 80; i++) {
const x = (Math.sin(time * 0.8 + i * 0.2) * 0.4 + 0.5) * width;
const y = (Math.cos(time * 0.6 + i * 0.3) * 0.4 + 0.5) * height;
// 绘制粒子轨迹
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(
x + Math.cos(time + i) * 12,
y + Math.sin(time * 1.3 + i) * 12
);
ctx.stroke();
}
// 持续动画
requestAnimationFrame(() => particleLayer.needRedraw());
};
// 添加图层到地图
particleLayer.addTo(map);
// 启动动画
particleLayer.needRedraw();
console.log('示例粒子动画已启动');
}
</script>
</body>
</html>