地图车辆超出范围
This commit is contained in:
@@ -53,8 +53,8 @@ export const queryMapAllStation = () => {
|
||||
"station_name": "起点",
|
||||
"station_type": "Station",
|
||||
"action_type": "Customize",
|
||||
"x": 1.20309,
|
||||
"y": -0.238922,
|
||||
"x": 0,
|
||||
"y": -0,
|
||||
"angle": -0.006407
|
||||
},
|
||||
{
|
||||
@@ -346,8 +346,8 @@ export const getRouteInfo = () => {
|
||||
"route_id": 1,
|
||||
"start_id": 1,
|
||||
"end_id": 2,
|
||||
"start_x": 1.20309,
|
||||
"start_y": -0.238922,
|
||||
"start_x": 0,
|
||||
"start_y": -0,
|
||||
"end_x": 4.01522,
|
||||
"end_y": 0.0628335,
|
||||
"navigation_mode": "0",
|
||||
@@ -711,8 +711,8 @@ export const getRouteInfo = () => {
|
||||
"end_id": 1,
|
||||
"start_x": 2.33115,
|
||||
"start_y": -0.196742,
|
||||
"end_x": 1.20309,
|
||||
"end_y": -0.238922,
|
||||
"end_x": 0,
|
||||
"end_y": -0,
|
||||
"navigation_mode": "1",
|
||||
"route_type": "1"
|
||||
}
|
||||
|
||||
@@ -64,6 +64,46 @@
|
||||
<el-button class="zoom_btn" style="margin-top: .2rem !important" type="primary" icon="el-icon-rank" size="mini" @click="resetZoomAndDrag"></el-button>
|
||||
<el-button class="zoom_btn" type="primary" icon="el-icon-location" size="mini" :disabled="isCarLocked" @click="handleRelocate"></el-button>
|
||||
</div>
|
||||
<div class="marker_wrap">
|
||||
<div class="path-legend">
|
||||
<div class="legend-title">{{ $t('pathType') }}</div>
|
||||
<div class="legend-item">
|
||||
<div class="line-sample straight-line">
|
||||
<div class="arrow-container">
|
||||
<svg class="arrow-svg" viewBox="0 0 5 6" width="5" height="6">
|
||||
<path d="M0,0 L5,3 L0,6" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="legend-text">{{ $t('straightPath') }}</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="line-sample reverse-line">
|
||||
<div class="arrow-container">
|
||||
<svg class="arrow-svg" viewBox="0 0 5 6" width="5" height="6">
|
||||
<path d="M0,0 L5,3 L0,6" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="legend-text">{{ $t('reversePath') }}</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="line-sample bidirectional-line">
|
||||
<div class="arrow-container left-arrow">
|
||||
<svg class="arrow-svg" viewBox="0 0 5 6" width="5" height="6">
|
||||
<path d="M0,0 L5,3 L0,6" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="arrow-container right-arrow">
|
||||
<svg class="arrow-svg" viewBox="0 0 5 6" width="5" height="6">
|
||||
<path d="M5,0 L0,3 L5,6" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<span class="legend-text">{{ $t('bidirectionalPath') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PointPopup
|
||||
v-if="showPopup"
|
||||
@@ -101,6 +141,10 @@ const ZOOM_CONFIG = {
|
||||
TOUCH_STEP_FACTOR: 50
|
||||
}
|
||||
const PATH_CLICK_THRESHOLD = 5
|
||||
const ARROW_CONFIG = {
|
||||
LENGTH: 10, // 箭头长度(像素)
|
||||
ANGLE: Math.PI / 6 // 箭头与路径的夹角(60度)
|
||||
}
|
||||
const MARKER_SIZE = 16
|
||||
const CAR_SIZE = 15
|
||||
export default {
|
||||
@@ -349,31 +393,235 @@ export default {
|
||||
}, 30),
|
||||
drawPath () {
|
||||
if (!this.pathData.length) return
|
||||
|
||||
this.pathData.forEach((point, index) => {
|
||||
const nextPoint = this.pathData[index + 1]
|
||||
let controlX, controlY
|
||||
if (nextPoint) {
|
||||
// 有下一个点时,使用当前终点和下一个起点的中点作为控制点
|
||||
controlX = (point.px_end_x + nextPoint.px_start_x) / 2
|
||||
controlY = (point.px_end_y + nextPoint.px_start_y) / 2
|
||||
} else {
|
||||
// 最后一个点,使用当前终点作为控制点
|
||||
controlX = point.px_end_x
|
||||
controlY = point.px_end_y
|
||||
// 根据 navigation_mode 设置路径颜色
|
||||
let pathColor, pathColor1
|
||||
switch(point.navigation_mode) {
|
||||
case '0': // 直行 - 绿色
|
||||
pathColor = 'rgba(0, 167, 26, 0.5)'
|
||||
pathColor1= '#00a71a'
|
||||
break
|
||||
case '1': // 后退 - 黄色
|
||||
pathColor = 'rgba(255, 87, 34, 0.5)'
|
||||
pathColor1= '#ff5722'
|
||||
break
|
||||
case '2': // 双向 - 蓝色
|
||||
pathColor = 'rgba(0, 125, 255, 0.5)'
|
||||
pathColor1= '#007dff'
|
||||
break
|
||||
default:
|
||||
pathColor = '#fff'
|
||||
pathColor1= '#fff'
|
||||
}
|
||||
this.ctx.beginPath()
|
||||
this.ctx.moveTo(point.px_start_x, point.px_start_y)
|
||||
this.ctx.quadraticCurveTo(controlX, controlY, point.px_end_x, point.px_end_y)
|
||||
|
||||
// 选中状态高亮
|
||||
if (this.selectedPath.route_id === point.route_id) {
|
||||
this.ctx.strokeStyle = '#ff5722' // 橙色高亮
|
||||
this.ctx.lineWidth = 4
|
||||
this.ctx.strokeStyle = pathColor1
|
||||
|
||||
} else {
|
||||
this.ctx.strokeStyle = '#009de5' // 默认蓝色
|
||||
this.ctx.lineWidth = 2
|
||||
this.ctx.strokeStyle = pathColor
|
||||
}
|
||||
this.ctx.lineWidth = 6
|
||||
this.ctx.beginPath()
|
||||
|
||||
if (point.route_type === '0') {
|
||||
this.drawArc(point, index)
|
||||
} else {
|
||||
this.drawStraightLine(point)
|
||||
}
|
||||
this.ctx.stroke()
|
||||
|
||||
// 绘制箭头(大于号样式)
|
||||
this.drawArrowheads(point, index)
|
||||
})
|
||||
},
|
||||
drawStraightLine(point) {
|
||||
this.ctx.moveTo(point.px_start_x, point.px_start_y)
|
||||
this.ctx.lineTo(point.px_end_x, point.px_end_y)
|
||||
},
|
||||
drawArc(point, index) {
|
||||
const prevPoint = this.pathData[index - 1]
|
||||
const nextPoint = this.pathData[index + 1]
|
||||
|
||||
// 计算进入方向(从上一个线段)
|
||||
let entryDirection
|
||||
if (prevPoint && prevPoint.route_type === "1") {
|
||||
// 上一个点是直线,使用上一个线段的朝向
|
||||
entryDirection = Math.atan2(
|
||||
point.px_start_y - prevPoint.px_start_y,
|
||||
point.px_start_x - prevPoint.px_start_x
|
||||
)
|
||||
} else {
|
||||
// 没有上一个点或上一个点也是圆弧,使用默认方向
|
||||
entryDirection = Math.atan2(
|
||||
point.px_end_y - point.px_start_y,
|
||||
point.px_end_x - point.px_start_x
|
||||
)
|
||||
}
|
||||
|
||||
// 计算离开方向(到下一个线段)
|
||||
let exitDirection
|
||||
if (nextPoint && nextPoint.route_type === "1") {
|
||||
// 下一个点是直线,使用下一个线段的朝向
|
||||
exitDirection = Math.atan2(
|
||||
nextPoint.px_end_y - point.px_end_y,
|
||||
nextPoint.px_end_x - point.px_end_x
|
||||
)
|
||||
} else {
|
||||
// 没有下一个点或下一个点也是圆弧,使用默认方向
|
||||
exitDirection = Math.atan2(
|
||||
point.px_end_y - point.px_start_y,
|
||||
point.px_end_x - point.px_start_x
|
||||
)
|
||||
}
|
||||
|
||||
// 计算圆弧控制点
|
||||
const controlPoint = this.calculateArcControlPoint(
|
||||
point.px_start_x, point.px_start_y,
|
||||
point.px_end_x, point.px_end_y,
|
||||
entryDirection, exitDirection
|
||||
)
|
||||
|
||||
this.ctx.moveTo(point.px_start_x, point.px_start_y)
|
||||
this.ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, point.px_end_x, point.px_end_y)
|
||||
},
|
||||
// 绘制箭头(大于号样式)
|
||||
drawArrowheads(point, index) {
|
||||
const t = 0.5 // 箭头位置在路径中间
|
||||
|
||||
let arrowX, arrowY, dirX, dirY
|
||||
|
||||
if (point.route_type === '1') {
|
||||
// 直线路径
|
||||
arrowX = point.px_start_x + (point.px_end_x - point.px_start_x) * t
|
||||
arrowY = point.px_start_y + (point.px_end_y - point.px_start_y) * t
|
||||
dirX = point.px_end_x - point.px_start_x
|
||||
dirY = point.px_end_y - point.px_start_y
|
||||
} else {
|
||||
// 圆弧路径
|
||||
const prevPoint = this.pathData[index - 1]
|
||||
const nextPoint = this.pathData[index + 1]
|
||||
const entryDirection = prevPoint && prevPoint.route_type === "1"
|
||||
? Math.atan2(point.px_start_y - prevPoint.px_start_y, point.px_start_x - prevPoint.px_start_x)
|
||||
: Math.atan2(point.px_end_y - point.px_start_y, point.px_end_x - point.px_start_x)
|
||||
const exitDirection = nextPoint && nextPoint.route_type === "1"
|
||||
? Math.atan2(nextPoint.px_end_y - point.px_end_y, nextPoint.px_end_x - point.px_end_x)
|
||||
: Math.atan2(point.px_end_y - point.px_start_y, point.px_end_x - point.px_start_x)
|
||||
const controlPoint = this.calculateArcControlPoint(
|
||||
point.px_start_x, point.px_start_y,
|
||||
point.px_end_x, point.px_end_y,
|
||||
entryDirection, exitDirection
|
||||
)
|
||||
|
||||
// 二次贝塞尔曲线中间点
|
||||
arrowX = Math.pow(1 - t, 2) * point.px_start_x
|
||||
+ 2 * (1 - t) * t * controlPoint.x
|
||||
+ Math.pow(t, 2) * point.px_end_x
|
||||
arrowY = Math.pow(1 - t, 2) * point.px_start_y
|
||||
+ 2 * (1 - t) * t * controlPoint.y
|
||||
+ Math.pow(t, 2) * point.px_end_y
|
||||
|
||||
// 曲线在中间点的方向向量
|
||||
dirX = 2 * (1 - t) * (controlPoint.x - point.px_start_x)
|
||||
+ 2 * t * (point.px_end_x - controlPoint.x)
|
||||
dirY = 2 * (1 - t) * (controlPoint.y - point.px_start_y)
|
||||
+ 2 * t * (point.px_end_y - controlPoint.y)
|
||||
}
|
||||
|
||||
// 归一化方向向量
|
||||
const length = Math.sqrt(dirX ** 2 + dirY ** 2)
|
||||
if (length === 0) return
|
||||
|
||||
const normDirX = dirX / length
|
||||
const normDirY = dirY / length
|
||||
|
||||
// 根据 navigation_mode 绘制箭头
|
||||
switch(point.navigation_mode) {
|
||||
case '0': // 直行 - 单个向前箭头
|
||||
this.drawGreaterThanArrow(arrowX, arrowY, normDirX, normDirY)
|
||||
break
|
||||
|
||||
case '1': // 后退 - 也是单个向前箭头
|
||||
this.drawGreaterThanArrow(arrowX, arrowY, normDirX, normDirY)
|
||||
break
|
||||
|
||||
case '2': // 双向 - 两个方向箭头
|
||||
// 向前箭头(稍微偏移)
|
||||
this.drawGreaterThanArrow(
|
||||
arrowX - normDirX * 8,
|
||||
arrowY - normDirY * 8,
|
||||
normDirX,
|
||||
normDirY
|
||||
)
|
||||
// 向后箭头(稍微偏移)
|
||||
this.drawGreaterThanArrow(
|
||||
arrowX + normDirX * 8,
|
||||
arrowY + normDirY * 8,
|
||||
-normDirX,
|
||||
-normDirY
|
||||
)
|
||||
break
|
||||
}
|
||||
},
|
||||
// 绘制大于号样式的箭头
|
||||
drawGreaterThanArrow(x, y, dirX, dirY) {
|
||||
const arrowLength = 2 // 箭头长度
|
||||
const arrowWidth = 4 // 箭头宽度
|
||||
|
||||
// 计算垂直方向向量(旋转90度)
|
||||
const perpX = -dirY
|
||||
const perpY = dirX
|
||||
|
||||
// 计算大于号的三个点
|
||||
// 顶点(箭头尖端)
|
||||
const tipX = x + dirX * arrowLength
|
||||
const tipY = y + dirY * arrowLength
|
||||
|
||||
// 左侧点
|
||||
const leftX = x - dirX * arrowLength / 2 + perpX * arrowWidth / 2
|
||||
const leftY = y - dirY * arrowLength / 2 + perpY * arrowWidth / 2
|
||||
|
||||
// 右侧点
|
||||
const rightX = x - dirX * arrowLength / 2 - perpX * arrowWidth / 2
|
||||
const rightY = y - dirY * arrowLength / 2 - perpY * arrowWidth / 2
|
||||
|
||||
// 绘制大于号形状
|
||||
this.ctx.beginPath()
|
||||
this.ctx.moveTo(leftX, leftY) // 从左侧点开始
|
||||
this.ctx.lineTo(tipX, tipY) // 画到尖端
|
||||
this.ctx.lineTo(rightX, rightY) // 画到右侧点
|
||||
this.ctx.strokeStyle = '#fff'
|
||||
this.ctx.lineWidth = 1.5
|
||||
this.ctx.lineCap = 'round'
|
||||
this.ctx.lineJoin = 'round'
|
||||
this.ctx.stroke()
|
||||
},
|
||||
// 计算圆弧控制点
|
||||
calculateArcControlPoint(startX, startY, endX, endY, entryDir, exitDir) {
|
||||
const midX = (startX + endX) / 2
|
||||
const midY = (startY + endY) / 2
|
||||
|
||||
// 计算方向差异
|
||||
let angleDiff = exitDir - entryDir
|
||||
|
||||
// 规范化角度差异到 [-PI, PI] 范围
|
||||
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI
|
||||
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI
|
||||
|
||||
// 计算弯曲方向和强度
|
||||
const distance = Math.sqrt((endX - startX) ** 2 + (endY - startY) ** 2)
|
||||
const bendStrength = Math.tan(angleDiff / 4) * distance * 0.5
|
||||
|
||||
// 计算垂直于平均方向的控制点偏移
|
||||
const avgDirection = (entryDir + exitDir) / 2
|
||||
const perpendicularDir = avgDirection + Math.PI / 2
|
||||
|
||||
const controlX = midX + bendStrength * Math.cos(perpendicularDir)
|
||||
const controlY = midY + bendStrength * Math.sin(perpendicularDir)
|
||||
|
||||
return { x: controlX, y: controlY }
|
||||
},
|
||||
drawMarkers () {
|
||||
if (!this.pointData.length || !this.ctx) return
|
||||
if (!this.cachedImages.marker) {
|
||||
@@ -385,24 +633,24 @@ export default {
|
||||
}
|
||||
return // 等待图标加载完成后再绘制
|
||||
}
|
||||
this.pointData.forEach(point => {
|
||||
// 绘制选中状态
|
||||
if (point.station_id === this.selectedPointId) {
|
||||
this.pointData.forEach((point, index) => {
|
||||
if (point.station_type === 'Station') {
|
||||
this.ctx.beginPath()
|
||||
this.ctx.arc(point.px_x, point.px_y, 5, 0, Math.PI * 2)
|
||||
this.ctx.fillStyle = '#59ccd2'
|
||||
if (point.station_id === this.selectedPointId) {
|
||||
this.ctx.fillStyle = index === 0 ? '#d700c1' : '#009de5'
|
||||
this.ctx.fill()
|
||||
} else {
|
||||
this.ctx.strokeStyle = index === 0 ? 'rgba(215, 0, 193, 0.6)' : 'rgba(0, 157, 229, 0.6)'
|
||||
this.ctx.lineWidth = 11
|
||||
this.ctx.stroke()
|
||||
this.ctx.fillStyle = 'rgba(255,255,255,0.6)'
|
||||
}
|
||||
this.ctx.fill()
|
||||
} else {
|
||||
this.ctx.drawImage(this.cachedImages.marker, point.px_x - MARKER_SIZE / 2, point.px_y - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE)
|
||||
}
|
||||
// 绘制点位名称(文字大小随缩放变化)
|
||||
if (point.station_type === 'Station') {
|
||||
this.ctx.font = '12px Arial'
|
||||
this.ctx.fillStyle = '#62fa0a'
|
||||
this.ctx.fillStyle = index === 0 ? '#d700c1' : '#62fa0a'
|
||||
this.ctx.textAlign = 'center'
|
||||
this.ctx.fillText(point.station_name, point.px_x, point.px_y + 22)
|
||||
// this.ctx.fillText(point.x, point.px_x, point.px_y + 34)
|
||||
// this.ctx.fillText(point.y, point.px_x, point.px_y + 50)
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -501,7 +749,7 @@ export default {
|
||||
this.showPathPopup = false
|
||||
} else {
|
||||
this.selectedPath = path
|
||||
this.calculatePopupPosition(event, 95, this.pathPopupStyle)
|
||||
this.calculatePopupPosition(event, 143, this.pathPopupStyle)
|
||||
this.showPathPopup = true
|
||||
}
|
||||
this.redrawCanvas()
|
||||
@@ -893,4 +1141,69 @@ export default {
|
||||
font-size: 0.18rem;
|
||||
line-height: 0.32rem;
|
||||
color: #fff;
|
||||
|
||||
.marker_wrap
|
||||
position absolute
|
||||
bottom 10px
|
||||
right 10px
|
||||
z-index 20
|
||||
.path-legend
|
||||
border 1px solid rgba(77, 155, 205, 0.6)
|
||||
border-radius 8px
|
||||
padding 12px
|
||||
box-shadow 0 2px 8px rgba(0, 0, 0, 0.3)
|
||||
.legend-title
|
||||
font-size 14px
|
||||
font-weight bold
|
||||
color #fff
|
||||
margin-bottom 10px
|
||||
text-align center
|
||||
border-bottom 1px solid rgba(255, 255, 255, 0.2)
|
||||
padding-bottom 5px
|
||||
.legend-item
|
||||
display flex
|
||||
align-items center
|
||||
margin-bottom 8px
|
||||
&:last-child
|
||||
margin-bottom 0
|
||||
.line-sample
|
||||
position relative
|
||||
width 50px
|
||||
height 10px
|
||||
margin-right 10px
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
.straight-line
|
||||
background rgba(0, 167, 26, 0.4) // 直行 - 绿色
|
||||
.reverse-line
|
||||
background rgba(255, 87, 34, 0.4) // 后退 - 橙色
|
||||
.bidirectional-line
|
||||
background rgba(0, 125, 255, 0.4) // 双向 - 蓝色
|
||||
justify-content space-between
|
||||
.legend-text
|
||||
font-size 12px
|
||||
color #e0e0e0
|
||||
white-space nowrap
|
||||
.arrow-container
|
||||
position absolute
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
|
||||
/* 直行和后退路径箭头 - 右侧 */
|
||||
.straight-line .arrow-container,
|
||||
.reverse-line .arrow-container
|
||||
left calc(50% - 3px)
|
||||
|
||||
/* 双向路径箭头 */
|
||||
.bidirectional-line .arrow-container
|
||||
&.left-arrow
|
||||
left 12px
|
||||
&.right-arrow
|
||||
right 12px
|
||||
|
||||
/* 箭头SVG样式 */
|
||||
.arrow-svg
|
||||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5))
|
||||
</style>
|
||||
@@ -18,6 +18,8 @@
|
||||
class="mapSvg"
|
||||
ref="mapSvg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="5000"
|
||||
height="5000"
|
||||
>
|
||||
<g
|
||||
ref="pointImgGrop"
|
||||
@@ -364,9 +366,11 @@ export default {
|
||||
this.redrawCanvas()
|
||||
// svg
|
||||
const mapSvg = this.$refs.mapSvg
|
||||
mapSvg.setAttribute('width', this.canvasWidth)
|
||||
mapSvg.setAttribute('height', this.canvasHeight)
|
||||
mapSvg.setAttribute('viewBox', `-${this.originX} -${this.originY} ${this.canvasWidth} ${this.canvasHeight}`)
|
||||
// mapSvg.setAttribute('width', this.canvasWidth)
|
||||
// mapSvg.setAttribute('height', this.canvasHeight)
|
||||
const originX2 = (5000 - this.canvasWidth) / 2
|
||||
const originY2 = (5000 - this.canvasHeight) / 2
|
||||
mapSvg.setAttribute('viewBox', `-${originX2 + this.originX} -${originY2 + this.originY} 5000 5000`)
|
||||
},
|
||||
redrawCanvas: throttle(function () {
|
||||
if (!this.ctx || !this.mapData) return
|
||||
@@ -787,7 +791,7 @@ export default {
|
||||
const mapSvg = this.$refs.mapSvg
|
||||
if (this.canvas) {
|
||||
this.canvas.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale})`
|
||||
mapSvg.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale})`
|
||||
mapSvg.style.transform = `translate(-50%, -50%) translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale})`
|
||||
}
|
||||
},
|
||||
// 计算两点之间的距离
|
||||
@@ -1114,7 +1118,7 @@ export default {
|
||||
background-color rgba(4, 33, 58, 70%)
|
||||
box-shadow inset 1px 1px 7px 2px #4d9bcd
|
||||
overflow hidden
|
||||
.mapCanvas, .mapSvg
|
||||
.mapCanvas
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
@@ -1122,6 +1126,12 @@ export default {
|
||||
height 100%
|
||||
z-index 9
|
||||
.mapSvg
|
||||
position absolute
|
||||
top 50%
|
||||
left 50%
|
||||
transform translate(-50%, -50%)
|
||||
width 5000px
|
||||
height 5000px
|
||||
pointer-events none
|
||||
z-index 10
|
||||
#pointImgGrop
|
||||
|
||||
Reference in New Issue
Block a user