_simulateMaxband.js 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. // 模拟 MAXBAND 输出: 输入路口距离/周期/双向车速/协调模式 → 输出每路口 offset (秒)
  2. //
  3. // 不是真正的 MAXBAND 线性规划求解, 是几何启发式:
  4. // - forward : 让正向波带 A→D 在每个路口贴着绿灯起点进入 (与 mock 旧逻辑等价)
  5. // - reverse : 反过来, 让反向波带 D→A 在每个路口贴着绿灯起点进入
  6. // - balanced : 折中, 取正反到达时刻的中点, 双向都能擦上绿窗 (默认)
  7. //
  8. // 真后端用 MAXBAND 时, 删本文件, apiGetTrunkCoordination 改成 axios.get; 前端契约不变。
  9. /**
  10. * @param {Object} opts
  11. * @param {number[]} opts.distances 每段路距离 (m), 最后一个路口 distanceNext = 0
  12. * @param {number} opts.cycle 信号周期 (s)
  13. * @param {number} opts.speedFwd 正向设计车速 (km/h)
  14. * @param {number} opts.speedRev 反向设计车速 (km/h); 不传 fallback 到 speedFwd
  15. * @param {string} opts.mode 'forward' | 'reverse' | 'balanced' (默认)
  16. * @returns {number[]} 每个路口的 offset (s), 长度同 distances; 第一个锚定为 0
  17. */
  18. export function simulateMaxband(opts) {
  19. const {
  20. distances,
  21. cycle = 100,
  22. speedFwd = 40,
  23. speedRev,
  24. mode = 'balanced',
  25. } = opts;
  26. const N = distances.length;
  27. if (N === 0) return [];
  28. const speedFwdMs = speedFwd / 3.6;
  29. const speedRevMs = (speedRev || speedFwd) / 3.6;
  30. // 累计距离: cumDist[i] = 第一个路口到第 i 个路口的距离
  31. const cumDist = [0];
  32. for (let i = 0; i < N - 1; i++) {
  33. cumDist.push(cumDist[i] + distances[i]);
  34. }
  35. const totalLen = cumDist[N - 1];
  36. const norm = (x) => {
  37. let v = Math.round(x) % cycle;
  38. if (v < 0) v += cycle;
  39. return v;
  40. };
  41. // 锚定第一个路口为 offset=0 (绿波协调的相对参考)
  42. if (mode === 'forward') {
  43. // 正向波带从 A 出发 y=0, 沿 t = x / speedFwd 斜上;
  44. // 让每个路口 offset = 该时刻 mod cycle, 波带紧贴各路口绿窗起点
  45. return cumDist.map(x => norm(x / speedFwdMs));
  46. }
  47. if (mode === 'reverse') {
  48. // 反向波带从 D 出发 y=0 (反向坐标), 让每个路口 offset = (totalLen - x) / speedRev mod cycle
  49. // 最后一个路口 (D) offset = 0; 第一个路口 (A) offset = totalLen / speedRev mod cycle
  50. // 为了保持第一个路口锚定 0, 整体减去 A 的反向到达时间
  51. const anchorRev = totalLen / speedRevMs;
  52. return cumDist.map(x => norm((totalLen - x) / speedRevMs - anchorRev));
  53. }
  54. // balanced: 圆周平均 forward-optimal 和 reverse-optimal 两个角度
  55. //
  56. // 为什么不用线性中点?
  57. // - mod cycle 后, 线性中点会塌缩 (例: 平均 89.5 和 4 给 46.75, 但圆周上它们其实只相差 ~10°)
  58. // - 圆周平均把 offset 看作单位圆角度, cos/sin 平均后 atan2 还原, 自然处理模周期回绕
  59. //
  60. // 公式:
  61. // forwardOpt_i = x_i / speedFwd (mod cycle)
  62. // reverseOpt_i = -x_i / speedRev (mod cycle, 锚定 A=0)
  63. // balanced_i = circularAvg(forwardOpt_i, reverseOpt_i)
  64. const TWO_PI = 2 * Math.PI;
  65. const toAngle = (offset) => (offset / cycle) * TWO_PI;
  66. const fromAngle = (angle) => {
  67. let v = (angle / TWO_PI) * cycle;
  68. while (v < 0) v += cycle;
  69. return v % cycle;
  70. };
  71. return cumDist.map((x) => {
  72. const fwdOpt = (x / speedFwdMs) % cycle;
  73. const revOpt = ((-x / speedRevMs) % cycle + cycle) % cycle;
  74. const aFwd = toAngle(fwdOpt);
  75. const aRev = toAngle(revOpt);
  76. const cosAvg = (Math.cos(aFwd) + Math.cos(aRev)) / 2;
  77. const sinAvg = (Math.sin(aFwd) + Math.sin(aRev)) / 2;
  78. const avgAngle = Math.atan2(sinAvg, cosAvg);
  79. return Math.round(fromAngle(avgAngle));
  80. });
  81. }