This commit is contained in:
2025-08-08 17:44:36 +08:00
parent 7367906a45
commit af93101ff2
23 changed files with 926 additions and 1650 deletions

View File

@@ -3,7 +3,7 @@
<el-row type="flex" justify="space-between">
<el-col :span="10"><button class="button_control" :disabled="disabled" @click="addPoint"><p>打点</p></button></el-col>
</el-row>
<gl-map></gl-map>
<gl-map ref="glMap"/>
<el-row type="flex" justify="end">
<button class="button_control" @click="$router.push('/index/home')"><p>放弃建图</p></button>
<button class="button_control" style="margin-left: 10px" :disabled="disabled" @click="_stopMapping"><p>结束建图</p></button>
@@ -13,8 +13,8 @@
:visible.sync="dialogVisible"
width="50%">
<el-form :model="dataForm" ref="dataForm" :label-width="$i18n.locale === 'en-us' ? '' : '1.1rem'" size="mini">
<el-form-item label="站点名称" prop="point">
<el-input v-model="dataForm.point" id="point" @focus="show" data-layout="normal"></el-input>
<el-form-item label="站点名称" prop="stationName">
<el-input v-model="dataForm.stationName" id="stationName" @focus="show" data-layout="normal"></el-input>
</el-form-item>
</el-form>
<el-row type="flex" justify="space-around" style="margin-top: .3rem">
@@ -29,17 +29,30 @@
<p>{{ message }}</p>
</div>
</transition>
<div v-if="showProgress" class="progress-mask">
<!-- 进度条内容区 -->
<div class="progress-container">
<el-progress
:percentage="percentage"
:stroke-width="6"
style="width: 300px;"
></el-progress>
</div>
</div>
</div>
</template>
<script>
import GlMap from './gl-map1.vue'
import { startMapping, setStation, stopMapping, getLocalMaps, oneClickDeployment, abandonMapping } from '../../config/getData.js'
import GlMap from './gl-map.vue'
// import PointCloudMap from './point-cloud-map.vue'
// import { startMapping, stopMapping, getMappingStatus } from '../../config/mork.js'
import { startMapping, stopMapping, getMappingStatus, setStation, oneClickDeployment, abandonMapping } from '../../config/getData.js'
import { mapGetters } from 'vuex'
export default {
name: 'ModuleBuilding',
components: {
GlMap
GlMap,
// PointCloudMap
},
beforeRouteLeave (to, from, next) {
if (this.needsConfirmation) {
@@ -86,21 +99,23 @@ export default {
mapName: '',
dialogVisible: false,
dataForm: {
point: ''
stationCode: '',
stationName: ''
},
keyPoints: [],
disabled: false,
message: '', // 用于显示消息
error: false,
intervalId: null, // 用于存储定时器ID
startTime: null, // 用于记录开始时间
visible: false,
layout: 'normal',
input: null,
options: {
useKbEvents: false,
preventClickEvent: false
}
},
showProgress: false,
percentage: 0,
intervalId: null // 用于存储定时器ID
}
},
computed: {
@@ -108,7 +123,7 @@ export default {
},
beforeDestroy () {
if (this.intervalId) {
clearInterval(this.intervalId)
clearTimeout(this.intervalId)
}
},
created () {
@@ -157,12 +172,21 @@ export default {
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.6)'
})
const getTimestamp = new Date().getTime()
this.mapName = `apt_map_${getTimestamp}`
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0') // 月份从0开始补0
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
this.mapName = `apt_map_${year}${month}${day}${hours}${minutes}${seconds}`
let res = await startMapping(this.mapName)
if (res && res.code === 200) {
this.message = '小车正在建图中'
this.error = false
this.$nextTick(() => {
this.$refs.glMap.init()
})
}
this.loading.close()
} catch (e) {
@@ -174,20 +198,21 @@ export default {
// 打点
addPoint () {
this.dialogVisible = true
this.dataForm.point = 'B' + (this.keyPoints.length + 1)
this.dataForm.stationCode = 'B' + (this.keyPoints.length + 1)
this.dataForm.stationName = 'B' + (this.keyPoints.length + 1)
},
// 打点->保存
async _setStation () {
this.disabled = true
try {
let res = await setStation(this.dataForm.point)
let res = await setStation(this.dataForm.stationName, this.dataForm.stationCode)
if (res) {
if (res.code === 200) {
this.$message({
type: 'success',
message: res.message
})
this.keyPoints.push(this.dataForm.point)
this.keyPoints.push(this.dataForm.stationCode)
} else {
this.$message.error(res.message)
}
@@ -213,65 +238,75 @@ export default {
})
let res = await stopMapping()
if (res && res.code === 200) {
this.message = '正在建图,请等待。。。'
this.error = false
this.startTime = Date.now() // 记录开始时间
this._getLocalMaps()
this.intervalId = setInterval(this._getLocalMaps, 3000) // 每5秒检查一次
} else {
this.$message.error(res.message)
this.message = ''
this.error = false
this.disabled = false
this.loading.close()
this.$nextTick(() => {
this.$refs.glMap.closeWebSocket()
})
this._getMappingStatus()
}
this.disabled = false
this.loading.close()
} catch (e) {
this.$message.error(e)
this.message = ''
this.error = false
this.message = '结束建图出现错误'
this.error = true
this.disabled = false
this.loading.close()
}
},
async _getLocalMaps () {
// 进度条
async _getMappingStatus () {
try {
let res = await getLocalMaps()
let res = await getMappingStatus()
if (res && res.code === 200) {
let flag = false
res.data.map(el => {
if (el.id === this.mapName) {
flag = true
}
})
if (flag) {
clearInterval(this.intervalId) // 停止定时器
await this._oneClickDeployment()
} else {
const elapsedTime = Date.now() - this.startTime
if (elapsedTime >= 60000) { // 超过1分钟
clearInterval(this.intervalId) // 停止定时器
this.message = ''
this.error = false
this.$message.error('建图失败,请重新建图')
this.disabled = false
this.loading.close()
}
this.showProgress = true
this.percentage = Number(res.mapping_percent) || 0
if (res.mapping_return === '0') {
if (this.intervalId) clearTimeout(this.intervalId)
this.intervalId = setTimeout(() => this._getMappingStatus(), 1000)
}
if (res.mapping_return === '1') {
if (this.intervalId) clearTimeout(this.intervalId)
this.showProgress = false
this.percentage = 0
this._oneClickDeployment()
}
if (res.mapping_return === '2') {
if (this.intervalId) clearTimeout(this.intervalId)
this.showProgress = false
this.percentage = 0
this.keyPoints = []
this.$confirm('新建地图失败, 是否重新建图?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this._startMapping()
}).catch(() => {
this.$message({
type: 'info',
message: '已取消建图'
})
})
}
} else {
clearInterval(this.intervalId) // 停止定时器
this.message = ''
this.error = false
this.$message.error('建图失败,请重新建图')
this.disabled = false
this.loading.close()
}
this.loading.close()
} catch (e) {
clearInterval(this.intervalId) // 出错时停止定时器
this.message = ''
this.error = false
this.disabled = false
this.$message.error(e)
this.loading.close()
this.keyPoints = []
this.$confirm('新建地图失败, 是否重新建图?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this._startMapping()
}).catch(() => {
this.$message({
type: 'info',
message: '已取消建图'
})
})
}
},
async _oneClickDeployment () {
@@ -298,17 +333,6 @@ export default {
this.disabled = false
this.loading.close()
}
},
// 放大
zoomIn () {
// this.scale += 0.1
// this.redrawCanvas()
},
// 缩小
zoomOut () {
// this.scale -= 0.1
// if (this.scale < 0.1) this.scale = 0.1 // 防止缩放到 0 或负值
// this.redrawCanvas()
}
}
}
@@ -336,4 +360,25 @@ export default {
.custom-message-enter, .custom-message-leave-to
opacity 0
transform translate(-50%,-100%)
.progress-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6); /* 半透明黑色遮罩 */
z-index: 9999; /* 确保在最上层 */
display: flex;
justify-content: center;
align-items: center;
}
/* 进度条容器 */
.progress-container {
background-color: #fff;
padding: 30px 40px;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>

View File

@@ -12,7 +12,7 @@
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { mapGetters } from 'vuex'
import { points } from '../../config/point.js'
// import { points } from '../../config/point.js'
export default {
/* eslint-disable */
data () {
@@ -36,6 +36,7 @@ export default {
// WebSocket
socket: null,
isLoading: true,
reconnectTimer: null, //
//
viewSize: 20,
animationId: null,
@@ -46,7 +47,7 @@ export default {
}
},
computed: {
...mapGetters(['carPosition']),
...mapGetters(['serverUrl', , 'userRole', 'carPosition']),
},
watch: {
carPosition: {
@@ -65,9 +66,6 @@ export default {
this.initPointCloud();
//
this.initCar();
// WebSocket
this.initWebSocket();
// this.init()
//
this.startAnimationLoop();
//
@@ -81,14 +79,7 @@ export default {
//
this.isDestroyed = true;
// 1. WebSocket
if (this.socket) {
this.socket.onopen = null;
this.socket.onmessage = null;
this.socket.onclose = null;
this.socket.onerror = null;
this.socket.close(1000, '组件销毁');
this.socket = null;
}
this.closeWebSocket()
// 2.
if (this.animationId) {
cancelAnimationFrame(this.animationId);
@@ -144,7 +135,6 @@ export default {
// 8.
window.removeEventListener('resize', this.handleResize);
// 9. GC
this.allPoints = [];
this.camera = null;
this.carMesh = null;
this.pointCloudMesh = null;
@@ -294,9 +284,9 @@ export default {
* 初始化WebSocket连接
*/
initWebSocket() {
const wsHost = process.env.VUE_APP_API_BASE_URL.replace(/^https?:\/\//, '')
const sid = this.$store.getters.userInfo === 'true' ? 1 : 2
this.socket = new WebSocket(`ws://${wsHost}/webSocket/PointCloudData/${sid}`);
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const wsHost = this.serverUrl.replace(/^https?:\/\//, '')
this.socket = new WebSocket(`ws://${wsHost}/webSocket/PointCloudData/${this.userRole}`);
this.socket.onopen = () => {
console.log('WebSocket连接已建立');
@@ -313,7 +303,7 @@ export default {
return;
}
this.lastUpdateTime = now;
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const pointData = JSON.parse(event.data);
this.updatePointCloud(pointData.data);
} catch (error) {
@@ -325,22 +315,38 @@ export default {
console.log('WebSocket连接已关闭代码:', event.code);
this.isLoading = true;
//
setTimeout(() => this.initWebSocket(), 3000);
this.reconnectTimer = setTimeout(() => this.initWebSocket(), 3000);
};
this.socket.onerror = (error) => {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
console.error('WebSocket错误:', error);
this.isLoading = true;
};
},
closeWebSocket () {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
if (this.socket) {
this.socket.onopen = null;
this.socket.onmessage = null;
this.socket.onclose = null;
this.socket.onerror = null;
this.socket.close(1000, '组件销毁');
this.socket = null;
}
this.allPoints = []
},
init () {
const pointData = points.data
this.updatePointCloud(pointData);
setTimeout(() => {
const arr = [{x: 2, y: 2}, {x: 4, y: 4}]
this.updatePointCloud(arr);
}, 2000)
// WebSocket
this.initWebSocket();
// const pointData = points.data
// this.updatePointCloud(pointData);
// setTimeout(() => {
// const arr = [{x: 2, y: 2}, {x: 4, y: 4}]
// this.updatePointCloud(arr);
// }, 2000)
},
/**
@@ -457,7 +463,7 @@ export default {
.point-cloud-map
position relative
width 100%
height calc(100% - 1rem)
height calc(100% - 1rem - 6px)
margin .14rem 0
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd

View File

@@ -1,364 +0,0 @@
<template>
<div class="canvas-container" :style="{'background-color': isConnected ? 'rgba(4, 33, 58, 70%)' : '#fff'}">
<div ref="container" class="point-cloud-container"></div>
<!-- <div v-if="!isConnected" class="reload_bg"><el-button type="danger">刷新</el-button></div> -->
</div>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { mapGetters } from 'vuex'
import { points } from '../../config/point.js'
export default {
/* eslint-disable */
data () {
return {
isConnected: true,
points: [], // 存储所有点云数据
ws: null, // WebSocket 实例
// Three.js 相关对象
scene: null,
camera: null,
renderer: null,
controls: null,
pointCloud: null,
vehicleSprite: null,
// 渲染循环
animationFrameId: null,
// 性能优化
pointsBuffer: [],
bufferUpdateThreshold: 500,
lastUpdateTime: 0,
// 小车图片
vehicleImage: require('../../images/new/agv.png'),
// 2D视图参数
viewSize: 20,
minZoom: 0.5,
maxZoom: 5
}
},
computed: {
...mapGetters(['agvObj', 'position', 'rotation']),
},
watch: {
agvObj: {
handler() {
this.updateVehiclePosition()
},
deep: true
},
position: {
handler() {
this.updateVehiclePosition()
},
deep: true
},
rotation: {
handler() {
this.updateVehiclePosition()
},
deep: true
}
},
mounted () {
this.initThreeJS()
// this.initWebSocket()
this.init()
this.startRendering()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy () {
if (this.ws) this.ws.close()
window.removeEventListener('resize', this.handleResize)
if (this.renderer) this.renderer.dispose()
if (this.controls) {
this.controls.dispose()
}
},
methods: {
initWebSocket() {
const wsHost = process.env.VUE_APP_API_BASE_URL.replace(/^https?:\/\//, '')
const sid = this.$store.getters.userInfo === 'true' ? 1 : 2
this.ws = new WebSocket(`ws://${wsHost}/webSocket/PointCloudData/${sid}`)
this.ws.onopen = () => {
console.log('WebSocket connected')
}
this.ws.onmessage = (event) => {
const newPoints = event.data
// 确保所有点都在XY平面(z=0)
const points2D = newPoints.map(p => ({ x: p.x, y: p.y, z: 0 }))
this.processNewPoints(points2D)
}
this.ws.onerror = (error) => {
console.error('WebSocket error:', error)
}
this.ws.onclose = () => {
console.log('WebSocket disconnected')
}
},
init () {
this.isConnected = true
const newPoints = points.data
const points2D = newPoints.map(p => ({ x: p.x, y: p.y, z: 0 }))
this.processNewPoints(points2D)
},
initThreeJS() {
// viewSize: 控制视图范围大小,值越小看到的范围越小(视角越近)
// camera.position.z: 相机Z轴位置值越小视角越近
// camera.zoom: 直接控制缩放级别大于1放大小于1缩小
// 1. 创建场景
this.scene = new THREE.Scene()
// 2. 创建正交相机(2D视图)
const container = this.$refs.container
const aspect = container.clientWidth / container.clientHeight
this.camera = new THREE.OrthographicCamera(
-this.viewSize * aspect / 2,
this.viewSize * aspect / 2,
this.viewSize / 2,
-this.viewSize / 2,
0.1,
1000
)
this.camera.position.set(0, 0, 40)
this.camera.lookAt(0, 0, 0)
// 3.创建渲染器 - 启用alpha通道实现透明背景
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true, // 关键启用alpha通道
transparent: true // 关键:允许透明
});
this.renderer.setPixelRatio(window.devicePixelRatio)
this.renderer.setSize(container.clientWidth, container.clientHeight)
// 可选设置clearAlpha确保完全透明
this.renderer.setClearAlpha(0);
container.appendChild(this.renderer.domElement)
// 4. 添加OrbitControls并配置为2D模式
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.configure2DControls()
// 5. 添加2D坐标轴辅助
const axesHelper = new THREE.AxesHelper(10)
this.scene.add(axesHelper)
// 6. 初始化点云
this.initPointCloud()
// 7. 初始化车辆精灵
this.initVehicleSprite()
},
configure2DControls() {
// 禁用旋转
this.controls.enableRotate = false
// 启用平面平移
this.controls.screenSpacePanning = true
// 设置缩放限制
this.controls.minZoom = this.minZoom
this.controls.maxZoom = this.maxZoom
// 启用阻尼效果
this.controls.enableDamping = true
this.controls.dampingFactor = 0.25
// 确保相机保持2D视角
this.controls.addEventListener('change', () => {
this.camera.position.z = 40
// this.camera.lookAt(this.controls.target)
})
// 修复触摸事件处理
this.fixTouchEvents()
},
fixTouchEvents() {
// 确保控制器的触摸处理函数存在
if (!this.controls) return;
// 备份原始触摸处理函数
const originalOnTouchStart = this.controls.onTouchStart
const originalOnTouchMove = this.controls.onTouchMove
const originalOnTouchEnd = this.controls.onTouchEnd
// 修复触摸开始事件
this.controls.onTouchStart = (event) => {
if (!event.touches) return
if (originalOnTouchStart) originalOnTouchStart(event)
}
// 修复触摸移动事件
this.controls.onTouchMove = (event) => {
if (!event.touches || event.touches.length === 0) return
if (event.touches[0] && event.touches[0].clientX !== undefined) {
if (originalOnTouchMove) originalOnTouchMove(event)
}
}
// 修复触摸结束事件
this.controls.onTouchEnd = (event) => {
if (!event.touches) return
if (originalOnTouchEnd) originalOnTouchEnd(event)
}
},
initPointCloud() {
const geometry = new THREE.BufferGeometry()
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 1,
transparent: true,
opacity: 1
})
this.pointCloud = new THREE.Points(geometry, material)
this.scene.add(this.pointCloud)
},
initVehicleSprite() {
const textureLoader = new THREE.TextureLoader()
textureLoader.load(this.vehicleImage, (texture) => {
// 97px × 67px图片的比例
const imageAspect = 97 / 67
const spriteWidth = 2
const spriteHeight = spriteWidth / imageAspect
this.vehicleSprite = new THREE.Sprite(
new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: 0.9
})
)
this.vehicleSprite.scale.set(spriteWidth, spriteHeight, 1)
// 角度偏移(根据图片初始朝向调整)
this.updateVehiclePosition()
this.scene.add(this.vehicleSprite)
})
},
processNewPoints(newPoints) {
this.pointsBuffer.push(...newPoints)
const now = performance.now()
if (this.pointsBuffer.length >= this.bufferUpdateThreshold || now - this.lastUpdateTime > 1000) {
this.updatePointCloudGeometry()
this.lastUpdateTime = now
}
},
updatePointCloudGeometry() {
if (this.pointsBuffer.length === 0) return
this.points = this.points.concat(this.pointsBuffer)
this.pointsBuffer = []
if (this.points.length > 50000) {
this.points = this.points.slice(-40000)
}
const positions = new Float32Array(this.points.length * 3)
for (let i = 0; i < this.points.length; i++) {
positions[i * 3] = this.points[i].x
positions[i * 3 + 1] = this.points[i].y
positions[i * 3 + 2] = 0
}
this.pointCloud.geometry.dispose()
this.pointCloud.geometry = new THREE.BufferGeometry()
this.pointCloud.geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
},
updateVehiclePosition() {
if (!this.vehicleSprite) return
this.vehicleSprite.position.set(
this.position.x,
this.position.y,
0
)
const radians = this.rotation * (Math.PI / 180)
this.vehicleSprite.rotation.X = radians
},
startRendering() {
const render = () => {
this.controls.update()
this.renderer.render(this.scene, this.camera)
this.animationFrameId = requestAnimationFrame(render)
}
render()
},
handleResize() {
const container = this.$refs.container
const width = container.clientWidth
const height = container.clientHeight
const aspect = width / height
this.camera.left = -this.viewSize * aspect / 2
this.camera.right = this.viewSize * aspect / 2
this.camera.top = this.viewSize / 2
this.camera.bottom = -this.viewSize / 2
this.camera.updateProjectionMatrix()
this.renderer.setSize(width, height)
},
zoomToArea(minX, maxX, minY, maxY) {
const width = maxX - minX
const height = maxY - minY
const centerX = (minX + maxX) / 2
const centerY = (minY + maxY) / 2
const margin = 0.1
const targetViewSize = Math.max(width, height) * (1 + margin)
// 计算合适的缩放级别
const zoomLevel = this.viewSize / targetViewSize
this.controls.target.set(centerX, centerY, 0)
// 通过调整camera.zoom实现平滑缩放
this.camera.zoom = Math.min(this.maxZoom, Math.max(this.minZoom, zoomLevel))
this.camera.updateProjectionMatrix()
this.controls.update()
}
}
}
</script>
<style lang="stylus" scoped>
.canvas-container
position relative
display flex
justify-content: center;
align-items: center;
height calc(100% - 1rem)
margin .14rem 0
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.point-cloud-container
width 100%
height 100%
.reload_bg {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
}
</style>

View File

@@ -1,384 +0,0 @@
<template>
<div class="point-cloud-map">
<div ref="canvasContainer" class="canvas-container"></div>
<!-- 加载状态指示器 -->
<div v-if="isLoading" class="loading-indicator">
<i class="fa fa-circle-o-notch fa-spin"></i> 加载中...
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { mapGetters } from 'vuex'
import { points } from '../../config/point.js'
export default {
/* eslint-disable */
data () {
return {
// Three.js核心对象
scene: null,
camera: null,
renderer: null,
controls: null,
// 点云相关
pointCloudGeometry: null,
pointCloudMaterial: null,
pointCloudMesh: null,
pointCount: 0,
pointScale: 1.0,
// 小车相关
vehicleImage: require('../../images/new/agv.png'),
carMesh: null,
carTexture: null,
// WebSocket相关
socket: null,
isLoading: true,
// 动画与性能
viewSize: 20,
animationId: null,
lastUpdateTime: 0,
updateInterval: 100, // 限制更新频率,毫秒
}
},
computed: {
...mapGetters(['carPosition']),
},
watch: {
carPosition: {
handler(newVal) {
this.updateCarPosition(newVal)
},
deep: true
}
},
mounted () {
// 初始化Three.js场景
this.initThreeJs();
// 初始化控制器(拖动和缩放)
this.initControls();
// 初始化点云
this.initPointCloud();
// 初始化小车模型
this.initCar();
// 初始化WebSocket连接
// this.initWebSocket();
this.init()
// 启动动画循环
this.startAnimationLoop();
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize);
// 初始加载完成
setTimeout(() => {
this.isLoading = false;
}, 1000);
},
beforeDestroy () {
// 清理资源
if (this.socket) {
this.socket.close();
}
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
window.removeEventListener('resize', this.handleResize);
// 清理Three.js资源
if (this.renderer) this.renderer.dispose();
if (this.pointCloudGeometry) this.pointCloudGeometry.dispose();
if (this.pointCloudMaterial) this.pointCloudMaterial.dispose();
if (this.carTexture) this.carTexture.dispose();
},
methods: {
/**
* 初始化Three.js场景、相机和渲染器
*/
initThreeJs() {
const container = this.$refs.canvasContainer;
const aspect = container.clientWidth / container.clientHeight
// 创建场景
this.scene = new THREE.Scene();
// 创建正交相机适合2D场景
this.camera = new THREE.OrthographicCamera(
-this.viewSize * aspect / 2, // left
this.viewSize * aspect / 2, // right
this.viewSize / 2, // top
-this.viewSize / 2, // bottom
0.1, // near
1000 // far
);
this.camera.position.z = 100; // 保持在Z轴上方不影响2D视图
// 创建渲染器 - 启用alpha通道实现透明背景
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true, // 关键启用alpha通道
transparent: true // 关键:允许透明
});
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
// 可选设置clearAlpha确保完全透明
this.renderer.setClearAlpha(0);
container.appendChild(this.renderer.domElement);
},
/**
* 初始化控制器,支持拖动和缩放
*/
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableRotate = false; // 禁用旋转保持2D视图
this.controls.enableZoom = true; // 启用缩放
this.controls.enablePan = true; // 启用平移(拖动)
this.controls.screenSpacePanning = true; // 2D平移模式
this.controls.touchZoomSpeed = 0.5; // 降低触摸缩放速度
this.controls.panSpeed = 0.3; // 降低平移速度
// 鼠标/触摸优化
this.controls.mouseButtons = {
LEFT: THREE.MOUSE.PAN, // 左键拖动
MIDDLE: THREE.MOUSE.DOLLY // 中键缩放(可选)
};
// 触摸支持
this.controls.touchAction = 'none';
this.controls.touchPan = true;
this.controls.touchZoom = true;
// 添加错误处理逻辑
this.controls.addEventListener('error', (event) => {
console.error('OrbitControls error:', event);
});
},
/**
* 初始化点云
*/
initPointCloud() {
// 使用BufferGeometry提高大量点的渲染性能
this.pointCloudGeometry = new THREE.BufferGeometry();
// 点材质设置
this.pointCloudMaterial = new THREE.PointsMaterial({
color: 0xFFFFFF,
size: 1, // 初始值会被动态覆盖
sizeAttenuation: true, // 关键!
transparent: true,
opacity: 0.8
});
// 创建点云网格
this.pointCloudMesh = new THREE.Points(
this.pointCloudGeometry,
this.pointCloudMaterial
);
this.scene.add(this.pointCloudMesh);
},
/**
* 初始化小车模型
*/
initCar() {
// 加载小车图片作为精灵
const loader = new THREE.TextureLoader();
this.carTexture = loader.load(this.vehicleImage, (texture) => {
// 97px × 67px图片的比例
const imageAspect = 97 / 67
const spriteWidth = 2
const spriteHeight = spriteWidth / imageAspect
// 创建平面几何体
const geometry = new THREE.PlaneGeometry(spriteWidth, spriteHeight);
// 创建材质并应用纹理
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.9
});
// 创建网格对象
this.carMesh = new THREE.Mesh(geometry, material);
// 设置锚点为小车中心(以便旋转正确)
this.carMesh.position.set(spriteWidth / 2, spriteHeight / 2, 0);
// 添加到场景中
this.scene.add(this.carMesh);
// 初始位置角度更新
if (this.carPosition) {
this.updateCarPosition(this.carPosition);
}
}, undefined, (error) => {
console.error('小车图片加载失败:', error);
});
},
/**
* 更新小车位置和角度
*/
updateCarPosition(position) {
if (this.carMesh && position) {
this.carMesh.position.x = position.x * this.pointScale;
this.carMesh.position.y = position.y * this.pointScale;
// 转换角度为弧度并调整方向Three.js使用弧度
this.carMesh.rotation.z = THREE.MathUtils.degToRad(position.angle);
}
},
/**
* 初始化WebSocket连接
*/
initWebSocket() {
const wsHost = process.env.VUE_APP_API_BASE_URL.replace(/^https?:\/\//, '')
const sid = this.$store.getters.userInfo === 'true' ? 1 : 2
this.socket = new WebSocket(`ws://${wsHost}/webSocket/PointCloudData/${sid}`);
this.socket.onopen = () => {
console.log('WebSocket连接已建立');
this.isLoading = false;
};
this.socket.onmessage = (event) => {
try {
// 限制更新频率,优化性能
const now = Date.now();
if (now - this.lastUpdateTime < this.updateInterval) {
return;
}
this.lastUpdateTime = now;
const pointData = event.data;
this.updatePointCloud(pointData);
} catch (error) {
console.error('解析点云数据失败:', error);
}
};
this.socket.onclose = (event) => {
console.log('WebSocket连接已关闭代码:', event.code);
this.isLoading = true;
// 自动重连
setTimeout(() => this.initWebSocket(), 3000);
};
this.socket.onerror = (error) => {
console.error('WebSocket错误:', error);
this.isLoading = true;
};
},
init () {
const pointData = points.data
this.updatePointCloud(pointData);
},
/**
* 更新点云数据,优化大量点的渲染性能
*/
updatePointCloud(points) {
if (!Array.isArray(points) || points.length === 0) return;
// 性能优化:仅在点数变化时重新分配缓冲区
if (points.length !== this.pointCount) {
this.pointCount = points.length;
// 创建新的缓冲区 (x, y, z)z始终为0
const positions = new Float32Array(this.pointCount * 3);
this.pointCloudGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
);
}
// 获取位置缓冲区并更新数据
const positionAttribute = this.pointCloudGeometry.getAttribute('position');
const positions = positionAttribute.array;
// 填充数据z始终为0
for (let i = 0; i < this.pointCount; i++) {
const point = points[i];
// 假设点数据格式为 {x: number, y: number}
positions[i * 3] = (point.x || 0) * this.pointScale;
positions[i * 3 + 1] = (point.y || 0) * this.pointScale;
positions[i * 3 + 2] = 0; // Z轴固定为0
}
// 标记属性需要更新
positionAttribute.needsUpdate = true;
},
/**
* 处理窗口大小变化
*/
handleResize() {
const container = this.$refs.canvasContainer;
const width = container.clientWidth;
const height = container.clientHeight;
// 更新相机
this.camera.left = width / -2;
this.camera.right = width / 2;
this.camera.top = height / 2;
this.camera.bottom = height / -2;
this.camera.updateProjectionMatrix();
// 更新渲染器
this.renderer.setSize(width, height);
},
/**
* 启动动画循环
*/
startAnimationLoop() {
const animate = () => {
this.animationId = requestAnimationFrame(animate);
// 更新控制器(用于阻尼效果)
if (this.controls) {
this.controls.update();
}
// 渲染场景
this.renderer.render(this.scene, this.camera);
};
animate();
}
}
}
</script>
<style lang="stylus" scoped>
.point-cloud-map
position relative
width 100%
height calc(100% - 1rem)
margin .14rem 0
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.canvas-container {
width: 100%;
height: 100%;
position: relative;
z-index: 0;
}
.loading-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.8);
padding: .01rem .02rem;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
font-size: .12rem;
color: #333;
z-index: 100;
display: flex;
align-items: center;
}
</style>

