import { dataService } from "../Service/DataService.js"; import { stateService } from "../Service/StateService.js"; import { coordinate } from "../Coordinate.js"; import Style from "@/graphic/CanvasStyle/index.js"; import VectorType from "../enum/VectorType.js"; import { mathUtil } from "../Util/MathUtil.js"; import ElementEvents from "../enum/ElementEvents.js"; import { elementService } from "../Service/ElementService.js"; import UIEvents from "@/graphic/enum/UIEvents.js"; import VectorCategory from "@/graphic/enum/VectorCategory.js"; import Settings from "@/graphic/Settings.js"; import { Canvg } from 'canvg' import SVGIcons from "../CanvasStyle/ImageLabels/SVGIcons"; const imgCache = {}; const help = { getVectorStyle(vector, geoType = vector.geoType) { const geoId = vector?.vectorId; if (!geoId) { return [Style[geoType], undefined]; } const itemsEntry = [ [stateService.getSelectItem(), "Select"], [stateService.getDraggingItem(), "Dragging"], [stateService.getFocusItem(), "Focus"], ]; let currentAttr; return [ itemsEntry.reduce((prev, [item, attr]) => { if (!item) return prev; const selected = geoId === item.vectorId || (item.parent && Object.keys(item.parent).some((id) => id === geoId)); if (selected && Style[attr]) { const style = Style[attr][geoType] || Style[attr][vector.category]; if (style) { currentAttr = attr; return style; } } return prev; }, Style[geoType]), currentAttr, ]; }, setStyle(ctx, styles) { for (const style in styles) { if (typeof styles[style] === "function") { styles[style](ctx, vector); } else { ctx[style] = styles[style]; } } }, setVectorStyle(ctx, vector, geoType = vector.geoType) { let styles, attr; if (Array.isArray(geoType)) { for (const type of geoType) { [styles, attr] = help.getVectorStyle(vector, type); if (styles) { break; } } } else { [styles, attr] = help.getVectorStyle(vector, geoType); } help.setStyle(ctx, styles); return [styles, attr]; }, transformCoves(lines) { return lines.map((line) => line.map((line) => ({ start: coordinate.getScreenXY(line.start), end: coordinate.getScreenXY(line.end), controls: line.controls.map(coordinate.getScreenXY.bind(coordinate)), })) ); }, drawCoves(ctx, coves) { for (const curve of coves) { ctx.beginPath(); ctx.moveTo(curve.start.x, curve.start.y); if (curve.controls.length === 1) { ctx.quadraticCurveTo( curve.controls[0].x, curve.controls[0].y, curve.end.x, curve.end.y ); } else { ctx.bezierCurveTo( curve.controls[0].x, curve.controls[0].y, curve.controls[1].x, curve.controls[1].y, curve.end.x, curve.end.y ); } ctx.stroke(); } }, getReal(data) { return (data * coordinate.ratio * coordinate.zoom) / coordinate.defaultZoom; }, getImage(src) { if (imgCache[src]) { return imgCache[src]; } const img = new Image(); img.src = src; return (imgCache[src] = new Promise((resolve) => { img.onload = () => { resolve(img); }; })); }, getTextCenter(ctx, txt) { const text = ctx.measureText(txt); const height = text.actualBoundingBoxAscent + text.actualBoundingBoxDescent; return { width: text.width, height, x: text.width / 2, y: -height / 2, }; }, // 绘制圆角矩形 roundRect(ctx, x, y, width, height, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); }, getRealDistance(p1, p2) { return ( Math.round(mathUtil.getDistance(p1, p2) * coordinate.res * 100) / 100 ); }, getPerpendicularPoint(p1, p2, p3, d) { if (p1.x === p2.x) { return { x: p3.x + d, y: p3.y }; } else if (p1.y === p2.y) { return { x: p3.x, y: p3.y + d }; } // 计算通过 p1 和 p2 的直线的斜率和截距 const slope = (p2.y - p1.y) / (p2.x - p1.x); const intercept = p1.y - slope * p1.x; // 计算垂直线的斜率和截距 const perpendicularSlope = -1 / slope; const perpendicularIntercept = p3.y - perpendicularSlope * p3.x; // 计算垂足点 p0 const x = (perpendicularIntercept - intercept) / (slope - perpendicularSlope); const y = slope * x + intercept; const p0 = { x, y }; // 计算点 p4 const distance = d; // 指定距离 const dx = distance / Math.sqrt(1 + perpendicularSlope ** 2); const dy = perpendicularSlope * dx; return { x: p0.x + dx, y: p0.y + dy }; }, drawLineText(ctx, start, end, text, style) { if (start.x > end.x) { [start, end] = [end, start]; } const angle = (Math.atan2(end.y - start.y, end.x - start.x) * 180) / Math.PI; const center = mathUtil.lineCenter(start, end); ctx.save(); ctx.translate(center.x, center.y); ctx.rotate((angle * Math.PI) / 180); ctx.font = `${(style.fontSize || 10) * coordinate.ratio}px Microsoft YaHei`; const textCenter = help.getTextCenter(ctx, text); const padding = style.padding; help.roundRect( ctx, -textCenter.x - padding, textCenter.y - padding, textCenter.width + 2 * padding, textCenter.height + 2 * padding, textCenter.height / 2 + padding ); ctx.fillStyle = style.backColor; ctx.fill(); ctx.fillStyle = style.fillColor; ctx.fillText(text, -textCenter.x, -textCenter.y); ctx.restore(); }, }; export default class Draw { constructor() { this.canvas = null; this.context = null; } initContext(canvas) { if (canvas) { this.canvas = canvas; this.context = canvas.getContext("2d"); } else { this.context = null; this.canvas = null; } } clear() { this.context.clearRect( 0, 0, this.context.canvas.width, this.context.canvas.height ); } drawBackGroundImg(vector) { if (!vector.display) { return; } const img = vector.imageData; const width = help.getReal(img.width); const height = help.getReal(img.height); const center = coordinate.getScreenXY(vector.center); this.context.save(); this.context.drawImage( img, center.x - width / 2, center.y - height / 2, width, height ); this.context.restore(); } drawGrid(startX, startY, w, h, step1, step2) { this.context.save(); this.context.beginPath(); for (var x = startX; x <= w; x += step1) { this.context.moveTo(x, 0); this.context.lineTo(x, h); } for (var y = startY; y <= h; y += step1) { this.context.moveTo(0, y); this.context.lineTo(w, y); } this.context.strokeStyle = "rgba(0,0,0,0.1)"; this.context.lineWidth = 0.5 * coordinate.ratio; this.context.stroke(); this.context.beginPath(); for (var x = startX; x <= w; x += step2) { this.context.moveTo(x, 0); this.context.lineTo(x, h); } for (var y = startY; y <= h; y += step2) { this.context.moveTo(0, y); this.context.lineTo(w, y); } this.context.strokeStyle = "rgba(0,0,0,0.2)"; this.context.lineWidth = 1 * coordinate.ratio; this.context.stroke(); this.context.restore(); } drawRoad(vector, isTemp) { if (!isTemp && vector.display && vector.way !== "oneWay") { const ctx = this.context; const draw = (midDivide) => { const startScreen = coordinate.getScreenXY(midDivide.start); const endScreen = coordinate.getScreenXY(midDivide.end); ctx.beginPath(); ctx.moveTo(startScreen.x, startScreen.y); ctx.lineTo(endScreen.x, endScreen.y); ctx.stroke(); }; ctx.save(); help.setVectorStyle(ctx, vector); vector.midDivide.leftMidDivide && draw(vector.midDivide.leftMidDivide); vector.midDivide.rightMidDivide && draw(vector.midDivide.rightMidDivide); ctx.restore(); } if (import.meta.env.DEV && !isTemp) { const startReal = isTemp ? vector.start : dataService.getRoadPoint(vector.startId); const endReal = isTemp ? vector.end : dataService.getRoadPoint(vector.endId); this.drawTextByInfo( { x: (startReal.x + endReal.x) / 2, y: (startReal.y + endReal.y) / 2 }, vector.vectorId ); } this.drawRoadEdge(vector, isTemp); vector.leftLanes && vector.leftLanes.forEach(this.drawLan.bind(this)); vector.rightLanes && vector.rightLanes.forEach(this.drawLan.bind(this)); } drawLan(lan) { const ctx = this.context; const start = coordinate.getScreenXY(lan.start); const end = coordinate.getScreenXY(lan.end); ctx.save(); ctx.beginPath(); help.setVectorStyle(ctx, null, "Lane"); ctx.setLineDash(Style.Lane.dash); ctx.moveTo(start.x, start.y); ctx.lineTo(end.x, end.y); ctx.stroke(); ctx.restore(); if (import.meta.env.DEV) { this.drawPoint(lan.start); this.drawPoint(lan.end); } } drawRoadEdge(vector, isTemp) { //判断是否与road方向一致。角度足够小,路足够宽,有可能向量方向不一致 const start = isTemp ? vector.start : dataService.getRoadPoint(vector.startId); const end = isTemp ? vector.end : dataService.getRoadPoint(vector.endId); const drawRoadEdgeChild = (edgeVector) => { const flag = mathUtil.isSameDirForVector( start, end, edgeVector.start, edgeVector.end ); if (flag) { ctx.beginPath(); const point1 = coordinate.getScreenXY(edgeVector.start); const point2 = coordinate.getScreenXY(edgeVector.end); ctx.moveTo(point1.x, point1.y); ctx.lineTo(point2.x, point2.y); ctx.stroke(); } this.drawTextByInfo( { x: (edgeVector.start.x + edgeVector.end.x) / 2, y: (edgeVector.start.y + edgeVector.end.y) / 2, }, edgeVector.vectorId ); }; const leftEdge = isTemp ? vector.leftEdge : dataService.getRoadEdge(vector.leftEdgeId); const rightEdge = isTemp ? vector.rightEdge : dataService.getRoadEdge(vector.rightEdgeId); const ctx = this.context; ctx.save(); isTemp && (ctx.globalAlpha = 0.3); help.setVectorStyle(ctx, leftEdge); drawRoadEdgeChild(leftEdge); help.setVectorStyle(ctx, rightEdge); drawRoadEdgeChild(rightEdge); ctx.restore(); if (import.meta.env.DEV) { this.drawPoint(leftEdge.start); this.drawPoint(leftEdge.end); this.drawPoint(rightEdge.start); this.drawPoint(rightEdge.end); } } drawCrossPoint(vector) { const start = coordinate.getScreenXY( dataService .getRoadEdge(vector.edgeInfo1.id) .getPosition(vector.edgeInfo1.dir) ); const end = coordinate.getScreenXY( dataService .getRoadEdge(vector.edgeInfo2.id) .getPosition(vector.edgeInfo2.dir) ); const pt2 = mathUtil.twoOrderBezier( 0.5, start, coordinate.getScreenXY({ x: vector.x, y: vector.y }), end ); const pt = mathUtil.twoOrderBezier2(0.5, start, pt2, end); const extremePoint = coordinate.getScreenXY(vector.extremePoint); const ctx = this.context; ctx.save(); ctx.strokeStyle = "red"; ctx.beginPath(); ctx.arc( // pt.x, // pt.y, extremePoint.x, extremePoint.y, Style.CrossPoint.radius * coordinate.ratio, 0, Math.PI * 2, true ); ctx.stroke(); ctx.fill(); ctx.restore(); ctx.save(); ctx.beginPath(); help.setVectorStyle(ctx, null, "RoadEdge"); //曲线 // ctx.moveTo(start.x, start.y); // ctx.quadraticCurveTo(pt.x, pt.y, end.x, end.y); const [coves] = help.transformCoves([vector.curves]); help.drawCoves(ctx, coves); ctx.restore(); } drawCurveRoad(vector) { if (vector.display && vector.midDivide) { const covesArray = help.transformCoves([ vector.midDivide.leftMidDivideCurves, vector.midDivide.rightMidDivideCurves, ]); const ctx = this.context; ctx.save(); help.setVectorStyle(ctx, vector); for (let coves of covesArray) { help.drawCoves(ctx, coves); } ctx.restore(); } this.drawCurveRoadEdge(dataService.getCurveRoadEdge(vector.rightEdgeId)); this.drawCurveRoadEdge(dataService.getCurveRoadEdge(vector.leftEdgeId)); vector.leftLanesCurves && vector.leftLanesCurves.forEach(this.drawCurveLan.bind(this)); vector.rightLanesCurves && vector.rightLanesCurves.forEach(this.drawCurveLan.bind(this)); if (import.meta.env.DEV) { vector.points.forEach(this.drawPoint.bind(this)); } } drawCurveRoadEdge(vector, isTemp) { const [coves] = help.transformCoves([vector.curves]); const ctx = this.context; ctx.save(); help.setVectorStyle(ctx, vector); help.drawCoves(ctx, coves); ctx.restore(); if (import.meta.env.DEV) { vector.points.forEach(this.drawPoint.bind(this)); } } drawCurveLan(lines) { const [coves] = help.transformCoves([lines]); const ctx = this.context; ctx.save(); help.setVectorStyle(ctx, null, "CurveLan"); ctx.setLineDash(Style.Lane.dash); help.drawCoves(ctx, coves); ctx.restore(); if (import.meta.env.DEV) { lines.map((line) => { this.drawPoint(line.start); this.drawPoint(line.end); }); } } drawRoadPoint(vector) { this.drawPoint(vector); } drawArrow(vector) { const startReal = dataService.getPoint(vector.startId); const start = coordinate.getScreenXY(startReal); const endReal = dataService.getPoint(vector.endId); const end = coordinate.getScreenXY(endReal); const ctx = this.context; ctx.save(); const [style] = help.setVectorStyle(this.context, vector); if (vector.color) { ctx.strokeStyle = vector.color; } const dires = vector.category === UIEvents.MeasureLine ? [ [start, end], [end, start], ] : [[start, end]]; for (let [start, end] of dires) { const lines = mathUtil.getArrow(start, end); ctx.moveTo(lines[0].x, lines[0].y); ctx.lineTo(lines[1].x, lines[1].y); ctx.lineTo(lines[2].x, lines[2].y); } ctx.stroke(); ctx.restore(); } drawMagnifier(vector) { const ctx = this.context; const [style] = help.setVectorStyle(ctx, vector); const radius = vector.radius || style.radius; this.drawPoint({ ...vector, ...vector.position, radius, }); const pt = coordinate.getScreenXY(vector.position); vector.setPopPosition(); const target = { x: vector.popPosition.x, y: vector.popPosition.y, }; const offset = radius / 2; const targetPts = style === Style.Focus.Magnifier ? [mathUtil.translate(pt, target, pt, radius), target] : null; ctx.save(); ctx.beginPath(); ctx.moveTo(pt.x - offset, pt.y); ctx.lineTo(pt.x + offset, pt.y); ctx.stroke(); ctx.beginPath(); ctx.moveTo(pt.x, pt.y - offset); ctx.lineTo(pt.x, pt.y + offset); ctx.stroke(); if (targetPts) { ctx.beginPath(); ctx.moveTo(targetPts[0].x, targetPts[0].y); ctx.lineTo(targetPts[1].x, targetPts[1].y); ctx.stroke(); let img, imgBound; if (vector.photoImage) { img = vector.photoImage; imgBound = [0, 0, img.width, img.height]; } else { const size = help.getReal(style.target.realRadius); const backImg = dataService.getBackgroundImg(); img = backImg.imageData; const imgCenter = coordinate.getScreenXY(backImg.center); const start = { x: imgCenter.x - help.getReal(img.width) / 2, y: imgCenter.y - help.getReal(img.height) / 2, }; const ro = img.width / help.getReal(img.width); imgBound = [ (pt.x - start.x - size) * ro, (pt.y - start.y - size) * ro, size * 2 * ro, size * 2 * ro, ]; } const size = style.target.radius; ctx.beginPath(); ctx.arc(target.x, target.y, size, 0, 2 * Math.PI); ctx.clip(); ctx.drawImage( img, ...imgBound, target.x - size, target.y - size, size * 2, size * 2 ); ctx.strokeStyle = style.target.strokeStyle; ctx.lineWidth = style.target.lineWidth; ctx.stroke(); } ctx.restore(); } drawCircle(element) { const [_, label] = help.getVectorStyle(element); this.drawPoint({ ...element, // radius: help.getReal(element.radius), radius: (element.radius * coordinate.zoom) / coordinate.defaultZoom, geoType: VectorType.Circle, ...element.center, }); console.log(label); label && element.points.forEach((point) => this.drawPoint(point)); } drawPoint(vector) { if (vector.category === VectorCategory.Point.TestBasePoint) { return; } const pt = coordinate.getScreenXY({ x: vector.x, y: vector.y }); const ctx = this.context; let [style, attr] = help.setVectorStyle(ctx, vector, [ vector.category, vector.geoType, "Point", ]); if (vector.category === VectorCategory.Point.NormalPoint) { const lineid = Object.keys(vector.parent)[0]; let line, style; if (!(lineid && (line = dataService.getLine(lineid)))) { return; } const [_, attr] = help.getVectorStyle(line); if (!attr) { return; } } if (vector.color) { ctx.strokeStyle = vector.color; } const draw = (style) => { const radius = vector.radius || style.radius; ctx.save(); ctx.beginPath(); ctx.arc(pt.x, pt.y, radius, 0, Math.PI * 2, true); help.setStyle(ctx, style); ctx.stroke(); ctx.fill(); ctx.restore(); }; if (Settings.selectBasePointId === vector.vectorId) { style = { ...style, fillStyle: "red", out: style.out && { ...style.out, strokeStyle: "red", }, }; } draw(style); if (style.out) { draw(style.out); } if (import.meta.env.DEV) { if (vector.vectorId) { this.drawTextByInfo(vector, vector.vectorId); } } } drawTextByInfo(position, txt, angle, setStyle = true) { const ctx = this.context; ctx.save(); setStyle && help.setVectorStyle(ctx, null, "Text"); const pt = coordinate.getScreenXY(position); const textCenter = help.getTextCenter(ctx, txt); // pt.x -= textCenter.x; // pt.y -= textCenter.y; if (angle) { ctx.translate(pt.x, pt.y); ctx.rotate(angle); ctx.translate(-textCenter.x, -textCenter.y); ctx.fillText(txt, 0, 0); } else { ctx.fillText(txt, pt.x, pt.y); } ctx.restore(); } // 文字 drawText(vector) { help.setVectorStyle(this.context, vector); this.context.fillStyle = vector.color; const oldFont = this.context.font; this.context.font = `${ vector.fontSize * coordinate.ratio }px Microsoft YaHei`; this.drawTextByInfo( vector.center, vector.value, -(vector.angle || 0), false ); const ctx = this.context; const pt = coordinate.getScreenXY(vector.center); const text = ctx.measureText(vector.value); pt.x -= text.width / 2; pt.y += (text.actualBoundingBoxAscent + text.actualBoundingBoxDescent) / 2; this.context.font = oldFont; } drawSVG(vector) { const points = vector.points.map(coordinate.getScreenXY.bind(coordinate)) const svgWidth= 64 const svgHidth= 64 const width = mathUtil.getDistance(points[0], points[1]) const height = mathUtil.getDistance(points[0], points[3]) const dires = [points[0], {...points[0], x: 10000}, points[1]] let angle = mathUtil.Angle(...dires) * (Math.PI / 180) angle = mathUtil.isClockwise(dires) ? angle : -angle; this.context.save(); this.context.translate(points[0].x, points[0].y) this.context.rotate(angle) this.context.scale(width / svgWidth, height / svgHidth) const [style] = help.setVectorStyle(this.context, vector) this.context.lineWidth = style.lineWidth / (width / svgWidth) SVGIcons.keche_plane.draw(this.context) if (import.meta.env.DEV) { this.context.restore(); this.context.beginPath(); this.context.moveTo(points[0].x, points[0].y); this.context.lineTo(points[1].x, points[1].y); this.context.lineTo(points[2].x, points[2].y); this.context.lineTo(points[3].x, points[3].y); this.context.closePath(); this.context.stroke(); this.context.restore(); } } drawLineText(vector, style) { const startReal = dataService.getPoint(vector.startId); const endReal = dataService.getPoint(vector.endId); help.drawLineText( this.context, coordinate.getScreenXY(startReal), coordinate.getScreenXY(endReal), (vector.value ? Math.round(vector.value * 100) / 100 : help.getRealDistance(startReal, endReal)) + "m", style ); } drawBaseLineLabel(vector) { const startReal = dataService.getPoint(vector.startId); const start = coordinate.getScreenXY(startReal); const endReal = dataService.getPoint(vector.endId); const end = coordinate.getScreenXY(endReal); const point = mathUtil.translate( end, start, end, mathUtil.getDistance(start, end) / 3 ); const p4 = help.getPerpendicularPoint( start, end, point, 30 * coordinate.ratio ); const ctx = this.context; ctx.beginPath(); const [style] = help.setVectorStyle( this.context, vector, vector.category || vector.geoType ); ctx.moveTo(point.x, point.y); ctx.lineTo(p4.x, p4.y); ctx.stroke(); const p5 = help.getPerpendicularPoint( start, end, point, 35 * coordinate.ratio ); this.context.font = `${12 * coordinate.ratio}px Microsoft YaHei`; help.drawLineText( this.context, help.getPerpendicularPoint(point, p5, p5, 10 * coordinate.ratio), help.getPerpendicularPoint(point, p5, p5, -10 * coordinate.ratio), "基准线", { padding: 6 * coordinate.ratio, backColor: "rgba(0,0,0,0)", fillColor: style.strokeStyle, } ); } drawCurveLine(vector) { // points CurveLine console.log(vector) const ctx = this.context; ctx.save(); help.setVectorStyle(ctx, vector); help.drawCoves(ctx, help.transformCoves([vector.points])); ctx.restore(); if (import.meta.env.DEV) { vector.points.forEach(this.drawPoint.bind(this)); } } drawLine(vector) { const startReal = dataService.getPoint(vector.startId); const start = coordinate.getScreenXY(startReal); const endReal = dataService.getPoint(vector.endId); const end = coordinate.getScreenXY(endReal); this.context.save(); const [style, attr] = help.setVectorStyle(this.context, vector, [ vector.category, vector.geoType, "BaseLine", ]); if (style.dash) { this.context.setLineDash(style.dash); } this.context.beginPath(); this.context.moveTo(start.x, start.y); this.context.lineTo(end.x, end.y); this.context.stroke(); this.context.restore(); const drawPoints = () => { // if (attr) { // this.drawPoint(dataService.getPoint(vector.startId)) // this.drawPoint(dataService.getPoint(vector.endId)) // } }; switch (vector.category) { case VectorCategory.Line.ArrowLine: this.drawArrow(vector); drawPoints(); break; case VectorCategory.Line.BaseLine: this.drawBaseLineLabel(vector); drawPoints(); break; case VectorCategory.Line.MeasureLine: case VectorCategory.Line.PositionLine: this.drawLineText(vector, style.text); break; } } drawElementLine(element) { let start = elementService.getPoint(element.startId); start = coordinate.getScreenXY(start); let end = elementService.getPoint(element.endId); end = coordinate.getScreenXY(end); this.context.save(); const [style] = help.setVectorStyle( this.context, element, element.category || element.geoType ); if (style.dash) { this.context.setLineDash(style.dash); } this.context.beginPath(); this.context.moveTo(start.x, start.y); this.context.lineTo(end.x, end.y); this.context.stroke(); this.context.restore(); } } const draw = new Draw(); export { draw };