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) {