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)是一个非常…...

SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...
Admin.Net中的消息通信SignalR解释
定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...

UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...
Xen Server服务器释放磁盘空间
disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...