| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 |
- // 模拟 MAXBAND 输出: 输入路口距离/周期/双向车速/协调模式 → 输出每路口 offset (秒)
- //
- // 不是真正的 MAXBAND 线性规划求解, 是几何启发式:
- // - forward : 让正向波带 A→D 在每个路口贴着绿灯起点进入 (与 mock 旧逻辑等价)
- // - reverse : 反过来, 让反向波带 D→A 在每个路口贴着绿灯起点进入
- // - balanced : 折中, 取正反到达时刻的中点, 双向都能擦上绿窗 (默认)
- //
- // 真后端用 MAXBAND 时, 删本文件, apiGetTrunkCoordination 改成 axios.get; 前端契约不变。
- /**
- * @param {Object} opts
- * @param {number[]} opts.distances 每段路距离 (m), 最后一个路口 distanceNext = 0
- * @param {number} opts.cycle 信号周期 (s)
- * @param {number} opts.speedFwd 正向设计车速 (km/h)
- * @param {number} opts.speedRev 反向设计车速 (km/h); 不传 fallback 到 speedFwd
- * @param {string} opts.mode 'forward' | 'reverse' | 'balanced' (默认)
- * @returns {number[]} 每个路口的 offset (s), 长度同 distances; 第一个锚定为 0
- */
- export function simulateMaxband(opts) {
- const {
- distances,
- cycle = 100,
- speedFwd = 40,
- speedRev,
- mode = 'balanced',
- } = opts;
- const N = distances.length;
- if (N === 0) return [];
- const speedFwdMs = speedFwd / 3.6;
- const speedRevMs = (speedRev || speedFwd) / 3.6;
- // 累计距离: cumDist[i] = 第一个路口到第 i 个路口的距离
- const cumDist = [0];
- for (let i = 0; i < N - 1; i++) {
- cumDist.push(cumDist[i] + distances[i]);
- }
- const totalLen = cumDist[N - 1];
- const norm = (x) => {
- let v = Math.round(x) % cycle;
- if (v < 0) v += cycle;
- return v;
- };
- // 锚定第一个路口为 offset=0 (绿波协调的相对参考)
- if (mode === 'forward') {
- // 正向波带从 A 出发 y=0, 沿 t = x / speedFwd 斜上;
- // 让每个路口 offset = 该时刻 mod cycle, 波带紧贴各路口绿窗起点
- return cumDist.map(x => norm(x / speedFwdMs));
- }
- if (mode === 'reverse') {
- // 反向波带从 D 出发 y=0 (反向坐标), 让每个路口 offset = (totalLen - x) / speedRev mod cycle
- // 最后一个路口 (D) offset = 0; 第一个路口 (A) offset = totalLen / speedRev mod cycle
- // 为了保持第一个路口锚定 0, 整体减去 A 的反向到达时间
- const anchorRev = totalLen / speedRevMs;
- return cumDist.map(x => norm((totalLen - x) / speedRevMs - anchorRev));
- }
- // balanced: 圆周平均 forward-optimal 和 reverse-optimal 两个角度
- //
- // 为什么不用线性中点?
- // - mod cycle 后, 线性中点会塌缩 (例: 平均 89.5 和 4 给 46.75, 但圆周上它们其实只相差 ~10°)
- // - 圆周平均把 offset 看作单位圆角度, cos/sin 平均后 atan2 还原, 自然处理模周期回绕
- //
- // 公式:
- // forwardOpt_i = x_i / speedFwd (mod cycle)
- // reverseOpt_i = -x_i / speedRev (mod cycle, 锚定 A=0)
- // balanced_i = circularAvg(forwardOpt_i, reverseOpt_i)
- const TWO_PI = 2 * Math.PI;
- const toAngle = (offset) => (offset / cycle) * TWO_PI;
- const fromAngle = (angle) => {
- let v = (angle / TWO_PI) * cycle;
- while (v < 0) v += cycle;
- return v % cycle;
- };
- return cumDist.map((x) => {
- const fwdOpt = (x / speedFwdMs) % cycle;
- const revOpt = ((-x / speedRevMs) % cycle + cycle) % cycle;
- const aFwd = toAngle(fwdOpt);
- const aRev = toAngle(revOpt);
- const cosAvg = (Math.cos(aFwd) + Math.cos(aRev)) / 2;
- const sinAvg = (Math.sin(aFwd) + Math.sin(aRev)) / 2;
- const avgAngle = Math.atan2(sinAvg, cosAvg);
- return Math.round(fromAngle(avgAngle));
- });
- }
|