diff --git a/src/static/map.html b/src/static/map.html
index a1367fe..9305379 100644
--- a/src/static/map.html
+++ b/src/static/map.html
@@ -414,6 +414,10 @@
baiduGeolocation: null,
lastHeading: null, // 记录上一次的方向,用于避免频繁更新图标
updateMarkerTimer: null, // 防抖定时器
+ headingHistory: [], // 方向历史记录,用于平滑滤波(移动平均)
+ headingHistorySize: 5, // 历史记录大小(5个样本的移动平均)
+ lastOrientationUpdateTime: 0, // 上次方向更新时间,用于节流
+ headingOffset: 0, // 方向偏移量,用于校正方向偏差(度),如果需要反转方向可以设置为180
elements: {
panel: null,
tree: null,
@@ -549,7 +553,7 @@
// 开始监听位置变化(异步获取位置,不会立即有位置信息)
this.startWatchingPosition();
- // 开始监听设备方向变化
+ // 开始监听设备方向变化(使用设备方向传感器)
this.startWatchingOrientation();
},
@@ -657,6 +661,8 @@
lat: point.lat,
accuracy: accuracy
};
+
+ // 方向由设备方向传感器提供,不在这里设置
this.updateLocationMarker();
isFirstLocation = false;
@@ -702,8 +708,10 @@
point.lat
);
- // 如果位置变化超过3米,或者精度提升超过10米,则更新
- if (distance > 3 || accuracy < this.state.currentLocation.accuracy - 10) {
+ // 位置变化超过2米就更新,或者精度提升超过5米
+ if (distance > 2 ||
+ (accuracy < this.state.currentLocation.accuracy - 5) ||
+ (accuracy < 50 && this.state.currentLocation.accuracy >= 50)) {
shouldUpdate = true;
}
}
@@ -715,6 +723,8 @@
lat: point.lat,
accuracy: accuracy
};
+
+ // 方向由设备方向传感器提供,不在这里更新
this.updateLocationMarker();
}
}
@@ -754,9 +764,9 @@
point.lat
);
- // 如果位置变化超过5米,或者精度提升超过10米,则更新
- if (distance > 5 ||
- (accuracy < this.state.currentLocation.accuracy - 10) ||
+ // 位置变化超过2米就更新,或者精度提升超过5米
+ if (distance > 2 ||
+ (accuracy < this.state.currentLocation.accuracy - 5) ||
(accuracy < 50 && this.state.currentLocation.accuracy >= 50)) {
shouldUpdate = true;
}
@@ -777,15 +787,17 @@
lat: point.lat,
accuracy: accuracy
};
+
+ // 方向由设备方向传感器实时提供,不在这里更新
this.updateLocationMarker();
}
}
}
}, {
enableHighAccuracy: true, // 使用GPS精确定位
- timeout: 8000 // 8秒超时
+ timeout: 6000 // 6秒超时
});
- }, 2000); // 每2秒更新一次,实时跟踪移动
+ }, 500); // 每0.5秒更新一次,提高实时性和灵敏度
// 保存geolocation实例,以便后续使用
this.baiduGeolocation = geolocation;
@@ -1285,7 +1297,7 @@
},
/**
- * 更新定位标记(使用requestAnimationFrame实现平滑更新,避免闪烁)
+ * 更新定位标记(实时更新,优先更新位置)
*/
updateLocationMarker() {
// 确保地图已加载完成
@@ -1304,6 +1316,7 @@
}
// 使用requestAnimationFrame实现平滑更新,保持实时性
+ // 对于位置更新,立即执行;对于方向更新,异步执行
this.updateMarkerTimer = requestAnimationFrame(() => {
this.updateMarkerTimer = null;
this._doUpdateLocationMarker();
@@ -1320,35 +1333,33 @@
// 如果标记已存在,直接更新(避免删除重建)
if (this.overlays.locationMarker) {
try {
- // 更新位置(百度地图GL API支持setPosition)
+ // 优先更新位置(立即执行,不等待方向更新)
if (typeof this.overlays.locationMarker.setPosition === 'function') {
+ // 直接更新位置,实时跟随移动(不检查距离,每次都更新以确保平滑)
this.overlays.locationMarker.setPosition(point);
} else {
- // 如果不支持setPosition,检查位置是否变化
+ // 如果不支持setPosition,检查位置是否变化(降低阈值,提高灵敏度)
const currentPoint = this.overlays.locationMarker.getPosition();
+ // 降低坐标变化阈值,从0.000001(约0.1米)到0.000005(约0.5米),但实际应该总是更新
if (!currentPoint ||
- Math.abs(currentPoint.lng - lng) > 0.00001 ||
- Math.abs(currentPoint.lat - lat) > 0.00001) {
+ Math.abs(currentPoint.lng - lng) > 0.000005 ||
+ Math.abs(currentPoint.lat - lat) > 0.000005) {
// 位置变化,需要更新(但尽量不删除重建)
- const needsRecreate = true;
- if (needsRecreate) {
- // 保存当前图标,避免重新创建
- const currentIcon = this.overlays.locationMarker.getIcon();
- this.map.removeOverlay(this.overlays.locationMarker);
- this.overlays.locationMarker = new BMapGL.Marker(point, { icon: currentIcon });
- this.map.addOverlay(this.overlays.locationMarker);
- }
+ const currentIcon = this.overlays.locationMarker.getIcon();
+ this.map.removeOverlay(this.overlays.locationMarker);
+ this.overlays.locationMarker = new BMapGL.Marker(point, { icon: currentIcon });
+ this.map.addOverlay(this.overlays.locationMarker);
}
}
- // 实时更新方向(降低阈值到1度,保持实时性)
- const headingChanged = !this.lastHeading || Math.abs(this.state.currentHeading - this.lastHeading) >= 1;
+ // 实时更新方向(使用角度差值计算,考虑0-360度边界)
+ const headingChanged = !this.lastHeading || Math.abs(this.angleDifference(this.state.currentHeading, this.lastHeading)) >= 1;
if (headingChanged) {
const oldHeading = this.lastHeading;
this.lastHeading = this.state.currentHeading;
- // 实时更新图标(使用requestAnimationFrame确保平滑)
+ // 异步更新图标(使用requestAnimationFrame确保平滑,不阻塞位置更新)
requestAnimationFrame(() => {
this.createRotatedLocationIcon(this.state.currentHeading).then((icon) => {
if (this.overlays.locationMarker) {
@@ -1492,10 +1503,24 @@
// 旋转画布(heading是相对于正北的角度,需要转换为相对于画布的角度)
// 百度地图中,heading是相对于正北的角度(0-360),顺时针为正
- // 画布中,0度是向右,需要转换
- // 如果方向反了,尝试反转:使用 (90 - heading) 或 (270 - heading)
- // 或者添加180度来反转:heading + 180
- const rotation = (90 - heading) * Math.PI / 180; // 反转方向:从 (heading - 90) 改为 (90 - heading)
+ // 画布坐标系:0度向右,90度向下,180度向左,270度向上
+ // 地图坐标系:0度正北,90度正东,180度正南,270度正西
+ // 转换公式:画布角度 = 90度 - 地图角度(因为画布的0度对应地图的90度)
+
+ // 应用方向偏移量(用于校正方向偏差)
+ const adjustedHeading = this.normalizeHeading(heading + (this.headingOffset || 0));
+
+ // 转换为画布旋转角度
+ // 标准转换:画布的0度(向右)对应地图的90度(正东)
+ // 所以:画布角度 = 90度 - 地图角度
+ let rotation = (90 - adjustedHeading) * Math.PI / 180;
+
+ // 如果方向仍然不对,可以尝试以下方案(取消注释):
+ // 方案1:反转180度
+ // rotation = (270 - adjustedHeading) * Math.PI / 180;
+ // 方案2:直接使用heading(如果图标本身已经指向正确方向)
+ // rotation = -adjustedHeading * Math.PI / 180;
+
ctx.rotate(rotation);
// 绘制图片(居中,保持原始宽高比)
@@ -1512,85 +1537,306 @@
},
/**
- * 开始监听设备方向变化
+ * 平滑方向角度(处理0-360度边界问题)
+ */
+ normalizeHeading(heading) {
+ // 确保角度在0-360范围内
+ while (heading < 0) heading += 360;
+ while (heading >= 360) heading -= 360;
+ return heading;
+ },
+
+ /**
+ * 计算两个角度之间的最短差值(考虑0-360度边界)
+ */
+ angleDifference(a, b) {
+ let diff = a - b;
+ if (diff > 180) diff -= 360;
+ if (diff < -180) diff += 360;
+ return diff;
+ },
+
+ /**
+ * 移动平均滤波(减少抖动)
+ */
+ smoothHeading(rawHeading) {
+ // 初始化历史记录数组和大小(如果不存在)
+ if (!this.headingHistory) {
+ this.headingHistory = [];
+ }
+ if (!this.headingHistorySize) {
+ this.headingHistorySize = 5; // 默认5个样本的移动平均
+ }
+
+ // 添加到历史记录
+ this.headingHistory.push(rawHeading);
+ if (this.headingHistory.length > this.headingHistorySize) {
+ this.headingHistory.shift();
+ }
+
+ // 如果历史记录不足,直接返回原始值
+ if (this.headingHistory.length < 3) {
+ return rawHeading;
+ }
+
+ // 处理0-360度边界问题:计算相对于第一个值的差值
+ const baseHeading = this.headingHistory[0];
+ const normalizedHistory = this.headingHistory.map(h => {
+ let diff = this.angleDifference(h, baseHeading);
+ return baseHeading + diff;
+ });
+
+ // 计算平均值
+ const sum = normalizedHistory.reduce((a, b) => a + b, 0);
+ const avg = sum / normalizedHistory.length;
+
+ return this.normalizeHeading(avg);
+ },
+
+ /**
+ * 从四元数计算方向角(heading)- 修正版本
+ */
+ quaternionToHeading(qx, qy, qz, qw) {
+ // 四元数到欧拉角转换(提取heading/azimuth)
+ // 使用标准公式计算Z轴旋转角度(heading)
+ // 注意:四元数顺序可能是 [w, x, y, z] 或 [x, y, z, w],需要根据实际情况调整
+ // 这里假设是 [x, y, z, w] 格式
+
+ // 方法1:使用标准四元数到欧拉角转换公式(ZYX顺序)
+ // 计算heading(azimuth/yaw)- 绕Z轴旋转
+ const siny_cosp = 2 * (qw * qz + qx * qy);
+ const cosy_cosp = 1 - 2 * (qy * qy + qz * qz);
+ let heading = Math.atan2(siny_cosp, cosy_cosp) * 180 / Math.PI;
+
+ // 转换为0-360度范围
+ heading = this.normalizeHeading(heading);
+
+ // 如果方向反了,可以添加180度校正
+ // heading = (heading + 180) % 360;
+
+ return heading;
+ },
+
+ /**
+ * 开始监听设备方向变化(优化:优先使用旋转矢量传感器)
*/
startWatchingOrientation() {
// 绑定方法,确保在事件监听器中能正确访问 this
this.handleOrientation = this.handleOrientation.bind(this);
+ this.handleMotion = this.handleMotion.bind(this);
+ this.updateHeadingIfChanged = this.updateHeadingIfChanged.bind(this);
- console.log('开始监听设备方向...');
+ // 初始化方向历史记录
+ this.headingHistory = [];
+ this.lastOrientationUpdateTime = 0;
- // 检查是否支持 DeviceOrientationEvent
- if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
- // iOS 13+ 需要请求权限
- console.log('请求设备方向权限...');
- DeviceOrientationEvent.requestPermission()
- .then(response => {
- if (response === 'granted') {
- console.log('设备方向权限已授予');
- window.addEventListener('deviceorientation', this.handleOrientation);
- } else {
- console.warn('设备方向权限被拒绝');
- }
- })
- .catch(error => {
- console.error('请求设备方向权限失败:', error);
- });
- } else if (window.DeviceOrientationEvent) {
- // 其他设备直接监听
- console.log('直接监听设备方向事件');
- window.addEventListener('deviceorientation', this.handleOrientation);
- } else {
- console.log('DeviceOrientationEvent不支持,尝试使用uni-app Compass API');
- // 如果不支持,尝试使用 Compass API(uni-app)
- if (typeof plus !== 'undefined' && plus.compass) {
- this.state.watchOrientationId = plus.compass.watchHeading(
- (heading) => {
- const newHeading = heading.magneticHeading;
- if (Math.abs(newHeading - this.state.currentHeading) > 1) {
- this.state.currentHeading = newHeading;
- console.log('方向更新(来自Compass):', newHeading);
- // 只有在有位置信息时才更新标记
- if (this.state.currentLocation) {
- this.updateLocationMarker();
- }
+ console.log('开始监听设备方向(优先使用旋转矢量传感器)...');
+
+ // 1. 优先尝试使用旋转矢量传感器(通过DeviceOrientationEvent的quaternion)
+ if (window.DeviceOrientationEvent) {
+ // 检查是否支持quaternion(旋转矢量传感器)
+ const testEvent = new DeviceOrientationEvent('test', {});
+ if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
+ // iOS 13+ 需要请求权限
+ console.log('请求设备方向权限(尝试使用旋转矢量传感器)...');
+ DeviceOrientationEvent.requestPermission()
+ .then(response => {
+ if (response === 'granted') {
+ console.log('设备方向权限已授予,监听旋转矢量传感器');
+ window.addEventListener('deviceorientation', this.handleOrientation, { passive: false });
+ } else {
+ console.warn('设备方向权限被拒绝,尝试传统传感器');
+ this.fallbackToTraditionalSensor();
}
- },
- (error) => console.error('获取方向失败:', error),
- { frequency: 100 }
- );
+ })
+ .catch(error => {
+ console.error('请求设备方向权限失败:', error);
+ this.fallbackToTraditionalSensor();
+ });
} else {
- console.warn('设备方向功能不可用');
+ // 其他设备直接监听
+ console.log('直接监听设备方向事件(尝试旋转矢量传感器)');
+ window.addEventListener('deviceorientation', this.handleOrientation, { passive: false });
+ }
+ } else {
+ // 2. 尝试使用DeviceMotionEvent(可能包含旋转矢量数据)
+ if (window.DeviceMotionEvent) {
+ console.log('尝试使用DeviceMotionEvent(旋转矢量传感器)');
+ if (typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function') {
+ DeviceMotionEvent.requestPermission()
+ .then(response => {
+ if (response === 'granted') {
+ window.addEventListener('devicemotion', this.handleMotion, { passive: false });
+ } else {
+ this.fallbackToTraditionalSensor();
+ }
+ })
+ .catch(() => this.fallbackToTraditionalSensor());
+ } else {
+ window.addEventListener('devicemotion', this.handleMotion, { passive: false });
+ }
+ } else {
+ // 3. 降级到传统传感器
+ console.log('旋转矢量传感器不可用,使用传统传感器');
+ this.fallbackToTraditionalSensor();
}
}
},
/**
- * 处理设备方向变化
+ * 处理DeviceMotion事件(可能包含旋转矢量数据)
+ */
+ handleMotion(event) {
+ const now = Date.now();
+ if (now - this.lastOrientationUpdateTime < 50) {
+ return;
+ }
+ this.lastOrientationUpdateTime = now;
+
+ // 检查是否有旋转速率数据(融合传感器)
+ if (event.rotationRate) {
+ const { alpha, beta, gamma } = event.rotationRate;
+ if (alpha !== null && alpha !== undefined && !isNaN(alpha)) {
+ // 使用旋转速率计算方向(需要积分,这里简化处理)
+ // 实际应用中,旋转矢量传感器通常通过DeviceOrientationEvent的quaternion提供
+ const rawHeading = this.normalizeHeading(alpha);
+ const smoothedHeading = this.smoothHeading(rawHeading);
+ this.updateHeadingIfChanged(smoothedHeading);
+ }
+ }
+ },
+
+ /**
+ * 降级到传统传感器
+ */
+ fallbackToTraditionalSensor() {
+ console.log('使用传统方向传感器(DeviceOrientationEvent alpha)');
+ // 使用传统的DeviceOrientationEvent(如果没有quaternion)
+ if (window.DeviceOrientationEvent) {
+ if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
+ DeviceOrientationEvent.requestPermission()
+ .then(response => {
+ if (response === 'granted') {
+ window.addEventListener('deviceorientation', this.handleOrientation, { passive: false });
+ } else {
+ this.fallbackToCompassAPI();
+ }
+ })
+ .catch(() => this.fallbackToCompassAPI());
+ } else {
+ window.addEventListener('deviceorientation', this.handleOrientation, { passive: false });
+ }
+ } else {
+ this.fallbackToCompassAPI();
+ }
+ },
+
+ /**
+ * 更新方向(如果变化足够大)
+ */
+ updateHeadingIfChanged(newHeading) {
+ const oldHeading = this.state.currentHeading;
+ const diff = Math.abs(this.angleDifference(newHeading, oldHeading));
+
+ if (diff >= 1 || oldHeading === 0) {
+ this.state.currentHeading = newHeading;
+ if (this.state.currentLocation) {
+ requestAnimationFrame(() => {
+ this.updateLocationMarker();
+ });
+ }
+ }
+ },
+
+ /**
+ * 降级到Compass API(uni-app)
+ */
+ fallbackToCompassAPI() {
+ if (typeof plus !== 'undefined' && plus.compass) {
+ this.state.watchOrientationId = plus.compass.watchHeading(
+ (heading) => {
+ const rawHeading = heading.magneticHeading;
+ // 使用平滑滤波处理Compass数据
+ const smoothedHeading = this.smoothHeading(rawHeading);
+ const oldHeading = this.state.currentHeading;
+ const diff = Math.abs(this.angleDifference(smoothedHeading, oldHeading));
+
+ // 只有当方向变化超过1度时才更新
+ if (diff >= 1 || oldHeading === 0) {
+ this.state.currentHeading = smoothedHeading;
+ // 只有在有位置信息时才更新标记
+ if (this.state.currentLocation) {
+ requestAnimationFrame(() => {
+ this.updateLocationMarker();
+ });
+ }
+ }
+ },
+ (error) => console.error('获取方向失败:', error),
+ { frequency: 100 } // 100ms更新一次
+ );
+ } else {
+ console.warn('设备方向功能完全不可用');
+ }
+ },
+
+ /**
+ * 处理设备方向变化(优化:优先使用旋转矢量传感器)
*/
handleOrientation(event) {
- // alpha: 绕Z轴旋转(指南针方向,0-360度)
- // 注意:某些设备可能使用不同的属性
- let newHeading = null;
+ const now = Date.now();
+ // 节流:限制更新频率到每50ms一次(20Hz),平衡实时性和性能
+ if (now - this.lastOrientationUpdateTime < 50) {
+ return;
+ }
+ this.lastOrientationUpdateTime = now;
+
+ let rawHeading = null;
- if (event.alpha !== null && event.alpha !== undefined && !isNaN(event.alpha)) {
- newHeading = event.alpha;
- } else if (event.webkitCompassHeading !== null && event.webkitCompassHeading !== undefined) {
- // iOS Safari 可能使用 webkitCompassHeading
- newHeading = event.webkitCompassHeading;
+ // 1. 优先使用旋转矢量传感器(四元数数据)- 最稳定
+ if (event.quaternion && Array.isArray(event.quaternion) && event.quaternion.length >= 4) {
+ // 旋转矢量传感器提供的四元数数据 [x, y, z, w]
+ const [qx, qy, qz, qw] = event.quaternion;
+ rawHeading = this.quaternionToHeading(qx, qy, qz, qw);
+ console.log('使用旋转矢量传感器(四元数):', rawHeading.toFixed(1), '度');
+ }
+ // 2. 检查是否有quaternion属性(某些浏览器可能以对象形式提供)
+ else if (event.quaternion && typeof event.quaternion === 'object') {
+ const q = event.quaternion;
+ if (q.x !== undefined && q.y !== undefined && q.z !== undefined && q.w !== undefined) {
+ rawHeading = this.quaternionToHeading(q.x, q.y, q.z, q.w);
+ console.log('使用旋转矢量传感器(四元数对象):', rawHeading.toFixed(1), '度');
+ }
+ }
+ // 3. 降级到传统传感器(alpha)
+ else if (event.alpha !== null && event.alpha !== undefined && !isNaN(event.alpha)) {
+ // event.alpha 是绕Z轴旋转的角度(0-360度)
+ // 在某些设备上,alpha是逆时针的,需要转换为顺时针
+ // 根据设备类型调整:Android通常需要反转,iOS可能不需要
+ // 先尝试不反转
+ rawHeading = event.alpha;
+
+ // 如果方向反了,可以尝试反转(取消下面的注释):
+ // rawHeading = (360 - event.alpha) % 360;
+
+ // 或者使用headingOffset来校正(更灵活)
+ // this.headingOffset = 180; // 在初始化时设置
+ }
+ // 4. iOS Safari 使用 webkitCompassHeading
+ else if (event.webkitCompassHeading !== null && event.webkitCompassHeading !== undefined) {
+ rawHeading = event.webkitCompassHeading;
+ }
+ // 5. 某些平板设备使用 absolute 模式
+ else if (event.absolute !== undefined && event.absolute && event.alpha !== null) {
+ rawHeading = (360 - event.alpha) % 360;
}
- if (newHeading !== null && !isNaN(newHeading)) {
- // 只有当方向变化超过1度时才更新,避免频繁更新
- const oldHeading = this.state.currentHeading;
- if (Math.abs(newHeading - oldHeading) > 1 || oldHeading === 0) {
- this.state.currentHeading = newHeading;
- console.log('方向更新:', newHeading, '度');
- // 只有在有位置信息时才更新标记
- if (this.state.currentLocation) {
- this.updateLocationMarker();
- }
- }
+ if (rawHeading !== null && !isNaN(rawHeading)) {
+ // 使用移动平均滤波平滑方向数据(减少抖动)
+ const smoothedHeading = this.smoothHeading(rawHeading);
+
+ // 更新方向(如果变化足够大)
+ this.updateHeadingIfChanged(smoothedHeading);
}
},
@@ -1653,10 +1899,19 @@
this.state.watchOrientationId = null;
}
- // 移除方向事件监听
+ // 移除方向事件监听(包括旋转矢量传感器和传统传感器)
if (this.handleOrientation) {
window.removeEventListener('deviceorientation', this.handleOrientation);
}
+ if (this.handleMotion) {
+ window.removeEventListener('devicemotion', this.handleMotion);
+ }
+
+ // 清理方向历史记录
+ if (this.headingHistory) {
+ this.headingHistory = [];
+ }
+ this.lastOrientationUpdateTime = 0;
// 移除定位标记
if (this.overlays.locationMarker && this.map) {