|
|
@@ -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
|