|
|
<!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 hλ = λ < 0 ? H : -H
|
|
|
var hφ = φ < 0 ? H : -H
|
|
|
var pλ = projection([λ + hλ, φ])
|
|
|
var pφ = projection([λ, φ + hφ]) // 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 [(pλ[0] - x) / hλ / k, (pλ[1] - y) / hλ / k, (pφ[0] - x) / hφ, (pφ[1] - y) / hφ]
|
|
|
}
|
|
|
/**
|
|
|
* 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> |