优化小车平滑行驶

This commit is contained in:
renna 2025-07-16 11:30:24 +08:00
parent a5317cb848
commit 01088e3c9d
4 changed files with 235 additions and 336 deletions

View File

@ -0,0 +1,67 @@
# 车辆平滑移动优化需求文档
## 介绍
当前地图上的车辆移动存在"平滑行驶一小段然后停顿再平滑行驶"的问题,需要优化为完全平滑的连续行驶效果。系统每秒接收一次车辆位置数据,但当前的动画实现在数据间隔期间会出现停顿现象,影响用户体验。
## 需求
### 需求 1
**用户故事:** 作为系统操作员,我希望地图上的车辆能够完全平滑地移动,这样我可以更直观地监控车辆的实时状态。
#### 验收标准
1. WHEN 系统接收到车辆位置更新数据 THEN 车辆应该平滑地从当前位置移动到新位置
2. WHEN 两次位置数据之间存在时间间隔 THEN 车辆应该继续保持平滑移动而不是停顿
3. WHEN 车辆速度发生变化 THEN 移动速度应该平滑过渡而不是突然改变
### 需求 2
**用户故事:** 作为系统操作员,我希望车辆的移动轨迹能够预测性地延续,这样即使在数据传输延迟时也能保持视觉连续性。
#### 验收标准
1. WHEN 超过预设时间没有接收到新的位置数据 THEN 系统应该基于历史轨迹预测车辆移动方向
2. WHEN 接收到新的位置数据 THEN 预测轨迹应该平滑地调整到实际位置
3. WHEN 车辆停止移动 THEN 预测移动应该逐渐减速直至停止
### 需求 3
**用户故事:** 作为系统操作员,我希望车辆的旋转角度变化也能平滑过渡,这样车辆转向时看起来更自然。
#### 验收标准
1. WHEN 车辆heading角度发生变化 THEN 车辆图标应该平滑旋转到新角度
2. WHEN 角度变化较大时 THEN 应该选择最短旋转路径进行过渡
3. WHEN 车辆移动方向与heading不一致时 THEN 应该优先使用实际移动方向进行旋转
### 需求 4
**用户故事:** 作为系统操作员,我希望动画系统能够自适应不同的数据接收频率,这样无论网络状况如何都能保持平滑效果。
#### 验收标准
1. WHEN 数据接收频率为每秒1次 THEN 动画应该在1秒内平滑完成位置过渡
2. WHEN 数据接收出现延迟 THEN 动画应该自动调整插值时间以保持平滑
3. WHEN 数据接收频率变化 THEN 动画参数应该动态调整以适应新频率
### 需求 5
**用户故事:** 作为系统操作员,我希望车辆标签能够始终跟随车辆图标移动,这样我可以持续看到车辆的实时信息。
#### 验收标准
1. WHEN 车辆图标移动时 THEN 车辆标签应该同步移动
2. WHEN 车辆处于动画移动状态 THEN 标签位置应该实时更新
3. WHEN 地图缩放或平移时 THEN 标签应该保持正确的相对位置
### 需求 6
**用户故事:** 作为系统操作员,我希望动画系统具有良好的性能表现,这样即使有多辆车同时移动也不会影响系统响应速度。
#### 验收标准
1. WHEN 同时有多辆车进行动画移动 THEN 系统帧率应该保持在30fps以上
2. WHEN 动画运行时 THEN CPU使用率不应该显著增加
3. WHEN 车辆数量增加时 THEN 动画性能应该保持稳定

View File

