canvas绘制红绿灯路口(二)
系列文章
canvas绘制红绿灯路口(一)
无图不欢,先上图

优化项:
一:加入人行道红绿信号
二:加入专用车道标识(无方向标识时采用专用车道标识)
三:东南西北四项路口优化绘制逻辑,美化图像
四:加入拖拽、缩放图例
使用方法(以vue3为例)
<template><canvas class="lane" ref="laneCanvas" />
</template><script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import Lane from 'services/roadCanvas/lane';const laneCanvas = ref(null);
/*** 车道方向,进口方向* 1 - 北,2 - 东北,3 - 东,4 - 东南,* 5 - 南,6 - 西南,7 - 西,8 - 西北** 直行放行 nThrough 0不放行 1放行* 左转放行 nTurnLeft 0不放行 1放行* 右转放行 nTurnRight 0不放行 1放行* 调头 nTurnAround 0不放行 1放行** 通道相位 nChannelNumberPhase 1-红灯 2绿灯 3黄灯*/const data = [{'approachDirection': 1,'cdireCtion': '北','lanes': [{'laneNo': '1','through': 0,'turnLeft': 1,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '左转','channelNumberPhase': '2','trafficLightColor': '#33CC00'},{'laneNo': '2','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '3','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '4','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '5','through': 0,'turnLeft': 0,'turnRight': 1,'turnAround': 0,'directionIdentifyings': '右转','channelNumberPhase': 0,'trafficLightColor': '#ccc'}],'peoples': [{'laneNo': '0','lfd': '0(人行)','channelNumberPhase': '1'},{'laneNo': '99','lfd': '99(人行)','channelNumberPhase': '1'}]},{'approachDirection': 3,'cdireCtion': '东','lanes': [{'laneNo': '1','through': 0,'turnLeft': 1,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '左转','channelNumberPhase': 0,'trafficLightColor': '#ccc'},{'laneNo': '2','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '3','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '4','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '5','through': 0,'turnLeft': 0,'turnRight': 1,'turnAround': 0,'directionIdentifyings': '右转','channelNumberPhase': 0,'trafficLightColor': '#ccc'}],'peoples': [{'laneNo': '0','lfd': '0(人行)','channelNumberPhase': '2'},{'laneNo': '99','lfd': '99(人行)','channelNumberPhase': '1'}]},{'approachDirection': 5,'cdireCtion': '南','lanes': [{'laneNo': '1','through': 0,'turnLeft': 1,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '左转','channelNumberPhase': '2','trafficLightColor': '#33CC00'},{'laneNo': '2','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '3','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '4','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '5','through': 0,'turnLeft': 0,'turnRight': 1,'turnAround': 0,'directionIdentifyings': '右转','channelNumberPhase': 0,'trafficLightColor': '#ccc'}],'peoples': [{'laneNo': '0','lfd': '0(人行)','channelNumberPhase': '1'},{'laneNo': '99','lfd': '99(人行)','channelNumberPhase': '1'}]},{'approachDirection': 7,'cdireCtion': '西','lanes': [{'laneNo': '1','through': 0,'turnLeft': 1,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '左转','channelNumberPhase': 0,'trafficLightColor': '#ccc'},{'laneNo': '2','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '3','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '4','through': 1,'turnLeft': 0,'turnRight': 0,'turnAround': 0,'directionIdentifyings': '直行','channelNumberPhase': '1','trafficLightColor': '#FF0033'},{'laneNo': '5','through': 0,'turnLeft': 0,'turnRight': 1,'turnAround': 0,'directionIdentifyings': '右转','channelNumberPhase': 0,'trafficLightColor': '#ccc'}],'peoples': [{'laneNo': '0','lfd': '0(人行)','channelNumberPhase': '2'},{'laneNo': '99','lfd': '99(人行)','channelNumberPhase': '1'}]}
];
let laneC = null;onMounted(() => {laneC = new Lane({canvas: laneCanvas.value,data});// 如红绿数据更新可采用setData方法刷新红绿状态// laneC.setData(data)
});onUnmounted(() => {laneC?.destroy();laneC = null;
});</script><style scoped lang="scss">
.lane {width: 100%;height: 100%;background-color: #325e76;
}
</style>
lane.js封装如下
import { getDirectionIdentifyings, computePosition } from './baseDI';class Lane {constructor(opt) {this.dpr = window.devicePixelRatio || 1;this.canvas = opt.canvas;this.w = null;this.h = null;this.ctx = null;this.data = opt.data;// 车道范围坐标this.region = [];// 车道线坐标this.dataXY = [];// 路中心空白区域占canvas宽高最小值的比,用来计算车道宽度。占比越大,中心空白区域越大,车道越宽,线路越短。取值范围0-1,不允许取0,1。this.laneCenterProportion = 'auto' || opt.laneCenterProportion; // ex: 0.8// 车道样式this.laneStyle = opt.laneStyle;// 缩放this.scaleFlag = false;this.mouseScaleSpeed = 5; // 缩放速度this.scaleIndex = 100; // 初始缩放系数this.normalScaleIndex = 100; // 标准缩放系数this.minScaleIndex = 50; // 最小缩放系数this.scaleC = this.scaleIndex / this.normalScaleIndex; // 缩放比例// 平移this.translate = {x: 0,y: 0};// 异步任务listthis.taskList = [];this.hasTaskDone = false;this.status = 'do'; // do or stopthis.init();}init() {if (!this.canvas) {return;}if (this.canvas.width !== Math.floor(this.canvas.offsetWidth * this.dpr) || this.canvas.height !== Math.floor(this.canvas.offsetHeight * this.dpr)) {// eslint-disable-next-linethis.w = this.canvas.width = Math.floor(this.canvas.offsetWidth * this.dpr);// eslint-disable-next-linethis.h = this.canvas.height = Math.floor(this.canvas.offsetHeight * this.dpr);} else {this.w = this.canvas.width;this.h = this.canvas.height;}this.ctx = this.canvas.getContext('2d');this.getLaneStyle();this.formatDataXY();this.getRegion();this.draw();this.addEvent();this.addAnimationFrame();}// 获取车道样式getLaneStyle() {const laneStyle = {// 车道范围region: {width: 2 * this.dpr,color: '#fff',type: 'solid',CurveType: 'quadratic', // normal: 插值曲线, quadratic: 二次贝塞尔, arc: 圆弧线。arc有问题,请勿使用background: '#1f2748'},// 车道左侧车道线innerLeft: {width: 1 * this.dpr,color: '#999',type: [10 * this.dpr, 10 * this.dpr],},// 车道右侧车道线innerRight: {width: 1 * this.dpr,color: '#eee',type: [10 * this.dpr, 10 * this.dpr],},// 车道分割线innerDivider: {width: 2 * this.dpr,color: '#f0bf0a',type: 'solid'},// 车道标识direction: {widthProportion: 0.1, // 占车道比例,建议小于0.2HeightWidthProportion: 10, // 高宽比,建议大于5maxWidth: 20 * this.dpr,arrowWidth: 2, // 箭头/方向线的比例, 建议大于1小于2background: '#ddd'},// 斑马线zebraCrossing: {widthProportion: 0.05, // 单个斑马线宽占车道比例,建议小于0.2widthHeightProportion: 0.2, // 单个斑马线宽高比,建议小于0.5color: '#ddd'},// 红绿灯trafficLight: {rProportion: 0.3, // 单个红绿灯半径占车道比例,建议小于0.5,colors: ['#fff', '#FF0033', '#33CC00', '#FFFF33'],}};if (this.laneStyle) {this.laneStyle = Object.assign(laneStyle, this.laneStyle);} else {this.laneStyle = laneStyle;}const laneMaxNum = this.getLaneMaxNum();const sideLength = this.getSideLength();// 车道宽度 / 2 表示双向this.laneStyle.width = sideLength / 2 / laneMaxNum;// 方向表示线宽高this.laneStyle.direction.width = this.laneStyle.width * this.laneStyle.direction.widthProportion;if (this.laneStyle.direction.width > this.laneStyle.direction.maxWidth) {this.laneStyle.direction.width = this.laneStyle.direction.maxWidth;}this.laneStyle.direction.height = this.laneStyle.direction.width * this.laneStyle.direction.HeightWidthProportion;// 斑马线宽高this.laneStyle.zebraCrossing.width = this.laneStyle.width * this.laneStyle.zebraCrossing.widthProportion;this.laneStyle.zebraCrossing.height = this.laneStyle.zebraCrossing.width / this.laneStyle.zebraCrossing.widthHeightProportion;this.laneStyle.zebraCrossing.type = [this.laneStyle.zebraCrossing.width * 4, this.laneStyle.zebraCrossing.width];// 红绿灯半径this.laneStyle.trafficLight.r = this.laneStyle.width * this.laneStyle.trafficLight.rProportion;}// 获取最大车道数getLaneMaxNum() {let laneMaxNum = 0;this.data.forEach(item => {if (item.lanes.length > laneMaxNum) {laneMaxNum = item.lanes.length;}});if (laneMaxNum === 1) {laneMaxNum = 2;}return laneMaxNum;}// 获取中心路口(四边形/八边形)边长getSideLength() {const minW = this.w > this.h ? this.h : this.w;let legitimate = true;let maxLans = 0;const cdireCtions = ['东', '南', '西', '北'];for (let i = 0; i < this.data.length; i++) {if (cdireCtions.indexOf(this.data[i].cdireCtion) === -1) {legitimate = false;}if (this.data[i].lanes.length > maxLans) {maxLans = this.data[i].lanes.length;}}if (this.laneCenterProportion === 'auto') {this.laneCenterProportion = maxLans / 10 > 0.8 ? 0.8 : maxLans / 10;}if (legitimate) {return minW * this.laneCenterProportion / 1.1;}return minW * this.laneCenterProportion / (Math.sqrt(2) + 1);}// 计算车道坐标formatDataXY() {const dataXY = [];// 车道起始中心位置const centerX = this.w / 2;const centerY = this.h - this.h * (1 - this.laneCenterProportion) / 2;// 车道长度const laneLength = Math.sqrt(this.w ** 2 * this.h ** 2);this.data.forEach(dataItem => {const dataXYItem = {approachDirection: dataItem.approachDirection,};// 起始xconst startX = centerX - this.laneStyle.width * dataItem.lanes.length;// 起始yconst startY = centerY + this.laneStyle.zebraCrossing.height * 2;// 结束Yconst endY = startY + laneLength;// 线const lines = [];// 单向车道分割线数量const innerLines = dataItem.lanes.length - 1;// 车道左边线lines.push({x0: startX,y0: startY - this.laneStyle.zebraCrossing.height * 2,x1: startX,y1: endY,type: 'outer'});// 车道左侧分割线for (let i = 0; i < innerLines; i++) {const x = startX + (i + 1) * this.laneStyle.width;lines.push({x0: x,y0: startY,x1: x,y1: endY,style: { ...this.laneStyle.innerLeft }});}// 左右车道分割线const dividerX = startX + (innerLines + 1) * this.laneStyle.width;lines.push({x0: dividerX,y0: startY,x1: dividerX,y1: endY,style: { ...this.laneStyle.innerDivider }});// 车道右侧分割线for (let i = 0; i < innerLines; i++) {const x = startX + (innerLines + i + 2) * this.laneStyle.width;lines.push({x0: x,y0: startY,x1: x,y1: endY,style: { ...this.laneStyle.innerRight }});}// 车道右边线const outerRightx = startX + (innerLines + 1) * 2 * this.laneStyle.width;lines.push({x0: outerRightx,y0: startY - this.laneStyle.zebraCrossing.height * 2,x1: outerRightx,y1: endY,type: 'outer'});dataXYItem.lines = lines;// 方向标识const directionIdentifyings = [];for (let i = 0; i < dataItem.lanes.length; i++) {const laneItem = dataItem.lanes[i];const key = [laneItem.through, laneItem.turnLeft, laneItem.turnRight, laneItem.turnAround].join('');const line = lines[innerLines + i + 1];directionIdentifyings.push(getDirectionIdentifyings(key, this.laneStyle.direction.width, this.laneStyle.direction.height, {x: line.x0 + this.laneStyle.width / 2,y: line.y0 + this.laneStyle.direction.height / 2 + this.laneStyle.trafficLight.r * 4}, this.laneStyle.direction.arrowWidth));}dataXYItem.directionIdentifyings = directionIdentifyings;// 斑马线if (dataItem.peoples.length === 1) {dataXYItem.zebraCrossing = [{x0: lines[0].x0 + this.laneStyle.zebraCrossing.width * 4,y0: startY - this.laneStyle.zebraCrossing.height,x1: lines[lines.length - 1].x0 - this.laneStyle.zebraCrossing.width * 4,y1: startY - this.laneStyle.zebraCrossing.height,color: this.laneStyle.trafficLight.colors[dataItem.peoples[0].channelNumberPhase]}];} else if (dataItem.peoples.length === 2) {dataXYItem.zebraCrossing = [{x0: lines[0].x0 + this.laneStyle.zebraCrossing.width * 4,y0: startY - this.laneStyle.zebraCrossing.height,x1: lines[(lines.length - 1) / 2].x0,y1: startY - this.laneStyle.zebraCrossing.height,color: this.laneStyle.trafficLight.colors[dataItem.peoples[0].channelNumberPhase]}, {x0: lines[(lines.length - 1) / 2].x0,y0: startY - this.laneStyle.zebraCrossing.height,x1: lines[lines.length - 1].x0 - this.laneStyle.zebraCrossing.width * 4,y1: startY - this.laneStyle.zebraCrossing.height,color: this.laneStyle.trafficLight.colors[dataItem.peoples[1].channelNumberPhase]}];} else {dataXYItem.zebraCrossing = [{x0: lines[0].x0 + this.laneStyle.zebraCrossing.width * 4,y0: startY - this.laneStyle.zebraCrossing.height,x1: lines[lines.length - 1].x0 - this.laneStyle.zebraCrossing.width * 4,y1: startY - this.laneStyle.zebraCrossing.height,color: this.laneStyle.trafficLight.colors[0]}];}// 红绿灯const trafficLights = [];for (let i = 0; i < dataItem.lanes.length; i++) {const laneItem = dataItem.lanes[i];const line = lines[innerLines + i + 1];trafficLights.push({x: line.x0 + this.laneStyle.width / 2,y: line.y0 + this.laneStyle.trafficLight.r * 2,r: this.laneStyle.trafficLight.r,color: this.laneStyle.trafficLight.colors[laneItem.channelNumberPhase]});}dataXYItem.trafficLights = trafficLights;dataXY.push(dataXYItem);});this.dataXYByRotate(dataXY);this.dataXY = dataXY;}// 计算旋转坐标dataXYByRotate(dataXY) {const centerX = this.w / 2;const centerY = this.h / 2;dataXY.forEach(dataXYItem => {// 八边形,一个边占45度const rotateReg = -180 + (dataXYItem.approachDirection - 1) * 45;dataXYItem.lines.forEach(line => {const xy0 = computePosition(line.x0, line.y0, rotateReg, centerX, centerY);line.x0 = xy0.x;line.y0 = xy0.y;const xy1 = computePosition(line.x1, line.y1, rotateReg, centerX, centerY);line.x1 = xy1.x;line.y1 = xy1.y;});dataXYItem.directionIdentifyings.forEach(directionIdentifying => {directionIdentifying.points.forEach(point => {point.forEach(item => {const { x, y } = computePosition(item.x, item.y, rotateReg, centerX, centerY);item.x = x;item.y = y;});});directionIdentifying.arrowPoints.forEach(arrowPoint => {arrowPoint.forEach(item => {const { x, y } = computePosition(item.x, item.y, rotateReg, centerX, centerY);item.x = x;item.y = y;});});});dataXYItem.zebraCrossing.forEach(zebraCrossing => {const xy0 = computePosition(zebraCrossing.x0, zebraCrossing.y0, rotateReg, centerX, centerY);zebraCrossing.x0 = xy0.x;zebraCrossing.y0 = xy0.y;const xy1 = computePosition(zebraCrossing.x1, zebraCrossing.y1, rotateReg, centerX, centerY);zebraCrossing.x1 = xy1.x;zebraCrossing.y1 = xy1.y;});dataXYItem.trafficLights.forEach(trafficLight => {const { x, y } = computePosition(trafficLight.x, trafficLight.y, rotateReg, centerX, centerY);trafficLight.x = x;trafficLight.y = y;});});}// 获取车道范围getRegion() {const region = [];for (let i = 0; i < this.dataXY.length; i++) {const dataXYItem = this.dataXY[i];const linesLength = dataXYItem.lines.length;if (i !== 0) {// 衔接上一车道const prevDataXYItem = this.dataXY[i - 1];const data = {prevapproachDirection: prevDataXYItem.approachDirection,approachDirection: dataXYItem.approachDirection,type: 'connect'};let diffapproachDirection = dataXYItem.approachDirection - prevDataXYItem.approachDirection;if (diffapproachDirection > 4) {diffapproachDirection = (prevDataXYItem.approachDirection + 8) - dataXYItem.approachDirection;}if (diffapproachDirection === 4) {// 车道正对,直线即可data.point = [{ x: prevDataXYItem.lines[0].x0, y: prevDataXYItem.lines[0].y0 },{ x: dataXYItem.lines[linesLength - 1].x0, y: dataXYItem.lines[linesLength - 1].y0 },];region.push(data);} else {if (this.laneStyle.region.CurveType === 'arc') {const angle = 45 * diffapproachDirection;const startAngle = 45 * (prevDataXYItem.approachDirection - 5);data.OR = this.findCircleCenter(prevDataXYItem.lines[0].x0,prevDataXYItem.lines[0].y0,dataXYItem.lines[linesLength - 1].x0,dataXYItem.lines[linesLength - 1].y0,angle,true);data.OR.startAngle = startAngle;data.OR.endAngle = startAngle - angle;data.OR.anticlockwise = true;} else {// 曲线const laneXY0 = this.calculateIntersection([[prevDataXYItem.lines[0].x0, prevDataXYItem.lines[0].y0],[prevDataXYItem.lines[0].x1, prevDataXYItem.lines[0].y1],], [[dataXYItem.lines[linesLength - 1].x0, dataXYItem.lines[linesLength - 1].y0],[dataXYItem.lines[linesLength - 1].x1, dataXYItem.lines[linesLength - 1].y1],]);const laneXY1 = [(prevDataXYItem.lines[0].x0 + dataXYItem.lines[linesLength - 1].x0) / 2, (prevDataXYItem.lines[0].y0 + dataXYItem.lines[linesLength - 1].y0) / 2];const originPoints = [{ x: prevDataXYItem.lines[0].x0, y: prevDataXYItem.lines[0].y0 },{ x: laneXY1[0] - (laneXY1[0] - laneXY0[0]) * diffapproachDirection / 4, y: laneXY1[1] - (laneXY1[1] - laneXY0[1]) * diffapproachDirection / 4 },{ x: dataXYItem.lines[linesLength - 1].x0, y: dataXYItem.lines[linesLength - 1].y0 },];if (this.laneStyle.region.CurveType === 'normal') {const point = this.getCurveVertex(originPoints);data.point = point;} else {data.point = originPoints;}}region.push(data);}}// 车道范围region.push({approachDirection: dataXYItem.approachDirection,x0: dataXYItem.lines[linesLength - 1].x0,y0: dataXYItem.lines[linesLength - 1].y0,x1: dataXYItem.lines[linesLength - 1].x1,y1: dataXYItem.lines[linesLength - 1].y1,x2: dataXYItem.lines[0].x1,y2: dataXYItem.lines[0].y1,x3: dataXYItem.lines[0].x0,y3: dataXYItem.lines[0].y0,type: 'lane'});if (i === this.dataXY.length - 1) {// 衔接起始车道const startDataXYItem = this.dataXY[0];const startLinesLength = startDataXYItem.lines.length;const data = {startapproachDirection: startDataXYItem.approachDirection,approachDirection: dataXYItem.approachDirection,type: 'connect'};let diffapproachDirection = startDataXYItem.approachDirection + 8 - dataXYItem.approachDirection;if (diffapproachDirection > 4) {diffapproachDirection = dataXYItem.approachDirection - startDataXYItem.approachDirection;}if (diffapproachDirection === 4) {// 车道正对,直线即可data.point = [{ x: dataXYItem.lines[0].x0, y: dataXYItem.lines[0].y0 },{ x: startDataXYItem.lines[startLinesLength - 1].x0, y: startDataXYItem.lines[startLinesLength - 1].y0 },];region.push(data);} else {if (this.laneStyle.region.CurveType === 'arc') {const angle = 45 * diffapproachDirection;const startAngle = 45 * (dataXYItem.approachDirection - 1);data.OR = this.findCircleCenter(dataXYItem.lines[0].x0,dataXYItem.lines[0].y0,startDataXYItem.lines[linesLength - 1].x0,startDataXYItem.lines[linesLength - 1].y0,angle,true);data.OR.endAngle = startAngle + angle;data.OR.startAngle = startAngle;data.OR.anticlockwise = false;} else {// 曲线const laneXY0 = this.calculateIntersection([[dataXYItem.lines[0].x0, dataXYItem.lines[0].y0],[dataXYItem.lines[0].x1, dataXYItem.lines[0].y1],], [[startDataXYItem.lines[startLinesLength - 1].x0, startDataXYItem.lines[startLinesLength - 1].y0],[startDataXYItem.lines[startLinesLength - 1].x1, startDataXYItem.lines[startLinesLength - 1].y1],]);const laneXY1 = [(startDataXYItem.lines[startLinesLength - 1].x0 + dataXYItem.lines[0].x0) / 2, (startDataXYItem.lines[startLinesLength - 1].y0 + dataXYItem.lines[0].y0) / 2];const originPoints = [{ x: dataXYItem.lines[0].x0, y: dataXYItem.lines[0].y0 },{ x: laneXY1[0] - (laneXY1[0] - laneXY0[0]) * diffapproachDirection / 4, y: laneXY1[1] - (laneXY1[1] - laneXY0[1]) * diffapproachDirection / 4 },{ x: startDataXYItem.lines[startLinesLength - 1].x0, y: startDataXYItem.lines[startLinesLength - 1].y0 },];if (this.laneStyle.region.CurveType === 'normal') {const point = this.getCurveVertex(originPoints);data.point = point;} else {data.point = originPoints;}}region.push(data);}}}this.region = region;}// 获取两条直线的交点calculateIntersection(line1, line2) {// 解方程组const x1 = line1[0][0];const y1 = line1[0][1];const x2 = line1[1][0];const y2 = line1[1][1];const x3 = line2[0][0];const y3 = line2[0][1];const x4 = line2[1][0];const y4 = line2[1][1];const denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);if (denominator === 0) {// 直线平行,没有交点return null;}const intersectionX = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denominator;const intersectionY = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denominator;return [intersectionX, intersectionY];}// 以下四个方法获取曲线getCurveVertex(vertex, pointsPow = 0.4) {let length = 0;for (let i = 0; i < vertex.length - 1; i++) {length += Math.sqrt((vertex[i].x - vertex[i + 1].x) ** 2 + (vertex[i].y - vertex[i + 1].y) ** 2);}length = Math.ceil(length);return this.getNewData(vertex, length * pointsPow);}// 曲线 插值getNewData(pointsOrigin, pointsPow) {const points = [];const divisions = (pointsOrigin.length - 1) * pointsPow;for (let i = 0; i < divisions; i++) {points.push(this.getPoint(i, divisions, pointsOrigin, pointsPow));}return points;}getPoint(i, divisions, pointsOrigin, pointsPow) {const isRealI = (i * divisions) % pointsPow;const p = ((pointsOrigin.length - 1) * i) / divisions;const intPoint = Math.floor(p);const weight = p - intPoint;const p0 = pointsOrigin[intPoint === 0 ? intPoint : intPoint - 1];const p1 = pointsOrigin[intPoint];const p2 = pointsOrigin[intPoint > pointsOrigin.length - 2 ? pointsOrigin.length - 1 : intPoint + 1];const p3 = pointsOrigin[intPoint > pointsOrigin.length - 3 ? pointsOrigin.length - 1 : intPoint + 2];return {isReal: isRealI === 0,x: this.catmullRom(weight, p0.x, p1.x, p2.x, p3.x),y: this.catmullRom(weight, p0.y, p1.y, p2.y, p3.y)};}catmullRom(t, p0, p1, p2, p3) {const v0 = (p2 - p0) * 0.5;const v1 = (p3 - p1) * 0.5;const t2 = t * t;const t3 = t * t2;return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;}// 根据圆上两点以及夹角角度 求 圆心findCircleCenter(x1, y1, x2, y2, theta, isNeg) {let cx = 0;let cy = 0;const dDistance = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));const dRadius = dDistance * 0.5 / Math.sin(Math.PI / 180 * theta * 0.5);if (dDistance === 0.0) {// cout << "\n输入了相同的点!\n";return false;}if ((2 * dRadius) < dDistance) {// cout << "\n两点间距离大于直径!\n";return false;}let k_verticle = 0.0;let mid_x = 0.0;let mid_y = 0.0;let a = 1.0;let b = 1.0;let c = 1.0;const k = (y2 - y1) / (x2 - x1);let cx1; let cy1; let cx2; letcy2;if (k === 0) {cx1 = (x1 + x2) / 2.0;cx2 = (x1 + x2) / 2.0;cy1 = y1 + Math.sqrt(dRadius * dRadius - (x1 - x2) * (x1 - x2) / 4.0);cy2 = y2 - Math.sqrt(dRadius * dRadius - (x1 - x2) * (x1 - x2) / 4.0);} else {k_verticle = -1.0 / k;mid_x = (x1 + x2) / 2.0;mid_y = (y1 + y2) / 2.0;a = 1.0 + k_verticle * k_verticle;b = -2 * mid_x - k_verticle * k_verticle * (x1 + x2);c = mid_x * mid_x + k_verticle * k_verticle * (x1 + x2) * (x1 + x2) / 4.0- (dRadius * dRadius - ((mid_x - x1) * (mid_x - x1) + (mid_y - y1) * (mid_y - y1)));cx1 = (-1.0 * b + Math.sqrt(b * b - 4 * a * c)) / (2 * a);cx2 = (-1.0 * b - Math.sqrt(b * b - 4 * a * c)) / (2 * a);cy1 = this.y_Coordinates(mid_x, mid_y, k_verticle, cx1);cy2 = this.y_Coordinates(mid_x, mid_y, k_verticle, cx2);}// cx2,cy2为顺时针圆心坐标,cx1,cy1为逆时针圆心坐标if (isNeg) {cx = cx1;cy = cy1;} else {cx = cx2;cy = cy2;}return { x: cx, y: cy, r: Math.sqrt((cx - x1) ** 2 + (cy - y1) ** 2) };}y_Coordinates(x, y, k, x0) {return k * x0 - k * x + y;}// 设置新的红绿灯数据setData(data) {this.dataXY.forEach((dataXYItem, dataXYIndex) => {dataXYItem.trafficLights.forEach((trafficLight, trafficLightIndex) => {if (data[dataXYIndex]?.lanes[trafficLightIndex]) {trafficLight.color = this.laneStyle.trafficLight.colors[data[dataXYIndex].lanes[trafficLightIndex].channelNumberPhase];}});dataXYItem.zebraCrossing.forEach((zebra, zebraIndex) => {if (data[dataXYIndex]?.peoples[zebraIndex]) {zebra.color = this.laneStyle.trafficLight.colors[data[dataXYIndex].peoples[zebraIndex].channelNumberPhase];}});});this.addTask();}// 重新绘制reDraw() {this.ctx.clearRect(0, 0, this.w, this.h);this.draw();}// 绘制draw() {this.drawRegion();this.drawLines();this.drawDirectionIdentifyings();this.drawZebraCrossing();this.drawTrafficLight();// this.drawHelper()}// 缩放、平移translateAndScale() {// 缩放this.ctx.translate(this.w / 2, this.h / 2);this.ctx.scale(this.scaleC, this.scaleC);this.ctx.translate(-this.w / 2, -this.h / 2);// 平移this.ctx.translate(this.translate.x, this.translate.y);}// 绘制车道范围drawRegion() {this.ctx.save();this.translateAndScale();this.ctx.beginPath();this.ctx.fillStyle = this.laneStyle.region.background;this.ctx.lineWidth = this.laneStyle.region.width;this.ctx.strokeStyle = this.laneStyle.region.color;this.ctx.lineJoin = 'round';for (let i = 0; i < this.region.length; i++) {const regionItem = this.region[i];if (regionItem.type === 'connect') {if (regionItem?.point?.length === 2 && this.laneStyle.region.CurveType !== 'arc') {// 直线regionItem.point.forEach(item => {this.ctx.lineTo(item.x, item.y);});} else if (this.laneStyle.region.CurveType === 'arc') {// 圆if (regionItem.OR) this.ctx.arc(regionItem.OR.x, regionItem.OR.y, regionItem.OR.r, regionItem.OR.startAngle * Math.PI / 180, regionItem.OR.endAngle * Math.PI / 180, regionItem.OR.anticlockwise);} else if (this.laneStyle.region.CurveType === 'normal') {// 插值regionItem.point.forEach(item => {this.ctx.lineTo(item.x, item.y);});} else {// 二次贝塞尔this.ctx.lineTo(regionItem.point[0].x, regionItem.point[0].y);this.ctx.quadraticCurveTo(regionItem.point[1].x, regionItem.point[1].y, regionItem.point[2].x, regionItem.point[2].y);}} else {this.ctx.lineTo(regionItem.x0, regionItem.y0);this.ctx.lineTo(regionItem.x1, regionItem.y1);this.ctx.lineTo(regionItem.x2, regionItem.y2);this.ctx.lineTo(regionItem.x3, regionItem.y3);}}this.ctx.fill();this.ctx.stroke();this.ctx.closePath();this.ctx.restore();}// 绘制车道线drawLines() {this.dataXY.forEach((dataXYItem) => {dataXYItem.lines.forEach(lineItem => {if (lineItem.type !== 'outer') {this.ctx.save();this.translateAndScale();this.ctx.beginPath();this.ctx.lineWidth = lineItem.style.width;this.ctx.strokeStyle = lineItem.style.color;if (lineItem.style.type !== 'solid') {this.ctx.setLineDash(lineItem.style.type);}this.ctx.lineTo(lineItem.x0, lineItem.y0);this.ctx.lineTo(lineItem.x1, lineItem.y1);this.ctx.stroke();this.ctx.closePath();this.ctx.restore();}});});}// 绘制方向箭头drawDirectionIdentifyings() {this.dataXY.forEach((dataXYItem) => {dataXYItem.directionIdentifyings.forEach(directionIdentifying => {this.ctx.save();this.translateAndScale();directionIdentifying.points.forEach(pointItem => {this.ctx.beginPath();this.ctx.lineWidth = directionIdentifying.w;this.ctx.strokeStyle = this.laneStyle.direction.background;if (directionIdentifying.exclusive) {this.ctx.setLineDash([directionIdentifying.w, directionIdentifying.w]);}pointItem.forEach(item => {this.ctx.lineTo(item.x, item.y);});this.ctx.stroke();this.ctx.closePath();this.ctx.setLineDash([]);});directionIdentifying.arrowPoints.forEach(arrowPoint => {this.ctx.beginPath();this.ctx.fillStyle = this.laneStyle.direction.background;arrowPoint.forEach(item => {this.ctx.lineTo(item.x, item.y);});this.ctx.fill();this.ctx.closePath();});this.ctx.restore();});});}// 绘制信号灯drawTrafficLight() {this.dataXY.forEach((dataXYItem) => {dataXYItem.trafficLights.forEach(trafficLight => {this.ctx.save();this.translateAndScale();this.ctx.beginPath();this.ctx.fillStyle = trafficLight.color;this.ctx.arc(trafficLight.x, trafficLight.y, trafficLight.r, 0, Math.PI * 2);this.ctx.fill();this.ctx.closePath();this.ctx.restore();});});}// 绘制斑马线drawZebraCrossing() {this.dataXY.forEach((dataXYItem) => {this.ctx.save();this.translateAndScale();this.ctx.beginPath();this.ctx.lineWidth = this.laneStyle.zebraCrossing.height;dataXYItem.zebraCrossing.forEach(zebraCrossing => {this.ctx.strokeStyle = zebraCrossing.color;this.ctx.lineTo(zebraCrossing.x0, zebraCrossing.y0);this.ctx.lineTo(zebraCrossing.x1, zebraCrossing.y1);});this.ctx.stroke();this.ctx.closePath();this.ctx.beginPath();this.ctx.lineWidth = this.laneStyle.zebraCrossing.height + 1;this.ctx.strokeStyle = this.laneStyle.region.background;this.ctx.setLineDash(this.laneStyle.zebraCrossing.type);dataXYItem.zebraCrossing.forEach(zebraCrossing => {this.ctx.lineTo(zebraCrossing.x0, zebraCrossing.y0);this.ctx.lineTo(zebraCrossing.x1, zebraCrossing.y1);});this.ctx.stroke();this.ctx.closePath();this.ctx.restore();});}drawHelper() {// 绘制车道方向数字,用来查看车道是否正确this.ctx.beginPath();this.ctx.fillStyle = '#fff';this.ctx.font = 20 * this.dpr + 'px Arial';for (let i = 0; i < this.region.length; i++) {const regionItem = this.region[i];this.ctx.fillText(regionItem.approachDirection, regionItem.x0, regionItem.y0);}this.ctx.closePath();// 绘制坐标线this.ctx.save();this.ctx.lineWidth = 2 * this.dpr;this.ctx.strokeStyle = 'red';this.ctx.beginPath();this.ctx.lineTo(this.w / 2, 0);this.ctx.lineTo(this.w / 2, this.h);this.ctx.stroke();this.ctx.closePath();this.ctx.beginPath();this.ctx.lineTo(0, this.h / 2);this.ctx.lineTo(this.w, this.h / 2);this.ctx.stroke();this.ctx.closePath();this.ctx.restore();}// 事件相关addEvent() {// 缩放this.mousewheelBind = this.mousewheel.bind(this);this.canvas.addEventListener('mousewheel', this.mousewheelBind);// 平移this.canvasMousedownBind = this.canvasMousedown.bind(this);this.documentMouseupBind = this.documentMouseup.bind(this);this.documentMouseMoveBind = this.documentMouseMove.bind(this);this.canvas.addEventListener('mousedown', this.canvasMousedownBind);document.addEventListener('mousemove', this.documentMouseMoveBind);}removeEvent() {if (this.mousewheelBind) {this.canvas.removeEventListener('mousewheel', this.mousewheelBind);}if (this.canvasMousedownBind) {this.canvas.removeEventListener('mousedown', this.canvasMousedownBind);}if (this.documentMouseMoveBind) {document.removeEventListener('mousemove', this.documentMouseMoveBind);}if (this.documentMouseupBind) {document.removeEventListener('mouseup', this.documentMouseupBind);}}mousewheel(e) {if (this.scaleFlag) {return;}this.scaleFlag = true;if (e.wheelDelta > 0) {this.scaleIndex += this.mouseScaleSpeed;} else if (this.scaleIndex > this.minScaleIndex) {this.scaleIndex -= this.mouseScaleSpeed;}this.scaleC = this.scaleIndex / this.normalScaleIndex;// canvas缩放操作this.addTask();this.scaleFlag = false;}canvasMousedown(e) {this.mousedownXY = {x: e.clientX,y: e.clientY};document.addEventListener('mouseup', this.documentMouseupBind);e.preventDefault();}documentMouseMove(e) {if (this.moveFlag) {return;}if (!this.mousedownXY) {return;}// 长按移动this.moveFlag = true;const E = {x: e.clientX,y: e.clientY};this.translate.x += ((E.x - this.mousedownXY.x) * this.dpr) / this.scaleC;this.translate.y += ((E.y - this.mousedownXY.y) * this.dpr) / this.scaleC;// canvas拖拽操作this.addTask();this.mousedownXY = {x: e.clientX,y: e.clientY};this.moveFlag = false;}documentMouseup() {document.removeEventListener('mouseup', this.documentMouseupBind);this.mousedownXY = null;}// 异步处理重绘机制addAnimationFrame() {this.requestAnimationFrame = null;this.requestAnimationFrameDrawBind = this.requestAnimationFrameDraw.bind(this);}removeAnimationFrame() {this.stop();this.clearRequestAnimationFrame();}addTask(func = () => {}) {this.taskList.push(func);if (this.requestAnimationFrame === null) {this.addRequestAnimationFrame();this.do();}}do() {this.status = 'do';new Promise(res => {if (this.taskList[0]) {this.taskList[0]();this.taskList.shift();}this.hasTaskDone = true;res();}).then(() => {if (this.status === 'do' && this.taskList.length) {this.do();}});}stop() {this.status = 'stop';}requestAnimationFrameDraw() {this.stop();if (this.hasTaskDone && this.reDraw) {this.hasTaskDone = false;this.reDraw();}if (this.taskList.length) {this.addRequestAnimationFrame();this.do();} else {this.clearRequestAnimationFrame();}}addRequestAnimationFrame() {this.requestAnimationFrame = window.requestAnimationFrame(this.requestAnimationFrameDrawBind);}clearRequestAnimationFrame() {window.cancelAnimationFrame(this.requestAnimationFrame);this.requestAnimationFrame = null;}// 销毁destroy() {this.removeEvent();this.removeAnimationFrame();}
}export default Lane;
baseDI.js封装如下
const getType = val => {return Object.prototype.toString.call(val).replace(/\[object (\w+)\]/, '$1');
};// 旋转计算
const computePosition = (x, y, angle, centerX, centerY) => {// 圆心const a = centerX;const b = centerY;// 计算const c = Math.PI / 180 * angle;const rx = (x - a) * Math.cos(c) - (y - b) * Math.sin(c) + a;const ry = (y - b) * Math.cos(c) + (x - a) * Math.sin(c) + b;return { x: rx, y: ry };
};// 获取方向标识坐标
const getArrow = (key, w, h, w2, h2, topY, bottomY, leftX, rightX, cX, cY) => {let point = [];const wd = (w - w2) / 2; // 三角形边长与线宽的差值的一半const rotateDeg = 30;// 左转右转旋转角度const hv = h2 / Math.cos(Math.PI / 180 * rotateDeg); // 计算左转右转虚拟线长const topYv = topY - (hv - h2) / 2; // 虚拟起始高度switch (key) {case 1:// 调头point = [{ x: leftX - wd, y: bottomY - h },{ x: leftX + w2 / 2, y: bottomY },{ x: leftX + w2 + wd, y: bottomY - h }];break;case 2:// 左转point = [{ x: cX + w / 2, y: topYv + h },{ x: cX, y: topYv },{ x: cX - w / 2, y: topYv + h }];point.forEach(item => {const newXY = computePosition(item.x, item.y, -rotateDeg, cX, cY);item.x = newXY.x;item.y = newXY.y;});break;case 3:// 直行point = [{ x: cX + w / 2, y: topY + h },{ x: cX, y: topY },{ x: cX - w / 2, y: topY + h }];break;case 4:// 右转point = [{ x: cX + w / 2, y: topYv + h },{ x: cX, y: topYv },{ x: cX - w / 2, y: topYv + h }];point.forEach(item => {const newXY = computePosition(item.x, item.y, rotateDeg, cX, cY);item.x = newXY.x;item.y = newXY.y;});break;default:break;}return point;
};
const getDirectionIdentifyings = (key, w, h, centerXY, arrowWidth) => {// 标识边界const topY = centerXY.y - h / 2;const bottomY = centerXY.y + h / 2;let leftX = centerXY.x - w / 2 * 3;let rightX = centerXY.x + w / 2 * 3;// 直行线中心位置let cX = centerXY.x + w;const cY = centerXY.y;// 箭头宽高const arrowW = w * arrowWidth;const arrowH = arrowW * Math.sin(Math.PI / 3);// 线坐标const points = [];// 三角形坐标const arrowPoints = [];// 专用车道(没有方向标识的车道)let exclusive = false;switch (key) {case '0001':// 调头arrowPoints.push(getArrow(1, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: leftX + w / 2, y: bottomY - arrowH },{ x: leftX + w / 2, y: topY + w / 2 },{ x: leftX + w / 2 * 5, y: topY + w / 2 },{ x: leftX + w / 2 * 5, y: bottomY },]);break;case '0100':// 左转arrowPoints.push(getArrow(2, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[0][0].x + arrowPoints[0][2].x) / 2, y: (arrowPoints[0][0].y + arrowPoints[0][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;case '1000':// 直行leftX = centerXY.x - w / 2;rightX = centerXY.x + w / 2;cX = centerXY.x;arrowPoints.push(getArrow(3, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: cX, y: topY + arrowH },{ x: cX, y: bottomY }]);break;case '0010':// 右转cX = centerXY.x - w;arrowPoints.push(getArrow(4, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[0][0].x + arrowPoints[0][2].x) / 2, y: (arrowPoints[0][0].y + arrowPoints[0][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;case '0101':// 左转调头arrowPoints.push(getArrow(1, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: leftX + w / 2, y: bottomY - arrowH },{ x: leftX + w / 2, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: bottomY },]);arrowPoints.push(getArrow(2, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[1][0].x + arrowPoints[1][2].x) / 2, y: (arrowPoints[1][0].y + arrowPoints[1][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;case '1001':// 直行调头arrowPoints.push(getArrow(1, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: leftX + w / 2, y: bottomY - arrowH },{ x: leftX + w / 2, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: bottomY },]);arrowPoints.push(getArrow(3, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: cX, y: topY + arrowH },{ x: cX, y: bottomY }]);break;case '0011':// 右转调头leftX = centerXY.x - w / 2 * 5;rightX = centerXY.x + w / 2 * 5;arrowPoints.push(getArrow(1, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: leftX + w / 2, y: bottomY - arrowH },{ x: leftX + w / 2, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: bottomY },]);cX = centerXY.x;arrowPoints.push(getArrow(4, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[1][0].x + arrowPoints[1][2].x) / 2, y: (arrowPoints[1][0].y + arrowPoints[1][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;case '1100':// 直行左转arrowPoints.push(getArrow(2, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[0][0].x + arrowPoints[0][2].x) / 2, y: (arrowPoints[0][0].y + arrowPoints[0][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(3, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: cX, y: topY + arrowH },{ x: cX, y: bottomY }]);break;case '0110':// 左转右转leftX = centerXY.x - w / 2 * 5;rightX = centerXY.x + w / 2 * 5;cX = centerXY.x;arrowPoints.push(getArrow(2, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[0][0].x + arrowPoints[0][2].x) / 2, y: (arrowPoints[0][0].y + arrowPoints[0][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(4, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[1][0].x + arrowPoints[1][2].x) / 2, y: (arrowPoints[1][0].y + arrowPoints[1][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;case '1010':// 直行右转cX = centerXY.x - w;arrowPoints.push(getArrow(3, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: cX, y: topY + arrowH },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(4, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[1][0].x + arrowPoints[1][2].x) / 2, y: (arrowPoints[1][0].y + arrowPoints[1][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;case '1101':// 直行左转调头arrowPoints.push(getArrow(1, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: leftX + w / 2, y: bottomY - arrowH },{ x: leftX + w / 2, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: bottomY },]);arrowPoints.push(getArrow(2, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[1][0].x + arrowPoints[1][2].x) / 2, y: (arrowPoints[1][0].y + arrowPoints[1][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(3, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: cX, y: topY + arrowH },{ x: cX, y: bottomY }]);break;case '1011':// 直行右转调头leftX = centerXY.x - w / 2 * 5;rightX = centerXY.x + w / 2 * 5;cX = centerXY.x;arrowPoints.push(getArrow(1, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: leftX + w / 2, y: bottomY - arrowH },{ x: leftX + w / 2, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: bottomY },]);arrowPoints.push(getArrow(3, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: cX, y: topY + arrowH },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(4, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[2][0].x + arrowPoints[2][2].x) / 2, y: (arrowPoints[2][0].y + arrowPoints[2][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;case '0111':// 左转右转调头leftX = centerXY.x - w / 2 * 5;rightX = centerXY.x + w / 2 * 5;cX = centerXY.x;arrowPoints.push(getArrow(1, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: leftX + w / 2, y: bottomY - arrowH },{ x: leftX + w / 2, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: bottomY },]);arrowPoints.push(getArrow(2, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[1][0].x + arrowPoints[1][2].x) / 2, y: (arrowPoints[1][0].y + arrowPoints[1][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(4, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[2][0].x + arrowPoints[2][2].x) / 2, y: (arrowPoints[2][0].y + arrowPoints[2][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;case '1110':// 直行左转右转leftX = centerXY.x - w / 2 * 5;rightX = centerXY.x + w / 2 * 5;cX = centerXY.x;arrowPoints.push(getArrow(2, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[0][0].x + arrowPoints[0][2].x) / 2, y: (arrowPoints[0][0].y + arrowPoints[0][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(3, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: cX, y: topY + arrowH },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(4, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[2][0].x + arrowPoints[2][2].x) / 2, y: (arrowPoints[2][0].y + arrowPoints[2][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;case '1111':// 直行左转右转调头leftX = centerXY.x - w / 2 * 5;rightX = centerXY.x + w / 2 * 5;cX = centerXY.x;arrowPoints.push(getArrow(1, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: leftX + w / 2, y: bottomY - arrowH },{ x: leftX + w / 2, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: cY + w / 2 },{ x: leftX + w / 2 * 5, y: bottomY },]);arrowPoints.push(getArrow(2, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[1][0].x + arrowPoints[1][2].x) / 2, y: (arrowPoints[1][0].y + arrowPoints[1][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(3, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: cX, y: topY + arrowH },{ x: cX, y: bottomY }]);arrowPoints.push(getArrow(4, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: (arrowPoints[3][0].x + arrowPoints[3][2].x) / 2, y: (arrowPoints[3][0].y + arrowPoints[3][2].y) / 2 },{ x: cX, y: cY },{ x: cX, y: bottomY }]);break;default:// 专用车道,采用直行坐标exclusive = true;leftX = centerXY.x - w / 2;rightX = centerXY.x + w / 2;cX = centerXY.x;arrowPoints.push(getArrow(3, arrowW, arrowH, w, h, topY, bottomY, leftX, rightX, cX, cY));points.push([{ x: cX, y: topY + arrowH },{ x: cX, y: bottomY }]);break;}return { arrowPoints, points, w, exclusive };
};
const getDirectionLabel = (key) => {let label = '';switch (key) {case '0001':// 调头label = '调头';break;case '0100':// 左转label = '左转';break;case '1000':// 直行label = '直行';break;case '0010':// 右转label = '右转';break;case '0101':// 左转调头label = '左转调头';break;case '1001':// 直行调头label = '直行调头';break;case '0011':// 右转调头label = '右转调头';break;case '1100':// 直行左转label = '直行左转';break;case '0110':// 左转右转label = '左转右转';break;case '1010':// 直行右转label = '直行右转';break;case '1101':// 直行左转调头label = '直行左转调头';break;case '1011':// 直行右转调头label = '直行右转调头';break;case '0111':// 左转右转调头label = '左转右转调头';break;case '1110':// 直行左转右转label = '直行左转右转';break;case '1111':// 直行左转右转调头label = '直行左转右转调头';break;default:// 专用车道,采用直行坐标label = '专用车道';break;}return label;
};// 获取人行标识坐标
const getWalkIdentifyings = (w, h, boundaryW, centerXY) => {// 标识边界const topY = centerXY.y - h / 2;const bottomY = centerXY.y + h / 2;const leftX = centerXY.x - w / 2;const rightX = centerXY.x + w / 2;const boundaryW2 = boundaryW / 2;const boundaryLine = [[{ x: leftX + boundaryW2, y: topY },{ x: leftX + boundaryW2, y: bottomY },],[{ x: rightX - boundaryW2, y: topY },{ x: rightX - boundaryW2, y: bottomY },],[{ x: leftX, y: centerXY.y },{ x: rightX, y: centerXY.y },]];const walkLine = [{ x: leftX, y: centerXY.y - h / 4 },{ x: rightX, y: centerXY.y - h / 4 },];return { boundaryLine, walkLine, w: boundaryW };
};// 转换margin/padding
const convertMorP = (val, dpr) => {let MorP = [];const type = getType(val);if (type === 'Number') {MorP = [val * dpr, val * dpr, val * dpr, val * dpr];} else if (type === 'Array') {switch (val.length) {case 1:MorP = [val[0] * dpr, val[0] * dpr, val[0] * dpr, val[0] * dpr];break;case 2:MorP = [val[0] * dpr, val[1] * dpr, val[0] * dpr, val[1] * dpr];break;case 3:MorP = [val[0] * dpr, val[1] * dpr, val[2] * dpr, val[1] * dpr];break;case 4:MorP = val;break;default:MorP = [0, 0, 0, 0];break;}} else {MorP = [0, 0, 0, 0];}return MorP;
};export { getDirectionIdentifyings, getDirectionLabel, computePosition, getWalkIdentifyings, convertMorP };相关文章:
canvas绘制红绿灯路口(二)
系列文章 canvas绘制红绿灯路口(一) 无图不欢,先上图 优化项: 一:加入人行道红绿信号 二:加入专用车道标识(无方向标识时采用专用车道标识) 三:东南西北四项路口优化绘…...
Semantic Kernel 直接调用本地大模型与阿里云灵积 DashScope
本文主要介绍如何在无需网关,无需配置 HttpClient 的情况下,使用 Semantic Kernel 直接调用本地大模型与阿里云灵积 DashScope 等 OpenAI 接口兼容的大模型服务。 1. 背景 一直以来,我们都在探索如何更好地利用大型语言模型(LLM&…...
【人工智能】深度解读 ChatGPT基本原理
ChatGPT是OpenAI开发的一种基于人工智能技术的自然语言处理工具,它代表了自然语言处理(NLP)技术的前沿进展。ChatGPT的基本原理建立在一系列先进技术和方法之上,主要包括GPT(Generative Pre-trained Transformer&#…...
【教程】2024年如何快速提取爆款视频的视频文案?
关于如何提取爆款视频的视频文案,很朋友都不是很清楚,今天小编就带大家了解一下,希望这个知识点对大家有所帮助。 剪辑工作者有剪映、arctime、视频字幕等,但唯独编辑工作者或者编导没用直接提取视频文案的工具今天就说说可直接在…...
【MySQL连接器(Python)指南】02-MySQL连接器(Python)版本与实现
文章目录 前言MySQL连接器(Python)版本MySQL连接器(Python)实现总结前言 MySQL连接器(Python),用于让Python程序能够访问MySQL数据库。要想让Python应用程序正确高效地使用MySQL数据,就需要深入了解MySQL连接器的特性和使用方法。 MySQL连接器(Python)版本 下表总结了可用的…...
Vim入门教程
Vim是一个高度可配置的文本编辑器,用于创建和修改各种类型的文本文件。以下是一些基本的Vim使用示例,展示如何在Vim中进行编辑和操作。 1. 打开和保存文件 打开一个名为example.txt的文件: vim example.txt 打开多个文件,使用大…...
机器学习课程复习——隐马尔可夫
不考计算题 Q:概率图有几种结构? 条件独立性的公式? 顺序结构发散结构汇总结构Q:隐马尔可夫模型理解? 概念 集合:状态集合、观测集合 序列:状态序列、观测序列...
大数据-数据分析初步学习,待补充
参考视频:数据分析只需3小时从入门到进阶(up亲身实践)_哔哩哔哩_bilibili 数据指标: 对当前业务有参考价值的统计数据 分类:用户数据,业务数据,行为数据 用户数据 存量: DAU&#…...
微服务为什么使用RPC而不使用HTTP通信
微服务架构中使用RPC(Remote Procedure Call)而不是HTTP通信,主要是因为RPC在某些方面相比HTTP具有显著的优势。以下是一些关键原因: 性能: RPC通常比HTTP性能更高。RPC协议可以使用二进制序列化格式(如gRP…...
怪物猎人物语什么时候上线?游戏售价多少?
怪物猎人物语是一款全新的RPG游戏,玩家在游戏中将化身为骑士,不断与怪物建立羁绊、不断成长,踏上前往外面世界的旅程,且最终目的地是以狩猎怪物为生的猎人世界。因为最近有不少玩家在关注这款游戏,所以下面就给大家分享…...
以创新思维点亮盲盒小程序:探索未来零售新趋势
随着科技的飞速发展和消费者需求的不断变化,零售行业正迎来一场前所未有的变革。在这个变革的浪潮中,盲盒小程序凭借其独特的魅力和巨大的潜力,成为未来零售新趋势的代表之一。本文将探讨如何以创新思维点亮盲盒小程序,探索未来零…...
DzzOffice集成功能最丰富的开源PHP+MySQL办公系统套件
DzzOffice是一套开源办公套件,旨在为企业和团队提供类似“Google企业应用套件”和“微软Office365”的协同办公平台。以下是对DzzOffice的详细介绍: 主要功能和应用: 网盘:支持企业、团队文件的集中管理,提供文件标签…...
关于生成式人工智能的发展
近年来,人工智能的发展引起了广泛关注,尤其是在深度学习领域,以深度神经网络为代表的人工智能技术已经取得了重大突破。然而,深度神经网络也有其局限性。深度学习技术在处理一些复杂问题时表现良好,但在解决更广泛的任…...
Python魔法方法__call__深入详解
目录 1、魔法方法__call__初探 🧙♂️ 1.1 什么是__call__? 1.2 基础用法演示 1.3 自定义行为与参数传递 2、实现轻量级装饰器模式 🎗️ 2.1 装饰器概念回顾 2.2 利用__call__构建装饰器 2.3 深入理解装饰器应用场景 3、类实例变身函数调用 🔮 3.1 类似函数的…...
PyQt5 生成py文件不能运行;pushButton点击事件;QTextEdit 获取输入框内容
目录 cant open file c.pyuic: c.pyuic $FileName$ -o $FileNameWithoutExtension$.p PyQt5 生成py文件不能运行 pushButton点击事件 QTextEdit 获取输入框内容 整体运行代码: Creating a Qt Widget Based Application | Qt Creator Manual cant open file c.pyuic: c.…...
HarmonyOS最佳实践文档总结汇总(面试题可能会问)
api12 上面来了最佳实现方案,未来面试题有的问了 编号分类内容子类链接 1性能体验设计体验设计概述 文档中心用户体验设计 文档中心流畅评测指标 文档中心交互流畅体验设计 文档中心视觉流畅体验设计 文档中心2性能优化开发高性能ArkUIUI组件性能优化文档中心合…...
leetcode 56合并区间
思路 合并就是首先应该按照left左边界排序,排完序以后,如果i的左边界小于等于i-1的右边界,说明有重合,此时这两个可以合并,右边界应该取最大值。 代码 排序 我是定义了一个类,存储左右边界,先将数组转化…...
企业微信内嵌H5项目接入聊天功能
产品需求是,在列表中把符合条件的列表接入聊天功能,以下是详细步骤: 1.引入企业微信 <script src"https://res.wx.qq.com/wwopen/js/jsapi/jweixin-1.0.0.js"></script> 2.获取wx签名(必须要) /*** 获取wx签名**/ export function getWxJsApi(data) {r…...
微信小程序 this.setData高级用法(只更改单个数据)
合理使用 setData | 微信开放文档 1、页面 <view class"h-100px"></view> <view>最简单的数据:</view> <button bind:tap"handleAdd" data-type"1">点我加 1: {{text}}</button> &…...
使用npm发布自己的插件包
文章目录 1. 准备工作1.1 拥有一个npm账号1.2 准备你的插件代码1.3 编写package.json文件 2. 本地测试3. 发布到npm3.1 登录npm3.2 发布插件3.3 更新插件 4. 注意事项 在JavaScript和Node.js的生态系统中,npm(Node Package Manager)是一个非常…...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
AtCoder 第409场初级竞赛 A~E题解
A Conflict 【题目链接】 原题链接:A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串,只有在同时为 o 时输出 Yes 并结束程序,否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...
Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...
WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...
20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
