|
|
@@ -62,14 +62,8 @@ import XgVideoPlayer from '@/components/ui/XgVideoPlayer.vue';
|
|
|
import SegmentedRadio from '@/components/ui/SegmentedRadio.vue';
|
|
|
import { apiGetDetectorMonitorData } from '@/api';
|
|
|
|
|
|
-// 检测器锚点 = 原摄像机所在的 world 坐标(armNode 局部 (-80,-190) 经 arm 旋转后)。
|
|
|
-// 图标位于锚点上方,下方一行 [流量/占有率(右对齐) ①]。
|
|
|
-const DETECTOR_POSITIONS = {
|
|
|
- N: { x: 370, y: 100 },
|
|
|
- E: { x: 800, y: 370 },
|
|
|
- S: { x: 530, y: 800 },
|
|
|
- W: { x: 100, y: 530 },
|
|
|
-};
|
|
|
+// 检测器现挂在 armNode 内部,位置使用 arm-local 坐标(与 createArrowIcon 一致),
|
|
|
+// y=-190 对应原摄像机锚点(路口前方约一倍车道处的虚拟检测门架),由 arm 旋转自动定位到四个方向。
|
|
|
|
|
|
// 方向箭头 SVG 路径映射
|
|
|
// 所有方向臂共用同一套图标(以N方向/向下驶入为基准),由 createRoadArm 的 rotation 自动旋转
|
|
|
@@ -377,70 +371,81 @@ export default {
|
|
|
return group;
|
|
|
},
|
|
|
|
|
|
- /**
|
|
|
- * 检测器条目布局(N 数据行在图标上方,其他方向在下方;编号在左、文字两行在右):
|
|
|
- * N 方向: 其他方向:
|
|
|
- * ① 流量:1230 [icon]
|
|
|
- * 占有率:50% ① 流量:1230
|
|
|
- * [icon] 占有率:50%
|
|
|
- */
|
|
|
- createDetectorIcon(num, dir, x, y) {
|
|
|
- const group = new Konva.Group({ x, y });
|
|
|
+ /** 检测器图标(球机造型)。放在进口道中线、徽章列上方;
|
|
|
+ * `armRotation` 用来反向旋转,让所有方向的图标都朝上,与 arm 旋转无关。 */
|
|
|
+ createDetectorIcon(armRotation) {
|
|
|
const stroke = '#7fb6ff';
|
|
|
- const fill = '#3a7fd1';
|
|
|
-
|
|
|
- // 1) 顶部图标:沿用原 球机 造型(圆角矩形 + 内嵌镜头圆 + 底部支架横杆+竖杆)
|
|
|
- const iconG = new Konva.Group({ x: 0, y: 2 });
|
|
|
- iconG.add(new Konva.Rect({
|
|
|
+ const group = new Konva.Group({
|
|
|
+ x: -80, y: -240,
|
|
|
+ rotation: -armRotation,
|
|
|
+ });
|
|
|
+ group.add(new Konva.Rect({
|
|
|
x: -18, y: -32, width: 36, height: 32,
|
|
|
stroke, strokeWidth: 2.5, cornerRadius: 8,
|
|
|
fill: 'rgba(58,127,209,0.2)',
|
|
|
}));
|
|
|
- iconG.add(new Konva.Circle({ x: 0, y: -16, radius: 7, stroke, strokeWidth: 2.5 }));
|
|
|
- iconG.add(new Konva.Line({ points: [-13, 0, 13, 0], stroke, strokeWidth: 2.5, lineCap: 'round' }));
|
|
|
- iconG.add(new Konva.Line({ points: [0, 0, 0, 8], stroke, strokeWidth: 2.5 }));
|
|
|
- group.add(iconG);
|
|
|
-
|
|
|
- // 2) 数据行 Y 偏移:N 在图标上方,其他在下方
|
|
|
- const dataY = dir === 'N' ? -62 : 42;
|
|
|
+ group.add(new Konva.Circle({ x: 0, y: -16, radius: 7, stroke, strokeWidth: 2.5 }));
|
|
|
+ group.add(new Konva.Line({ points: [-13, 0, 13, 0], stroke, strokeWidth: 2.5, lineCap: 'round' }));
|
|
|
+ group.add(new Konva.Line({ points: [0, 0, 0, 8], stroke, strokeWidth: 2.5 }));
|
|
|
+ return group;
|
|
|
+ },
|
|
|
|
|
|
- // 3) 编号圆圈(左侧)
|
|
|
- const badgeCx = -55;
|
|
|
- group.add(new Konva.Circle({ x: badgeCx, y: dataY, radius: 13, fill, stroke, strokeWidth: 1.5 }));
|
|
|
+ /** 检测器编号徽章(小圆 + 数字)。文字反向旋转,保证四个方向都朝上。 */
|
|
|
+ createDetectorBadge(lx, ly, num, armRotation) {
|
|
|
+ const stroke = '#7fb6ff';
|
|
|
+ const fill = '#3a7fd1';
|
|
|
+ const group = new Konva.Group();
|
|
|
+ group.add(new Konva.Circle({ x: lx, y: ly, radius: 13, fill, stroke, strokeWidth: 1.5 }));
|
|
|
group.add(new Konva.Text({
|
|
|
- x: badgeCx - 13, y: dataY - 9, width: 26, align: 'center',
|
|
|
+ x: lx, y: ly,
|
|
|
+ offsetX: 13, offsetY: 9, // 把旋转中心从左上角搬到 (lx, ly)
|
|
|
+ width: 26, align: 'center',
|
|
|
text: String(num), fontSize: 16, fontStyle: 'bold', fill: '#fff',
|
|
|
+ rotation: -armRotation,
|
|
|
}));
|
|
|
-
|
|
|
- // 4) 流量 / 占有率两行(编号右侧 6px 间隔)
|
|
|
- const textX = badgeCx + 13 + 6;
|
|
|
- const flowText = new Konva.Text({
|
|
|
- x: textX, y: dataY - 18, text: '', fontSize: 18, fontStyle: 'bold', fill: '#fff', fontFamily: 'monospace',
|
|
|
- });
|
|
|
- const occText = new Konva.Text({
|
|
|
- x: textX, y: dataY + 2, text: '', fontSize: 18, fontStyle: 'bold', fill: '#fff', fontFamily: 'monospace',
|
|
|
- });
|
|
|
- group.add(flowText, occText);
|
|
|
- group.flowText = flowText;
|
|
|
- group.occText = occText;
|
|
|
return group;
|
|
|
},
|
|
|
|
|
|
- /** (重)创建四个方向的检测器节点;如已存在先 destroy */
|
|
|
+ /** (重)创建四个方向的检测器组:图标 + 每条车道一个编号徽章。
|
|
|
+ * 组挂在 armNode 内,arm 旋转/平移自动定位;图标和数字通过反向旋转保持朝上。 */
|
|
|
renderDetectors() {
|
|
|
const cfg = (this.mapData && this.mapData.armsConfig) || {};
|
|
|
+ const { laneWidth } = this.sizeConfig;
|
|
|
+ const detY = -190; // 与原 cameraNode 锚点一致
|
|
|
+
|
|
|
+ // 全路口连续编号:N→E→S→W 顺时针累加,每条车道一个号;
|
|
|
+ // 每个方向内按司机视角"左→右"(arm-local 最外侧 → 最内侧)编号。
|
|
|
+ let badgeNum = 1;
|
|
|
+
|
|
|
['N', 'E', 'S', 'W'].forEach(dir => {
|
|
|
- if (this.detectorNodes[dir]) {
|
|
|
- this.detectorNodes[dir].destroy();
|
|
|
- this.detectorNodes[dir] = null;
|
|
|
+ const armNode = this.armsNodes[dir];
|
|
|
+ if (!armNode) return;
|
|
|
+
|
|
|
+ // 清理旧检测器组
|
|
|
+ if (armNode.detectorGroup) {
|
|
|
+ armNode.detectorGroup.destroy();
|
|
|
+ armNode.detectorGroup = null;
|
|
|
}
|
|
|
- const det = cfg[dir] && cfg[dir].detector;
|
|
|
- if (!det) return;
|
|
|
- const pos = DETECTOR_POSITIONS[dir];
|
|
|
- const node = this.createDetectorIcon(det.index, dir, pos.x, pos.y);
|
|
|
- node.visible(this.displayMode === 'detector');
|
|
|
- this.layer.add(node);
|
|
|
- this.detectorNodes[dir] = node;
|
|
|
+ this.detectorNodes[dir] = null;
|
|
|
+
|
|
|
+ const armData = cfg[dir];
|
|
|
+ if (!armData) return;
|
|
|
+ const lanes = armData.lanes || [];
|
|
|
+ const armRotation = armNode.rotation() || 0;
|
|
|
+
|
|
|
+ const detGroup = new Konva.Group();
|
|
|
+ // 检测器图标(一方向一个,徽章列上方)
|
|
|
+ detGroup.add(this.createDetectorIcon(armRotation));
|
|
|
+ // 车道徽章
|
|
|
+ for (let index = lanes.length - 1; index >= 0; index--) {
|
|
|
+ const lx = -20 - index * laneWidth;
|
|
|
+ detGroup.add(this.createDetectorBadge(lx, detY, badgeNum++, armRotation));
|
|
|
+ }
|
|
|
+
|
|
|
+ detGroup.visible(this.displayMode === 'detector');
|
|
|
+ armNode.add(detGroup);
|
|
|
+ armNode.detectorGroup = detGroup;
|
|
|
+ this.detectorNodes[dir] = detGroup;
|
|
|
});
|
|
|
this.syncDetectorBase();
|
|
|
this.updateDetectorTexts();
|
|
|
@@ -455,14 +460,15 @@ export default {
|
|
|
});
|
|
|
},
|
|
|
|
|
|
- /** 用 detectorBase 当前值刷一次显示文本 */
|
|
|
+ /** 用 detectorBase 当前值刷一次显示文本(当前画布版未绘制流量/占有率文字,
|
|
|
+ * 保留方法以便未来恢复显示;polling 仍在跑,detectorBase 持续更新供 DetectorTable 等下游消费) */
|
|
|
updateDetectorTexts() {
|
|
|
['N', 'E', 'S', 'W'].forEach(dir => {
|
|
|
const base = this.detectorBase[dir];
|
|
|
const node = this.detectorNodes[dir];
|
|
|
if (!base || !node) return;
|
|
|
- node.flowText.text(`流量:${base.flow}`);
|
|
|
- node.occText.text(`占有率:${base.occupancy}%`);
|
|
|
+ if (node.flowText) node.flowText.text(`流量:${base.flow}`);
|
|
|
+ if (node.occText) node.occText.text(`占有率:${base.occupancy}%`);
|
|
|
});
|
|
|
if (this.layer) this.layer.batchDraw();
|
|
|
},
|
|
|
@@ -524,18 +530,41 @@ export default {
|
|
|
return '';
|
|
|
},
|
|
|
|
|
|
- /** 检测器模式下打开弹窗:DetectorTable 居中展示「检测器运行数据监视」 */
|
|
|
+ /** 检测器模式下打开弹窗:默认贴到本路口详情面板右侧(控制方式区域)覆盖显示。
|
|
|
+ * 无法定位时回退到居中默认尺寸。SmartDialog 用 1920 设计宽度做缩放,所以这里把
|
|
|
+ * 实际像素 rect 换算回设计坐标传过去。 */
|
|
|
openDetectorDialog() {
|
|
|
if (!this.dialogManager || typeof this.dialogManager.openDialog !== 'function') return;
|
|
|
- this.dialogManager.openDialog({
|
|
|
+
|
|
|
+ const cfg = {
|
|
|
id: `detector-monitor-${this._uid}`,
|
|
|
title: '检测器运行数据监视',
|
|
|
component: 'DetectorTable',
|
|
|
- width: 620,
|
|
|
- height: 360,
|
|
|
noPadding: false,
|
|
|
data: { intersectionId: this.getIntersectionId() },
|
|
|
- });
|
|
|
+ };
|
|
|
+
|
|
|
+ const DESIGN_WIDTH = 1920;
|
|
|
+ const scale = window.innerWidth / DESIGN_WIDTH;
|
|
|
+ const root = this.$el && this.$el.closest && this.$el.closest('.crossing-detail-panel');
|
|
|
+ const rightPanel = root && root.querySelector(':scope > .detail-panel-right');
|
|
|
+
|
|
|
+ if (rightPanel && scale > 0) {
|
|
|
+ const rect = rightPanel.getBoundingClientRect();
|
|
|
+ if (rect.width > 0 && rect.height > 0) {
|
|
|
+ cfg.center = false;
|
|
|
+ cfg.position = { x: rect.left / scale, y: rect.top / scale };
|
|
|
+ cfg.width = Math.round(rect.width / scale);
|
|
|
+ cfg.height = Math.round(rect.height / scale);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 兜底:找不到右侧面板(独立使用场景)则居中 620×360
|
|
|
+ if (cfg.width == null) {
|
|
|
+ cfg.width = 620;
|
|
|
+ cfg.height = 360;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.dialogManager.openDialog(cfg);
|
|
|
},
|
|
|
|
|
|
closeDetectorDialog() {
|