Sfoglia il codice sorgente

merge: integrate map engine north reference work

zhangyan 2 settimane fa
parent
commit
71e1866e99

+ 12 - 0
.gitattributes

@@ -0,0 +1,12 @@
+* text=auto eol=lf
+
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.webp binary
+*.ico binary
+*.pdf binary
+*.ttf binary
+*.woff binary
+*.woff2 binary

+ 15 - 0
.gitignore

@@ -1,5 +1,20 @@
 node_modules/
 .tmp-ts/
+miniprogram_npm/
+dist/
+build/
+coverage/
 project.private.config.json
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+.idea/
+.vscode/
+*.suo
+*.user
+*.tmp
+*.swp
 .DS_Store
 Thumbs.db

+ 1 - 2
miniprogram/app.ts

@@ -9,8 +9,7 @@ App<IAppOption>({
 
     // 登录
     wx.login({
-      success: res => {
-        console.log(res.code)
+      success: () => {
         // 发送 res.code 到后台换取 openId, sessionKey, unionId
       },
     })

+ 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 = 12
-const AUTO_ROTATE_EASE = 0.2
+const AUTO_ROTATE_FRAME_MS = 8
+const AUTO_ROTATE_EASE = 0.34
 const AUTO_ROTATE_SNAP_DEG = 0.1
-const AUTO_ROTATE_DEADZONE_DEG = 0.35
-const AUTO_ROTATE_MAX_STEP_DEG = 1.35
+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

+ 0 - 247
miniprogram/engine/renderer/canvasMapRenderer.ts

@@ -1,247 +0,0 @@
-import { getTileSizePx, type CameraState } from '../camera/camera'
-import {
-  TileStore,
-  type TileStoreCallbacks,
-  type TileStoreStats,
-} from '../tile/tileStore'
-import { type LonLatPoint } from '../../utils/projection'
-import { type MapLayer } from '../layer/mapLayer'
-import { TileLayer } from '../layer/tileLayer'
-import { TrackLayer } from '../layer/trackLayer'
-import { GpsLayer } from '../layer/gpsLayer'
-
-const RENDER_FRAME_MS = 16
-
-export interface CanvasMapScene {
-  tileSource: string
-  zoom: number
-  centerTileX: number
-  centerTileY: number
-  viewportWidth: number
-  viewportHeight: number
-  visibleColumns: number
-  overdraw: number
-  translateX: number
-  translateY: number
-  rotationRad: number
-  previewScale: number
-  previewOriginX: number
-  previewOriginY: number
-  track: LonLatPoint[]
-  gpsPoint: LonLatPoint
-}
-
-export type CanvasMapRendererStats = TileStoreStats
-
-function buildCamera(scene: CanvasMapScene): CameraState {
-  return {
-    centerWorldX: scene.centerTileX,
-    centerWorldY: scene.centerTileY,
-    viewportWidth: scene.viewportWidth,
-    viewportHeight: scene.viewportHeight,
-    visibleColumns: scene.visibleColumns,
-    rotationRad: scene.rotationRad,
-  }
-}
-
-export class CanvasMapRenderer {
-  canvas: any
-  ctx: any
-  dpr: number
-  scene: CanvasMapScene | null
-  tileStore: TileStore
-  tileLayer: TileLayer
-  layers: MapLayer[]
-  renderTimer: number
-  animationTimer: number
-  destroyed: boolean
-  animationPaused: boolean
-  pulseFrame: number
-  lastStats: CanvasMapRendererStats
-  onStats?: (stats: CanvasMapRendererStats) => void
-  onTileError?: (message: string) => void
-
-  constructor(
-    onStats?: (stats: CanvasMapRendererStats) => void,
-    onTileError?: (message: string) => void,
-  ) {
-    this.onStats = onStats
-    this.onTileError = onTileError
-    this.canvas = null
-    this.ctx = null
-    this.dpr = 1
-    this.scene = null
-    this.tileStore = new TileStore({
-      onTileReady: () => {
-        this.scheduleRender()
-      },
-      onTileError: (message) => {
-        if (this.onTileError) {
-          this.onTileError(message)
-        }
-        this.scheduleRender()
-      },
-    } satisfies TileStoreCallbacks)
-    this.tileLayer = new TileLayer()
-    this.layers = [
-      this.tileLayer,
-      new TrackLayer(),
-      new GpsLayer(),
-    ]
-    this.renderTimer = 0
-    this.animationTimer = 0
-    this.destroyed = false
-    this.animationPaused = false
-    this.pulseFrame = 0
-    this.lastStats = {
-      visibleTileCount: 0,
-      readyTileCount: 0,
-      memoryTileCount: 0,
-      diskTileCount: 0,
-      memoryHitCount: 0,
-      diskHitCount: 0,
-      networkFetchCount: 0,
-    }
-  }
-
-  attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
-    this.canvas = canvasNode
-    this.ctx = canvasNode.getContext('2d')
-    this.dpr = dpr || 1
-
-    canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
-    canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
-
-    if (typeof this.ctx.setTransform === 'function') {
-      this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0)
-    } else {
-      this.ctx.scale(this.dpr, this.dpr)
-    }
-
-    this.tileStore.attachCanvas(canvasNode)
-    this.startAnimation()
-    this.scheduleRender()
-  }
-
-  updateScene(scene: CanvasMapScene): void {
-    this.scene = scene
-    this.scheduleRender()
-  }
-
-  setAnimationPaused(paused: boolean): void {
-    this.animationPaused = paused
-    if (!paused) {
-      this.scheduleRender()
-    }
-  }
-
-  destroy(): void {
-    this.destroyed = true
-    if (this.renderTimer) {
-      clearTimeout(this.renderTimer)
-      this.renderTimer = 0
-    }
-    if (this.animationTimer) {
-      clearTimeout(this.animationTimer)
-      this.animationTimer = 0
-    }
-    this.tileStore.destroy()
-    this.canvas = null
-    this.ctx = null
-    this.scene = null
-  }
-
-  startAnimation(): void {
-    if (this.animationTimer) {
-      return
-    }
-
-    const tick = () => {
-      if (this.destroyed) {
-        this.animationTimer = 0
-        return
-      }
-
-      if (!this.animationPaused) {
-        this.pulseFrame = (this.pulseFrame + 1) % 360
-        this.scheduleRender()
-      }
-
-      this.animationTimer = setTimeout(tick, 33) as unknown as number
-    }
-
-    tick()
-  }
-
-  scheduleRender(): void {
-    if (this.renderTimer || !this.ctx || !this.scene || this.destroyed) {
-      return
-    }
-
-    this.renderTimer = setTimeout(() => {
-      this.renderTimer = 0
-      this.renderFrame()
-    }, RENDER_FRAME_MS) as unknown as number
-  }
-
-  emitStats(stats: CanvasMapRendererStats): void {
-    if (
-      stats.visibleTileCount === this.lastStats.visibleTileCount
-      && stats.readyTileCount === this.lastStats.readyTileCount
-      && stats.memoryTileCount === this.lastStats.memoryTileCount
-      && stats.diskTileCount === this.lastStats.diskTileCount
-      && stats.memoryHitCount === this.lastStats.memoryHitCount
-      && stats.diskHitCount === this.lastStats.diskHitCount
-      && stats.networkFetchCount === this.lastStats.networkFetchCount
-    ) {
-      return
-    }
-
-    this.lastStats = stats
-    if (this.onStats) {
-      this.onStats(stats)
-    }
-  }
-
-  renderFrame(): void {
-    if (!this.ctx || !this.scene) {
-      return
-    }
-
-    const scene = this.scene
-    const ctx = this.ctx
-    const camera = buildCamera(scene)
-    const tileSize = getTileSizePx(camera)
-
-    ctx.clearRect(0, 0, scene.viewportWidth, scene.viewportHeight)
-    ctx.fillStyle = '#dbeed4'
-    ctx.fillRect(0, 0, scene.viewportWidth, scene.viewportHeight)
-
-    if (!tileSize) {
-      this.emitStats(this.tileStore.getStats(0, 0))
-      return
-    }
-
-    const previewScale = scene.previewScale || 1
-    const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
-    const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
-
-    ctx.save()
-    ctx.translate(previewOriginX, previewOriginY)
-    ctx.scale(previewScale, previewScale)
-    ctx.translate(-previewOriginX, -previewOriginY)
-
-    for (const layer of this.layers) {
-      layer.draw({
-        ctx,
-        camera,
-        scene,
-        pulseFrame: this.pulseFrame,
-        tileStore: this.tileStore,
-      })
-    }
-
-    ctx.restore()
-    this.emitStats(this.tileStore.getStats(this.tileLayer.lastVisibleTileCount, this.tileLayer.lastReadyTileCount))
-  }
-}

+ 0 - 67
miniprogram/engine/renderer/canvasOverlayRenderer.ts

@@ -1,67 +0,0 @@
-import { type MapLayer } from '../layer/mapLayer'
-import { buildCamera, type MapScene } from './mapRenderer'
-import { type TileStore } from '../tile/tileStore'
-
-export class CanvasOverlayRenderer {
-  canvas: any
-  ctx: any
-  dpr: number
-  layers: MapLayer[]
-
-  constructor(layers: MapLayer[]) {
-    this.canvas = null
-    this.ctx = null
-    this.dpr = 1
-    this.layers = layers
-  }
-
-  attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
-    this.canvas = canvasNode
-    this.ctx = canvasNode.getContext('2d')
-    this.dpr = dpr || 1
-
-    canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
-    canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
-
-    if (typeof this.ctx.setTransform === 'function') {
-      this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0)
-    } else {
-      this.ctx.scale(this.dpr, this.dpr)
-    }
-  }
-
-  clear(): void {
-    this.canvas = null
-    this.ctx = null
-  }
-
-  render(scene: MapScene, tileStore: TileStore, pulseFrame: number): void {
-    if (!this.ctx) {
-      return
-    }
-
-    const camera = buildCamera(scene)
-    const ctx = this.ctx
-    const previewScale = scene.previewScale || 1
-    const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
-    const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
-
-    ctx.clearRect(0, 0, scene.viewportWidth, scene.viewportHeight)
-    ctx.save()
-    ctx.translate(previewOriginX, previewOriginY)
-    ctx.scale(previewScale, previewScale)
-    ctx.translate(-previewOriginX, -previewOriginY)
-
-    for (const layer of this.layers) {
-      layer.draw({
-        ctx,
-        camera,
-        scene,
-        pulseFrame,
-        tileStore,
-      })
-    }
-
-    ctx.restore()
-  }
-}

