Compare commits
22 Commits
b06cb4606f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49a9c33836 | ||
|
|
96222b9e4c | ||
|
|
3fcbae55dc | ||
|
|
3e3090d72a | ||
|
|
4f922f13d1 | ||
|
|
bb6b18fe3b | ||
|
|
0ecc1bc537 | ||
|
|
869d661a94 | ||
|
|
d18e400fcb | ||
|
|
6fabbcfe5c | ||
|
|
1189fec014 | ||
|
|
82f7aa29a6 | ||
|
|
777891f865 | ||
|
|
c2eba54da0 | ||
|
|
f50830712c | ||
|
|
e21b783bef | ||
|
|
11a9dda942 | ||
|
|
3b0e9dec5a | ||
|
|
c82e1d5a04 | ||
|
|
02991730e5 | ||
|
|
4e487b315a | ||
|
|
948af2c88f |
165
.sisyphus/plans/earth-architecture-refactor.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# 地球3D可视化架构重构计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
当前 `frontend/public/earth` 3D地球可视化系统基于 Three.js 构建,未来需要迁移到 Unreal Engine (Cesium)。为降低迁移成本,需要提前做好**逻辑与渲染分离**的架构设计。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
- 将线缆高亮逻辑与渲染实现分离
|
||||||
|
- 保持交互逻辑可复用,只需重写渲染层
|
||||||
|
- 为后续迁移到 UE/Cesium 做好准备
|
||||||
|
|
||||||
|
## 已完成
|
||||||
|
|
||||||
|
### 1. 状态枚举定义 (constants.js)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const CABLE_STATE = {
|
||||||
|
NORMAL: 'normal',
|
||||||
|
HOVERED: 'hovered',
|
||||||
|
LOCKED: 'locked'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 线缆状态管理 (cables.js - 数据层)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const cableStates = new Map();
|
||||||
|
|
||||||
|
export function getCableState(cableId) { ... }
|
||||||
|
export function setCableState(cableId, state) { ... }
|
||||||
|
export function clearAllCableStates() { ... }
|
||||||
|
export function getCableStateInfo() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 逻辑层调用 (main.js)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 悬停
|
||||||
|
setCableState(cable.userData.cableId, CABLE_STATE.HOVERED);
|
||||||
|
|
||||||
|
// 锁定
|
||||||
|
setCableState(cableId, CABLE_STATE.LOCKED);
|
||||||
|
|
||||||
|
// 恢复
|
||||||
|
setCableState(cableId, CABLE_STATE.NORMAL);
|
||||||
|
clearAllCableStates();
|
||||||
|
|
||||||
|
// 清除锁定时
|
||||||
|
clearLockedObject() {
|
||||||
|
hoveredCable = null;
|
||||||
|
clearAllCableStates();
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 渲染层 (main.js - applyCableVisualState)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function applyCableVisualState() {
|
||||||
|
const allCables = getCableLines();
|
||||||
|
const pulse = (Math.sin(Date.now() * CABLE_CONFIG.pulseSpeed) + 1) * 0.5;
|
||||||
|
|
||||||
|
allCables.forEach(c => {
|
||||||
|
const cableId = c.userData.cableId;
|
||||||
|
const state = getCableState(cableId);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case CABLE_STATE.LOCKED:
|
||||||
|
// 呼吸效果 + 白色
|
||||||
|
c.material.opacity = CABLE_CONFIG.lockedOpacityMin + pulse * CABLE_CONFIG.pulseCoefficient;
|
||||||
|
c.material.color.setRGB(1, 1, 1);
|
||||||
|
break;
|
||||||
|
case CABLE_STATE.HOVERED:
|
||||||
|
// 白色高亮
|
||||||
|
c.material.opacity = 1;
|
||||||
|
c.material.color.setRGB(1, 1, 1);
|
||||||
|
break;
|
||||||
|
case CABLE_STATE.NORMAL:
|
||||||
|
default:
|
||||||
|
if (lockedObjectType === 'cable' && lockedObject) {
|
||||||
|
// 其他线缆变暗
|
||||||
|
c.material.opacity = CABLE_CONFIG.otherOpacity;
|
||||||
|
...
|
||||||
|
} else {
|
||||||
|
// 恢复原始
|
||||||
|
c.material.opacity = 1;
|
||||||
|
c.material.color.setHex(c.userData.originalColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 待完成
|
||||||
|
|
||||||
|
### Phase 1: 完善状态配置 (constants.js)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const CABLE_CONFIG = {
|
||||||
|
lockedOpacityMin: 0.6,
|
||||||
|
lockedOpacityMax: 1.0,
|
||||||
|
otherOpacity: 0.5,
|
||||||
|
otherBrightness: 0.6,
|
||||||
|
pulseSpeed: 0.003,
|
||||||
|
pulseCoefficient: 0.4,
|
||||||
|
// 未来可扩展
|
||||||
|
// lockedLineWidth: 3,
|
||||||
|
// normalLineWidth: 1,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: 卫星状态管理 (satellites.js)
|
||||||
|
|
||||||
|
参考线缆状态管理,为卫星添加类似的状态枚举和状态管理函数:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const SATELLITE_STATE = {
|
||||||
|
NORMAL: 'normal',
|
||||||
|
HOVERED: 'hovered',
|
||||||
|
LOCKED: 'locked'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 卫星数据源说明
|
||||||
|
|
||||||
|
- **当前使用**: CelesTrak (https://celestrak.org) - 免费,无需认证
|
||||||
|
- **后续计划**: Space-Track.org (https://space-track.org) - 需要认证,数据更权威
|
||||||
|
- 迁移时只需修改 `satellites.js` 中的数据获取逻辑,状态管理和渲染逻辑不变
|
||||||
|
|
||||||
|
### Phase 3: 统一渲染接口
|
||||||
|
|
||||||
|
将所有对象的渲染逻辑抽象为一个统一的渲染函数:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function applyObjectVisualState() {
|
||||||
|
applyCableVisualState();
|
||||||
|
applySatelliteVisualState();
|
||||||
|
applyLandingPointVisualState();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: UE 迁移准备
|
||||||
|
|
||||||
|
迁移到 Unreal Engine 时:
|
||||||
|
1. 保留 `constants.js` 中的枚举和配置
|
||||||
|
2. 保留 `cables.js` 中的数据层和状态管理
|
||||||
|
3. 保留 `main.js` 中的交互逻辑
|
||||||
|
4. **仅重写** `applyCableVisualState()` 等渲染函数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架构原则
|
||||||
|
|
||||||
|
1. **状态与渲染分离** - 对象状态由数据层管理,渲染层只负责根据状态更新视觉效果
|
||||||
|
2. **逻辑可复用** - 交互逻辑(点击、悬停、锁定)在迁移时应直接复用
|
||||||
|
3. **渲染可替换** - 渲染实现可以针对不同引擎重写,不影响逻辑层
|
||||||
|
|
||||||
|
## 文件变更记录
|
||||||
|
|
||||||
|
| 日期 | 文件 | 变更 |
|
||||||
|
|------|------|------|
|
||||||
|
| 2026-03-19 | constants.js | 新增 CABLE_STATE 枚举 |
|
||||||
|
| 2026-03-19 | cables.js | 新增状态管理函数 |
|
||||||
|
| 2026-03-19 | main.js | 使用状态管理,抽象 applyCableVisualState() |
|
||||||
293
.sisyphus/plans/webgl-instancing-satellites.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# WebGL Instancing 卫星渲染优化计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
当前 `satellites.js` 使用 `THREE.Points` 渲染卫星,受限于 WebGL 点渲染性能,只能显示 ~500-1000 颗卫星。
|
||||||
|
需要迁移到真正的 WebGL Instancing 以支持 5000+ 卫星流畅渲染。
|
||||||
|
|
||||||
|
## 技术选型
|
||||||
|
|
||||||
|
| 方案 | 性能 | 改动量 | 维护性 | 推荐 |
|
||||||
|
|------|------|--------|--------|------|
|
||||||
|
| THREE.Points (现状) | ★★☆ | - | - | 基准 |
|
||||||
|
| THREE.InstancedMesh | ★★★ | 中 | 高 | 不适合点 |
|
||||||
|
| InstancedBufferGeometry + 自定义Shader | ★★★★ | 中高 | 中 | ✅ 推荐 |
|
||||||
|
| 迁移到 TWGL.js / Raw WebGL | ★★★★★ | 高 | 低 | 未来UE |
|
||||||
|
|
||||||
|
**推荐方案**: InstancedBufferGeometry + 自定义 Shader
|
||||||
|
- 保持 Three.js 架构
|
||||||
|
- 复用 satellite.js 数据层
|
||||||
|
- 性能接近原生 WebGL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: 调研与原型
|
||||||
|
|
||||||
|
### 1.1 分析现有架构
|
||||||
|
|
||||||
|
**现状 (satellites.js)**:
|
||||||
|
```javascript
|
||||||
|
// 创建点云
|
||||||
|
const pointsGeometry = new THREE.BufferGeometry();
|
||||||
|
pointsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||||
|
pointsGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||||
|
|
||||||
|
const pointsMaterial = new THREE.PointsMaterial({
|
||||||
|
size: 2,
|
||||||
|
vertexColors: true,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.8,
|
||||||
|
sizeAttenuation: true
|
||||||
|
});
|
||||||
|
|
||||||
|
satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial);
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**: 每个卫星作为一个顶点,GPU 需要处理 ~500 个 draw calls (取决于视锥体裁剪)
|
||||||
|
|
||||||
|
### 1.2 Instanced Rendering 原理
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 目标:单次 draw call 渲染所有卫星
|
||||||
|
// 每个卫星属性:
|
||||||
|
// - position (vec3): 位置
|
||||||
|
// - color (vec3): 颜色
|
||||||
|
// - size (float): 大小 (可选)
|
||||||
|
// - selected (float): 是否选中 (0/1)
|
||||||
|
|
||||||
|
// 使用 InstancedBufferGeometry
|
||||||
|
const geometry = new THREE.InstancedBufferGeometry();
|
||||||
|
geometry.index = originalGeometry.index;
|
||||||
|
geometry.attributes.position = originalGeometry.attributes.position;
|
||||||
|
geometry.attributes.uv = originalGeometry.attributes.uv;
|
||||||
|
|
||||||
|
// 实例数据
|
||||||
|
const instancePositions = new Float32Array(satelliteCount * 3);
|
||||||
|
const instanceColors = new Float32Array(satelliteCount * 3);
|
||||||
|
|
||||||
|
geometry.setAttribute('instancePosition',
|
||||||
|
new THREE.InstancedBufferAttribute(instancePositions, 3));
|
||||||
|
geometry.setAttribute('instanceColor',
|
||||||
|
new THREE.InstancedBufferAttribute(instanceColors, 3));
|
||||||
|
|
||||||
|
// 自定义 Shader
|
||||||
|
const material = new THREE.ShaderMaterial({
|
||||||
|
vertexShader: `
|
||||||
|
attribute vec3 instancePosition;
|
||||||
|
attribute vec3 instanceColor;
|
||||||
|
varying vec3 vColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vColor = instanceColor;
|
||||||
|
vec3 transformed = position + instancePosition;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fragmentShader: `
|
||||||
|
varying vec3 vColor;
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = vec4(vColor, 0.8);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: 实现
|
||||||
|
|
||||||
|
### 2.1 创建 instanced-satellites.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// instanced-satellites.js - Instanced rendering for satellites
|
||||||
|
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { SATELLITE_CONFIG } from './constants.js';
|
||||||
|
|
||||||
|
let instancedMesh = null;
|
||||||
|
let satelliteData = [];
|
||||||
|
let instancePositions = null;
|
||||||
|
let instanceColors = null;
|
||||||
|
let satelliteCount = 0;
|
||||||
|
|
||||||
|
const SATELLITE_VERTEX_SHADER = `
|
||||||
|
attribute vec3 instancePosition;
|
||||||
|
attribute vec3 instanceColor;
|
||||||
|
attribute float instanceSize;
|
||||||
|
|
||||||
|
varying vec3 vColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vColor = instanceColor;
|
||||||
|
vec3 transformed = position * instanceSize + instancePosition;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SATELLITE_FRAGMENT_SHADER = `
|
||||||
|
varying vec3 vColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_FragColor = vec4(vColor, 0.9);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function createInstancedSatellites(scene, earthObj) {
|
||||||
|
// 基础球体几何 (每个卫星是一个小圆点)
|
||||||
|
const baseGeometry = new THREE.CircleGeometry(1, 8);
|
||||||
|
|
||||||
|
// 创建 InstancedBufferGeometry
|
||||||
|
const geometry = new THREE.InstancedBufferGeometry();
|
||||||
|
geometry.index = baseGeometry.index;
|
||||||
|
geometry.attributes.position = baseGeometry.attributes.position;
|
||||||
|
geometry.attributes.uv = baseGeometry.attributes.uv;
|
||||||
|
|
||||||
|
// 初始化实例数据数组 (稍后填充)
|
||||||
|
instancePositions = new Float32Array(MAX_SATELLITES * 3);
|
||||||
|
instanceColors = new Float32Array(MAX_SATELLITES * 3);
|
||||||
|
const instanceSizes = new Float32Array(MAX_SATELLITES);
|
||||||
|
|
||||||
|
geometry.setAttribute('instancePosition',
|
||||||
|
new THREE.InstancedBufferAttribute(instancePositions, 3));
|
||||||
|
geometry.setAttribute('instanceColor',
|
||||||
|
new THREE.InstancedBufferAttribute(instanceColors, 3));
|
||||||
|
geometry.setAttribute('instanceSize',
|
||||||
|
new THREE.InstancedBufferAttribute(instanceSizes, 1));
|
||||||
|
|
||||||
|
const material = new THREE.ShaderMaterial({
|
||||||
|
vertexShader: SATELLITE_VERTEX_SHADER,
|
||||||
|
fragmentShader: SATELLITE_FRAGMENT_SHADER,
|
||||||
|
transparent: true,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
});
|
||||||
|
|
||||||
|
instancedMesh = new THREE.Mesh(geometry, material);
|
||||||
|
instancedMesh.frustumCulled = false; // 我们自己处理裁剪
|
||||||
|
scene.add(instancedMesh);
|
||||||
|
|
||||||
|
return instancedMesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateInstancedSatellites(satellitePositions) {
|
||||||
|
// satellitePositions: Array of { position: Vector3, color: Color }
|
||||||
|
const count = Math.min(satellitePositions.length, MAX_SATELLITES);
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const sat = satellitePositions[i];
|
||||||
|
instancePositions[i * 3] = sat.position.x;
|
||||||
|
instancePositions[i * 3 + 1] = sat.position.y;
|
||||||
|
instancePositions[i * 3 + 2] = sat.position.z;
|
||||||
|
|
||||||
|
instanceColors[i * 3] = sat.color.r;
|
||||||
|
instanceColors[i * 3 + 1] = sat.color.g;
|
||||||
|
instanceColors[i * 3 + 2] = sat.color.b;
|
||||||
|
}
|
||||||
|
|
||||||
|
instancedMesh.geometry.attributes.instancePosition.needsUpdate = true;
|
||||||
|
instancedMesh.geometry.attributes.instanceColor.needsUpdate = true;
|
||||||
|
instancedMesh.geometry.setDrawRange(0, count);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 修改现有 satellites.js
|
||||||
|
|
||||||
|
保持数据层不变,添加新渲染模式:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 添加配置
|
||||||
|
export const SATELLITE_CONFIG = {
|
||||||
|
USE_INSTANCING: true, // 切换渲染模式
|
||||||
|
MAX_SATELLITES: 5000,
|
||||||
|
SATELLITE_SIZE: 0.5,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 性能优化点
|
||||||
|
|
||||||
|
1. **GPU 实例化**: 单次 draw call 渲染所有卫星
|
||||||
|
2. **批量更新**: 所有位置/颜色一次更新
|
||||||
|
3. **视锥体裁剪**: 自定义裁剪逻辑,避免 CPU 端逐卫星检测
|
||||||
|
4. **LOD (可选)**: 远处卫星简化显示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: 与现有系统集成
|
||||||
|
|
||||||
|
### 3.1 悬停/选中处理
|
||||||
|
|
||||||
|
当前通过 `selectSatellite()` 设置选中状态,Instanced 模式下需要:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 shader 中通过 instanceId 判断是否选中
|
||||||
|
// 或者使用单独的 InstancedBufferAttribute 存储选中状态
|
||||||
|
const instanceSelected = new Float32Array(MAX_SATELLITES);
|
||||||
|
geometry.setAttribute('instanceSelected',
|
||||||
|
new THREE.InstancedBufferAttribute(instanceSelected, 1));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 轨迹线
|
||||||
|
|
||||||
|
轨迹线仍然使用 `THREE.Line` 或 `THREE.LineSegments`,但可以类似地 Instanced 化:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Instanced LineSegments for trails
|
||||||
|
const trailGeometry = new THREE.InstancedBufferGeometry();
|
||||||
|
trailGeometry.setAttribute('position', trailPositions);
|
||||||
|
trailGeometry.setAttribute('instanceStart', ...);
|
||||||
|
trailGeometry.setAttribute('instanceEnd', ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: 验证与调优
|
||||||
|
|
||||||
|
### 4.1 性能测试
|
||||||
|
|
||||||
|
| 卫星数量 | Points 模式 | Instanced 模式 |
|
||||||
|
|----------|-------------|----------------|
|
||||||
|
| 500 | ✅ 60fps | ✅ 60fps |
|
||||||
|
| 2000 | ⚠️ 30fps | ✅ 60fps |
|
||||||
|
| 5000 | ❌ 10fps | ✅ 45fps |
|
||||||
|
| 10000 | ❌ 卡顿 | ⚠️ 30fps |
|
||||||
|
|
||||||
|
### 4.2 可能遇到的问题
|
||||||
|
|
||||||
|
1. **Shader 编译错误**: 需要调试 GLSL
|
||||||
|
2. **实例数量限制**: GPU 最大实例数 (通常 65535)
|
||||||
|
3. **大小不一**: 需要 per-instance size 属性
|
||||||
|
4. **透明度排序**: Instanced 渲染透明度处理复杂
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件变更清单
|
||||||
|
|
||||||
|
| 文件 | 变更 |
|
||||||
|
|------|------|
|
||||||
|
| `constants.js` | 新增 `SATELLITE_CONFIG` |
|
||||||
|
| `satellites.js` | 添加 Instanced 模式支持 |
|
||||||
|
| `instanced-satellites.js` | 新文件 - Instanced 渲染核心 |
|
||||||
|
| `main.js` | 集成新渲染模块 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 时间估算
|
||||||
|
|
||||||
|
| Phase | 工作量 | 难度 |
|
||||||
|
|-------|--------|------|
|
||||||
|
| Phase 1 | 1-2 天 | 低 |
|
||||||
|
| Phase 2 | 2-3 天 | 中 |
|
||||||
|
| Phase 3 | 1-2 天 | 中 |
|
||||||
|
| Phase 4 | 1 天 | 低 |
|
||||||
|
| **总计** | **5-8 天** | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 替代方案考虑
|
||||||
|
|
||||||
|
如果 Phase 2 实施困难,可以考虑:
|
||||||
|
|
||||||
|
1. **使用 Three.js InstancedMesh**: 适合渲染小型 3D 模型替代点
|
||||||
|
2. **使用 pointcloud2 格式**: 类似 LiDAR 点云渲染
|
||||||
|
3. **Web Workers**: 将轨道计算移到 Worker 线程
|
||||||
|
4. **迁移到 Cesium**: Cesium 原生支持 Instancing,且是 UE 迁移的中间步骤
|
||||||
@@ -16,4 +16,4 @@ COPY . .
|
|||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||||
|
|||||||
@@ -120,6 +120,20 @@ COLLECTOR_INFO = {
|
|||||||
"priority": "P1",
|
"priority": "P1",
|
||||||
"frequency_hours": 168,
|
"frequency_hours": 168,
|
||||||
},
|
},
|
||||||
|
"spacetrack_tle": {
|
||||||
|
"id": 19,
|
||||||
|
"name": "Space-Track TLE",
|
||||||
|
"module": "L3",
|
||||||
|
"priority": "P2",
|
||||||
|
"frequency_hours": 24,
|
||||||
|
},
|
||||||
|
"celestrak_tle": {
|
||||||
|
"id": 20,
|
||||||
|
"name": "CelesTrak TLE",
|
||||||
|
"module": "L3",
|
||||||
|
"priority": "P2",
|
||||||
|
"frequency_hours": 24,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ID_TO_COLLECTOR = {info["id"]: name for name, info in COLLECTOR_INFO.items()}
|
ID_TO_COLLECTOR = {info["id"]: name for name, info in COLLECTOR_INFO.items()}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
"""Visualization API - GeoJSON endpoints for 3D Earth display"""
|
"""Visualization API - GeoJSON endpoints for 3D Earth display
|
||||||
|
|
||||||
|
Unified API for all visualization data sources.
|
||||||
|
Returns GeoJSON format compatible with Three.js, CesiumJS, and Unreal Cesium.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, func
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
@@ -12,6 +17,9 @@ from app.services.cable_graph import build_graph_from_data, CableGraph
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
# ============== Converter Functions ==============
|
||||||
|
|
||||||
|
|
||||||
def convert_cable_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
def convert_cable_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
||||||
"""Convert cable records to GeoJSON FeatureCollection"""
|
"""Convert cable records to GeoJSON FeatureCollection"""
|
||||||
features = []
|
features = []
|
||||||
@@ -66,6 +74,7 @@ def convert_cable_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
|||||||
"geometry": {"type": "MultiLineString", "coordinates": all_lines},
|
"geometry": {"type": "MultiLineString", "coordinates": all_lines},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": record.id,
|
"id": record.id,
|
||||||
|
"cable_id": record.name,
|
||||||
"source_id": record.source_id,
|
"source_id": record.source_id,
|
||||||
"Name": record.name,
|
"Name": record.name,
|
||||||
"name": record.name,
|
"name": record.name,
|
||||||
@@ -87,8 +96,129 @@ def convert_cable_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
|||||||
return {"type": "FeatureCollection", "features": features}
|
return {"type": "FeatureCollection", "features": features}
|
||||||
|
|
||||||
|
|
||||||
def convert_landing_point_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
def convert_landing_point_to_geojson(records: List[CollectedData], city_to_cable_ids_map: Dict[int, List[int]] = None, cable_id_to_name_map: Dict[int, str] = None) -> Dict[str, Any]:
|
||||||
"""Convert landing point records to GeoJSON FeatureCollection"""
|
features = []
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
try:
|
||||||
|
lat = float(record.latitude) if record.latitude else None
|
||||||
|
lon = float(record.longitude) if record.longitude else None
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if lat is None or lon is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
metadata = record.extra_data or {}
|
||||||
|
city_id = metadata.get("city_id")
|
||||||
|
|
||||||
|
props = {
|
||||||
|
"id": record.id,
|
||||||
|
"source_id": record.source_id,
|
||||||
|
"name": record.name,
|
||||||
|
"country": record.country,
|
||||||
|
"city": record.city,
|
||||||
|
"is_tbd": metadata.get("is_tbd", False),
|
||||||
|
}
|
||||||
|
|
||||||
|
cable_names = []
|
||||||
|
if city_to_cable_ids_map and city_id in city_to_cable_ids_map:
|
||||||
|
for cable_id in city_to_cable_ids_map[city_id]:
|
||||||
|
if cable_id_to_name_map and cable_id in cable_id_to_name_map:
|
||||||
|
cable_names.append(cable_id_to_name_map[cable_id])
|
||||||
|
|
||||||
|
if cable_names:
|
||||||
|
props["cable_names"] = cable_names
|
||||||
|
|
||||||
|
features.append(
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
||||||
|
"properties": props,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"type": "FeatureCollection", "features": features}
|
||||||
|
|
||||||
|
|
||||||
|
def convert_satellite_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
||||||
|
"""Convert satellite TLE records to GeoJSON"""
|
||||||
|
features = []
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
metadata = record.extra_data or {}
|
||||||
|
norad_id = metadata.get("norad_cat_id")
|
||||||
|
|
||||||
|
if not norad_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
features.append(
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"id": norad_id,
|
||||||
|
"geometry": {"type": "Point", "coordinates": [0, 0, 0]},
|
||||||
|
"properties": {
|
||||||
|
"id": record.id,
|
||||||
|
"norad_cat_id": norad_id,
|
||||||
|
"name": record.name,
|
||||||
|
"international_designator": metadata.get("international_designator"),
|
||||||
|
"epoch": metadata.get("epoch"),
|
||||||
|
"inclination": metadata.get("inclination"),
|
||||||
|
"raan": metadata.get("raan"),
|
||||||
|
"eccentricity": metadata.get("eccentricity"),
|
||||||
|
"arg_of_perigee": metadata.get("arg_of_perigee"),
|
||||||
|
"mean_anomaly": metadata.get("mean_anomaly"),
|
||||||
|
"mean_motion": metadata.get("mean_motion"),
|
||||||
|
"bstar": metadata.get("bstar"),
|
||||||
|
"classification_type": metadata.get("classification_type"),
|
||||||
|
"data_type": "satellite_tle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"type": "FeatureCollection", "features": features}
|
||||||
|
|
||||||
|
|
||||||
|
def convert_supercomputer_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
||||||
|
"""Convert TOP500 supercomputer records to GeoJSON"""
|
||||||
|
features = []
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
try:
|
||||||
|
lat = float(record.latitude) if record.latitude and record.latitude != "0.0" else None
|
||||||
|
lon = (
|
||||||
|
float(record.longitude) if record.longitude and record.longitude != "0.0" else None
|
||||||
|
)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
lat, lon = None, None
|
||||||
|
|
||||||
|
metadata = record.extra_data or {}
|
||||||
|
|
||||||
|
features.append(
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"id": record.id,
|
||||||
|
"geometry": {"type": "Point", "coordinates": [lon or 0, lat or 0]},
|
||||||
|
"properties": {
|
||||||
|
"id": record.id,
|
||||||
|
"name": record.name,
|
||||||
|
"rank": metadata.get("rank"),
|
||||||
|
"r_max": record.value,
|
||||||
|
"r_peak": metadata.get("r_peak"),
|
||||||
|
"cores": metadata.get("cores"),
|
||||||
|
"power": metadata.get("power"),
|
||||||
|
"country": record.country,
|
||||||
|
"city": record.city,
|
||||||
|
"data_type": "supercomputer",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"type": "FeatureCollection", "features": features}
|
||||||
|
|
||||||
|
|
||||||
|
def convert_gpu_cluster_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
||||||
|
"""Convert GPU cluster records to GeoJSON"""
|
||||||
features = []
|
features = []
|
||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
@@ -96,24 +226,22 @@ def convert_landing_point_to_geojson(records: List[CollectedData]) -> Dict[str,
|
|||||||
lat = float(record.latitude) if record.latitude else None
|
lat = float(record.latitude) if record.latitude else None
|
||||||
lon = float(record.longitude) if record.longitude else None
|
lon = float(record.longitude) if record.longitude else None
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
continue
|
lat, lon = None, None
|
||||||
|
|
||||||
if lat is None or lon is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
metadata = record.extra_data or {}
|
metadata = record.extra_data or {}
|
||||||
|
|
||||||
features.append(
|
features.append(
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
"id": record.id,
|
||||||
|
"geometry": {"type": "Point", "coordinates": [lon or 0, lat or 0]},
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": record.id,
|
"id": record.id,
|
||||||
"source_id": record.source_id,
|
|
||||||
"name": record.name,
|
"name": record.name,
|
||||||
"country": record.country,
|
"country": record.country,
|
||||||
"city": record.city,
|
"city": record.city,
|
||||||
"is_tbd": metadata.get("is_tbd", False),
|
"metadata": metadata,
|
||||||
|
"data_type": "gpu_cluster",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -121,6 +249,9 @@ def convert_landing_point_to_geojson(records: List[CollectedData]) -> Dict[str,
|
|||||||
return {"type": "FeatureCollection", "features": features}
|
return {"type": "FeatureCollection", "features": features}
|
||||||
|
|
||||||
|
|
||||||
|
# ============== API Endpoints ==============
|
||||||
|
|
||||||
|
|
||||||
@router.get("/geo/cables")
|
@router.get("/geo/cables")
|
||||||
async def get_cables_geojson(db: AsyncSession = Depends(get_db)):
|
async def get_cables_geojson(db: AsyncSession = Depends(get_db)):
|
||||||
"""获取海底电缆 GeoJSON 数据 (LineString)"""
|
"""获取海底电缆 GeoJSON 数据 (LineString)"""
|
||||||
@@ -144,19 +275,45 @@ async def get_cables_geojson(db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
@router.get("/geo/landing-points")
|
@router.get("/geo/landing-points")
|
||||||
async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
|
async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
|
||||||
"""获取登陆点 GeoJSON 数据 (Point)"""
|
|
||||||
try:
|
try:
|
||||||
stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
|
landing_stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
|
||||||
result = await db.execute(stmt)
|
landing_result = await db.execute(landing_stmt)
|
||||||
records = result.scalars().all()
|
records = landing_result.scalars().all()
|
||||||
|
|
||||||
|
relation_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cable_landing_relation")
|
||||||
|
relation_result = await db.execute(relation_stmt)
|
||||||
|
relation_records = relation_result.scalars().all()
|
||||||
|
|
||||||
|
cable_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cables")
|
||||||
|
cable_result = await db.execute(cable_stmt)
|
||||||
|
cable_records = cable_result.scalars().all()
|
||||||
|
|
||||||
|
city_to_cable_ids_map = {}
|
||||||
|
for rel in relation_records:
|
||||||
|
if rel.extra_data:
|
||||||
|
city_id = rel.extra_data.get("city_id")
|
||||||
|
cable_id = rel.extra_data.get("cable_id")
|
||||||
|
if city_id is not None and cable_id is not None:
|
||||||
|
if city_id not in city_to_cable_ids_map:
|
||||||
|
city_to_cable_ids_map[city_id] = []
|
||||||
|
if cable_id not in city_to_cable_ids_map[city_id]:
|
||||||
|
city_to_cable_ids_map[city_id].append(cable_id)
|
||||||
|
|
||||||
|
cable_id_to_name_map = {}
|
||||||
|
for cable in cable_records:
|
||||||
|
if cable.extra_data:
|
||||||
|
cable_id = cable.extra_data.get("cable_id")
|
||||||
|
cable_name = cable.name
|
||||||
|
if cable_id and cable_name:
|
||||||
|
cable_id_to_name_map[cable_id] = cable_name
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
detail="No landing point data found. Please run the arcgis_landing_points collector first.",
|
detail="No landing point data found. Please run the arcgis_landing_points collector first.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return convert_landing_point_to_geojson(records)
|
return convert_landing_point_to_geojson(records, city_to_cable_ids_map, cable_id_to_name_map)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -165,7 +322,6 @@ async def get_landing_points_geojson(db: AsyncSession = Depends(get_db)):
|
|||||||
|
|
||||||
@router.get("/geo/all")
|
@router.get("/geo/all")
|
||||||
async def get_all_geojson(db: AsyncSession = Depends(get_db)):
|
async def get_all_geojson(db: AsyncSession = Depends(get_db)):
|
||||||
"""获取所有可视化数据 (电缆 + 登陆点)"""
|
|
||||||
cables_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cables")
|
cables_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cables")
|
||||||
cables_result = await db.execute(cables_stmt)
|
cables_result = await db.execute(cables_stmt)
|
||||||
cables_records = cables_result.scalars().all()
|
cables_records = cables_result.scalars().all()
|
||||||
@@ -173,6 +329,29 @@ async def get_all_geojson(db: AsyncSession = Depends(get_db)):
|
|||||||
points_stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
|
points_stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
|
||||||
points_result = await db.execute(points_stmt)
|
points_result = await db.execute(points_stmt)
|
||||||
points_records = points_result.scalars().all()
|
points_records = points_result.scalars().all()
|
||||||
|
|
||||||
|
relation_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cable_landing_relation")
|
||||||
|
relation_result = await db.execute(relation_stmt)
|
||||||
|
relation_records = relation_result.scalars().all()
|
||||||
|
|
||||||
|
city_to_cable_ids_map = {}
|
||||||
|
for rel in relation_records:
|
||||||
|
if rel.extra_data:
|
||||||
|
city_id = rel.extra_data.get("city_id")
|
||||||
|
cable_id = rel.extra_data.get("cable_id")
|
||||||
|
if city_id is not None and cable_id is not None:
|
||||||
|
if city_id not in city_to_cable_ids_map:
|
||||||
|
city_to_cable_ids_map[city_id] = []
|
||||||
|
if cable_id not in city_to_cable_ids_map[city_id]:
|
||||||
|
city_to_cable_ids_map[city_id].append(cable_id)
|
||||||
|
|
||||||
|
cable_id_to_name_map = {}
|
||||||
|
for cable in cables_records:
|
||||||
|
if cable.extra_data:
|
||||||
|
cable_id = cable.extra_data.get("cable_id")
|
||||||
|
cable_name = cable.name
|
||||||
|
if cable_id and cable_name:
|
||||||
|
cable_id_to_name_map[cable_id] = cable_name
|
||||||
|
|
||||||
cables = (
|
cables = (
|
||||||
convert_cable_to_geojson(cables_records)
|
convert_cable_to_geojson(cables_records)
|
||||||
@@ -180,7 +359,7 @@ async def get_all_geojson(db: AsyncSession = Depends(get_db)):
|
|||||||
else {"type": "FeatureCollection", "features": []}
|
else {"type": "FeatureCollection", "features": []}
|
||||||
)
|
)
|
||||||
points = (
|
points = (
|
||||||
convert_landing_point_to_geojson(points_records)
|
convert_landing_point_to_geojson(points_records, city_to_cable_ids_map, cable_id_to_name_map)
|
||||||
if points_records
|
if points_records
|
||||||
else {"type": "FeatureCollection", "features": []}
|
else {"type": "FeatureCollection", "features": []}
|
||||||
)
|
)
|
||||||
@@ -195,6 +374,178 @@ async def get_all_geojson(db: AsyncSession = Depends(get_db)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/geo/satellites")
|
||||||
|
async def get_satellites_geojson(
|
||||||
|
limit: int = 10000,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""获取卫星 TLE GeoJSON 数据"""
|
||||||
|
stmt = (
|
||||||
|
select(CollectedData)
|
||||||
|
.where(CollectedData.source == "celestrak_tle")
|
||||||
|
.where(CollectedData.name != "Unknown")
|
||||||
|
.order_by(CollectedData.id.desc())
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
records = result.scalars().all()
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
return {"type": "FeatureCollection", "features": [], "count": 0}
|
||||||
|
|
||||||
|
geojson = convert_satellite_to_geojson(list(records))
|
||||||
|
return {
|
||||||
|
**geojson,
|
||||||
|
"count": len(geojson.get("features", [])),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/geo/supercomputers")
|
||||||
|
async def get_supercomputers_geojson(
|
||||||
|
limit: int = 500,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""获取 TOP500 超算中心 GeoJSON 数据"""
|
||||||
|
stmt = (
|
||||||
|
select(CollectedData)
|
||||||
|
.where(CollectedData.source == "top500")
|
||||||
|
.where(CollectedData.name != "Unknown")
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
records = result.scalars().all()
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
return {"type": "FeatureCollection", "features": [], "count": 0}
|
||||||
|
|
||||||
|
geojson = convert_supercomputer_to_geojson(list(records))
|
||||||
|
return {
|
||||||
|
**geojson,
|
||||||
|
"count": len(geojson.get("features", [])),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/geo/gpu-clusters")
|
||||||
|
async def get_gpu_clusters_geojson(
|
||||||
|
limit: int = 100,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""获取 GPU 集群 GeoJSON 数据"""
|
||||||
|
stmt = (
|
||||||
|
select(CollectedData)
|
||||||
|
.where(CollectedData.source == "epoch_ai_gpu")
|
||||||
|
.where(CollectedData.name != "Unknown")
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
result = await db.execute(stmt)
|
||||||
|
records = result.scalars().all()
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
return {"type": "FeatureCollection", "features": [], "count": 0}
|
||||||
|
|
||||||
|
geojson = convert_gpu_cluster_to_geojson(list(records))
|
||||||
|
return {
|
||||||
|
**geojson,
|
||||||
|
"count": len(geojson.get("features", [])),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/all")
|
||||||
|
async def get_all_visualization_data(db: AsyncSession = Depends(get_db)):
|
||||||
|
"""获取所有可视化数据的统一端点
|
||||||
|
|
||||||
|
Returns GeoJSON FeatureCollections for all data types:
|
||||||
|
- satellites: 卫星 TLE 数据
|
||||||
|
- cables: 海底电缆
|
||||||
|
- landing_points: 登陆点
|
||||||
|
- supercomputers: TOP500 超算
|
||||||
|
- gpu_clusters: GPU 集群
|
||||||
|
"""
|
||||||
|
cables_stmt = select(CollectedData).where(CollectedData.source == "arcgis_cables")
|
||||||
|
cables_result = await db.execute(cables_stmt)
|
||||||
|
cables_records = list(cables_result.scalars().all())
|
||||||
|
|
||||||
|
points_stmt = select(CollectedData).where(CollectedData.source == "arcgis_landing_points")
|
||||||
|
points_result = await db.execute(points_stmt)
|
||||||
|
points_records = list(points_result.scalars().all())
|
||||||
|
|
||||||
|
satellites_stmt = (
|
||||||
|
select(CollectedData)
|
||||||
|
.where(CollectedData.source == "celestrak_tle")
|
||||||
|
.where(CollectedData.name != "Unknown")
|
||||||
|
)
|
||||||
|
satellites_result = await db.execute(satellites_stmt)
|
||||||
|
satellites_records = list(satellites_result.scalars().all())
|
||||||
|
|
||||||
|
supercomputers_stmt = (
|
||||||
|
select(CollectedData)
|
||||||
|
.where(CollectedData.source == "top500")
|
||||||
|
.where(CollectedData.name != "Unknown")
|
||||||
|
)
|
||||||
|
supercomputers_result = await db.execute(supercomputers_stmt)
|
||||||
|
supercomputers_records = list(supercomputers_result.scalars().all())
|
||||||
|
|
||||||
|
gpu_stmt = (
|
||||||
|
select(CollectedData)
|
||||||
|
.where(CollectedData.source == "epoch_ai_gpu")
|
||||||
|
.where(CollectedData.name != "Unknown")
|
||||||
|
)
|
||||||
|
gpu_result = await db.execute(gpu_stmt)
|
||||||
|
gpu_records = list(gpu_result.scalars().all())
|
||||||
|
|
||||||
|
cables = (
|
||||||
|
convert_cable_to_geojson(cables_records)
|
||||||
|
if cables_records
|
||||||
|
else {"type": "FeatureCollection", "features": []}
|
||||||
|
)
|
||||||
|
landing_points = (
|
||||||
|
convert_landing_point_to_geojson(points_records)
|
||||||
|
if points_records
|
||||||
|
else {"type": "FeatureCollection", "features": []}
|
||||||
|
)
|
||||||
|
satellites = (
|
||||||
|
convert_satellite_to_geojson(satellites_records)
|
||||||
|
if satellites_records
|
||||||
|
else {"type": "FeatureCollection", "features": []}
|
||||||
|
)
|
||||||
|
supercomputers = (
|
||||||
|
convert_supercomputer_to_geojson(supercomputers_records)
|
||||||
|
if supercomputers_records
|
||||||
|
else {"type": "FeatureCollection", "features": []}
|
||||||
|
)
|
||||||
|
gpu_clusters = (
|
||||||
|
convert_gpu_cluster_to_geojson(gpu_records)
|
||||||
|
if gpu_records
|
||||||
|
else {"type": "FeatureCollection", "features": []}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"generated_at": datetime.utcnow().isoformat() + "Z",
|
||||||
|
"version": "1.0",
|
||||||
|
"data": {
|
||||||
|
"satellites": satellites,
|
||||||
|
"cables": cables,
|
||||||
|
"landing_points": landing_points,
|
||||||
|
"supercomputers": supercomputers,
|
||||||
|
"gpu_clusters": gpu_clusters,
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"total_features": (
|
||||||
|
len(satellites.get("features", []))
|
||||||
|
+ len(cables.get("features", []))
|
||||||
|
+ len(landing_points.get("features", []))
|
||||||
|
+ len(supercomputers.get("features", []))
|
||||||
|
+ len(gpu_clusters.get("features", []))
|
||||||
|
),
|
||||||
|
"satellites": len(satellites.get("features", [])),
|
||||||
|
"cables": len(cables.get("features", [])),
|
||||||
|
"landing_points": len(landing_points.get("features", [])),
|
||||||
|
"supercomputers": len(supercomputers.get("features", [])),
|
||||||
|
"gpu_clusters": len(gpu_clusters.get("features", [])),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Cache for cable graph
|
# Cache for cable graph
|
||||||
_cable_graph: Optional[CableGraph] = None
|
_cable_graph: Optional[CableGraph] = None
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
CORS_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8000"]
|
CORS_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8000"]
|
||||||
|
|
||||||
|
SPACETRACK_USERNAME: str = ""
|
||||||
|
SPACETRACK_PASSWORD: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def REDIS_URL(self) -> str:
|
def REDIS_URL(self) -> str:
|
||||||
return os.getenv(
|
return os.getenv(
|
||||||
@@ -34,7 +37,7 @@ class Settings(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = Path(__file__).parent.parent.parent / ".env"
|
||||||
case_sensitive = True
|
case_sensitive = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ COLLECTOR_URL_KEYS = {
|
|||||||
"peeringdb_facility": "peeringdb.facility_url",
|
"peeringdb_facility": "peeringdb.facility_url",
|
||||||
"top500": "top500.url",
|
"top500": "top500.url",
|
||||||
"epoch_ai_gpu": "epoch_ai.gpu_clusters_url",
|
"epoch_ai_gpu": "epoch_ai.gpu_clusters_url",
|
||||||
|
"spacetrack_tle": "spacetrack.tle_query_url",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,3 +33,7 @@ top500:
|
|||||||
|
|
||||||
epoch_ai:
|
epoch_ai:
|
||||||
gpu_clusters_url: "https://epoch.ai/data/gpu-clusters"
|
gpu_clusters_url: "https://epoch.ai/data/gpu-clusters"
|
||||||
|
|
||||||
|
spacetrack:
|
||||||
|
base_url: "https://www.space-track.org"
|
||||||
|
tle_query_url: "https://www.space-track.org/basicspacedata/query/class/gp/orderby/EPOCH%20desc/limit/1000/format/json"
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ from app.services.collectors.arcgis_cables import ArcGISCableCollector
|
|||||||
from app.services.collectors.fao_landing import FAOLandingPointCollector
|
from app.services.collectors.fao_landing import FAOLandingPointCollector
|
||||||
from app.services.collectors.arcgis_landing import ArcGISLandingPointCollector
|
from app.services.collectors.arcgis_landing import ArcGISLandingPointCollector
|
||||||
from app.services.collectors.arcgis_relation import ArcGISCableLandingRelationCollector
|
from app.services.collectors.arcgis_relation import ArcGISCableLandingRelationCollector
|
||||||
|
from app.services.collectors.spacetrack import SpaceTrackTLECollector
|
||||||
|
from app.services.collectors.celestrak import CelesTrakTLECollector
|
||||||
|
|
||||||
collector_registry.register(TOP500Collector())
|
collector_registry.register(TOP500Collector())
|
||||||
collector_registry.register(EpochAIGPUCollector())
|
collector_registry.register(EpochAIGPUCollector())
|
||||||
@@ -47,3 +49,5 @@ collector_registry.register(ArcGISCableCollector())
|
|||||||
collector_registry.register(FAOLandingPointCollector())
|
collector_registry.register(FAOLandingPointCollector())
|
||||||
collector_registry.register(ArcGISLandingPointCollector())
|
collector_registry.register(ArcGISLandingPointCollector())
|
||||||
collector_registry.register(ArcGISCableLandingRelationCollector())
|
collector_registry.register(ArcGISCableLandingRelationCollector())
|
||||||
|
collector_registry.register(SpaceTrackTLECollector())
|
||||||
|
collector_registry.register(CelesTrakTLECollector())
|
||||||
|
|||||||
@@ -39,8 +39,13 @@ class ArcGISLandingPointCollector(BaseCollector):
|
|||||||
props = feature.get("properties", {})
|
props = feature.get("properties", {})
|
||||||
geometry = feature.get("geometry", {})
|
geometry = feature.get("geometry", {})
|
||||||
|
|
||||||
lat = geometry.get("y") if geometry else None
|
if geometry.get("type") == "Point":
|
||||||
lon = geometry.get("x") if geometry else None
|
coords = geometry.get("coordinates", [])
|
||||||
|
lon = coords[0] if len(coords) > 0 else None
|
||||||
|
lat = coords[1] if len(coords) > 1 else None
|
||||||
|
else:
|
||||||
|
lat = geometry.get("y") if geometry else None
|
||||||
|
lon = geometry.get("x") if geometry else None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
entry = {
|
entry = {
|
||||||
@@ -54,6 +59,7 @@ class ArcGISLandingPointCollector(BaseCollector):
|
|||||||
"unit": "",
|
"unit": "",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"objectid": props.get("OBJECTID"),
|
"objectid": props.get("OBJECTID"),
|
||||||
|
"city_id": props.get("city_id"),
|
||||||
"cable_id": props.get("cable_id"),
|
"cable_id": props.get("cable_id"),
|
||||||
"cable_name": props.get("cable_name"),
|
"cable_name": props.get("cable_name"),
|
||||||
"facility": props.get("facility"),
|
"facility": props.get("facility"),
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class ArcGISCableLandingRelationCollector(BaseCollector):
|
|||||||
"unit": "",
|
"unit": "",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"objectid": props.get("OBJECTID"),
|
"objectid": props.get("OBJECTID"),
|
||||||
|
"city_id": props.get("city_id"),
|
||||||
"cable_id": props.get("cable_id"),
|
"cable_id": props.get("cable_id"),
|
||||||
"cable_name": props.get("cable_name"),
|
"cable_name": props.get("cable_name"),
|
||||||
"landing_point_id": props.get("landing_point_id"),
|
"landing_point_id": props.get("landing_point_id"),
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ class BaseCollector(ABC):
|
|||||||
records_added = 0
|
records_added = 0
|
||||||
|
|
||||||
for i, item in enumerate(data):
|
for i, item in enumerate(data):
|
||||||
|
print(
|
||||||
|
f"DEBUG: Saving item {i}: name={item.get('name')}, metadata={item.get('metadata', 'NOT FOUND')}"
|
||||||
|
)
|
||||||
record = CollectedData(
|
record = CollectedData(
|
||||||
source=self.name,
|
source=self.name,
|
||||||
source_id=item.get("source_id") or item.get("id"),
|
source_id=item.get("source_id") or item.get("id"),
|
||||||
|
|||||||
99
backend/app/services/collectors/celestrak.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"""CelesTrak TLE Collector
|
||||||
|
|
||||||
|
Collects satellite TLE (Two-Line Element) data from CelesTrak.org.
|
||||||
|
Free, no authentication required.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from app.services.collectors.base import BaseCollector
|
||||||
|
|
||||||
|
|
||||||
|
class CelesTrakTLECollector(BaseCollector):
|
||||||
|
name = "celestrak_tle"
|
||||||
|
priority = "P2"
|
||||||
|
module = "L3"
|
||||||
|
frequency_hours = 24
|
||||||
|
data_type = "satellite_tle"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self) -> str:
|
||||||
|
return "https://celestrak.org/NORAD/elements/gp.php"
|
||||||
|
|
||||||
|
async def fetch(self) -> List[Dict[str, Any]]:
|
||||||
|
satellite_groups = [
|
||||||
|
"starlink",
|
||||||
|
"gps-ops",
|
||||||
|
"galileo",
|
||||||
|
"glonass",
|
||||||
|
"beidou",
|
||||||
|
"leo",
|
||||||
|
"geo",
|
||||||
|
"iridium-next",
|
||||||
|
]
|
||||||
|
|
||||||
|
all_satellites = []
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||||
|
for group in satellite_groups:
|
||||||
|
try:
|
||||||
|
url = f"https://celestrak.org/NORAD/elements/gp.php?GROUP={group}&FORMAT=json"
|
||||||
|
response = await client.get(url)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if isinstance(data, list):
|
||||||
|
all_satellites.extend(data)
|
||||||
|
print(f"CelesTrak: Fetched {len(data)} satellites from group '{group}'")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"CelesTrak: Error fetching group '{group}': {e}")
|
||||||
|
|
||||||
|
if not all_satellites:
|
||||||
|
return self._get_sample_data()
|
||||||
|
|
||||||
|
print(f"CelesTrak: Total satellites fetched: {len(all_satellites)}")
|
||||||
|
|
||||||
|
# Return raw data - base.run() will call transform()
|
||||||
|
return all_satellites
|
||||||
|
|
||||||
|
def transform(self, raw_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
transformed = []
|
||||||
|
for item in raw_data:
|
||||||
|
transformed.append(
|
||||||
|
{
|
||||||
|
"name": item.get("OBJECT_NAME", "Unknown"),
|
||||||
|
"reference_date": item.get("EPOCH", ""),
|
||||||
|
"metadata": {
|
||||||
|
"norad_cat_id": item.get("NORAD_CAT_ID"),
|
||||||
|
"international_designator": item.get("OBJECT_ID"),
|
||||||
|
"epoch": item.get("EPOCH"),
|
||||||
|
"mean_motion": item.get("MEAN_MOTION"),
|
||||||
|
"eccentricity": item.get("ECCENTRICITY"),
|
||||||
|
"inclination": item.get("INCLINATION"),
|
||||||
|
"raan": item.get("RA_OF_ASC_NODE"),
|
||||||
|
"arg_of_perigee": item.get("ARG_OF_PERICENTER"),
|
||||||
|
"mean_anomaly": item.get("MEAN_ANOMALY"),
|
||||||
|
"classification_type": item.get("CLASSIFICATION_TYPE"),
|
||||||
|
"bstar": item.get("BSTAR"),
|
||||||
|
"mean_motion_dot": item.get("MEAN_MOTION_DOT"),
|
||||||
|
"mean_motion_ddot": item.get("MEAN_MOTION_DDOT"),
|
||||||
|
"ephemeris_type": item.get("EPHEMERIS_TYPE"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return transformed
|
||||||
|
|
||||||
|
def _get_sample_data(self) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "STARLINK-1000",
|
||||||
|
"norad_cat_id": 44720,
|
||||||
|
"international_designator": "2019-029AZ",
|
||||||
|
"epoch": "2026-03-13T00:00:00Z",
|
||||||
|
"mean_motion": 15.79234567,
|
||||||
|
"eccentricity": 0.0001234,
|
||||||
|
"inclination": 53.0,
|
||||||
|
},
|
||||||
|
]
|
||||||
222
backend/app/services/collectors/spacetrack.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
"""Space-Track TLE Collector
|
||||||
|
|
||||||
|
Collects satellite TLE (Two-Line Element) data from Space-Track.org.
|
||||||
|
API documentation: https://www.space-track.org/documentation
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from app.services.collectors.base import BaseCollector
|
||||||
|
from app.core.data_sources import get_data_sources_config
|
||||||
|
|
||||||
|
|
||||||
|
class SpaceTrackTLECollector(BaseCollector):
|
||||||
|
name = "spacetrack_tle"
|
||||||
|
priority = "P2"
|
||||||
|
module = "L3"
|
||||||
|
frequency_hours = 24
|
||||||
|
data_type = "satellite_tle"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self) -> str:
|
||||||
|
config = get_data_sources_config()
|
||||||
|
if self._resolved_url:
|
||||||
|
return self._resolved_url
|
||||||
|
return config.get_yaml_url("spacetrack_tle")
|
||||||
|
|
||||||
|
async def fetch(self) -> List[Dict[str, Any]]:
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
username = settings.SPACETRACK_USERNAME
|
||||||
|
password = settings.SPACETRACK_PASSWORD
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
print("SPACETRACK: No credentials configured, using sample data")
|
||||||
|
return self._get_sample_data()
|
||||||
|
|
||||||
|
print(f"SPACETRACK: Attempting to fetch TLE data with username: {username}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=120.0,
|
||||||
|
follow_redirects=True,
|
||||||
|
headers={
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "application/json, text/html, */*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Referer": "https://www.space-track.org/",
|
||||||
|
},
|
||||||
|
) as client:
|
||||||
|
await client.get("https://www.space-track.org/")
|
||||||
|
|
||||||
|
login_response = await client.post(
|
||||||
|
"https://www.space-track.org/ajaxauth/login",
|
||||||
|
data={
|
||||||
|
"identity": username,
|
||||||
|
"password": password,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(f"SPACETRACK: Login response status: {login_response.status_code}")
|
||||||
|
print(f"SPACETRACK: Login response URL: {login_response.url}")
|
||||||
|
|
||||||
|
if login_response.status_code == 403:
|
||||||
|
print("SPACETRACK: Trying alternate login method...")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=120.0,
|
||||||
|
follow_redirects=True,
|
||||||
|
) as alt_client:
|
||||||
|
await alt_client.get("https://www.space-track.org/")
|
||||||
|
|
||||||
|
form_data = {
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
"query": "class/gp/NORAD_CAT_ID/25544/format/json",
|
||||||
|
}
|
||||||
|
alt_login = await alt_client.post(
|
||||||
|
"https://www.space-track.org/ajaxauth/login",
|
||||||
|
data={
|
||||||
|
"identity": username,
|
||||||
|
"password": password,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(f"SPACETRACK: Alt login status: {alt_login.status_code}")
|
||||||
|
|
||||||
|
if alt_login.status_code == 200:
|
||||||
|
tle_response = await alt_client.get(
|
||||||
|
"https://www.space-track.org/basicspacedata/query/class/gp/NORAD_CAT_ID/25544/format/json"
|
||||||
|
)
|
||||||
|
if tle_response.status_code == 200:
|
||||||
|
data = tle_response.json()
|
||||||
|
print(f"SPACETRACK: Received {len(data)} records via alt method")
|
||||||
|
return data
|
||||||
|
|
||||||
|
if login_response.status_code != 200:
|
||||||
|
print(f"SPACETRACK: Login failed, using sample data")
|
||||||
|
return self._get_sample_data()
|
||||||
|
|
||||||
|
tle_response = await client.get(
|
||||||
|
"https://www.space-track.org/basicspacedata/query/class/gp/NORAD_CAT_ID/25544/format/json"
|
||||||
|
)
|
||||||
|
print(f"SPACETRACK: TLE query status: {tle_response.status_code}")
|
||||||
|
|
||||||
|
if tle_response.status_code != 200:
|
||||||
|
print(f"SPACETRACK: Query failed, using sample data")
|
||||||
|
return self._get_sample_data()
|
||||||
|
|
||||||
|
data = tle_response.json()
|
||||||
|
print(f"SPACETRACK: Received {len(data)} records")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SPACETRACK: Error - {e}, using sample data")
|
||||||
|
return self._get_sample_data()
|
||||||
|
|
||||||
|
print(f"SPACETRACK: Attempting to fetch TLE data with username: {username}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=120.0,
|
||||||
|
follow_redirects=True,
|
||||||
|
headers={
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
"Accept": "application/json, text/html, */*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
},
|
||||||
|
) as client:
|
||||||
|
# First, visit the main page to get any cookies
|
||||||
|
await client.get("https://www.space-track.org/")
|
||||||
|
|
||||||
|
# Login to get session cookie
|
||||||
|
login_response = await client.post(
|
||||||
|
"https://www.space-track.org/ajaxauth/login",
|
||||||
|
data={
|
||||||
|
"identity": username,
|
||||||
|
"password": password,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(f"SPACETRACK: Login response status: {login_response.status_code}")
|
||||||
|
print(f"SPACETRACK: Login response URL: {login_response.url}")
|
||||||
|
print(f"SPACETRACK: Login response body: {login_response.text[:500]}")
|
||||||
|
|
||||||
|
if login_response.status_code != 200:
|
||||||
|
print(f"SPACETRACK: Login failed, using sample data")
|
||||||
|
return self._get_sample_data()
|
||||||
|
|
||||||
|
# Query for TLE data (get first 1000 satellites)
|
||||||
|
tle_response = await client.get(
|
||||||
|
"https://www.space-track.org/basicspacedata/query"
|
||||||
|
"/class/gp"
|
||||||
|
"/orderby/EPOCH%20desc"
|
||||||
|
"/limit/1000"
|
||||||
|
"/format/json"
|
||||||
|
)
|
||||||
|
print(f"SPACETRACK: TLE query status: {tle_response.status_code}")
|
||||||
|
|
||||||
|
if tle_response.status_code != 200:
|
||||||
|
print(f"SPACETRACK: Query failed, using sample data")
|
||||||
|
return self._get_sample_data()
|
||||||
|
|
||||||
|
data = tle_response.json()
|
||||||
|
print(f"SPACETRACK: Received {len(data)} records")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SPACETRACK: Error - {e}, using sample data")
|
||||||
|
return self._get_sample_data()
|
||||||
|
|
||||||
|
def transform(self, raw_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
"""Transform TLE data to internal format"""
|
||||||
|
transformed = []
|
||||||
|
for item in raw_data:
|
||||||
|
transformed.append(
|
||||||
|
{
|
||||||
|
"name": item.get("OBJECT_NAME", "Unknown"),
|
||||||
|
"norad_cat_id": item.get("NORAD_CAT_ID"),
|
||||||
|
"international_designator": item.get("INTL_DESIGNATOR"),
|
||||||
|
"epoch": item.get("EPOCH"),
|
||||||
|
"mean_motion": item.get("MEAN_MOTION"),
|
||||||
|
"eccentricity": item.get("ECCENTRICITY"),
|
||||||
|
"inclination": item.get("INCLINATION"),
|
||||||
|
"raan": item.get("RAAN"),
|
||||||
|
"arg_of_perigee": item.get("ARG_OF_PERIGEE"),
|
||||||
|
"mean_anomaly": item.get("MEAN_ANOMALY"),
|
||||||
|
"ephemeris_type": item.get("EPHEMERIS_TYPE"),
|
||||||
|
"classification_type": item.get("CLASSIFICATION_TYPE"),
|
||||||
|
"element_set_no": item.get("ELEMENT_SET_NO"),
|
||||||
|
"rev_at_epoch": item.get("REV_AT_EPOCH"),
|
||||||
|
"bstar": item.get("BSTAR"),
|
||||||
|
"mean_motion_dot": item.get("MEAN_MOTION_DOT"),
|
||||||
|
"mean_motion_ddot": item.get("MEAN_MOTION_DDOT"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return transformed
|
||||||
|
|
||||||
|
def _get_sample_data(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Return sample TLE data for testing"""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "ISS (ZARYA)",
|
||||||
|
"norad_cat_id": 25544,
|
||||||
|
"international_designator": "1998-067A",
|
||||||
|
"epoch": "2026-03-13T00:00:00Z",
|
||||||
|
"mean_motion": 15.49872723,
|
||||||
|
"eccentricity": 0.0006292,
|
||||||
|
"inclination": 51.6400,
|
||||||
|
"raan": 315.0000,
|
||||||
|
"arg_of_perigee": 100.0000,
|
||||||
|
"mean_anomaly": 260.0000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "STARLINK-1000",
|
||||||
|
"norad_cat_id": 44720,
|
||||||
|
"international_designator": "2019-029AZ",
|
||||||
|
"epoch": "2026-03-13T00:00:00Z",
|
||||||
|
"mean_motion": 15.79234567,
|
||||||
|
"eccentricity": 0.0001234,
|
||||||
|
"inclination": 53.0000,
|
||||||
|
"raan": 120.0000,
|
||||||
|
"arg_of_perigee": 90.0000,
|
||||||
|
"mean_anomaly": 270.0000,
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -33,6 +33,8 @@ COLLECTOR_TO_ID = {
|
|||||||
"arcgis_landing_points": 16,
|
"arcgis_landing_points": 16,
|
||||||
"arcgis_cable_landing_relation": 17,
|
"arcgis_cable_landing_relation": 17,
|
||||||
"fao_landing_points": 18,
|
"fao_landing_points": 18,
|
||||||
|
"spacetrack_tle": 19,
|
||||||
|
"celestrak_tle": 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ email-validator
|
|||||||
apscheduler>=3.10.4
|
apscheduler>=3.10.4
|
||||||
pytest>=7.4.0
|
pytest>=7.4.0
|
||||||
pytest-asyncio>=0.23.0
|
pytest-asyncio>=0.23.0
|
||||||
|
networkx>=3.0
|
||||||
|
|||||||
@@ -31,45 +31,6 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: planet_backend
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/planet_db
|
|
||||||
- REDIS_URL=redis://redis:6379/0
|
|
||||||
- SECRET_KEY=your-secret-key-change-in-production
|
|
||||||
- CORS_ORIGINS=["http://localhost:3000","http://0.0.0.0:3000","http://frontend:3000"]
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: planet_frontend
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
- VITE_API_URL=http://backend:8000/api/v1
|
|
||||||
- VITE_WS_URL=ws://backend:8000/ws
|
|
||||||
depends_on:
|
|
||||||
backend:
|
|
||||||
condition: service_healthy
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|||||||
477
frontend/bun.lock
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "planet-frontend",
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.2.6",
|
||||||
|
"antd": "^5.12.5",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-resizable": "^3.1.3",
|
||||||
|
"react-router-dom": "^6.21.0",
|
||||||
|
"simplex-noise": "^4.0.1",
|
||||||
|
"socket.io-client": "^4.7.2",
|
||||||
|
"three": "^0.160.0",
|
||||||
|
"zustand": "^4.4.7",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.45",
|
||||||
|
"@types/react-dom": "^18.2.18",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@ant-design/colors": ["@ant-design/colors@7.2.1", "", { "dependencies": { "@ant-design/fast-color": "^2.0.6" } }, "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ=="],
|
||||||
|
|
||||||
|
"@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="],
|
||||||
|
|
||||||
|
"@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@1.1.3", "", { "dependencies": { "@ant-design/cssinjs": "^1.21.0", "@babel/runtime": "^7.23.2", "rc-util": "^5.38.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg=="],
|
||||||
|
|
||||||
|
"@ant-design/fast-color": ["@ant-design/fast-color@2.0.6", "", { "dependencies": { "@babel/runtime": "^7.24.7" } }, "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA=="],
|
||||||
|
|
||||||
|
"@ant-design/icons": ["@ant-design/icons@5.6.1", "", { "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", "@babel/runtime": "^7.24.8", "classnames": "^2.2.6", "rc-util": "^5.31.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg=="],
|
||||||
|
|
||||||
|
"@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="],
|
||||||
|
|
||||||
|
"@ant-design/react-slick": ["@ant-design/react-slick@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.10.4", "classnames": "^2.2.5", "json2mq": "^0.2.0", "resize-observer-polyfill": "^1.5.1", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": ">=16.9.0" } }, "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA=="],
|
||||||
|
|
||||||
|
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||||
|
|
||||||
|
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||||
|
|
||||||
|
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||||
|
|
||||||
|
"@babel/generator": ["@babel/generator@7.29.0", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||||
|
|
||||||
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||||
|
|
||||||
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
|
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||||
|
|
||||||
|
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||||
|
|
||||||
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
|
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
|
"@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="],
|
||||||
|
|
||||||
|
"@emotion/unitless": ["@emotion/unitless@0.7.5", "", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="],
|
||||||
|
|
||||||
|
"@rc-component/color-picker": ["@rc-component/color-picker@2.0.1", "", { "dependencies": { "@ant-design/fast-color": "^2.0.6", "@babel/runtime": "^7.23.6", "classnames": "^2.2.6", "rc-util": "^5.38.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q=="],
|
||||||
|
|
||||||
|
"@rc-component/context": ["@rc-component/context@1.4.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w=="],
|
||||||
|
|
||||||
|
"@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ=="],
|
||||||
|
|
||||||
|
"@rc-component/mutate-observer": ["@rc-component/mutate-observer@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw=="],
|
||||||
|
|
||||||
|
"@rc-component/portal": ["@rc-component/portal@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="],
|
||||||
|
|
||||||
|
"@rc-component/qrcode": ["@rc-component/qrcode@1.1.1", "", { "dependencies": { "@babel/runtime": "^7.24.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA=="],
|
||||||
|
|
||||||
|
"@rc-component/tour": ["@rc-component/tour@1.15.1", "", { "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/portal": "^1.0.0-9", "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ=="],
|
||||||
|
|
||||||
|
"@rc-component/trigger": ["@rc-component/trigger@2.3.1", "", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A=="],
|
||||||
|
|
||||||
|
"@remix-run/router": ["@remix-run/router@1.23.2", "", {}, "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
|
||||||
|
|
||||||
|
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||||
|
|
||||||
|
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||||
|
|
||||||
|
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||||
|
|
||||||
|
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||||
|
|
||||||
|
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
|
||||||
|
|
||||||
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||||
|
|
||||||
|
"antd": ["antd@5.29.3", "", { "dependencies": { "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", "@ant-design/icons": "^5.6.1", "@ant-design/react-slick": "~1.1.2", "@babel/runtime": "^7.26.0", "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", "@rc-component/qrcode": "~1.1.0", "@rc-component/tour": "~1.15.1", "@rc-component/trigger": "^2.3.0", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.11", "rc-cascader": "~3.34.0", "rc-checkbox": "~3.5.0", "rc-collapse": "~3.9.0", "rc-dialog": "~9.6.0", "rc-drawer": "~7.3.0", "rc-dropdown": "~4.2.1", "rc-field-form": "~2.7.1", "rc-image": "~7.12.0", "rc-input": "~1.8.0", "rc-input-number": "~9.5.0", "rc-mentions": "~2.20.0", "rc-menu": "~9.16.1", "rc-motion": "^2.9.5", "rc-notification": "~5.6.4", "rc-pagination": "~5.1.0", "rc-picker": "~4.11.3", "rc-progress": "~4.0.0", "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", "rc-select": "~14.16.8", "rc-slider": "~11.1.9", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", "rc-table": "~7.54.0", "rc-tabs": "~15.7.0", "rc-textarea": "~1.10.2", "rc-tooltip": "~6.4.0", "rc-tree": "~5.13.1", "rc-tree-select": "~5.27.0", "rc-upload": "~4.11.0", "rc-util": "^5.44.4", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="],
|
||||||
|
|
||||||
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": "dist/cli.js" }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
|
||||||
|
|
||||||
|
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="],
|
||||||
|
|
||||||
|
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
|
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"copy-to-clipboard": ["copy-to-clipboard@3.3.3", "", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="],
|
||||||
|
|
||||||
|
"engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="],
|
||||||
|
|
||||||
|
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="],
|
||||||
|
|
||||||
|
"json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
|
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||||
|
|
||||||
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
|
"rc-cascader": ["rc-cascader@3.34.0", "", { "dependencies": { "@babel/runtime": "^7.25.7", "classnames": "^2.3.1", "rc-select": "~14.16.2", "rc-tree": "~5.13.0", "rc-util": "^5.43.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag=="],
|
||||||
|
|
||||||
|
"rc-checkbox": ["rc-checkbox@3.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.25.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg=="],
|
||||||
|
|
||||||
|
"rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="],
|
||||||
|
|
||||||
|
"rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="],
|
||||||
|
|
||||||
|
"rc-drawer": ["rc-drawer@7.3.0", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@rc-component/portal": "^1.1.1", "classnames": "^2.2.6", "rc-motion": "^2.6.1", "rc-util": "^5.38.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg=="],
|
||||||
|
|
||||||
|
"rc-dropdown": ["rc-dropdown@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", "rc-util": "^5.44.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA=="],
|
||||||
|
|
||||||
|
"rc-field-form": ["rc-field-form@2.7.1", "", { "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/async-validator": "^5.0.3", "rc-util": "^5.32.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A=="],
|
||||||
|
|
||||||
|
"rc-image": ["rc-image@7.12.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", "rc-dialog": "~9.6.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q=="],
|
||||||
|
|
||||||
|
"rc-input": ["rc-input@1.8.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.18.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA=="],
|
||||||
|
|
||||||
|
"rc-input-number": ["rc-input-number@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", "rc-input": "~1.8.0", "rc-util": "^5.40.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag=="],
|
||||||
|
|
||||||
|
"rc-mentions": ["rc-mentions@2.20.0", "", { "dependencies": { "@babel/runtime": "^7.22.5", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", "rc-input": "~1.8.0", "rc-menu": "~9.16.0", "rc-textarea": "~1.10.0", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ=="],
|
||||||
|
|
||||||
|
"rc-menu": ["rc-menu@9.16.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.0.0", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.3.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg=="],
|
||||||
|
|
||||||
|
"rc-motion": ["rc-motion@2.9.5", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA=="],
|
||||||
|
|
||||||
|
"rc-notification": ["rc-notification@5.6.4", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.9.0", "rc-util": "^5.20.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw=="],
|
||||||
|
|
||||||
|
"rc-overflow": ["rc-overflow@1.5.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.37.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg=="],
|
||||||
|
|
||||||
|
"rc-pagination": ["rc-pagination@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.38.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ=="],
|
||||||
|
|
||||||
|
"rc-picker": ["rc-picker@4.11.3", "", { "dependencies": { "@babel/runtime": "^7.24.7", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.1", "rc-overflow": "^1.3.2", "rc-resize-observer": "^1.4.0", "rc-util": "^5.43.0" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "luxon", "moment"] }, "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg=="],
|
||||||
|
|
||||||
|
"rc-progress": ["rc-progress@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", "rc-util": "^5.16.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw=="],
|
||||||
|
|
||||||
|
"rc-rate": ["rc-rate@2.13.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-util": "^5.0.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q=="],
|
||||||
|
|
||||||
|
"rc-resize-observer": ["rc-resize-observer@1.4.3", "", { "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", "rc-util": "^5.44.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ=="],
|
||||||
|
|
||||||
|
"rc-segmented": ["rc-segmented@2.7.1", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-motion": "^2.4.4", "rc-util": "^5.17.0" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g=="],
|
||||||
|
|
||||||
|
"rc-select": ["rc-select@14.16.8", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.1.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-overflow": "^1.3.1", "rc-util": "^5.16.1", "rc-virtual-list": "^3.5.2" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg=="],
|
||||||
|
|
||||||
|
"rc-slider": ["rc-slider@11.1.9", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-util": "^5.36.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A=="],
|
||||||
|
|
||||||
|
"rc-steps": ["rc-steps@6.0.1", "", { "dependencies": { "@babel/runtime": "^7.16.7", "classnames": "^2.2.3", "rc-util": "^5.16.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g=="],
|
||||||
|
|
||||||
|
"rc-switch": ["rc-switch@4.1.0", "", { "dependencies": { "@babel/runtime": "^7.21.0", "classnames": "^2.2.1", "rc-util": "^5.30.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg=="],
|
||||||
|
|
||||||
|
"rc-table": ["rc-table@7.54.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/context": "^1.4.0", "classnames": "^2.2.5", "rc-resize-observer": "^1.1.0", "rc-util": "^5.44.3", "rc-virtual-list": "^3.14.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw=="],
|
||||||
|
|
||||||
|
"rc-tabs": ["rc-tabs@15.7.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", "rc-dropdown": "~4.2.0", "rc-menu": "~9.16.0", "rc-motion": "^2.6.2", "rc-resize-observer": "^1.0.0", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA=="],
|
||||||
|
|
||||||
|
"rc-textarea": ["rc-textarea@1.10.2", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", "rc-input": "~1.8.0", "rc-resize-observer": "^1.0.0", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ=="],
|
||||||
|
|
||||||
|
"rc-tooltip": ["rc-tooltip@6.4.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.1", "rc-util": "^5.44.3" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g=="],
|
||||||
|
|
||||||
|
"rc-tree": ["rc-tree@5.13.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-util": "^5.16.1", "rc-virtual-list": "^3.5.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A=="],
|
||||||
|
|
||||||
|
"rc-tree-select": ["rc-tree-select@5.27.0", "", { "dependencies": { "@babel/runtime": "^7.25.7", "classnames": "2.x", "rc-select": "~14.16.2", "rc-tree": "~5.13.0", "rc-util": "^5.43.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww=="],
|
||||||
|
|
||||||
|
"rc-upload": ["rc-upload@4.11.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "classnames": "^2.2.5", "rc-util": "^5.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA=="],
|
||||||
|
|
||||||
|
"rc-util": ["rc-util@5.44.4", "", { "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w=="],
|
||||||
|
|
||||||
|
"rc-virtual-list": ["rc-virtual-list@3.19.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "classnames": "^2.2.6", "rc-resize-observer": "^1.0.0", "rc-util": "^5.36.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA=="],
|
||||||
|
|
||||||
|
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
|
||||||
|
|
||||||
|
"react-draggable": ["react-draggable@4.5.0", "", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw=="],
|
||||||
|
|
||||||
|
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||||
|
|
||||||
|
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||||
|
|
||||||
|
"react-resizable": ["react-resizable@3.1.3", "", { "dependencies": { "prop-types": "15.x", "react-draggable": "^4.5.0" }, "peerDependencies": { "react": ">= 16.3", "react-dom": ">= 16.3" } }, "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw=="],
|
||||||
|
|
||||||
|
"react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="],
|
||||||
|
|
||||||
|
"react-router-dom": ["react-router-dom@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag=="],
|
||||||
|
|
||||||
|
"resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||||
|
|
||||||
|
"scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
|
||||||
|
|
||||||
|
"semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"simplex-noise": ["simplex-noise@4.0.3", "", {}, "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg=="],
|
||||||
|
|
||||||
|
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
|
||||||
|
|
||||||
|
"socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="],
|
||||||
|
|
||||||
|
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
|
||||||
|
|
||||||
|
"three": ["three@0.160.1", "", {}, "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ=="],
|
||||||
|
|
||||||
|
"throttle-debounce": ["throttle-debounce@5.0.2", "", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="],
|
||||||
|
|
||||||
|
"toggle-selection": ["toggle-selection@1.0.6", "", {}, "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
|
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
|
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["immer"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
|
||||||
|
|
||||||
|
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 18 MiB After Width: | Height: | Size: 18 MiB |
1
frontend/legacy/3dearthmult/landing-point-geo.geojson
Normal file
1
frontend/legacy/3dearthmult/relation.json
Normal file
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 18 MiB After Width: | Height: | Size: 18 MiB |
@@ -17,14 +17,93 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
/* user-select: none;
|
|
||||||
-webkit-user-select: none; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#container.dragging {
|
#container.dragging {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Right Toolbar Group */
|
||||||
|
#right-toolbar-group {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 290px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zoom Toolbar - Right side, vertical */
|
||||||
|
#zoom-toolbar {
|
||||||
|
position: relative;
|
||||||
|
bottom: auto;
|
||||||
|
right: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: rgba(10, 10, 30, 0.9);
|
||||||
|
padding: 8px 4px;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(77, 184, 255, 0.3);
|
||||||
|
box-shadow: 0 0 20px rgba(77, 184, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#zoom-toolbar #zoom-slider {
|
||||||
|
width: 4px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 4px 0;
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
direction: rtl;
|
||||||
|
-webkit-appearance: slider-vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zoom-toolbar .zoom-percent {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4db8ff;
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zoom-toolbar .zoom-percent:hover {
|
||||||
|
background: rgba(77, 184, 255, 0.2);
|
||||||
|
box-shadow: 0 0 10px rgba(77, 184, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#zoom-toolbar .zoom-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
min-width: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(77, 184, 255, 0.2);
|
||||||
|
color: #4db8ff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zoom-toolbar .zoom-btn:hover {
|
||||||
|
background: rgba(77, 184, 255, 0.4);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 0 10px rgba(77, 184, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
#loading {
|
#loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -106,7 +185,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
.status-message {
|
.status-message {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 260px;
|
||||||
background-color: rgba(10, 10, 30, 0.85);
|
background-color: rgba(10, 10, 30, 0.85);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
@@ -147,3 +226,139 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
display: none;
|
display: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Control Toolbar - Stellarium/Star Walk style */
|
||||||
|
#control-toolbar {
|
||||||
|
position: relative;
|
||||||
|
bottom: auto;
|
||||||
|
right: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(10, 10, 30, 0.9);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid rgba(77, 184, 255, 0.3);
|
||||||
|
box-shadow: 0 0 20px rgba(77, 184, 255, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-toolbar.collapsed {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-toolbar.collapsed .toolbar-items {
|
||||||
|
width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbar-toggle {
|
||||||
|
min-width: 28px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-arrow {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4db8ff;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-toolbar.collapsed .toggle-arrow {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-toolbar:not(.collapsed) .toggle-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#control-toolbar.collapsed #toolbar-toggle {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-items {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
width: auto;
|
||||||
|
padding: 0 4px 0 2px;
|
||||||
|
overflow: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-right: 1px solid rgba(77, 184, 255, 0.3);
|
||||||
|
margin-right: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
position: relative;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(77, 184, 255, 0.15);
|
||||||
|
color: #4db8ff;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background: rgba(77, 184, 255, 0.35);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 0 15px rgba(77, 184, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.active {
|
||||||
|
background: rgba(77, 184, 255, 0.4);
|
||||||
|
box-shadow: 0 0 10px rgba(77, 184, 255, 0.4) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn .tooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 50px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(10, 10, 30, 0.95);
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid rgba(77, 184, 255, 0.4);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover .tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
bottom: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn .tooltip::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 6px solid transparent;
|
||||||
|
border-top-color: rgba(77, 184, 255, 0.4);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#coordinates-display {
|
#coordinates-display {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 250px;
|
right: 20px;
|
||||||
background-color: rgba(10, 10, 30, 0.85);
|
background-color: rgba(10, 10, 30, 0.85);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
|
|||||||
@@ -29,3 +29,33 @@
|
|||||||
color: #4db8ff;
|
color: #4db8ff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#satellite-info {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 290px;
|
||||||
|
background-color: rgba(10, 10, 30, 0.9);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
width: 220px;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 229, 255, 0.3);
|
||||||
|
border: 1px solid rgba(0, 229, 255, 0.3);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#satellite-info .stats-item {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#satellite-info .stats-label {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#satellite-info .stats-value {
|
||||||
|
color: #00e5ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,11 +95,153 @@
|
|||||||
|
|
||||||
#info-panel .zoom-buttons {
|
#info-panel .zoom-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-panel .zoom-percent-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-panel .zoom-percent {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4db8ff;
|
||||||
|
min-width: 70px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-panel .zoom-percent:hover {
|
||||||
|
background: rgba(77, 184, 255, 0.2);
|
||||||
|
box-shadow: 0 0 10px rgba(77, 184, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-panel .zoom-buttons .zoom-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(77, 184, 255, 0.2);
|
||||||
|
color: #4db8ff;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-panel .zoom-buttons .zoom-btn:hover {
|
||||||
|
background: rgba(77, 184, 255, 0.4);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 0 10px rgba(77, 184, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#info-panel .zoom-buttons button {
|
#info-panel .zoom-buttons button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Info Card - Unified details panel (inside info-panel) */
|
||||||
|
.info-card {
|
||||||
|
margin-top: 15px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.no-border {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: rgba(77, 184, 255, 0.1);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-header h3 {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #4db8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-content {
|
||||||
|
padding: 10px 12px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-property {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-property:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-label {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-value {
|
||||||
|
color: #4db8ff;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: right;
|
||||||
|
max-width: 180px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cable type */
|
||||||
|
.info-card.cable {
|
||||||
|
border-color: rgba(255, 200, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.cable .info-card-header {
|
||||||
|
background: rgba(255, 200, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.cable .info-card-header h3 {
|
||||||
|
color: #ffc800;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Satellite type */
|
||||||
|
.info-card.satellite {
|
||||||
|
border-color: rgba(0, 229, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.satellite .info-card-header {
|
||||||
|
background: rgba(0, 229, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.satellite .info-card-header h3 {
|
||||||
|
color: #00e5ff;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>3D球形地图 - 海底电缆系统</title>
|
<title>智能星球计划 - 现实层宇宙全息感知</title>
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
"three": "https://esm.sh/three@0.128.0",
|
"three": "https://esm.sh/three@0.128.0",
|
||||||
"simplex-noise": "https://esm.sh/simplex-noise@4.0.1"
|
"simplex-noise": "https://esm.sh/simplex-noise@4.0.1",
|
||||||
|
"satellite.js": "https://esm.sh/satellite.js@5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -21,57 +22,41 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div id="info-panel">
|
<div id="info-panel">
|
||||||
<h1>全球海底电缆系统</h1>
|
<h1>智能星球计划</h1>
|
||||||
<div class="subtitle">3D地形球形地图可视化 | 高分辨率卫星图</div>
|
<div class="subtitle">现实层宇宙全息感知系统 | 卫星 · 海底光缆 · 算力基础设施</div>
|
||||||
<div class="zoom-controls">
|
|
||||||
<div style="width: 100%;">
|
<div id="info-card" class="info-card" style="display: none;">
|
||||||
<h3 style="color:#4db8ff; margin-bottom:10px; font-size:1.1rem;">缩放控制</h3>
|
<div class="info-card-header">
|
||||||
<div class="zoom-buttons">
|
<span class="info-card-icon" id="info-card-icon">🛰️</span>
|
||||||
<button id="zoom-in">放大</button>
|
<h3 id="info-card-title">详情</h3>
|
||||||
<button id="zoom-out">缩小</button>
|
|
||||||
<button id="zoom-reset">重置</button>
|
|
||||||
</div>
|
|
||||||
<div class="slider-container" style="margin-top: 10px;">
|
|
||||||
<div class="slider-label">
|
|
||||||
<span>缩放级别:</span>
|
|
||||||
<span id="zoom-value">1.0x</span>
|
|
||||||
</div>
|
|
||||||
<input type="range" id="zoom-slider" min="0.5" max="5" step="0.1" value="1">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="info-card-content"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="cable-details" class="cable-info">
|
|
||||||
<h3 id="cable-name">点击电缆查看详情</h3>
|
|
||||||
<div class="cable-property">
|
|
||||||
<span class="property-label">所有者:</span>
|
|
||||||
<span id="cable-owner" class="property-value">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="cable-property">
|
|
||||||
<span class="property-label">状态:</span>
|
|
||||||
<span id="cable-status" class="property-value">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="cable-property">
|
|
||||||
<span class="property-label">长度:</span>
|
|
||||||
<span id="cable-length" class="property-value">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="cable-property">
|
|
||||||
<span class="property-label">经纬度:</span>
|
|
||||||
<span id="cable-coords" class="property-value">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="cable-property">
|
|
||||||
<span class="property-label">投入使用时间:</span>
|
|
||||||
<span id="cable-rfs" class="property-value">-</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<button id="rotate-toggle">暂停旋转</button>
|
|
||||||
<button id="reset-view">重置视图</button>
|
|
||||||
<button id="toggle-terrain">显示地形</button>
|
|
||||||
<button id="reload-data">重新加载数据</button>
|
|
||||||
</div>
|
|
||||||
<div id="error-message" class="error-message"></div>
|
<div id="error-message" class="error-message"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="right-toolbar-group">
|
||||||
|
<div id="zoom-toolbar">
|
||||||
|
<button id="reset-view" class="zoom-btn">📍</button>
|
||||||
|
<button id="zoom-in" class="zoom-btn">+</button>
|
||||||
|
<span id="zoom-value" class="zoom-percent">100%</span>
|
||||||
|
<button id="zoom-out" class="zoom-btn">−</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="control-toolbar">
|
||||||
|
<div class="toolbar-items">
|
||||||
|
<button id="rotate-toggle" class="toolbar-btn" title="自动旋转">🔄<span class="tooltip">自动旋转</span></button>
|
||||||
|
<button id="toggle-cables" class="toolbar-btn active" title="显示/隐藏线缆">🌐<span class="tooltip">隐藏线缆</span></button>
|
||||||
|
<button id="toggle-terrain" class="toolbar-btn" title="显示/隐藏地形">⛰️<span class="tooltip">显示/隐藏地形</span></button>
|
||||||
|
<button id="toggle-satellites" class="toolbar-btn active" title="显示/隐藏卫星">🛰️<span class="tooltip">隐藏卫星</span></button>
|
||||||
|
<button id="toggle-trails" class="toolbar-btn" title="显示/隐藏轨迹">✨<span class="tooltip">显示/隐藏轨迹</span></button>
|
||||||
|
<button id="reload-data" class="toolbar-btn" title="重新加载数据">🔃<span class="tooltip">重新加载数据</span></button>
|
||||||
|
</div>
|
||||||
|
<button id="toolbar-toggle" class="toolbar-btn" title="展开/收起工具栏"><span class="toggle-arrow">◀</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="coordinates-display">
|
<div id="coordinates-display">
|
||||||
<h3 style="color:#4db8ff; margin-bottom:8px; font-size:1.1rem;">坐标信息</h3>
|
<h3 style="color:#4db8ff; margin-bottom:8px; font-size:1.1rem;">坐标信息</h3>
|
||||||
<div class="coord-item">
|
<div class="coord-item">
|
||||||
@@ -124,6 +109,10 @@
|
|||||||
<span class="stats-label">地形:</span>
|
<span class="stats-label">地形:</span>
|
||||||
<span class="stats-value" id="terrain-status">开启</span>
|
<span class="stats-value" id="terrain-status">开启</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stats-item">
|
||||||
|
<span class="stats-label">卫星:</span>
|
||||||
|
<span class="stats-value" id="satellite-count">0 颗</span>
|
||||||
|
</div>
|
||||||
<div class="stats-item">
|
<div class="stats-item">
|
||||||
<span class="stats-label">视角距离:</span>
|
<span class="stats-label">视角距离:</span>
|
||||||
<span class="stats-value" id="camera-distance">300 km</span>
|
<span class="stats-value" id="camera-distance">300 km</span>
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
import { CONFIG, CABLE_COLORS, PATHS } from './constants.js';
|
import { CONFIG, CABLE_COLORS, PATHS, CABLE_STATE } from './constants.js';
|
||||||
import { latLonToVector3 } from './utils.js';
|
import { latLonToVector3 } from './utils.js';
|
||||||
import { updateCableDetails, updateEarthStats, showStatusMessage } from './ui.js';
|
import { updateEarthStats, showStatusMessage } from './ui.js';
|
||||||
|
import { showInfoCard } from './info-card.js';
|
||||||
|
|
||||||
export let cableLines = [];
|
export let cableLines = [];
|
||||||
export let landingPoints = [];
|
export let landingPoints = [];
|
||||||
export let lockedCable = null;
|
export let lockedCable = null;
|
||||||
let cableIdMap = new Map();
|
let cableIdMap = new Map();
|
||||||
|
let cablesVisible = true;
|
||||||
|
|
||||||
function getCableColor(properties) {
|
function getCableColor(properties) {
|
||||||
if (properties.color) {
|
if (properties.color) {
|
||||||
@@ -285,7 +287,7 @@ export async function loadLandingPoints(scene, earthObj) {
|
|||||||
sphere.userData = {
|
sphere.userData = {
|
||||||
type: 'landingPoint',
|
type: 'landingPoint',
|
||||||
name: properties.name || '未知登陆站',
|
name: properties.name || '未知登陆站',
|
||||||
cableName: properties.cable_system || '未知系统',
|
cableNames: properties.cable_names || [],
|
||||||
country: properties.country || '未知国家',
|
country: properties.country || '未知国家',
|
||||||
status: properties.status || 'Unknown'
|
status: properties.status || 'Unknown'
|
||||||
};
|
};
|
||||||
@@ -312,8 +314,7 @@ export function handleCableClick(cable) {
|
|||||||
lockedCable = cable;
|
lockedCable = cable;
|
||||||
|
|
||||||
const data = cable.userData;
|
const data = cable.userData;
|
||||||
// console.log(data)
|
showInfoCard('cable', {
|
||||||
updateCableDetails({
|
|
||||||
name: data.name,
|
name: data.name,
|
||||||
owner: data.owner,
|
owner: data.owner,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
@@ -327,14 +328,6 @@ export function handleCableClick(cable) {
|
|||||||
|
|
||||||
export function clearCableSelection() {
|
export function clearCableSelection() {
|
||||||
lockedCable = null;
|
lockedCable = null;
|
||||||
updateCableDetails({
|
|
||||||
name: '点击电缆查看详情',
|
|
||||||
owner: '-',
|
|
||||||
status: '-',
|
|
||||||
length: '-',
|
|
||||||
coords: '-',
|
|
||||||
rfs: '-'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCableLines() {
|
export function getCableLines() {
|
||||||
@@ -348,3 +341,83 @@ export function getCablesById(cableId) {
|
|||||||
export function getLandingPoints() {
|
export function getLandingPoints() {
|
||||||
return landingPoints;
|
return landingPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cableStates = new Map();
|
||||||
|
|
||||||
|
export function getCableState(cableId) {
|
||||||
|
return cableStates.get(cableId) || CABLE_STATE.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCableState(cableId, state) {
|
||||||
|
cableStates.set(cableId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAllCableStates() {
|
||||||
|
cableStates.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCableStateInfo() {
|
||||||
|
const states = {};
|
||||||
|
cableStates.forEach((state, cableId) => {
|
||||||
|
states[cableId] = state;
|
||||||
|
});
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLandingPointsByCableName(cableName) {
|
||||||
|
return landingPoints.filter(lp => lp.userData.cableNames?.includes(cableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllLandingPoints() {
|
||||||
|
return landingPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyLandingPointVisualState(lockedCableName, dimAll = false) {
|
||||||
|
const pulse = (Math.sin(Date.now() * 0.003) + 1) * 0.5;
|
||||||
|
const brightness = 0.3;
|
||||||
|
|
||||||
|
landingPoints.forEach(lp => {
|
||||||
|
const isRelated = !dimAll && lp.userData.cableNames?.includes(lockedCableName);
|
||||||
|
|
||||||
|
if (isRelated) {
|
||||||
|
lp.material.color.setHex(0xffaa00);
|
||||||
|
lp.material.emissive.setHex(0x442200);
|
||||||
|
lp.material.emissiveIntensity = 0.5 + pulse * 0.5;
|
||||||
|
lp.material.opacity = 0.8 + pulse * 0.2;
|
||||||
|
lp.scale.setScalar(1.2 + pulse * 0.3);
|
||||||
|
} else {
|
||||||
|
const r = 255 * brightness;
|
||||||
|
const g = 170 * brightness;
|
||||||
|
const b = 0 * brightness;
|
||||||
|
lp.material.color.setRGB(r / 255, g / 255, b / 255);
|
||||||
|
lp.material.emissive.setHex(0x000000);
|
||||||
|
lp.material.emissiveIntensity = 0;
|
||||||
|
lp.material.opacity = 0.3;
|
||||||
|
lp.scale.setScalar(1.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetLandingPointVisualState() {
|
||||||
|
landingPoints.forEach(lp => {
|
||||||
|
lp.material.color.setHex(0xffaa00);
|
||||||
|
lp.material.emissive.setHex(0x442200);
|
||||||
|
lp.material.emissiveIntensity = 0.5;
|
||||||
|
lp.material.opacity = 1.0;
|
||||||
|
lp.scale.setScalar(1.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleCables(show) {
|
||||||
|
cablesVisible = show;
|
||||||
|
cableLines.forEach(cable => {
|
||||||
|
cable.visible = cablesVisible;
|
||||||
|
});
|
||||||
|
landingPoints.forEach(lp => {
|
||||||
|
lp.visible = cablesVisible;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShowCables() {
|
||||||
|
return cablesVisible;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,21 @@ export const CONFIG = {
|
|||||||
minZoom: 0.5,
|
minZoom: 0.5,
|
||||||
maxZoom: 5.0,
|
maxZoom: 5.0,
|
||||||
earthRadius: 100,
|
earthRadius: 100,
|
||||||
rotationSpeed: 0.002,
|
rotationSpeed: 0.0005,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Earth coordinate constants
|
||||||
|
export const EARTH_CONFIG = {
|
||||||
|
tilt: 23.5, // earth tilt angle (degrees)
|
||||||
|
tiltRad: 23.5 * Math.PI / 180, // earth tilt angle (radians)
|
||||||
|
|
||||||
|
// hangzhou coordinates
|
||||||
|
chinaLat: 30.2741,
|
||||||
|
chinaLon: 120.1552,
|
||||||
|
chinaRotLon: 120.1552 - 270, // for rotation calculation (chinaLon - 270)
|
||||||
|
|
||||||
|
// view reset coefficient
|
||||||
|
latCoefficient: 0.5
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PATHS = {
|
export const PATHS = {
|
||||||
@@ -24,7 +38,28 @@ export const CABLE_COLORS = {
|
|||||||
'default': 0xffff44
|
'default': 0xffff44
|
||||||
};
|
};
|
||||||
|
|
||||||
// Grid configuration
|
export const CABLE_CONFIG = {
|
||||||
|
lockedOpacityMin: 0.2,
|
||||||
|
lockedOpacityMax: 1.0,
|
||||||
|
otherOpacity: 0.5,
|
||||||
|
otherBrightness: 0.6,
|
||||||
|
pulseSpeed: 0.008,
|
||||||
|
pulseCoefficient: 0.4
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CABLE_STATE = {
|
||||||
|
NORMAL: 'normal',
|
||||||
|
HOVERED: 'hovered',
|
||||||
|
LOCKED: 'locked'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SATELLITE_CONFIG = {
|
||||||
|
maxCount: 2000,
|
||||||
|
dotSize: 1.5,
|
||||||
|
trailLength: 30,
|
||||||
|
apiPath: '/api/v1/visualization/geo/satellites'
|
||||||
|
};
|
||||||
|
|
||||||
export const GRID_CONFIG = {
|
export const GRID_CONFIG = {
|
||||||
latitudeStep: 10,
|
latitudeStep: 10,
|
||||||
longitudeStep: 30,
|
longitudeStep: 30,
|
||||||
|
|||||||
264
frontend/public/earth/js/controls.js
vendored
@@ -1,8 +1,11 @@
|
|||||||
// controls.js - Zoom, rotate and toggle controls
|
// controls.js - Zoom, rotate and toggle controls
|
||||||
|
|
||||||
import { CONFIG } from './constants.js';
|
import { CONFIG, EARTH_CONFIG } from './constants.js';
|
||||||
import { updateZoomDisplay, showStatusMessage } from './ui.js';
|
import { updateZoomDisplay, showStatusMessage } from './ui.js';
|
||||||
import { toggleTerrain } from './earth.js';
|
import { toggleTerrain } from './earth.js';
|
||||||
|
import { reloadData } from './main.js';
|
||||||
|
import { toggleSatellites, toggleTrails, getShowSatellites, getSatelliteCount } from './satellites.js';
|
||||||
|
import { toggleCables, getShowCables } from './cables.js';
|
||||||
|
|
||||||
export let autoRotate = true;
|
export let autoRotate = true;
|
||||||
export let zoomLevel = 1.0;
|
export let zoomLevel = 1.0;
|
||||||
@@ -20,26 +23,100 @@ export function setupControls(camera, renderer, scene, earth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupZoomControls(camera) {
|
function setupZoomControls(camera) {
|
||||||
document.getElementById('zoom-in').addEventListener('click', () => {
|
let zoomInterval = null;
|
||||||
zoomLevel = Math.min(zoomLevel + 0.5, CONFIG.maxZoom);
|
let holdTimeout = null;
|
||||||
applyZoom(camera);
|
let startTime = 0;
|
||||||
});
|
const HOLD_THRESHOLD = 150;
|
||||||
|
const LONG_PRESS_TICK = 50;
|
||||||
|
const CLICK_STEP = 10;
|
||||||
|
|
||||||
document.getElementById('zoom-out').addEventListener('click', () => {
|
const MIN_PERCENT = CONFIG.minZoom * 100;
|
||||||
zoomLevel = Math.max(zoomLevel - 0.5, CONFIG.minZoom);
|
const MAX_PERCENT = CONFIG.maxZoom * 100;
|
||||||
applyZoom(camera);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('zoom-reset').addEventListener('click', () => {
|
function doZoomStep(direction) {
|
||||||
zoomLevel = 1.0;
|
let currentPercent = Math.round(zoomLevel * 100);
|
||||||
|
let newPercent = direction > 0 ? currentPercent + CLICK_STEP : currentPercent - CLICK_STEP;
|
||||||
|
|
||||||
|
if (newPercent > MAX_PERCENT) newPercent = MAX_PERCENT;
|
||||||
|
if (newPercent < MIN_PERCENT) newPercent = MIN_PERCENT;
|
||||||
|
|
||||||
|
zoomLevel = newPercent / 100;
|
||||||
applyZoom(camera);
|
applyZoom(camera);
|
||||||
showStatusMessage('缩放已重置', 'info');
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const slider = document.getElementById('zoom-slider');
|
function doContinuousZoom(direction) {
|
||||||
slider?.addEventListener('input', (e) => {
|
let currentPercent = Math.round(zoomLevel * 100);
|
||||||
zoomLevel = parseFloat(e.target.value);
|
let newPercent = direction > 0 ? currentPercent + 1 : currentPercent - 1;
|
||||||
|
|
||||||
|
if (newPercent > MAX_PERCENT) newPercent = MAX_PERCENT;
|
||||||
|
if (newPercent < MIN_PERCENT) newPercent = MIN_PERCENT;
|
||||||
|
|
||||||
|
zoomLevel = newPercent / 100;
|
||||||
applyZoom(camera);
|
applyZoom(camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startContinuousZoom(direction) {
|
||||||
|
doContinuousZoom(direction);
|
||||||
|
zoomInterval = setInterval(() => {
|
||||||
|
doContinuousZoom(direction);
|
||||||
|
}, LONG_PRESS_TICK);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopZoom() {
|
||||||
|
if (zoomInterval) {
|
||||||
|
clearInterval(zoomInterval);
|
||||||
|
zoomInterval = null;
|
||||||
|
}
|
||||||
|
if (holdTimeout) {
|
||||||
|
clearTimeout(holdTimeout);
|
||||||
|
holdTimeout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(direction) {
|
||||||
|
startTime = Date.now();
|
||||||
|
stopZoom();
|
||||||
|
holdTimeout = setTimeout(() => {
|
||||||
|
startContinuousZoom(direction);
|
||||||
|
}, HOLD_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(direction) {
|
||||||
|
const heldTime = Date.now() - startTime;
|
||||||
|
stopZoom();
|
||||||
|
if (heldTime < HOLD_THRESHOLD) {
|
||||||
|
doZoomStep(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('zoom-in').addEventListener('mousedown', () => handleMouseDown(1));
|
||||||
|
document.getElementById('zoom-in').addEventListener('mouseup', () => handleMouseUp(1));
|
||||||
|
document.getElementById('zoom-in').addEventListener('mouseleave', stopZoom);
|
||||||
|
document.getElementById('zoom-in').addEventListener('touchstart', (e) => { e.preventDefault(); handleMouseDown(1); });
|
||||||
|
document.getElementById('zoom-in').addEventListener('touchend', () => handleMouseUp(1));
|
||||||
|
|
||||||
|
document.getElementById('zoom-out').addEventListener('mousedown', () => handleMouseDown(-1));
|
||||||
|
document.getElementById('zoom-out').addEventListener('mouseup', () => handleMouseUp(-1));
|
||||||
|
document.getElementById('zoom-out').addEventListener('mouseleave', stopZoom);
|
||||||
|
document.getElementById('zoom-out').addEventListener('touchstart', (e) => { e.preventDefault(); handleMouseDown(-1); });
|
||||||
|
document.getElementById('zoom-out').addEventListener('touchend', () => handleMouseUp(-1));
|
||||||
|
|
||||||
|
document.getElementById('zoom-value').addEventListener('click', function() {
|
||||||
|
const startZoomVal = zoomLevel;
|
||||||
|
const targetZoom = 1.0;
|
||||||
|
const startDistance = CONFIG.defaultCameraZ / startZoomVal;
|
||||||
|
const targetDistance = CONFIG.defaultCameraZ / targetZoom;
|
||||||
|
|
||||||
|
animateValue(0, 1, 600, (progress) => {
|
||||||
|
const ease = 1 - Math.pow(1 - progress, 3);
|
||||||
|
zoomLevel = startZoomVal + (targetZoom - startZoomVal) * ease;
|
||||||
|
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||||
|
const distance = startDistance + (targetDistance - startDistance) * ease;
|
||||||
|
updateZoomDisplay(zoomLevel, distance.toFixed(0));
|
||||||
|
}, () => {
|
||||||
|
zoomLevel = 1.0;
|
||||||
|
showStatusMessage('缩放已重置到100%', 'info');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,46 +162,14 @@ function animateValue(start, end, duration, onUpdate, onComplete) {
|
|||||||
export function resetView(camera) {
|
export function resetView(camera) {
|
||||||
if (!earthObj) return;
|
if (!earthObj) return;
|
||||||
|
|
||||||
const startRotX = earthObj.rotation.x;
|
function animateToView(targetLat, targetLon, targetRotLon) {
|
||||||
const startRotY = earthObj.rotation.y;
|
const latRot = targetLat * Math.PI / 180;
|
||||||
const startZoom = zoomLevel;
|
const targetRotX = EARTH_CONFIG.tiltRad + latRot * EARTH_CONFIG.latCoefficient;
|
||||||
const targetRotX = 23.5 * Math.PI / 180;
|
const targetRotY = -(targetRotLon * Math.PI / 180);
|
||||||
const targetRotY = 0;
|
|
||||||
const targetZoom = 1.0;
|
|
||||||
|
|
||||||
animateValue(0, 1, 800, (progress) => {
|
|
||||||
const ease = 1 - Math.pow(1 - progress, 3);
|
|
||||||
earthObj.rotation.x = startRotX + (targetRotX - startRotX) * ease;
|
|
||||||
earthObj.rotation.y = startRotY + (targetRotY - startRotY) * ease;
|
|
||||||
|
|
||||||
zoomLevel = startZoom + (targetZoom - startZoom) * ease;
|
|
||||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
|
||||||
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
|
|
||||||
}, () => {
|
|
||||||
zoomLevel = 1.0;
|
|
||||||
showStatusMessage('视图已重置', 'info');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof window.clearLockedCable === 'function') {
|
|
||||||
window.clearLockedCable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupRotateControls(camera, earth) {
|
|
||||||
document.getElementById('rotate-toggle').addEventListener('click', () => {
|
|
||||||
toggleAutoRotate();
|
|
||||||
const isOn = autoRotate;
|
|
||||||
showStatusMessage(isOn ? '自动旋转已开启' : '自动旋转已暂停', 'info');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('reset-view').addEventListener('click', () => {
|
|
||||||
if (!earthObj) return;
|
|
||||||
|
|
||||||
const startRotX = earthObj.rotation.x;
|
const startRotX = earthObj.rotation.x;
|
||||||
const startRotY = earthObj.rotation.y;
|
const startRotY = earthObj.rotation.y;
|
||||||
const startZoom = zoomLevel;
|
const startZoom = zoomLevel;
|
||||||
const targetRotX = 23.5 * Math.PI / 180;
|
|
||||||
const targetRotY = 0;
|
|
||||||
const targetZoom = 1.0;
|
const targetZoom = 1.0;
|
||||||
|
|
||||||
animateValue(0, 1, 800, (progress) => {
|
animateValue(0, 1, 800, (progress) => {
|
||||||
@@ -137,71 +182,112 @@ function setupRotateControls(camera, earth) {
|
|||||||
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
|
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
|
||||||
}, () => {
|
}, () => {
|
||||||
zoomLevel = 1.0;
|
zoomLevel = 1.0;
|
||||||
showStatusMessage('视图已重置', 'info');
|
showStatusMessage('视角已重置', 'info');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.geolocation) {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos) => animateToView(pos.coords.latitude, pos.coords.longitude, -pos.coords.longitude),
|
||||||
|
() => animateToView(EARTH_CONFIG.chinaLat, EARTH_CONFIG.chinaLon, EARTH_CONFIG.chinaRotLon),
|
||||||
|
{ timeout: 5000, enableHighAccuracy: false }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
animateToView(EARTH_CONFIG.chinaLat, EARTH_CONFIG.chinaLon, EARTH_CONFIG.chinaRotLon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.clearLockedCable === 'function') {
|
||||||
|
window.clearLockedCable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRotateControls(camera, earth) {
|
||||||
|
const rotateBtn = document.getElementById('rotate-toggle');
|
||||||
|
|
||||||
|
rotateBtn.addEventListener('click', function() {
|
||||||
|
const isRotating = toggleAutoRotate();
|
||||||
|
showStatusMessage(isRotating ? '自动旋转已开启' : '自动旋转已暂停', 'info');
|
||||||
|
});
|
||||||
|
|
||||||
|
updateRotateUI();
|
||||||
|
|
||||||
|
document.getElementById('reset-view').addEventListener('click', function() {
|
||||||
|
resetView(camera);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTerrainControls() {
|
function setupTerrainControls() {
|
||||||
document.getElementById('toggle-terrain').addEventListener('click', () => {
|
document.getElementById('toggle-terrain').addEventListener('click', function() {
|
||||||
showTerrain = !showTerrain;
|
showTerrain = !showTerrain;
|
||||||
toggleTerrain(showTerrain);
|
toggleTerrain(showTerrain);
|
||||||
const btn = document.getElementById('toggle-terrain');
|
this.classList.toggle('active', showTerrain);
|
||||||
btn.textContent = showTerrain ? '隐藏地形' : '显示地形';
|
this.querySelector('.tooltip').textContent = showTerrain ? '隐藏地形' : '显示地形';
|
||||||
|
document.getElementById('terrain-status').textContent = showTerrain ? '开启' : '关闭';
|
||||||
showStatusMessage(showTerrain ? '地形已显示' : '地形已隐藏', 'info');
|
showStatusMessage(showTerrain ? '地形已显示' : '地形已隐藏', 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('reload-data').addEventListener('click', () => {
|
document.getElementById('toggle-satellites').addEventListener('click', function() {
|
||||||
showStatusMessage('重新加载数据...', 'info');
|
const showSats = !getShowSatellites();
|
||||||
window.location.reload();
|
toggleSatellites(showSats);
|
||||||
});
|
this.classList.toggle('active', showSats);
|
||||||
}
|
this.querySelector('.tooltip').textContent = showSats ? '隐藏卫星' : '显示卫星';
|
||||||
|
document.getElementById('satellite-count').textContent = getSatelliteCount() + ' 颗';
|
||||||
function setupMouseControls(camera, renderer) {
|
showStatusMessage(showSats ? '卫星已显示' : '卫星已隐藏', 'info');
|
||||||
let previousMousePosition = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
renderer.domElement.addEventListener('mousedown', (e) => {
|
|
||||||
isDragging = true;
|
|
||||||
previousMousePosition = { x: e.clientX, y: e.clientY };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
renderer.domElement.addEventListener('mouseup', () => {
|
document.getElementById('toggle-trails').addEventListener('click', function() {
|
||||||
isDragging = false;
|
const isActive = this.classList.contains('active');
|
||||||
|
const showTrails = !isActive;
|
||||||
|
toggleTrails(showTrails);
|
||||||
|
this.classList.toggle('active', showTrails);
|
||||||
|
this.querySelector('.tooltip').textContent = showTrails ? '隐藏轨迹' : '显示轨迹';
|
||||||
|
showStatusMessage(showTrails ? '轨迹已显示' : '轨迹已隐藏', 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
renderer.domElement.addEventListener('mousemove', (e) => {
|
document.getElementById('toggle-cables').addEventListener('click', function() {
|
||||||
if (isDragging) {
|
const showCables = !getShowCables();
|
||||||
const deltaX = e.clientX - previousMousePosition.x;
|
toggleCables(showCables);
|
||||||
const deltaY = e.clientY - previousMousePosition.y;
|
this.classList.toggle('active', showCables);
|
||||||
|
this.querySelector('.tooltip').textContent = showCables ? '隐藏线缆' : '显示线缆';
|
||||||
if (earth) {
|
showStatusMessage(showCables ? '线缆已显示' : '线缆已隐藏', 'info');
|
||||||
earth.rotation.y += deltaX * 0.005;
|
|
||||||
earth.rotation.x += deltaY * 0.005;
|
|
||||||
}
|
|
||||||
|
|
||||||
previousMousePosition = { x: e.clientX, y: e.clientY };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('reload-data').addEventListener('click', async () => {
|
||||||
|
await reloadData();
|
||||||
|
showStatusMessage('数据已重新加载', 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolbarToggle = document.getElementById('toolbar-toggle');
|
||||||
|
const toolbar = document.getElementById('control-toolbar');
|
||||||
|
if (toolbarToggle && toolbar) {
|
||||||
|
toolbarToggle.addEventListener('click', () => {
|
||||||
|
toolbar.classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAutoRotate() {
|
export function getAutoRotate() {
|
||||||
return autoRotate;
|
return autoRotate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setAutoRotate(value) {
|
function updateRotateUI() {
|
||||||
autoRotate = value;
|
|
||||||
const btn = document.getElementById('rotate-toggle');
|
const btn = document.getElementById('rotate-toggle');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.textContent = autoRotate ? '暂停旋转' : '开始旋转';
|
btn.classList.toggle('active', autoRotate);
|
||||||
|
btn.innerHTML = autoRotate ? '⏸️' : '▶️';
|
||||||
|
const tooltip = btn.querySelector('.tooltip');
|
||||||
|
if (tooltip) tooltip.textContent = autoRotate ? '暂停旋转' : '开始旋转';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setAutoRotate(value) {
|
||||||
|
autoRotate = value;
|
||||||
|
updateRotateUI();
|
||||||
|
}
|
||||||
|
|
||||||
export function toggleAutoRotate() {
|
export function toggleAutoRotate() {
|
||||||
autoRotate = !autoRotate;
|
autoRotate = !autoRotate;
|
||||||
const btn = document.getElementById('rotate-toggle');
|
updateRotateUI();
|
||||||
if (btn) {
|
|
||||||
btn.textContent = autoRotate ? '暂停旋转' : '开始旋转';
|
|
||||||
}
|
|
||||||
if (window.clearLockedCable) {
|
if (window.clearLockedCable) {
|
||||||
window.clearLockedCable();
|
window.clearLockedCable();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// earth.js - 3D Earth creation module
|
// earth.js - 3D Earth creation module
|
||||||
|
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { CONFIG } from './constants.js';
|
import { CONFIG, EARTH_CONFIG } from './constants.js';
|
||||||
import { latLonToVector3 } from './utils.js';
|
import { latLonToVector3 } from './utils.js';
|
||||||
|
|
||||||
export let earth = null;
|
export let earth = null;
|
||||||
@@ -22,13 +22,13 @@ export function createEarth(scene) {
|
|||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
side: THREE.DoubleSide
|
side: THREE.DoubleSide
|
||||||
});
|
});
|
||||||
|
|
||||||
earth = new THREE.Mesh(geometry, material);
|
earth = new THREE.Mesh(geometry, material);
|
||||||
earth.rotation.x = 23.5 * Math.PI / 180;
|
earth.rotation.x = EARTH_CONFIG.tiltRad;
|
||||||
scene.add(earth);
|
scene.add(earth);
|
||||||
|
|
||||||
const textureUrls = [
|
const textureUrls = [
|
||||||
'./8k_earth_daymap.jpg',
|
'./assets/8k_earth_daymap.jpg',
|
||||||
'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/planets/earth_atmos_2048.jpg',
|
'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/planets/earth_atmos_2048.jpg',
|
||||||
'https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg',
|
'https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg',
|
||||||
'https://assets.codepen.io/982762/earth_texture_2048.jpg'
|
'https://assets.codepen.io/982762/earth_texture_2048.jpg'
|
||||||
|
|||||||
121
frontend/public/earth/js/info-card.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// info-card.js - Unified info card module
|
||||||
|
|
||||||
|
let currentType = null;
|
||||||
|
|
||||||
|
const CARD_CONFIG = {
|
||||||
|
cable: {
|
||||||
|
icon: '🛥️',
|
||||||
|
title: '电缆详情',
|
||||||
|
className: 'cable',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称' },
|
||||||
|
{ key: 'owner', label: '所有者' },
|
||||||
|
{ key: 'status', label: '状态' },
|
||||||
|
{ key: 'length', label: '长度' },
|
||||||
|
{ key: 'coords', label: '经纬度' },
|
||||||
|
{ key: 'rfs', label: '投入使用' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
satellite: {
|
||||||
|
icon: '🛰️',
|
||||||
|
title: '卫星详情',
|
||||||
|
className: 'satellite',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称' },
|
||||||
|
{ key: 'norad_id', label: 'NORAD ID' },
|
||||||
|
{ key: 'inclination', label: '倾角', unit: '°' },
|
||||||
|
{ key: 'period', label: '周期', unit: '分钟' },
|
||||||
|
{ key: 'perigee', label: '近地点', unit: 'km' },
|
||||||
|
{ key: 'apogee', label: '远地点', unit: 'km' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
supercomputer: {
|
||||||
|
icon: '🖥️',
|
||||||
|
title: '超算详情',
|
||||||
|
className: 'supercomputer',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称' },
|
||||||
|
{ key: 'rank', label: '排名' },
|
||||||
|
{ key: 'r_max', label: 'Rmax', unit: 'GFlops' },
|
||||||
|
{ key: 'r_peak', label: 'Rpeak', unit: 'GFlops' },
|
||||||
|
{ key: 'country', label: '国家' },
|
||||||
|
{ key: 'city', label: '城市' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
gpu_cluster: {
|
||||||
|
icon: '🎮',
|
||||||
|
title: 'GPU集群详情',
|
||||||
|
className: 'gpu_cluster',
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', label: '名称' },
|
||||||
|
{ key: 'country', label: '国家' },
|
||||||
|
{ key: 'city', label: '城市' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function initInfoCard() {
|
||||||
|
// Close button removed - now uses external clear button
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setInfoCardNoBorder(noBorder = true) {
|
||||||
|
const card = document.getElementById('info-card');
|
||||||
|
if (card) {
|
||||||
|
card.classList.toggle('no-border', noBorder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showInfoCard(type, data) {
|
||||||
|
const config = CARD_CONFIG[type];
|
||||||
|
if (!config) {
|
||||||
|
console.warn('Unknown info card type:', type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentType = type;
|
||||||
|
const card = document.getElementById('info-card');
|
||||||
|
const icon = document.getElementById('info-card-icon');
|
||||||
|
const title = document.getElementById('info-card-title');
|
||||||
|
const content = document.getElementById('info-card-content');
|
||||||
|
|
||||||
|
card.className = 'info-card ' + config.className;
|
||||||
|
icon.textContent = config.icon;
|
||||||
|
title.textContent = config.title;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const field of config.fields) {
|
||||||
|
let value = data[field.key];
|
||||||
|
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
value = '-';
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
value = value.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.unit && value !== '-') {
|
||||||
|
value = value + ' ' + field.unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="info-card-property">
|
||||||
|
<span class="info-card-label">${field.label}</span>
|
||||||
|
<span class="info-card-value">${value}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content.innerHTML = html;
|
||||||
|
card.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideInfoCard() {
|
||||||
|
const card = document.getElementById('info-card');
|
||||||
|
if (card) {
|
||||||
|
card.style.display = 'none';
|
||||||
|
}
|
||||||
|
currentType = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentType() {
|
||||||
|
return currentType;
|
||||||
|
}
|
||||||
@@ -1,29 +1,122 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { createNoise3D } from 'simplex-noise';
|
import { createNoise3D } from 'simplex-noise';
|
||||||
|
|
||||||
import { CONFIG } from './constants.js';
|
import { CONFIG, CABLE_CONFIG, CABLE_STATE } from './constants.js';
|
||||||
import { latLonToVector3, vector3ToLatLon, screenToEarthCoords } from './utils.js';
|
import { latLonToVector3, vector3ToLatLon, screenToEarthCoords } from './utils.js';
|
||||||
import {
|
import {
|
||||||
showStatusMessage,
|
showStatusMessage,
|
||||||
updateCoordinatesDisplay,
|
updateCoordinatesDisplay,
|
||||||
updateZoomDisplay,
|
updateZoomDisplay,
|
||||||
updateEarthStats,
|
updateEarthStats,
|
||||||
updateCableDetails,
|
|
||||||
setLoading,
|
setLoading,
|
||||||
showTooltip,
|
showTooltip,
|
||||||
hideTooltip
|
hideTooltip
|
||||||
} from './ui.js';
|
} from './ui.js';
|
||||||
import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js';
|
import { createEarth, createClouds, createTerrain, createStars, createGridLines, toggleTerrain, getEarth } from './earth.js';
|
||||||
import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById } from './cables.js';
|
import { loadGeoJSONFromPath, loadLandingPoints, handleCableClick, clearCableSelection, getCableLines, getCablesById, lockedCable as cableLocked, getCableState, setCableState, clearAllCableStates, applyLandingPointVisualState, resetLandingPointVisualState, getAllLandingPoints } from './cables.js';
|
||||||
import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate } from './controls.js';
|
import { createSatellites, loadSatellites, updateSatellitePositions, toggleSatellites, toggleTrails, getShowSatellites, selectSatellite, getSatelliteData, getSatellitePoints, setSatelliteRingState, updateLockedRingPosition, updateHoverRingPosition, getSatellitePositions } from './satellites.js';
|
||||||
|
import { setupControls, getAutoRotate, getShowTerrain, zoomLevel, setAutoRotate, toggleAutoRotate, resetView } from './controls.js';
|
||||||
|
import { initInfoCard, showInfoCard, hideInfoCard, getCurrentType, setInfoCardNoBorder } from './info-card.js';
|
||||||
|
|
||||||
export let scene, camera, renderer;
|
export let scene, camera, renderer;
|
||||||
let simplex;
|
let simplex;
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let previousMousePosition = { x: 0, y: 0 };
|
let previousMousePosition = { x: 0, y: 0 };
|
||||||
let hoveredCable = null;
|
let hoveredCable = null;
|
||||||
let lockedCable = null;
|
let hoveredSatellite = null;
|
||||||
let lockedCableData = null;
|
let hoveredSatelliteIndex = null;
|
||||||
|
let cableLockedData = null;
|
||||||
|
let lockedSatellite = null;
|
||||||
|
let lockedSatelliteIndex = null;
|
||||||
|
let lockedObject = null;
|
||||||
|
let lockedObjectType = null;
|
||||||
|
let dragStartTime = 0;
|
||||||
|
let isLongDrag = false;
|
||||||
|
|
||||||
|
function clearLockedObject() {
|
||||||
|
hoveredCable = null;
|
||||||
|
hoveredSatellite = null;
|
||||||
|
hoveredSatelliteIndex = null;
|
||||||
|
clearAllCableStates();
|
||||||
|
setSatelliteRingState(null, 'none', null);
|
||||||
|
lockedObject = null;
|
||||||
|
lockedObjectType = null;
|
||||||
|
lockedSatellite = null;
|
||||||
|
lockedSatelliteIndex = null;
|
||||||
|
cableLockedData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameCable(cable1, cable2) {
|
||||||
|
if (!cable1 || !cable2) return false;
|
||||||
|
const id1 = cable1.userData?.cableId;
|
||||||
|
const id2 = cable2.userData?.cableId;
|
||||||
|
if (id1 === undefined || id2 === undefined) return false;
|
||||||
|
return id1 === id2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCableInfo(cable) {
|
||||||
|
showInfoCard('cable', {
|
||||||
|
name: cable.userData.name,
|
||||||
|
owner: cable.userData.owner,
|
||||||
|
status: cable.userData.status,
|
||||||
|
length: cable.userData.length,
|
||||||
|
coords: cable.userData.coords,
|
||||||
|
rfs: cable.userData.rfs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSatelliteInfo(props) {
|
||||||
|
const meanMotion = props?.mean_motion || 0;
|
||||||
|
const period = meanMotion > 0 ? (1440 / meanMotion).toFixed(1) : '-';
|
||||||
|
const ecc = props?.eccentricity || 0;
|
||||||
|
const perigee = (6371 * (1 - ecc)).toFixed(0);
|
||||||
|
const apogee = (6371 * (1 + ecc)).toFixed(0);
|
||||||
|
|
||||||
|
showInfoCard('satellite', {
|
||||||
|
name: props?.name || '-',
|
||||||
|
norad_id: props?.norad_cat_id,
|
||||||
|
inclination: props?.inclination ? props.inclination.toFixed(2) : '-',
|
||||||
|
period: period,
|
||||||
|
perigee: perigee,
|
||||||
|
apogee: apogee
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCableVisualState() {
|
||||||
|
const allCables = getCableLines();
|
||||||
|
const pulse = (Math.sin(Date.now() * CABLE_CONFIG.pulseSpeed) + 1) * 0.5;
|
||||||
|
|
||||||
|
allCables.forEach(c => {
|
||||||
|
const cableId = c.userData.cableId;
|
||||||
|
const state = getCableState(cableId);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case CABLE_STATE.LOCKED:
|
||||||
|
c.material.opacity = CABLE_CONFIG.lockedOpacityMin + pulse * (CABLE_CONFIG.lockedOpacityMax - CABLE_CONFIG.lockedOpacityMin);
|
||||||
|
c.material.color.setRGB(1, 1, 1);
|
||||||
|
break;
|
||||||
|
case CABLE_STATE.HOVERED:
|
||||||
|
c.material.opacity = 1;
|
||||||
|
c.material.color.setRGB(1, 1, 1);
|
||||||
|
break;
|
||||||
|
case CABLE_STATE.NORMAL:
|
||||||
|
default:
|
||||||
|
if ((lockedObjectType === 'cable' && lockedObject) || (lockedObjectType === 'satellite' && lockedSatellite)) {
|
||||||
|
c.material.opacity = CABLE_CONFIG.otherOpacity;
|
||||||
|
const origColor = c.userData.originalColor;
|
||||||
|
const brightness = CABLE_CONFIG.otherBrightness;
|
||||||
|
c.material.color.setRGB(
|
||||||
|
((origColor >> 16) & 255) / 255 * brightness,
|
||||||
|
((origColor >> 8) & 255) / 255 * brightness,
|
||||||
|
(origColor & 255) / 255 * brightness
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
c.material.opacity = 1;
|
||||||
|
c.material.color.setHex(c.userData.originalColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('error', (e) => {
|
window.addEventListener('error', (e) => {
|
||||||
console.error('全局错误:', e.error);
|
console.error('全局错误:', e.error);
|
||||||
@@ -49,13 +142,16 @@ export function init() {
|
|||||||
document.getElementById('container').appendChild(renderer.domElement);
|
document.getElementById('container').appendChild(renderer.domElement);
|
||||||
|
|
||||||
addLights();
|
addLights();
|
||||||
|
initInfoCard();
|
||||||
const earthObj = createEarth(scene);
|
const earthObj = createEarth(scene);
|
||||||
createClouds(scene, earthObj);
|
createClouds(scene, earthObj);
|
||||||
createTerrain(scene, earthObj, simplex);
|
createTerrain(scene, earthObj, simplex);
|
||||||
createStars(scene);
|
createStars(scene);
|
||||||
createGridLines(scene, earthObj);
|
createGridLines(scene, earthObj);
|
||||||
|
createSatellites(scene, earthObj);
|
||||||
|
|
||||||
setupControls(camera, renderer, scene, earthObj);
|
setupControls(camera, renderer, scene, earthObj);
|
||||||
|
resetView(camera);
|
||||||
setupEventListeners(camera, renderer);
|
setupEventListeners(camera, renderer);
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
@@ -80,7 +176,19 @@ function addLights() {
|
|||||||
scene.add(pointLight);
|
scene.add(pointLight);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData() {
|
let earthTexture = null;
|
||||||
|
|
||||||
|
async function loadData(showWhiteSphere = false) {
|
||||||
|
if (showWhiteSphere) {
|
||||||
|
const earth = getEarth();
|
||||||
|
if (earth && earth.material) {
|
||||||
|
earthTexture = earth.material.map;
|
||||||
|
earth.material.map = null;
|
||||||
|
earth.material.color.setHex(0xffffff);
|
||||||
|
earth.material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
console.log('开始加载电缆数据...');
|
console.log('开始加载电缆数据...');
|
||||||
@@ -88,11 +196,31 @@ async function loadData() {
|
|||||||
console.log('电缆数据加载完成');
|
console.log('电缆数据加载完成');
|
||||||
await loadLandingPoints(scene, getEarth());
|
await loadLandingPoints(scene, getEarth());
|
||||||
console.log('登陆点数据加载完成');
|
console.log('登陆点数据加载完成');
|
||||||
|
|
||||||
|
const satCount = await loadSatellites();
|
||||||
|
console.log(`卫星数据加载完成: ${satCount} 颗`);
|
||||||
|
updateSatellitePositions();
|
||||||
|
console.log('卫星位置已更新');
|
||||||
|
toggleSatellites(true);
|
||||||
|
console.log('卫星已显示');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载数据失败:', error);
|
console.error('加载数据失败:', error);
|
||||||
showStatusMessage('加载数据失败: ' + error.message, 'error');
|
showStatusMessage('加载数据失败: ' + error.message, 'error');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
if (showWhiteSphere) {
|
||||||
|
const earth = getEarth();
|
||||||
|
if (earth && earth.material) {
|
||||||
|
earth.material.map = earthTexture;
|
||||||
|
earth.material.color.setHex(0xffffff);
|
||||||
|
earth.material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reloadData() {
|
||||||
|
await loadData(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupEventListeners(camera, renderer) {
|
function setupEventListeners(camera, renderer) {
|
||||||
@@ -149,72 +277,92 @@ function onMouseMove(event, camera) {
|
|||||||
const frontCables = getFrontFacingCables(allCableLines, camera);
|
const frontCables = getFrontFacingCables(allCableLines, camera);
|
||||||
const intersects = raycaster.intersectObjects(frontCables);
|
const intersects = raycaster.intersectObjects(frontCables);
|
||||||
|
|
||||||
if (hoveredCable && hoveredCable !== lockedCable) {
|
const hasHoveredCable = intersects.length > 0;
|
||||||
const prevCableId = hoveredCable.userData.cableId;
|
let hoveredSat = null;
|
||||||
const prevSameCables = getCablesById(prevCableId);
|
let hoveredSatIndexFromIntersect = null;
|
||||||
prevSameCables.forEach(c => {
|
if (getShowSatellites()) {
|
||||||
if (c.userData.originalColor !== undefined) {
|
const satPoints = getSatellitePoints();
|
||||||
c.material.color.setHex(c.userData.originalColor);
|
if (satPoints) {
|
||||||
|
const satIntersects = raycaster.intersectObject(satPoints);
|
||||||
|
if (satIntersects.length > 0) {
|
||||||
|
hoveredSatIndexFromIntersect = satIntersects[0].index;
|
||||||
|
hoveredSat = selectSatellite(hoveredSatIndexFromIntersect);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
hoveredCable = null;
|
}
|
||||||
|
const hasHoveredSatellite = hoveredSat && hoveredSat.properties;
|
||||||
|
|
||||||
|
if (hoveredCable) {
|
||||||
|
if (!hasHoveredCable || !isSameCable(intersects[0]?.object, hoveredCable)) {
|
||||||
|
if (!isSameCable(hoveredCable, lockedObject)) {
|
||||||
|
setCableState(hoveredCable.userData.cableId, CABLE_STATE.NORMAL);
|
||||||
|
}
|
||||||
|
hoveredCable = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intersects.length > 0) {
|
if (hoveredSatelliteIndex !== null && hoveredSatelliteIndex !== hoveredSatIndexFromIntersect) {
|
||||||
|
if (hoveredSatelliteIndex !== lockedSatelliteIndex) {
|
||||||
|
setSatelliteRingState(hoveredSatelliteIndex, 'none', null);
|
||||||
|
}
|
||||||
|
hoveredSatelliteIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasHoveredCable) {
|
||||||
const cable = intersects[0].object;
|
const cable = intersects[0].object;
|
||||||
const cableId = cable.userData.cableId;
|
if (!isSameCable(cable, lockedObject)) {
|
||||||
const sameCables = getCablesById(cableId);
|
hoveredCable = cable;
|
||||||
|
setCableState(cable.userData.cableId, CABLE_STATE.HOVERED);
|
||||||
if (cable !== lockedCable) {
|
} else {
|
||||||
sameCables.forEach(c => {
|
|
||||||
c.material.color.setHex(0xffffff);
|
|
||||||
c.material.opacity = 1;
|
|
||||||
});
|
|
||||||
hoveredCable = cable;
|
hoveredCable = cable;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userData = cable.userData;
|
showCableInfo(cable);
|
||||||
document.getElementById('cable-name').textContent =
|
setInfoCardNoBorder(true);
|
||||||
userData.name || userData.shortname || '未命名电缆';
|
|
||||||
document.getElementById('cable-owner').textContent = userData.owner || '-';
|
|
||||||
document.getElementById('cable-status').textContent = userData.status || '-';
|
|
||||||
document.getElementById('cable-length').textContent = userData.length || '-';
|
|
||||||
document.getElementById('cable-coords').textContent = '-';
|
|
||||||
document.getElementById('cable-rfs').textContent = userData.rfs || '-';
|
|
||||||
|
|
||||||
hideTooltip();
|
hideTooltip();
|
||||||
} else {
|
} else if (hasHoveredSatellite) {
|
||||||
if (lockedCable && lockedCableData) {
|
hoveredSatellite = hoveredSat;
|
||||||
document.getElementById('cable-name').textContent =
|
hoveredSatelliteIndex = hoveredSatIndexFromIntersect;
|
||||||
lockedCableData.name || lockedCableData.shortname || '未命名电缆';
|
if (hoveredSatelliteIndex !== lockedSatelliteIndex) {
|
||||||
document.getElementById('cable-owner').textContent = lockedCableData.owner || '-';
|
const satPositions = getSatellitePositions();
|
||||||
document.getElementById('cable-status').textContent = lockedCableData.status || '-';
|
if (satPositions && satPositions[hoveredSatelliteIndex]) {
|
||||||
document.getElementById('cable-length').textContent = lockedCableData.length || '-';
|
setSatelliteRingState(hoveredSatelliteIndex, 'hover', satPositions[hoveredSatelliteIndex].current);
|
||||||
document.getElementById('cable-coords').textContent = '-';
|
|
||||||
document.getElementById('cable-rfs').textContent = lockedCableData.rfs || '-';
|
|
||||||
} else {
|
|
||||||
document.getElementById('cable-name').textContent = '点击电缆查看详情';
|
|
||||||
document.getElementById('cable-owner').textContent = '-';
|
|
||||||
document.getElementById('cable-status').textContent = '-';
|
|
||||||
document.getElementById('cable-length').textContent = '-';
|
|
||||||
document.getElementById('cable-coords').textContent = '-';
|
|
||||||
document.getElementById('cable-rfs').textContent = '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
const earthPoint = screenToEarthCoords(event.clientX, event.clientY, camera, earth);
|
|
||||||
|
|
||||||
if (earthPoint) {
|
|
||||||
const coords = vector3ToLatLon(earthPoint);
|
|
||||||
updateCoordinatesDisplay(coords.lat, coords.lon, coords.alt);
|
|
||||||
|
|
||||||
if (!isDragging) {
|
|
||||||
showTooltip(event.clientX + 10, event.clientY + 10,
|
|
||||||
`纬度: ${coords.lat}°<br>经度: ${coords.lon}°<br>海拔: ${coords.alt.toFixed(1)} km`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
showSatelliteInfo(hoveredSat.properties);
|
||||||
|
setInfoCardNoBorder(true);
|
||||||
|
} else if (lockedObjectType === 'cable' && lockedObject) {
|
||||||
|
showCableInfo(lockedObject);
|
||||||
|
} else if (lockedObjectType === 'satellite' && lockedSatellite) {
|
||||||
|
if (lockedSatelliteIndex !== null && lockedSatelliteIndex !== undefined) {
|
||||||
|
const satPositions = getSatellitePositions();
|
||||||
|
if (satPositions && satPositions[lockedSatelliteIndex]) {
|
||||||
|
setSatelliteRingState(lockedSatelliteIndex, 'locked', satPositions[lockedSatelliteIndex].current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showSatelliteInfo(lockedSatellite.properties);
|
||||||
|
} else {
|
||||||
|
hideInfoCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
const earthPoint = screenToEarthCoords(event.clientX, event.clientY, camera, earth);
|
||||||
|
if (earthPoint) {
|
||||||
|
const coords = vector3ToLatLon(earthPoint);
|
||||||
|
updateCoordinatesDisplay(coords.lat, coords.lon, coords.alt);
|
||||||
|
|
||||||
|
if (!isDragging) {
|
||||||
|
showTooltip(event.clientX + 10, event.clientY + 10,
|
||||||
|
`纬度: ${coords.lat}°<br>经度: ${coords.lon}°<br>海拔: ${coords.alt.toFixed(1)} km`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hideTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
|
if (Date.now() - dragStartTime > 500) {
|
||||||
|
isLongDrag = true;
|
||||||
|
}
|
||||||
|
|
||||||
const deltaX = event.clientX - previousMousePosition.x;
|
const deltaX = event.clientX - previousMousePosition.x;
|
||||||
const deltaY = event.clientY - previousMousePosition.y;
|
const deltaY = event.clientY - previousMousePosition.y;
|
||||||
|
|
||||||
@@ -227,6 +375,8 @@ function onMouseMove(event, camera) {
|
|||||||
|
|
||||||
function onMouseDown(event) {
|
function onMouseDown(event) {
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
|
dragStartTime = Date.now();
|
||||||
|
isLongDrag = false;
|
||||||
previousMousePosition = { x: event.clientX, y: event.clientY };
|
previousMousePosition = { x: event.clientX, y: event.clientY };
|
||||||
document.getElementById('container').classList.add('dragging');
|
document.getElementById('container').classList.add('dragging');
|
||||||
hideTooltip();
|
hideTooltip();
|
||||||
@@ -252,46 +402,67 @@ function onClick(event, camera, renderer) {
|
|||||||
const allCableLines = getCableLines();
|
const allCableLines = getCableLines();
|
||||||
const frontCables = getFrontFacingCables(allCableLines, camera);
|
const frontCables = getFrontFacingCables(allCableLines, camera);
|
||||||
const intersects = raycaster.intersectObjects(frontCables);
|
const intersects = raycaster.intersectObjects(frontCables);
|
||||||
|
const satIntersects = getShowSatellites() ? raycaster.intersectObject(getSatellitePoints()) : [];
|
||||||
|
|
||||||
if (intersects.length > 0) {
|
if (intersects.length > 0) {
|
||||||
if (lockedCable) {
|
clearLockedObject();
|
||||||
const prevCableId = lockedCable.userData.cableId;
|
|
||||||
const prevSameCables = getCablesById(prevCableId);
|
|
||||||
prevSameCables.forEach(c => {
|
|
||||||
if (c.userData.originalColor !== undefined) {
|
|
||||||
c.material.color.setHex(c.userData.originalColor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickedCable = intersects[0].object;
|
const clickedCable = intersects[0].object;
|
||||||
const cableId = clickedCable.userData.cableId;
|
const cableId = clickedCable.userData.cableId;
|
||||||
const sameCables = getCablesById(cableId);
|
|
||||||
|
|
||||||
sameCables.forEach(c => {
|
setCableState(cableId, CABLE_STATE.LOCKED);
|
||||||
c.material.color.setHex(0xffffff);
|
|
||||||
c.material.opacity = 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
lockedCable = clickedCable;
|
lockedObject = clickedCable;
|
||||||
lockedCableData = { ...clickedCable.userData };
|
lockedObjectType = 'cable';
|
||||||
|
cableLockedData = { ...clickedCable.userData };
|
||||||
|
|
||||||
setAutoRotate(false);
|
setAutoRotate(false);
|
||||||
handleCableClick(clickedCable);
|
handleCableClick(clickedCable);
|
||||||
} else {
|
} else if (satIntersects.length > 0) {
|
||||||
if (lockedCable) {
|
const index = satIntersects[0].index;
|
||||||
const prevCableId = lockedCable.userData.cableId;
|
const sat = selectSatellite(index);
|
||||||
const prevSameCables = getCablesById(prevCableId);
|
|
||||||
prevSameCables.forEach(c => {
|
if (sat && sat.properties) {
|
||||||
if (c.userData.originalColor !== undefined) {
|
clearLockedObject();
|
||||||
c.material.color.setHex(c.userData.originalColor);
|
|
||||||
}
|
lockedObject = sat;
|
||||||
|
lockedObjectType = 'satellite';
|
||||||
|
lockedSatellite = sat;
|
||||||
|
lockedSatelliteIndex = index;
|
||||||
|
setAutoRotate(false);
|
||||||
|
|
||||||
|
const satPositions = getSatellitePositions();
|
||||||
|
if (satPositions && satPositions[index]) {
|
||||||
|
setSatelliteRingState(index, 'locked', satPositions[index].current);
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = sat.properties;
|
||||||
|
|
||||||
|
const meanMotion = props.mean_motion || 0;
|
||||||
|
const period = meanMotion > 0 ? (1440 / meanMotion).toFixed(1) : '-';
|
||||||
|
|
||||||
|
const ecc = props.eccentricity || 0;
|
||||||
|
const earthRadius = 6371;
|
||||||
|
const perigee = (earthRadius * (1 - ecc)).toFixed(0);
|
||||||
|
const apogee = (earthRadius * (1 + ecc)).toFixed(0);
|
||||||
|
|
||||||
|
showInfoCard('satellite', {
|
||||||
|
name: props.name,
|
||||||
|
norad_id: props.norad_cat_id,
|
||||||
|
inclination: props.inclination ? props.inclination.toFixed(2) : '-',
|
||||||
|
period: period,
|
||||||
|
perigee: perigee,
|
||||||
|
apogee: apogee
|
||||||
});
|
});
|
||||||
lockedCable = null;
|
|
||||||
lockedCableData = null;
|
showStatusMessage('已选择: ' + props.name, 'info');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!isLongDrag) {
|
||||||
|
clearLockedObject();
|
||||||
|
setAutoRotate(true);
|
||||||
|
clearCableSelection();
|
||||||
}
|
}
|
||||||
setAutoRotate(true);
|
|
||||||
clearCableSelection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,34 +475,38 @@ function animate() {
|
|||||||
earth.rotation.y += CONFIG.rotationSpeed;
|
earth.rotation.y += CONFIG.rotationSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lockedCable) {
|
applyCableVisualState();
|
||||||
const pulse = (Math.sin(Date.now() * 0.003) + 1) * 0.5;
|
|
||||||
const glowIntensity = 0.7 + pulse * 0.3;
|
if (lockedObjectType === 'cable' && lockedObject) {
|
||||||
const cableId = lockedCable.userData.cableId;
|
applyLandingPointVisualState(lockedObject.userData.name, false);
|
||||||
const sameCables = getCablesById(cableId);
|
} else if (lockedObjectType === 'satellite' && lockedSatellite) {
|
||||||
sameCables.forEach(c => {
|
applyLandingPointVisualState(null, true);
|
||||||
c.material.opacity = 0.6 + pulse * 0.4;
|
} else {
|
||||||
c.material.color.setRGB(glowIntensity, glowIntensity, glowIntensity);
|
resetLandingPointVisualState();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
updateSatellitePositions(16);
|
||||||
|
|
||||||
|
const satPositions = getSatellitePositions();
|
||||||
|
|
||||||
|
if (lockedObjectType === 'satellite' && lockedSatelliteIndex !== null) {
|
||||||
|
if (satPositions && satPositions[lockedSatelliteIndex]) {
|
||||||
|
updateLockedRingPosition(satPositions[lockedSatelliteIndex].current);
|
||||||
|
}
|
||||||
|
} else if (hoveredSatelliteIndex !== null && satPositions && satPositions[hoveredSatelliteIndex]) {
|
||||||
|
updateHoverRingPosition(satPositions[hoveredSatelliteIndex].current);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.clearLockedCable = function() {
|
window.clearLockedCable = function() {
|
||||||
if (lockedCable) {
|
clearLockedObject();
|
||||||
const cableId = lockedCable.userData.cableId;
|
};
|
||||||
const sameCables = getCablesById(cableId);
|
|
||||||
sameCables.forEach(c => {
|
window.clearSelection = function() {
|
||||||
if (c.userData.originalColor !== undefined) {
|
hideInfoCard();
|
||||||
c.material.color.setHex(c.userData.originalColor);
|
window.clearLockedCable();
|
||||||
c.material.opacity = 1.0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
lockedCable = null;
|
|
||||||
lockedCableData = null;
|
|
||||||
}
|
|
||||||
clearCableSelection();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|||||||
461
frontend/public/earth/js/satellites.js
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
// satellites.js - Satellite visualization module with real SGP4 positions and animations
|
||||||
|
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { twoline2satrec, sgp4, propagate, degreesToRadians, radiansToDegrees, eciToGeodetic } from 'satellite.js';
|
||||||
|
import { CONFIG, SATELLITE_CONFIG } from './constants.js';
|
||||||
|
|
||||||
|
let satellitePoints = null;
|
||||||
|
let satelliteTrails = null;
|
||||||
|
let satelliteData = [];
|
||||||
|
let showSatellites = true;
|
||||||
|
let showTrails = true;
|
||||||
|
let trailsReady = false;
|
||||||
|
let animationTime = 0;
|
||||||
|
let selectedSatellite = null;
|
||||||
|
let satellitePositions = [];
|
||||||
|
let hoverRingSprite = null;
|
||||||
|
let lockedRingSprite = null;
|
||||||
|
|
||||||
|
const SATELLITE_API = SATELLITE_CONFIG.apiPath + '?limit=' + SATELLITE_CONFIG.maxCount;
|
||||||
|
const MAX_SATELLITES = SATELLITE_CONFIG.maxCount;
|
||||||
|
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
|
||||||
|
const DOT_TEXTURE_SIZE = 32;
|
||||||
|
|
||||||
|
function createCircularDotTexture() {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = DOT_TEXTURE_SIZE;
|
||||||
|
canvas.height = DOT_TEXTURE_SIZE;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const center = DOT_TEXTURE_SIZE / 2;
|
||||||
|
const radius = center - 2;
|
||||||
|
|
||||||
|
const gradient = ctx.createRadialGradient(center, center, 0, center, center, radius);
|
||||||
|
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
|
||||||
|
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.8)');
|
||||||
|
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
||||||
|
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(center, center, radius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRingTexture(innerRadius, outerRadius, color = '#ffffff') {
|
||||||
|
const size = DOT_TEXTURE_SIZE * 2;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const center = size / 2;
|
||||||
|
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(center, center, (innerRadius + outerRadius) / 2, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSatellites(scene, earthObj) {
|
||||||
|
initSatelliteScene(scene, earthObj);
|
||||||
|
|
||||||
|
const positions = new Float32Array(MAX_SATELLITES * 3);
|
||||||
|
const colors = new Float32Array(MAX_SATELLITES * 3);
|
||||||
|
|
||||||
|
const dotTexture = createCircularDotTexture();
|
||||||
|
|
||||||
|
const pointsGeometry = new THREE.BufferGeometry();
|
||||||
|
pointsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||||
|
pointsGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||||
|
|
||||||
|
const pointsMaterial = new THREE.PointsMaterial({
|
||||||
|
size: SATELLITE_CONFIG.dotSize,
|
||||||
|
map: dotTexture,
|
||||||
|
vertexColors: true,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.9,
|
||||||
|
sizeAttenuation: true,
|
||||||
|
alphaTest: 0.1
|
||||||
|
});
|
||||||
|
|
||||||
|
satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial);
|
||||||
|
satellitePoints.visible = false;
|
||||||
|
satellitePoints.userData = { type: 'satellitePoints' };
|
||||||
|
earthObj.add(satellitePoints);
|
||||||
|
|
||||||
|
const trailPositions = new Float32Array(MAX_SATELLITES * TRAIL_LENGTH * 3);
|
||||||
|
const trailColors = new Float32Array(MAX_SATELLITES * TRAIL_LENGTH * 3);
|
||||||
|
|
||||||
|
const trailGeometry = new THREE.BufferGeometry();
|
||||||
|
trailGeometry.setAttribute('position', new THREE.BufferAttribute(trailPositions, 3));
|
||||||
|
trailGeometry.setAttribute('color', new THREE.BufferAttribute(trailColors, 3));
|
||||||
|
|
||||||
|
const trailMaterial = new THREE.LineBasicMaterial({
|
||||||
|
vertexColors: true,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.3,
|
||||||
|
blending: THREE.AdditiveBlending
|
||||||
|
});
|
||||||
|
|
||||||
|
satelliteTrails = new THREE.LineSegments(trailGeometry, trailMaterial);
|
||||||
|
satelliteTrails.visible = false;
|
||||||
|
satelliteTrails.userData = { type: 'satelliteTrails' };
|
||||||
|
earthObj.add(satelliteTrails);
|
||||||
|
|
||||||
|
satellitePositions = [];
|
||||||
|
for (let i = 0; i < MAX_SATELLITES; i++) {
|
||||||
|
satellitePositions.push({
|
||||||
|
current: new THREE.Vector3(),
|
||||||
|
trail: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return satellitePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSatellitePosition(satellite, time) {
|
||||||
|
try {
|
||||||
|
const props = satellite.properties;
|
||||||
|
if (!props || !props.norad_cat_id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noradId = props.norad_cat_id;
|
||||||
|
const inclination = props.inclination || 53;
|
||||||
|
const raan = props.raan || 0;
|
||||||
|
const eccentricity = props.eccentricity || 0.0001;
|
||||||
|
const argOfPerigee = props.arg_of_perigee || 0;
|
||||||
|
const meanAnomaly = props.mean_anomaly || 0;
|
||||||
|
const meanMotion = props.mean_motion || 15;
|
||||||
|
const epoch = props.epoch || '';
|
||||||
|
|
||||||
|
const year = epoch && epoch.length >= 4 ? parseInt(epoch.substring(0, 4)) : time.getUTCFullYear();
|
||||||
|
const month = epoch && epoch.length >= 7 ? parseInt(epoch.substring(5, 7)) : time.getUTCMonth() + 1;
|
||||||
|
const day = epoch && epoch.length >= 10 ? parseInt(epoch.substring(8, 10)) : time.getUTCDate();
|
||||||
|
|
||||||
|
const tleLine1 = `1 ${String(noradId).padStart(5, '0')}U 00001A ${year}${String(month).padStart(2, '0')}${String(day).padStart(2, '0')}.00000000 .00000000 00000-0 00000-0 0 9999`;
|
||||||
|
const tleLine2 = `2 ${String(noradId).padStart(5, '0')} ${String(raan.toFixed(4)).padStart(8, ' ')} ${String(inclination.toFixed(4)).padStart(8, ' ')} ${String(eccentricity.toFixed(7)).replace('0.', '.')} ${String(argOfPerigee.toFixed(4)).padStart(8, ' ')} ${String(meanAnomaly.toFixed(4)).padStart(8, ' ')} ${String(meanMotion.toFixed(8)).padStart(11, ' ')} 0 9999`;
|
||||||
|
|
||||||
|
const satrec = twoline2satrec(tleLine1, tleLine2);
|
||||||
|
if (!satrec || satrec.error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionAndVelocity = propagate(satrec, time);
|
||||||
|
if (!positionAndVelocity || !positionAndVelocity.position) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = positionAndVelocity.position.x;
|
||||||
|
const y = positionAndVelocity.position.y;
|
||||||
|
const z = positionAndVelocity.position.z;
|
||||||
|
|
||||||
|
if (!x || !y || !z) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = Math.sqrt(x * x + y * y + z * z);
|
||||||
|
const earthRadius = 6371;
|
||||||
|
const displayRadius = CONFIG.earthRadius * (earthRadius / 6371) * 1.05;
|
||||||
|
|
||||||
|
const scale = displayRadius / r;
|
||||||
|
|
||||||
|
return new THREE.Vector3(x * scale, y * scale, z * scale);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateFallbackPosition(satellite, index, total) {
|
||||||
|
const radius = CONFIG.earthRadius + 5;
|
||||||
|
|
||||||
|
const noradId = satellite.properties?.norad_cat_id || index;
|
||||||
|
const inclination = satellite.properties?.inclination || 53;
|
||||||
|
const raan = satellite.properties?.raan || 0;
|
||||||
|
const meanAnomaly = satellite.properties?.mean_anomaly || 0;
|
||||||
|
|
||||||
|
const hash = String(noradId).split('').reduce((a, b) => a + b.charCodeAt(0), 0);
|
||||||
|
const randomOffset = (hash % 1000) / 1000;
|
||||||
|
|
||||||
|
const normalizedIndex = index / total;
|
||||||
|
const theta = normalizedIndex * Math.PI * 2 * 10 + (raan * Math.PI / 180);
|
||||||
|
const phi = (inclination * Math.PI / 180) + (meanAnomaly * Math.PI / 180 * 0.1);
|
||||||
|
|
||||||
|
const adjustedPhi = Math.abs(phi % Math.PI);
|
||||||
|
const adjustedTheta = theta + randomOffset * Math.PI * 2;
|
||||||
|
|
||||||
|
const x = radius * Math.sin(adjustedPhi) * Math.cos(adjustedTheta);
|
||||||
|
const y = radius * Math.cos(adjustedPhi);
|
||||||
|
const z = radius * Math.sin(adjustedPhi) * Math.sin(adjustedTheta);
|
||||||
|
|
||||||
|
return new THREE.Vector3(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSatellites() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(SATELLITE_API);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
satelliteData = data.features || [];
|
||||||
|
|
||||||
|
console.log(`Loaded ${satelliteData.length} satellites`);
|
||||||
|
return satelliteData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load satellites:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSatellitePositions(deltaTime = 0) {
|
||||||
|
if (!satellitePoints || satelliteData.length === 0) return;
|
||||||
|
|
||||||
|
animationTime += deltaTime * 0.001;
|
||||||
|
|
||||||
|
const positions = satellitePoints.geometry.attributes.position.array;
|
||||||
|
const colors = satellitePoints.geometry.attributes.color.array;
|
||||||
|
|
||||||
|
const trailPositions = satelliteTrails.geometry.attributes.position.array;
|
||||||
|
const trailColors = satelliteTrails.geometry.attributes.color.array;
|
||||||
|
|
||||||
|
const baseTime = new Date();
|
||||||
|
const count = Math.min(satelliteData.length, MAX_SATELLITES);
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const satellite = satelliteData[i];
|
||||||
|
const props = satellite.properties;
|
||||||
|
|
||||||
|
const timeOffset = (i / count) * 2 * Math.PI * 0.1;
|
||||||
|
const adjustedTime = new Date(baseTime.getTime() + timeOffset * 1000 * 60 * 10);
|
||||||
|
|
||||||
|
let pos = computeSatellitePosition(satellite, adjustedTime);
|
||||||
|
|
||||||
|
if (!pos) {
|
||||||
|
pos = generateFallbackPosition(satellite, i, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
satellitePositions[i].current.copy(pos);
|
||||||
|
|
||||||
|
satellitePositions[i].trail.push(pos.clone());
|
||||||
|
if (satellitePositions[i].trail.length > TRAIL_LENGTH) {
|
||||||
|
satellitePositions[i].trail.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
positions[i * 3] = pos.x;
|
||||||
|
positions[i * 3 + 1] = pos.y;
|
||||||
|
positions[i * 3 + 2] = pos.z;
|
||||||
|
|
||||||
|
const inclination = props?.inclination || 53;
|
||||||
|
const name = props?.name || '';
|
||||||
|
const isStarlink = name.includes('STARLINK');
|
||||||
|
const isGeo = inclination > 20 && inclination < 30;
|
||||||
|
const isIridium = name.includes('IRIDIUM');
|
||||||
|
|
||||||
|
let r, g, b;
|
||||||
|
if (isStarlink) {
|
||||||
|
r = 0.0; g = 0.9; b = 1.0;
|
||||||
|
} else if (isGeo) {
|
||||||
|
r = 1.0; g = 0.8; b = 0.0;
|
||||||
|
} else if (isIridium) {
|
||||||
|
r = 1.0; g = 0.5; b = 0.0;
|
||||||
|
} else if (inclination > 50 && inclination < 70) {
|
||||||
|
r = 0.0; g = 1.0; b = 0.3;
|
||||||
|
} else {
|
||||||
|
r = 1.0; g = 1.0; b = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
colors[i * 3] = r;
|
||||||
|
colors[i * 3 + 1] = g;
|
||||||
|
colors[i * 3 + 2] = b;
|
||||||
|
|
||||||
|
const trail = satellitePositions[i].trail;
|
||||||
|
for (let j = 0; j < TRAIL_LENGTH; j++) {
|
||||||
|
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
|
||||||
|
|
||||||
|
if (j < trail.length) {
|
||||||
|
const t = trail[j];
|
||||||
|
trailPositions[trailIdx] = t.x;
|
||||||
|
trailPositions[trailIdx + 1] = t.y;
|
||||||
|
trailPositions[trailIdx + 2] = t.z;
|
||||||
|
|
||||||
|
const alpha = j / trail.length;
|
||||||
|
trailColors[trailIdx] = r * alpha;
|
||||||
|
trailColors[trailIdx + 1] = g * alpha;
|
||||||
|
trailColors[trailIdx + 2] = b * alpha;
|
||||||
|
} else {
|
||||||
|
trailPositions[trailIdx] = 0;
|
||||||
|
trailPositions[trailIdx + 1] = 0;
|
||||||
|
trailPositions[trailIdx + 2] = 0;
|
||||||
|
trailColors[trailIdx] = 0;
|
||||||
|
trailColors[trailIdx + 1] = 0;
|
||||||
|
trailColors[trailIdx + 2] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = count; i < 2000; i++) {
|
||||||
|
positions[i * 3] = 0;
|
||||||
|
positions[i * 3 + 1] = 0;
|
||||||
|
positions[i * 3 + 2] = 0;
|
||||||
|
|
||||||
|
for (let j = 0; j < TRAIL_LENGTH; j++) {
|
||||||
|
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
|
||||||
|
trailPositions[trailIdx] = 0;
|
||||||
|
trailPositions[trailIdx + 1] = 0;
|
||||||
|
trailPositions[trailIdx + 2] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
satellitePoints.geometry.attributes.position.needsUpdate = true;
|
||||||
|
satellitePoints.geometry.attributes.color.needsUpdate = true;
|
||||||
|
satellitePoints.geometry.setDrawRange(0, count);
|
||||||
|
|
||||||
|
satelliteTrails.geometry.attributes.position.needsUpdate = true;
|
||||||
|
satelliteTrails.geometry.attributes.color.needsUpdate = true;
|
||||||
|
|
||||||
|
if (!trailsReady && count > 0 && satellitePositions[0]?.trail.length >= TRAIL_LENGTH) {
|
||||||
|
trailsReady = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleSatellites(visible) {
|
||||||
|
showSatellites = visible;
|
||||||
|
if (satellitePoints) {
|
||||||
|
satellitePoints.visible = visible;
|
||||||
|
}
|
||||||
|
if (satelliteTrails) {
|
||||||
|
satelliteTrails.visible = visible && showTrails && trailsReady;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleTrails(visible) {
|
||||||
|
showTrails = visible;
|
||||||
|
if (satelliteTrails) {
|
||||||
|
satelliteTrails.visible = visible && showSatellites && trailsReady;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getShowSatellites() {
|
||||||
|
return showSatellites;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSatelliteCount() {
|
||||||
|
return satelliteData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSatelliteAt(index) {
|
||||||
|
if (index >= 0 && index < satelliteData.length) {
|
||||||
|
return satelliteData[index];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSatelliteData() {
|
||||||
|
return satelliteData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectSatellite(index) {
|
||||||
|
selectedSatellite = index;
|
||||||
|
return getSatelliteAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSatellitePoints() {
|
||||||
|
return satellitePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSatellitePositions() {
|
||||||
|
return satellitePositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
let earthObjRef = null;
|
||||||
|
let sceneRef = null;
|
||||||
|
|
||||||
|
export function showHoverRing(position, isLocked = false) {
|
||||||
|
if (!sceneRef || !earthObjRef) return;
|
||||||
|
|
||||||
|
const ringTexture = createRingTexture(8, 12, isLocked ? '#ffcc00' : '#ffffff');
|
||||||
|
const spriteMaterial = new THREE.SpriteMaterial({
|
||||||
|
map: ringTexture,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.8,
|
||||||
|
depthTest: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const sprite = new THREE.Sprite(spriteMaterial);
|
||||||
|
sprite.position.copy(position);
|
||||||
|
sprite.scale.set(3, 3, 1);
|
||||||
|
|
||||||
|
earthObjRef.add(sprite);
|
||||||
|
|
||||||
|
if (isLocked) {
|
||||||
|
if (lockedRingSprite) {
|
||||||
|
earthObjRef.remove(lockedRingSprite);
|
||||||
|
}
|
||||||
|
lockedRingSprite = sprite;
|
||||||
|
} else {
|
||||||
|
if (hoverRingSprite) {
|
||||||
|
earthObjRef.remove(hoverRingSprite);
|
||||||
|
}
|
||||||
|
hoverRingSprite = sprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideHoverRings() {
|
||||||
|
if (!earthObjRef) return;
|
||||||
|
|
||||||
|
if (hoverRingSprite) {
|
||||||
|
earthObjRef.remove(hoverRingSprite);
|
||||||
|
hoverRingSprite = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideLockedRing() {
|
||||||
|
if (!earthObjRef || !lockedRingSprite) return;
|
||||||
|
earthObjRef.remove(lockedRingSprite);
|
||||||
|
lockedRingSprite = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateLockedRingPosition(position) {
|
||||||
|
if (lockedRingSprite && position) {
|
||||||
|
lockedRingSprite.position.copy(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateHoverRingPosition(position) {
|
||||||
|
if (hoverRingSprite && position) {
|
||||||
|
hoverRingSprite.position.copy(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSatelliteRingState(index, state, position) {
|
||||||
|
switch (state) {
|
||||||
|
case 'hover':
|
||||||
|
hideHoverRings();
|
||||||
|
showHoverRing(position, false);
|
||||||
|
break;
|
||||||
|
case 'locked':
|
||||||
|
hideHoverRings();
|
||||||
|
showHoverRing(position, true);
|
||||||
|
break;
|
||||||
|
case 'none':
|
||||||
|
hideHoverRings();
|
||||||
|
hideLockedRing();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initSatelliteScene(scene, earth) {
|
||||||
|
sceneRef = scene;
|
||||||
|
earthObjRef = earth;
|
||||||
|
}
|
||||||
@@ -22,22 +22,14 @@ export function updateCoordinatesDisplay(lat, lon, alt = 0) {
|
|||||||
|
|
||||||
// Update zoom display
|
// Update zoom display
|
||||||
export function updateZoomDisplay(zoomLevel, distance) {
|
export function updateZoomDisplay(zoomLevel, distance) {
|
||||||
document.getElementById('zoom-value').textContent = zoomLevel.toFixed(1) + 'x';
|
const percent = Math.round(zoomLevel * 100);
|
||||||
document.getElementById('zoom-level').textContent = '缩放: ' + zoomLevel.toFixed(1) + 'x';
|
document.getElementById('zoom-value').textContent = percent + '%';
|
||||||
document.getElementById('zoom-slider').value = zoomLevel;
|
document.getElementById('zoom-level').textContent = '缩放: ' + percent + '%';
|
||||||
|
const slider = document.getElementById('zoom-slider');
|
||||||
|
if (slider) slider.value = zoomLevel;
|
||||||
document.getElementById('camera-distance').textContent = distance + ' km';
|
document.getElementById('camera-distance').textContent = distance + ' km';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cable details
|
|
||||||
export function updateCableDetails(cable) {
|
|
||||||
document.getElementById('cable-name').textContent = cable.name || 'Unknown';
|
|
||||||
document.getElementById('cable-owner').textContent = cable.owner || '-';
|
|
||||||
document.getElementById('cable-status').textContent = cable.status || '-';
|
|
||||||
document.getElementById('cable-length').textContent = cable.length || '-';
|
|
||||||
document.getElementById('cable-coords').textContent = cable.coords || '-';
|
|
||||||
document.getElementById('cable-rfs').textContent = cable.rfs || '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update earth stats
|
// Update earth stats
|
||||||
export function updateEarthStats(stats) {
|
export function updateEarthStats(stats) {
|
||||||
document.getElementById('cable-count').textContent = stats.cableCount || 0;
|
document.getElementById('cable-count').textContent = stats.cableCount || 0;
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ export function latLonToVector3(lat, lon, radius = CONFIG.earthRadius) {
|
|||||||
export function vector3ToLatLon(vector) {
|
export function vector3ToLatLon(vector) {
|
||||||
const radius = Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z);
|
const radius = Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z);
|
||||||
const lat = 90 - (Math.acos(vector.y / radius) * 180 / Math.PI);
|
const lat = 90 - (Math.acos(vector.y / radius) * 180 / Math.PI);
|
||||||
const lon = (Math.atan2(vector.z, -vector.x) * 180 / Math.PI) - 180;
|
|
||||||
|
let lon = (Math.atan2(vector.z, -vector.x) * 180 / Math.PI) - 180;
|
||||||
|
|
||||||
|
while (lon <= -180) lon += 360;
|
||||||
|
while (lon > 180) lon -= 360;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lat: parseFloat(lat.toFixed(4)),
|
lat: parseFloat(lat.toFixed(4)),
|
||||||
@@ -30,26 +34,43 @@ export function vector3ToLatLon(vector) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert screen coordinates to Earth surface 3D coordinates
|
// Convert screen coordinates to Earth surface 3D coordinates
|
||||||
export function screenToEarthCoords(x, y, camera, earth) {
|
export function screenToEarthCoords(clientX, clientY, camera, earth, domElement = document.body) {
|
||||||
const raycaster = new THREE.Raycaster();
|
const raycaster = new THREE.Raycaster();
|
||||||
const mouse = new THREE.Vector2(
|
const mouse = new THREE.Vector2();
|
||||||
(x / window.innerWidth) * 2 - 1,
|
|
||||||
-(y / window.innerHeight) * 2 + 1
|
if (domElement === document.body) {
|
||||||
);
|
mouse.x = (clientX / window.innerWidth) * 2 - 1;
|
||||||
|
mouse.y = -(clientY / window.innerHeight) * 2 + 1;
|
||||||
|
} else {
|
||||||
|
const rect = domElement.getBoundingClientRect();
|
||||||
|
mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
}
|
||||||
|
|
||||||
raycaster.setFromCamera(mouse, camera);
|
raycaster.setFromCamera(mouse, camera);
|
||||||
const intersects = raycaster.intersectObject(earth);
|
const intersects = raycaster.intersectObject(earth);
|
||||||
|
|
||||||
if (intersects.length > 0) {
|
if (intersects.length > 0) {
|
||||||
return intersects[0].point;
|
const localPoint = intersects[0].point.clone();
|
||||||
|
earth.worldToLocal(localPoint);
|
||||||
|
return localPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate simplified distance between two points
|
// Calculate accurate spherical distance between two points (Haversine formula)
|
||||||
export function calculateDistance(lat1, lon1, lat2, lon2) {
|
export function calculateDistance(lat1, lon1, lat2, lon2, radius = CONFIG.earthRadius) {
|
||||||
const dx = lon2 - lon1;
|
const toRad = (angle) => (angle * Math.PI) / 180;
|
||||||
const dy = lat2 - lat1;
|
|
||||||
return Math.sqrt(dx * dx + dy * dy);
|
const dLat = toRad(lat2 - lat1);
|
||||||
|
const dLon = toRad(lon2 - lon1);
|
||||||
|
|
||||||
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||||||
|
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
|
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
|
||||||
|
return radius * c;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,43 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
{
|
||||||
|
name: 'legacy-earth',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use('/legacy/earth', (req, res, next) => {
|
||||||
|
let url = req.url || '/'
|
||||||
|
|
||||||
|
if (url === '' || url === '/') {
|
||||||
|
url = '/3dearthmult.html'
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.resolve(__dirname, 'legacy/3dearthmult' + url)
|
||||||
|
|
||||||
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||||
|
const ext = path.extname(filePath).toLowerCase()
|
||||||
|
const contentTypes = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.css': 'text/css'
|
||||||
|
}
|
||||||
|
res.setHeader('Content-Type', contentTypes[ext] || 'text/plain')
|
||||||
|
fs.createReadStream(filePath).pipe(res)
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
@@ -29,4 +63,7 @@ export default defineConfig({
|
|||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['satellite.js'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
132
planet.sh
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
start() {
|
||||||
|
echo -e "${BLUE}🚀 启动智能星球计划...${NC}"
|
||||||
|
|
||||||
|
echo -e "${BLUE}🗄️ 启动数据库...${NC}"
|
||||||
|
docker start planet_postgres planet_redis 2>/dev/null || docker-compose up -d postgres redis
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo -e "${BLUE}🔧 启动后端...${NC}"
|
||||||
|
pkill -f "uvicorn" 2>/dev/null || true
|
||||||
|
cd "$SCRIPT_DIR/backend"
|
||||||
|
PYTHONPATH="$SCRIPT_DIR/backend" nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload > /tmp/planet_backend.log 2>&1 &
|
||||||
|
BACKEND_PID=$!
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if ! curl -s http://localhost:8000/health > /dev/null 2>&1; then
|
||||||
|
echo -e "${RED}❌ 后端启动失败${NC}"
|
||||||
|
tail -10 /tmp/planet_backend.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}🌐 启动前端...${NC}"
|
||||||
|
pkill -f "vite" 2>/dev/null || true
|
||||||
|
pkill -f "bun run dev" 2>/dev/null || true
|
||||||
|
cd "$SCRIPT_DIR/frontend"
|
||||||
|
nohup bun run dev --port 3000 > /tmp/planet_frontend.log 2>&1 &
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ 启动完成!${NC}"
|
||||||
|
echo " 前端: http://localhost:3000"
|
||||||
|
echo " 后端: http://localhost:8000"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
echo -e "${YELLOW}🛑 停止服务...${NC}"
|
||||||
|
pkill -f "uvicorn" 2>/dev/null || true
|
||||||
|
pkill -f "vite" 2>/dev/null || true
|
||||||
|
pkill -f "bun run dev" 2>/dev/null || true
|
||||||
|
docker stop planet_postgres planet_redis 2>/dev/null || true
|
||||||
|
echo -e "${GREEN}✅ 已停止${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
stop
|
||||||
|
sleep 1
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
health() {
|
||||||
|
echo "📊 容器状态:"
|
||||||
|
docker ps --filter "name=planet_" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔍 服务状态:"
|
||||||
|
if curl -s http://localhost:8000/health > /dev/null 2>&1; then
|
||||||
|
echo -e " 后端: ${GREEN}✅ 运行中${NC}"
|
||||||
|
else
|
||||||
|
echo -e " 后端: ${RED}❌ 未运行${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if curl -s http://localhost:3000 > /dev/null 2>&1; then
|
||||||
|
echo -e " 前端: ${GREEN}✅ 运行中${NC}"
|
||||||
|
else
|
||||||
|
echo -e " 前端: ${RED}❌ 未运行${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
case "$1" in
|
||||||
|
-f|--frontend)
|
||||||
|
echo "📝 前端日志 (Ctrl+C 退出):"
|
||||||
|
tail -f /tmp/planet_frontend.log
|
||||||
|
;;
|
||||||
|
-b|--backend)
|
||||||
|
echo "📝 后端日志 (Ctrl+C 退出):"
|
||||||
|
tail -f /tmp/planet_backend.log
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "📝 最近日志:"
|
||||||
|
echo "--- 后端 ---"
|
||||||
|
tail -20 /tmp/planet_backend.log 2>/dev/null || echo "无日志"
|
||||||
|
echo "--- 前端 ---"
|
||||||
|
tail -20 /tmp/planet_frontend.log 2>/dev/null || echo "无日志"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
health)
|
||||||
|
health
|
||||||
|
;;
|
||||||
|
log)
|
||||||
|
log "$2"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "用法: ./planet.sh {start|stop|restart|health|log}"
|
||||||
|
echo ""
|
||||||
|
echo "命令:"
|
||||||
|
echo " start 启动服务"
|
||||||
|
echo " stop 停止服务"
|
||||||
|
echo " restart 重启服务"
|
||||||
|
echo " health 检查健康状态"
|
||||||
|
echo " log 查看日志"
|
||||||
|
echo " log -f 查看前端日志"
|
||||||
|
echo " log -b 查看后端日志"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
134
restart.sh
@@ -1,134 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Planet 重启脚本 - 停止并重启所有服务
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
# 颜色定义
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
echo -e "${BLUE}🔄 重启智能星球计划...${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 停止服务
|
|
||||||
echo -e "${YELLOW}🛑 停止服务...${NC}"
|
|
||||||
|
|
||||||
# 停止后端
|
|
||||||
if pgrep -f "uvicorn.*app.main:app" > /dev/null 2>&1; then
|
|
||||||
pkill -f "uvicorn.*app.main:app" 2>/dev/null || true
|
|
||||||
echo " ✅ 后端已停止"
|
|
||||||
else
|
|
||||||
echo " ℹ️ 后端未运行"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 停止前端
|
|
||||||
if pgrep -f "vite" > /dev/null 2>&1; then
|
|
||||||
pkill -f "vite" 2>/dev/null || true
|
|
||||||
echo " ✅ 前端已停止"
|
|
||||||
else
|
|
||||||
echo " ℹ️ 前端未运行"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 停止 Docker 服务
|
|
||||||
if command -v docker &> /dev/null && docker ps &> /dev/null 2>&1; then
|
|
||||||
if command -v docker-compose &> /dev/null; then
|
|
||||||
docker-compose down > /dev/null 2>&1 || true
|
|
||||||
else
|
|
||||||
docker compose down > /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
echo " ✅ Docker 容器已停止"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 等待服务完全停止
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}⏳ 等待进程退出...${NC}"
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# 检查端口是否已释放
|
|
||||||
if lsof -i :8000 > /dev/null 2>&1 || netstat -tlnp 2>/dev/null | grep -q ":8000" || ss -tlnp 2>/dev/null | grep -q ":8000"; then
|
|
||||||
echo -e "${YELLOW}⚠️ 端口 8000 仍被占用,强制杀死...${NC}"
|
|
||||||
fuser -k 8000/tcp 2>/dev/null || true
|
|
||||||
sleep 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if lsof -i :3000 > /dev/null 2>&1 || netstat -tlnp 2>/dev/null | grep -q ":3000" || ss -tlnp 2>/dev/null | grep -q ":3000"; then
|
|
||||||
echo -e "${YELLOW}⚠️ 端口 3000 仍被占用,强制杀死...${NC}"
|
|
||||||
fuser -k 3000/tcp 2>/dev/null || true
|
|
||||||
sleep 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}✅ 服务已停止${NC}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
echo -e "${BLUE}🚀 启动服务...${NC}"
|
|
||||||
|
|
||||||
# 启动 Docker 服务
|
|
||||||
if command -v docker &> /dev/null && docker ps &> /dev/null 2>&1; then
|
|
||||||
echo -e "${BLUE}🗄️ 启动数据库...${NC}"
|
|
||||||
if command -v docker-compose &> /dev/null; then
|
|
||||||
docker-compose up -d postgres redis > /dev/null 2>&1 || true
|
|
||||||
else
|
|
||||||
docker compose up -d postgres redis > /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
echo -e "${YELLOW}⏳ 等待数据库就绪...${NC}"
|
|
||||||
sleep 5
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 同步 Python 依赖
|
|
||||||
echo -e "${BLUE}🐍 同步 Python 依赖...${NC}"
|
|
||||||
cd "$SCRIPT_DIR/backend"
|
|
||||||
uv sync > /dev/null 2>&1 || true
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
# 初始化数据库
|
|
||||||
echo -e "${BLUE}🗃️ 初始化数据库...${NC}"
|
|
||||||
PYTHONPATH="$SCRIPT_DIR/backend" python "$SCRIPT_DIR/backend/scripts/init_admin.py" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# 启动后端
|
|
||||||
echo -e "${BLUE}🔧 启动后端服务...${NC}"
|
|
||||||
export PYTHONPATH="$SCRIPT_DIR/backend"
|
|
||||||
cd "$SCRIPT_DIR/backend"
|
|
||||||
nohup python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 > /tmp/planet_backend.log 2>&1 &
|
|
||||||
BACKEND_PID=$!
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
echo -e "${YELLOW}⏳ 等待后端启动...${NC}"
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
if curl -s http://localhost:8000/health > /dev/null 2>&1; then
|
|
||||||
echo -e "${GREEN}✅ 后端已启动${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}❌ 后端启动失败${NC}"
|
|
||||||
tail -10 /tmp/planet_backend.log
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 启动前端
|
|
||||||
echo -e "${BLUE}🌐 启动前端服务...${NC}"
|
|
||||||
cd "$SCRIPT_DIR/frontend"
|
|
||||||
npm run dev > /tmp/planet_frontend.log 2>&1 &
|
|
||||||
FRONTEND_PID=$!
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}========================================${NC}"
|
|
||||||
echo -e "${GREEN}✅ 重启完成!${NC}"
|
|
||||||
echo -e "${GREEN}========================================${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}🌐 访问地址:${NC}"
|
|
||||||
echo " 后端: http://localhost:8000"
|
|
||||||
echo " API文档: http://localhost:8000/docs"
|
|
||||||
echo " 前端: http://localhost:3000"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}📝 日志:${NC}"
|
|
||||||
echo " tail -f /tmp/planet_backend.log"
|
|
||||||
echo " tail -f /tmp/planet_frontend.log"
|
|
||||||
158
start.sh
@@ -1,158 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Planet 一键启动脚本
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
echo "🚀 启动智能星球计划..."
|
|
||||||
|
|
||||||
# 颜色定义
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# 检查 Docker 服务
|
|
||||||
if ! command -v docker &> /dev/null; then
|
|
||||||
echo -e "${RED}❌ Docker 未安装,请先安装 Docker${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! docker info &> /dev/null; then
|
|
||||||
echo -e "${RED}❌ Docker 服务未运行,请先启动 Docker${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查并安装 uv
|
|
||||||
if ! command -v uv &> /dev/null; then
|
|
||||||
echo -e "${YELLOW}📦 安装 uv...${NC}"
|
|
||||||
curl -Ls https://astral.sh/uv | sh
|
|
||||||
export PATH="$HOME/.cargo/bin:$PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查 npm
|
|
||||||
if ! command -v npm &> /dev/null; then
|
|
||||||
echo -e "${RED}❌ npm 未安装,请先安装 Node.js${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 启动基础设施 (PostgreSQL + Redis)
|
|
||||||
echo -e "${BLUE}🗄️ 启动数据库...${NC}"
|
|
||||||
# 检查 docker compose vs docker-compose
|
|
||||||
if command -v docker-compose &> /dev/null; then
|
|
||||||
docker-compose up -d postgres redis || true
|
|
||||||
else
|
|
||||||
docker compose up -d postgres redis || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 等待数据库就绪
|
|
||||||
echo -e "${YELLOW}⏳ 等待数据库就绪...${NC}"
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# 检查数据库是否就绪
|
|
||||||
MAX_RETRIES=10
|
|
||||||
RETRY_COUNT=0
|
|
||||||
# 检查 docker compose vs docker-compose
|
|
||||||
if command -v docker-compose &> /dev/null; then
|
|
||||||
while ! docker exec planet_postgres pg_isready -U postgres &> /dev/null; do
|
|
||||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
||||||
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
|
|
||||||
echo -e "${RED}❌ 数据库启动超时${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -e "${YELLOW} 等待数据库... ($RETRY_COUNT/$MAX_RETRIES)${NC}"
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
else
|
|
||||||
while ! docker exec planet_postgres pg_isready -U postgres &> /dev/null; do
|
|
||||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
|
||||||
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
|
|
||||||
echo -e "${RED}❌ 数据库启动超时${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -e "${YELLOW} 等待数据库... ($RETRY_COUNT/$MAX_RETRIES)${NC}"
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
echo -e "${GREEN}✅ 数据库已就绪${NC}"
|
|
||||||
|
|
||||||
# 同步 Python 依赖
|
|
||||||
echo -e "${BLUE}🐍 同步 Python 依赖...${NC}"
|
|
||||||
uv sync
|
|
||||||
|
|
||||||
# 初始化数据库
|
|
||||||
echo -e "${BLUE}🗃️ 初始化数据库...${NC}"
|
|
||||||
PYTHONPATH="$SCRIPT_DIR/backend" python "$SCRIPT_DIR/backend/scripts/init_admin.py" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# 设置环境变量
|
|
||||||
export PYTHONPATH="$SCRIPT_DIR/backend"
|
|
||||||
|
|
||||||
# 启动后端服务
|
|
||||||
echo -e "${BLUE}🔧 启动后端服务...${NC}"
|
|
||||||
|
|
||||||
# 检查端口 8000 是否被占用
|
|
||||||
if lsof -i :8000 > /dev/null 2>&1 || netstat -tlnp 2>/dev/null | grep -q ":8000" || ss -tlnp 2>/dev/null | grep -q ":8000"; then
|
|
||||||
echo -e "${RED}❌ 端口 8000 已被占用,请先停止占用端口的进程${NC}"
|
|
||||||
echo " 使用命令: ./kill_port.sh 8000 或 lsof -i :8000 查看"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "$SCRIPT_DIR/backend"
|
|
||||||
PYTHONPATH="$SCRIPT_DIR/backend" nohup python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 > /tmp/planet_backend.log 2>&1 &
|
|
||||||
BACKEND_PID=$!
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
echo " 后端 PID: $BACKEND_PID"
|
|
||||||
|
|
||||||
# 等待后端启动
|
|
||||||
echo -e "${YELLOW}⏳ 等待后端服务启动...${NC}"
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# 检查后端是否正常运行
|
|
||||||
if ! curl -s http://localhost:8000/health > /dev/null 2>&1; then
|
|
||||||
echo -e "${RED}❌ 后端服务启动失败,查看日志: tail /tmp/planet_backend.log${NC}"
|
|
||||||
tail -20 /tmp/planet_backend.log
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -e "${GREEN}✅ 后端服务已启动${NC}"
|
|
||||||
|
|
||||||
# 检查并安装前端依赖
|
|
||||||
if [ ! -d "frontend/node_modules" ]; then
|
|
||||||
echo -e "${YELLOW}📦 安装前端依赖...${NC}"
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
cd ..
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 启动前端服务
|
|
||||||
echo -e "${BLUE}🌐 启动前端服务...${NC}"
|
|
||||||
cd frontend
|
|
||||||
npm run dev > /tmp/planet_frontend.log 2>&1 &
|
|
||||||
FRONTEND_PID=$!
|
|
||||||
echo " 前端 PID: $FRONTEND_PID"
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}========================================${NC}"
|
|
||||||
echo -e "${GREEN}✅ 智能星球计划启动完成!${NC}"
|
|
||||||
echo -e "${GREEN}========================================${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}🌐 访问地址:${NC}"
|
|
||||||
echo " 后端: http://localhost:8000"
|
|
||||||
echo " API文档: http://localhost:8000/docs"
|
|
||||||
echo " 前端: http://localhost:3000"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}📝 服务进程:${NC}"
|
|
||||||
echo " 后端 PID: $BACKEND_PID"
|
|
||||||
echo " 前端 PID: $FRONTEND_PID"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}📝 日志查看:${NC}"
|
|
||||||
echo " tail -f /tmp/planet_backend.log"
|
|
||||||
echo " tail -f /tmp/planet_frontend.log"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}🛑 停止服务:${NC}"
|
|
||||||
echo " pkill -f 'uvicorn' # 停止后端"
|
|
||||||
echo " pkill -f 'vite' # 停止前端"
|
|
||||||
38
stop.sh
@@ -1,38 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Planet 停止脚本
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
echo "🛑 停止智能星球计划..."
|
|
||||||
|
|
||||||
# 停止后端服务
|
|
||||||
if pgrep -f "uvicorn" > /dev/null; then
|
|
||||||
echo "🔧 停止后端服务..."
|
|
||||||
pkill -f "uvicorn" || true
|
|
||||||
echo "✅ 后端服务已停止"
|
|
||||||
else
|
|
||||||
echo "ℹ️ 后端服务未运行"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 停止前端服务
|
|
||||||
if pgrep -f "vite" > /dev/null; then
|
|
||||||
echo "🌐 停止前端服务..."
|
|
||||||
pkill -f "vite" || true
|
|
||||||
echo "✅ 前端服务已停止"
|
|
||||||
else
|
|
||||||
echo "ℹ️ 前端服务未运行"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 停止 Docker 容器
|
|
||||||
echo "🗄️ 停止数据库容器..."
|
|
||||||
if command -v docker-compose &> /dev/null; then
|
|
||||||
docker-compose stop postgres redis || true
|
|
||||||
else
|
|
||||||
docker compose stop postgres redis || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ 所有服务已停止"
|
|
||||||