Skip to content

Commit fd1971d

Browse files
authored
Merge pull request #94 from irlin8-ttimm/pr93-link-animation
feat(frontend): 链路传输动画与按速率映射的链路粗细
2 parents fbc4217 + a3b0d88 commit fd1971d

1 file changed

Lines changed: 163 additions & 17 deletions

File tree

frontend/app.js

Lines changed: 163 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ const app = Vue.createApp({
4040
// Cesium 增量更新:实体索引表,避免每次 removeAll 重建场景
4141
_entityIndex: {}, // id -> Cesium.Entity,管理节点(卫星、地面站、中继)
4242
_linkIndex: {}, // linkKey -> Cesium.Entity,管理链路 polyline
43+
// 链路动画状态:流动动画偏移量与 rAF 句柄
44+
_linkAnimOffset: 0, // 当前动画时间偏移(0–1 循环,用于 dashPattern 驱动流动效果)
45+
_linkAnimFrame: null, // requestAnimationFrame 句柄,非 null 时表示动画循环运行
46+
_linkAnimRateMap: {}, // linkKey -> { rate, method },缓存链路传输速率与方式
4347
// 轨道轨迹索引:satId -> { orbitEntity, groundEntity }
4448
_trackIndex: {},
4549
_tracksLoading: false,
@@ -258,6 +262,11 @@ const app = Vue.createApp({
258262
window.clearInterval(this.refreshTimer);
259263
this.refreshTimer = null;
260264
}
265+
// 取消链路动画循环
266+
if (this._linkAnimFrame !== null) {
267+
cancelAnimationFrame(this._linkAnimFrame);
268+
this._linkAnimFrame = null;
269+
}
261270
window.removeEventListener('resize', this.resizeViewer);
262271
if (this.viewer && !this.viewer.isDestroyed()) {
263272
this.viewer.destroy();
@@ -267,6 +276,7 @@ const app = Vue.createApp({
267276
this._linkIndex = {};
268277
this._trackIndex = {};
269278
this._coverageIndex = {};
279+
this._linkAnimRateMap = {};
270280
},
271281

272282
methods: {
@@ -674,17 +684,25 @@ const app = Vue.createApp({
674684
.forEach((req) => {
675685
const satellite = satMap.get(req.satellite_id);
676686
if (!satellite) return;
677-
this.updateRequestLinks(req, satellite, geoMap, gsMap, linkColor, liveLinkKeys);
687+
this.updateRequestLinks(req, satellite, geoMap, gsMap, liveLinkKeys);
678688
});
679689

680690
// 移除已消失的链路实体
681691
for (const lkey of Object.keys(this._linkIndex)) {
682692
if (!liveLinkKeys.has(lkey)) {
683693
this.viewer.entities.remove(this._linkIndex[lkey]);
684694
delete this._linkIndex[lkey];
695+
delete this._linkAnimRateMap[lkey];
685696
}
686697
}
687698

699+
// 启动链路流动动画循环(若尚未运行)
700+
if (liveLinkKeys.size > 0) {
701+
this._startLinkAnimation();
702+
} else {
703+
this._stopLinkAnimation();
704+
}
705+
688706
// ── 覆盖足迹与可见性圈 ────────────────────────────────────────
689707
this.updateCoverageFootprints();
690708

@@ -697,8 +715,9 @@ const app = Vue.createApp({
697715
/**
698716
* 为单条请求按需增量更新/创建链路 polyline 实体。
699717
* liveLinkKeys 用于记录本轮活跃的链路 key,以便事后清理。
718+
* 链路颜色由 transmission_method 决定,线宽由 transmission_rate 归一化映射。
700719
*/
701-
updateRequestLinks(req, satellite, geoMap, gsMap, color, liveLinkKeys) {
720+
updateRequestLinks(req, satellite, geoMap, gsMap, liveLinkKeys) {
702721
const groundStation = req.selected_ground_station
703722
? gsMap.get(req.selected_ground_station)
704723
: null;
@@ -709,54 +728,181 @@ const app = Vue.createApp({
709728
? geoMap.get(req.selected_relay2)
710729
: null;
711730

731+
const rate = Number(req.transmission_rate || 0);
732+
const method = req.transmission_method || 'direct';
733+
712734
if (firstRelay && secondRelay) {
713-
this.upsertLink(`${req.id}:sat-relay1`, satellite, firstRelay, color, liveLinkKeys);
714-
this.upsertLink(`${req.id}:relay1-relay2`, firstRelay, secondRelay, color, liveLinkKeys);
735+
this.upsertLink(`${req.id}:sat-relay1`, satellite, firstRelay, rate, method, liveLinkKeys);
736+
this.upsertLink(`${req.id}:relay1-relay2`, firstRelay, secondRelay, rate, method, liveLinkKeys);
715737
if (groundStation) {
716-
this.upsertLink(`${req.id}:relay2-gs`, secondRelay, groundStation, color, liveLinkKeys);
738+
this.upsertLink(`${req.id}:relay2-gs`, secondRelay, groundStation, rate, method, liveLinkKeys);
717739
}
718740
return;
719741
}
720742

721743
if (firstRelay) {
722-
this.upsertLink(`${req.id}:sat-relay1`, satellite, firstRelay, color, liveLinkKeys);
744+
this.upsertLink(`${req.id}:sat-relay1`, satellite, firstRelay, rate, method, liveLinkKeys);
723745
if (groundStation) {
724-
this.upsertLink(`${req.id}:relay1-gs`, firstRelay, groundStation, color, liveLinkKeys);
746+
this.upsertLink(`${req.id}:relay1-gs`, firstRelay, groundStation, rate, method, liveLinkKeys);
725747
}
726748
return;
727749
}
728750

729-
if (groundStation && req.transmission_method === 'direct') {
730-
this.upsertLink(`${req.id}:sat-gs`, satellite, groundStation, color, liveLinkKeys);
751+
if (groundStation && method === 'direct') {
752+
this.upsertLink(`${req.id}:sat-gs`, satellite, groundStation, rate, method, liveLinkKeys);
753+
}
754+
},
755+
756+
/**
757+
* 将传输速率(Mbps)归一化为链路线宽(1.5–6px)。
758+
* 使用对数映射,使低速率链路也有明显变化。
759+
* @param {number} rateMbps - 传输速率(Mbps)
760+
* @returns {number} 线宽(px)
761+
*/
762+
_rateToLineWidth(rateMbps) {
763+
const MIN_W = 1.5;
764+
const MAX_W = 6.0;
765+
const MAX_RATE = 1000; // 参考最大速率 1000 Mbps
766+
if (!rateMbps || rateMbps <= 0) return MIN_W;
767+
// 对数归一化:log(1 + rate) / log(1 + MAX_RATE)
768+
const norm = Math.log1p(Math.min(rateMbps, MAX_RATE)) / Math.log1p(MAX_RATE);
769+
return MIN_W + norm * (MAX_W - MIN_W);
770+
},
771+
772+
/**
773+
* 根据传输方式返回对应的链路颜色(Cesium.Color)。
774+
* - direct(直连):金黄色
775+
* - relay(单中继):青绿色
776+
* - multi_relay(多跳):紫色
777+
* @param {string} method - 传输方式
778+
* @returns {Cesium.Color}
779+
*/
780+
_methodToLinkColor(method) {
781+
if (!window.Cesium) return null;
782+
switch (method) {
783+
case 'relay': return Cesium.Color.fromCssColorString('#3dcfc4'); // 青绿
784+
case 'multi_relay': return Cesium.Color.fromCssColorString('#b67cf6'); // 紫色
785+
default: return Cesium.Color.fromCssColorString('#f0c76b'); // 金黄(直连)
731786
}
732787
},
733788

734789
/**
735790
* 按 linkKey 复用已有的 polyline 实体;不存在时新建并注册到 _linkIndex。
736-
* 复用时更新两端点坐标,保持实体引用稳定。
791+
* 复用时更新两端点坐标与动画速率缓存,保持实体引用稳定。
792+
* 使用 PolylineGlowMaterialProperty 表现数据流向,线宽随传输速率变化。
793+
* @param {string} linkKey - 链路唯一键
794+
* @param {object} from - 起始节点 { lon, lat, alt }
795+
* @param {object} to - 终止节点 { lon, lat, alt }
796+
* @param {number} rateMbps - 传输速率(Mbps),用于映射线宽
797+
* @param {string} method - 传输方式(direct/relay/multi_relay),用于确定颜色
798+
* @param {Set<string>} liveLinkKeys - 本轮活跃链路集合
737799
*/
738-
upsertLink(linkKey, from, to, color, liveLinkKeys) {
800+
upsertLink(linkKey, from, to, rateMbps, method, liveLinkKeys) {
739801
liveLinkKeys.add(linkKey);
740802
const positions = [
741803
this.toCartesian(from.lon, from.lat, from.alt || 0),
742804
this.toCartesian(to.lon, to.lat, to.alt || 0),
743805
];
806+
const width = this._rateToLineWidth(rateMbps);
807+
const baseColor = this._methodToLinkColor(method || 'direct');
808+
809+
// 缓存当前链路速率与方式,供动画循环使用
810+
this._linkAnimRateMap[linkKey] = { rate: rateMbps, method: method || 'direct' };
744811

745812
if (this._linkIndex[linkKey]) {
746-
// 更新两端点位置
747-
this._linkIndex[linkKey].polyline.positions = positions;
813+
// 更新两端点位置与线宽
814+
const poly = this._linkIndex[linkKey].polyline;
815+
poly.positions = positions;
816+
poly.width = width;
817+
// 更新发光材质颜色(速率/方式可能在刷新中变化)
818+
if (poly.material && poly.material instanceof Cesium.PolylineGlowMaterialProperty) {
819+
poly.material = new Cesium.PolylineGlowMaterialProperty({
820+
glowPower: 0.25,
821+
taperPower: 0.9,
822+
color: baseColor.withAlpha(0.85),
823+
});
824+
}
748825
} else {
749826
const entity = this.viewer.entities.add({
750827
polyline: {
751828
positions,
752-
width: 2.5,
753-
material: color,
829+
width,
830+
material: new Cesium.PolylineGlowMaterialProperty({
831+
glowPower: 0.25,
832+
taperPower: 0.9,
833+
color: baseColor.withAlpha(0.85),
834+
}),
835+
arcType: Cesium.ArcType.NONE,
754836
},
755837
});
756838
this._linkIndex[linkKey] = entity;
757839
}
758840
},
759841

842+
/**
843+
* 启动链路流动动画循环。
844+
* 通过持续调用 requestRender 驱动 Cesium 在 requestRenderMode 下不断重绘,
845+
* 同时利用 PolylineGlowMaterialProperty 的自发光效果模拟数据流动感。
846+
* 每帧递增 _linkAnimOffset 并对有速率信息的链路微调发光强度,产生脉冲效果。
847+
*/
848+
_startLinkAnimation() {
849+
if (this._linkAnimFrame !== null) return; // 已在运行
850+
if (!this.viewer || !this.cesiumReady) return;
851+
852+
let lastTime = performance.now();
853+
const tick = (now) => {
854+
const dt = Math.min((now - lastTime) / 1000, 0.1); // 单帧增量(秒),上限 100ms
855+
lastTime = now;
856+
857+
// 更新全局动画偏移(0–1 循环)
858+
this._linkAnimOffset = (this._linkAnimOffset + dt * 0.5) % 1;
859+
860+
// 对每条活跃链路:按速率调整发光强度,营造脉冲流动感
861+
if (window.Cesium) {
862+
const t = this._linkAnimOffset;
863+
for (const [lkey, info] of Object.entries(this._linkAnimRateMap)) {
864+
const entity = this._linkIndex[lkey];
865+
if (!entity || !entity.polyline) continue;
866+
// 速率越高脉冲越快;基础脉冲频率 1Hz,速率比例最高 3x
867+
const freq = 1 + Math.min(2, (info.rate || 0) / 200);
868+
const phase = (t * freq) % 1;
869+
// 发光强度在 0.15–0.55 间脉冲
870+
const glow = 0.15 + 0.40 * Math.abs(Math.sin(phase * Math.PI));
871+
const baseColor = this._methodToLinkColor(info.method);
872+
entity.polyline.material = new Cesium.PolylineGlowMaterialProperty({
873+
glowPower: glow,
874+
taperPower: 0.9,
875+
color: baseColor.withAlpha(0.85),
876+
});
877+
}
878+
}
879+
880+
// 在 requestRenderMode 下触发重绘
881+
if (this.viewer && !this.viewer.isDestroyed()) {
882+
this.viewer.scene.requestRender();
883+
}
884+
885+
// 若仍有活跃链路则继续循环
886+
if (Object.keys(this._linkAnimRateMap).length > 0) {
887+
this._linkAnimFrame = requestAnimationFrame(tick);
888+
} else {
889+
this._linkAnimFrame = null;
890+
}
891+
};
892+
893+
this._linkAnimFrame = requestAnimationFrame(tick);
894+
},
895+
896+
/**
897+
* 停止链路流动动画循环(当无活跃传输链路时调用)。
898+
*/
899+
_stopLinkAnimation() {
900+
if (this._linkAnimFrame !== null) {
901+
cancelAnimationFrame(this._linkAnimFrame);
902+
this._linkAnimFrame = null;
903+
}
904+
},
905+
760906
makeLabel(text, color, pixelOffsetY) {
761907
return {
762908
text: String(text || ''),
@@ -790,9 +936,9 @@ const app = Vue.createApp({
790936
/**
791937
* @deprecated 由 updateRequestLinks 取代。
792938
*/
793-
drawRequestLinks(req, satellite, geoMap, gsMap, color) {
939+
drawRequestLinks(req, satellite, geoMap, gsMap) {
794940
const liveLinkKeys = new Set();
795-
this.updateRequestLinks(req, satellite, geoMap, gsMap, color, liveLinkKeys);
941+
this.updateRequestLinks(req, satellite, geoMap, gsMap, liveLinkKeys);
796942
},
797943

798944
/**

0 commit comments

Comments
 (0)