// 模拟 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)); }); }