+ 0 - 2
miniprogram/pages/index/index.ts

@@ -1,6 +1,5 @@
 // index.ts
 // 获取应用实例
-const app = getApp<IAppOption>()
 const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
 
 Component({
@@ -42,7 +41,6 @@ Component({
       wx.getUserProfile({
         desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
         success: (res) => {
-          console.log(res)
           this.setData({
             userInfo: res.userInfo,
             hasUserInfo: true

+ 19 - 3
miniprogram/pages/map/map.ts

@@ -1,6 +1,10 @@
 import { MapEngine, type MapEngineStageRect, type MapEngineViewState } from '../../engine/map/mapEngine'
 
-const INTERNAL_BUILD_VERSION = 'map-build-58'
+type MapPageData = MapEngineViewState & {
+  showDebugPanel: boolean
+}
+
+const INTERNAL_BUILD_VERSION = 'map-build-75'
 
 let mapEngine: MapEngine | null = null
 
@@ -18,7 +22,7 @@ function getFallbackStageRect(): MapEngineStageRect {
 }
 
 Page({
-  data: {} as MapEngineViewState,
+  data: { showDebugPanel: false } as MapPageData,
 
   onLoad() {
     mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
@@ -27,7 +31,7 @@ Page({
       },
     })
 
-    this.setData(mapEngine.getInitialData())
+    this.setData({ ...mapEngine.getInitialData(), showDebugPanel: false })
   },
 
   onReady() {
@@ -149,11 +153,23 @@ Page({
     }
   },
 
+  handleCycleNorthReferenceMode() {
+    if (mapEngine) {
+      mapEngine.handleCycleNorthReferenceMode()
+    }
+  },
+
   handleAutoRotateCalibrate() {
     if (mapEngine) {
       mapEngine.handleAutoRotateCalibrate()
     }
   },
+
+  handleToggleDebugPanel() {
+    this.setData({
+      showDebugPanel: !this.data.showDebugPanel,
+    })
+  },
 })
 
 

+ 72 - 61
miniprogram/pages/map/map.wxml

@@ -42,24 +42,13 @@
             <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>
   </view>
 
   <scroll-view class="info-panel" scroll-y enhanced show-scrollbar="true">
-    <view class="info-panel__row">
-      <text class="info-panel__label">Build</text>
-      <text class="info-panel__value">{{buildVersion}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Renderer</text>
-      <text class="info-panel__value">{{renderMode}}</text>
-    </view>
-    <view class="info-panel__row info-panel__row--stack">
-      <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">Heading Mode</text>
       <text class="info-panel__value">{{orientationModeText}}</text>
@@ -68,22 +57,10 @@
       <text class="info-panel__label">Sensor Heading</text>
       <text class="info-panel__value">{{sensorHeadingText}}</text>
     </view>
-    <view class="info-panel__row">
+    <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">Auto Source</text>
-      <text class="info-panel__value">{{autoRotateSourceText}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Calibration</text>
-      <text class="info-panel__value">{{autoRotateCalibrationText}}</text>
-    </view>
-    <view class="info-panel__row info-panel__row--stack">
-      <text class="info-panel__label">Tile URL</text>
-      <text class="info-panel__value">{{tileSource}}</text>
-    </view>
     <view class="info-panel__row">
       <text class="info-panel__label">Zoom</text>
       <text class="info-panel__value">{{zoom}}</text>
@@ -92,47 +69,78 @@
       <text class="info-panel__label">Rotation</text>
       <text class="info-panel__value">{{rotationText}}</text>
     </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Center Tile</text>
-      <text class="info-panel__value">{{centerText}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Tile Size</text>
-      <text class="info-panel__value">{{tileSizePx}}px</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Visible Tiles</text>
-      <text class="info-panel__value">{{visibleTileCount}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Ready Tiles</text>
-      <text class="info-panel__value">{{readyTileCount}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Memory Tiles</text>
-      <text class="info-panel__value">{{memoryTileCount}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Disk Tiles</text>
-      <text class="info-panel__value">{{diskTileCount}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Cache Hit</text>
-      <text class="info-panel__value">{{cacheHitRateText}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Disk Hits</text>
-      <text class="info-panel__value">{{diskHitCount}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Net Fetches</text>
-      <text class="info-panel__value">{{networkFetchCount}}</text>
-    </view>
     <view class="info-panel__row info-panel__row--stack">
       <text class="info-panel__label">Status</text>
       <text class="info-panel__value">{{statusText}}</text>
     </view>
 
+    <view class="control-row">
+      <view class="control-chip control-chip--secondary" bindtap="handleToggleDebugPanel">{{showDebugPanel ? '隐藏调试' : '查看调试'}}</view>
+    </view>
+
+    <block wx:if="{{showDebugPanel}}">
+      <view class="info-panel__row">
+        <text class="info-panel__label">Build</text>
+        <text class="info-panel__value">{{buildVersion}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Renderer</text>
+        <text class="info-panel__value">{{renderMode}}</text>
+      </view>
+      <view class="info-panel__row info-panel__row--stack">
+        <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">Auto Source</text>
+        <text class="info-panel__value">{{autoRotateSourceText}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Calibration</text>
+        <text class="info-panel__value">{{autoRotateCalibrationText}}</text>
+      </view>
+      <view class="info-panel__row info-panel__row--stack">
+        <text class="info-panel__label">Tile URL</text>
+        <text class="info-panel__value">{{tileSource}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Center Tile</text>
+        <text class="info-panel__value">{{centerText}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Tile Size</text>
+        <text class="info-panel__value">{{tileSizePx}}px</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Visible Tiles</text>
+        <text class="info-panel__value">{{visibleTileCount}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Ready Tiles</text>
+        <text class="info-panel__value">{{readyTileCount}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Memory Tiles</text>
+        <text class="info-panel__value">{{memoryTileCount}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Disk Tiles</text>
+        <text class="info-panel__value">{{diskTileCount}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Cache Hit</text>
+        <text class="info-panel__value">{{cacheHitRateText}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Disk Hits</text>
+        <text class="info-panel__value">{{diskHitCount}}</text>
+      </view>
+      <view class="info-panel__row">
+        <text class="info-panel__label">Net Fetches</text>
+        <text class="info-panel__value">{{networkFetchCount}}</text>
+      </view>
+    </block>
+
     <view class="control-row">
       <view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view>
       <view class="control-chip control-chip--secondary" bindtap="handleRotationReset">旋转归零</view>
@@ -142,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;