refactor: clean map page and tune heading mode

This commit is contained in:
2026-03-19 16:49:37 +08:00
parent 5fea805ac3
commit ceb5479c69
7 changed files with 89 additions and 389 deletions

View File

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

View File

@@ -25,11 +25,11 @@ 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 = 10
const AUTO_ROTATE_EASE = 0.24
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 = 1.5
const AUTO_ROTATE_MAX_STEP_DEG = 0.9
const AUTO_ROTATE_HEADING_SMOOTHING = 0.32
const SAMPLE_TRACK_WGS84: LonLatPoint[] = [

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
// 获取应用实例
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

View File

@@ -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-63'
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() {
@@ -154,6 +158,12 @@ Page({
mapEngine.handleAutoRotateCalibrate()
}
},
handleToggleDebugPanel() {
this.setData({
showDebugPanel: !this.data.showDebugPanel,
})
},
})

View File

@@ -48,6 +48,32 @@
</view>
<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">
<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">
<text class="info-panel__label">Build</text>
<text class="info-panel__value">{{buildVersion}}</text>
@@ -60,14 +86,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">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>
@@ -84,14 +102,6 @@
<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>
</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">
<text class="info-panel__label">Center Tile</text>
<text class="info-panel__value">{{centerText}}</text>
@@ -128,10 +138,7 @@
<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>
</block>
<view class="control-row">
<view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view>