Files
cmr-mini/miniprogram/pages/map/map.ts

1269 lines
37 KiB
TypeScript

import {
MapEngine,
type MapEngineGameInfoRow,
type MapEngineGameInfoSnapshot,
type MapEngineStageRect,
type MapEngineViewState,
} from '../../engine/map/mapEngine'
import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
type CompassTickData = {
angle: number
long: boolean
major: boolean
}
type CompassLabelData = {
text: string
angle: number
rotateBack: number
radius: number
className: string
}
type ScaleRulerMinorTickData = {
key: string
topPx: number
long: boolean
}
type ScaleRulerMajorMarkData = {
key: string
topPx: number
label: string
}
type SideButtonMode = 'all' | 'left' | 'right' | 'hidden'
type SideActionButtonState = 'muted' | 'default' | 'active'
type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center'
type MapPageData = MapEngineViewState & {
showDebugPanel: boolean
showGameInfoPanel: boolean
showCenterScaleRuler: boolean
centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
statusBarHeight: number
topInsetHeight: number
hudPanelIndex: number
configSourceText: string
mockBridgeUrlDraft: string
mockHeartRateBridgeUrlDraft: string
gameInfoTitle: string
gameInfoSubtitle: string
gameInfoLocalRows: MapEngineGameInfoRow[]
gameInfoGlobalRows: MapEngineGameInfoRow[]
panelTimerText: string
panelMileageText: string
panelDistanceValueText: string
panelProgressText: string
panelSpeedValueText: string
compassTicks: CompassTickData[]
compassLabels: CompassLabelData[]
sideButtonMode: SideButtonMode
sideToggleIconSrc: string
sideButton2Class: string
sideButton4Class: string
sideButton11Class: string
sideButton13Class: string
sideButton14Class: string
sideButton16Class: string
centerScaleRulerVisible: boolean
centerScaleRulerCenterXPx: number
centerScaleRulerZeroYPx: number
centerScaleRulerHeightPx: number
centerScaleRulerAxisBottomPx: number
centerScaleRulerZeroVisible: boolean
centerScaleRulerZeroLabel: string
centerScaleRulerMinorTicks: ScaleRulerMinorTickData[]
centerScaleRulerMajorMarks: ScaleRulerMajorMarkData[]
showLeftButtonGroup: boolean
showRightButtonGroups: boolean
showBottomDebugButton: boolean
}
const INTERNAL_BUILD_VERSION = 'map-build-252'
const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
let mapEngine: MapEngine | null = null
let stageCanvasAttached = false
function buildSideButtonVisibility(mode: SideButtonMode) {
return {
sideButtonMode: mode,
showLeftButtonGroup: mode === 'all' || mode === 'left' || mode === 'right',
showRightButtonGroups: mode === 'all' || mode === 'right',
showBottomDebugButton: mode !== 'hidden',
}
}
function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
if (currentMode === 'all') {
return 'left'
}
if (currentMode === 'left') {
return 'right'
}
if (currentMode === 'right') {
return 'hidden'
}
return 'left'
}
function buildCompassTicks(): CompassTickData[] {
const ticks: CompassTickData[] = []
for (let angle = 0; angle < 360; angle += 5) {
ticks.push({
angle,
long: angle % 15 === 0,
major: angle % 45 === 0,
})
}
return ticks
}
function buildCompassLabels(): CompassLabelData[] {
return [
{ text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
{ text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
{ text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
{ text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
{ text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
{ text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
{ text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
{ text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
]
}
function getFallbackStageRect(): MapEngineStageRect {
const systemInfo = wx.getSystemInfoSync()
const width = Math.max(320, systemInfo.windowWidth)
const height = Math.max(280, systemInfo.windowHeight)
return {
width,
height,
left: 0,
top: 0,
}
}
function getSideToggleIconSrc(mode: SideButtonMode): string {
if (mode === 'left') {
return '../../assets/btn_more2.png'
}
if (mode === 'hidden') {
return '../../assets/btn_more1.png'
}
return '../../assets/btn_more3.png'
}
function getSideActionButtonClass(state: SideActionButtonState): string {
if (state === 'muted') {
return 'map-side-button map-side-button--muted'
}
if (state === 'active') {
return 'map-side-button map-side-button--active'
}
return 'map-side-button map-side-button--default'
}
function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
? 'muted'
: data.gpsLockEnabled
? 'active'
: 'default'
const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'idle' ? 'default' : 'active'
const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
? 'muted'
: data.centerScaleRulerAnchorMode === 'compass-center'
? 'active'
: 'default'
const sideButton16State: SideActionButtonState = data.skipButtonEnabled ? 'default' : 'muted'
return {
sideToggleIconSrc: getSideToggleIconSrc(data.sideButtonMode),
sideButton2Class: getSideActionButtonClass(sideButton2State),
sideButton4Class: getSideActionButtonClass(sideButton4State),
sideButton11Class: getSideActionButtonClass(sideButton11State),
sideButton13Class: getSideActionButtonClass(sideButton13State),
sideButton14Class: getSideActionButtonClass(sideButton14State),
sideButton16Class: getSideActionButtonClass(sideButton16State),
}
}
function getRpxUnitInPx(): number {
const systemInfo = wx.getSystemInfoSync()
return systemInfo.windowWidth / 750
}
function worldTileYToLat(worldTileY: number, zoom: number): number {
const scale = Math.pow(2, zoom)
const n = Math.PI - (2 * Math.PI * worldTileY) / scale
return (180 / Math.PI) * Math.atan(Math.sinh(n))
}
function getNiceDistanceMeters(rawDistanceMeters: number): number {
if (!Number.isFinite(rawDistanceMeters) || rawDistanceMeters <= 0) {
return 50
}
const exponent = Math.floor(Math.log10(rawDistanceMeters))
const base = Math.pow(10, exponent)
const normalized = rawDistanceMeters / base
if (normalized <= 1) {
return base
}
if (normalized <= 2) {
return 2 * base
}
if (normalized <= 5) {
return 5 * base
}
return 10 * base
}
function formatScaleDistanceLabel(distanceMeters: number): string {
if (distanceMeters >= 1000) {
const distanceKm = distanceMeters / 1000
const formatted = distanceKm >= 10 ? distanceKm.toFixed(0) : distanceKm.toFixed(1)
return `${formatted.replace(/\.0$/, '')} km`
}
return `${Math.round(distanceMeters)} m`
}
function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
if (!data.showCenterScaleRuler) {
return {
centerScaleRulerVisible: false,
centerScaleRulerCenterXPx: 0,
centerScaleRulerZeroYPx: 0,
centerScaleRulerHeightPx: 0,
centerScaleRulerAxisBottomPx: 0,
centerScaleRulerZeroVisible: false,
centerScaleRulerZeroLabel: '0 m',
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
}
}
if (!data.stageWidth || !data.stageHeight) {
return {
centerScaleRulerVisible: false,
centerScaleRulerCenterXPx: 0,
centerScaleRulerZeroYPx: 0,
centerScaleRulerHeightPx: 0,
centerScaleRulerAxisBottomPx: 0,
centerScaleRulerZeroVisible: false,
centerScaleRulerZeroLabel: '0 m',
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
}
}
const topPadding = 12
const rpxUnitPx = getRpxUnitInPx()
const compassBottomPaddingPx = 248 * rpxUnitPx
const compassDialRadiusPx = (196 * rpxUnitPx) / 2
const compassHeadingOverlayHeightPx = 40 * rpxUnitPx
const compassOcclusionPaddingPx = 10 * rpxUnitPx
const zeroYPx = data.centerScaleRulerAnchorMode === 'compass-center'
? Math.round(data.stageHeight - compassBottomPaddingPx - compassDialRadiusPx)
: Math.round(data.stageHeight / 2)
const fallbackHeight = Math.max(zeroYPx - topPadding, 160)
const coveredBottomPx = data.centerScaleRulerAnchorMode === 'compass-center'
? Math.round(compassDialRadiusPx + compassHeadingOverlayHeightPx + compassOcclusionPaddingPx)
: 0
if (
!data.tileSizePx
|| !Number.isFinite(data.zoom)
|| !Number.isFinite(data.centerTileY)
) {
return {
centerScaleRulerVisible: true,
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
centerScaleRulerZeroYPx: zeroYPx,
centerScaleRulerHeightPx: fallbackHeight,
centerScaleRulerAxisBottomPx: coveredBottomPx,
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
centerScaleRulerZeroLabel: '0 m',
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
}
}
const centerLat = worldTileYToLat(data.centerTileY + 0.5, data.zoom)
const metersPerTile = Math.cos(centerLat * Math.PI / 180) * 40075016.686 / Math.pow(2, data.zoom)
const metersPerPixel = metersPerTile / data.tileSizePx
const effectivePreviewScale = Number.isFinite(data.previewScale) && data.previewScale > 0 ? data.previewScale : 1
const effectiveMetersPerPixel = metersPerPixel / effectivePreviewScale
const rulerHeight = Math.floor(zeroYPx - topPadding)
if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
return {
centerScaleRulerVisible: true,
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
centerScaleRulerZeroYPx: zeroYPx,
centerScaleRulerHeightPx: Math.max(rulerHeight, fallbackHeight),
centerScaleRulerAxisBottomPx: coveredBottomPx,
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
centerScaleRulerZeroLabel: '0 m',
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
}
}
const labelDistanceMeters = getNiceDistanceMeters(effectiveMetersPerPixel * 80)
const minorDistanceMeters = labelDistanceMeters / 8
const minorStepPx = minorDistanceMeters / effectiveMetersPerPixel
const visibleTopLimitPx = rulerHeight - coveredBottomPx
const minorTicks: ScaleRulerMinorTickData[] = []
const majorMarks: ScaleRulerMajorMarkData[] = []
for (let index = 1; index <= 200; index += 1) {
const topPx = Math.round(rulerHeight - index * minorStepPx)
if (topPx < 0) {
break
}
if (topPx >= visibleTopLimitPx) {
continue
}
const isHalfMajor = index % 4 === 0
const isLabelMajor = index % 8 === 0
minorTicks.push({
key: `minor-${index}`,
topPx,
long: isHalfMajor,
})
if (isLabelMajor) {
majorMarks.push({
key: `major-${index}`,
topPx,
label: formatScaleDistanceLabel((index / 8) * labelDistanceMeters),
})
}
}
return {
centerScaleRulerVisible: true,
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
centerScaleRulerZeroYPx: zeroYPx,
centerScaleRulerHeightPx: rulerHeight,
centerScaleRulerAxisBottomPx: coveredBottomPx,
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
centerScaleRulerZeroLabel: '0 m',
centerScaleRulerMinorTicks: minorTicks,
centerScaleRulerMajorMarks: majorMarks,
}
}
function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
return {
title: '当前游戏',
subtitle: '未开始',
localRows: [],
globalRows: [
{ label: '全球积分', value: '未接入' },
{ label: '全球排名', value: '未接入' },
{ label: '在线人数', value: '未接入' },
{ label: '队伍状态', value: '未接入' },
{ label: '实时广播', value: '未接入' },
],
}
}
Page({
data: {
showDebugPanel: false,
showGameInfoPanel: false,
showCenterScaleRuler: false,
statusBarHeight: 0,
topInsetHeight: 12,
hudPanelIndex: 0,
configSourceText: '顺序赛配置',
centerScaleRulerAnchorMode: 'screen-center',
gameInfoTitle: '当前游戏',
gameInfoSubtitle: '未开始',
gameInfoLocalRows: [],
gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
panelTimerText: '00:00:00',
panelMileageText: '0m',
panelActionTagText: '目标',
panelDistanceTagText: '点距',
panelDistanceValueText: '--',
panelDistanceUnitText: '',
panelProgressText: '0/0',
gameSessionStatus: 'idle',
gameModeText: '顺序赛',
gpsLockEnabled: false,
gpsLockAvailable: false,
locationSourceMode: 'real',
locationSourceText: '真实定位',
mockBridgeConnected: false,
mockBridgeStatusText: '未连接',
mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
mockCoordText: '--',
mockSpeedText: '--',
heartRateSourceMode: 'real',
heartRateSourceText: '真实心率',
mockHeartRateBridgeConnected: false,
mockHeartRateBridgeStatusText: '未连接',
mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
mockHeartRateText: '--',
heartRateScanText: '未扫描',
heartRateDiscoveredDevices: [],
panelSpeedValueText: '0',
panelTelemetryTone: 'blue',
panelHeartRateZoneNameText: '--',
panelHeartRateZoneRangeText: '',
heartRateConnected: false,
heartRateStatusText: '心率带未连接',
heartRateDeviceText: '--',
panelHeartRateValueText: '--',
panelHeartRateUnitText: '',
panelCaloriesValueText: '0',
panelCaloriesUnitText: 'kcal',
panelAverageSpeedValueText: '0',
panelAverageSpeedUnitText: 'km/h',
panelAccuracyValueText: '--',
panelAccuracyUnitText: '',
deviceHeadingText: '--',
devicePoseText: '竖持',
headingConfidenceText: '低',
accelerometerText: '--',
gyroscopeText: '--',
deviceMotionText: '--',
punchButtonText: '打点',
punchButtonEnabled: false,
skipButtonEnabled: false,
punchHintText: '等待进入检查点范围',
punchFeedbackVisible: false,
punchFeedbackText: '',
punchFeedbackTone: 'neutral',
contentCardVisible: false,
contentCardTitle: '',
contentCardBody: '',
punchButtonFxClass: '',
punchFeedbackFxClass: '',
contentCardFxClass: '',
mapPulseVisible: false,
mapPulseLeftPx: 0,
mapPulseTopPx: 0,
mapPulseFxClass: '',
stageFxVisible: false,
stageFxClass: '',
centerScaleRulerVisible: false,
centerScaleRulerCenterXPx: 0,
centerScaleRulerZeroYPx: 0,
centerScaleRulerHeightPx: 0,
centerScaleRulerAxisBottomPx: 0,
centerScaleRulerZeroVisible: false,
centerScaleRulerZeroLabel: '0 m',
centerScaleRulerMinorTicks: [],
centerScaleRulerMajorMarks: [],
compassTicks: buildCompassTicks(),
compassLabels: buildCompassLabels(),
...buildSideButtonVisibility('left'),
...buildSideButtonState({
sideButtonMode: 'left',
showGameInfoPanel: false,
showCenterScaleRuler: false,
centerScaleRulerAnchorMode: 'screen-center',
skipButtonEnabled: false,
gameSessionStatus: 'idle',
gpsLockEnabled: false,
gpsLockAvailable: false,
}),
} as unknown as MapPageData,
onLoad() {
const systemInfo = wx.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 0
const menuButtonRect = wx.getMenuButtonBoundingClientRect()
const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
if (mapEngine) {
mapEngine.destroy()
mapEngine = null
}
mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
onData: (patch) => {
const nextPatch = patch as Partial<MapPageData>
const nextData: Partial<MapPageData> = {
...nextPatch,
}
if (
typeof nextPatch.mockBridgeUrlText === 'string'
&& this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
) {
nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText
}
if (
typeof nextPatch.mockHeartRateBridgeUrlText === 'string'
&& this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText
) {
nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
}
const mergedData = {
...this.data,
...nextData,
} as MapPageData
this.setData({
...nextData,
...buildCenterScaleRulerPatch(mergedData),
...buildSideButtonState(mergedData),
})
if (this.data.showGameInfoPanel) {
this.syncGameInfoPanelSnapshot()
}
},
})
this.setData({
...mapEngine.getInitialData(),
showDebugPanel: false,
showGameInfoPanel: false,
statusBarHeight,
topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
hudPanelIndex: 0,
configSourceText: '顺序赛配置',
gameInfoTitle: '当前游戏',
gameInfoSubtitle: '未开始',
gameInfoLocalRows: [],
gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
panelTimerText: '00:00:00',
panelMileageText: '0m',
panelActionTagText: '目标',
panelDistanceTagText: '点距',
panelDistanceValueText: '--',
panelDistanceUnitText: '',
panelProgressText: '0/0',
gameSessionStatus: 'idle',
gameModeText: '顺序赛',
gpsLockEnabled: false,
gpsLockAvailable: false,
locationSourceMode: 'real',
locationSourceText: '真实定位',
mockBridgeConnected: false,
mockBridgeStatusText: '未连接',
mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
mockCoordText: '--',
mockSpeedText: '--',
heartRateSourceMode: 'real',
heartRateSourceText: '真实心率',
mockHeartRateBridgeConnected: false,
mockHeartRateBridgeStatusText: '未连接',
mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
mockHeartRateText: '--',
panelSpeedValueText: '0',
panelTelemetryTone: 'blue',
panelHeartRateZoneNameText: '--',
panelHeartRateZoneRangeText: '',
heartRateConnected: false,
heartRateStatusText: '心率带未连接',
heartRateDeviceText: '--',
panelHeartRateValueText: '--',
panelHeartRateUnitText: '',
panelCaloriesValueText: '0',
panelCaloriesUnitText: 'kcal',
panelAverageSpeedValueText: '0',
panelAverageSpeedUnitText: 'km/h',
panelAccuracyValueText: '--',
panelAccuracyUnitText: '',
deviceHeadingText: '--',
devicePoseText: '竖持',
headingConfidenceText: '低',
accelerometerText: '--',
gyroscopeText: '--',
deviceMotionText: '--',
punchButtonText: '打点',
punchButtonEnabled: false,
skipButtonEnabled: false,
punchHintText: '等待进入检查点范围',
punchFeedbackVisible: false,
punchFeedbackText: '',
punchFeedbackTone: 'neutral',
contentCardVisible: false,
contentCardTitle: '',
contentCardBody: '',
punchButtonFxClass: '',
punchFeedbackFxClass: '',
contentCardFxClass: '',
mapPulseVisible: false,
mapPulseLeftPx: 0,
mapPulseTopPx: 0,
mapPulseFxClass: '',
stageFxVisible: false,
stageFxClass: '',
compassTicks: buildCompassTicks(),
compassLabels: buildCompassLabels(),
...buildSideButtonVisibility('left'),
...buildSideButtonState({
sideButtonMode: 'left',
showGameInfoPanel: false,
showCenterScaleRuler: false,
centerScaleRulerAnchorMode: 'screen-center',
skipButtonEnabled: false,
gameSessionStatus: 'idle',
gpsLockEnabled: false,
gpsLockAvailable: false,
}),
...buildCenterScaleRulerPatch({
...(mapEngine.getInitialData() as MapPageData),
showCenterScaleRuler: false,
centerScaleRulerAnchorMode: 'screen-center',
stageWidth: 0,
stageHeight: 0,
topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
zoom: 0,
centerTileY: 0,
tileSizePx: 0,
}),
})
},
onReady() {
stageCanvasAttached = false
this.measureStageAndCanvas()
this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
},
onShow() {
if (mapEngine) {
mapEngine.handleAppShow()
}
},
onHide() {
if (mapEngine) {
mapEngine.handleAppHide()
}
},
onUnload() {
if (mapEngine) {
mapEngine.destroy()
mapEngine = null
}
stageCanvasAttached = false
},
loadMapConfigFromRemote(configUrl: string, configLabel: string) {
const currentEngine = mapEngine
if (!currentEngine) {
return
}
this.setData({
configSourceText: configLabel,
configStatusText: `加载中: ${configLabel}`,
})
loadRemoteMapConfig(configUrl)
.then((config) => {
if (mapEngine !== currentEngine) {
return
}
currentEngine.applyRemoteMapConfig(config)
})
.catch((error) => {
if (mapEngine !== currentEngine) {
return
}
const errorMessage = error && error.message ? error.message : '未知错误'
this.setData({
configStatusText: `载入失败: ${errorMessage}`,
statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
})
})
},
measureStageAndCanvas() {
const page = this
const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
const fallbackRect = getFallbackStageRect()
const rect: MapEngineStageRect = {
width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
}
const currentEngine = mapEngine
if (!currentEngine) {
return
}
currentEngine.setStage(rect)
if (stageCanvasAttached) {
return
}
const canvasQuery = wx.createSelectorQuery().in(page)
canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
canvasQuery.exec((canvasRes) => {
const canvasRef = canvasRes[0] as any
const labelCanvasRef = canvasRes[1] as any
if (!canvasRef || !canvasRef.node) {
page.setData({
statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
})
return
}
const dpr = wx.getSystemInfoSync().pixelRatio || 1
try {
currentEngine.attachCanvas(
canvasRef.node,
rect.width,
rect.height,
dpr,
labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
)
stageCanvasAttached = true
} catch (error) {
page.setData({
statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
})
}
})
}
const query = wx.createSelectorQuery().in(page)
query.select('.map-stage').boundingClientRect()
query.exec((res) => {
const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
applyStage(rect)
})
},
handleTouchStart(event: WechatMiniprogram.TouchEvent) {
if (mapEngine) {
mapEngine.handleTouchStart(event)
}
},
handleTouchMove(event: WechatMiniprogram.TouchEvent) {
if (mapEngine) {
mapEngine.handleTouchMove(event)
}
},
handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
if (mapEngine) {
mapEngine.handleTouchEnd(event)
}
},
handleTouchCancel() {
if (mapEngine) {
mapEngine.handleTouchCancel()
}
},
handleRecenter() {
if (mapEngine) {
mapEngine.handleRecenter()
}
},
handleRotateStep() {
if (mapEngine) {
mapEngine.handleRotateStep()
}
},
handleRotationReset() {
if (mapEngine) {
mapEngine.handleRotationReset()
}
},
handleSetManualMode() {
if (mapEngine) {
mapEngine.handleSetManualMode()
}
},
handleSetNorthUpMode() {
if (mapEngine) {
mapEngine.handleSetNorthUpMode()
}
},
handleSetHeadingUpMode() {
if (mapEngine) {
mapEngine.handleSetHeadingUpMode()
}
},
handleCycleNorthReferenceMode() {
if (mapEngine) {
mapEngine.handleCycleNorthReferenceMode()
}
},
handleAutoRotateCalibrate() {
if (mapEngine) {
mapEngine.handleAutoRotateCalibrate()
}
},
handleToggleGpsTracking() {
if (mapEngine) {
mapEngine.handleToggleGpsTracking()
}
},
handleSetRealLocationMode() {
if (mapEngine) {
mapEngine.handleSetRealLocationMode()
}
},
handleSetMockLocationMode() {
if (mapEngine) {
mapEngine.handleSetMockLocationMode()
}
},
handleConnectMockLocationBridge() {
if (mapEngine) {
mapEngine.handleConnectMockLocationBridge()
}
},
handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
this.setData({
mockBridgeUrlDraft: event.detail.value,
})
},
handleSaveMockBridgeUrl() {
if (mapEngine) {
mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
}
},
handleDisconnectMockLocationBridge() {
if (mapEngine) {
mapEngine.handleDisconnectMockLocationBridge()
}
},
handleSetRealHeartRateMode() {
if (mapEngine) {
mapEngine.handleSetRealHeartRateMode()
}
},
handleSetMockHeartRateMode() {
if (mapEngine) {
mapEngine.handleSetMockHeartRateMode()
}
},
handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) {
this.setData({
mockHeartRateBridgeUrlDraft: event.detail.value,
})
},
handleSaveMockHeartRateBridgeUrl() {
if (mapEngine) {
mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
}
},
handleConnectMockHeartRateBridge() {
if (mapEngine) {
mapEngine.handleConnectMockHeartRateBridge()
}
},
handleDisconnectMockHeartRateBridge() {
if (mapEngine) {
mapEngine.handleDisconnectMockHeartRateBridge()
}
},
handleConnectHeartRate() {
if (mapEngine) {
mapEngine.handleConnectHeartRate()
}
},
handleDisconnectHeartRate() {
if (mapEngine) {
mapEngine.handleDisconnectHeartRate()
}
},
handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
mapEngine.handleConnectHeartRateDevice(event.currentTarget.dataset.deviceId)
}
},
handleClearPreferredHeartRateDevice() {
if (mapEngine) {
mapEngine.handleClearPreferredHeartRateDevice()
}
},
handleDebugHeartRateBlue() {
if (mapEngine) {
mapEngine.handleDebugHeartRateTone('blue')
}
},
handleDebugHeartRatePurple() {
if (mapEngine) {
mapEngine.handleDebugHeartRateTone('purple')
}
},
handleDebugHeartRateGreen() {
if (mapEngine) {
mapEngine.handleDebugHeartRateTone('green')
}
},
handleDebugHeartRateYellow() {
if (mapEngine) {
mapEngine.handleDebugHeartRateTone('yellow')
}
},
handleDebugHeartRateOrange() {
if (mapEngine) {
mapEngine.handleDebugHeartRateTone('orange')
}
},
handleDebugHeartRateRed() {
if (mapEngine) {
mapEngine.handleDebugHeartRateTone('red')
}
},
handleClearDebugHeartRate() {
if (mapEngine) {
mapEngine.handleClearDebugHeartRate()
}
},
handleToggleOsmReference() {
if (mapEngine) {
mapEngine.handleToggleOsmReference()
}
},
handleStartGame() {
if (mapEngine) {
mapEngine.handleStartGame()
}
},
handleLoadClassicConfig() {
this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
},
handleLoadScoreOConfig() {
this.loadMapConfigFromRemote(SCORE_O_REMOTE_GAME_CONFIG_URL, '积分赛配置')
},
handleForceExitGame() {
if (!mapEngine || this.data.gameSessionStatus === 'idle') {
return
}
wx.showModal({
title: '确认退出',
content: '确认强制结束当前对局并返回开始前状态?',
confirmText: '确认退出',
cancelText: '取消',
success: (result) => {
if (result.confirm && mapEngine) {
mapEngine.handleForceExitGame()
}
},
})
},
handleSkipAction() {
if (!mapEngine || !this.data.skipButtonEnabled) {
return
}
if (!mapEngine.shouldConfirmSkipAction()) {
mapEngine.handleSkipAction()
return
}
wx.showModal({
title: '确认跳点',
content: '确认跳过当前检查点并切换到下一个目标点?',
confirmText: '确认跳过',
cancelText: '取消',
success: (result) => {
if (result.confirm && mapEngine) {
mapEngine.handleSkipAction()
}
},
})
},
handleClearMapTestArtifacts() {
if (mapEngine) {
mapEngine.handleClearMapTestArtifacts()
}
},
syncGameInfoPanelSnapshot() {
if (!mapEngine) {
return
}
const snapshot = mapEngine.getGameInfoSnapshot()
const localRows = snapshot.localRows.concat([
{ label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
{ label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
{ label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
{ label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
{ label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
{ label: '比例尺高度', value: `${this.data.centerScaleRulerHeightPx}px` },
{ label: '比例尺主刻度数', value: String(this.data.centerScaleRulerMajorMarks.length) },
])
this.setData({
gameInfoTitle: snapshot.title,
gameInfoSubtitle: snapshot.subtitle,
gameInfoLocalRows: localRows,
gameInfoGlobalRows: snapshot.globalRows,
})
},
handleOpenGameInfoPanel() {
this.syncGameInfoPanelSnapshot()
this.setData({
showDebugPanel: false,
showGameInfoPanel: true,
...buildSideButtonState({
sideButtonMode: this.data.sideButtonMode,
showGameInfoPanel: true,
showCenterScaleRuler: this.data.showCenterScaleRuler,
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
skipButtonEnabled: this.data.skipButtonEnabled,
gameSessionStatus: this.data.gameSessionStatus,
gpsLockEnabled: this.data.gpsLockEnabled,
gpsLockAvailable: this.data.gpsLockAvailable,
}),
})
},
handleCloseGameInfoPanel() {
this.setData({
showGameInfoPanel: false,
...buildSideButtonState({
sideButtonMode: this.data.sideButtonMode,
showGameInfoPanel: false,
showCenterScaleRuler: this.data.showCenterScaleRuler,
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
skipButtonEnabled: this.data.skipButtonEnabled,
gameSessionStatus: this.data.gameSessionStatus,
gpsLockEnabled: this.data.gpsLockEnabled,
gpsLockAvailable: this.data.gpsLockAvailable,
}),
})
},
handleGameInfoPanelTap() {},
handleOverlayTouch() {},
handlePunchAction() {
if (!this.data.punchButtonEnabled) {
return
}
if (mapEngine) {
mapEngine.handlePunchAction()
}
},
handleCloseContentCard() {
if (mapEngine) {
mapEngine.closeContentCard()
}
},
handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
this.setData({
hudPanelIndex: event.detail.current || 0,
})
},
handleCycleSideButtons() {
const nextMode = getNextSideButtonMode(this.data.sideButtonMode)
this.setData({
...buildSideButtonVisibility(nextMode),
...buildSideButtonState({
sideButtonMode: nextMode,
showGameInfoPanel: this.data.showGameInfoPanel,
showCenterScaleRuler: this.data.showCenterScaleRuler,
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
skipButtonEnabled: this.data.skipButtonEnabled,
gameSessionStatus: this.data.gameSessionStatus,
gpsLockEnabled: this.data.gpsLockEnabled,
gpsLockAvailable: this.data.gpsLockAvailable,
}),
})
},
handleToggleGpsLock() {
if (mapEngine) {
mapEngine.handleToggleGpsLock()
}
},
handleToggleMapRotateMode() {
if (!mapEngine) {
return
}
if (this.data.orientationMode === 'heading-up') {
mapEngine.handleSetManualMode()
return
}
mapEngine.handleSetHeadingUpMode()
},
handleToggleDebugPanel() {
this.setData({
showDebugPanel: !this.data.showDebugPanel,
showGameInfoPanel: false,
...buildSideButtonState({
sideButtonMode: this.data.sideButtonMode,
showGameInfoPanel: false,
showCenterScaleRuler: this.data.showCenterScaleRuler,
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
skipButtonEnabled: this.data.skipButtonEnabled,
gameSessionStatus: this.data.gameSessionStatus,
gpsLockEnabled: this.data.gpsLockEnabled,
gpsLockAvailable: this.data.gpsLockAvailable,
}),
})
},
handleCloseDebugPanel() {
this.setData({
showDebugPanel: false,
...buildSideButtonState({
sideButtonMode: this.data.sideButtonMode,
showGameInfoPanel: this.data.showGameInfoPanel,
showCenterScaleRuler: this.data.showCenterScaleRuler,
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
skipButtonEnabled: this.data.skipButtonEnabled,
gameSessionStatus: this.data.gameSessionStatus,
gpsLockEnabled: this.data.gpsLockEnabled,
gpsLockAvailable: this.data.gpsLockAvailable,
}),
})
},
handleToggleCenterScaleRuler() {
const nextEnabled = !this.data.showCenterScaleRuler
this.data.showCenterScaleRuler = nextEnabled
const mergedData = {
...this.data,
showCenterScaleRuler: nextEnabled,
} as MapPageData
this.setData({
showCenterScaleRuler: nextEnabled,
...buildCenterScaleRulerPatch(mergedData),
...buildSideButtonState(mergedData),
})
},
handleToggleCenterScaleRulerAnchor() {
if (!this.data.showCenterScaleRuler) {
return
}
const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
? 'compass-center'
: 'screen-center'
this.data.centerScaleRulerAnchorMode = nextAnchorMode
const mergedData = {
...this.data,
centerScaleRulerAnchorMode: nextAnchorMode,
} as MapPageData
this.setData({
centerScaleRulerAnchorMode: nextAnchorMode,
...buildCenterScaleRulerPatch(mergedData),
...buildSideButtonState(mergedData),
})
if (this.data.showGameInfoPanel) {
this.syncGameInfoPanelSnapshot()
}
},
handleDebugPanelTap() {},
})