|
|
@@ -0,0 +1,90 @@
|
|
|
+// 模拟 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));
|
|
|
+ });
|
|
|
+}
|