|
|
@@ -56,6 +56,56 @@
|
|
|
import Konva from 'konva';
|
|
|
import XgVideoPlayer from '@/components/ui/XgVideoPlayer.vue';
|
|
|
|
|
|
+// 方向箭头 SVG 路径映射
|
|
|
+// 所有方向臂共用同一套图标(以N方向/向下驶入为基准),由 createRoadArm 的 rotation 自动旋转
|
|
|
+// R(右转)用左转图标水平翻转
|
|
|
+const arrowSvgMap = {
|
|
|
+ S: require('@/assets/images/svg/icon_straight_down.svg'),
|
|
|
+ L: require('@/assets/images/svg/icon_turn_down_left.svg'),
|
|
|
+ U: require('@/assets/images/svg/icon_turn_down_left_uturn.svg'),
|
|
|
+ R: require('@/assets/images/svg/icon_turn_down_left.svg'), // 右转用左转图标,渲染时水平翻转
|
|
|
+};
|
|
|
+
|
|
|
+// SVG 原始文本映射(内联,避免 webpack loader 问题)
|
|
|
+const svgRawCache = {};
|
|
|
+
|
|
|
+// 首次加载时通过 Image → Canvas 获取不到 SVG 内容,所以直接用内联方式
|
|
|
+// 从 require 得到的 URL(可能是 base64 或路径)加载图片
|
|
|
+const imgCache = {};
|
|
|
+
|
|
|
+function loadSvgImage(svgUrl, fillColor) {
|
|
|
+ const cacheKey = svgUrl + '|' + fillColor;
|
|
|
+ if (imgCache[cacheKey]) return Promise.resolve(imgCache[cacheKey]);
|
|
|
+
|
|
|
+ // 如果是 base64 data URL,解码后替换颜色
|
|
|
+ if (svgUrl.startsWith('data:image/svg+xml;base64,')) {
|
|
|
+ const base64 = svgUrl.split(',')[1];
|
|
|
+ let svgText = atob(base64);
|
|
|
+ svgText = svgText.replace(/fill="#[0-9a-fA-F]{3,8}"/g, `fill="${fillColor}"`);
|
|
|
+ const encoded = btoa(svgText);
|
|
|
+ const dataUrl = 'data:image/svg+xml;base64,' + encoded;
|
|
|
+ return new Promise(resolve => {
|
|
|
+ const img = new Image();
|
|
|
+ img.onload = () => { imgCache[cacheKey] = img; resolve(img); };
|
|
|
+ img.src = dataUrl;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 普通 URL,fetch 后替换颜色
|
|
|
+ return fetch(svgUrl)
|
|
|
+ .then(r => r.text())
|
|
|
+ .then(svgText => {
|
|
|
+ const colored = svgText.replace(/fill="#[0-9a-fA-F]{3,8}"/g, `fill="${fillColor}"`);
|
|
|
+ const blob = new Blob([colored], { type: 'image/svg+xml' });
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ return new Promise(resolve => {
|
|
|
+ const img = new Image();
|
|
|
+ img.onload = () => { imgCache[cacheKey] = img; resolve(img); };
|
|
|
+ img.src = url;
|
|
|
+ });
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
export default {
|
|
|
name: 'IntersectionMapVideos',
|
|
|
components: {
|
|
|
@@ -246,14 +296,32 @@ export default {
|
|
|
},
|
|
|
|
|
|
createArrowIcon(type, x, y, color = this.C.WHITE) {
|
|
|
- const group = new Konva.Group({ x, y, scaleX: 0.65, scaleY: 0.65 });
|
|
|
- group.add(new Konva.Circle({ x: 0, y: -35, radius: 3, fill: color, name: 'colorFill' }));
|
|
|
- let pathData = '';
|
|
|
- if (type === 'S') pathData = 'M 0 -35 L 0 0 M -7 -10 L 0 0 L 7 -10';
|
|
|
- else if (type === 'L') pathData = 'M 0 -35 L 0 -15 Q 0 0 15 0 M 5 -7 L 15 0 L 5 7';
|
|
|
- else if (type === 'R') pathData = 'M 0 -35 L 0 -15 Q 0 0 -15 0 M -5 -7 L -15 0 L -5 7';
|
|
|
- else if (type === 'U') pathData = 'M 0 -35 L 0 -15 Q 0 0 14 0 Q 28 0 28 -15 L 28 -25 M 21 -18 L 28 -25 L 35 -18';
|
|
|
- group.add(new Konva.Path({ data: pathData, stroke: color, strokeWidth: 3, lineCap: 'round', lineJoin: 'round', name: 'colorStroke' }));
|
|
|
+ const maxH = 40; // 最大高度
|
|
|
+ const group = new Konva.Group({ x, y });
|
|
|
+ if (type === 'R') group.scaleX(-1);
|
|
|
+ group._arrowMeta = { type };
|
|
|
+
|
|
|
+ const svgUrl = arrowSvgMap[type];
|
|
|
+ if (svgUrl) {
|
|
|
+ loadSvgImage(svgUrl, color).then(imgObj => {
|
|
|
+ // 按原始比例等比缩放,高度不超过 maxH
|
|
|
+ const natW = imgObj.naturalWidth || imgObj.width;
|
|
|
+ const natH = imgObj.naturalHeight || imgObj.height;
|
|
|
+ const scale = Math.min(maxH / natH, 1);
|
|
|
+ const w = Math.round(natW * scale);
|
|
|
+ const h = Math.round(natH * scale);
|
|
|
+ const konvaImg = new Konva.Image({
|
|
|
+ image: imgObj,
|
|
|
+ x: -w / 2,
|
|
|
+ y: -h - 5,
|
|
|
+ width: w,
|
|
|
+ height: h,
|
|
|
+ name: 'arrowImg'
|
|
|
+ });
|
|
|
+ group.add(konvaImg);
|
|
|
+ if (this.layer) this.layer.draw();
|
|
|
+ });
|
|
|
+ }
|
|
|
return group;
|
|
|
},
|
|
|
|
|
|
@@ -306,29 +374,63 @@ export default {
|
|
|
updateDynamicSignals() {
|
|
|
const signals = this.mapData.signals;
|
|
|
if (!signals) return;
|
|
|
+ const config = this.mapData.armsConfig || {};
|
|
|
|
|
|
const nsColor = signals.ns.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
|
|
|
const ewColor = signals.ew.isGreen ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
|
|
|
-
|
|
|
- const dyeArm = (armNode, color) => {
|
|
|
- armNode.lightGroup.getChildren().forEach(r => r.fill(color));
|
|
|
- const arrowColor = (color === this.C.SIGNAL_GREEN) ? this.C.SIGNAL_RED : this.C.SIGNAL_GREEN;
|
|
|
- Object.values(armNode.arrowNodes).forEach(arr => {
|
|
|
- if (arr) {
|
|
|
- arr.findOne('.colorFill').fill(arrowColor);
|
|
|
- arr.findOne('.colorStroke').stroke(arrowColor);
|
|
|
- }
|
|
|
+ const nsActiveTypes = signals.ns.activeArrowTypes || [];
|
|
|
+ const ewActiveTypes = signals.ew.activeArrowTypes || [];
|
|
|
+
|
|
|
+ const dyeArm = (dir, armNode, signalColor, activeTypes) => {
|
|
|
+ // 灯带颜色
|
|
|
+ armNode.lightGroup.getChildren().forEach(r => r.fill(signalColor));
|
|
|
+ // 箭头按 lane type 用不同颜色的 SVG 替换
|
|
|
+ const lanes = (config[dir] && config[dir].lanes) || [];
|
|
|
+ Object.keys(armNode.arrowNodes).forEach(index => {
|
|
|
+ const arr = armNode.arrowNodes[index];
|
|
|
+ if (!arr) return;
|
|
|
+ const laneType = lanes[index];
|
|
|
+ const isActive = activeTypes.length > 0 && activeTypes.includes(laneType);
|
|
|
+ const targetColor = isActive ? this.C.SIGNAL_GREEN : this.C.SIGNAL_RED;
|
|
|
+
|
|
|
+ const meta = arr._arrowMeta;
|
|
|
+ if (!meta) return;
|
|
|
+ const svgUrl = arrowSvgMap[meta.type];
|
|
|
+ if (!svgUrl) return;
|
|
|
+
|
|
|
+ loadSvgImage(svgUrl, targetColor).then(imgObj => {
|
|
|
+ const existing = arr.findOne('.arrowImg');
|
|
|
+ if (existing) {
|
|
|
+ existing.image(imgObj);
|
|
|
+ } else {
|
|
|
+ const maxH = 40;
|
|
|
+ const natW = imgObj.naturalWidth || imgObj.width;
|
|
|
+ const natH = imgObj.naturalHeight || imgObj.height;
|
|
|
+ const scale = Math.min(maxH / natH, 1);
|
|
|
+ const w = Math.round(natW * scale);
|
|
|
+ const h = Math.round(natH * scale);
|
|
|
+ arr.add(new Konva.Image({
|
|
|
+ image: imgObj,
|
|
|
+ x: -w / 2,
|
|
|
+ y: -h - 5,
|
|
|
+ width: w,
|
|
|
+ height: h,
|
|
|
+ name: 'arrowImg'
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ if (this.layer) this.layer.draw();
|
|
|
+ });
|
|
|
});
|
|
|
};
|
|
|
|
|
|
- dyeArm(this.armsNodes.N, nsColor);
|
|
|
- dyeArm(this.armsNodes.S, nsColor);
|
|
|
- dyeArm(this.armsNodes.E, ewColor);
|
|
|
- dyeArm(this.armsNodes.W, ewColor);
|
|
|
+ dyeArm('N', this.armsNodes.N, nsColor, nsActiveTypes);
|
|
|
+ dyeArm('S', this.armsNodes.S, nsColor, nsActiveTypes);
|
|
|
+ dyeArm('E', this.armsNodes.E, ewColor, ewActiveTypes);
|
|
|
+ dyeArm('W', this.armsNodes.W, ewColor, ewActiveTypes);
|
|
|
|
|
|
this.panelNodes.nsLabel.text(`${signals.ns.phaseName}:`);
|
|
|
this.panelNodes.nsVal.text(signals.ns.time.toString().padStart(2, '0')).fill(nsColor);
|
|
|
-
|
|
|
+
|
|
|
this.panelNodes.ewLabel.text(`${signals.ew.phaseName}:`);
|
|
|
this.panelNodes.ewVal.text(signals.ew.time.toString().padStart(2, '0')).fill(ewColor);
|
|
|
|