| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268 |
- 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() {},
- })
|