map.ts 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268
  1. import {
  2. MapEngine,
  3. type MapEngineGameInfoRow,
  4. type MapEngineGameInfoSnapshot,
  5. type MapEngineStageRect,
  6. type MapEngineViewState,
  7. } from '../../engine/map/mapEngine'
  8. import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
  9. type CompassTickData = {
  10. angle: number
  11. long: boolean
  12. major: boolean
  13. }
  14. type CompassLabelData = {
  15. text: string
  16. angle: number
  17. rotateBack: number
  18. radius: number
  19. className: string
  20. }
  21. type ScaleRulerMinorTickData = {
  22. key: string
  23. topPx: number
  24. long: boolean
  25. }
  26. type ScaleRulerMajorMarkData = {
  27. key: string
  28. topPx: number
  29. label: string
  30. }
  31. type SideButtonMode = 'all' | 'left' | 'right' | 'hidden'
  32. type SideActionButtonState = 'muted' | 'default' | 'active'
  33. type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center'
  34. type MapPageData = MapEngineViewState & {
  35. showDebugPanel: boolean
  36. showGameInfoPanel: boolean
  37. showCenterScaleRuler: boolean
  38. centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
  39. statusBarHeight: number
  40. topInsetHeight: number
  41. hudPanelIndex: number
  42. configSourceText: string
  43. mockBridgeUrlDraft: string
  44. mockHeartRateBridgeUrlDraft: string
  45. gameInfoTitle: string
  46. gameInfoSubtitle: string
  47. gameInfoLocalRows: MapEngineGameInfoRow[]
  48. gameInfoGlobalRows: MapEngineGameInfoRow[]
  49. panelTimerText: string
  50. panelMileageText: string
  51. panelDistanceValueText: string
  52. panelProgressText: string
  53. panelSpeedValueText: string
  54. compassTicks: CompassTickData[]
  55. compassLabels: CompassLabelData[]
  56. sideButtonMode: SideButtonMode
  57. sideToggleIconSrc: string
  58. sideButton2Class: string
  59. sideButton4Class: string
  60. sideButton11Class: string
  61. sideButton13Class: string
  62. sideButton14Class: string
  63. sideButton16Class: string
  64. centerScaleRulerVisible: boolean
  65. centerScaleRulerCenterXPx: number
  66. centerScaleRulerZeroYPx: number
  67. centerScaleRulerHeightPx: number
  68. centerScaleRulerAxisBottomPx: number
  69. centerScaleRulerZeroVisible: boolean
  70. centerScaleRulerZeroLabel: string
  71. centerScaleRulerMinorTicks: ScaleRulerMinorTickData[]
  72. centerScaleRulerMajorMarks: ScaleRulerMajorMarkData[]
  73. showLeftButtonGroup: boolean
  74. showRightButtonGroups: boolean
  75. showBottomDebugButton: boolean
  76. }
  77. const INTERNAL_BUILD_VERSION = 'map-build-252'
  78. const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
  79. const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
  80. let mapEngine: MapEngine | null = null
  81. let stageCanvasAttached = false
  82. function buildSideButtonVisibility(mode: SideButtonMode) {
  83. return {
  84. sideButtonMode: mode,
  85. showLeftButtonGroup: mode === 'all' || mode === 'left' || mode === 'right',
  86. showRightButtonGroups: mode === 'all' || mode === 'right',
  87. showBottomDebugButton: mode !== 'hidden',
  88. }
  89. }
  90. function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
  91. if (currentMode === 'all') {
  92. return 'left'
  93. }
  94. if (currentMode === 'left') {
  95. return 'right'
  96. }
  97. if (currentMode === 'right') {
  98. return 'hidden'
  99. }
  100. return 'left'
  101. }
  102. function buildCompassTicks(): CompassTickData[] {
  103. const ticks: CompassTickData[] = []
  104. for (let angle = 0; angle < 360; angle += 5) {
  105. ticks.push({
  106. angle,
  107. long: angle % 15 === 0,
  108. major: angle % 45 === 0,
  109. })
  110. }
  111. return ticks
  112. }
  113. function buildCompassLabels(): CompassLabelData[] {
  114. return [
  115. { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
  116. { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
  117. { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  118. { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  119. { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  120. { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  121. { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  122. { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
  123. ]
  124. }
  125. function getFallbackStageRect(): MapEngineStageRect {
  126. const systemInfo = wx.getSystemInfoSync()
  127. const width = Math.max(320, systemInfo.windowWidth)
  128. const height = Math.max(280, systemInfo.windowHeight)
  129. return {
  130. width,
  131. height,
  132. left: 0,
  133. top: 0,
  134. }
  135. }
  136. function getSideToggleIconSrc(mode: SideButtonMode): string {
  137. if (mode === 'left') {
  138. return '../../assets/btn_more2.png'
  139. }
  140. if (mode === 'hidden') {
  141. return '../../assets/btn_more1.png'
  142. }
  143. return '../../assets/btn_more3.png'
  144. }
  145. function getSideActionButtonClass(state: SideActionButtonState): string {
  146. if (state === 'muted') {
  147. return 'map-side-button map-side-button--muted'
  148. }
  149. if (state === 'active') {
  150. return 'map-side-button map-side-button--active'
  151. }
  152. return 'map-side-button map-side-button--default'
  153. }
  154. function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
  155. const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
  156. ? 'muted'
  157. : data.gpsLockEnabled
  158. ? 'active'
  159. : 'default'
  160. const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'idle' ? 'default' : 'active'
  161. const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
  162. const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
  163. const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
  164. ? 'muted'
  165. : data.centerScaleRulerAnchorMode === 'compass-center'
  166. ? 'active'
  167. : 'default'
  168. const sideButton16State: SideActionButtonState = data.skipButtonEnabled ? 'default' : 'muted'
  169. return {
  170. sideToggleIconSrc: getSideToggleIconSrc(data.sideButtonMode),
  171. sideButton2Class: getSideActionButtonClass(sideButton2State),
  172. sideButton4Class: getSideActionButtonClass(sideButton4State),
  173. sideButton11Class: getSideActionButtonClass(sideButton11State),
  174. sideButton13Class: getSideActionButtonClass(sideButton13State),
  175. sideButton14Class: getSideActionButtonClass(sideButton14State),
  176. sideButton16Class: getSideActionButtonClass(sideButton16State),
  177. }
  178. }
  179. function getRpxUnitInPx(): number {
  180. const systemInfo = wx.getSystemInfoSync()
  181. return systemInfo.windowWidth / 750
  182. }
  183. function worldTileYToLat(worldTileY: number, zoom: number): number {
  184. const scale = Math.pow(2, zoom)
  185. const n = Math.PI - (2 * Math.PI * worldTileY) / scale
  186. return (180 / Math.PI) * Math.atan(Math.sinh(n))
  187. }
  188. function getNiceDistanceMeters(rawDistanceMeters: number): number {
  189. if (!Number.isFinite(rawDistanceMeters) || rawDistanceMeters <= 0) {
  190. return 50
  191. }
  192. const exponent = Math.floor(Math.log10(rawDistanceMeters))
  193. const base = Math.pow(10, exponent)
  194. const normalized = rawDistanceMeters / base
  195. if (normalized <= 1) {
  196. return base
  197. }
  198. if (normalized <= 2) {
  199. return 2 * base
  200. }
  201. if (normalized <= 5) {
  202. return 5 * base
  203. }
  204. return 10 * base
  205. }
  206. function formatScaleDistanceLabel(distanceMeters: number): string {
  207. if (distanceMeters >= 1000) {
  208. const distanceKm = distanceMeters / 1000
  209. const formatted = distanceKm >= 10 ? distanceKm.toFixed(0) : distanceKm.toFixed(1)
  210. return `${formatted.replace(/\.0$/, '')} km`
  211. }
  212. return `${Math.round(distanceMeters)} m`
  213. }
  214. function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
  215. if (!data.showCenterScaleRuler) {
  216. return {
  217. centerScaleRulerVisible: false,
  218. centerScaleRulerCenterXPx: 0,
  219. centerScaleRulerZeroYPx: 0,
  220. centerScaleRulerHeightPx: 0,
  221. centerScaleRulerAxisBottomPx: 0,
  222. centerScaleRulerZeroVisible: false,
  223. centerScaleRulerZeroLabel: '0 m',
  224. centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
  225. centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
  226. }
  227. }
  228. if (!data.stageWidth || !data.stageHeight) {
  229. return {
  230. centerScaleRulerVisible: false,
  231. centerScaleRulerCenterXPx: 0,
  232. centerScaleRulerZeroYPx: 0,
  233. centerScaleRulerHeightPx: 0,
  234. centerScaleRulerAxisBottomPx: 0,
  235. centerScaleRulerZeroVisible: false,
  236. centerScaleRulerZeroLabel: '0 m',
  237. centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
  238. centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
  239. }
  240. }
  241. const topPadding = 12
  242. const rpxUnitPx = getRpxUnitInPx()
  243. const compassBottomPaddingPx = 248 * rpxUnitPx
  244. const compassDialRadiusPx = (196 * rpxUnitPx) / 2
  245. const compassHeadingOverlayHeightPx = 40 * rpxUnitPx
  246. const compassOcclusionPaddingPx = 10 * rpxUnitPx
  247. const zeroYPx = data.centerScaleRulerAnchorMode === 'compass-center'
  248. ? Math.round(data.stageHeight - compassBottomPaddingPx - compassDialRadiusPx)
  249. : Math.round(data.stageHeight / 2)
  250. const fallbackHeight = Math.max(zeroYPx - topPadding, 160)
  251. const coveredBottomPx = data.centerScaleRulerAnchorMode === 'compass-center'
  252. ? Math.round(compassDialRadiusPx + compassHeadingOverlayHeightPx + compassOcclusionPaddingPx)
  253. : 0
  254. if (
  255. !data.tileSizePx
  256. || !Number.isFinite(data.zoom)
  257. || !Number.isFinite(data.centerTileY)
  258. ) {
  259. return {
  260. centerScaleRulerVisible: true,
  261. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  262. centerScaleRulerZeroYPx: zeroYPx,
  263. centerScaleRulerHeightPx: fallbackHeight,
  264. centerScaleRulerAxisBottomPx: coveredBottomPx,
  265. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  266. centerScaleRulerZeroLabel: '0 m',
  267. centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
  268. centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
  269. }
  270. }
  271. const centerLat = worldTileYToLat(data.centerTileY + 0.5, data.zoom)
  272. const metersPerTile = Math.cos(centerLat * Math.PI / 180) * 40075016.686 / Math.pow(2, data.zoom)
  273. const metersPerPixel = metersPerTile / data.tileSizePx
  274. const effectivePreviewScale = Number.isFinite(data.previewScale) && data.previewScale > 0 ? data.previewScale : 1
  275. const effectiveMetersPerPixel = metersPerPixel / effectivePreviewScale
  276. const rulerHeight = Math.floor(zeroYPx - topPadding)
  277. if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
  278. return {
  279. centerScaleRulerVisible: true,
  280. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  281. centerScaleRulerZeroYPx: zeroYPx,
  282. centerScaleRulerHeightPx: Math.max(rulerHeight, fallbackHeight),
  283. centerScaleRulerAxisBottomPx: coveredBottomPx,
  284. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  285. centerScaleRulerZeroLabel: '0 m',
  286. centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
  287. centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
  288. }
  289. }
  290. const labelDistanceMeters = getNiceDistanceMeters(effectiveMetersPerPixel * 80)
  291. const minorDistanceMeters = labelDistanceMeters / 8
  292. const minorStepPx = minorDistanceMeters / effectiveMetersPerPixel
  293. const visibleTopLimitPx = rulerHeight - coveredBottomPx
  294. const minorTicks: ScaleRulerMinorTickData[] = []
  295. const majorMarks: ScaleRulerMajorMarkData[] = []
  296. for (let index = 1; index <= 200; index += 1) {
  297. const topPx = Math.round(rulerHeight - index * minorStepPx)
  298. if (topPx < 0) {
  299. break
  300. }
  301. if (topPx >= visibleTopLimitPx) {
  302. continue
  303. }
  304. const isHalfMajor = index % 4 === 0
  305. const isLabelMajor = index % 8 === 0
  306. minorTicks.push({
  307. key: `minor-${index}`,
  308. topPx,
  309. long: isHalfMajor,
  310. })
  311. if (isLabelMajor) {
  312. majorMarks.push({
  313. key: `major-${index}`,
  314. topPx,
  315. label: formatScaleDistanceLabel((index / 8) * labelDistanceMeters),
  316. })
  317. }
  318. }
  319. return {
  320. centerScaleRulerVisible: true,
  321. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  322. centerScaleRulerZeroYPx: zeroYPx,
  323. centerScaleRulerHeightPx: rulerHeight,
  324. centerScaleRulerAxisBottomPx: coveredBottomPx,
  325. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  326. centerScaleRulerZeroLabel: '0 m',
  327. centerScaleRulerMinorTicks: minorTicks,
  328. centerScaleRulerMajorMarks: majorMarks,
  329. }
  330. }
  331. function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
  332. return {
  333. title: '当前游戏',
  334. subtitle: '未开始',
  335. localRows: [],
  336. globalRows: [
  337. { label: '全球积分', value: '未接入' },
  338. { label: '全球排名', value: '未接入' },
  339. { label: '在线人数', value: '未接入' },
  340. { label: '队伍状态', value: '未接入' },
  341. { label: '实时广播', value: '未接入' },
  342. ],
  343. }
  344. }
  345. Page({
  346. data: {
  347. showDebugPanel: false,
  348. showGameInfoPanel: false,
  349. showCenterScaleRuler: false,
  350. statusBarHeight: 0,
  351. topInsetHeight: 12,
  352. hudPanelIndex: 0,
  353. configSourceText: '顺序赛配置',
  354. centerScaleRulerAnchorMode: 'screen-center',
  355. gameInfoTitle: '当前游戏',
  356. gameInfoSubtitle: '未开始',
  357. gameInfoLocalRows: [],
  358. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  359. panelTimerText: '00:00:00',
  360. panelMileageText: '0m',
  361. panelActionTagText: '目标',
  362. panelDistanceTagText: '点距',
  363. panelDistanceValueText: '--',
  364. panelDistanceUnitText: '',
  365. panelProgressText: '0/0',
  366. gameSessionStatus: 'idle',
  367. gameModeText: '顺序赛',
  368. gpsLockEnabled: false,
  369. gpsLockAvailable: false,
  370. locationSourceMode: 'real',
  371. locationSourceText: '真实定位',
  372. mockBridgeConnected: false,
  373. mockBridgeStatusText: '未连接',
  374. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  375. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  376. mockCoordText: '--',
  377. mockSpeedText: '--',
  378. heartRateSourceMode: 'real',
  379. heartRateSourceText: '真实心率',
  380. mockHeartRateBridgeConnected: false,
  381. mockHeartRateBridgeStatusText: '未连接',
  382. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  383. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  384. mockHeartRateText: '--',
  385. heartRateScanText: '未扫描',
  386. heartRateDiscoveredDevices: [],
  387. panelSpeedValueText: '0',
  388. panelTelemetryTone: 'blue',
  389. panelHeartRateZoneNameText: '--',
  390. panelHeartRateZoneRangeText: '',
  391. heartRateConnected: false,
  392. heartRateStatusText: '心率带未连接',
  393. heartRateDeviceText: '--',
  394. panelHeartRateValueText: '--',
  395. panelHeartRateUnitText: '',
  396. panelCaloriesValueText: '0',
  397. panelCaloriesUnitText: 'kcal',
  398. panelAverageSpeedValueText: '0',
  399. panelAverageSpeedUnitText: 'km/h',
  400. panelAccuracyValueText: '--',
  401. panelAccuracyUnitText: '',
  402. deviceHeadingText: '--',
  403. devicePoseText: '竖持',
  404. headingConfidenceText: '低',
  405. accelerometerText: '--',
  406. gyroscopeText: '--',
  407. deviceMotionText: '--',
  408. punchButtonText: '打点',
  409. punchButtonEnabled: false,
  410. skipButtonEnabled: false,
  411. punchHintText: '等待进入检查点范围',
  412. punchFeedbackVisible: false,
  413. punchFeedbackText: '',
  414. punchFeedbackTone: 'neutral',
  415. contentCardVisible: false,
  416. contentCardTitle: '',
  417. contentCardBody: '',
  418. punchButtonFxClass: '',
  419. punchFeedbackFxClass: '',
  420. contentCardFxClass: '',
  421. mapPulseVisible: false,
  422. mapPulseLeftPx: 0,
  423. mapPulseTopPx: 0,
  424. mapPulseFxClass: '',
  425. stageFxVisible: false,
  426. stageFxClass: '',
  427. centerScaleRulerVisible: false,
  428. centerScaleRulerCenterXPx: 0,
  429. centerScaleRulerZeroYPx: 0,
  430. centerScaleRulerHeightPx: 0,
  431. centerScaleRulerAxisBottomPx: 0,
  432. centerScaleRulerZeroVisible: false,
  433. centerScaleRulerZeroLabel: '0 m',
  434. centerScaleRulerMinorTicks: [],
  435. centerScaleRulerMajorMarks: [],
  436. compassTicks: buildCompassTicks(),
  437. compassLabels: buildCompassLabels(),
  438. ...buildSideButtonVisibility('left'),
  439. ...buildSideButtonState({
  440. sideButtonMode: 'left',
  441. showGameInfoPanel: false,
  442. showCenterScaleRuler: false,
  443. centerScaleRulerAnchorMode: 'screen-center',
  444. skipButtonEnabled: false,
  445. gameSessionStatus: 'idle',
  446. gpsLockEnabled: false,
  447. gpsLockAvailable: false,
  448. }),
  449. } as unknown as MapPageData,
  450. onLoad() {
  451. const systemInfo = wx.getSystemInfoSync()
  452. const statusBarHeight = systemInfo.statusBarHeight || 0
  453. const menuButtonRect = wx.getMenuButtonBoundingClientRect()
  454. const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
  455. if (mapEngine) {
  456. mapEngine.destroy()
  457. mapEngine = null
  458. }
  459. mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
  460. onData: (patch) => {
  461. const nextPatch = patch as Partial<MapPageData>
  462. const nextData: Partial<MapPageData> = {
  463. ...nextPatch,
  464. }
  465. if (
  466. typeof nextPatch.mockBridgeUrlText === 'string'
  467. && this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
  468. ) {
  469. nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText
  470. }
  471. if (
  472. typeof nextPatch.mockHeartRateBridgeUrlText === 'string'
  473. && this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText
  474. ) {
  475. nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
  476. }
  477. const mergedData = {
  478. ...this.data,
  479. ...nextData,
  480. } as MapPageData
  481. this.setData({
  482. ...nextData,
  483. ...buildCenterScaleRulerPatch(mergedData),
  484. ...buildSideButtonState(mergedData),
  485. })
  486. if (this.data.showGameInfoPanel) {
  487. this.syncGameInfoPanelSnapshot()
  488. }
  489. },
  490. })
  491. this.setData({
  492. ...mapEngine.getInitialData(),
  493. showDebugPanel: false,
  494. showGameInfoPanel: false,
  495. statusBarHeight,
  496. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  497. hudPanelIndex: 0,
  498. configSourceText: '顺序赛配置',
  499. gameInfoTitle: '当前游戏',
  500. gameInfoSubtitle: '未开始',
  501. gameInfoLocalRows: [],
  502. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  503. panelTimerText: '00:00:00',
  504. panelMileageText: '0m',
  505. panelActionTagText: '目标',
  506. panelDistanceTagText: '点距',
  507. panelDistanceValueText: '--',
  508. panelDistanceUnitText: '',
  509. panelProgressText: '0/0',
  510. gameSessionStatus: 'idle',
  511. gameModeText: '顺序赛',
  512. gpsLockEnabled: false,
  513. gpsLockAvailable: false,
  514. locationSourceMode: 'real',
  515. locationSourceText: '真实定位',
  516. mockBridgeConnected: false,
  517. mockBridgeStatusText: '未连接',
  518. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  519. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  520. mockCoordText: '--',
  521. mockSpeedText: '--',
  522. heartRateSourceMode: 'real',
  523. heartRateSourceText: '真实心率',
  524. mockHeartRateBridgeConnected: false,
  525. mockHeartRateBridgeStatusText: '未连接',
  526. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  527. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  528. mockHeartRateText: '--',
  529. panelSpeedValueText: '0',
  530. panelTelemetryTone: 'blue',
  531. panelHeartRateZoneNameText: '--',
  532. panelHeartRateZoneRangeText: '',
  533. heartRateConnected: false,
  534. heartRateStatusText: '心率带未连接',
  535. heartRateDeviceText: '--',
  536. panelHeartRateValueText: '--',
  537. panelHeartRateUnitText: '',
  538. panelCaloriesValueText: '0',
  539. panelCaloriesUnitText: 'kcal',
  540. panelAverageSpeedValueText: '0',
  541. panelAverageSpeedUnitText: 'km/h',
  542. panelAccuracyValueText: '--',
  543. panelAccuracyUnitText: '',
  544. deviceHeadingText: '--',
  545. devicePoseText: '竖持',
  546. headingConfidenceText: '低',
  547. accelerometerText: '--',
  548. gyroscopeText: '--',
  549. deviceMotionText: '--',
  550. punchButtonText: '打点',
  551. punchButtonEnabled: false,
  552. skipButtonEnabled: false,
  553. punchHintText: '等待进入检查点范围',
  554. punchFeedbackVisible: false,
  555. punchFeedbackText: '',
  556. punchFeedbackTone: 'neutral',
  557. contentCardVisible: false,
  558. contentCardTitle: '',
  559. contentCardBody: '',
  560. punchButtonFxClass: '',
  561. punchFeedbackFxClass: '',
  562. contentCardFxClass: '',
  563. mapPulseVisible: false,
  564. mapPulseLeftPx: 0,
  565. mapPulseTopPx: 0,
  566. mapPulseFxClass: '',
  567. stageFxVisible: false,
  568. stageFxClass: '',
  569. compassTicks: buildCompassTicks(),
  570. compassLabels: buildCompassLabels(),
  571. ...buildSideButtonVisibility('left'),
  572. ...buildSideButtonState({
  573. sideButtonMode: 'left',
  574. showGameInfoPanel: false,
  575. showCenterScaleRuler: false,
  576. centerScaleRulerAnchorMode: 'screen-center',
  577. skipButtonEnabled: false,
  578. gameSessionStatus: 'idle',
  579. gpsLockEnabled: false,
  580. gpsLockAvailable: false,
  581. }),
  582. ...buildCenterScaleRulerPatch({
  583. ...(mapEngine.getInitialData() as MapPageData),
  584. showCenterScaleRuler: false,
  585. centerScaleRulerAnchorMode: 'screen-center',
  586. stageWidth: 0,
  587. stageHeight: 0,
  588. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  589. zoom: 0,
  590. centerTileY: 0,
  591. tileSizePx: 0,
  592. }),
  593. })
  594. },
  595. onReady() {
  596. stageCanvasAttached = false
  597. this.measureStageAndCanvas()
  598. this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
  599. },
  600. onShow() {
  601. if (mapEngine) {
  602. mapEngine.handleAppShow()
  603. }
  604. },
  605. onHide() {
  606. if (mapEngine) {
  607. mapEngine.handleAppHide()
  608. }
  609. },
  610. onUnload() {
  611. if (mapEngine) {
  612. mapEngine.destroy()
  613. mapEngine = null
  614. }
  615. stageCanvasAttached = false
  616. },
  617. loadMapConfigFromRemote(configUrl: string, configLabel: string) {
  618. const currentEngine = mapEngine
  619. if (!currentEngine) {
  620. return
  621. }
  622. this.setData({
  623. configSourceText: configLabel,
  624. configStatusText: `加载中: ${configLabel}`,
  625. })
  626. loadRemoteMapConfig(configUrl)
  627. .then((config) => {
  628. if (mapEngine !== currentEngine) {
  629. return
  630. }
  631. currentEngine.applyRemoteMapConfig(config)
  632. })
  633. .catch((error) => {
  634. if (mapEngine !== currentEngine) {
  635. return
  636. }
  637. const errorMessage = error && error.message ? error.message : '未知错误'
  638. this.setData({
  639. configStatusText: `载入失败: ${errorMessage}`,
  640. statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
  641. })
  642. })
  643. },
  644. measureStageAndCanvas() {
  645. const page = this
  646. const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
  647. const fallbackRect = getFallbackStageRect()
  648. const rect: MapEngineStageRect = {
  649. width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
  650. height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
  651. left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
  652. top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
  653. }
  654. const currentEngine = mapEngine
  655. if (!currentEngine) {
  656. return
  657. }
  658. currentEngine.setStage(rect)
  659. if (stageCanvasAttached) {
  660. return
  661. }
  662. const canvasQuery = wx.createSelectorQuery().in(page)
  663. canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
  664. canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
  665. canvasQuery.exec((canvasRes) => {
  666. const canvasRef = canvasRes[0] as any
  667. const labelCanvasRef = canvasRes[1] as any
  668. if (!canvasRef || !canvasRef.node) {
  669. page.setData({
  670. statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
  671. })
  672. return
  673. }
  674. const dpr = wx.getSystemInfoSync().pixelRatio || 1
  675. try {
  676. currentEngine.attachCanvas(
  677. canvasRef.node,
  678. rect.width,
  679. rect.height,
  680. dpr,
  681. labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
  682. )
  683. stageCanvasAttached = true
  684. } catch (error) {
  685. page.setData({
  686. statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
  687. })
  688. }
  689. })
  690. }
  691. const query = wx.createSelectorQuery().in(page)
  692. query.select('.map-stage').boundingClientRect()
  693. query.exec((res) => {
  694. const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
  695. applyStage(rect)
  696. })
  697. },
  698. handleTouchStart(event: WechatMiniprogram.TouchEvent) {
  699. if (mapEngine) {
  700. mapEngine.handleTouchStart(event)
  701. }
  702. },
  703. handleTouchMove(event: WechatMiniprogram.TouchEvent) {
  704. if (mapEngine) {
  705. mapEngine.handleTouchMove(event)
  706. }
  707. },
  708. handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
  709. if (mapEngine) {
  710. mapEngine.handleTouchEnd(event)
  711. }
  712. },
  713. handleTouchCancel() {
  714. if (mapEngine) {
  715. mapEngine.handleTouchCancel()
  716. }
  717. },
  718. handleRecenter() {
  719. if (mapEngine) {
  720. mapEngine.handleRecenter()
  721. }
  722. },
  723. handleRotateStep() {
  724. if (mapEngine) {
  725. mapEngine.handleRotateStep()
  726. }
  727. },
  728. handleRotationReset() {
  729. if (mapEngine) {
  730. mapEngine.handleRotationReset()
  731. }
  732. },
  733. handleSetManualMode() {
  734. if (mapEngine) {
  735. mapEngine.handleSetManualMode()
  736. }
  737. },
  738. handleSetNorthUpMode() {
  739. if (mapEngine) {
  740. mapEngine.handleSetNorthUpMode()
  741. }
  742. },
  743. handleSetHeadingUpMode() {
  744. if (mapEngine) {
  745. mapEngine.handleSetHeadingUpMode()
  746. }
  747. },
  748. handleCycleNorthReferenceMode() {
  749. if (mapEngine) {
  750. mapEngine.handleCycleNorthReferenceMode()
  751. }
  752. },
  753. handleAutoRotateCalibrate() {
  754. if (mapEngine) {
  755. mapEngine.handleAutoRotateCalibrate()
  756. }
  757. },
  758. handleToggleGpsTracking() {
  759. if (mapEngine) {
  760. mapEngine.handleToggleGpsTracking()
  761. }
  762. },
  763. handleSetRealLocationMode() {
  764. if (mapEngine) {
  765. mapEngine.handleSetRealLocationMode()
  766. }
  767. },
  768. handleSetMockLocationMode() {
  769. if (mapEngine) {
  770. mapEngine.handleSetMockLocationMode()
  771. }
  772. },
  773. handleConnectMockLocationBridge() {
  774. if (mapEngine) {
  775. mapEngine.handleConnectMockLocationBridge()
  776. }
  777. },
  778. handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
  779. this.setData({
  780. mockBridgeUrlDraft: event.detail.value,
  781. })
  782. },
  783. handleSaveMockBridgeUrl() {
  784. if (mapEngine) {
  785. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  786. }
  787. },
  788. handleDisconnectMockLocationBridge() {
  789. if (mapEngine) {
  790. mapEngine.handleDisconnectMockLocationBridge()
  791. }
  792. },
  793. handleSetRealHeartRateMode() {
  794. if (mapEngine) {
  795. mapEngine.handleSetRealHeartRateMode()
  796. }
  797. },
  798. handleSetMockHeartRateMode() {
  799. if (mapEngine) {
  800. mapEngine.handleSetMockHeartRateMode()
  801. }
  802. },
  803. handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) {
  804. this.setData({
  805. mockHeartRateBridgeUrlDraft: event.detail.value,
  806. })
  807. },
  808. handleSaveMockHeartRateBridgeUrl() {
  809. if (mapEngine) {
  810. mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
  811. }
  812. },
  813. handleConnectMockHeartRateBridge() {
  814. if (mapEngine) {
  815. mapEngine.handleConnectMockHeartRateBridge()
  816. }
  817. },
  818. handleDisconnectMockHeartRateBridge() {
  819. if (mapEngine) {
  820. mapEngine.handleDisconnectMockHeartRateBridge()
  821. }
  822. },
  823. handleConnectHeartRate() {
  824. if (mapEngine) {
  825. mapEngine.handleConnectHeartRate()
  826. }
  827. },
  828. handleDisconnectHeartRate() {
  829. if (mapEngine) {
  830. mapEngine.handleDisconnectHeartRate()
  831. }
  832. },
  833. handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
  834. if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
  835. mapEngine.handleConnectHeartRateDevice(event.currentTarget.dataset.deviceId)
  836. }
  837. },
  838. handleClearPreferredHeartRateDevice() {
  839. if (mapEngine) {
  840. mapEngine.handleClearPreferredHeartRateDevice()
  841. }
  842. },
  843. handleDebugHeartRateBlue() {
  844. if (mapEngine) {
  845. mapEngine.handleDebugHeartRateTone('blue')
  846. }
  847. },
  848. handleDebugHeartRatePurple() {
  849. if (mapEngine) {
  850. mapEngine.handleDebugHeartRateTone('purple')
  851. }
  852. },
  853. handleDebugHeartRateGreen() {
  854. if (mapEngine) {
  855. mapEngine.handleDebugHeartRateTone('green')
  856. }
  857. },
  858. handleDebugHeartRateYellow() {
  859. if (mapEngine) {
  860. mapEngine.handleDebugHeartRateTone('yellow')
  861. }
  862. },
  863. handleDebugHeartRateOrange() {
  864. if (mapEngine) {
  865. mapEngine.handleDebugHeartRateTone('orange')
  866. }
  867. },
  868. handleDebugHeartRateRed() {
  869. if (mapEngine) {
  870. mapEngine.handleDebugHeartRateTone('red')
  871. }
  872. },
  873. handleClearDebugHeartRate() {
  874. if (mapEngine) {
  875. mapEngine.handleClearDebugHeartRate()
  876. }
  877. },
  878. handleToggleOsmReference() {
  879. if (mapEngine) {
  880. mapEngine.handleToggleOsmReference()
  881. }
  882. },
  883. handleStartGame() {
  884. if (mapEngine) {
  885. mapEngine.handleStartGame()
  886. }
  887. },
  888. handleLoadClassicConfig() {
  889. this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
  890. },
  891. handleLoadScoreOConfig() {
  892. this.loadMapConfigFromRemote(SCORE_O_REMOTE_GAME_CONFIG_URL, '积分赛配置')
  893. },
  894. handleForceExitGame() {
  895. if (!mapEngine || this.data.gameSessionStatus === 'idle') {
  896. return
  897. }
  898. wx.showModal({
  899. title: '确认退出',
  900. content: '确认强制结束当前对局并返回开始前状态?',
  901. confirmText: '确认退出',
  902. cancelText: '取消',
  903. success: (result) => {
  904. if (result.confirm && mapEngine) {
  905. mapEngine.handleForceExitGame()
  906. }
  907. },
  908. })
  909. },
  910. handleSkipAction() {
  911. if (!mapEngine || !this.data.skipButtonEnabled) {
  912. return
  913. }
  914. if (!mapEngine.shouldConfirmSkipAction()) {
  915. mapEngine.handleSkipAction()
  916. return
  917. }
  918. wx.showModal({
  919. title: '确认跳点',
  920. content: '确认跳过当前检查点并切换到下一个目标点?',
  921. confirmText: '确认跳过',
  922. cancelText: '取消',
  923. success: (result) => {
  924. if (result.confirm && mapEngine) {
  925. mapEngine.handleSkipAction()
  926. }
  927. },
  928. })
  929. },
  930. handleClearMapTestArtifacts() {
  931. if (mapEngine) {
  932. mapEngine.handleClearMapTestArtifacts()
  933. }
  934. },
  935. syncGameInfoPanelSnapshot() {
  936. if (!mapEngine) {
  937. return
  938. }
  939. const snapshot = mapEngine.getGameInfoSnapshot()
  940. const localRows = snapshot.localRows.concat([
  941. { label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
  942. { label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
  943. { label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
  944. { label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
  945. { label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
  946. { label: '比例尺高度', value: `${this.data.centerScaleRulerHeightPx}px` },
  947. { label: '比例尺主刻度数', value: String(this.data.centerScaleRulerMajorMarks.length) },
  948. ])
  949. this.setData({
  950. gameInfoTitle: snapshot.title,
  951. gameInfoSubtitle: snapshot.subtitle,
  952. gameInfoLocalRows: localRows,
  953. gameInfoGlobalRows: snapshot.globalRows,
  954. })
  955. },
  956. handleOpenGameInfoPanel() {
  957. this.syncGameInfoPanelSnapshot()
  958. this.setData({
  959. showDebugPanel: false,
  960. showGameInfoPanel: true,
  961. ...buildSideButtonState({
  962. sideButtonMode: this.data.sideButtonMode,
  963. showGameInfoPanel: true,
  964. showCenterScaleRuler: this.data.showCenterScaleRuler,
  965. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  966. skipButtonEnabled: this.data.skipButtonEnabled,
  967. gameSessionStatus: this.data.gameSessionStatus,
  968. gpsLockEnabled: this.data.gpsLockEnabled,
  969. gpsLockAvailable: this.data.gpsLockAvailable,
  970. }),
  971. })
  972. },
  973. handleCloseGameInfoPanel() {
  974. this.setData({
  975. showGameInfoPanel: false,
  976. ...buildSideButtonState({
  977. sideButtonMode: this.data.sideButtonMode,
  978. showGameInfoPanel: false,
  979. showCenterScaleRuler: this.data.showCenterScaleRuler,
  980. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  981. skipButtonEnabled: this.data.skipButtonEnabled,
  982. gameSessionStatus: this.data.gameSessionStatus,
  983. gpsLockEnabled: this.data.gpsLockEnabled,
  984. gpsLockAvailable: this.data.gpsLockAvailable,
  985. }),
  986. })
  987. },
  988. handleGameInfoPanelTap() {},
  989. handleOverlayTouch() {},
  990. handlePunchAction() {
  991. if (!this.data.punchButtonEnabled) {
  992. return
  993. }
  994. if (mapEngine) {
  995. mapEngine.handlePunchAction()
  996. }
  997. },
  998. handleCloseContentCard() {
  999. if (mapEngine) {
  1000. mapEngine.closeContentCard()
  1001. }
  1002. },
  1003. handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
  1004. this.setData({
  1005. hudPanelIndex: event.detail.current || 0,
  1006. })
  1007. },
  1008. handleCycleSideButtons() {
  1009. const nextMode = getNextSideButtonMode(this.data.sideButtonMode)
  1010. this.setData({
  1011. ...buildSideButtonVisibility(nextMode),
  1012. ...buildSideButtonState({
  1013. sideButtonMode: nextMode,
  1014. showGameInfoPanel: this.data.showGameInfoPanel,
  1015. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1016. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1017. skipButtonEnabled: this.data.skipButtonEnabled,
  1018. gameSessionStatus: this.data.gameSessionStatus,
  1019. gpsLockEnabled: this.data.gpsLockEnabled,
  1020. gpsLockAvailable: this.data.gpsLockAvailable,
  1021. }),
  1022. })
  1023. },
  1024. handleToggleGpsLock() {
  1025. if (mapEngine) {
  1026. mapEngine.handleToggleGpsLock()
  1027. }
  1028. },
  1029. handleToggleMapRotateMode() {
  1030. if (!mapEngine) {
  1031. return
  1032. }
  1033. if (this.data.orientationMode === 'heading-up') {
  1034. mapEngine.handleSetManualMode()
  1035. return
  1036. }
  1037. mapEngine.handleSetHeadingUpMode()
  1038. },
  1039. handleToggleDebugPanel() {
  1040. this.setData({
  1041. showDebugPanel: !this.data.showDebugPanel,
  1042. showGameInfoPanel: false,
  1043. ...buildSideButtonState({
  1044. sideButtonMode: this.data.sideButtonMode,
  1045. showGameInfoPanel: false,
  1046. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1047. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1048. skipButtonEnabled: this.data.skipButtonEnabled,
  1049. gameSessionStatus: this.data.gameSessionStatus,
  1050. gpsLockEnabled: this.data.gpsLockEnabled,
  1051. gpsLockAvailable: this.data.gpsLockAvailable,
  1052. }),
  1053. })
  1054. },
  1055. handleCloseDebugPanel() {
  1056. this.setData({
  1057. showDebugPanel: false,
  1058. ...buildSideButtonState({
  1059. sideButtonMode: this.data.sideButtonMode,
  1060. showGameInfoPanel: this.data.showGameInfoPanel,
  1061. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1062. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1063. skipButtonEnabled: this.data.skipButtonEnabled,
  1064. gameSessionStatus: this.data.gameSessionStatus,
  1065. gpsLockEnabled: this.data.gpsLockEnabled,
  1066. gpsLockAvailable: this.data.gpsLockAvailable,
  1067. }),
  1068. })
  1069. },
  1070. handleToggleCenterScaleRuler() {
  1071. const nextEnabled = !this.data.showCenterScaleRuler
  1072. this.data.showCenterScaleRuler = nextEnabled
  1073. const mergedData = {
  1074. ...this.data,
  1075. showCenterScaleRuler: nextEnabled,
  1076. } as MapPageData
  1077. this.setData({
  1078. showCenterScaleRuler: nextEnabled,
  1079. ...buildCenterScaleRulerPatch(mergedData),
  1080. ...buildSideButtonState(mergedData),
  1081. })
  1082. },
  1083. handleToggleCenterScaleRulerAnchor() {
  1084. if (!this.data.showCenterScaleRuler) {
  1085. return
  1086. }
  1087. const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
  1088. ? 'compass-center'
  1089. : 'screen-center'
  1090. this.data.centerScaleRulerAnchorMode = nextAnchorMode
  1091. const mergedData = {
  1092. ...this.data,
  1093. centerScaleRulerAnchorMode: nextAnchorMode,
  1094. } as MapPageData
  1095. this.setData({
  1096. centerScaleRulerAnchorMode: nextAnchorMode,
  1097. ...buildCenterScaleRulerPatch(mergedData),
  1098. ...buildSideButtonState(mergedData),
  1099. })
  1100. if (this.data.showGameInfoPanel) {
  1101. this.syncGameInfoPanelSnapshot()
  1102. }
  1103. },
  1104. handleDebugPanelTap() {},
  1105. })