Files
apt15e/src/pages/modules/map/map copy.vue

839 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page_container" :class="{'enClass': $i18n.locale === 'en-us'}">
<div class="canvas-container" ref="mapContainer">
<canvas
class="mapCanvas"
ref="mapCanvas"
@click="handleCanvasClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
@wheel="handleZoom"
></canvas>
<svg
class="mapSvg"
ref="mapSvg"
xmlns="http://www.w3.org/2000/svg"
>
<g ref="svgPoints">
<circle
v-for="(point, index) in greenPoints"
:key="index"
:cx="convertToSvgX(point.x)"
:cy="convertToSvgY(point.y)"
r="1"
fill="red"
/>
</g>
<image
ref="carImage"
:href="require('../../images/new/agv.png')"
alt="小车"
width="30"
height="30"
:x="carX"
:y="carY"
:transform="`rotate(${rotateAngle} ${carX + 15} ${carY + 15})`"
@mousedown="startCarDrag"
@touchstart="startCarDrag"
/>
</svg>
<div class="map_tools">
<el-button class="zoom_btn" type="primary" :disabled="zoomPercentage === 2" icon="el-icon-minus" size="mini" @click="zoomOut"></el-button>
<div class="zoom_data">{{ zoomPercentage }}%</div>
<el-button class="zoom_btn" type="primary" :disabled="zoomPercentage === 200" icon="el-icon-plus" size="mini" @click="zoomIn"></el-button>
<el-button class="zoom_btn" style="margin-top: .2rem !important" type="primary" icon="el-icon-rank" size="mini"></el-button>
<el-button class="zoom_btn" type="primary" icon="el-icon-location" size="mini" @click="handleRelocate"></el-button>
</div>
</div>
<div
v-if="showPopup"
class="point-popup"
:style="popupStyle"
@click.stop
>
<el-row type="flex" justify="space-between" class="popup-content" style="border-bottom: 1px solid #fff;">
<el-col :span="10"><h3>{{$t('Number')}}</h3></el-col>
<el-col :span="14"><p>{{selectedPoint.station_code}}</p></el-col>
</el-row>
<el-row type="flex" justify="space-between" class="popup-content">
<el-col :span="10"><h3>{{$t('Alias')}}</h3></el-col>
<el-col :span="14"><p>{{selectedPoint.station_name}}</p></el-col>
</el-row>
<el-row type="flex" justify="space-between" class="popup-content">
<el-col :span="10"><h3>{{$t('XCoordinate')}}</h3></el-col>
<el-col :span="14"><p>{{ selectedPoint.x }}</p></el-col>
</el-row>
<el-row type="flex" justify="space-between" class="popup-content">
<el-col :span="10"><h3>{{ $t('YCoordinate') }}</h3></el-col>
<el-col :span="14"><p>{{ selectedPoint.y }}</p></el-col>
</el-row>
<el-row type="flex" justify="space-between" class="popup-content">
<el-col :span="10"><h3>{{$t('AngleValue')}}</h3></el-col>
<el-col :span="14"><p>{{ selectedPoint.angle }}</p></el-col>
</el-row>
</div>
<div
v-if="showPathPopup"
class="point-popup"
:style="pathPopupStyle"
@click.stop
>
<el-row type="flex" justify="space-between" class="popup-content" style="border-bottom: 1px solid #fff;">
<el-col :span="10"><h3>{{ $t('PathID') }}</h3></el-col>
<el-col :span="14"><p>{{selectedPath.route_id}}</p></el-col>
</el-row>
<el-row type="flex" justify="space-between" class="popup-content">
<el-col :span="10"><h3>{{$t('StartCode')}}</h3></el-col>
<el-col :span="14"><p>{{getStationNameById(selectedPath.start_id)}}</p></el-col>
</el-row>
<el-row type="flex" justify="space-between" class="popup-content">
<el-col :span="10"><h3>{{$t('EndCode')}}</h3></el-col>
<el-col :span="14"><p>{{getStationNameById(selectedPath.end_id)}}</p></el-col>
</el-row>
</div>
</div>
</template>
<script>
import { throttle } from 'lodash'
import markerImage from '@/images/new/station.png'
import { getMapInfoByCode, getRouteInfo, queryMapAllStation } from '@/config/mork.js'
import { mapGetters } from 'vuex'
import { points } from '@/config/point.js'
/* eslint-disable */
export default {
name: 'ModuleMap',
data () {
return {
canvas: null, // Canvas元素
ctx: null, // Canvas绘图上下文
canvasWidth: 0, // Canvas/SVG 宽度(同步更新)
canvasHeight: 0, // Canvas/SVG 高度(同步更新)
originX: 0, // Canvas原点X
originY: 0, // Canvas原点Y
cachedImages: {
map: null,
marker: null
},
mapData: null, // 地图数据
pathData: null, // 路径数据
pointData: null, // 点位数据
mapScale: 1, // 地图图片缩放比例
showPopup: false,
selectedPoint: {},
popupStyle: {left: '0px', top: '0px'},
selectedPointId: null,
showPathPopup: false,
selectedPath: {route_id: null},
pathPopupStyle: { left: '0px', top: '0px' },
pathClickThreshold: 5,
touchStart: null, // 触摸起点
isDragging: false, // 是否正在拖动地图
lastX: 0, // 上一次鼠标/触摸X位置
lastY: 0, // 上一次鼠标/触摸Y位置
offsetX: 0, // Canvas偏移X
offsetY: 0, // Canvas偏移Y
scale: 1, // 缩放比例
zoomPercentage: 100, // 当前缩放百分比
greenPoints: [], // 绿色点位数组
websocket: null, // WebSocket实例
isCarLocked: false, // 是否锁定小车位置点击relocate后为true
isDraggingCar: false, // 是否正在拖拽小车
carX: 0, // 小车自然坐标转页面坐标
carY: 0, // 小车自然坐标转页面坐标
rotateAngle: 0, // 旋转弧度
dragStartX: 0, // 记录拖拽开始的偏移量
dragStartY: 0 // 记录拖拽开始的偏移量
}
},
computed: {
...mapGetters(['serverUrl', 'carPosition', 'userRole'])
},
watch: {
carPosition: {
handler(newVal) {
this.updateCarPosition(newVal)
},
deep: true
},
mapData: {
handler(newVal) {
if (newVal) {
this.updateCarPosition(this.carPosition)
}
}
}
},
mounted () {
this.loadAllDataInParallel()
},
beforeDestroy () {
this.closeWebSocket()
},
methods: {
async loadAllDataInParallel () {
try {
this.loading = this.$loading({
lock: true,
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.6)'
})
const [mapData, pathData, stations] = await Promise.all([
this._getMapInfoByCode(),
this._getRouteInfo(),
this._queryMapAllStation()
])
await Promise.all([
this.preloadImage('map', mapData.mapImageAddress),
this.preloadImage('marker', markerImage)
])
this.mapData = mapData
this.pathData = [...pathData]
this.pointData = this.filterPointData(pathData, stations)
this.initCanvas()
this.loading.close()
return true
} catch (e) {
this.$message.error(e.message)
this.loading.close()
return false
}
},
preloadImage(key, url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.src = url
img.onload = () => {
this.cachedImages[key] = img
resolve()
}
img.onerror = () => reject(new Error(`图片加载失败: ${url}`))
})
},
async _getMapInfoByCode () {
try {
const res = await getMapInfoByCode()
if (!res) throw new Error('地图信息为空')
return res
} catch (e) {
console.error('获取地图信息失败:', e)
throw new Error(`获取地图信息失败: ${e.message}`)
}
},
async _getRouteInfo () {
try {
const res = await getRouteInfo()
if (!res) throw new Error('路径信息为空')
return res
} catch (e) {
console.error('获取路径信息失败:', e)
throw new Error(`获取路径信息失败: ${e.message}`)
}
},
async _queryMapAllStation () {
try {
const res = await queryMapAllStation()
if (!res) throw new Error('站点信息为空')
return res
} catch (e) {
console.error('获取站点信息失败:', e)
throw new Error(`获取站点信息失败: ${e.message}`)
}
},
filterPointData (routes, stations) {
const result = []
const seenStationIds = new Set()
routes.forEach(route => {
const startStation = stations.find(s => s.station_id === route.start_id)
const endStation = stations.find(s => s.station_id === route.end_id)
if (startStation && !seenStationIds.has(startStation.station_id)) {
result.push(startStation)
seenStationIds.add(startStation.station_id)
}
if (endStation && !seenStationIds.has(endStation.station_id)) {
result.push(endStation)
seenStationIds.add(endStation.station_id)
}
})
return result
},
initCanvas () {
this.canvas = this.$refs.mapCanvas
this.ctx = this.canvas.getContext('2d')
const container = this.$refs.mapContainer
this.canvasWidth = container.clientWidth
this.canvasHeight = container.clientHeight
this.canvas.width = this.canvasWidth
this.canvas.height = this.canvasHeight
// 计算地图图片缩放比例
if (this.mapData.width > this.canvas.width || this.mapData.height > this.canvas.height) {
const scaleX = this.canvas.width / this.mapData.width
const scaleY = this.canvas.height / this.mapData.height
this.mapScale = Math.min(scaleX, scaleY)
}
const scaledWidth = this.mapData.width * this.mapScale
const scaledHeight = this.mapData.height * this.mapScale
// 计算地图图片左上角位置(即要移动到的原点)
this.originX = (this.canvas.width - scaledWidth) / 2
this.originY = (this.canvas.height - scaledHeight) / 2
this.ctx.translate(this.originX, this.originY)
this.ctx.save()
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}`)
},
updateCarPosition(newVal) {
if (!this.mapData) {
return
}
if (typeof newVal?.x !== 'number' || typeof newVal?.y !== 'number') {
return
}
if (!this.isCarLocked) {
const carX = ((newVal.x - this.mapData.x) / this.mapData.resolution) * this.mapScale
const carY = (this.mapData.height - (newVal.y - this.mapData.y) / this.mapData.resolution) * this.mapScale
const rotateAngle = -newVal.angle * 57.295779513
this.carX = carX - 15
this.carY = carY - 15
this.rotateAngle = rotateAngle
}
},
redrawCanvas: throttle(function () {
if (!this.ctx || !this.mapData) return
const scaledWidth = this.mapData.width * this.mapScale
const scaledHeight = this.mapData.height * this.mapScale
this.ctx.save()
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.restore()
if (!this.cachedImages.map) {
this.cachedImages.map = new Image()
this.cachedImages.map.src = this.mapData.mapImageAddress
this.cachedImages.map.onload = () => {
this.drawMapAndOverlays(scaledWidth, scaledHeight)
}
} else {
this.drawMapAndOverlays(scaledWidth, scaledHeight)
}
}, 30),
drawMapAndOverlays(scaledWidth, scaledHeight) {
if (!this.ctx || !this.cachedImages.map) return
// 绘制地图背景
this.ctx.drawImage(this.cachedImages.map, 0, 0, scaledWidth, scaledHeight)
// 绘制路径(叠加在地图上)
this.drawPath()
// 绘制点位(叠加在最上层)
this.drawMarkers()
},
drawPath () {
if (!this.pathData.length) return
this.pathData.forEach((point, index) => {
const startX = ((point.start_x - this.mapData.x) / this.mapData.resolution) * this.mapScale
const startY = (this.mapData.height - (point.start_y - this.mapData.y) / this.mapData.resolution) * this.mapScale
const endX = ((point.end_x - this.mapData.x) / this.mapData.resolution) * this.mapScale
const endY = (this.mapData.height - (point.end_y - this.mapData.y) / this.mapData.resolution) * this.mapScale
const nextPoint = this.pathData[index + 1];
let controlX, controlY;
if (nextPoint) {
// 有下一个点时,使用当前终点和下一个起点的中点作为控制点
const nextStartX = ((nextPoint.start_x - this.mapData.x) / this.mapData.resolution) * this.mapScale;
const nextStartY = (this.mapData.height - (nextPoint.start_y - this.mapData.y) / this.mapData.resolution) * this.mapScale;
controlX = (endX + nextStartX) / 2;
controlY = (endY + nextStartY) / 2;
} else {
// 最后一个点,使用当前终点作为控制点
controlX = endX;
controlY = endY;
}
this.ctx.beginPath()
this.ctx.moveTo(startX, startY)
this.ctx.quadraticCurveTo(controlX, controlY, endX, endY);
if (this.selectedPath.route_id === point.route_id) {
this.ctx.strokeStyle = '#ff5722' // 橙色高亮
this.ctx.lineWidth = 4
} else {
this.ctx.strokeStyle = '#009de5' // 默认蓝色
this.ctx.lineWidth = 2
}
this.ctx.stroke()
})
},
drawMarkers () {
if (!this.pointData.length || !this.ctx) return
const markerSize = 16
if (!this.cachedImages.marker) {
this.cachedImages.marker = new Image()
this.cachedImages.marker.src = markerImage
// 图标加载完成后重新绘制,确保点位显示
this.cachedImages.marker.onload = () => {
this.redrawCanvas()
}
return // 等待图标加载完成后再绘制
}
this.pointData.forEach(point => {
const x = ((point.x - this.mapData.x) / this.mapData.resolution) * this.mapScale
const y = (this.mapData.height - (point.y - this.mapData.y) / this.mapData.resolution) * this.mapScale
// 绘制选中状态
if (point.station_id === this.selectedPointId) {
this.ctx.beginPath()
this.ctx.arc(x, y, 5, 0, Math.PI * 2)
this.ctx.fillStyle = '#59ccd2'
this.ctx.fill()
} else {
this.ctx.drawImage(this.cachedImages.marker, x - markerSize / 2, y - markerSize / 2, markerSize, markerSize)
}
// 绘制点位名称(文字大小随缩放变化)
if (point.station_type !== 'Station') {
this.ctx.font = '12px Arial'
this.ctx.fillStyle = '#62fa0a'
this.ctx.textAlign = 'center'
this.ctx.fillText(point.station_name, x, y + 22)
this.ctx.fillText(point.x, x, y + 34)
this.ctx.fillText(point.y, x, y + 50)
}
})
},
handleCanvasClick (event) {
const rect = this.canvas.getBoundingClientRect()
const mouseX = (event.clientX - rect.left - this.originX) / this.scale
const mouseY = (event.clientY - rect.top - this.originY) / this.scale
// 1. 优先检测点位点击
let pointClicked = false
for (const point of this.pointData) {
let x = ((point.x - this.mapData.x) / this.mapData.resolution) * this.mapScale
let y = (this.mapData.height - (point.y - this.mapData.y) / this.mapData.resolution) * this.mapScale
x = Math.abs(x) === 0 ? 0 : x
y = Math.abs(y) === 0 ? 0 : y
if (mouseX >= x - 10 && mouseX <= x + 10 && mouseY >= y - 10 && mouseY <= y + 10) {
this.handlePointSelect(point, event)
event.stopPropagation()
pointClicked = true
if (this.showPathPopup) {
this.selectedPath = {route_id: null}
this.showPathPopup = false
this.redrawCanvas()
}
break
}
}
// 2. 只有点位未被点击时,才检测路径点击
if (!pointClicked) {
if (this.showPopup) {
this.selectedPointId = null
this.selectedPoint = null
this.showPopup = false
this.redrawCanvas()
}
let pathClicked = false
for (const path of this.pathData) {
// 计算路径线段的Canvas坐标
const startX = ((path.start_x - this.mapData.x) / this.mapData.resolution) * this.mapScale
const startY = (this.mapData.height - (path.start_y - this.mapData.y) / this.mapData.resolution) * this.mapScale
const endX = ((path.end_x - this.mapData.x) / this.mapData.resolution) * this.mapScale
const endY = (this.mapData.height - (path.end_y - this.mapData.y) / this.mapData.resolution) * this.mapScale
// 计算点击点到线段的距离
const distance = this.pointToLineDistance(
{ x: mouseX, y: mouseY },
{ x: startX, y: startY },
{ x: endX, y: endY }
)
// 距离小于阈值则认为点击了该路径
if (distance <= this.pathClickThreshold) {
this.handlePathSelect(path, event)
event.stopPropagation()
pathClicked = true
break
}
}
if (!pathClicked && this.showPathPopup) {
this.selectedPath = {route_id: null}
this.showPathPopup = false
this.redrawCanvas()
}
}
},
// 计算点到线段的最短距离
pointToLineDistance (point, lineStart, lineEnd) {
// 线段向量
const dx = lineEnd.x - lineStart.x
const dy = lineEnd.y - lineStart.y
// 线段长度的平方
const lengthSquared = dx * dx + dy * dy
// 线段长度为0起点终点重合直接返回点到起点的距离
if (lengthSquared === 0) {
return Math.hypot(point.x - lineStart.x, point.y - lineStart.y)
}
// 计算点击点在投影到线段上的位置比例0~1之间为在线段上
let t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / lengthSquared
t = Math.max(0, Math.min(1, t)) // 限制在0~1范围内
// 投影点坐标
const projectionX = lineStart.x + t * dx
const projectionY = lineStart.y + t * dy
// 点到投影点的距离
return Math.hypot(point.x - projectionX, point.y - projectionY)
},
// 处理点位选中
handlePointSelect (point, event) {
if (this.selectedPointId === point.station_id) {
this.selectedPointId = null
this.selectedPoint = null
this.showPopup = false
} else {
this.selectedPointId = point.station_id
this.selectedPoint = point
this.showPopup = true
this.calculatePopupPosition(event)
}
this.redrawCanvas()
},
// 处理路径选中
handlePathSelect (path, event) {
if (this.selectedPath.route_id === path.route_id) {
this.selectedPath = {route_id: null}
this.showPathPopup = false
} else {
this.selectedPath = path
this.showPathPopup = true
this.calculatePathPopupPosition(event)
}
this.redrawCanvas()
},
// 计算弹窗位置
calculatePopupPosition (event) {
const popupWidth = 200
const popupHeight = 180
let left = event.clientX - 40
let top = event.clientY - 150
// 限制弹窗在窗口可视范围内
left = Math.max(10, Math.min(left, window.innerWidth - popupWidth - 10))
top = Math.max(10, Math.min(top, window.innerHeight - popupHeight - 10))
this.popupStyle = { left: `${left}px`, top: `${top}px` }
},
// 计算路径弹窗位置
calculatePathPopupPosition (event) {
const popupWidth = 160
const popupHeight = 100
let left = event.clientX - 40
let top = event.clientY - 120
// 限制弹窗在窗口可视范围内
left = Math.max(10, Math.min(left, window.innerWidth - popupWidth - 10))
top = Math.max(10, Math.min(top, window.innerHeight - popupHeight - 10))
this.pathPopupStyle = { left: `${left}px`, top: `${top}px` }
},
getStationNameById (stationId) {
if (!stationId || !this.pointData || !this.pointData.length) return null
const station = this.pointData.find(point => point.station_id === stationId)
return station ? station.station_name : null
},
// 触摸事件:支持单指拖动和双指缩放
handleTouchStart (event) {
// 记录触摸点
const touches = Array.from(event.touches)
if (touches.length === 1) {
// 单指:准备拖动
this.isDragging = true
this.lastX = touches[0].clientX
this.lastY = touches[0].clientY
} else if (touches.length === 2) {
// 双指:准备缩放(禁用拖动)
this.isDragging = false
this.touchStart = touches.map(touch => ({
x: touch.clientX,
y: touch.clientY
}))
}
},
applyTransform () {
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})`
}
},
// 计算两点之间的距离
getDistance (x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
},
zoom (step) {
this.scale = Math.min(2, Math.max(0.02, this.scale + step / 100))
this.zoomPercentage = Math.round(this.scale * 100)
this.applyTransform()
},
handleTouchMove (event) {
event.preventDefault() // 阻止页面滚动
const touches = Array.from(event.touches)
if (this.isDragging && touches.length === 1) {
// 单指拖动
const currentX = touches[0].clientX
const currentY = touches[0].clientY
// 计算移动距离(考虑缩放影响,拖动速度与缩放比例成反比)
const moveX = (currentX - this.lastX) / this.scale
const moveY = (currentY - this.lastY) / this.scale
// 更新偏移量
this.offsetX += moveX
this.offsetY += moveY
// 更新上一次位置
this.lastX = currentX
this.lastY = currentY
// 应用变换
this.applyTransform()
} else if (this.touchStart && touches.length === 2) {
// 双指缩放(原有逻辑)
const startDist = this.getDistance(this.touchStart[0].x, this.touchStart[0].y, this.touchStart[1].x, this.touchStart[1].y)
const currentDist = this.getDistance(touches[0].clientX, touches[0].clientY, touches[1].clientX, touches[1].clientY)
// 计算缩放步长(更平滑的缩放)
const scaleFactor = currentDist / startDist
const step = (scaleFactor - 1) * 50; // 调整缩放灵敏度
this.zoom(step)
// 更新触摸起点
this.touchStart = touches.map(touch => ({
x: touch.clientX,
y: touch.clientY
}))
}
},
handleTouchEnd () {
this.touchStart = null
this.isDragging = false
},
// 鼠标按下(开始拖动)
handleMouseDown (event) {
event.preventDefault()
this.isDragging = true
// 记录初始鼠标位置
this.lastX = event.clientX
this.lastY = event.clientY
// 鼠标样式变为抓手
this.canvas.style.cursor = 'grabbing'
},
// 鼠标移动(拖动中)
handleMouseMove (event) {
if (!this.isDragging) return
event.preventDefault()
// 计算移动距离(考虑缩放影响)
const currentX = event.clientX
const currentY = event.clientY
const moveX = (currentX - this.lastX) / this.scale
const moveY = (currentY - this.lastY) / this.scale
// 更新偏移量
this.offsetX += moveX
this.offsetY += moveY
// 更新上一次位置
this.lastX = currentX
this.lastY = currentY
// 应用变换
this.applyTransform()
},
// 鼠标松开(结束拖动)
handleMouseUp () {
this.isDragging = false
this.canvas.style.cursor = 'default'
},
// 鼠标滚动缩放
handleZoom (event) {
event.preventDefault()
const delta = event.deltaY
const step = delta > 0 ? -1 : 1
this.zoom(step)
},
zoomIn () {
if (this.scale < 2) {
this.zoom(8) // 每次放大8%
}
},
zoomOut () {
if (this.scale > 0.02) {
this.zoom(-8) // 每次缩小8%
}
},
initWebSocket () {
this.loading = this.$loading({
lock: true,
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.6)'
})
const protocol = this.serverUrl.startsWith('https') ? 'wss' : 'ws'
const wsHost = this.serverUrl.replace(/^https?:\/\//, '')
this.closeWebSocket()
this.websocket = new WebSocket(`${protocol}://${wsHost}/webSocket/PointCloudData/${this.userRole}`)
this.websocket.onopen = () => {}
this.websocket.onmessage = (event) => {
try {
const res = JSON.parse(event.data)
this.greenPoints = res
this.loading.close()
} catch (error) {
console.error('WebSocket消息解析失败:', error)
}
}
this.websocket.onerror = (error) => {
this.$message.error(this.$t('WebSocketerror') + ':', error)
}
this.websocket.onclose = () => {
this.loading.close()
}
},
closeWebSocket () {
if (this.websocket) {
this.websocket.close(1000, '正常关闭')
this.websocket = null
}
},
// 点击重定位:锁定小车位置,初始化拖拽起点
handleRelocate () {
this.greenPoints = []
this.greenPoints = points.data
// this.initWebSocket()
if (typeof this.carPosition?.x !== 'number' || typeof this.carPosition?.y !== 'number' || !this.mapData) {
return
}
// 锁定小车不再响应carPosition变化
this.isCarLocked = true
},
// 开始拖拽小车
startCarDrag (e) {
if (!this.isCarLocked) return // 未锁定时不允许拖拽
this.isDraggingCar = true
// 初始化拖拽起点
const clientX = e.clientX || e.touches[0].clientX
const clientY = e.clientY || e.touches[0].clientY
// 将 clientX 转换为 SVG 坐标系中的 carX
const svgPoint = this.clientToSvg(clientX, clientY)
// 记录拖拽开始的偏移量
this.dragStartX = svgPoint.x - this.carX
this.dragStartY = svgPoint.y - this.carY
const options = { passive: false }
document.addEventListener('mousemove', this.onCarDragMove, options)
document.addEventListener('mouseup', this.endCarDrag, options)
document.addEventListener('touchmove', this.onCarDragMove, options)
document.addEventListener('touchend', this.endCarDrag, options)
e.preventDefault()
},
// 拖拽小车移动
onCarDragMove: throttle(function (e) {
if (!this.isDraggingCar || !this.isCarLocked) return
e.preventDefault()
const clientX = e.clientX || e.touches[0].clientX
const clientY = e.clientY || e.touches[0].clientY
// 转换为 SVG 坐标
const svgPoint = this.clientToSvg(clientX, clientY)
// 计算新的车辆位置(减去偏移量)
this.carX = svgPoint.x - this.dragStartX
this.carY = svgPoint.y - this.dragStartY
}, 16),
// 结束拖拽小车
endCarDrag () {
if (this.isDraggingCar) {
this.isDraggingCar = false
const options = { passive: false }
document.removeEventListener('mousemove', this.onCarDragMove, options)
document.removeEventListener('mouseup', this.endCarDrag, options)
document.removeEventListener('touchmove', this.onCarDragMove, options)
document.removeEventListener('touchend', this.endCarDrag, options)
const mapCoord = this.svgToMapCoordinate(this.carX + 15, this.carY + 15)
console.log(mapCoord)
}
},
// 将 clientX 转换为 SVG 坐标系
clientToSvg(clientX, clientY) {
const svg = this.$refs.mapSvg
const pt = svg.createSVGPoint()
pt.x = clientX
pt.y = clientY
// 使用 SVG 的变换矩阵将屏幕坐标转换为 SVG 内部坐标
return pt.matrixTransform(svg.getScreenCTM().inverse())
},
// 将 SVG 坐标转换为 clientX
svgToClient(svgX, svgY) {
const svg = this.$refs.mapSvg
const pt = svg.createSVGPoint()
pt.x = svgX
pt.y = svgY
// 将 SVG 坐标转换为屏幕坐标
return pt.matrixTransform(svg.getScreenCTM())
},
// 将 SVG 坐标 carX 转换回地图坐标系
svgToMapCoordinate(svgX, svgY) {
if (!this.mapData) return null
// 反向计算您的原始公式
const mapX = (svgX / this.mapScale) * this.mapData.resolution + this.mapData.x
const mapY = this.mapData.y + (this.mapData.height - (svgY / this.mapScale)) * this.mapData.resolution
return { x: mapX, y: mapY }
},
convertToSvgX (x) {
if (!this.mapData || !this.mapScale) return 0
const svgX = ((x - this.mapData.x) / this.mapData.resolution) * this.mapScale
return svgX
},
convertToSvgY (y) {
if (!this.mapData || !this.mapScale) return 0
const svgY = (this.mapData.height - (y - this.mapData.y) / this.mapData.resolution) * this.mapScale
return svgY
}
}
}
</script>
<style lang="stylus" scoped>
.canvas-container
position relative
height 100%
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.mapCanvas, .mapSvg
position absolute
top 0
left 0
width 100%
height 100%
.mapSvg
pointer-events none
z-index 10
image
pointer-events auto
.point-popup
position fixed
background rgba(0, 0, 0, 70%)
border 1px solid rgba(255, 255, 255, .3)
border-radius: 8px;
padding 10px
box-shadow 0 0px 4px 2px rgba(255,255,255,0.1)
z-index 100
min-width 160px
animation fadeIn 0.2s ease-out
h3
color #fff
font-size 14px
line-height 24px
text-align center
p
color #fff
font-size 14px
line-height 24px
text-align center
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.enClass
.point-popup
min-width 240px
</style>