Преглед на файлове

feat: add magnetic declination north reference modes

zhangyan преди 2 седмици
родител
ревизия
8e6291885d
променени са 4 файла, в които са добавени 209 реда и са изтрити 33 реда
  1. 185 28
      miniprogram/engine/map/mapEngine.ts
  2. 7 1
      miniprogram/pages/map/map.ts
  3. 8 4
      miniprogram/pages/map/map.wxml
  4. 9 0
      miniprogram/pages/map/map.wxss

+ 185 - 28
miniprogram/engine/map/mapEngine.ts

@@ -7,6 +7,8 @@ import { worldTileToLonLat, type LonLatPoint } from '../../utils/projection'
 const RENDER_MODE = 'Single WebGL Pipeline'
 const PROJECTION_MODE = 'WGS84 -> WorldTile -> Camera -> Screen'
 const MAP_NORTH_OFFSET_DEG = 0
+const MAGNETIC_DECLINATION_DEG = -6.91
+const MAGNETIC_DECLINATION_TEXT = '6.91° W'
 const MIN_ZOOM = 15
 const MAX_ZOOM = 20
 const DEFAULT_ZOOM = 17
@@ -25,12 +27,13 @@ const INERTIA_MIN_SPEED = 0.02
 const PREVIEW_RESET_DURATION_MS = 140
 const UI_SYNC_INTERVAL_MS = 80
 const ROTATE_STEP_DEG = 15
-const AUTO_ROTATE_FRAME_MS = 10
-const AUTO_ROTATE_EASE = 0.24
+const AUTO_ROTATE_FRAME_MS = 8
+const AUTO_ROTATE_EASE = 0.34
 const AUTO_ROTATE_SNAP_DEG = 0.1
-const AUTO_ROTATE_DEADZONE_DEG = 5
-const AUTO_ROTATE_MAX_STEP_DEG = 0.9
+const AUTO_ROTATE_DEADZONE_DEG = 4
+const AUTO_ROTATE_MAX_STEP_DEG = 0.75
 const AUTO_ROTATE_HEADING_SMOOTHING = 0.32
+const COMPASS_NEEDLE_SMOOTHING = 0.12
 
 const SAMPLE_TRACK_WGS84: LonLatPoint[] = [
   worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X - 0.72, y: DEFAULT_CENTER_TILE_Y + 0.44 }, DEFAULT_ZOOM),
@@ -49,6 +52,9 @@ type GestureMode = 'idle' | 'pan' | 'pinch'
 type RotationMode = 'manual' | 'auto'
 type OrientationMode = 'manual' | 'north-up' | 'heading-up'
 type AutoRotateSourceMode = 'sensor' | 'course' | 'fusion'