@ -19,6 +19,15 @@ export function exportCarRunInfo(query) {
})
}
// 批量查询车辆轨迹
export function batchQueryVehicleTrajectory(data) {
return request({
url: '/system/vehicle_location/trajectory/batch',
method: 'post',
data: data
})
}
// 根据车辆ID查询车辆运动信息列表
export function listCarRunInfoByVehicleId(vehicleId, query) {
return request({
@ -28,6 +37,42 @@ export function listCarRunInfoByVehicleId(vehicleId, query) {
})
}
// 根据车辆ID查询车辆轨迹
export function getVehicleTrajectory(vehicleId, query) {
return request({
url: '/system/vehicle_location/trajectory/' + vehicleId,
method: 'get',
params: query
})
}
// 根据车辆ID查询轨迹统计信息
export function getTrajectoryStatistics(vehicleId, query) {
return request({
url: '/system/vehicle_location/trajectory/' + vehicleId + '/statistics',
method: 'get',
params: query
})
}
// 根据车辆ID查询轨迹回放数据
export function getTrajectoryPlayback(vehicleId, query) {
return request({
url: '/system/vehicle_location/trajectory/' + vehicleId + '/playback',
method: 'get',
params: query
})
}
// 根据车辆ID查询区域内轨迹
export function getAreaTrajectory(vehicleId, query) {
return request({
url: '/system/vehicle_location/trajectory/' + vehicleId + '/area',
method: 'get',
params: query
})
}
// 根据车牌号查询车辆运动信息列表
export function listCarRunInfoByLicensePlate(licensePlate, query) {
return request({

View File

@ -1,4 +1,4 @@
<!-- TrackPlayback.vue轨迹回放页面UI还原 -->
<!-- TrackPlayback.vue轨迹回放页面 -->
<template>
<div class="track-playback-content">
<!-- 左侧搜索+列表 -->

View File

@ -22,21 +22,15 @@ const vehicleMotionHistory = ref({}); // 存储车辆移动历史
const PREDICTION_THRESHOLD = 50; // 100ms50ms
const MAX_PREDICTION_TIME = 15000; // 10000ms15000ms
//
const ACCELERATION = 0.15; //
const DECELERATION = 0.25; //
const INERTIA_FACTOR = 0.99; // 0.9850.99
const PREDICTION_STRENGTH = 3; // 43
const SPEED_SMOOTHING = 0.98; // 0.970.98
const POSITION_SMOOTHING = 0.06; // 0.080.06
const MIN_MOVE_THRESHOLD = 0.000005; // 0.000010.000005
const CONTINUOUS_MOVEMENT = true; //
const MIN_SPEED = 0.5; // 1.00.5
const PREDICTION_DECAY_RATE = 0.9995; // 0.9990.9995
const PATH_PREDICTION_ENABLED = true; //
const PATH_PREDICTION_POINTS = 15; // 2015
const CONTINUOUS_MOVEMENT_THRESHOLD = 200; // 300ms200ms
const POSITION_UPDATE_THRESHOLD = 3; // 53
// -
const SPEED_SMOOTHING = 0.3; //
const POSITION_SMOOTHING = 1.0; // 1.0
const MIN_MOVE_THRESHOLD = 0.0000001; //
const CONTINUOUS_MOVEMENT = true; //
const MIN_SPEED = 1.0; //
const PREDICTION_DECAY_RATE = 0.99; //
const CONTINUOUS_MOVEMENT_THRESHOLD = 50; //
const INTERPOLATION_STEPS = 60; // 60
// -
function easeInOutQuad(t) {
@ -78,7 +72,7 @@ function startAnimationLoop() {
animationFrameId.value = requestAnimationFrame(animate);
}
//
// -
function updateVehicleAnimations(deltaTime) {
if (!props.vehicleSource || !props.map) return;
@ -91,144 +85,63 @@ function updateVehicleAnimations(deltaTime) {
const animData = vehicleAnimations.value[id];
if (!animData) return;
// 使
//
ensureContinuousMovement(animData, vehicle, currentTime, deltaTime);
const timeSinceLastUpdate = currentTime - animData.lastUpdated;
// 1.
let targetPosition = [...animData.targetPosition];
// -
//
if (animData.currentSpeed === undefined) {
animData.currentSpeed = animData.speed;
} else {
// 使
const speedSmoothingFactor = vehicle.speedViolation ? 0.98 : SPEED_SMOOTHING;
animData.currentSpeed = animData.currentSpeed * speedSmoothingFactor +
animData.speed * (1 - speedSmoothingFactor);
animData.currentSpeed = animData.currentSpeed * SPEED_SMOOTHING +
animData.speed * (1 - SPEED_SMOOTHING);
}
let currentSpeed = animData.currentSpeed;
let currentSpeed = Math.max(animData.currentSpeed, MIN_SPEED);
//
if (timeSinceLastUpdate > PREDICTION_THRESHOLD && animData.predictionVector) {
//
const predictTime = Math.min(timeSinceLastUpdate, MAX_PREDICTION_TIME);
const predictFactor = (predictTime - PREDICTION_THRESHOLD) / 1000 * 0.6; // 0.80.6
//
const speedFactor = Math.min(1.0, currentSpeed / 30); // 1.2/251.0/30
//
const predictedPosition = [
animData.targetPosition[0] + animData.predictionVector[0] * predictFactor * speedFactor,
animData.targetPosition[1] + animData.predictionVector[1] * predictFactor * speedFactor
];
//
targetPosition = predictedPosition;
//
if (predictTime > 2000) {
//
const slowdownFactor = Math.max(0.6, 1 - (predictTime - 2000) / 15000); // 0.5/100000.6/15000
currentSpeed = Math.max(currentSpeed * slowdownFactor, MIN_SPEED); // 0
}
}
// 2.
const dx = targetPosition[0] - animData.position[0];
const dy = targetPosition[1] - animData.position[1];
//
const dx = animData.targetPosition[0] - animData.position[0];
const dy = animData.targetPosition[1] - animData.position[1];
const distance = Math.sqrt(dx * dx + dy * dy);
//
if (distance < MIN_MOVE_THRESHOLD && !CONTINUOUS_MOVEMENT) {
return;
}
// 3.
if (distance > MIN_MOVE_THRESHOLD || CONTINUOUS_MOVEMENT) {
//
//
const idealDistance = (currentSpeed * 1000 / 3600) * (deltaTime / 1000);
//
let moveRatio;
// -
let moveDistance;
// 使
const positionSmoothingFactor = vehicle.speedViolation ? POSITION_SMOOTHING * 0.7 : POSITION_SMOOTHING;
if (distance < idealDistance * 1.2) { // 1.51.2
// 使
moveRatio = Math.min(positionSmoothingFactor * 1.2, idealDistance / distance); // 1.51.2
moveRatio = easeOutQuint(moveRatio);
//
if (distance > 0.0001) {
//
if (distance > idealDistance) {
//
moveDistance = idealDistance;
} else {
// 使
moveRatio = Math.min(positionSmoothingFactor, idealDistance / distance);
moveRatio = easeOutCubic(moveRatio);
// 使
// 16ms60fps
moveDistance = Math.max(distance * 0.5, idealDistance * 0.3);
}
} else {
// 使
moveDistance = idealDistance * 0.8;
}
//
// 使
if (distance < MIN_MOVE_THRESHOLD && animData.predictionVector) {
// 使使
const minMoveAmount = (currentSpeed * 1000 / 3600) * (deltaTime / 1000) * 0.05; // 0.10.05
//
if (distance > 0.0001) {
const moveRatio = Math.min(moveDistance / distance, 1.0);
//
//
animData.position[0] += dx * moveRatio;
animData.position[1] += dy * moveRatio;
} else if (animData.predictionVector) {
// 使
const predLen = Math.sqrt(
animData.predictionVector[0] * animData.predictionVector[0] +
animData.predictionVector[1] * animData.predictionVector[1]
);
if (predLen > 0) {
const normPredX = animData.predictionVector[0] / predLen;
const normPredY = animData.predictionVector[1] / predLen;
//
animData.position[0] += normPredX * minMoveAmount;
animData.position[1] += normPredY * minMoveAmount;
//
animData.lastDx = normPredX * minMoveAmount;
animData.lastDy = normPredY * minMoveAmount;
}
}
//
else {
if (animData.lastDx !== undefined && animData.lastDy !== undefined) {
// 使
const inertiaFactor = vehicle.speedViolation ? INERTIA_FACTOR * 1.05 : INERTIA_FACTOR; // 1.11.05
const inertiaX = animData.lastDx * inertiaFactor;
const inertiaY = animData.lastDy * inertiaFactor;
//
const newDx = dx * moveRatio;
const newDy = dy * moveRatio;
//
const inertiaWeight = Math.min(0.75, currentSpeed / 80) * // 0.85/700.75/80
Math.min(0.9, distance / 10); // 1.0/80.9/10
//
const nextX = animData.position[0] + newDx * (1 - inertiaWeight) + inertiaX * inertiaWeight;
const nextY = animData.position[1] + newDy * (1 - inertiaWeight) + inertiaY * inertiaWeight;
//
animData.position[0] = nextX;
animData.position[1] = nextY;
//
animData.lastDx = nextX - animData.position[0];
animData.lastDy = nextY - animData.position[1];
} else {
//
animData.position[0] += dx * moveRatio;
animData.position[1] += dy * moveRatio;
//
animData.lastDx = dx * moveRatio;
animData.lastDy = dy * moveRatio;
animData.position[0] += (animData.predictionVector[0] / predLen) * moveDistance;
animData.position[1] += (animData.predictionVector[1] / predLen) * moveDistance;
}
}
@ -237,30 +150,26 @@ function updateVehicleAnimations(deltaTime) {
if (feature) {
feature.getGeometry().setCoordinates(animData.position);
//
//
if (props.vehicles[id]) {
props.vehicles[id].position = animData.position;
}
// heading
if (feature && props.getVehicleStyle) {
// heading
const vehicle = props.vehicles[id];
//
if (props.getVehicleStyle) {
const currentHeading = vehicle ? vehicle.heading : 0;
// heading
if (!animData.lastHeading || Math.abs(animData.lastHeading - currentHeading) > 1) {
feature.setStyle(props.getVehicleStyle(id, currentSpeed, currentHeading));
animData.lastHeading = currentHeading;
}
}
}
}
//
//
animData.currentSpeed = currentSpeed;
//
//
updateVehiclePathHistory(id, animData, currentTime);
});
}
@ -282,8 +191,8 @@ function updateVehiclePathHistory(id, animData, currentTime) {
speed: animData.currentSpeed
});
//
if (animData.pathHistory.length > PATH_PREDICTION_POINTS + 4) {
// 10
if (animData.pathHistory.length > 10) {
animData.pathHistory.shift();
}
@ -291,92 +200,46 @@ function updateVehiclePathHistory(id, animData, currentTime) {
}
}
// 使
// 使 -
function ensureContinuousMovement(animData, vehicle, currentTime, deltaTime) {
// 使
const timeSinceLastUpdate = currentTime - animData.lastUpdated;
// 0
//
if (timeSinceLastUpdate > CONTINUOUS_MOVEMENT_THRESHOLD && animData.currentSpeed > 0) {
// 使
if (PATH_PREDICTION_ENABLED && animData.pathHistory && animData.pathHistory.length >= 3) {
const predictedPath = predictFuturePath(animData);
if (predictedPath) {
//
//
if (!animData.predictionVector) {
animData.predictionVector = predictedPath.vector;
} else {
// 使
animData.predictionVector = [
animData.predictionVector[0] * 0.8 + predictedPath.vector[0] * 0.2, // 0.7/0.30.8/0.2
animData.predictionVector[1] * 0.8 + predictedPath.vector[1] * 0.2
];
}
// 使
animData.targetPosition = [
animData.targetPosition[0] * 0.8 + predictedPath.position[0] * 0.2, // 0.7/0.30.8/0.2
animData.targetPosition[1] * 0.8 + predictedPath.position[1] * 0.2
];
}
}
//
else if (!animData.predictionVector ||
(Math.abs(animData.predictionVector[0]) < 0.001 && Math.abs(animData.predictionVector[1]) < 0.001)) {
// 使
// 使
if (animData.pathHistory && animData.pathHistory.length >= 2) {
const latestPoints = animData.pathHistory.slice(-2);
const dx = latestPoints[1].position[0] - latestPoints[0].position[0];
const dy = latestPoints[1].position[1] - latestPoints[0].position[1];
const latest = animData.pathHistory[animData.pathHistory.length - 1];
const previous = animData.pathHistory[animData.pathHistory.length - 2];
const dx = latest.position[0] - previous.position[0];
const dy = latest.position[1] - previous.position[1];
const len = Math.sqrt(dx * dx + dy * dy);
if (len > 0.001) {
const normalizedDx = dx / len;
const normalizedDy = dy / len;
const predX = normalizedDx * animData.currentSpeed * 0.18;
const predY = normalizedDy * animData.currentSpeed * 0.18;
animData.predictionVector = [predX, predY];
//
animData.targetPosition = [
animData.position[0] + predX * 15,
animData.position[1] + predY * 15
];
if (len > 0) {
//
animData.predictionVector = [dx / len, dy / len];
} else {
// 使
animData.predictionVector = [0, animData.currentSpeed * 0.18];
animData.targetPosition = [
animData.position[0],
animData.position[1] + animData.currentSpeed * 0.18 * 15
];
//
animData.predictionVector = [0, 1];
}
} else {
// 使
animData.predictionVector = [0, animData.currentSpeed * 0.18];
animData.targetPosition = [
animData.position[0],
animData.position[1] + animData.currentSpeed * 0.18 * 15
];
//
animData.predictionVector = [0, 1];
}
} else {
//
// 使
animData.predictionVector = [
animData.predictionVector[0] * PREDICTION_DECAY_RATE,
animData.predictionVector[1] * PREDICTION_DECAY_RATE
];
//
animData.targetPosition = [
animData.position[0] + animData.predictionVector[0] * 15, // 1215
animData.position[1] + animData.predictionVector[1] * 15
];
}
//
if (!animData.lastSpeedReduction || currentTime - animData.lastSpeedReduction > 1000) {
animData.speed = Math.max(animData.speed * 0.98, MIN_SPEED); // 0.970.98
//
const moveDistance = (animData.currentSpeed * 1000 / 3600) * 1.0; // 1
animData.targetPosition = [
animData.position[0] + animData.predictionVector[0] * moveDistance,
animData.position[1] + animData.predictionVector[1] * moveDistance
];
//
if (!animData.lastSpeedReduction || currentTime - animData.lastSpeedReduction > 2000) {
animData.speed = Math.max(animData.speed * 0.95, MIN_SPEED);
animData.lastSpeedReduction = currentTime;
}
}
@ -481,124 +344,48 @@ function initVehicleAnimation(id, coordinates, heading, speed) {
vehicleMotionHistory.value[id] = [];
}
//
// -
function updateVehicleAnimationTarget(id, coordinates, heading, speed) {
const animData = vehicleAnimations.value[id] || {};
const now = Date.now();
//
vehicleMotionHistory.value[id] = vehicleMotionHistory.value[id] || [];
vehicleMotionHistory.value[id].push({
time: now,
position: coordinates,
heading: heading, // heading
speed: speed
});
//
if (vehicleMotionHistory.value[id].length > 10) { // 810
vehicleMotionHistory.value[id].shift();
}
// - 使
//
let predictionVector = [0, 0];
if (vehicleMotionHistory.value[id].length >= 2) {
// - 使
const history = vehicleMotionHistory.value[id];
const latest = history[history.length - 1];
const previous = history[history.length - 2];
//
if (animData.position) {
const dx = coordinates[0] - animData.position[0];
const dy = coordinates[1] - animData.position[1];
const distance = Math.sqrt(dx * dx + dy * dy);
const dt = Math.max(1, latest.time - previous.time);
const dx = (latest.position[0] - previous.position[0]);
const dy = (latest.position[1] - previous.position[1]);
// -
const baseSpeed = Math.sqrt(dx*dx + dy*dy) * (1000 / dt); //
const normalizedDx = baseSpeed > 0 ? dx / baseSpeed : 0;
const normalizedDy = baseSpeed > 0 ? dy / baseSpeed : 0;
//
predictionVector = [
normalizedDx * (PREDICTION_STRENGTH * 0.9) * baseSpeed,
normalizedDy * (PREDICTION_STRENGTH * 0.9) * baseSpeed
];
// 使
if (vehicleMotionHistory.value[id].length >= 3) {
// 使
let totalWeight = 0;
let weightedDx = 0;
let weightedDy = 0;
// 使
for (let i = history.length - 2; i >= 0 && i >= history.length - 7; i--) { // 57
const curr = history[i+1];
const prev = history[i];
const pointDt = Math.max(1, curr.time - prev.time);
const pointDx = curr.position[0] - prev.position[0];
const pointDy = curr.position[1] - prev.position[1];
// 使
const weight = Math.pow(0.85, (history.length - i - 1));
totalWeight += weight;
weightedDx += pointDx * weight;
weightedDy += pointDy * weight;
}
if (totalWeight > 0) {
//
weightedDx /= totalWeight;
weightedDy /= totalWeight;
//
predictionVector = [
predictionVector[0] * 0.6 + weightedDx * PREDICTION_STRENGTH * 0.4, // 0.7/0.30.6/0.4
predictionVector[1] * 0.6 + weightedDy * PREDICTION_STRENGTH * 0.4
];
if (distance > 0.0001) {
//
predictionVector = [dx / distance, dy / distance];
}
}
// 使线
const speedFactor = Math.min(1.8, Math.pow(speed / 15, 0.8)); // 使使线
predictionVector = [
predictionVector[0] * speedFactor,
predictionVector[1] * speedFactor
];
//
if (speed < 2) {
predictionVector = [
predictionVector[0] * 0.4, // 0.30.4
predictionVector[1] * 0.4
];
}
}
//
//
if (!animData.speedHistory) {
animData.speedHistory = [speed, speed, speed, speed]; //
animData.speedHistory = [speed, speed, speed];
} else {
animData.speedHistory.push(speed);
if (animData.speedHistory.length > 4) { // 34
if (animData.speedHistory.length > 3) {
animData.speedHistory.shift();
}
}
//
//
const smoothedSpeed = animData.speedHistory.reduce((sum, s) => sum + s, 0) /
animData.speedHistory.length;
// -
//
vehicleAnimations.value[id] = {
...animData,
targetPosition: coordinates,
targetHeading: heading, // heading
heading: animData.heading || heading, // heading使heading
targetHeading: heading,
heading: animData.heading || heading,
position: animData.position || coordinates,
speed: smoothedSpeed, // 使
speed: smoothedSpeed,
lastUpdated: now,
predictionVector,
speedHistory: animData.speedHistory