瀏覽代碼

干线时空图: 抽 mock 模拟 MAXBAND + 完美对称协调参数 + 修底部柱条裁切

  mock/_simulateMaxband.js (新增):
  - 启发式 MAXBAND 模拟器 (B 档): forward / reverse / balanced 三种 mode
  - balanced 用圆周平均 (cos/sin 平均后 atan2 还原) 处理 mod cycle 回绕,
    避免线性中点在角度回绕处塌缩

  mock/api.js:
  - apiGetTrafficTimeSpace 改用 simulateMaxband 算 offsets, 取代原"硬编码
    正向最优"的循环, 返回 meta.algorithm / coordinationMode
  - 参数换成对称协调完美值: speed=36 km/h, cycle=100, 间距 1000m × 3,
    满足 间距 = k×cycle×speed_m/s, simulateMaxband 自然算出 offsets = [0,0,0,0],
    双向所有路口绿条/蓝条都完美对齐波带
  - 调用方传 mode='forward'/'reverse'/'balanced' 可切协调方向 (默认 balanced)

  TrafficTimeSpace.vue:
  - yAxisMin 从硬码 -80 改为 -this.cycle, 跟 cycle 联动
  - 修原 cycle=100 时 k=-1 那根蓝红柱(y 从 -100 起)蓝段[-100,-80]被裁掉一半
    的视觉问题; 完整覆盖一个 cycle 的下方 padding
画安 1 月之前
父節點
當前提交
d2df76c3dc
共有 3 個文件被更改,包括 114 次插入22 次删除
  1. 3 2
      src/components/ui/TrafficTimeSpace.vue
  2. 90 0
      src/mock/_simulateMaxband.js
  3. 21 20
      src/mock/api.js

+ 3 - 2
src/components/ui/TrafficTimeSpace.vue

@@ -77,8 +77,9 @@ export default {
       return last.x + 80;
     },
     yAxisMin() {
-      // 留出底部 padding,避免首个路口(distance=0)向下延伸的柱子被 clip 裁掉
-      return -80;
+      // 至少留一个完整 cycle 的下方空间, 避免 k=-1 那根柱(从 -cycle 起)被裁
+      // 原来 -80 是 cycle=100 时的近似值, 蓝条 [-100, -60] 会被裁掉 20s
+      return -this.cycle;
     },
     maxDataTime() {
       return this.viewWindow * 3;

+ 90 - 0
src/mock/_simulateMaxband.js

@@ -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));
+    });
+}

+ 21 - 20
src/mock/api.js

@@ -13,6 +13,7 @@
  */
 
 import mockData from './mock_data.json'
+import { simulateMaxband } from './_simulateMaxband'
 
 // ── 静态资源(模拟 CDN / 后端返回的资源 URL)─────────────────────
 
@@ -807,31 +808,27 @@ export async function apiGetKeyIntersections() {
 
 export async function apiGetTrafficTimeSpace(opts = {}) {
   await delay(300)
-  const { label } = opts
+  const { label, mode = 'balanced' } = opts  // mode: 'forward' | 'reverse' | 'balanced'
   const labels = ['A', 'B', 'C', 'D']
-  const dists = [450, 669, 1050, 0]
   const prefix = label || '交叉口'
 
-  // 正反两向设计车速各自 42~48 km/h 随机(保留一位小数),且两者不相同
-  const randSpeed = () => Math.round((42 + Math.random() * 6) * 10) / 10
-  const speedKmh = randSpeed()
-  let speedKmhBackward = randSpeed()
-  while (speedKmhBackward === speedKmh) {
-    speedKmhBackward = randSpeed()
-  }
-
+  // ==== "对称协调"完美参数 ====
+  // 数学条件: 2 × 总路长 / 速度 ≡ k × cycle (k 为整数), 双向都能完美协调
+  // 这里: speed=36 km/h (10 m/s), 路口间距 1000m, 单程走 100s = 1 个 cycle
+  // 整条路 (3000m) 反向走 300s = 3 个 cycle, 所有路口 offset 都为 0 时双向都对齐绿/蓝条
   const cycle = 100
   const greenDuration = 40
   const bandwidth = 31.5
-  const gStartY = 0  // 与组件保持一致
-  const vMs = speedKmh / 3.6
-
-  // 正向 offset:让 A→D 绿波在每个路口贴着绿灯起点进入;首路口 offset=0 保证 X 轴刻度干净
-  let cumDist = 0
-  const offsets = labels.map((_, i) => {
-    const entryTime = gStartY + cumDist / vMs
-    cumDist += dists[i]
-    return Math.round(((entryTime) % cycle + cycle) % cycle)
+  const speedKmh = 36
+  const speedKmhBackward = 36
+  const dists = [1000, 1000, 1000, 0]
+
+  const offsets = simulateMaxband({
+    distances: dists,
+    cycle,
+    speedFwd: speedKmh,
+    speedRev: speedKmhBackward,
+    mode,
   })
 
   return ok({
@@ -845,7 +842,11 @@ export async function apiGetTrafficTimeSpace(opts = {}) {
     cycle,
     greenDuration,
     bandwidth,
-    scanLineStart: Math.round(Math.random() * 100) / 100
+    scanLineStart: Math.round(Math.random() * 100) / 100,
+    meta: {
+      algorithm: `heuristic_v1_${mode}`,
+      coordinationMode: mode,
+    }
   })
 }