View File

@@ -106,7 +106,7 @@ export default {
}
},
computed: {
...mapGetters(['carPosition'])
...mapGetters(['serverUrl', 'carPosition'])
},
watch: {
carPosition: {
@@ -172,8 +172,7 @@ export default {
}
return new Promise((resolve, reject) => {
const img = new Image()
const urlHost = process.env.VUE_APP_API_BASE_URL
img.src = `${urlHost}${this.mapData.mapImageAddress}`
img.src = `${this.serverUrl}${this.mapData.mapImageAddress}`
// img.src = this.mapData.mapImageAddress
img.onload = () => {
this.cachedImages.map = img

View File

@@ -1,273 +0,0 @@
<template>
<div class="page_container">
<div class="canvas-container">
<canvas
id="mapCanvas"
ref="mapCanvas"
@wheel="handleZoom"
@click="handleCanvasClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
></canvas>
<el-row type="flex" justify="end" class="map_tools">
<el-button type="primary" :disabled="zoomPercentage === 2" icon="el-icon-minus" size="mini" style="border: 0;border-radius: 0;" @click="zoomOut"></el-button>
<div class="zoom_data">{{ zoomPercentage }}%</div>
<el-button type="primary" :disabled="zoomPercentage === 200" icon="el-icon-plus" size="mini" style="border: 0;border-radius: 0;" @click="zoomIn"></el-button>
</el-row>
</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>编号</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>别名</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>X坐标</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>Y坐标</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>角度值</h3></el-col>
<el-col :span="14"><p>{{ selectedPoint.angle }}</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 canvasZoomDrag from '../../config/canvasZoomDrag'
export default {
data () {
return {
canvas: null, // Canvas 元素
ctx: null, // Canvas 绘图上下文
mapData: null, // 地图数据
pathData: null, // 路径数据
pointData: null, // 点位数据
showPopup: false,
selectedPoint: {},
popupStyle: {left: '0px', top: '0px'},
selectedPointId: null
}
},
mixins: [canvasZoomDrag],
mounted () {
this._getMapInfoByCode()
document.addEventListener('click', this.handleDocumentClick)
},
beforeDestroy () {
// 移除事件监听
document.removeEventListener('click', this.handleDocumentClick)
},
methods: {
async _getMapInfoByCode () {
try {
let res = await getMapInfoByCode()
if (res) {
this.mapData = res
this._getRouteInfo()
}
} catch (e) {
this.$message.error(e)
}
},
async _getRouteInfo () {
try {
let res = await getRouteInfo()
this.pathData = [...res]
this._queryMapAllStation()
} catch (e) {
this.$message.error(e)
}
},
async _queryMapAllStation () {
try {
let res = await queryMapAllStation()
const stations = [...res]
this.pointData = this.filterPointData(this.pathData, stations)
this.initCanvas()
} catch (e) {
this.$message.error(e)
}
},
filterPointData (routes, stations) {
const result = []
const seenStationIds = new Set()
routes.forEach((route) => {
const startStation = stations.find((station) => station.station_id === route.start_id)
const endStation = stations.find((station) => station.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')
this.canvas.width = this.mapData.width
this.canvas.height = this.mapData.height
this.redrawCanvas()
},
redrawCanvas () {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
const img = new Image()
// img.src = `${this.$store.getters.baseUrl}${this.mapData.mapImageAddress}`
img.src = `${this.mapData.mapImageAddress}`
img.onload = () => {
this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height)
this.ctx.save()
this.drawPath()
this.drawMarkers()
}
},
drawPath () {
this.ctx.beginPath()
this.pathData.forEach((point, index) => {
const startX = (point.start_x - this.mapData.x) / this.mapData.resolution
const startY = this.mapData.height - (point.start_y - this.mapData.y) / this.mapData.resolution
const endX = (point.end_x - this.mapData.x) / this.mapData.resolution
const endY = this.mapData.height - (point.end_y - this.mapData.y) / this.mapData.resolution
this.ctx.moveTo(startX, startY)
this.ctx.lineTo(endX, endY)
this.ctx.strokeStyle = '#009de5'
this.ctx.lineWidth = 2
this.ctx.stroke()
})
},
drawMarkers () {
const markerImg = new Image()
markerImg.src = markerImage
markerImg.onload = () => {
this.pointData.forEach(point => {
const x = (point.x - this.mapData.x) / this.mapData.resolution
const y = this.mapData.height - (point.y - this.mapData.y) / this.mapData.resolution
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(markerImg, x - 5, y - 5, 10, 10)
}
this.ctx.font = '12px Arial'
this.ctx.fillStyle = 'white'
this.ctx.textAlign = 'center'
this.ctx.fillText(point.station_name, x, y + 18)
})
}
},
handleCanvasClick (event) {
const rect = this.canvas.getBoundingClientRect()
const mouseX = (event.clientX - rect.left) / this.scale
const mouseY = (event.clientY - rect.top) / this.scale
this.pointData.forEach(point => {
let x = (point.x - this.mapData.x) / this.mapData.resolution
let y = this.mapData.height - (point.y - this.mapData.y) / this.mapData.resolution
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) {
if (this.selectedPointId === point.station_id) {
this.resetSelection()
} else {
this.selectedPointId = point.station_id
this.selectedPoint = point
this.showPopup = true
this.popupStyle = {
left: `${event.clientX - 40}px`,
top: `${event.clientY - 150}px`
}
this.initCanvas()
event.stopPropagation()
}
}
})
},
handleDocumentClick () {
this.resetSelection()
},
resetSelection () {
if (this.selectedPointId) {
this.selectedPointId = null
this.selectedPoint = null
this.showPopup = false
this.initCanvas()
}
}
}
}
</script>
<style lang="stylus" scoped>
.canvas-container
position relative
display flex
justify-content: center;
align-items: center;
height calc(100% - .32rem)
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.map_tools
position absolute
top 0
right 0
.zoom_data
width .6rem
font-size .16rem
height .32rem
line-height .32rem
color #00d9f3
text-align center
border-top 1px solid #009fde
border-bottom 1px solid #009fde
.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 150px
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); }
}
</style>

View File

@@ -1,430 +0,0 @@
<template>
<div class="page_container">
<div class="canvas-container">
<canvas
id="mapCanvas"
ref="mapCanvas"
@wheel="handleZoom"
@click="handleCanvasClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
></canvas>
<el-row type="flex" justify="end" class="map_tools">
<el-button type="primary" :disabled="zoomPercentage === 2" icon="el-icon-minus" size="mini" style="border: 0;border-radius: 0;" @click="zoomOut"></el-button>
<div class="zoom_data">{{ zoomPercentage }}%</div>
<el-button type="primary" :disabled="zoomPercentage === 200" icon="el-icon-plus" size="mini" style="border: 0;border-radius: 0;" @click="zoomIn"></el-button>
</el-row>
</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>编号</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>别名</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>X坐标</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>Y坐标</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>角度值</h3></el-col>
<el-col :span="14"><p>{{ selectedPoint.angle }}</p></el-col>
</el-row>
</div>
</div>
</template>
<script>
import { throttle } from 'lodash'
import markerImage from '@images/new/station.png'
import carImage from '../../images/new/car.png'
import { getMapInfoByCode, getRouteInfo, queryMapAllStation } from '../../config/getData.js'
import canvasZoomDrag from '../../config/canvasZoomDrag'
import { mapGetters } from 'vuex'
export default {
data () {
return {
canvas: null, // Canvas 元素
ctx: null, // Canvas 绘图上下文
mapData: null, // 地图数据
pathData: null, // 路径数据
pointData: null, // 点位数据
showPopup: false,
selectedPoint: {},
popupStyle: {left: '0px', top: '0px'},
selectedPointId: null,
cachedImages: {
map: null,
marker: null,
car: null
},
imageLoadStatus: {
map: false,
marker: false,
car: false
}
}
},
computed: {
...mapGetters(['agvObj'])
},
mixins: [canvasZoomDrag],
mounted () {
this.preloadMarkerImage()
this.preloadCarImage()
this.loadAllDataInParallel()
document.addEventListener('click', this.handleDocumentClick)
this.carPositionWatcher = this.$watch(
() => this.agvObj,
(newVal) => {
if (newVal && newVal.x !== undefined && newVal.y !== undefined) {
this.redrawCanvas()
}
},
{ deep: true }
)
},
beforeDestroy () {
document.removeEventListener('click', this.handleDocumentClick)
if (this.carPositionWatcher) {
this.carPositionWatcher()
}
},
methods: {
preloadCarImage () {
if (this.cachedImages.car) {
this.imageLoadStatus.car = true
return
}
const img = new Image()
img.src = carImage
img.onload = () => {
this.cachedImages.car = img
this.imageLoadStatus.car = true
this.checkImagesLoadedAndRedraw()
}
img.onerror = () => {
console.error('小车图片加载失败')
this.imageLoadStatus.car = true
this.checkImagesLoadedAndRedraw()
}
},
preloadMarkerImage () {
if (this.cachedImages.marker) {
this.imageLoadStatus.marker = true
return
}
const img = new Image()
img.src = markerImage
img.onload = () => {
this.cachedImages.marker = img
this.imageLoadStatus.marker = true
this.checkImagesLoadedAndRedraw()
}
img.onerror = () => {
console.error('标记图片加载失败')
this.imageLoadStatus.marker = true
this.checkImagesLoadedAndRedraw()
}
},
loadMapImage () {
if (!this.mapData.mapImageAddress) {
return Promise.reject(new Error('地图数据缺失'))
}
if (this.cachedImages.map && this.imageLoadStatus.map) {
return Promise.resolve(this.cachedImages.map)
}
return new Promise((resolve, reject) => {
const img = new Image()
img.src = `${this.$store.getters.baseUrl}${this.mapData.mapImageAddress}`
// img.src = this.mapData.mapImageAddress
img.onload = () => {
this.cachedImages.map = img
this.imageLoadStatus.map = true
this.checkImagesLoadedAndRedraw()
resolve(img)
}
img.onerror = (error) => {
console.error('地图图片加载失败:', error)
this.imageLoadStatus.map = true
reject(error)
}
})
},
checkImagesLoadedAndRedraw () {
if (this.imageLoadStatus.map && this.imageLoadStatus.marker && this.imageLoadStatus.car) {
this.redrawCanvas()
}
},
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()
])
this.mapData = mapData
this.pathData = [...pathData]
this.pointData = this.filterPointData(pathData, stations)
this.initCanvas()
await this.loadMapImage()
this.loading.close()
} catch (e) {
this.$message.error(`数据加载失败: ${e.message || '未知错误'}`)
this.loading.close()
}
},
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')
this.canvas.width = this.mapData.width
this.canvas.height = this.mapData.height
this.redrawCanvas()
},
redrawCanvas: throttle(function () {
if (!this.ctx || !this.mapData) return
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.save()
// 绘制地图背景
if (this.cachedImages.map) {
this.ctx.drawImage(
this.cachedImages.map,
0, 0,
this.canvas.width,
this.canvas.height
)
} else {
this.ctx.fillStyle = '#ccc'
this.ctx.fillText('地图加载中...', 50, 50)
}
this.drawPath()
this.drawMarkers()
this.drawCar()
this.ctx.restore()
}, 30),
drawPath () {
if (!this.pathData.length) return
this.ctx.beginPath()
this.pathData.forEach((point, index) => {
const startX = (point.start_x - this.mapData.x) / this.mapData.resolution
const startY = this.mapData.height - (point.start_y - this.mapData.y) / this.mapData.resolution
const endX = (point.end_x - this.mapData.x) / this.mapData.resolution
const endY = this.mapData.height - (point.end_y - this.mapData.y) / this.mapData.resolution
this.ctx.moveTo(startX, startY)
this.ctx.lineTo(endX, endY)
})
this.ctx.strokeStyle = '#009de5'
this.ctx.lineWidth = 2
this.ctx.stroke()
},
drawMarkers () {
if (!this.pointData.length) return
const markerSize = 10 // 标记大小随缩放变化
this.pointData.forEach(point => {
const x = (point.x - this.mapData.x) / this.mapData.resolution
const y = this.mapData.height - (point.y - this.mapData.y) / this.mapData.resolution
// 绘制选中状态
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 if (this.cachedImages.marker) {
this.ctx.drawImage(
this.cachedImages.marker,
x - markerSize / 2, // 居中对齐
y - markerSize / 2,
markerSize,
markerSize
)
}
// 绘制点位名称(文字大小随缩放变化)
this.ctx.font = '12px Arial'
this.ctx.fillStyle = 'white'
this.ctx.textAlign = 'center'
this.ctx.fillText(point.station_name, x, y + 18)
})
},
drawCar () {
console.log(this.$store.getters.agvObj)
if (!this.agvObj || !this.cachedImages.car || !this.mapData) return
const carX = (this.agvObj.x - this.mapData.x) / this.mapData.resolution
const carY = this.mapData.height - (this.agvObj.y - this.mapData.y) / this.mapData.resolution
this.ctx.drawImage(
this.cachedImages.car,
carX - 35,
carY - 21,
70,
42
)
},
handleCanvasClick (event) {
const rect = this.canvas.getBoundingClientRect()
const mouseX = (event.clientX - rect.left) / this.scale
const mouseY = (event.clientY - rect.top) / this.scale
this.pointData.forEach(point => {
let x = (point.x - this.mapData.x) / this.mapData.resolution
let y = this.mapData.height - (point.y - this.mapData.y) / this.mapData.resolution
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()
}
})
},
handlePointSelect (point, event) {
if (this.selectedPointId === point.station_id) {
this.resetSelection()
} else {
this.selectedPointId = point.station_id
this.selectedPoint = point
this.showPopup = true
this.calculatePopupPosition(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` }
},
handleDocumentClick () {
this.resetSelection()
},
resetSelection () {
if (this.selectedPointId) {
this.selectedPointId = null
this.selectedPoint = null
this.showPopup = false
this.redrawCanvas()
}
}
}
}
</script>
<style lang="stylus" scoped>
.canvas-container
position relative
display flex
justify-content: center;
align-items: center;
height calc(100% - .32rem)
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.map_tools
position absolute
top 0
right 0
.zoom_data
width .6rem
font-size .16rem
height .32rem
line-height .32rem
color #00d9f3
text-align center
border-top 1px solid #009fde
border-bottom 1px solid #009fde
.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 150px
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); }
}
</style>

View File

@@ -0,0 +1,295 @@
<template>
<div class="point-cloud-map">
<div ref="canvasContainer" class="canvas-container"></div>
<div v-if="isLoading" class="loading-indicator">
<i class="fa fa-circle-o-notch fa-spin"></i> 加载中...
</div>
</div>
</template>
<script>
import { Application, ParticleContainer, Sprite, Graphics, Texture } from 'pixi.js'
import { mapGetters } from 'vuex'
import { points } from '../../config/point.js'
export default {
data() {
return {
// PixiJS核心对象
app: null,
pointContainer: null,
carSprite: null,
// 点云数据
allPoints: [],
pointScale: 1.0,
// 小车资源
vehicleImage: require('../../images/new/agv.png'),
carTexture: null,
// WebSocket
socket: null,
isLoading: true,
// 性能控制
lastUpdateTime: 0,
updateInterval: 800,
isDestroyed: false
}
},
computed: {
...mapGetters(['serverUrl', 'userRole', 'carPosition']),
},
watch: {
carPosition: {
handler(newVal) {
this.updateCarPosition(newVal)
},
deep: true
}
},
mounted() {
// 使用$nextTick确保DOM完全渲染后再初始化
this.$nextTick(() => {
this.initPixi()
// this.initWebSocket()
this.init()
window.addEventListener('resize', this.handleResize)
setTimeout(() => {
this.isLoading = false
}, 1000)
})
},
beforeDestroy() {
this.isDestroyed = true
// 清理WebSocket
if (this.socket) {
this.socket.close()
this.socket = null
}
// 销毁Pixi应用
if (this.app) {
this.app.destroy()
const container = this.$refs.canvasContainer
if (container && container.children.length > 0) {
container.removeChild(container.children[0])
}
this.app = null
}
// 清理资源
if (this.carTexture) {
this.carTexture.destroy()
this.carTexture = null
}
window.removeEventListener('resize', this.handleResize)
this.allPoints = []
},
methods: {
async initPixi() {
// 确保容器元素存在
const container = this.$refs.canvasContainer
if (!container) {
console.error('Canvas容器未找到')
return
}
try {
// PixiJS v8+ 使用 Application.init() 替代 new Application()
this.app = await Application.init({
width: container.clientWidth,
height: container.clientHeight,
backgroundColor: 0x0c0c0c,
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
})
// PixiJS v8+ 使用 canvas 属性替代 view 属性
if (this.app.canvas) {
container.appendChild(this.app.canvas)
} else {
console.error('PixiJS未能创建有效的canvas')
return
}
// 创建点容器 - 使用ParticleContainer提高性能
this.pointContainer = new ParticleContainer(10000, {
position: true,
rotation: true,
tint: true,
alpha: true
})
this.app.stage.addChild(this.pointContainer)
// 加载小车纹理
this.carTexture = await Texture.fromURL(this.vehicleImage)
this.initCar()
} catch (error) {
console.error('Pixi初始化失败:', error)
}
},
initCar() {
if (!this.carTexture) {
console.error('小车纹理未加载')
return
}
this.carSprite = new Sprite(this.carTexture)
this.carSprite.anchor.set(0.5) // 设置锚点为中心
this.carSprite.width = 30
this.carSprite.height = 30
this.app.stage.addChild(this.carSprite)
if (this.carPosition) {
this.updateCarPosition(this.carPosition)
}
},
updateCarPosition(position) {
if (!this.carSprite || !position) return
this.carSprite.x = position.x * this.pointScale
this.carSprite.y = position.y * this.pointScale
this.carSprite.rotation = position.angle * (Math.PI / 180) // 转换为弧度
},
initWebSocket() {
const wsHost = this.serverUrl.replace(/^https?:\/\//, '')
this.socket = new WebSocket(`ws://${wsHost}/webSocket/PointCloudData/${this.userRole}`)
this.socket.onopen = () => {
console.log('WebSocket连接已建立')
this.isLoading = false
}
this.socket.onmessage = (event) => {
if (this.isDestroyed) return
try {
const now = Date.now()
if (now - this.lastUpdateTime < this.updateInterval) return
this.lastUpdateTime = now
const pointData = JSON.parse(event.data)
this.updatePointCloud(pointData.data)
} catch (error) {
console.error('解析点云数据失败:', error)
}
}
this.socket.onclose = (event) => {
console.log('WebSocket连接已关闭代码:', event.code)
this.isLoading = true
setTimeout(() => this.initWebSocket(), 3000)
}
this.socket.onerror = (error) => {
console.error('WebSocket错误:', error)
this.isLoading = true
}
},
updatePointCloud(points) {
if (!Array.isArray(points) || points.length === 0) return
// 去重逻辑
const existingPoints = {}
this.allPoints.forEach(point => {
existingPoints[`${point.x},${point.y}`] = true
})
const newUniquePoints = points.filter(point => {
const key = `${point.x},${point.y}`
if (!existingPoints[key]) {
existingPoints[key] = true
return true
}
return false
})
this.allPoints = [...this.allPoints, ...newUniquePoints]
// 限制总点数
const maxPoints = 5000
if (this.allPoints.length > maxPoints) {
this.allPoints = this.allPoints.slice(-maxPoints)
}
// 清空容器
this.pointContainer.removeChildren()
// 添加新点 - 使用Graphics批量绘制
const graphics = new Graphics()
graphics.beginFill(0xFFFFFF, 0.8)
this.allPoints.forEach(point => {
graphics.drawCircle(
point.x * this.pointScale,
point.y * this.pointScale,
1
)
})
graphics.endFill()
this.pointContainer.addChild(graphics)
},
handleResize() {
if (!this.app || !this.$refs.canvasContainer) return
const container = this.$refs.canvasContainer
this.app.renderer.resize(
container.clientWidth,
container.clientHeight
)
},
init () {
this.isLoading = false
const pointData = points.data
this.updatePointCloud(pointData);
setTimeout(() => {
const arr = [{x: 2, y: 2}, {x: 4, y: 4}]
this.updatePointCloud(arr);
}, 2000)
}
}
}
</script>
<style lang="stylus" scoped>
.point-cloud-map
position relative
width 100%
height calc(100% - 1rem)
margin .14rem 0
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.canvas-container
width 100%
height 100%
position relative
z-index 0
.loading-indicator
position absolute
top 50%
left 50%
transform translate(-50%, -50%)
background-color rgba(255, 255, 255, 0.8)
padding .01rem .02rem
border-radius 4px
box-shadow 0 2px 10px rgba(0, 0, 0, 0.1)
font-size .12rem
color #333
z-index 100
display flex
align-items center
</style>