merge: integrate map engine north reference work

This commit is contained in:
2026-03-20 11:41:36 +08:00
10 changed files with 313 additions and 410 deletions

12
.gitattributes vendored Normal file
View File

@@ -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
.gitignore vendored
View File

@@ -1,5 +1,20 @@
node_modules/ node_modules/
.tmp-ts/ .tmp-ts/
miniprogram_npm/
dist/
build/
coverage/
project.private.config.json 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 .DS_Store
Thumbs.db Thumbs.db

View File

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

View File

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

View File

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

View File

@@ -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()
}
}

View File

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

View File

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

View File

@@ -42,12 +42,43 @@
<view class="compass-widget__center"></view> <view class="compass-widget__center"></view>
</view> </view>
<view class="compass-widget__label">{{sensorHeadingText}}</view> <view class="compass-widget__label">{{sensorHeadingText}}</view>
<view class="compass-widget__hint" wx:if="{{compassDeclinationText}}">{{compassDeclinationText}}</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<scroll-view class="info-panel" scroll-y enhanced show-scrollbar="true"> <scroll-view class="info-panel" scroll-y enhanced show-scrollbar="true">
<view class="info-panel__row">
<text class="info-panel__label">Heading Mode</text>
<text class="info-panel__value">{{orientationModeText}}</text>
</view>
<view class="info-panel__row">
<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>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Rotation</text>
<text class="info-panel__value">{{rotationText}}</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"> <view class="info-panel__row">
<text class="info-panel__label">Build</text> <text class="info-panel__label">Build</text>
<text class="info-panel__value">{{buildVersion}}</text> <text class="info-panel__value">{{buildVersion}}</text>
@@ -60,18 +91,6 @@
<text class="info-panel__label">Projection</text> <text class="info-panel__label">Projection</text>
<text class="info-panel__value">{{projectionMode}}</text> <text class="info-panel__value">{{projectionMode}}</text>
</view> </view>
<view class="info-panel__row">
<text class="info-panel__label">Heading Mode</text>
<text class="info-panel__value">{{orientationModeText}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Sensor Heading</text>
<text class="info-panel__value">{{sensorHeadingText}}</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"> <view class="info-panel__row">
<text class="info-panel__label">Auto Source</text> <text class="info-panel__label">Auto Source</text>
<text class="info-panel__value">{{autoRotateSourceText}}</text> <text class="info-panel__value">{{autoRotateSourceText}}</text>
@@ -84,14 +103,6 @@
<text class="info-panel__label">Tile URL</text> <text class="info-panel__label">Tile URL</text>
<text class="info-panel__value">{{tileSource}}</text> <text class="info-panel__value">{{tileSource}}</text>
</view> </view>
<view class="info-panel__row">
<text class="info-panel__label">Zoom</text>
<text class="info-panel__value">{{zoom}}</text>
</view>
<view class="info-panel__row">
<text class="info-panel__label">Rotation</text>
<text class="info-panel__value">{{rotationText}}</text>
</view>
<view class="info-panel__row"> <view class="info-panel__row">
<text class="info-panel__label">Center Tile</text> <text class="info-panel__label">Center Tile</text>
<text class="info-panel__value">{{centerText}}</text> <text class="info-panel__value">{{centerText}}</text>
@@ -128,10 +139,7 @@
<text class="info-panel__label">Net Fetches</text> <text class="info-panel__label">Net Fetches</text>
<text class="info-panel__value">{{networkFetchCount}}</text> <text class="info-panel__value">{{networkFetchCount}}</text>
</view> </view>
<view class="info-panel__row info-panel__row--stack"> </block>
<text class="info-panel__label">Status</text>
<text class="info-panel__value">{{statusText}}</text>
</view>
<view class="control-row"> <view class="control-row">
<view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view> <view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view>
@@ -142,6 +150,9 @@
<view class="control-chip {{orientationMode === 'north-up' ? 'control-chip--active' : ''}}" bindtap="handleSetNorthUpMode">北朝上</view> <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 class="control-chip {{orientationMode === 'heading-up' ? 'control-chip--active' : ''}}" bindtap="handleSetHeadingUpMode">朝向朝上</view>
</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-row" wx:if="{{orientationMode === 'heading-up'}}">
<view class="control-chip" bindtap="handleAutoRotateCalibrate">按当前方向校准</view> <view class="control-chip" bindtap="handleAutoRotateCalibrate">按当前方向校准</view>
</view> </view>

View File

@@ -213,6 +213,15 @@
box-shadow: 0 8rpx 18rpx rgba(22, 48, 32, 0.08); 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 { .info-panel {
flex: 1; flex: 1;
min-height: 0; min-height: 0;