|
@@ -141,40 +141,68 @@ function _camerasToArmTypes(cameras) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 生成检测器模拟数据:四方向各 1 个,编号 1-4 顺时针(N=1, E=2, S=3, W=4)。
|
|
|
|
|
- * flow / occupancy 给基础值;客户端在检测器模式下会基于此值做 ±15% / ±5 的随机走步。
|
|
|
|
|
|
|
+ * 检测器单一数据源:4 方向各 1 条(与画布 ① ② ③ ④ 一一对应)。
|
|
|
|
|
+ * 编号 1=北、2=东、3=南、4=西。
|
|
|
|
|
+ * bucketIdx(5s 桶)决定确定性噪声,同桶内多次调用返回相同值,让画布与表格读数同步。
|
|
|
*/
|
|
*/
|
|
|
-function _makeDetectors(id, name, seed) {
|
|
|
|
|
|
|
+function _detectorBucketSnapshot(id, bucketIdx, name) {
|
|
|
|
|
+ const seed = _idSeed(id || '')
|
|
|
const dirs = [
|
|
const dirs = [
|
|
|
- { dir: 'N', label: '北进口', index: 1 },
|
|
|
|
|
- { dir: 'E', label: '东进口', index: 2 },
|
|
|
|
|
- { dir: 'S', label: '南进口', index: 3 },
|
|
|
|
|
- { dir: 'W', label: '西进口', index: 4 },
|
|
|
|
|
|
|
+ { dir: 'N', label: '北', index: 1 },
|
|
|
|
|
+ { dir: 'E', label: '东', index: 2 },
|
|
|
|
|
+ { dir: 'S', label: '南', index: 3 },
|
|
|
|
|
+ { dir: 'W', label: '西', index: 4 },
|
|
|
]
|
|
]
|
|
|
- return dirs.map(p => ({
|
|
|
|
|
- intersection: name || id,
|
|
|
|
|
- intersectionId: id,
|
|
|
|
|
- detectorId: `DT${(id || '000').slice(-6)}_${String(p.index).padStart(2, '0')}`,
|
|
|
|
|
- index: p.index,
|
|
|
|
|
- position: p.label,
|
|
|
|
|
- direction: p.dir,
|
|
|
|
|
- flow: 600 + ((seed * (p.index + 7)) % 1500), // 600~2100
|
|
|
|
|
- occupancy: 15 + ((seed * (p.index + 3)) % 60), // 15~75 (%)
|
|
|
|
|
- enabled: true,
|
|
|
|
|
- }))
|
|
|
|
|
|
|
+ return dirs.map(d => {
|
|
|
|
|
+ const i = d.index
|
|
|
|
|
+ const baseFlow = 60 + ((seed * (i * 7 + 13)) % 160) // 60~220
|
|
|
|
|
+ const baseOcc = 15 + ((seed * (i * 11 + 17)) % 50) // 15~65
|
|
|
|
|
+ // 桶内确定性噪声(无 Math.random,保证同 bucketIdx 多次调用结果一致)
|
|
|
|
|
+ const flowNoise01 = (((seed ^ (i * 257) ^ (bucketIdx * 9176)) >>> 0) % 10000) / 10000
|
|
|
|
|
+ const occNoise01 = (((seed ^ (i * 521) ^ (bucketIdx * 4093)) >>> 0) % 10000) / 10000
|
|
|
|
|
+ const flow = Math.max(0, Math.round(baseFlow * (1 + (flowNoise01 - 0.5) * 0.3))) // ±15%
|
|
|
|
|
+ const occupancy = Math.max(0, Math.min(100, Math.round(baseOcc + (occNoise01 - 0.5) * 10))) // ±5
|
|
|
|
|
+ return {
|
|
|
|
|
+ intersection: name || id,
|
|
|
|
|
+ intersectionId: id,
|
|
|
|
|
+ detectorId: `DT${(id || '000').slice(-6)}_${String(i).padStart(2, '0')}`,
|
|
|
|
|
+ index: i,
|
|
|
|
|
+ position: `${d.label}进口`,
|
|
|
|
|
+ direction: d.dir,
|
|
|
|
|
+ name: `${d.label}进口`,
|
|
|
|
|
+ flow,
|
|
|
|
|
+ occupancy,
|
|
|
|
|
+ enabled: true,
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 从检测器列表推导各方向 { index, flow, occupancy } */
|
|
|
|
|
-function _detectorsToArmConfig(detectors) {
|
|
|
|
|
|
|
+/** 4 条 lane → 4 方向 armsDetector(一一对应,无需挑选) */
|
|
|
|
|
+function _bucketArmsDetector(lanes) {
|
|
|
const out = { N: null, S: null, E: null, W: null }
|
|
const out = { N: null, S: null, E: null, W: null }
|
|
|
- detectors.forEach(d => {
|
|
|
|
|
- if (d.direction && !out[d.direction]) {
|
|
|
|
|
- out[d.direction] = { index: d.index, detectorId: d.detectorId, flow: d.flow, occupancy: d.occupancy }
|
|
|
|
|
|
|
+ for (const lane of lanes) {
|
|
|
|
|
+ if (!out[lane.direction]) {
|
|
|
|
|
+ out[lane.direction] = {
|
|
|
|
|
+ index: lane.index,
|
|
|
|
|
+ detectorId: lane.detectorId,
|
|
|
|
|
+ flow: lane.flow,
|
|
|
|
|
+ occupancy: lane.occupancy,
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- })
|
|
|
|
|
|
|
+ }
|
|
|
return out
|
|
return out
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 保留旧入口名,让调用方零修改(_makeIntersectionConfig 等仍能用)
|
|
|
|
|
+function _makeDetectors(id, name /* seed 不再使用:bucket 自己用 _idSeed */) {
|
|
|
|
|
+ const bucketIdx = Math.floor(Date.now() / 5000)
|
|
|
|
|
+ return _detectorBucketSnapshot(id, bucketIdx, name)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function _detectorsToArmConfig(detectors) {
|
|
|
|
|
+ return _bucketArmsDetector(detectors)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function _makeIntersectionConfig(id, name, { fixedNsGreen, iconMode = 'default' } = {}) {
|
|
function _makeIntersectionConfig(id, name, { fixedNsGreen, iconMode = 'default' } = {}) {
|
|
|
const phases = ['南北直行', '东西直行', '北单放', '东单放']
|
|
const phases = ['南北直行', '东西直行', '北单放', '东单放']
|
|
|
const nsGreen = fixedNsGreen !== undefined ? fixedNsGreen : false
|
|
const nsGreen = fixedNsGreen !== undefined ? fixedNsGreen : false
|
|
@@ -1094,6 +1122,30 @@ export async function apiGetCrossingDetailData(id, { iconMode = 'default' } = {}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
|
|
+ * GET /api/detector/monitor/:id — 检测器运行数据监视
|
|
|
|
|
+ * 返回单一数据源的两种视图:
|
|
|
|
|
+ * - armsDetector: { N/E/S/W → {index, flow, occupancy} } 给画布 ① ② ③ ④ 用
|
|
|
|
|
+ * - tableData: 8 条 lane-level 数据 给弹窗表格用
|
|
|
|
|
+ * 同 5s 桶内重复调用返回相同值(确定性噪声),保证画布和弹窗轮询在同一窗口内值一致。
|
|
|
|
|
+ */
|
|
|
|
|
+export async function apiGetDetectorMonitorData(id) {
|
|
|
|
|
+ await delay(150)
|
|
|
|
|
+ const bucketIdx = Math.floor(Date.now() / 5000)
|
|
|
|
|
+ const lanes = _detectorBucketSnapshot(id, bucketIdx)
|
|
|
|
|
+ const armsDetector = _bucketArmsDetector(lanes)
|
|
|
|
|
+ return ok({
|
|
|
|
|
+ timeTicks: [180, 150, 120, 90, 60, 30],
|
|
|
|
|
+ tableData: lanes.map(l => ({
|
|
|
|
|
+ id: l.index,
|
|
|
|
|
+ name: l.name,
|
|
|
|
|
+ flow: l.flow,
|
|
|
|
|
+ occupancy: `${l.occupancy}%`,
|
|
|
|
|
+ })),
|
|
|
|
|
+ armsDetector,
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
* GET /api/crossing/top-charts — 路口Tab顶部圆环图(动态波动)
|
|
* GET /api/crossing/top-charts — 路口Tab顶部圆环图(动态波动)
|
|
|
*/
|
|
*/
|
|
|
export async function apiGetCrossingTopCharts() {
|
|
export async function apiGetCrossingTopCharts() {
|