|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>风速剖面三维图</title>
|
|
|
<style>
|
|
|
body {
|
|
|
margin: 0;
|
|
|
overflow: hidden;
|
|
|
font-family: Arial, sans-serif;
|
|
|
background: linear-gradient(to bottom, #1a237e, #283593);
|
|
|
color: white;
|
|
|
}
|
|
|
#container {
|
|
|
position: relative;
|
|
|
width: 100vw;
|
|
|
height: 100vh;
|
|
|
}
|
|
|
#info-panel {
|
|
|
position: absolute;
|
|
|
top: 20px;
|
|
|
left: 20px;
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
padding: 15px;
|
|
|
border-radius: 8px;
|
|
|
max-width: 300px;
|
|
|
z-index: 100;
|
|
|
}
|
|
|
#controls {
|
|
|
position: absolute;
|
|
|
bottom: 20px;
|
|
|
left: 20px;
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
padding: 15px;
|
|
|
border-radius: 8px;
|
|
|
z-index: 100;
|
|
|
}
|
|
|
button {
|
|
|
background: #3949ab;
|
|
|
color: white;
|
|
|
border: none;
|
|
|
padding: 8px 15px;
|
|
|
margin: 5px;
|
|
|
border-radius: 4px;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
button:hover {
|
|
|
background: #5c6bc0;
|
|
|
}
|
|
|
.axis-label {
|
|
|
position: absolute;
|
|
|
color: white;
|
|
|
font-size: 14px;
|
|
|
z-index: 10;
|
|
|
}
|
|
|
#time-label {
|
|
|
bottom: 50px;
|
|
|
left: 50%;
|
|
|
transform: translateX(-50%);
|
|
|
}
|
|
|
#height-label {
|
|
|
top: 50%;
|
|
|
left: 30px;
|
|
|
transform: translateY(-50%) rotate(-90deg);
|
|
|
}
|
|
|
#wind-label {
|
|
|
top: 20px;
|
|
|
right: 30px;
|
|
|
}
|
|
|
.legend {
|
|
|
position: absolute;
|
|
|
right: 20px;
|
|
|
bottom: 100px;
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
padding: 10px;
|
|
|
border-radius: 8px;
|
|
|
width: 150px;
|
|
|
}
|
|
|
.legend-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
margin: 5px 0;
|
|
|
}
|
|
|
.legend-color {
|
|
|
width: 20px;
|
|
|
height: 20px;
|
|
|
margin-right: 10px;
|
|
|
border-radius: 3px;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<div id="container">
|
|
|
<div id="info-panel">
|
|
|
<h3>风速剖面数据</h3>
|
|
|
<p>日期: 2018-10-05 03:00:00</p>
|
|
|
<p>数据来源: CMA</p>
|
|
|
<p>参数: CAMP, MULTI, BRAZY, HONOR, PERSONAL, SERVICE, VOLUME, VARIABILITY</p>
|
|
|
</div>
|
|
|
|
|
|
<div id="controls">
|
|
|
<button id="reset-view">重置视图</button>
|
|
|
<button id="auto-rotate">自动旋转</button>
|
|
|
<button id="toggle-labels">切换标签</button>
|
|
|
</div>
|
|
|
|
|
|
<div class="axis-label" id="time-label">时间轴</div>
|
|
|
<div class="axis-label" id="height-label">高度轴 (m)</div>
|
|
|
<div class="axis-label" id="wind-label">风速 (m/s)</div>
|
|
|
|
|
|
<div class="legend">
|
|
|
<h4>风速图例</h4>
|
|
|
<div class="legend-item">
|
|
|
<div class="legend-color" style="background-color: #00b0ff;"></div>
|
|
|
<span>0-5 m/s</span>
|
|
|
</div>
|
|
|
<div class="legend-item">
|
|
|
<div class="legend-color" style="background-color: #00e676;"></div>
|
|
|
<span>5-10 m/s</span>
|
|
|
</div>
|
|
|
<div class="legend-item">
|
|
|
<div class="legend-color" style="background-color: #ffeb3b;"></div>
|
|
|
<span>10-15 m/s</span>
|
|
|
</div>
|
|
|
<div class="legend-item">
|
|
|
<div class="legend-color" style="background-color: #ff9800;"></div>
|
|
|
<span>15-20 m/s</span>
|
|
|
</div>
|
|
|
<div class="legend-item">
|
|
|
<div class="legend-color" style="background-color: #f44336;"></div>
|
|
|
<span>20+ m/s</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
|
|
<script>
|
|
|
// 初始化场景
|
|
|
const scene = new THREE.Scene();
|
|
|
scene.background = new THREE.Color(0x1a237e);
|
|
|
|
|
|
// 初始化相机 - 调整视角以更好地查看贴地效果
|
|
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
|
camera.position.set(20, 15, 30);
|
|
|
|
|
|
// 初始化渲染器
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
|
document.getElementById('container').appendChild(renderer.domElement);
|
|
|
|
|
|
// 添加轨道控制器
|
|
|
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
|
controls.enableDamping = true;
|
|
|
controls.dampingFactor = 0.05;
|
|
|
|
|
|
// 添加光源
|
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
|
scene.add(ambientLight);
|
|
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
|
directionalLight.position.set(50, 50, 50);
|
|
|
scene.add(directionalLight);
|
|
|
|
|
|
// 创建坐标轴
|
|
|
const axesHelper = new THREE.AxesHelper(20);
|
|
|
scene.add(axesHelper);
|
|
|
|
|
|
// 生成模拟风速数据
|
|
|
function generateWindData() {
|
|
|
const data = [];
|
|
|
const timePoints = 24; // 24小时
|
|
|
const heightPoints = 20; // 20个高度点
|
|
|
|
|
|
for (let t = 0; t < timePoints; t++) {
|
|
|
const timeData = [];
|
|
|
for (let h = 0; h < heightPoints; h++) {
|
|
|
// 模拟风速数据,随时间和高度变化
|
|
|
const baseWind = 5 + Math.sin(t / 3) * 3;
|
|
|
const heightEffect = h * 0.5;
|
|
|
const turbulence = Math.sin(t * 0.5 + h * 0.3) * 2;
|
|
|
const windSpeed = baseWind + heightEffect + turbulence;
|
|
|
|
|
|
timeData.push(Math.max(0, windSpeed));
|
|
|
}
|
|
|
data.push(timeData);
|
|
|
}
|
|
|
|
|
|
return data;
|
|
|
}
|
|
|
|
|
|
// 根据风速获取颜色
|
|
|
function getWindColor(speed) {
|
|
|
if (speed < 5) return new THREE.Color(0x00b0ff);
|
|
|
if (speed < 10) return new THREE.Color(0x00e676);
|
|
|
if (speed < 15) return new THREE.Color(0xffeb3b);
|
|
|
if (speed < 20) return new THREE.Color(0xff9800);
|
|
|
return new THREE.Color(0xf44336);
|
|
|
}
|
|
|
|
|
|
// 创建风速剖面可视化
|
|
|
function createWindProfile(data) {
|
|
|
const group = new THREE.Group();
|
|
|
|
|
|
// 创建地面网格平面作为参考
|
|
|
const gridHelper = new THREE.GridHelper(30, 24, 0x444444, 0x222222);
|
|
|
scene.add(gridHelper);
|
|
|
|
|
|
// 创建风速曲面
|
|
|
const geometry = new THREE.BufferGeometry();
|
|
|
const vertices = [];
|
|
|
const colors = [];
|
|
|
const indices = [];
|
|
|
|
|
|
const timePoints = data.length;
|
|
|
const heightPoints = data[0].length;
|
|
|
|
|
|
// 创建顶点和颜色 - 调整坐标系使图形贴合地面
|
|
|
for (let t = 0; t < timePoints; t++) {
|
|
|
for (let h = 0; h < heightPoints; h++) {
|
|
|
const x = t - timePoints / 2; // 时间轴
|
|
|
const z = h * 1.5; // 高度轴 (沿Z轴向上)
|
|
|
const y = data[t][h] * 0.5; // 风速轴 (沿Y轴延伸,贴近地面)
|
|
|
|
|
|
vertices.push(x, y, z);
|
|
|
|
|
|
const color = getWindColor(data[t][h]);
|
|
|
colors.push(color.r, color.g, color.b);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 创建索引(三角形)
|
|
|
for (let t = 0; t < timePoints - 1; t++) {
|
|
|
for (let h = 0; h < heightPoints - 1; h++) {
|
|
|
const a = t * heightPoints + h;
|
|
|
const b = t * heightPoints + h + 1;
|
|
|
const c = (t + 1) * heightPoints + h;
|
|
|
const d = (t + 1) * heightPoints + h + 1;
|
|
|
|
|
|
indices.push(a, b, d);
|
|
|
indices.push(a, d, c);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
geometry.setIndex(indices);
|
|
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
|
|
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
|
|
|
|
|
// 计算法向量以获得更好的光照效果
|
|
|
geometry.computeVertexNormals();
|
|
|
|
|
|
const material = new THREE.MeshLambertMaterial({
|
|
|
vertexColors: true,
|
|
|
side: THREE.DoubleSide,
|
|
|
transparent: true,
|
|
|
opacity: 0.8
|
|
|
});
|
|
|
|
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
|
group.add(mesh);
|
|
|
|
|
|
// 添加数据点标记
|
|
|
for (let t = 0; t < timePoints; t += 3) {
|
|
|
for (let h = 0; h < heightPoints; h += 3) {
|
|
|
const x = t - timePoints / 2;
|
|
|
const z = h * 1.5;
|
|
|
const y = data[t][h] * 0.5;
|
|
|
|
|
|
const sphereGeometry = new THREE.SphereGeometry(0.2, 8, 8);
|
|
|
const sphereMaterial = new THREE.MeshBasicMaterial({
|
|
|
color: getWindColor(data[t][h])
|
|
|
});
|
|
|
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
|
|
|
sphere.position.set(x, y, z);
|
|
|
group.add(sphere);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
scene.add(group);
|
|
|
return group;
|
|
|
}
|
|
|
|
|
|
// 生成数据并创建可视化
|
|
|
const windData = generateWindData();
|
|
|
const windProfile = createWindProfile(windData);
|
|
|
|
|
|
// 添加坐标轴标签
|
|
|
function addAxisLabels() {
|
|
|
// 时间轴标签
|
|
|
for (let i = 0; i <= 24; i += 6) {
|
|
|
const text = document.createElement('div');
|
|
|
text.className = 'axis-label';
|
|
|
text.style.position = 'absolute';
|
|
|
text.style.bottom = '30px';
|
|
|
text.style.left = `${50 + (i - 12) * 10}px`;
|
|
|
text.textContent = `${i}:00`;
|
|
|
document.getElementById('container').appendChild(text);
|
|
|
}
|
|
|
|
|
|
// 高度轴标签
|
|
|
for (let i = 0; i <= 30; i += 10) {
|
|
|
const text = document.createElement('div');
|
|
|
text.className = 'axis-label';
|
|
|
text.style.position = 'absolute';
|
|
|
text.style.top = `${50 + i * 5}px`;
|
|
|
text.style.left = '10px';
|
|
|
text.textContent = `${i * 100}m`;
|
|
|
document.getElementById('container').appendChild(text);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
addAxisLabels();
|
|
|
|
|
|
// 动画循环
|
|
|
function animate() {
|
|
|
requestAnimationFrame(animate);
|
|
|
controls.update();
|
|
|
renderer.render(scene, camera);
|
|
|
}
|
|
|
|
|
|
animate();
|
|
|
|
|
|
// 窗口大小调整
|
|
|
window.addEventListener('resize', () => {
|
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
|
camera.updateProjectionMatrix();
|
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
|
});
|
|
|
|
|
|
// 控制按钮功能
|
|
|
document.getElementById('reset-view').addEventListener('click', () => {
|
|
|
controls.reset();
|
|
|
});
|
|
|
|
|
|
let autoRotate = false;
|
|
|
document.getElementById('auto-rotate').addEventListener('click', () => {
|
|
|
autoRotate = !autoRotate;
|
|
|
controls.autoRotate = autoRotate;
|
|
|
document.getElementById('auto-rotate').textContent =
|
|
|
autoRotate ? '停止旋转' : '自动旋转';
|
|
|
});
|
|
|
|
|
|
let labelsVisible = true;
|
|
|
document.getElementById('toggle-labels').addEventListener('click', () => {
|
|
|
labelsVisible = !labelsVisible;
|
|
|
const labels = document.querySelectorAll('.axis-label');
|
|
|
labels.forEach(label => {
|
|
|
if (!label.id) { // 只切换数据标签,不切换轴标题
|
|
|
label.style.display = labelsVisible ? 'block' : 'none';
|
|
|
}
|
|
|
});
|
|
|
document.getElementById('toggle-labels').textContent =
|
|
|
labelsVisible ? '隐藏标签' : '显示标签';
|
|
|
});
|
|
|
</script>
|
|
|
</body>
|
|
|
</html> |