map.ts 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901
  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. import { type AnimationLevel } from '../../utils/animationLevel'
  10. type CompassTickData = {
  11. angle: number
  12. long: boolean
  13. major: boolean
  14. }
  15. type CompassLabelData = {
  16. text: string
  17. angle: number
  18. rotateBack: number
  19. radius: number
  20. className: string
  21. }
  22. type ScaleRulerMinorTickData = {
  23. key: string
  24. topPx: number
  25. long: boolean
  26. }
  27. type ScaleRulerMajorMarkData = {
  28. key: string
  29. topPx: number
  30. label: string
  31. }
  32. type SideButtonMode = 'all' | 'left' | 'right' | 'hidden'
  33. type SideActionButtonState = 'muted' | 'default' | 'active'
  34. type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center'
  35. type UserNorthReferenceMode = 'magnetic' | 'true'
  36. type StoredUserSettings = {
  37. animationLevel?: AnimationLevel
  38. northReferenceMode?: UserNorthReferenceMode
  39. showCenterScaleRuler?: boolean
  40. centerScaleRulerAnchorMode?: CenterScaleRulerAnchorMode
  41. }
  42. type MapPageData = MapEngineViewState & {
  43. showDebugPanel: boolean
  44. showGameInfoPanel: boolean
  45. showSystemSettingsPanel: boolean
  46. showCenterScaleRuler: boolean
  47. showPunchHintBanner: boolean
  48. centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
  49. statusBarHeight: number
  50. topInsetHeight: number
  51. hudPanelIndex: number
  52. configSourceText: string
  53. mockBridgeUrlDraft: string
  54. mockHeartRateBridgeUrlDraft: string
  55. gameInfoTitle: string
  56. gameInfoSubtitle: string
  57. gameInfoLocalRows: MapEngineGameInfoRow[]
  58. gameInfoGlobalRows: MapEngineGameInfoRow[]
  59. panelTimerText: string
  60. panelMileageText: string
  61. panelDistanceValueText: string
  62. panelProgressText: string
  63. panelSpeedValueText: string
  64. panelTimerFxClass: string
  65. panelMileageFxClass: string
  66. panelSpeedFxClass: string
  67. panelHeartRateFxClass: string
  68. compassTicks: CompassTickData[]
  69. compassLabels: CompassLabelData[]
  70. sideButtonMode: SideButtonMode
  71. sideToggleIconSrc: string
  72. sideButton2Class: string
  73. sideButton4Class: string
  74. sideButton11Class: string
  75. sideButton12Class: string
  76. sideButton13Class: string
  77. sideButton14Class: string
  78. sideButton16Class: string
  79. centerScaleRulerVisible: boolean
  80. centerScaleRulerCenterXPx: number
  81. centerScaleRulerZeroYPx: number
  82. centerScaleRulerHeightPx: number
  83. centerScaleRulerAxisBottomPx: number
  84. centerScaleRulerZeroVisible: boolean
  85. centerScaleRulerZeroLabel: string
  86. centerScaleRulerMinorTicks: ScaleRulerMinorTickData[]
  87. centerScaleRulerMajorMarks: ScaleRulerMajorMarkData[]
  88. showLeftButtonGroup: boolean
  89. showRightButtonGroups: boolean
  90. showBottomDebugButton: boolean
  91. }
  92. const INTERNAL_BUILD_VERSION = 'map-build-283'
  93. const USER_SETTINGS_STORAGE_KEY = 'cmr_user_settings_v1'
  94. const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
  95. const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
  96. const PUNCH_HINT_AUTO_HIDE_MS = 30000
  97. let mapEngine: MapEngine | null = null
  98. let stageCanvasAttached = false
  99. let gameInfoPanelSyncTimer = 0
  100. let centerScaleRulerSyncTimer = 0
  101. let centerScaleRulerUpdateTimer = 0
  102. let punchHintDismissTimer = 0
  103. let panelTimerFxTimer = 0
  104. let panelMileageFxTimer = 0
  105. let panelSpeedFxTimer = 0
  106. let panelHeartRateFxTimer = 0
  107. let lastCenterScaleRulerStablePatch: Pick<
  108. MapPageData,
  109. | 'centerScaleRulerVisible'
  110. | 'centerScaleRulerCenterXPx'
  111. | 'centerScaleRulerZeroYPx'
  112. | 'centerScaleRulerHeightPx'
  113. | 'centerScaleRulerAxisBottomPx'
  114. | 'centerScaleRulerZeroVisible'
  115. | 'centerScaleRulerZeroLabel'
  116. | 'centerScaleRulerMinorTicks'
  117. | 'centerScaleRulerMajorMarks'
  118. > = {
  119. centerScaleRulerVisible: false,
  120. centerScaleRulerCenterXPx: 0,
  121. centerScaleRulerZeroYPx: 0,
  122. centerScaleRulerHeightPx: 0,
  123. centerScaleRulerAxisBottomPx: 0,
  124. centerScaleRulerZeroVisible: false,
  125. centerScaleRulerZeroLabel: '0 m',
  126. centerScaleRulerMinorTicks: [],
  127. centerScaleRulerMajorMarks: [],
  128. }
  129. let centerScaleRulerInputCache: Partial<Pick<
  130. MapPageData,
  131. 'stageWidth'
  132. | 'stageHeight'
  133. | 'zoom'
  134. | 'centerTileY'
  135. | 'tileSizePx'
  136. | 'previewScale'
  137. >> = {}
  138. const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
  139. 'buildVersion',
  140. 'renderMode',
  141. 'projectionMode',
  142. 'mapReady',
  143. 'mapReadyText',
  144. 'mapName',
  145. 'configStatusText',
  146. 'deviceHeadingText',
  147. 'devicePoseText',
  148. 'headingConfidenceText',
  149. 'accelerometerText',
  150. 'gyroscopeText',
  151. 'deviceMotionText',
  152. 'compassSourceText',
  153. 'compassTuningProfile',
  154. 'compassTuningProfileText',
  155. 'northReferenceButtonText',
  156. 'autoRotateSourceText',
  157. 'autoRotateCalibrationText',
  158. 'northReferenceText',
  159. 'centerText',
  160. 'tileSource',
  161. 'visibleTileCount',
  162. 'readyTileCount',
  163. 'memoryTileCount',
  164. 'diskTileCount',
  165. 'memoryHitCount',
  166. 'diskHitCount',
  167. 'networkFetchCount',
  168. 'cacheHitRateText',
  169. 'locationSourceMode',
  170. 'locationSourceText',
  171. 'mockBridgeConnected',
  172. 'mockBridgeStatusText',
  173. 'mockBridgeUrlText',
  174. 'mockCoordText',
  175. 'mockSpeedText',
  176. 'gpsCoordText',
  177. 'heartRateSourceMode',
  178. 'heartRateSourceText',
  179. 'heartRateConnected',
  180. 'heartRateStatusText',
  181. 'heartRateDeviceText',
  182. 'heartRateScanText',
  183. 'heartRateDiscoveredDevices',
  184. 'mockHeartRateBridgeConnected',
  185. 'mockHeartRateBridgeStatusText',
  186. 'mockHeartRateBridgeUrlText',
  187. 'mockHeartRateText',
  188. ])
  189. const CENTER_SCALE_RULER_DEP_KEYS = new Set<string>([
  190. 'showCenterScaleRuler',
  191. 'centerScaleRulerAnchorMode',
  192. 'stageWidth',
  193. 'stageHeight',
  194. 'topInsetHeight',
  195. 'zoom',
  196. 'centerTileY',
  197. 'tileSizePx',
  198. 'previewScale',
  199. ])
  200. const CENTER_SCALE_RULER_CACHE_KEYS: Array<keyof typeof centerScaleRulerInputCache> = [
  201. 'stageWidth',
  202. 'stageHeight',
  203. 'zoom',
  204. 'centerTileY',
  205. 'tileSizePx',
  206. 'previewScale',
  207. ]
  208. const RULER_ONLY_VIEW_KEYS = new Set<string>([
  209. 'zoom',
  210. 'centerTileX',
  211. 'centerTileY',
  212. 'tileSizePx',
  213. 'previewScale',
  214. 'stageWidth',
  215. 'stageHeight',
  216. 'stageLeft',
  217. 'stageTop',
  218. ])
  219. const SIDE_BUTTON_DEP_KEYS = new Set<string>([
  220. 'sideButtonMode',
  221. 'showGameInfoPanel',
  222. 'showCenterScaleRuler',
  223. 'centerScaleRulerAnchorMode',
  224. 'skipButtonEnabled',
  225. 'gameSessionStatus',
  226. 'gpsLockEnabled',
  227. 'gpsLockAvailable',
  228. ])
  229. function hasAnyPatchKey(patch: Record<string, unknown>, keys: Set<string>): boolean {
  230. return Object.keys(patch).some((key) => keys.has(key))
  231. }
  232. function filterDebugOnlyPatch(
  233. patch: Partial<MapPageData>,
  234. includeDebugFields: boolean,
  235. includeRulerFields: boolean,
  236. ): Partial<MapPageData> {
  237. if (includeDebugFields && includeRulerFields) {
  238. return patch
  239. }
  240. const filteredPatch: Partial<MapPageData> = {}
  241. for (const [key, value] of Object.entries(patch)) {
  242. if (!includeDebugFields && DEBUG_ONLY_VIEW_KEYS.has(key)) {
  243. continue
  244. }
  245. if (!includeRulerFields && RULER_ONLY_VIEW_KEYS.has(key)) {
  246. continue
  247. }
  248. {
  249. ;(filteredPatch as Record<string, unknown>)[key] = value
  250. }
  251. }
  252. return filteredPatch
  253. }
  254. function clearGameInfoPanelSyncTimer() {
  255. if (gameInfoPanelSyncTimer) {
  256. clearTimeout(gameInfoPanelSyncTimer)
  257. gameInfoPanelSyncTimer = 0
  258. }
  259. }
  260. function clearCenterScaleRulerSyncTimer() {
  261. if (centerScaleRulerSyncTimer) {
  262. clearTimeout(centerScaleRulerSyncTimer)
  263. centerScaleRulerSyncTimer = 0
  264. }
  265. }
  266. function clearCenterScaleRulerUpdateTimer() {
  267. if (centerScaleRulerUpdateTimer) {
  268. clearTimeout(centerScaleRulerUpdateTimer)
  269. centerScaleRulerUpdateTimer = 0
  270. }
  271. }
  272. function clearPunchHintDismissTimer() {
  273. if (punchHintDismissTimer) {
  274. clearTimeout(punchHintDismissTimer)
  275. punchHintDismissTimer = 0
  276. }
  277. }
  278. function clearHudFxTimer(key: 'timer' | 'mileage' | 'speed' | 'heartRate') {
  279. const timerMap = {
  280. timer: panelTimerFxTimer,
  281. mileage: panelMileageFxTimer,
  282. speed: panelSpeedFxTimer,
  283. heartRate: panelHeartRateFxTimer,
  284. }
  285. const timer = timerMap[key]
  286. if (timer) {
  287. clearTimeout(timer)
  288. }
  289. if (key === 'timer') {
  290. panelTimerFxTimer = 0
  291. } else if (key === 'mileage') {
  292. panelMileageFxTimer = 0
  293. } else if (key === 'speed') {
  294. panelSpeedFxTimer = 0
  295. } else {
  296. panelHeartRateFxTimer = 0
  297. }
  298. }
  299. function updateCenterScaleRulerInputCache(patch: Partial<MapPageData>) {
  300. for (const key of CENTER_SCALE_RULER_CACHE_KEYS) {
  301. if (Object.prototype.hasOwnProperty.call(patch, key)) {
  302. ;(centerScaleRulerInputCache as Record<string, unknown>)[key] =
  303. (patch as Record<string, unknown>)[key]
  304. }
  305. }
  306. }
  307. function loadStoredUserSettings(): StoredUserSettings {
  308. try {
  309. const stored = wx.getStorageSync(USER_SETTINGS_STORAGE_KEY)
  310. if (!stored || typeof stored !== 'object') {
  311. return {}
  312. }
  313. const normalized = stored as Record<string, unknown>
  314. const settings: StoredUserSettings = {}
  315. if (normalized.animationLevel === 'standard' || normalized.animationLevel === 'lite') {
  316. settings.animationLevel = normalized.animationLevel
  317. }
  318. if (normalized.northReferenceMode === 'magnetic' || normalized.northReferenceMode === 'true') {
  319. settings.northReferenceMode = normalized.northReferenceMode
  320. }
  321. if (typeof normalized.showCenterScaleRuler === 'boolean') {
  322. settings.showCenterScaleRuler = normalized.showCenterScaleRuler
  323. }
  324. if (normalized.centerScaleRulerAnchorMode === 'screen-center' || normalized.centerScaleRulerAnchorMode === 'compass-center') {
  325. settings.centerScaleRulerAnchorMode = normalized.centerScaleRulerAnchorMode
  326. }
  327. return settings
  328. } catch {
  329. return {}
  330. }
  331. }
  332. function persistStoredUserSettings(settings: StoredUserSettings) {
  333. try {
  334. wx.setStorageSync(USER_SETTINGS_STORAGE_KEY, settings)
  335. } catch {}
  336. }
  337. function buildSideButtonVisibility(mode: SideButtonMode) {
  338. return {
  339. sideButtonMode: mode,
  340. showLeftButtonGroup: mode === 'all' || mode === 'left' || mode === 'right',
  341. showRightButtonGroups: mode === 'all' || mode === 'right',
  342. showBottomDebugButton: mode !== 'hidden',
  343. }
  344. }
  345. function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
  346. if (currentMode === 'all') {
  347. return 'left'
  348. }
  349. if (currentMode === 'left') {
  350. return 'right'
  351. }
  352. if (currentMode === 'right') {
  353. return 'hidden'
  354. }
  355. return 'left'
  356. }
  357. function buildCompassTicks(): CompassTickData[] {
  358. const ticks: CompassTickData[] = []
  359. for (let angle = 0; angle < 360; angle += 5) {
  360. ticks.push({
  361. angle,
  362. long: angle % 15 === 0,
  363. major: angle % 45 === 0,
  364. })
  365. }
  366. return ticks
  367. }
  368. function buildCompassLabels(): CompassLabelData[] {
  369. return [
  370. { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
  371. { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
  372. { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  373. { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  374. { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  375. { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  376. { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  377. { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
  378. ]
  379. }
  380. function getFallbackStageRect(): MapEngineStageRect {
  381. const systemInfo = wx.getSystemInfoSync()
  382. const width = Math.max(320, systemInfo.windowWidth)
  383. const height = Math.max(280, systemInfo.windowHeight)
  384. return {
  385. width,
  386. height,
  387. left: 0,
  388. top: 0,
  389. }
  390. }
  391. function getSideToggleIconSrc(mode: SideButtonMode): string {
  392. if (mode === 'left') {
  393. return '../../assets/btn_more2.png'
  394. }
  395. if (mode === 'hidden') {
  396. return '../../assets/btn_more1.png'
  397. }
  398. return '../../assets/btn_more3.png'
  399. }
  400. function getSideActionButtonClass(state: SideActionButtonState): string {
  401. if (state === 'muted') {
  402. return 'map-side-button map-side-button--muted'
  403. }
  404. if (state === 'active') {
  405. return 'map-side-button map-side-button--active'
  406. }
  407. return 'map-side-button map-side-button--default'
  408. }
  409. function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showSystemSettingsPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
  410. const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
  411. ? 'muted'
  412. : data.gpsLockEnabled
  413. ? 'active'
  414. : 'default'
  415. const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'idle' ? 'default' : 'active'
  416. const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
  417. const sideButton12State: SideActionButtonState = data.showSystemSettingsPanel ? 'active' : 'default'
  418. const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
  419. const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
  420. ? 'muted'
  421. : data.centerScaleRulerAnchorMode === 'compass-center'
  422. ? 'active'
  423. : 'default'
  424. const sideButton16State: SideActionButtonState = data.skipButtonEnabled ? 'default' : 'muted'
  425. return {
  426. sideToggleIconSrc: getSideToggleIconSrc(data.sideButtonMode),
  427. sideButton2Class: getSideActionButtonClass(sideButton2State),
  428. sideButton4Class: getSideActionButtonClass(sideButton4State),
  429. sideButton11Class: getSideActionButtonClass(sideButton11State),
  430. sideButton12Class: getSideActionButtonClass(sideButton12State),
  431. sideButton13Class: getSideActionButtonClass(sideButton13State),
  432. sideButton14Class: getSideActionButtonClass(sideButton14State),
  433. sideButton16Class: getSideActionButtonClass(sideButton16State),
  434. }
  435. }
  436. function getRpxUnitInPx(): number {
  437. const systemInfo = wx.getSystemInfoSync()
  438. return systemInfo.windowWidth / 750
  439. }
  440. function worldTileYToLat(worldTileY: number, zoom: number): number {
  441. const scale = Math.pow(2, zoom)
  442. const n = Math.PI - (2 * Math.PI * worldTileY) / scale
  443. return (180 / Math.PI) * Math.atan(Math.sinh(n))
  444. }
  445. function getNiceDistanceMeters(rawDistanceMeters: number): number {
  446. if (!Number.isFinite(rawDistanceMeters) || rawDistanceMeters <= 0) {
  447. return 50
  448. }
  449. const exponent = Math.floor(Math.log10(rawDistanceMeters))
  450. const base = Math.pow(10, exponent)
  451. const normalized = rawDistanceMeters / base
  452. if (normalized <= 1) {
  453. return base
  454. }
  455. if (normalized <= 2) {
  456. return 2 * base
  457. }
  458. if (normalized <= 5) {
  459. return 5 * base
  460. }
  461. return 10 * base
  462. }
  463. function formatScaleDistanceLabel(distanceMeters: number): string {
  464. if (distanceMeters >= 1000) {
  465. const distanceKm = distanceMeters / 1000
  466. const formatted = distanceKm >= 10 ? distanceKm.toFixed(0) : distanceKm.toFixed(1)
  467. return `${formatted.replace(/\.0$/, '')} km`
  468. }
  469. return `${Math.round(distanceMeters)} m`
  470. }
  471. function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
  472. if (!data.showCenterScaleRuler) {
  473. lastCenterScaleRulerStablePatch = {
  474. centerScaleRulerVisible: false,
  475. centerScaleRulerCenterXPx: 0,
  476. centerScaleRulerZeroYPx: 0,
  477. centerScaleRulerHeightPx: 0,
  478. centerScaleRulerAxisBottomPx: 0,
  479. centerScaleRulerZeroVisible: false,
  480. centerScaleRulerZeroLabel: '0 m',
  481. centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
  482. centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
  483. }
  484. return { ...lastCenterScaleRulerStablePatch }
  485. }
  486. if (!data.stageWidth || !data.stageHeight) {
  487. return { ...lastCenterScaleRulerStablePatch }
  488. }
  489. const topPadding = 12
  490. const rpxUnitPx = getRpxUnitInPx()
  491. const compassBottomPaddingPx = 248 * rpxUnitPx
  492. const compassDialRadiusPx = (196 * rpxUnitPx) / 2
  493. const compassHeadingOverlayHeightPx = 40 * rpxUnitPx
  494. const compassOcclusionPaddingPx = 10 * rpxUnitPx
  495. const zeroYPx = data.centerScaleRulerAnchorMode === 'compass-center'
  496. ? Math.round(data.stageHeight - compassBottomPaddingPx - compassDialRadiusPx)
  497. : Math.round(data.stageHeight / 2)
  498. const fallbackHeight = Math.max(zeroYPx - topPadding, 160)
  499. const coveredBottomPx = data.centerScaleRulerAnchorMode === 'compass-center'
  500. ? Math.round(compassDialRadiusPx + compassHeadingOverlayHeightPx + compassOcclusionPaddingPx)
  501. : 0
  502. if (
  503. !data.tileSizePx
  504. || !Number.isFinite(data.zoom)
  505. || !Number.isFinite(data.centerTileY)
  506. ) {
  507. return {
  508. ...lastCenterScaleRulerStablePatch,
  509. centerScaleRulerVisible: true,
  510. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  511. centerScaleRulerZeroYPx: zeroYPx,
  512. centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
  513. centerScaleRulerAxisBottomPx: coveredBottomPx,
  514. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  515. }
  516. }
  517. const centerLat = worldTileYToLat(data.centerTileY + 0.5, data.zoom)
  518. const metersPerTile = Math.cos(centerLat * Math.PI / 180) * 40075016.686 / Math.pow(2, data.zoom)
  519. const metersPerPixel = metersPerTile / data.tileSizePx
  520. const effectivePreviewScale = Number.isFinite(data.previewScale) && data.previewScale > 0 ? data.previewScale : 1
  521. const effectiveMetersPerPixel = metersPerPixel / effectivePreviewScale
  522. const rulerHeight = Math.floor(zeroYPx - topPadding)
  523. if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
  524. return {
  525. ...lastCenterScaleRulerStablePatch,
  526. centerScaleRulerVisible: true,
  527. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  528. centerScaleRulerZeroYPx: zeroYPx,
  529. centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
  530. centerScaleRulerAxisBottomPx: coveredBottomPx,
  531. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  532. }
  533. }
  534. const labelDistanceMeters = getNiceDistanceMeters(effectiveMetersPerPixel * 80)
  535. const minorDistanceMeters = labelDistanceMeters / 8
  536. const minorStepPx = minorDistanceMeters / effectiveMetersPerPixel
  537. const visibleTopLimitPx = rulerHeight - coveredBottomPx
  538. const minorTicks: ScaleRulerMinorTickData[] = []
  539. const majorMarks: ScaleRulerMajorMarkData[] = []
  540. for (let index = 1; index <= 200; index += 1) {
  541. const topPx = Math.round(rulerHeight - index * minorStepPx)
  542. if (topPx < 0) {
  543. break
  544. }
  545. if (topPx >= visibleTopLimitPx) {
  546. continue
  547. }
  548. const isHalfMajor = index % 4 === 0
  549. const isLabelMajor = index % 8 === 0
  550. minorTicks.push({
  551. key: `minor-${index}`,
  552. topPx,
  553. long: isHalfMajor,
  554. })
  555. if (isLabelMajor) {
  556. majorMarks.push({
  557. key: `major-${index}`,
  558. topPx,
  559. label: formatScaleDistanceLabel((index / 8) * labelDistanceMeters),
  560. })
  561. }
  562. }
  563. lastCenterScaleRulerStablePatch = {
  564. centerScaleRulerVisible: true,
  565. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  566. centerScaleRulerZeroYPx: zeroYPx,
  567. centerScaleRulerHeightPx: rulerHeight,
  568. centerScaleRulerAxisBottomPx: coveredBottomPx,
  569. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  570. centerScaleRulerZeroLabel: '0 m',
  571. centerScaleRulerMinorTicks: minorTicks,
  572. centerScaleRulerMajorMarks: majorMarks,
  573. }
  574. return { ...lastCenterScaleRulerStablePatch }
  575. }
  576. function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
  577. return {
  578. title: '当前游戏',
  579. subtitle: '未开始',
  580. localRows: [],
  581. globalRows: [
  582. { label: '全球积分', value: '未接入' },
  583. { label: '全球排名', value: '未接入' },
  584. { label: '在线人数', value: '未接入' },
  585. { label: '队伍状态', value: '未接入' },
  586. { label: '实时广播', value: '未接入' },
  587. ],
  588. }
  589. }
  590. Page({
  591. data: {
  592. showDebugPanel: false,
  593. showGameInfoPanel: false,
  594. showSystemSettingsPanel: false,
  595. showCenterScaleRuler: false,
  596. statusBarHeight: 0,
  597. topInsetHeight: 12,
  598. hudPanelIndex: 0,
  599. configSourceText: '顺序赛配置',
  600. centerScaleRulerAnchorMode: 'screen-center',
  601. gameInfoTitle: '当前游戏',
  602. gameInfoSubtitle: '未开始',
  603. gameInfoLocalRows: [],
  604. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  605. panelTimerText: '00:00:00',
  606. panelMileageText: '0m',
  607. panelActionTagText: '目标',
  608. panelDistanceTagText: '点距',
  609. panelDistanceValueText: '--',
  610. panelDistanceUnitText: '',
  611. panelProgressText: '0/0',
  612. showPunchHintBanner: true,
  613. gameSessionStatus: 'idle',
  614. gameModeText: '顺序赛',
  615. gpsLockEnabled: false,
  616. gpsLockAvailable: false,
  617. locationSourceMode: 'real',
  618. locationSourceText: '真实定位',
  619. mockBridgeConnected: false,
  620. mockBridgeStatusText: '未连接',
  621. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  622. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  623. mockCoordText: '--',
  624. mockSpeedText: '--',
  625. heartRateSourceMode: 'real',
  626. heartRateSourceText: '真实心率',
  627. mockHeartRateBridgeConnected: false,
  628. mockHeartRateBridgeStatusText: '未连接',
  629. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  630. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  631. mockHeartRateText: '--',
  632. heartRateScanText: '未扫描',
  633. heartRateDiscoveredDevices: [],
  634. panelSpeedValueText: '0',
  635. panelTelemetryTone: 'blue',
  636. panelHeartRateZoneNameText: '--',
  637. panelHeartRateZoneRangeText: '',
  638. heartRateConnected: false,
  639. heartRateStatusText: '心率带未连接',
  640. heartRateDeviceText: '--',
  641. panelHeartRateValueText: '--',
  642. panelHeartRateUnitText: '',
  643. panelCaloriesValueText: '0',
  644. panelCaloriesUnitText: 'kcal',
  645. panelAverageSpeedValueText: '0',
  646. panelAverageSpeedUnitText: 'km/h',
  647. panelAccuracyValueText: '--',
  648. panelAccuracyUnitText: '',
  649. deviceHeadingText: '--',
  650. devicePoseText: '竖持',
  651. headingConfidenceText: '低',
  652. accelerometerText: '--',
  653. gyroscopeText: '--',
  654. deviceMotionText: '--',
  655. compassSourceText: '无数据',
  656. compassTuningProfile: 'balanced',
  657. compassTuningProfileText: '平衡',
  658. punchButtonText: '打点',
  659. punchButtonEnabled: false,
  660. skipButtonEnabled: false,
  661. punchHintText: '等待进入检查点范围',
  662. punchFeedbackVisible: false,
  663. punchFeedbackText: '',
  664. punchFeedbackTone: 'neutral',
  665. contentCardVisible: false,
  666. contentCardTitle: '',
  667. contentCardBody: '',
  668. punchButtonFxClass: '',
  669. panelProgressFxClass: '',
  670. panelDistanceFxClass: '',
  671. punchFeedbackFxClass: '',
  672. contentCardFxClass: '',
  673. mapPulseVisible: false,
  674. mapPulseLeftPx: 0,
  675. mapPulseTopPx: 0,
  676. mapPulseFxClass: '',
  677. stageFxVisible: false,
  678. stageFxClass: '',
  679. centerScaleRulerVisible: false,
  680. centerScaleRulerCenterXPx: 0,
  681. centerScaleRulerZeroYPx: 0,
  682. centerScaleRulerHeightPx: 0,
  683. centerScaleRulerAxisBottomPx: 0,
  684. centerScaleRulerZeroVisible: false,
  685. centerScaleRulerZeroLabel: '0 m',
  686. centerScaleRulerMinorTicks: [],
  687. centerScaleRulerMajorMarks: [],
  688. compassTicks: buildCompassTicks(),
  689. compassLabels: buildCompassLabels(),
  690. ...buildSideButtonVisibility('left'),
  691. ...buildSideButtonState({
  692. sideButtonMode: 'left',
  693. showGameInfoPanel: false,
  694. showSystemSettingsPanel: false,
  695. showCenterScaleRuler: false,
  696. centerScaleRulerAnchorMode: 'screen-center',
  697. skipButtonEnabled: false,
  698. gameSessionStatus: 'idle',
  699. gpsLockEnabled: false,
  700. gpsLockAvailable: false,
  701. }),
  702. } as unknown as MapPageData,
  703. onLoad() {
  704. const systemInfo = wx.getSystemInfoSync()
  705. const statusBarHeight = systemInfo.statusBarHeight || 0
  706. const menuButtonRect = wx.getMenuButtonBoundingClientRect()
  707. const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
  708. if (mapEngine) {
  709. mapEngine.destroy()
  710. mapEngine = null
  711. }
  712. mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
  713. onData: (patch) => {
  714. const nextPatch = patch as Partial<MapPageData>
  715. const includeDebugFields = this.data.showDebugPanel
  716. const includeRulerFields = this.data.showCenterScaleRuler
  717. const nextData: Partial<MapPageData> = filterDebugOnlyPatch({
  718. ...nextPatch,
  719. }, includeDebugFields, includeRulerFields)
  720. if (
  721. typeof nextPatch.mockBridgeUrlText === 'string'
  722. && this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
  723. ) {
  724. nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText
  725. }
  726. if (
  727. typeof nextPatch.mockHeartRateBridgeUrlText === 'string'
  728. && this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText
  729. ) {
  730. nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
  731. }
  732. updateCenterScaleRulerInputCache(nextPatch)
  733. const mergedData = {
  734. ...centerScaleRulerInputCache,
  735. ...this.data,
  736. ...nextData,
  737. } as MapPageData
  738. const derivedPatch: Partial<MapPageData> = {}
  739. if (
  740. this.data.showCenterScaleRuler
  741. && hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
  742. ) {
  743. clearCenterScaleRulerUpdateTimer()
  744. Object.assign(derivedPatch, buildCenterScaleRulerPatch(mergedData))
  745. }
  746. if (hasAnyPatchKey(nextPatch as Record<string, unknown>, SIDE_BUTTON_DEP_KEYS)) {
  747. Object.assign(derivedPatch, buildSideButtonState(mergedData))
  748. }
  749. if (typeof nextPatch.punchHintText === 'string') {
  750. const nextHintText = nextPatch.punchHintText.trim()
  751. if (nextHintText !== this.data.punchHintText) {
  752. clearPunchHintDismissTimer()
  753. nextData.showPunchHintBanner = nextHintText.length > 0
  754. if (nextHintText.length > 0) {
  755. punchHintDismissTimer = setTimeout(() => {
  756. punchHintDismissTimer = 0
  757. this.setData({
  758. showPunchHintBanner: false,
  759. })
  760. }, PUNCH_HINT_AUTO_HIDE_MS) as unknown as number
  761. }
  762. } else if (!nextHintText) {
  763. clearPunchHintDismissTimer()
  764. nextData.showPunchHintBanner = false
  765. }
  766. }
  767. const nextAnimationLevel = typeof nextPatch.animationLevel === 'string'
  768. ? nextPatch.animationLevel
  769. : this.data.animationLevel
  770. if (nextAnimationLevel === 'lite') {
  771. clearHudFxTimer('timer')
  772. clearHudFxTimer('mileage')
  773. clearHudFxTimer('speed')
  774. clearHudFxTimer('heartRate')
  775. nextData.panelTimerFxClass = ''
  776. nextData.panelMileageFxClass = ''
  777. nextData.panelSpeedFxClass = ''
  778. nextData.panelHeartRateFxClass = ''
  779. } else {
  780. if (typeof nextPatch.panelTimerText === 'string' && nextPatch.panelTimerText !== this.data.panelTimerText && this.data.panelTimerText !== '00:00:00') {
  781. clearHudFxTimer('timer')
  782. nextData.panelTimerFxClass = 'race-panel__timer--fx-tick'
  783. panelTimerFxTimer = setTimeout(() => {
  784. panelTimerFxTimer = 0
  785. this.setData({ panelTimerFxClass: '' })
  786. }, 320) as unknown as number
  787. }
  788. if (typeof nextPatch.panelMileageText === 'string' && nextPatch.panelMileageText !== this.data.panelMileageText && this.data.panelMileageText !== '0m') {
  789. clearHudFxTimer('mileage')
  790. nextData.panelMileageFxClass = 'race-panel__mileage-wrap--fx-update'
  791. panelMileageFxTimer = setTimeout(() => {
  792. panelMileageFxTimer = 0
  793. this.setData({ panelMileageFxClass: '' })
  794. }, 360) as unknown as number
  795. }
  796. if (typeof nextPatch.panelSpeedValueText === 'string' && nextPatch.panelSpeedValueText !== this.data.panelSpeedValueText && this.data.panelSpeedValueText !== '0') {
  797. clearHudFxTimer('speed')
  798. nextData.panelSpeedFxClass = 'race-panel__metric-group--fx-speed-update'
  799. panelSpeedFxTimer = setTimeout(() => {
  800. panelSpeedFxTimer = 0
  801. this.setData({ panelSpeedFxClass: '' })
  802. }, 360) as unknown as number
  803. }
  804. if (typeof nextPatch.panelHeartRateValueText === 'string' && nextPatch.panelHeartRateValueText !== this.data.panelHeartRateValueText && this.data.panelHeartRateValueText !== '--') {
  805. clearHudFxTimer('heartRate')
  806. nextData.panelHeartRateFxClass = 'race-panel__metric-group--fx-heart-rate-update'
  807. panelHeartRateFxTimer = setTimeout(() => {
  808. panelHeartRateFxTimer = 0
  809. this.setData({ panelHeartRateFxClass: '' })
  810. }, 400) as unknown as number
  811. }
  812. }
  813. if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
  814. this.setData({
  815. ...nextData,
  816. ...derivedPatch,
  817. })
  818. }
  819. if (this.data.showGameInfoPanel) {
  820. this.scheduleGameInfoPanelSnapshotSync()
  821. }
  822. },
  823. })
  824. const storedUserSettings = loadStoredUserSettings()
  825. if (storedUserSettings.animationLevel) {
  826. mapEngine.handleSetAnimationLevel(storedUserSettings.animationLevel)
  827. }
  828. if (storedUserSettings.northReferenceMode) {
  829. mapEngine.handleSetNorthReferenceMode(storedUserSettings.northReferenceMode)
  830. }
  831. mapEngine.setDiagnosticUiEnabled(false)
  832. centerScaleRulerInputCache = {
  833. stageWidth: 0,
  834. stageHeight: 0,
  835. zoom: 0,
  836. centerTileY: 0,
  837. tileSizePx: 0,
  838. previewScale: 1,
  839. }
  840. const initialShowCenterScaleRuler = !!storedUserSettings.showCenterScaleRuler
  841. const initialCenterScaleRulerAnchorMode = storedUserSettings.centerScaleRulerAnchorMode || 'screen-center'
  842. this.setData({
  843. ...mapEngine.getInitialData(),
  844. showDebugPanel: false,
  845. showGameInfoPanel: false,
  846. showSystemSettingsPanel: false,
  847. showCenterScaleRuler: initialShowCenterScaleRuler,
  848. statusBarHeight,
  849. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  850. hudPanelIndex: 0,
  851. configSourceText: '顺序赛配置',
  852. centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
  853. gameInfoTitle: '当前游戏',
  854. gameInfoSubtitle: '未开始',
  855. gameInfoLocalRows: [],
  856. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  857. panelTimerText: '00:00:00',
  858. panelTimerFxClass: '',
  859. panelMileageText: '0m',
  860. panelMileageFxClass: '',
  861. panelActionTagText: '目标',
  862. panelDistanceTagText: '点距',
  863. panelDistanceValueText: '--',
  864. panelDistanceUnitText: '',
  865. panelProgressText: '0/0',
  866. showPunchHintBanner: true,
  867. gameSessionStatus: 'idle',
  868. gameModeText: '顺序赛',
  869. gpsLockEnabled: false,
  870. gpsLockAvailable: false,
  871. locationSourceMode: 'real',
  872. locationSourceText: '真实定位',
  873. mockBridgeConnected: false,
  874. mockBridgeStatusText: '未连接',
  875. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  876. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  877. mockCoordText: '--',
  878. mockSpeedText: '--',
  879. heartRateSourceMode: 'real',
  880. heartRateSourceText: '真实心率',
  881. mockHeartRateBridgeConnected: false,
  882. mockHeartRateBridgeStatusText: '未连接',
  883. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  884. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  885. mockHeartRateText: '--',
  886. panelSpeedValueText: '0',
  887. panelSpeedFxClass: '',
  888. panelTelemetryTone: 'blue',
  889. panelHeartRateZoneNameText: '--',
  890. panelHeartRateZoneRangeText: '',
  891. heartRateConnected: false,
  892. heartRateStatusText: '心率带未连接',
  893. heartRateDeviceText: '--',
  894. panelHeartRateValueText: '--',
  895. panelHeartRateFxClass: '',
  896. panelHeartRateUnitText: '',
  897. panelCaloriesValueText: '0',
  898. panelCaloriesUnitText: 'kcal',
  899. panelAverageSpeedValueText: '0',
  900. panelAverageSpeedUnitText: 'km/h',
  901. panelAccuracyValueText: '--',
  902. panelAccuracyUnitText: '',
  903. deviceHeadingText: '--',
  904. devicePoseText: '竖持',
  905. headingConfidenceText: '低',
  906. accelerometerText: '--',
  907. gyroscopeText: '--',
  908. deviceMotionText: '--',
  909. compassSourceText: '无数据',
  910. compassTuningProfile: 'balanced',
  911. compassTuningProfileText: '平衡',
  912. punchButtonText: '打点',
  913. punchButtonEnabled: false,
  914. skipButtonEnabled: false,
  915. punchHintText: '等待进入检查点范围',
  916. punchFeedbackVisible: false,
  917. punchFeedbackText: '',
  918. punchFeedbackTone: 'neutral',
  919. contentCardVisible: false,
  920. contentCardTitle: '',
  921. contentCardBody: '',
  922. punchButtonFxClass: '',
  923. panelProgressFxClass: '',
  924. panelDistanceFxClass: '',
  925. punchFeedbackFxClass: '',
  926. contentCardFxClass: '',
  927. mapPulseVisible: false,
  928. mapPulseLeftPx: 0,
  929. mapPulseTopPx: 0,
  930. mapPulseFxClass: '',
  931. stageFxVisible: false,
  932. stageFxClass: '',
  933. compassTicks: buildCompassTicks(),
  934. compassLabels: buildCompassLabels(),
  935. ...buildSideButtonVisibility('left'),
  936. ...buildSideButtonState({
  937. sideButtonMode: 'left',
  938. showGameInfoPanel: false,
  939. showSystemSettingsPanel: false,
  940. showCenterScaleRuler: initialShowCenterScaleRuler,
  941. centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
  942. skipButtonEnabled: false,
  943. gameSessionStatus: 'idle',
  944. gpsLockEnabled: false,
  945. gpsLockAvailable: false,
  946. }),
  947. ...buildCenterScaleRulerPatch({
  948. ...(mapEngine.getInitialData() as MapPageData),
  949. showCenterScaleRuler: initialShowCenterScaleRuler,
  950. centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
  951. stageWidth: 0,
  952. stageHeight: 0,
  953. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  954. zoom: 0,
  955. centerTileY: 0,
  956. tileSizePx: 0,
  957. }),
  958. })
  959. },
  960. onReady() {
  961. stageCanvasAttached = false
  962. this.measureStageAndCanvas()
  963. this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
  964. },
  965. onShow() {
  966. if (mapEngine) {
  967. mapEngine.handleAppShow()
  968. }
  969. },
  970. onHide() {
  971. if (mapEngine) {
  972. mapEngine.handleAppHide()
  973. }
  974. },
  975. onUnload() {
  976. clearGameInfoPanelSyncTimer()
  977. clearCenterScaleRulerSyncTimer()
  978. clearCenterScaleRulerUpdateTimer()
  979. clearPunchHintDismissTimer()
  980. clearHudFxTimer('timer')
  981. clearHudFxTimer('mileage')
  982. clearHudFxTimer('speed')
  983. clearHudFxTimer('heartRate')
  984. if (mapEngine) {
  985. mapEngine.destroy()
  986. mapEngine = null
  987. }
  988. stageCanvasAttached = false
  989. },
  990. loadMapConfigFromRemote(configUrl: string, configLabel: string) {
  991. const currentEngine = mapEngine
  992. if (!currentEngine) {
  993. return
  994. }
  995. this.setData({
  996. configSourceText: configLabel,
  997. configStatusText: `加载中: ${configLabel}`,
  998. })
  999. loadRemoteMapConfig(configUrl)
  1000. .then((config) => {
  1001. if (mapEngine !== currentEngine) {
  1002. return
  1003. }
  1004. currentEngine.applyRemoteMapConfig(config)
  1005. })
  1006. .catch((error) => {
  1007. if (mapEngine !== currentEngine) {
  1008. return
  1009. }
  1010. const errorMessage = error && error.message ? error.message : '未知错误'
  1011. this.setData({
  1012. configStatusText: `载入失败: ${errorMessage}`,
  1013. statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
  1014. })
  1015. })
  1016. },
  1017. measureStageAndCanvas(onApplied?: () => void) {
  1018. const page = this
  1019. const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
  1020. const fallbackRect = getFallbackStageRect()
  1021. const rect: MapEngineStageRect = {
  1022. width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
  1023. height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
  1024. left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
  1025. top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
  1026. }
  1027. const currentEngine = mapEngine
  1028. if (!currentEngine) {
  1029. return
  1030. }
  1031. currentEngine.setStage(rect)
  1032. if (onApplied) {
  1033. onApplied()
  1034. }
  1035. if (stageCanvasAttached) {
  1036. return
  1037. }
  1038. const canvasQuery = wx.createSelectorQuery().in(page)
  1039. canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
  1040. canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
  1041. canvasQuery.exec((canvasRes) => {
  1042. const canvasRef = canvasRes[0] as any
  1043. const labelCanvasRef = canvasRes[1] as any
  1044. if (!canvasRef || !canvasRef.node) {
  1045. page.setData({
  1046. statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
  1047. })
  1048. return
  1049. }
  1050. const dpr = wx.getSystemInfoSync().pixelRatio || 1
  1051. try {
  1052. currentEngine.attachCanvas(
  1053. canvasRef.node,
  1054. rect.width,
  1055. rect.height,
  1056. dpr,
  1057. labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
  1058. )
  1059. stageCanvasAttached = true
  1060. } catch (error) {
  1061. page.setData({
  1062. statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
  1063. })
  1064. }
  1065. })
  1066. }
  1067. const query = wx.createSelectorQuery().in(page)
  1068. query.select('.map-stage').boundingClientRect()
  1069. query.exec((res) => {
  1070. const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
  1071. applyStage(rect)
  1072. })
  1073. },
  1074. handleTouchStart(event: WechatMiniprogram.TouchEvent) {
  1075. if (mapEngine) {
  1076. mapEngine.handleTouchStart(event)
  1077. }
  1078. },
  1079. handleTouchMove(event: WechatMiniprogram.TouchEvent) {
  1080. if (mapEngine) {
  1081. mapEngine.handleTouchMove(event)
  1082. }
  1083. },
  1084. handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
  1085. if (mapEngine) {
  1086. mapEngine.handleTouchEnd(event)
  1087. }
  1088. },
  1089. handleTouchCancel() {
  1090. if (mapEngine) {
  1091. mapEngine.handleTouchCancel()
  1092. }
  1093. },
  1094. handleRecenter() {
  1095. if (mapEngine) {
  1096. mapEngine.handleRecenter()
  1097. }
  1098. },
  1099. handleRotateStep() {
  1100. if (mapEngine) {
  1101. mapEngine.handleRotateStep()
  1102. }
  1103. },
  1104. handleRotationReset() {
  1105. if (mapEngine) {
  1106. mapEngine.handleRotationReset()
  1107. }
  1108. },
  1109. handleSetManualMode() {
  1110. if (mapEngine) {
  1111. mapEngine.handleSetManualMode()
  1112. }
  1113. },
  1114. handleSetNorthUpMode() {
  1115. if (mapEngine) {
  1116. mapEngine.handleSetNorthUpMode()
  1117. }
  1118. },
  1119. handleSetHeadingUpMode() {
  1120. if (mapEngine) {
  1121. mapEngine.handleSetHeadingUpMode()
  1122. }
  1123. },
  1124. handleCycleNorthReferenceMode() {
  1125. if (mapEngine) {
  1126. mapEngine.handleCycleNorthReferenceMode()
  1127. }
  1128. },
  1129. handleSetCompassTuningSmooth() {
  1130. if (mapEngine) {
  1131. mapEngine.handleSetCompassTuningProfile('smooth')
  1132. }
  1133. },
  1134. handleSetCompassTuningBalanced() {
  1135. if (mapEngine) {
  1136. mapEngine.handleSetCompassTuningProfile('balanced')
  1137. }
  1138. },
  1139. handleSetCompassTuningResponsive() {
  1140. if (mapEngine) {
  1141. mapEngine.handleSetCompassTuningProfile('responsive')
  1142. }
  1143. },
  1144. handleAutoRotateCalibrate() {
  1145. if (mapEngine) {
  1146. mapEngine.handleAutoRotateCalibrate()
  1147. }
  1148. },
  1149. handleToggleGpsTracking() {
  1150. if (mapEngine) {
  1151. mapEngine.handleToggleGpsTracking()
  1152. }
  1153. },
  1154. handleSetRealLocationMode() {
  1155. if (mapEngine) {
  1156. mapEngine.handleSetRealLocationMode()
  1157. }
  1158. },
  1159. handleSetMockLocationMode() {
  1160. if (mapEngine) {
  1161. mapEngine.handleSetMockLocationMode()
  1162. }
  1163. },
  1164. handleConnectMockLocationBridge() {
  1165. if (mapEngine) {
  1166. mapEngine.handleConnectMockLocationBridge()
  1167. }
  1168. },
  1169. handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
  1170. this.setData({
  1171. mockBridgeUrlDraft: event.detail.value,
  1172. })
  1173. },
  1174. handleSaveMockBridgeUrl() {
  1175. if (mapEngine) {
  1176. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  1177. }
  1178. },
  1179. handleDisconnectMockLocationBridge() {
  1180. if (mapEngine) {
  1181. mapEngine.handleDisconnectMockLocationBridge()
  1182. }
  1183. },
  1184. handleSetRealHeartRateMode() {
  1185. if (mapEngine) {
  1186. mapEngine.handleSetRealHeartRateMode()
  1187. }
  1188. },
  1189. handleSetMockHeartRateMode() {
  1190. if (mapEngine) {
  1191. mapEngine.handleSetMockHeartRateMode()
  1192. }
  1193. },
  1194. handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) {
  1195. this.setData({
  1196. mockHeartRateBridgeUrlDraft: event.detail.value,
  1197. })
  1198. },
  1199. handleSaveMockHeartRateBridgeUrl() {
  1200. if (mapEngine) {
  1201. mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
  1202. }
  1203. },
  1204. handleConnectMockHeartRateBridge() {
  1205. if (mapEngine) {
  1206. mapEngine.handleConnectMockHeartRateBridge()
  1207. }
  1208. },
  1209. handleDisconnectMockHeartRateBridge() {
  1210. if (mapEngine) {
  1211. mapEngine.handleDisconnectMockHeartRateBridge()
  1212. }
  1213. },
  1214. handleConnectHeartRate() {
  1215. if (mapEngine) {
  1216. mapEngine.handleConnectHeartRate()
  1217. }
  1218. },
  1219. handleDisconnectHeartRate() {
  1220. if (mapEngine) {
  1221. mapEngine.handleDisconnectHeartRate()
  1222. }
  1223. },
  1224. handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
  1225. if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
  1226. mapEngine.handleConnectHeartRateDevice(event.currentTarget.dataset.deviceId)
  1227. }
  1228. },
  1229. handleClearPreferredHeartRateDevice() {
  1230. if (mapEngine) {
  1231. mapEngine.handleClearPreferredHeartRateDevice()
  1232. }
  1233. },
  1234. handleDebugHeartRateBlue() {
  1235. if (mapEngine) {
  1236. mapEngine.handleDebugHeartRateTone('blue')
  1237. }
  1238. },
  1239. handleDebugHeartRatePurple() {
  1240. if (mapEngine) {
  1241. mapEngine.handleDebugHeartRateTone('purple')
  1242. }
  1243. },
  1244. handleDebugHeartRateGreen() {
  1245. if (mapEngine) {
  1246. mapEngine.handleDebugHeartRateTone('green')
  1247. }
  1248. },
  1249. handleDebugHeartRateYellow() {
  1250. if (mapEngine) {
  1251. mapEngine.handleDebugHeartRateTone('yellow')
  1252. }
  1253. },
  1254. handleDebugHeartRateOrange() {
  1255. if (mapEngine) {
  1256. mapEngine.handleDebugHeartRateTone('orange')
  1257. }
  1258. },
  1259. handleDebugHeartRateRed() {
  1260. if (mapEngine) {
  1261. mapEngine.handleDebugHeartRateTone('red')
  1262. }
  1263. },
  1264. handleClearDebugHeartRate() {
  1265. if (mapEngine) {
  1266. mapEngine.handleClearDebugHeartRate()
  1267. }
  1268. },
  1269. handleToggleOsmReference() {
  1270. if (mapEngine) {
  1271. mapEngine.handleToggleOsmReference()
  1272. }
  1273. },
  1274. handleStartGame() {
  1275. if (mapEngine) {
  1276. mapEngine.handleStartGame()
  1277. }
  1278. },
  1279. handleLoadClassicConfig() {
  1280. this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
  1281. },
  1282. handleLoadScoreOConfig() {
  1283. this.loadMapConfigFromRemote(SCORE_O_REMOTE_GAME_CONFIG_URL, '积分赛配置')
  1284. },
  1285. handleForceExitGame() {
  1286. if (!mapEngine || this.data.gameSessionStatus === 'idle') {
  1287. return
  1288. }
  1289. wx.showModal({
  1290. title: '确认退出',
  1291. content: '确认强制结束当前对局并返回开始前状态?',
  1292. confirmText: '确认退出',
  1293. cancelText: '取消',
  1294. success: (result) => {
  1295. if (result.confirm && mapEngine) {
  1296. mapEngine.handleForceExitGame()
  1297. }
  1298. },
  1299. })
  1300. },
  1301. handleSkipAction() {
  1302. if (!mapEngine || !this.data.skipButtonEnabled) {
  1303. return
  1304. }
  1305. if (!mapEngine.shouldConfirmSkipAction()) {
  1306. mapEngine.handleSkipAction()
  1307. return
  1308. }
  1309. wx.showModal({
  1310. title: '确认跳点',
  1311. content: '确认跳过当前检查点并切换到下一个目标点?',
  1312. confirmText: '确认跳过',
  1313. cancelText: '取消',
  1314. success: (result) => {
  1315. if (result.confirm && mapEngine) {
  1316. mapEngine.handleSkipAction()
  1317. }
  1318. },
  1319. })
  1320. },
  1321. handleClearMapTestArtifacts() {
  1322. if (mapEngine) {
  1323. mapEngine.handleClearMapTestArtifacts()
  1324. }
  1325. },
  1326. syncGameInfoPanelSnapshot() {
  1327. if (!mapEngine) {
  1328. return
  1329. }
  1330. const snapshot = mapEngine.getGameInfoSnapshot()
  1331. const localRows = snapshot.localRows.concat([
  1332. { label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
  1333. { label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
  1334. { label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
  1335. { label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
  1336. { label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
  1337. { label: '比例尺高度', value: `${this.data.centerScaleRulerHeightPx}px` },
  1338. { label: '比例尺主刻度数', value: String(this.data.centerScaleRulerMajorMarks.length) },
  1339. ])
  1340. this.setData({
  1341. gameInfoTitle: snapshot.title,
  1342. gameInfoSubtitle: snapshot.subtitle,
  1343. gameInfoLocalRows: localRows,
  1344. gameInfoGlobalRows: snapshot.globalRows,
  1345. })
  1346. },
  1347. scheduleGameInfoPanelSnapshotSync() {
  1348. if (!this.data.showGameInfoPanel) {
  1349. clearGameInfoPanelSyncTimer()
  1350. return
  1351. }
  1352. if (gameInfoPanelSyncTimer) {
  1353. return
  1354. }
  1355. gameInfoPanelSyncTimer = setTimeout(() => {
  1356. gameInfoPanelSyncTimer = 0
  1357. if (this.data.showGameInfoPanel) {
  1358. this.syncGameInfoPanelSnapshot()
  1359. }
  1360. }, 400) as unknown as number
  1361. },
  1362. handleOpenGameInfoPanel() {
  1363. clearGameInfoPanelSyncTimer()
  1364. this.syncGameInfoPanelSnapshot()
  1365. this.setData({
  1366. showDebugPanel: false,
  1367. showSystemSettingsPanel: false,
  1368. showGameInfoPanel: true,
  1369. ...buildSideButtonState({
  1370. sideButtonMode: this.data.sideButtonMode,
  1371. showGameInfoPanel: true,
  1372. showSystemSettingsPanel: false,
  1373. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1374. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1375. skipButtonEnabled: this.data.skipButtonEnabled,
  1376. gameSessionStatus: this.data.gameSessionStatus,
  1377. gpsLockEnabled: this.data.gpsLockEnabled,
  1378. gpsLockAvailable: this.data.gpsLockAvailable,
  1379. }),
  1380. })
  1381. },
  1382. handleCloseGameInfoPanel() {
  1383. clearGameInfoPanelSyncTimer()
  1384. this.setData({
  1385. showGameInfoPanel: false,
  1386. ...buildSideButtonState({
  1387. sideButtonMode: this.data.sideButtonMode,
  1388. showGameInfoPanel: false,
  1389. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  1390. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1391. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1392. skipButtonEnabled: this.data.skipButtonEnabled,
  1393. gameSessionStatus: this.data.gameSessionStatus,
  1394. gpsLockEnabled: this.data.gpsLockEnabled,
  1395. gpsLockAvailable: this.data.gpsLockAvailable,
  1396. }),
  1397. })
  1398. },
  1399. handleGameInfoPanelTap() {},
  1400. handleOpenSystemSettingsPanel() {
  1401. clearGameInfoPanelSyncTimer()
  1402. this.setData({
  1403. showDebugPanel: false,
  1404. showGameInfoPanel: false,
  1405. showSystemSettingsPanel: true,
  1406. ...buildSideButtonState({
  1407. sideButtonMode: this.data.sideButtonMode,
  1408. showGameInfoPanel: false,
  1409. showSystemSettingsPanel: true,
  1410. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1411. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1412. skipButtonEnabled: this.data.skipButtonEnabled,
  1413. gameSessionStatus: this.data.gameSessionStatus,
  1414. gpsLockEnabled: this.data.gpsLockEnabled,
  1415. gpsLockAvailable: this.data.gpsLockAvailable,
  1416. }),
  1417. })
  1418. },
  1419. handleCloseSystemSettingsPanel() {
  1420. this.setData({
  1421. showSystemSettingsPanel: false,
  1422. ...buildSideButtonState({
  1423. sideButtonMode: this.data.sideButtonMode,
  1424. showGameInfoPanel: this.data.showGameInfoPanel,
  1425. showSystemSettingsPanel: false,
  1426. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1427. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1428. skipButtonEnabled: this.data.skipButtonEnabled,
  1429. gameSessionStatus: this.data.gameSessionStatus,
  1430. gpsLockEnabled: this.data.gpsLockEnabled,
  1431. gpsLockAvailable: this.data.gpsLockAvailable,
  1432. }),
  1433. })
  1434. },
  1435. handleSystemSettingsPanelTap() {},
  1436. handleSetAnimationLevelStandard() {
  1437. if (!mapEngine) {
  1438. return
  1439. }
  1440. mapEngine.handleSetAnimationLevel('standard')
  1441. persistStoredUserSettings({
  1442. ...loadStoredUserSettings(),
  1443. animationLevel: 'standard',
  1444. })
  1445. },
  1446. handleSetAnimationLevelLite() {
  1447. if (!mapEngine) {
  1448. return
  1449. }
  1450. mapEngine.handleSetAnimationLevel('lite')
  1451. persistStoredUserSettings({
  1452. ...loadStoredUserSettings(),
  1453. animationLevel: 'lite',
  1454. })
  1455. },
  1456. handleSetNorthReferenceMagnetic() {
  1457. if (!mapEngine) {
  1458. return
  1459. }
  1460. mapEngine.handleSetNorthReferenceMode('magnetic')
  1461. persistStoredUserSettings({
  1462. ...loadStoredUserSettings(),
  1463. northReferenceMode: 'magnetic',
  1464. })
  1465. },
  1466. handleSetNorthReferenceTrue() {
  1467. if (!mapEngine) {
  1468. return
  1469. }
  1470. mapEngine.handleSetNorthReferenceMode('true')
  1471. persistStoredUserSettings({
  1472. ...loadStoredUserSettings(),
  1473. northReferenceMode: 'true',
  1474. })
  1475. },
  1476. handleOverlayTouch() {},
  1477. handlePunchAction() {
  1478. if (!this.data.punchButtonEnabled) {
  1479. return
  1480. }
  1481. if (mapEngine) {
  1482. mapEngine.handlePunchAction()
  1483. }
  1484. },
  1485. handleCloseContentCard() {
  1486. if (mapEngine) {
  1487. mapEngine.closeContentCard()
  1488. }
  1489. },
  1490. handleClosePunchHint() {
  1491. clearPunchHintDismissTimer()
  1492. this.setData({
  1493. showPunchHintBanner: false,
  1494. })
  1495. },
  1496. handlePunchHintTap() {},
  1497. handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
  1498. this.setData({
  1499. hudPanelIndex: event.detail.current || 0,
  1500. })
  1501. },
  1502. handleCycleSideButtons() {
  1503. const nextMode = getNextSideButtonMode(this.data.sideButtonMode)
  1504. this.setData({
  1505. ...buildSideButtonVisibility(nextMode),
  1506. ...buildSideButtonState({
  1507. sideButtonMode: nextMode,
  1508. showGameInfoPanel: this.data.showGameInfoPanel,
  1509. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  1510. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1511. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1512. skipButtonEnabled: this.data.skipButtonEnabled,
  1513. gameSessionStatus: this.data.gameSessionStatus,
  1514. gpsLockEnabled: this.data.gpsLockEnabled,
  1515. gpsLockAvailable: this.data.gpsLockAvailable,
  1516. }),
  1517. })
  1518. },
  1519. handleToggleGpsLock() {
  1520. if (mapEngine) {
  1521. mapEngine.handleToggleGpsLock()
  1522. }
  1523. },
  1524. handleToggleMapRotateMode() {
  1525. if (!mapEngine) {
  1526. return
  1527. }
  1528. if (this.data.orientationMode === 'heading-up') {
  1529. mapEngine.handleSetManualMode()
  1530. return
  1531. }
  1532. mapEngine.handleSetHeadingUpMode()
  1533. },
  1534. handleToggleDebugPanel() {
  1535. const nextShowDebugPanel = !this.data.showDebugPanel
  1536. if (!nextShowDebugPanel) {
  1537. clearGameInfoPanelSyncTimer()
  1538. }
  1539. if (mapEngine) {
  1540. mapEngine.setDiagnosticUiEnabled(nextShowDebugPanel)
  1541. }
  1542. this.setData({
  1543. showDebugPanel: nextShowDebugPanel,
  1544. showGameInfoPanel: false,
  1545. showSystemSettingsPanel: false,
  1546. ...buildSideButtonState({
  1547. sideButtonMode: this.data.sideButtonMode,
  1548. showGameInfoPanel: false,
  1549. showSystemSettingsPanel: false,
  1550. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1551. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1552. skipButtonEnabled: this.data.skipButtonEnabled,
  1553. gameSessionStatus: this.data.gameSessionStatus,
  1554. gpsLockEnabled: this.data.gpsLockEnabled,
  1555. gpsLockAvailable: this.data.gpsLockAvailable,
  1556. }),
  1557. })
  1558. },
  1559. handleCloseDebugPanel() {
  1560. if (mapEngine) {
  1561. mapEngine.setDiagnosticUiEnabled(false)
  1562. }
  1563. this.setData({
  1564. showDebugPanel: false,
  1565. ...buildSideButtonState({
  1566. sideButtonMode: this.data.sideButtonMode,
  1567. showGameInfoPanel: this.data.showGameInfoPanel,
  1568. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  1569. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1570. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1571. skipButtonEnabled: this.data.skipButtonEnabled,
  1572. gameSessionStatus: this.data.gameSessionStatus,
  1573. gpsLockEnabled: this.data.gpsLockEnabled,
  1574. gpsLockAvailable: this.data.gpsLockAvailable,
  1575. }),
  1576. })
  1577. },
  1578. applyCenterScaleRulerSettings(nextEnabled: boolean, nextAnchorMode: CenterScaleRulerAnchorMode) {
  1579. this.data.showCenterScaleRuler = nextEnabled
  1580. this.data.centerScaleRulerAnchorMode = nextAnchorMode
  1581. clearCenterScaleRulerSyncTimer()
  1582. clearCenterScaleRulerUpdateTimer()
  1583. const syncRulerFromEngine = () => {
  1584. if (!mapEngine) {
  1585. return
  1586. }
  1587. const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
  1588. updateCenterScaleRulerInputCache(engineSnapshot)
  1589. const mergedData = {
  1590. ...centerScaleRulerInputCache,
  1591. ...this.data,
  1592. showCenterScaleRuler: nextEnabled,
  1593. centerScaleRulerAnchorMode: nextAnchorMode,
  1594. } as MapPageData
  1595. this.setData({
  1596. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
  1597. showCenterScaleRuler: nextEnabled,
  1598. centerScaleRulerAnchorMode: nextAnchorMode,
  1599. ...buildCenterScaleRulerPatch(mergedData),
  1600. ...buildSideButtonState(mergedData),
  1601. })
  1602. }
  1603. if (!nextEnabled) {
  1604. syncRulerFromEngine()
  1605. return
  1606. }
  1607. this.setData({
  1608. showCenterScaleRuler: true,
  1609. centerScaleRulerAnchorMode: nextAnchorMode,
  1610. ...buildSideButtonState({
  1611. ...this.data,
  1612. showCenterScaleRuler: true,
  1613. centerScaleRulerAnchorMode: nextAnchorMode,
  1614. } as MapPageData),
  1615. })
  1616. this.measureStageAndCanvas(() => {
  1617. syncRulerFromEngine()
  1618. })
  1619. centerScaleRulerSyncTimer = setTimeout(() => {
  1620. centerScaleRulerSyncTimer = 0
  1621. if (!this.data.showCenterScaleRuler) {
  1622. return
  1623. }
  1624. syncRulerFromEngine()
  1625. }, 96) as unknown as number
  1626. },
  1627. handleSetCenterScaleRulerVisibleOn() {
  1628. this.applyCenterScaleRulerSettings(true, this.data.centerScaleRulerAnchorMode)
  1629. persistStoredUserSettings({
  1630. ...loadStoredUserSettings(),
  1631. showCenterScaleRuler: true,
  1632. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1633. })
  1634. },
  1635. handleSetCenterScaleRulerVisibleOff() {
  1636. this.applyCenterScaleRulerSettings(false, this.data.centerScaleRulerAnchorMode)
  1637. persistStoredUserSettings({
  1638. ...loadStoredUserSettings(),
  1639. showCenterScaleRuler: false,
  1640. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1641. })
  1642. },
  1643. handleSetCenterScaleRulerAnchorScreenCenter() {
  1644. this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'screen-center')
  1645. persistStoredUserSettings({
  1646. ...loadStoredUserSettings(),
  1647. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1648. centerScaleRulerAnchorMode: 'screen-center',
  1649. })
  1650. },
  1651. handleSetCenterScaleRulerAnchorCompassCenter() {
  1652. this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'compass-center')
  1653. persistStoredUserSettings({
  1654. ...loadStoredUserSettings(),
  1655. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1656. centerScaleRulerAnchorMode: 'compass-center',
  1657. })
  1658. },
  1659. handleToggleCenterScaleRulerAnchor() {
  1660. if (!this.data.showCenterScaleRuler) {
  1661. return
  1662. }
  1663. const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
  1664. ? 'compass-center'
  1665. : 'screen-center'
  1666. const engineSnapshot = mapEngine ? (mapEngine.getInitialData() as Partial<MapPageData>) : {}
  1667. updateCenterScaleRulerInputCache(engineSnapshot)
  1668. this.data.centerScaleRulerAnchorMode = nextAnchorMode
  1669. const mergedData = {
  1670. ...centerScaleRulerInputCache,
  1671. ...this.data,
  1672. centerScaleRulerAnchorMode: nextAnchorMode,
  1673. } as MapPageData
  1674. this.setData({
  1675. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, true),
  1676. centerScaleRulerAnchorMode: nextAnchorMode,
  1677. ...buildCenterScaleRulerPatch(mergedData),
  1678. ...buildSideButtonState(mergedData),
  1679. })
  1680. if (this.data.showGameInfoPanel) {
  1681. this.syncGameInfoPanelSnapshot()
  1682. }
  1683. },
  1684. handleDebugPanelTap() {},
  1685. })