+type NorthReferenceMode = 'magnetic' | 'true'
+
+const DEFAULT_NORTH_REFERENCE_MODE: NorthReferenceMode = 'magnetic'
 
 export interface MapEngineStageRect {
   width: number
@@ -73,6 +79,8 @@ export interface MapEngineViewState {
   orientationMode: OrientationMode
   orientationModeText: string
   sensorHeadingText: string
+  compassDeclinationText: string
+  northReferenceButtonText: string
   autoRotateSourceText: string
   autoRotateCalibrationText: string
   northReferenceText: string
@@ -120,6 +128,8 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
   'orientationMode',
   'orientationModeText',
   'sensorHeadingText',
+  'compassDeclinationText',
+  'northReferenceButtonText',
   'autoRotateSourceText',
   'autoRotateCalibrationText',
   'northReferenceText',
@@ -247,16 +257,84 @@ function formatAutoRotateCalibrationText(pending: boolean, offsetDeg: number | n
   return `Offset ${Math.round(normalizeRotationDeg(offsetDeg))}deg`
 }
 
-function formatNorthReferenceText(): string {
-  return 'Map North = 0deg (TFW aligned)'
+function getTrueHeadingDeg(magneticHeadingDeg: number): number {
+  return normalizeRotationDeg(magneticHeadingDeg + MAGNETIC_DECLINATION_DEG)
 }
 
-function formatCompassNeedleDeg(headingDeg: number | null): number {
-  if (headingDeg === null) {
+function getMagneticHeadingDeg(trueHeadingDeg: number): number {
+  return normalizeRotationDeg(trueHeadingDeg - MAGNETIC_DECLINATION_DEG)
+}
+
+function getMapNorthOffsetDeg(_mode: NorthReferenceMode): number {
+  return MAP_NORTH_OFFSET_DEG
+}
+
+function getCompassReferenceHeadingDeg(mode: NorthReferenceMode, magneticHeadingDeg: number): number {
+  if (mode === 'true') {
+    return getTrueHeadingDeg(magneticHeadingDeg)
+  }
+
+  return normalizeRotationDeg(magneticHeadingDeg)
+}
+
+function getMapReferenceHeadingDegFromSensor(mode: NorthReferenceMode, magneticHeadingDeg: number): number {
+  if (mode === 'magnetic') {
+    return normalizeRotationDeg(magneticHeadingDeg)
+  }
+
+  return getTrueHeadingDeg(magneticHeadingDeg)
+}
+
+function getMapReferenceHeadingDegFromCourse(mode: NorthReferenceMode, trueHeadingDeg: number): number {
+  if (mode === 'magnetic') {
+    return getMagneticHeadingDeg(trueHeadingDeg)
+  }
+
+  return normalizeRotationDeg(trueHeadingDeg)
+}
+
+function formatNorthReferenceText(mode: NorthReferenceMode): string {
+  if (mode === 'magnetic') {
+    return `Compass Magnetic / Heading-Up Magnetic (${MAGNETIC_DECLINATION_TEXT})`
+  }
+
+  return `Compass True / Heading-Up True (${MAGNETIC_DECLINATION_TEXT})`
+}
+
+function formatCompassDeclinationText(mode: NorthReferenceMode): string {
+  if (mode === 'true') {
+    return MAGNETIC_DECLINATION_TEXT
+  }
+
+  return ''
+}
+
+function formatNorthReferenceButtonText(mode: NorthReferenceMode): string {
+  return mode === 'magnetic' ? '北参考:磁北' : '北参考:真北'
+}
+
+function formatNorthReferenceStatusText(mode: NorthReferenceMode): string {
+  if (mode === 'magnetic') {
+    return '已切到磁北模式'
+  }
+
+  return '已切到真北模式'
+}
+
+function getNextNorthReferenceMode(mode: NorthReferenceMode): NorthReferenceMode {
+  return mode === 'magnetic' ? 'true' : 'magnetic'
+}
+
+function formatCompassNeedleDegForMode(mode: NorthReferenceMode, magneticHeadingDeg: number | null): number {
+  if (magneticHeadingDeg === null) {
     return 0
   }
 
-  return normalizeRotationDeg(360 - headingDeg)
+  const referenceHeadingDeg = mode === 'true'
+    ? getTrueHeadingDeg(magneticHeadingDeg)
+    : normalizeRotationDeg(magneticHeadingDeg)
+
+  return normalizeRotationDeg(360 - referenceHeadingDeg)
 }
 
 function formatCacheHitRate(memoryHitCount: number, diskHitCount: number, networkFetchCount: number): string {
@@ -295,8 +373,10 @@ export class MapEngine {
   autoRotateTimer: number
   pendingViewPatch: Partial<MapEngineViewState>
   mounted: boolean
+  northReferenceMode: NorthReferenceMode
   sensorHeadingDeg: number | null
   smoothedSensorHeadingDeg: number | null
+  compassDisplayHeadingDeg: number | null
   autoRotateHeadingDeg: number | null
   courseHeadingDeg: number | null
   targetAutoRotationDeg: number | null
@@ -341,9 +421,11 @@ export class MapEngine {
       orientationMode: 'manual',
       orientationModeText: formatOrientationModeText('manual'),
       sensorHeadingText: '--',
+      compassDeclinationText: formatCompassDeclinationText(DEFAULT_NORTH_REFERENCE_MODE),
+      northReferenceButtonText: formatNorthReferenceButtonText(DEFAULT_NORTH_REFERENCE_MODE),
       autoRotateSourceText: formatAutoRotateSourceText('fusion', false),
-      autoRotateCalibrationText: formatAutoRotateCalibrationText(false, MAP_NORTH_OFFSET_DEG),
-      northReferenceText: formatNorthReferenceText(),
+      autoRotateCalibrationText: formatAutoRotateCalibrationText(false, getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)),
+      northReferenceText: formatNorthReferenceText(DEFAULT_NORTH_REFERENCE_MODE),
       compassNeedleDeg: 0,
       centerTileX: DEFAULT_CENTER_TILE_X,
       centerTileY: DEFAULT_CENTER_TILE_Y,
@@ -388,13 +470,15 @@ export class MapEngine {
     this.autoRotateTimer = 0
     this.pendingViewPatch = {}
     this.mounted = false
+    this.northReferenceMode = DEFAULT_NORTH_REFERENCE_MODE
     this.sensorHeadingDeg = null
     this.smoothedSensorHeadingDeg = null
+    this.compassDisplayHeadingDeg = null
     this.autoRotateHeadingDeg = null
     this.courseHeadingDeg = null
     this.targetAutoRotationDeg = null
     this.autoRotateSourceMode = 'fusion'
-    this.autoRotateCalibrationOffsetDeg = MAP_NORTH_OFFSET_DEG
+    this.autoRotateCalibrationOffsetDeg = getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)
     this.autoRotateCalibrationPending = false
   }
 
@@ -670,12 +754,13 @@ export class MapEngine {
       return
     }
 
-    if (!this.state.rotationDeg) {
+    const targetRotationDeg = MAP_NORTH_OFFSET_DEG
+    if (Math.abs(normalizeAngleDeltaDeg(this.state.rotationDeg - targetRotationDeg)) <= 0.01) {
       return
     }
 
     const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY)
-    const resolvedViewport = this.resolveViewportForExactCenter(exactCenter.x, exactCenter.y, 0)
+    const resolvedViewport = this.resolveViewportForExactCenter(exactCenter.x, exactCenter.y, targetRotationDeg)
 
     this.clearInertiaTimer()
     this.clearPreviewResetTimer()
@@ -685,10 +770,10 @@ export class MapEngine {
     this.commitViewport(
       {
         ...resolvedViewport,
-        rotationDeg: 0,
-        rotationText: formatRotationText(0),
+        rotationDeg: targetRotationDeg,
+        rotationText: formatRotationText(targetRotationDeg),
       },
-      `旋转角度已归零 (${this.buildVersion})`,
+      `旋转角度已回到真北参考 (${this.buildVersion})`,
       true,
       () => {
         this.resetPreviewState()
@@ -724,6 +809,10 @@ export class MapEngine {
     this.setHeadingUpMode()
   }
 
+  handleCycleNorthReferenceMode(): void {
+    this.cycleNorthReferenceMode()
+  }
+
   handleAutoRotateCalibrate(): void {
     if (this.state.orientationMode !== 'heading-up') {
       this.setState({
@@ -765,22 +854,25 @@ export class MapEngine {
     this.targetAutoRotationDeg = null
     this.autoRotateCalibrationPending = false
 
+    const mapNorthOffsetDeg = MAP_NORTH_OFFSET_DEG
+    this.autoRotateCalibrationOffsetDeg = mapNorthOffsetDeg
     const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY)
-    const resolvedViewport = this.resolveViewportForExactCenter(exactCenter.x, exactCenter.y, MAP_NORTH_OFFSET_DEG)
+    const resolvedViewport = this.resolveViewportForExactCenter(exactCenter.x, exactCenter.y, mapNorthOffsetDeg)
 
     this.commitViewport(
       {
         ...resolvedViewport,
-        rotationDeg: MAP_NORTH_OFFSET_DEG,
-        rotationText: formatRotationText(MAP_NORTH_OFFSET_DEG),
+        rotationDeg: mapNorthOffsetDeg,
+        rotationText: formatRotationText(mapNorthOffsetDeg),
         rotationMode: 'manual',
         rotationModeText: formatRotationModeText('north-up'),
         rotationToggleText: formatRotationToggleText('north-up'),
         orientationMode: 'north-up',
         orientationModeText: formatOrientationModeText('north-up'),
-        autoRotateCalibrationText: formatAutoRotateCalibrationText(false, this.autoRotateCalibrationOffsetDeg),
+        autoRotateCalibrationText: formatAutoRotateCalibrationText(false, mapNorthOffsetDeg),
+        northReferenceText: formatNorthReferenceText(this.northReferenceMode),
       },
-      `地图已固定为北朝上 (${this.buildVersion})`,
+      `地图已固定为北朝上 (${this.buildVersion})`,
       true,
       () => {
         this.resetPreviewState()
@@ -791,7 +883,7 @@ export class MapEngine {
 
   setHeadingUpMode(): void {
     this.autoRotateCalibrationPending = false
-    this.autoRotateCalibrationOffsetDeg = MAP_NORTH_OFFSET_DEG
+    this.autoRotateCalibrationOffsetDeg = getMapNorthOffsetDeg(this.northReferenceMode)
     this.targetAutoRotationDeg = null
     this.setState({
       rotationMode: 'auto',
@@ -800,6 +892,7 @@ export class MapEngine {
       orientationMode: 'heading-up',
       orientationModeText: formatOrientationModeText('heading-up'),
       autoRotateCalibrationText: formatAutoRotateCalibrationText(false, this.autoRotateCalibrationOffsetDeg),
+      northReferenceText: formatNorthReferenceText(this.northReferenceMode),
       statusText: `正在启用朝向朝上模式 (${this.buildVersion})`,
     }, true)
     if (this.refreshAutoRotateTarget()) {
@@ -813,12 +906,20 @@ export class MapEngine {
       ? this.sensorHeadingDeg
       : interpolateAngleDeg(this.smoothedSensorHeadingDeg, this.sensorHeadingDeg, AUTO_ROTATE_HEADING_SMOOTHING)
 
-    this.autoRotateHeadingDeg = this.smoothedSensorHeadingDeg
+    const compassHeadingDeg = getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)
+    this.compassDisplayHeadingDeg = this.compassDisplayHeadingDeg === null
+      ? compassHeadingDeg
+      : interpolateAngleDeg(this.compassDisplayHeadingDeg, compassHeadingDeg, COMPASS_NEEDLE_SMOOTHING)
+
+    this.autoRotateHeadingDeg = this.resolveAutoRotateInputHeadingDeg()
 
     this.setState({
-      sensorHeadingText: formatHeadingText(this.smoothedSensorHeadingDeg),
+      sensorHeadingText: formatHeadingText(compassHeadingDeg),
+      compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
+      northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
       autoRotateSourceText: formatAutoRotateSourceText(this.autoRotateSourceMode, this.courseHeadingDeg !== null),
-      compassNeedleDeg: formatCompassNeedleDeg(this.smoothedSensorHeadingDeg),
+      compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.smoothedSensorHeadingDeg),
+      northReferenceText: formatNorthReferenceText(this.northReferenceMode),
     })
 
     if (!this.refreshAutoRotateTarget()) {
@@ -839,6 +940,58 @@ export class MapEngine {
       statusText: `${message} (${this.buildVersion})`,
     }, true)
   }
+
+  cycleNorthReferenceMode(): void {
+    const nextMode = getNextNorthReferenceMode(this.northReferenceMode)
+    const nextMapNorthOffsetDeg = getMapNorthOffsetDeg(nextMode)
+    const compassHeadingDeg = this.smoothedSensorHeadingDeg === null
+      ? null
+      : getCompassReferenceHeadingDeg(nextMode, this.smoothedSensorHeadingDeg)
+
+    this.northReferenceMode = nextMode
+    this.autoRotateCalibrationOffsetDeg = nextMapNorthOffsetDeg
+    this.compassDisplayHeadingDeg = compassHeadingDeg
+
+    if (this.state.orientationMode === 'north-up') {
+      const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY)
+      const resolvedViewport = this.resolveViewportForExactCenter(exactCenter.x, exactCenter.y, MAP_NORTH_OFFSET_DEG)
+      this.commitViewport(
+        {
+          ...resolvedViewport,
+          rotationDeg: MAP_NORTH_OFFSET_DEG,
+          rotationText: formatRotationText(MAP_NORTH_OFFSET_DEG),
+          northReferenceText: formatNorthReferenceText(nextMode),
+          sensorHeadingText: formatHeadingText(compassHeadingDeg),
+          compassDeclinationText: formatCompassDeclinationText(nextMode),
+          northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
+          compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.smoothedSensorHeadingDeg),
+          autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
+        },
+        `${formatNorthReferenceStatusText(nextMode)} (${this.buildVersion})`,
+        true,
+        () => {
+          this.resetPreviewState()
+          this.syncRenderer()
+        },
+      )
+      return
+    }
+
+    this.setState({
+      northReferenceText: formatNorthReferenceText(nextMode),
+      sensorHeadingText: formatHeadingText(compassHeadingDeg),
+      compassDeclinationText: formatCompassDeclinationText(nextMode),
+      northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
+      compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.smoothedSensorHeadingDeg),
+      autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
+      statusText: `${formatNorthReferenceStatusText(nextMode)} (${this.buildVersion})`,
+    }, true)
+
+    if (this.state.orientationMode === 'heading-up' && this.refreshAutoRotateTarget()) {
+      this.scheduleAutoRotate()
+    }
+  }
+
   setCourseHeading(headingDeg: number | null): void {
     this.courseHeadingDeg = headingDeg === null ? null : normalizeRotationDeg(headingDeg)
     this.setState({
@@ -851,8 +1004,12 @@ export class MapEngine {
   }
 
   resolveAutoRotateInputHeadingDeg(): number | null {
-    const sensorHeadingDeg = this.smoothedSensorHeadingDeg
-    const courseHeadingDeg = this.courseHeadingDeg
+    const sensorHeadingDeg = this.smoothedSensorHeadingDeg === null
+      ? null
+      : getMapReferenceHeadingDegFromSensor(this.northReferenceMode, this.smoothedSensorHeadingDeg)
+    const courseHeadingDeg = this.courseHeadingDeg === null
+      ? null
+      : getMapReferenceHeadingDegFromCourse(this.northReferenceMode, this.courseHeadingDeg)
 
     if (this.autoRotateSourceMode === 'sensor') {
       return sensorHeadingDeg

+ 7 - 1
miniprogram/pages/map/map.ts

@@ -4,7 +4,7 @@ type MapPageData = MapEngineViewState & {
   showDebugPanel: boolean
 }
 
-const INTERNAL_BUILD_VERSION = 'map-build-64'
+const INTERNAL_BUILD_VERSION = 'map-build-75'
 
 let mapEngine: MapEngine | null = null
 
@@ -153,6 +153,12 @@ Page({
     }
   },
 
+  handleCycleNorthReferenceMode() {
+    if (mapEngine) {
+      mapEngine.handleCycleNorthReferenceMode()
+    }
+  },
+
   handleAutoRotateCalibrate() {
     if (mapEngine) {
       mapEngine.handleAutoRotateCalibrate()

+ 8 - 4
miniprogram/pages/map/map.wxml

@@ -42,6 +42,7 @@
             <view class="compass-widget__center"></view>
           </view>
           <view class="compass-widget__label">{{sensorHeadingText}}</view>
+          <view class="compass-widget__hint" wx:if="{{compassDeclinationText}}">{{compassDeclinationText}}</view>
         </view>
       </view>
     </view>
@@ -56,6 +57,10 @@
       <text class="info-panel__label">Sensor Heading</text>
       <text class="info-panel__value">{{sensorHeadingText}}</text>
     </view>
+    <view class="info-panel__row info-panel__row--stack">
+      <text class="info-panel__label">North Ref</text>
+      <text class="info-panel__value">{{northReferenceText}}</text>
+    </view>
     <view class="info-panel__row">
       <text class="info-panel__label">Zoom</text>
       <text class="info-panel__value">{{zoom}}</text>
@@ -86,10 +91,6 @@
         <text class="info-panel__label">Projection</text>
         <text class="info-panel__value">{{projectionMode}}</text>
       </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">North Ref</text>
-        <text class="info-panel__value">{{northReferenceText}}</text>
-      </view>
       <view class="info-panel__row">
         <text class="info-panel__label">Auto Source</text>
         <text class="info-panel__value">{{autoRotateSourceText}}</text>
@@ -149,6 +150,9 @@
       <view class="control-chip {{orientationMode === 'north-up' ? 'control-chip--active' : ''}}" bindtap="handleSetNorthUpMode">北朝上</view>
       <view class="control-chip {{orientationMode === 'heading-up' ? 'control-chip--active' : ''}}" bindtap="handleSetHeadingUpMode">朝向朝上</view>
     </view>
+    <view class="control-row">
+      <view class="control-chip control-chip--secondary" bindtap="handleCycleNorthReferenceMode">{{northReferenceButtonText}}</view>
+    </view>
     <view class="control-row" wx:if="{{orientationMode === 'heading-up'}}">
       <view class="control-chip" bindtap="handleAutoRotateCalibrate">按当前方向校准</view>
     </view>

+ 9 - 0
miniprogram/pages/map/map.wxss

@@ -213,6 +213,15 @@
   box-shadow: 0 8rpx 18rpx rgba(22, 48, 32, 0.08);
 }
 
+.compass-widget__hint {
+  margin-top: 8rpx;
+  font-size: 18rpx;
+  line-height: 1.4;
+  color: #d62828;
+  text-align: center;
+  font-weight: 700;
+}
+
 .info-panel {
   flex: 1;
   min-height: 0;