map.ts 82 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737
  1. import {
  2. MapEngine,
  3. type MapEngineGameInfoRow,
  4. type MapEngineGameInfoSnapshot,
  5. type MapEngineResultSnapshot,
  6. type MapEngineStageRect,
  7. type MapEngineViewState,
  8. } from '../../engine/map/mapEngine'
  9. import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
  10. import { type AnimationLevel } from '../../utils/animationLevel'
  11. import { type H5ExperienceFallbackPayload, type H5ExperienceRequest } from '../../game/experience/h5Experience'
  12. import { type TrackColorPreset, type TrackDisplayMode, type TrackStyleProfile, type TrackTailLengthPreset } from '../../game/presentation/trackStyleConfig'
  13. import { type GpsMarkerColorPreset, type GpsMarkerSizePreset, type GpsMarkerStyleId } from '../../game/presentation/gpsMarkerStyleConfig'
  14. type CompassTickData = {
  15. angle: number
  16. long: boolean
  17. major: boolean
  18. }
  19. type CompassLabelData = {
  20. text: string
  21. angle: number
  22. rotateBack: number
  23. radius: number
  24. className: string
  25. }
  26. type ScaleRulerMinorTickData = {
  27. key: string
  28. topPx: number
  29. long: boolean
  30. }
  31. type ScaleRulerMajorMarkData = {
  32. key: string
  33. topPx: number
  34. label: string
  35. }
  36. type SideButtonMode = 'shown' | 'hidden'
  37. type SideActionButtonState = 'muted' | 'default' | 'active'
  38. type SideButtonPlacement = 'left' | 'right'
  39. type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center'
  40. type UserNorthReferenceMode = 'magnetic' | 'true'
  41. type CompassTuningProfile = 'smooth' | 'balanced' | 'responsive'
  42. type SettingLockKey =
  43. | 'lockAnimationLevel'
  44. | 'lockTrackMode'
  45. | 'lockTrackTailLength'
  46. | 'lockTrackColor'
  47. | 'lockTrackStyle'
  48. | 'lockGpsMarkerVisible'
  49. | 'lockGpsMarkerStyle'
  50. | 'lockGpsMarkerSize'
  51. | 'lockGpsMarkerColor'
  52. | 'lockSideButtonPlacement'
  53. | 'lockAutoRotate'
  54. | 'lockCompassTuning'
  55. | 'lockScaleRulerVisible'
  56. | 'lockScaleRulerAnchor'
  57. | 'lockNorthReference'
  58. | 'lockHeartRateDevice'
  59. type StoredUserSettings = {
  60. animationLevel?: AnimationLevel
  61. trackDisplayMode?: TrackDisplayMode
  62. trackTailLength?: TrackTailLengthPreset
  63. trackColorPreset?: TrackColorPreset
  64. trackStyleProfile?: TrackStyleProfile
  65. gpsMarkerVisible?: boolean
  66. gpsMarkerStyle?: GpsMarkerStyleId
  67. gpsMarkerSize?: GpsMarkerSizePreset
  68. gpsMarkerColorPreset?: GpsMarkerColorPreset
  69. autoRotateEnabled?: boolean
  70. compassTuningProfile?: CompassTuningProfile
  71. northReferenceMode?: UserNorthReferenceMode
  72. sideButtonPlacement?: SideButtonPlacement
  73. showCenterScaleRuler?: boolean
  74. centerScaleRulerAnchorMode?: CenterScaleRulerAnchorMode
  75. lockAnimationLevel?: boolean
  76. lockTrackMode?: boolean
  77. lockTrackTailLength?: boolean
  78. lockTrackColor?: boolean
  79. lockTrackStyle?: boolean
  80. lockGpsMarkerVisible?: boolean
  81. lockGpsMarkerStyle?: boolean
  82. lockGpsMarkerSize?: boolean
  83. lockGpsMarkerColor?: boolean
  84. lockSideButtonPlacement?: boolean
  85. lockAutoRotate?: boolean
  86. lockCompassTuning?: boolean
  87. lockScaleRulerVisible?: boolean
  88. lockScaleRulerAnchor?: boolean
  89. lockNorthReference?: boolean
  90. lockHeartRateDevice?: boolean
  91. }
  92. type MapPageData = MapEngineViewState & {
  93. showDebugPanel: boolean
  94. showGameInfoPanel: boolean
  95. showResultScene: boolean
  96. showSystemSettingsPanel: boolean
  97. showCenterScaleRuler: boolean
  98. showPunchHintBanner: boolean
  99. centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
  100. statusBarHeight: number
  101. topInsetHeight: number
  102. hudPanelIndex: number
  103. configSourceText: string
  104. mockBridgeUrlDraft: string
  105. mockHeartRateBridgeUrlDraft: string
  106. mockDebugLogBridgeUrlDraft: string
  107. gameInfoTitle: string
  108. gameInfoSubtitle: string
  109. gameInfoLocalRows: MapEngineGameInfoRow[]
  110. gameInfoGlobalRows: MapEngineGameInfoRow[]
  111. resultSceneTitle: string
  112. resultSceneSubtitle: string
  113. resultSceneHeroLabel: string
  114. resultSceneHeroValue: string
  115. resultSceneRows: MapEngineGameInfoRow[]
  116. panelTimerText: string
  117. panelMileageText: string
  118. panelDistanceValueText: string
  119. panelProgressText: string
  120. panelSpeedValueText: string
  121. panelTimerFxClass: string
  122. panelMileageFxClass: string
  123. panelSpeedFxClass: string
  124. panelHeartRateFxClass: string
  125. compassTicks: CompassTickData[]
  126. compassLabels: CompassLabelData[]
  127. sideButtonMode: SideButtonMode
  128. sideButtonPlacement: SideButtonPlacement
  129. autoRotateEnabled: boolean
  130. lockAnimationLevel: boolean
  131. lockTrackMode: boolean
  132. lockTrackTailLength: boolean
  133. lockTrackColor: boolean
  134. lockTrackStyle: boolean
  135. lockGpsMarkerVisible: boolean
  136. lockGpsMarkerStyle: boolean
  137. lockGpsMarkerSize: boolean
  138. lockGpsMarkerColor: boolean
  139. lockSideButtonPlacement: boolean
  140. lockAutoRotate: boolean
  141. lockCompassTuning: boolean
  142. lockScaleRulerVisible: boolean
  143. lockScaleRulerAnchor: boolean
  144. lockNorthReference: boolean
  145. lockHeartRateDevice: boolean
  146. sideToggleIconSrc: string
  147. sideButton2Class: string
  148. sideButton4Class: string
  149. sideButton11Class: string
  150. sideButton12Class: string
  151. sideButton13Class: string
  152. sideButton14Class: string
  153. sideButton16Class: string
  154. centerScaleRulerVisible: boolean
  155. centerScaleRulerCenterXPx: number
  156. centerScaleRulerZeroYPx: number
  157. centerScaleRulerHeightPx: number
  158. centerScaleRulerAxisBottomPx: number
  159. centerScaleRulerZeroVisible: boolean
  160. centerScaleRulerZeroLabel: string
  161. centerScaleRulerMinorTicks: ScaleRulerMinorTickData[]
  162. centerScaleRulerMajorMarks: ScaleRulerMajorMarkData[]
  163. showLeftButtonGroup: boolean
  164. showRightButtonGroups: boolean
  165. showBottomDebugButton: boolean
  166. }
  167. const INTERNAL_BUILD_VERSION = 'map-build-293'
  168. const USER_SETTINGS_STORAGE_KEY = 'cmr_user_settings_v1'
  169. const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
  170. const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
  171. const PUNCH_HINT_AUTO_HIDE_MS = 30000
  172. let mapEngine: MapEngine | null = null
  173. let stageCanvasAttached = false
  174. let gameInfoPanelSyncTimer = 0
  175. let centerScaleRulerSyncTimer = 0
  176. let contentAudioRecorder: WechatMiniprogram.RecorderManager | null = null
  177. let contentAudioRecording = false
  178. let centerScaleRulerUpdateTimer = 0
  179. let punchHintDismissTimer = 0
  180. let panelTimerFxTimer = 0
  181. let panelMileageFxTimer = 0
  182. let panelSpeedFxTimer = 0
  183. let panelHeartRateFxTimer = 0
  184. let lastCenterScaleRulerStablePatch: Pick<
  185. MapPageData,
  186. | 'centerScaleRulerVisible'
  187. | 'centerScaleRulerCenterXPx'
  188. | 'centerScaleRulerZeroYPx'
  189. | 'centerScaleRulerHeightPx'
  190. | 'centerScaleRulerAxisBottomPx'
  191. | 'centerScaleRulerZeroVisible'
  192. | 'centerScaleRulerZeroLabel'
  193. | 'centerScaleRulerMinorTicks'
  194. | 'centerScaleRulerMajorMarks'
  195. > = {
  196. centerScaleRulerVisible: false,
  197. centerScaleRulerCenterXPx: 0,
  198. centerScaleRulerZeroYPx: 0,
  199. centerScaleRulerHeightPx: 0,
  200. centerScaleRulerAxisBottomPx: 0,
  201. centerScaleRulerZeroVisible: false,
  202. centerScaleRulerZeroLabel: '0 m',
  203. centerScaleRulerMinorTicks: [],
  204. centerScaleRulerMajorMarks: [],
  205. }
  206. let centerScaleRulerInputCache: Partial<Pick<
  207. MapPageData,
  208. 'stageWidth'
  209. | 'stageHeight'
  210. | 'zoom'
  211. | 'centerTileY'
  212. | 'tileSizePx'
  213. | 'previewScale'
  214. >> = {}
  215. const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
  216. 'buildVersion',
  217. 'renderMode',
  218. 'projectionMode',
  219. 'mapReady',
  220. 'mapReadyText',
  221. 'mapName',
  222. 'configStatusText',
  223. 'deviceHeadingText',
  224. 'devicePoseText',
  225. 'headingConfidenceText',
  226. 'accelerometerText',
  227. 'gyroscopeText',
  228. 'deviceMotionText',
  229. 'compassSourceText',
  230. 'compassTuningProfile',
  231. 'compassTuningProfileText',
  232. 'northReferenceButtonText',
  233. 'autoRotateSourceText',
  234. 'autoRotateCalibrationText',
  235. 'northReferenceText',
  236. 'centerText',
  237. 'tileSource',
  238. 'visibleTileCount',
  239. 'readyTileCount',
  240. 'memoryTileCount',
  241. 'diskTileCount',
  242. 'memoryHitCount',
  243. 'diskHitCount',
  244. 'networkFetchCount',
  245. 'cacheHitRateText',
  246. 'locationSourceMode',
  247. 'locationSourceText',
  248. 'mockBridgeConnected',
  249. 'mockBridgeStatusText',
  250. 'mockBridgeUrlText',
  251. 'mockCoordText',
  252. 'mockSpeedText',
  253. 'gpsCoordText',
  254. 'heartRateSourceMode',
  255. 'heartRateSourceText',
  256. 'heartRateConnected',
  257. 'heartRateStatusText',
  258. 'heartRateDeviceText',
  259. 'heartRateScanText',
  260. 'heartRateDiscoveredDevices',
  261. 'mockHeartRateBridgeConnected',
  262. 'mockHeartRateBridgeStatusText',
  263. 'mockHeartRateBridgeUrlText',
  264. 'mockHeartRateText',
  265. ])
  266. const CENTER_SCALE_RULER_DEP_KEYS = new Set<string>([
  267. 'showCenterScaleRuler',
  268. 'centerScaleRulerAnchorMode',
  269. 'stageWidth',
  270. 'stageHeight',
  271. 'topInsetHeight',
  272. 'zoom',
  273. 'centerTileY',
  274. 'tileSizePx',
  275. 'previewScale',
  276. ])
  277. const CENTER_SCALE_RULER_CACHE_KEYS: Array<keyof typeof centerScaleRulerInputCache> = [
  278. 'stageWidth',
  279. 'stageHeight',
  280. 'zoom',
  281. 'centerTileY',
  282. 'tileSizePx',
  283. 'previewScale',
  284. ]
  285. const RULER_ONLY_VIEW_KEYS = new Set<string>([
  286. 'zoom',
  287. 'centerTileX',
  288. 'centerTileY',
  289. 'tileSizePx',
  290. 'previewScale',
  291. 'stageWidth',
  292. 'stageHeight',
  293. 'stageLeft',
  294. 'stageTop',
  295. ])
  296. const SIDE_BUTTON_DEP_KEYS = new Set<string>([
  297. 'sideButtonMode',
  298. 'showGameInfoPanel',
  299. 'showCenterScaleRuler',
  300. 'centerScaleRulerAnchorMode',
  301. 'skipButtonEnabled',
  302. 'gameSessionStatus',
  303. 'gpsLockEnabled',
  304. 'gpsLockAvailable',
  305. ])
  306. function hasAnyPatchKey(patch: Record<string, unknown>, keys: Set<string>): boolean {
  307. return Object.keys(patch).some((key) => keys.has(key))
  308. }
  309. function filterDebugOnlyPatch(
  310. patch: Partial<MapPageData>,
  311. includeDebugFields: boolean,
  312. includeRulerFields: boolean,
  313. ): Partial<MapPageData> {
  314. if (includeDebugFields && includeRulerFields) {
  315. return patch
  316. }
  317. const filteredPatch: Partial<MapPageData> = {}
  318. for (const [key, value] of Object.entries(patch)) {
  319. if (!includeDebugFields && DEBUG_ONLY_VIEW_KEYS.has(key)) {
  320. continue
  321. }
  322. if (!includeRulerFields && RULER_ONLY_VIEW_KEYS.has(key)) {
  323. continue
  324. }
  325. {
  326. ;(filteredPatch as Record<string, unknown>)[key] = value
  327. }
  328. }
  329. return filteredPatch
  330. }
  331. function clearGameInfoPanelSyncTimer() {
  332. if (gameInfoPanelSyncTimer) {
  333. clearTimeout(gameInfoPanelSyncTimer)
  334. gameInfoPanelSyncTimer = 0
  335. }
  336. }
  337. function clearCenterScaleRulerSyncTimer() {
  338. if (centerScaleRulerSyncTimer) {
  339. clearTimeout(centerScaleRulerSyncTimer)
  340. centerScaleRulerSyncTimer = 0
  341. }
  342. }
  343. function clearCenterScaleRulerUpdateTimer() {
  344. if (centerScaleRulerUpdateTimer) {
  345. clearTimeout(centerScaleRulerUpdateTimer)
  346. centerScaleRulerUpdateTimer = 0
  347. }
  348. }
  349. function clearPunchHintDismissTimer() {
  350. if (punchHintDismissTimer) {
  351. clearTimeout(punchHintDismissTimer)
  352. punchHintDismissTimer = 0
  353. }
  354. }
  355. function clearHudFxTimer(key: 'timer' | 'mileage' | 'speed' | 'heartRate') {
  356. const timerMap = {
  357. timer: panelTimerFxTimer,
  358. mileage: panelMileageFxTimer,
  359. speed: panelSpeedFxTimer,
  360. heartRate: panelHeartRateFxTimer,
  361. }
  362. const timer = timerMap[key]
  363. if (timer) {
  364. clearTimeout(timer)
  365. }
  366. if (key === 'timer') {
  367. panelTimerFxTimer = 0
  368. } else if (key === 'mileage') {
  369. panelMileageFxTimer = 0
  370. } else if (key === 'speed') {
  371. panelSpeedFxTimer = 0
  372. } else {
  373. panelHeartRateFxTimer = 0
  374. }
  375. }
  376. function updateCenterScaleRulerInputCache(patch: Partial<MapPageData>) {
  377. for (const key of CENTER_SCALE_RULER_CACHE_KEYS) {
  378. if (Object.prototype.hasOwnProperty.call(patch, key)) {
  379. ;(centerScaleRulerInputCache as Record<string, unknown>)[key] =
  380. (patch as Record<string, unknown>)[key]
  381. }
  382. }
  383. }
  384. function loadStoredUserSettings(): StoredUserSettings {
  385. try {
  386. const stored = wx.getStorageSync(USER_SETTINGS_STORAGE_KEY)
  387. if (!stored || typeof stored !== 'object') {
  388. return {}
  389. }
  390. const normalized = stored as Record<string, unknown>
  391. const settings: StoredUserSettings = {}
  392. if (normalized.animationLevel === 'standard' || normalized.animationLevel === 'lite') {
  393. settings.animationLevel = normalized.animationLevel
  394. }
  395. if (normalized.trackDisplayMode === 'none' || normalized.trackDisplayMode === 'full' || normalized.trackDisplayMode === 'tail') {
  396. settings.trackDisplayMode = normalized.trackDisplayMode
  397. }
  398. if (normalized.trackTailLength === 'short' || normalized.trackTailLength === 'medium' || normalized.trackTailLength === 'long') {
  399. settings.trackTailLength = normalized.trackTailLength
  400. }
  401. if (normalized.trackStyleProfile === 'classic' || normalized.trackStyleProfile === 'neon') {
  402. settings.trackStyleProfile = normalized.trackStyleProfile
  403. }
  404. if (typeof normalized.gpsMarkerVisible === 'boolean') {
  405. settings.gpsMarkerVisible = normalized.gpsMarkerVisible
  406. }
  407. if (
  408. normalized.gpsMarkerStyle === 'dot'
  409. || normalized.gpsMarkerStyle === 'beacon'
  410. || normalized.gpsMarkerStyle === 'disc'
  411. || normalized.gpsMarkerStyle === 'badge'
  412. ) {
  413. settings.gpsMarkerStyle = normalized.gpsMarkerStyle
  414. }
  415. if (normalized.gpsMarkerSize === 'small' || normalized.gpsMarkerSize === 'medium' || normalized.gpsMarkerSize === 'large') {
  416. settings.gpsMarkerSize = normalized.gpsMarkerSize
  417. }
  418. if (
  419. normalized.gpsMarkerColorPreset === 'mint'
  420. || normalized.gpsMarkerColorPreset === 'cyan'
  421. || normalized.gpsMarkerColorPreset === 'sky'
  422. || normalized.gpsMarkerColorPreset === 'blue'
  423. || normalized.gpsMarkerColorPreset === 'violet'
  424. || normalized.gpsMarkerColorPreset === 'pink'
  425. || normalized.gpsMarkerColorPreset === 'orange'
  426. || normalized.gpsMarkerColorPreset === 'yellow'
  427. ) {
  428. settings.gpsMarkerColorPreset = normalized.gpsMarkerColorPreset
  429. }
  430. if (
  431. normalized.trackColorPreset === 'mint'
  432. || normalized.trackColorPreset === 'cyan'
  433. || normalized.trackColorPreset === 'sky'
  434. || normalized.trackColorPreset === 'blue'
  435. || normalized.trackColorPreset === 'violet'
  436. || normalized.trackColorPreset === 'pink'
  437. || normalized.trackColorPreset === 'orange'
  438. || normalized.trackColorPreset === 'yellow'
  439. ) {
  440. settings.trackColorPreset = normalized.trackColorPreset
  441. }
  442. if (normalized.northReferenceMode === 'magnetic' || normalized.northReferenceMode === 'true') {
  443. settings.northReferenceMode = normalized.northReferenceMode
  444. }
  445. if (typeof normalized.autoRotateEnabled === 'boolean') {
  446. settings.autoRotateEnabled = normalized.autoRotateEnabled
  447. }
  448. if (normalized.compassTuningProfile === 'smooth' || normalized.compassTuningProfile === 'balanced' || normalized.compassTuningProfile === 'responsive') {
  449. settings.compassTuningProfile = normalized.compassTuningProfile
  450. }
  451. if (normalized.sideButtonPlacement === 'left' || normalized.sideButtonPlacement === 'right') {
  452. settings.sideButtonPlacement = normalized.sideButtonPlacement
  453. }
  454. if (typeof normalized.showCenterScaleRuler === 'boolean') {
  455. settings.showCenterScaleRuler = normalized.showCenterScaleRuler
  456. }
  457. if (normalized.centerScaleRulerAnchorMode === 'screen-center' || normalized.centerScaleRulerAnchorMode === 'compass-center') {
  458. settings.centerScaleRulerAnchorMode = normalized.centerScaleRulerAnchorMode
  459. }
  460. if (typeof normalized.lockAnimationLevel === 'boolean') {
  461. settings.lockAnimationLevel = normalized.lockAnimationLevel
  462. }
  463. if (typeof normalized.lockTrackMode === 'boolean') {
  464. settings.lockTrackMode = normalized.lockTrackMode
  465. }
  466. if (typeof normalized.lockTrackTailLength === 'boolean') {
  467. settings.lockTrackTailLength = normalized.lockTrackTailLength
  468. }
  469. if (typeof normalized.lockTrackColor === 'boolean') {
  470. settings.lockTrackColor = normalized.lockTrackColor
  471. }
  472. if (typeof normalized.lockTrackStyle === 'boolean') {
  473. settings.lockTrackStyle = normalized.lockTrackStyle
  474. }
  475. if (typeof normalized.lockGpsMarkerVisible === 'boolean') {
  476. settings.lockGpsMarkerVisible = normalized.lockGpsMarkerVisible
  477. }
  478. if (typeof normalized.lockGpsMarkerStyle === 'boolean') {
  479. settings.lockGpsMarkerStyle = normalized.lockGpsMarkerStyle
  480. }
  481. if (typeof normalized.lockGpsMarkerSize === 'boolean') {
  482. settings.lockGpsMarkerSize = normalized.lockGpsMarkerSize
  483. }
  484. if (typeof normalized.lockGpsMarkerColor === 'boolean') {
  485. settings.lockGpsMarkerColor = normalized.lockGpsMarkerColor
  486. }
  487. if (typeof normalized.lockSideButtonPlacement === 'boolean') {
  488. settings.lockSideButtonPlacement = normalized.lockSideButtonPlacement
  489. }
  490. if (typeof normalized.lockAutoRotate === 'boolean') {
  491. settings.lockAutoRotate = normalized.lockAutoRotate
  492. }
  493. if (typeof normalized.lockCompassTuning === 'boolean') {
  494. settings.lockCompassTuning = normalized.lockCompassTuning
  495. }
  496. if (typeof normalized.lockScaleRulerVisible === 'boolean') {
  497. settings.lockScaleRulerVisible = normalized.lockScaleRulerVisible
  498. }
  499. if (typeof normalized.lockScaleRulerAnchor === 'boolean') {
  500. settings.lockScaleRulerAnchor = normalized.lockScaleRulerAnchor
  501. }
  502. if (typeof normalized.lockNorthReference === 'boolean') {
  503. settings.lockNorthReference = normalized.lockNorthReference
  504. }
  505. if (typeof normalized.lockHeartRateDevice === 'boolean') {
  506. settings.lockHeartRateDevice = normalized.lockHeartRateDevice
  507. }
  508. return settings
  509. } catch {
  510. return {}
  511. }
  512. }
  513. function persistStoredUserSettings(settings: StoredUserSettings) {
  514. try {
  515. wx.setStorageSync(USER_SETTINGS_STORAGE_KEY, settings)
  516. } catch {}
  517. }
  518. function toggleStoredSettingLock(settings: StoredUserSettings, key: SettingLockKey): StoredUserSettings {
  519. return {
  520. ...settings,
  521. [key]: !settings[key],
  522. }
  523. }
  524. function buildSideButtonVisibility(mode: SideButtonMode) {
  525. return {
  526. sideButtonMode: mode,
  527. showLeftButtonGroup: mode === 'shown',
  528. showRightButtonGroups: false,
  529. showBottomDebugButton: true,
  530. }
  531. }
  532. function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
  533. return currentMode === 'shown' ? 'hidden' : 'shown'
  534. }
  535. function buildCompassTicks(): CompassTickData[] {
  536. const ticks: CompassTickData[] = []
  537. for (let angle = 0; angle < 360; angle += 5) {
  538. ticks.push({
  539. angle,
  540. long: angle % 15 === 0,
  541. major: angle % 45 === 0,
  542. })
  543. }
  544. return ticks
  545. }
  546. function buildCompassLabels(): CompassLabelData[] {
  547. return [
  548. { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
  549. { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
  550. { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  551. { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  552. { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  553. { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  554. { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  555. { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
  556. ]
  557. }
  558. function getFallbackStageRect(): MapEngineStageRect {
  559. const systemInfo = wx.getSystemInfoSync()
  560. const width = Math.max(320, systemInfo.windowWidth)
  561. const height = Math.max(280, systemInfo.windowHeight)
  562. return {
  563. width,
  564. height,
  565. left: 0,
  566. top: 0,
  567. }
  568. }
  569. function getSideToggleIconSrc(mode: SideButtonMode): string {
  570. if (mode === 'hidden') {
  571. return '../../assets/btn_more1.png'
  572. }
  573. return '../../assets/btn_more3.png'
  574. }
  575. function getSideActionButtonClass(state: SideActionButtonState): string {
  576. if (state === 'muted') {
  577. return 'map-side-button map-side-button--muted'
  578. }
  579. if (state === 'active') {
  580. return 'map-side-button map-side-button--active'
  581. }
  582. return 'map-side-button map-side-button--default'
  583. }
  584. function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showSystemSettingsPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
  585. const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
  586. ? 'muted'
  587. : data.gpsLockEnabled
  588. ? 'active'
  589. : 'default'
  590. const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'running' ? 'active' : 'muted'
  591. const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
  592. const sideButton12State: SideActionButtonState = data.showSystemSettingsPanel ? 'active' : 'default'
  593. const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
  594. const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
  595. ? 'muted'
  596. : data.centerScaleRulerAnchorMode === 'compass-center'
  597. ? 'active'
  598. : 'default'
  599. const sideButton16State: SideActionButtonState = data.skipButtonEnabled ? 'default' : 'muted'
  600. return {
  601. sideToggleIconSrc: getSideToggleIconSrc(data.sideButtonMode),
  602. sideButton2Class: getSideActionButtonClass(sideButton2State),
  603. sideButton4Class: getSideActionButtonClass(sideButton4State),
  604. sideButton11Class: getSideActionButtonClass(sideButton11State),
  605. sideButton12Class: getSideActionButtonClass(sideButton12State),
  606. sideButton13Class: getSideActionButtonClass(sideButton13State),
  607. sideButton14Class: getSideActionButtonClass(sideButton14State),
  608. sideButton16Class: getSideActionButtonClass(sideButton16State),
  609. }
  610. }
  611. function getRpxUnitInPx(): number {
  612. const systemInfo = wx.getSystemInfoSync()
  613. return systemInfo.windowWidth / 750
  614. }
  615. function worldTileYToLat(worldTileY: number, zoom: number): number {
  616. const scale = Math.pow(2, zoom)
  617. const n = Math.PI - (2 * Math.PI * worldTileY) / scale
  618. return (180 / Math.PI) * Math.atan(Math.sinh(n))
  619. }
  620. function getNiceDistanceMeters(rawDistanceMeters: number): number {
  621. if (!Number.isFinite(rawDistanceMeters) || rawDistanceMeters <= 0) {
  622. return 50
  623. }
  624. const exponent = Math.floor(Math.log10(rawDistanceMeters))
  625. const base = Math.pow(10, exponent)
  626. const normalized = rawDistanceMeters / base
  627. if (normalized <= 1) {
  628. return base
  629. }
  630. if (normalized <= 2) {
  631. return 2 * base
  632. }
  633. if (normalized <= 5) {
  634. return 5 * base
  635. }
  636. return 10 * base
  637. }
  638. function formatScaleDistanceLabel(distanceMeters: number): string {
  639. if (distanceMeters >= 1000) {
  640. const distanceKm = distanceMeters / 1000
  641. const formatted = distanceKm >= 10 ? distanceKm.toFixed(0) : distanceKm.toFixed(1)
  642. return `${formatted.replace(/\.0$/, '')} km`
  643. }
  644. return `${Math.round(distanceMeters)} m`
  645. }
  646. function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
  647. if (!data.showCenterScaleRuler) {
  648. lastCenterScaleRulerStablePatch = {
  649. centerScaleRulerVisible: false,
  650. centerScaleRulerCenterXPx: 0,
  651. centerScaleRulerZeroYPx: 0,
  652. centerScaleRulerHeightPx: 0,
  653. centerScaleRulerAxisBottomPx: 0,
  654. centerScaleRulerZeroVisible: false,
  655. centerScaleRulerZeroLabel: '0 m',
  656. centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
  657. centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
  658. }
  659. return { ...lastCenterScaleRulerStablePatch }
  660. }
  661. if (!data.stageWidth || !data.stageHeight) {
  662. return { ...lastCenterScaleRulerStablePatch }
  663. }
  664. const topPadding = 12
  665. const rpxUnitPx = getRpxUnitInPx()
  666. const compassBottomPaddingPx = 248 * rpxUnitPx
  667. const compassDialRadiusPx = (196 * rpxUnitPx) / 2
  668. const compassHeadingOverlayHeightPx = 40 * rpxUnitPx
  669. const compassOcclusionPaddingPx = 10 * rpxUnitPx
  670. const zeroYPx = data.centerScaleRulerAnchorMode === 'compass-center'
  671. ? Math.round(data.stageHeight - compassBottomPaddingPx - compassDialRadiusPx)
  672. : Math.round(data.stageHeight / 2)
  673. const fallbackHeight = Math.max(zeroYPx - topPadding, 160)
  674. const coveredBottomPx = data.centerScaleRulerAnchorMode === 'compass-center'
  675. ? Math.round(compassDialRadiusPx + compassHeadingOverlayHeightPx + compassOcclusionPaddingPx)
  676. : 0
  677. if (
  678. !data.tileSizePx
  679. || !Number.isFinite(data.zoom)
  680. || !Number.isFinite(data.centerTileY)
  681. ) {
  682. return {
  683. ...lastCenterScaleRulerStablePatch,
  684. centerScaleRulerVisible: true,
  685. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  686. centerScaleRulerZeroYPx: zeroYPx,
  687. centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
  688. centerScaleRulerAxisBottomPx: coveredBottomPx,
  689. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  690. }
  691. }
  692. const centerLat = worldTileYToLat(data.centerTileY + 0.5, data.zoom)
  693. const metersPerTile = Math.cos(centerLat * Math.PI / 180) * 40075016.686 / Math.pow(2, data.zoom)
  694. const metersPerPixel = metersPerTile / data.tileSizePx
  695. const effectivePreviewScale = Number.isFinite(data.previewScale) && data.previewScale > 0 ? data.previewScale : 1
  696. const effectiveMetersPerPixel = metersPerPixel / effectivePreviewScale
  697. const rulerHeight = Math.floor(zeroYPx - topPadding)
  698. if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
  699. return {
  700. ...lastCenterScaleRulerStablePatch,
  701. centerScaleRulerVisible: true,
  702. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  703. centerScaleRulerZeroYPx: zeroYPx,
  704. centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
  705. centerScaleRulerAxisBottomPx: coveredBottomPx,
  706. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  707. }
  708. }
  709. const labelDistanceMeters = getNiceDistanceMeters(effectiveMetersPerPixel * 80)
  710. const minorDistanceMeters = labelDistanceMeters / 8
  711. const minorStepPx = minorDistanceMeters / effectiveMetersPerPixel
  712. const visibleTopLimitPx = rulerHeight - coveredBottomPx
  713. const minorTicks: ScaleRulerMinorTickData[] = []
  714. const majorMarks: ScaleRulerMajorMarkData[] = []
  715. for (let index = 1; index <= 200; index += 1) {
  716. const topPx = Math.round(rulerHeight - index * minorStepPx)
  717. if (topPx < 0) {
  718. break
  719. }
  720. if (topPx >= visibleTopLimitPx) {
  721. continue
  722. }
  723. const isHalfMajor = index % 4 === 0
  724. const isLabelMajor = index % 8 === 0
  725. minorTicks.push({
  726. key: `minor-${index}`,
  727. topPx,
  728. long: isHalfMajor,
  729. })
  730. if (isLabelMajor) {
  731. majorMarks.push({
  732. key: `major-${index}`,
  733. topPx,
  734. label: formatScaleDistanceLabel((index / 8) * labelDistanceMeters),
  735. })
  736. }
  737. }
  738. lastCenterScaleRulerStablePatch = {
  739. centerScaleRulerVisible: true,
  740. centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
  741. centerScaleRulerZeroYPx: zeroYPx,
  742. centerScaleRulerHeightPx: rulerHeight,
  743. centerScaleRulerAxisBottomPx: coveredBottomPx,
  744. centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
  745. centerScaleRulerZeroLabel: '0 m',
  746. centerScaleRulerMinorTicks: minorTicks,
  747. centerScaleRulerMajorMarks: majorMarks,
  748. }
  749. return { ...lastCenterScaleRulerStablePatch }
  750. }
  751. function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
  752. return {
  753. title: '当前游戏',
  754. subtitle: '未开始',
  755. localRows: [],
  756. globalRows: [
  757. { label: '全球积分', value: '未接入' },
  758. { label: '全球排名', value: '未接入' },
  759. { label: '在线人数', value: '未接入' },
  760. { label: '队伍状态', value: '未接入' },
  761. { label: '实时广播', value: '未接入' },
  762. ],
  763. }
  764. }
  765. function buildEmptyResultSceneSnapshot(): MapEngineResultSnapshot {
  766. return {
  767. title: '本局结果',
  768. subtitle: '未开始',
  769. heroLabel: '本局用时',
  770. heroValue: '--',
  771. rows: [],
  772. }
  773. }
  774. Page({
  775. data: {
  776. showDebugPanel: false,
  777. showGameInfoPanel: false,
  778. showResultScene: false,
  779. showSystemSettingsPanel: false,
  780. showCenterScaleRuler: false,
  781. statusBarHeight: 0,
  782. topInsetHeight: 12,
  783. hudPanelIndex: 0,
  784. configSourceText: '顺序赛配置',
  785. centerScaleRulerAnchorMode: 'screen-center',
  786. autoRotateEnabled: false,
  787. lockAnimationLevel: false,
  788. lockTrackMode: false,
  789. lockTrackTailLength: false,
  790. lockTrackColor: false,
  791. lockTrackStyle: false,
  792. lockGpsMarkerVisible: false,
  793. lockGpsMarkerStyle: false,
  794. lockGpsMarkerSize: false,
  795. lockGpsMarkerColor: false,
  796. lockSideButtonPlacement: false,
  797. lockAutoRotate: false,
  798. lockCompassTuning: false,
  799. lockScaleRulerVisible: false,
  800. lockScaleRulerAnchor: false,
  801. lockNorthReference: false,
  802. lockHeartRateDevice: false,
  803. gameInfoTitle: '当前游戏',
  804. gameInfoSubtitle: '未开始',
  805. gameInfoLocalRows: [],
  806. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  807. resultSceneTitle: '本局结果',
  808. resultSceneSubtitle: '未开始',
  809. resultSceneHeroLabel: '本局用时',
  810. resultSceneHeroValue: '--',
  811. resultSceneRows: buildEmptyResultSceneSnapshot().rows,
  812. panelTimerText: '00:00:00',
  813. panelMileageText: '0m',
  814. panelActionTagText: '目标',
  815. panelDistanceTagText: '点距',
  816. panelDistanceValueText: '--',
  817. panelDistanceUnitText: '',
  818. panelProgressText: '0/0',
  819. showPunchHintBanner: true,
  820. sideButtonPlacement: 'left',
  821. gameSessionStatus: 'idle',
  822. gameModeText: '顺序赛',
  823. gpsLockEnabled: false,
  824. gpsLockAvailable: false,
  825. locationSourceMode: 'real',
  826. locationSourceText: '真实定位',
  827. mockBridgeConnected: false,
  828. mockBridgeStatusText: '未连接',
  829. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  830. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  831. mockCoordText: '--',
  832. mockSpeedText: '--',
  833. heartRateSourceMode: 'real',
  834. heartRateSourceText: '真实心率',
  835. mockHeartRateBridgeConnected: false,
  836. mockHeartRateBridgeStatusText: '未连接',
  837. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-hr',
  838. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-hr',
  839. mockHeartRateText: '--',
  840. mockDebugLogBridgeConnected: false,
  841. mockDebugLogBridgeStatusText: '已关闭 (wss://gs.gotomars.xyz/debug-log)',
  842. mockDebugLogBridgeUrlText: 'wss://gs.gotomars.xyz/debug-log',
  843. mockDebugLogBridgeUrlDraft: 'wss://gs.gotomars.xyz/debug-log',
  844. heartRateScanText: '未扫描',
  845. heartRateDiscoveredDevices: [],
  846. panelSpeedValueText: '0',
  847. panelTelemetryTone: 'blue',
  848. trackDisplayMode: 'full',
  849. trackTailLength: 'medium',
  850. trackColorPreset: 'mint',
  851. trackStyleProfile: 'neon',
  852. gpsMarkerVisible: true,
  853. gpsMarkerStyle: 'beacon',
  854. gpsMarkerSize: 'medium',
  855. gpsMarkerColorPreset: 'cyan',
  856. gpsLogoStatusText: '未配置',
  857. gpsLogoSourceText: '--',
  858. panelHeartRateZoneNameText: '--',
  859. panelHeartRateZoneRangeText: '',
  860. heartRateConnected: false,
  861. heartRateStatusText: '心率带未连接',
  862. heartRateDeviceText: '--',
  863. panelHeartRateValueText: '--',
  864. panelHeartRateUnitText: '',
  865. panelCaloriesValueText: '0',
  866. panelCaloriesUnitText: 'kcal',
  867. panelAverageSpeedValueText: '0',
  868. panelAverageSpeedUnitText: 'km/h',
  869. panelAccuracyValueText: '--',
  870. panelAccuracyUnitText: '',
  871. deviceHeadingText: '--',
  872. devicePoseText: '竖持',
  873. headingConfidenceText: '低',
  874. accelerometerText: '--',
  875. gyroscopeText: '--',
  876. deviceMotionText: '--',
  877. compassSourceText: '无数据',
  878. compassTuningProfile: 'balanced',
  879. compassTuningProfileText: '平衡',
  880. punchButtonText: '打点',
  881. punchButtonEnabled: false,
  882. skipButtonEnabled: false,
  883. punchHintText: '等待进入检查点范围',
  884. punchFeedbackVisible: false,
  885. punchFeedbackText: '',
  886. punchFeedbackTone: 'neutral',
  887. contentCardVisible: false,
  888. contentCardTemplate: 'story',
  889. contentCardTitle: '',
  890. contentCardBody: '',
  891. contentCardActions: [],
  892. contentQuizVisible: false,
  893. contentQuizQuestionText: '',
  894. contentQuizCountdownText: '',
  895. contentQuizOptions: [],
  896. contentQuizFeedbackVisible: false,
  897. contentQuizFeedbackText: '',
  898. contentQuizFeedbackTone: 'neutral',
  899. punchButtonFxClass: '',
  900. panelProgressFxClass: '',
  901. panelDistanceFxClass: '',
  902. punchFeedbackFxClass: '',
  903. contentCardFxClass: '',
  904. mapPulseVisible: false,
  905. mapPulseLeftPx: 0,
  906. mapPulseTopPx: 0,
  907. mapPulseFxClass: '',
  908. stageFxVisible: false,
  909. stageFxClass: '',
  910. centerScaleRulerVisible: false,
  911. centerScaleRulerCenterXPx: 0,
  912. centerScaleRulerZeroYPx: 0,
  913. centerScaleRulerHeightPx: 0,
  914. centerScaleRulerAxisBottomPx: 0,
  915. centerScaleRulerZeroVisible: false,
  916. centerScaleRulerZeroLabel: '0 m',
  917. centerScaleRulerMinorTicks: [],
  918. centerScaleRulerMajorMarks: [],
  919. compassTicks: buildCompassTicks(),
  920. compassLabels: buildCompassLabels(),
  921. ...buildSideButtonVisibility('shown'),
  922. ...buildSideButtonState({
  923. sideButtonMode: 'shown',
  924. showGameInfoPanel: false,
  925. showSystemSettingsPanel: false,
  926. showCenterScaleRuler: false,
  927. centerScaleRulerAnchorMode: 'screen-center',
  928. skipButtonEnabled: false,
  929. gameSessionStatus: 'idle',
  930. gpsLockEnabled: false,
  931. gpsLockAvailable: false,
  932. }),
  933. } as unknown as MapPageData,
  934. onLoad() {
  935. const systemInfo = wx.getSystemInfoSync()
  936. const statusBarHeight = systemInfo.statusBarHeight || 0
  937. const menuButtonRect = wx.getMenuButtonBoundingClientRect()
  938. const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
  939. if (mapEngine) {
  940. mapEngine.destroy()
  941. mapEngine = null
  942. }
  943. mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
  944. onData: (patch) => {
  945. const nextPatch = patch as Partial<MapPageData>
  946. const includeDebugFields = this.data.showDebugPanel
  947. const includeRulerFields = this.data.showCenterScaleRuler
  948. const nextData: Partial<MapPageData> = filterDebugOnlyPatch({
  949. ...nextPatch,
  950. }, includeDebugFields, includeRulerFields)
  951. if (
  952. typeof nextPatch.mockBridgeUrlText === 'string'
  953. && this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
  954. ) {
  955. nextData.mockBridgeUrlDraft = nextPatch.mockBridgeUrlText
  956. }
  957. if (
  958. typeof nextPatch.mockHeartRateBridgeUrlText === 'string'
  959. && this.data.mockHeartRateBridgeUrlDraft === this.data.mockHeartRateBridgeUrlText
  960. ) {
  961. nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
  962. }
  963. if (
  964. typeof nextPatch.mockDebugLogBridgeUrlText === 'string'
  965. && this.data.mockDebugLogBridgeUrlDraft === this.data.mockDebugLogBridgeUrlText
  966. ) {
  967. nextData.mockDebugLogBridgeUrlDraft = nextPatch.mockDebugLogBridgeUrlText
  968. }
  969. updateCenterScaleRulerInputCache(nextPatch)
  970. const mergedData = {
  971. ...centerScaleRulerInputCache,
  972. ...this.data,
  973. ...nextData,
  974. } as MapPageData
  975. const derivedPatch: Partial<MapPageData> = {}
  976. if (typeof nextPatch.orientationMode === 'string') {
  977. nextData.autoRotateEnabled = nextPatch.orientationMode === 'heading-up'
  978. }
  979. if (
  980. this.data.showCenterScaleRuler
  981. && hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
  982. ) {
  983. clearCenterScaleRulerUpdateTimer()
  984. Object.assign(derivedPatch, buildCenterScaleRulerPatch(mergedData))
  985. }
  986. if (hasAnyPatchKey(nextPatch as Record<string, unknown>, SIDE_BUTTON_DEP_KEYS)) {
  987. Object.assign(derivedPatch, buildSideButtonState(mergedData))
  988. }
  989. if (typeof nextPatch.punchHintText === 'string') {
  990. const nextHintText = nextPatch.punchHintText.trim()
  991. if (nextHintText !== this.data.punchHintText) {
  992. clearPunchHintDismissTimer()
  993. nextData.showPunchHintBanner = nextHintText.length > 0
  994. if (nextHintText.length > 0) {
  995. punchHintDismissTimer = setTimeout(() => {
  996. punchHintDismissTimer = 0
  997. this.setData({
  998. showPunchHintBanner: false,
  999. })
  1000. }, PUNCH_HINT_AUTO_HIDE_MS) as unknown as number
  1001. }
  1002. } else if (!nextHintText) {
  1003. clearPunchHintDismissTimer()
  1004. nextData.showPunchHintBanner = false
  1005. }
  1006. }
  1007. const nextAnimationLevel = typeof nextPatch.animationLevel === 'string'
  1008. ? nextPatch.animationLevel
  1009. : this.data.animationLevel
  1010. if (nextAnimationLevel === 'lite') {
  1011. clearHudFxTimer('timer')
  1012. clearHudFxTimer('mileage')
  1013. clearHudFxTimer('speed')
  1014. clearHudFxTimer('heartRate')
  1015. nextData.panelTimerFxClass = ''
  1016. nextData.panelMileageFxClass = ''
  1017. nextData.panelSpeedFxClass = ''
  1018. nextData.panelHeartRateFxClass = ''
  1019. } else {
  1020. if (typeof nextPatch.panelTimerText === 'string' && nextPatch.panelTimerText !== this.data.panelTimerText && this.data.panelTimerText !== '00:00:00') {
  1021. clearHudFxTimer('timer')
  1022. nextData.panelTimerFxClass = 'race-panel__timer--fx-tick'
  1023. panelTimerFxTimer = setTimeout(() => {
  1024. panelTimerFxTimer = 0
  1025. this.setData({ panelTimerFxClass: '' })
  1026. }, 320) as unknown as number
  1027. }
  1028. if (typeof nextPatch.panelMileageText === 'string' && nextPatch.panelMileageText !== this.data.panelMileageText && this.data.panelMileageText !== '0m') {
  1029. clearHudFxTimer('mileage')
  1030. nextData.panelMileageFxClass = 'race-panel__mileage-wrap--fx-update'
  1031. panelMileageFxTimer = setTimeout(() => {
  1032. panelMileageFxTimer = 0
  1033. this.setData({ panelMileageFxClass: '' })
  1034. }, 360) as unknown as number
  1035. }
  1036. if (typeof nextPatch.panelSpeedValueText === 'string' && nextPatch.panelSpeedValueText !== this.data.panelSpeedValueText && this.data.panelSpeedValueText !== '0') {
  1037. clearHudFxTimer('speed')
  1038. nextData.panelSpeedFxClass = 'race-panel__metric-group--fx-speed-update'
  1039. panelSpeedFxTimer = setTimeout(() => {
  1040. panelSpeedFxTimer = 0
  1041. this.setData({ panelSpeedFxClass: '' })
  1042. }, 360) as unknown as number
  1043. }
  1044. if (typeof nextPatch.panelHeartRateValueText === 'string' && nextPatch.panelHeartRateValueText !== this.data.panelHeartRateValueText && this.data.panelHeartRateValueText !== '--') {
  1045. clearHudFxTimer('heartRate')
  1046. nextData.panelHeartRateFxClass = 'race-panel__metric-group--fx-heart-rate-update'
  1047. panelHeartRateFxTimer = setTimeout(() => {
  1048. panelHeartRateFxTimer = 0
  1049. this.setData({ panelHeartRateFxClass: '' })
  1050. }, 400) as unknown as number
  1051. }
  1052. }
  1053. if (typeof nextPatch.gameSessionStatus === 'string') {
  1054. if (
  1055. nextPatch.gameSessionStatus !== this.data.gameSessionStatus
  1056. && (nextPatch.gameSessionStatus === 'finished' || nextPatch.gameSessionStatus === 'failed')
  1057. ) {
  1058. this.syncResultSceneSnapshot()
  1059. nextData.showResultScene = true
  1060. nextData.showDebugPanel = false
  1061. nextData.showGameInfoPanel = false
  1062. nextData.showSystemSettingsPanel = false
  1063. clearGameInfoPanelSyncTimer()
  1064. } else if (nextPatch.gameSessionStatus === 'running' || nextPatch.gameSessionStatus === 'idle') {
  1065. nextData.showResultScene = false
  1066. }
  1067. }
  1068. if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
  1069. this.setData({
  1070. ...nextData,
  1071. ...derivedPatch,
  1072. })
  1073. }
  1074. if (this.data.showGameInfoPanel) {
  1075. this.scheduleGameInfoPanelSnapshotSync()
  1076. }
  1077. },
  1078. onOpenH5Experience: (request) => {
  1079. this.openH5Experience(request)
  1080. },
  1081. })
  1082. const storedUserSettings = loadStoredUserSettings()
  1083. if (storedUserSettings.animationLevel) {
  1084. mapEngine.handleSetAnimationLevel(storedUserSettings.animationLevel)
  1085. }
  1086. if (storedUserSettings.trackDisplayMode) {
  1087. mapEngine.handleSetTrackMode(storedUserSettings.trackDisplayMode)
  1088. }
  1089. if (storedUserSettings.trackTailLength) {
  1090. mapEngine.handleSetTrackTailLength(storedUserSettings.trackTailLength)
  1091. }
  1092. if (storedUserSettings.trackColorPreset) {
  1093. mapEngine.handleSetTrackColorPreset(storedUserSettings.trackColorPreset)
  1094. }
  1095. if (storedUserSettings.trackStyleProfile) {
  1096. mapEngine.handleSetTrackStyleProfile(storedUserSettings.trackStyleProfile)
  1097. }
  1098. if (typeof storedUserSettings.gpsMarkerVisible === 'boolean') {
  1099. mapEngine.handleSetGpsMarkerVisible(storedUserSettings.gpsMarkerVisible)
  1100. }
  1101. if (storedUserSettings.gpsMarkerStyle) {
  1102. mapEngine.handleSetGpsMarkerStyle(storedUserSettings.gpsMarkerStyle)
  1103. }
  1104. if (storedUserSettings.gpsMarkerSize) {
  1105. mapEngine.handleSetGpsMarkerSize(storedUserSettings.gpsMarkerSize)
  1106. }
  1107. if (storedUserSettings.gpsMarkerColorPreset) {
  1108. mapEngine.handleSetGpsMarkerColorPreset(storedUserSettings.gpsMarkerColorPreset)
  1109. }
  1110. const initialAutoRotateEnabled = storedUserSettings.autoRotateEnabled !== false
  1111. if (initialAutoRotateEnabled) {
  1112. mapEngine.handleSetHeadingUpMode()
  1113. } else {
  1114. mapEngine.handleSetManualMode()
  1115. }
  1116. if (storedUserSettings.compassTuningProfile) {
  1117. mapEngine.handleSetCompassTuningProfile(storedUserSettings.compassTuningProfile)
  1118. }
  1119. if (storedUserSettings.northReferenceMode) {
  1120. mapEngine.handleSetNorthReferenceMode(storedUserSettings.northReferenceMode)
  1121. }
  1122. const initialSideButtonPlacement = storedUserSettings.sideButtonPlacement || 'left'
  1123. mapEngine.setDiagnosticUiEnabled(false)
  1124. centerScaleRulerInputCache = {
  1125. stageWidth: 0,
  1126. stageHeight: 0,
  1127. zoom: 0,
  1128. centerTileY: 0,
  1129. tileSizePx: 0,
  1130. previewScale: 1,
  1131. }
  1132. const initialShowCenterScaleRuler = !!storedUserSettings.showCenterScaleRuler
  1133. const initialCenterScaleRulerAnchorMode = storedUserSettings.centerScaleRulerAnchorMode || 'screen-center'
  1134. this.setData({
  1135. ...mapEngine.getInitialData(),
  1136. showDebugPanel: false,
  1137. showGameInfoPanel: false,
  1138. showSystemSettingsPanel: false,
  1139. showCenterScaleRuler: initialShowCenterScaleRuler,
  1140. statusBarHeight,
  1141. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  1142. hudPanelIndex: 0,
  1143. configSourceText: '顺序赛配置',
  1144. centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
  1145. autoRotateEnabled: initialAutoRotateEnabled,
  1146. lockAnimationLevel: !!storedUserSettings.lockAnimationLevel,
  1147. lockTrackMode: !!storedUserSettings.lockTrackMode,
  1148. lockTrackTailLength: !!storedUserSettings.lockTrackTailLength,
  1149. lockTrackColor: !!storedUserSettings.lockTrackColor,
  1150. lockTrackStyle: !!storedUserSettings.lockTrackStyle,
  1151. lockGpsMarkerVisible: !!storedUserSettings.lockGpsMarkerVisible,
  1152. lockGpsMarkerStyle: !!storedUserSettings.lockGpsMarkerStyle,
  1153. lockGpsMarkerSize: !!storedUserSettings.lockGpsMarkerSize,
  1154. lockGpsMarkerColor: !!storedUserSettings.lockGpsMarkerColor,
  1155. lockSideButtonPlacement: !!storedUserSettings.lockSideButtonPlacement,
  1156. lockAutoRotate: !!storedUserSettings.lockAutoRotate,
  1157. lockCompassTuning: !!storedUserSettings.lockCompassTuning,
  1158. lockScaleRulerVisible: !!storedUserSettings.lockScaleRulerVisible,
  1159. lockScaleRulerAnchor: !!storedUserSettings.lockScaleRulerAnchor,
  1160. lockNorthReference: !!storedUserSettings.lockNorthReference,
  1161. lockHeartRateDevice: !!storedUserSettings.lockHeartRateDevice,
  1162. sideButtonPlacement: initialSideButtonPlacement,
  1163. gameInfoTitle: '当前游戏',
  1164. gameInfoSubtitle: '未开始',
  1165. gameInfoLocalRows: [],
  1166. gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
  1167. panelTimerText: '00:00:00',
  1168. panelTimerFxClass: '',
  1169. panelMileageText: '0m',
  1170. panelMileageFxClass: '',
  1171. panelActionTagText: '目标',
  1172. panelDistanceTagText: '点距',
  1173. panelDistanceValueText: '--',
  1174. panelDistanceUnitText: '',
  1175. panelProgressText: '0/0',
  1176. showPunchHintBanner: true,
  1177. gameSessionStatus: 'idle',
  1178. gameModeText: '顺序赛',
  1179. gpsLockEnabled: false,
  1180. gpsLockAvailable: false,
  1181. locationSourceMode: 'real',
  1182. locationSourceText: '真实定位',
  1183. mockBridgeConnected: false,
  1184. mockBridgeStatusText: '未连接',
  1185. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  1186. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  1187. mockCoordText: '--',
  1188. mockSpeedText: '--',
  1189. heartRateSourceMode: 'real',
  1190. heartRateSourceText: '真实心率',
  1191. mockHeartRateBridgeConnected: false,
  1192. mockHeartRateBridgeStatusText: '未连接',
  1193. mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-hr',
  1194. mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-hr',
  1195. mockHeartRateText: '--',
  1196. mockDebugLogBridgeConnected: false,
  1197. mockDebugLogBridgeStatusText: '已关闭 (wss://gs.gotomars.xyz/debug-log)',
  1198. mockDebugLogBridgeUrlText: 'wss://gs.gotomars.xyz/debug-log',
  1199. mockDebugLogBridgeUrlDraft: 'wss://gs.gotomars.xyz/debug-log',
  1200. panelSpeedValueText: '0',
  1201. panelSpeedFxClass: '',
  1202. panelTelemetryTone: 'blue',
  1203. gpsLogoStatusText: '未配置',
  1204. gpsLogoSourceText: '--',
  1205. panelHeartRateZoneNameText: '--',
  1206. panelHeartRateZoneRangeText: '',
  1207. heartRateConnected: false,
  1208. heartRateStatusText: '心率带未连接',
  1209. heartRateDeviceText: '--',
  1210. panelHeartRateValueText: '--',
  1211. panelHeartRateFxClass: '',
  1212. panelHeartRateUnitText: '',
  1213. panelCaloriesValueText: '0',
  1214. panelCaloriesUnitText: 'kcal',
  1215. panelAverageSpeedValueText: '0',
  1216. panelAverageSpeedUnitText: 'km/h',
  1217. panelAccuracyValueText: '--',
  1218. panelAccuracyUnitText: '',
  1219. deviceHeadingText: '--',
  1220. devicePoseText: '竖持',
  1221. headingConfidenceText: '低',
  1222. accelerometerText: '--',
  1223. gyroscopeText: '--',
  1224. deviceMotionText: '--',
  1225. compassSourceText: '无数据',
  1226. compassTuningProfile: 'balanced',
  1227. compassTuningProfileText: '平衡',
  1228. punchButtonText: '打点',
  1229. punchButtonEnabled: false,
  1230. skipButtonEnabled: false,
  1231. punchHintText: '等待进入检查点范围',
  1232. punchFeedbackVisible: false,
  1233. punchFeedbackText: '',
  1234. punchFeedbackTone: 'neutral',
  1235. contentCardVisible: false,
  1236. contentCardTemplate: 'story',
  1237. contentCardTitle: '',
  1238. contentCardBody: '',
  1239. contentCardActions: [],
  1240. contentQuizVisible: false,
  1241. contentQuizQuestionText: '',
  1242. contentQuizCountdownText: '',
  1243. contentQuizOptions: [],
  1244. contentQuizFeedbackVisible: false,
  1245. contentQuizFeedbackText: '',
  1246. contentQuizFeedbackTone: 'neutral',
  1247. punchButtonFxClass: '',
  1248. panelProgressFxClass: '',
  1249. panelDistanceFxClass: '',
  1250. punchFeedbackFxClass: '',
  1251. contentCardFxClass: '',
  1252. mapPulseVisible: false,
  1253. mapPulseLeftPx: 0,
  1254. mapPulseTopPx: 0,
  1255. mapPulseFxClass: '',
  1256. stageFxVisible: false,
  1257. stageFxClass: '',
  1258. compassTicks: buildCompassTicks(),
  1259. compassLabels: buildCompassLabels(),
  1260. ...buildSideButtonVisibility('shown'),
  1261. ...buildSideButtonState({
  1262. sideButtonMode: 'shown',
  1263. showGameInfoPanel: false,
  1264. showSystemSettingsPanel: false,
  1265. showCenterScaleRuler: initialShowCenterScaleRuler,
  1266. centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
  1267. skipButtonEnabled: false,
  1268. gameSessionStatus: 'idle',
  1269. gpsLockEnabled: false,
  1270. gpsLockAvailable: false,
  1271. }),
  1272. ...buildCenterScaleRulerPatch({
  1273. ...(mapEngine.getInitialData() as MapPageData),
  1274. showCenterScaleRuler: initialShowCenterScaleRuler,
  1275. centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
  1276. stageWidth: 0,
  1277. stageHeight: 0,
  1278. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  1279. zoom: 0,
  1280. centerTileY: 0,
  1281. tileSizePx: 0,
  1282. }),
  1283. })
  1284. },
  1285. onReady() {
  1286. stageCanvasAttached = false
  1287. this.measureStageAndCanvas()
  1288. this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
  1289. },
  1290. onShow() {
  1291. if (mapEngine) {
  1292. mapEngine.handleAppShow()
  1293. }
  1294. },
  1295. onHide() {
  1296. if (mapEngine) {
  1297. mapEngine.handleAppHide()
  1298. }
  1299. },
  1300. onUnload() {
  1301. clearGameInfoPanelSyncTimer()
  1302. clearCenterScaleRulerSyncTimer()
  1303. clearCenterScaleRulerUpdateTimer()
  1304. clearPunchHintDismissTimer()
  1305. clearHudFxTimer('timer')
  1306. clearHudFxTimer('mileage')
  1307. clearHudFxTimer('speed')
  1308. clearHudFxTimer('heartRate')
  1309. if (mapEngine) {
  1310. mapEngine.destroy()
  1311. mapEngine = null
  1312. }
  1313. stageCanvasAttached = false
  1314. },
  1315. loadMapConfigFromRemote(configUrl: string, configLabel: string) {
  1316. const currentEngine = mapEngine
  1317. if (!currentEngine) {
  1318. return
  1319. }
  1320. this.setData({
  1321. configSourceText: configLabel,
  1322. configStatusText: `加载中: ${configLabel}`,
  1323. })
  1324. loadRemoteMapConfig(configUrl)
  1325. .then((config) => {
  1326. if (mapEngine !== currentEngine) {
  1327. return
  1328. }
  1329. currentEngine.applyRemoteMapConfig(config)
  1330. })
  1331. .catch((error) => {
  1332. if (mapEngine !== currentEngine) {
  1333. return
  1334. }
  1335. const errorMessage = error && error.message ? error.message : '未知错误'
  1336. this.setData({
  1337. configStatusText: `载入失败: ${errorMessage}`,
  1338. statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
  1339. })
  1340. })
  1341. },
  1342. measureStageAndCanvas(onApplied?: () => void) {
  1343. const page = this
  1344. const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
  1345. const fallbackRect = getFallbackStageRect()
  1346. const rect: MapEngineStageRect = {
  1347. width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
  1348. height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
  1349. left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
  1350. top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
  1351. }
  1352. const currentEngine = mapEngine
  1353. if (!currentEngine) {
  1354. return
  1355. }
  1356. currentEngine.setStage(rect)
  1357. if (onApplied) {
  1358. onApplied()
  1359. }
  1360. if (stageCanvasAttached) {
  1361. return
  1362. }
  1363. const canvasQuery = wx.createSelectorQuery().in(page)
  1364. canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
  1365. canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
  1366. canvasQuery.exec((canvasRes) => {
  1367. const canvasRef = canvasRes[0] as any
  1368. const labelCanvasRef = canvasRes[1] as any
  1369. if (!canvasRef || !canvasRef.node) {
  1370. page.setData({
  1371. statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
  1372. })
  1373. return
  1374. }
  1375. const dpr = wx.getSystemInfoSync().pixelRatio || 1
  1376. try {
  1377. currentEngine.attachCanvas(
  1378. canvasRef.node,
  1379. rect.width,
  1380. rect.height,
  1381. dpr,
  1382. labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
  1383. )
  1384. stageCanvasAttached = true
  1385. } catch (error) {
  1386. page.setData({
  1387. statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
  1388. })
  1389. }
  1390. })
  1391. }
  1392. const query = wx.createSelectorQuery().in(page)
  1393. query.select('.map-stage').boundingClientRect()
  1394. query.exec((res) => {
  1395. const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
  1396. applyStage(rect)
  1397. })
  1398. },
  1399. handleTouchStart(event: WechatMiniprogram.TouchEvent) {
  1400. if (mapEngine) {
  1401. mapEngine.handleTouchStart(event)
  1402. }
  1403. },
  1404. handleTouchMove(event: WechatMiniprogram.TouchEvent) {
  1405. if (mapEngine) {
  1406. mapEngine.handleTouchMove(event)
  1407. }
  1408. },
  1409. handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
  1410. if (mapEngine) {
  1411. mapEngine.handleTouchEnd(event)
  1412. }
  1413. },
  1414. handleTouchCancel() {
  1415. if (mapEngine) {
  1416. mapEngine.handleTouchCancel()
  1417. }
  1418. },
  1419. handleRecenter() {
  1420. if (mapEngine) {
  1421. mapEngine.handleRecenter()
  1422. }
  1423. },
  1424. handleRotateStep() {
  1425. if (mapEngine) {
  1426. mapEngine.handleRotateStep()
  1427. }
  1428. },
  1429. handleRotationReset() {
  1430. if (mapEngine) {
  1431. mapEngine.handleRotationReset()
  1432. }
  1433. },
  1434. handleSetManualMode() {
  1435. if (mapEngine) {
  1436. mapEngine.handleSetManualMode()
  1437. }
  1438. },
  1439. handleSetNorthUpMode() {
  1440. if (mapEngine) {
  1441. mapEngine.handleSetNorthUpMode()
  1442. }
  1443. },
  1444. handleSetHeadingUpMode() {
  1445. if (mapEngine) {
  1446. mapEngine.handleSetHeadingUpMode()
  1447. }
  1448. },
  1449. handleCycleNorthReferenceMode() {
  1450. if (mapEngine) {
  1451. mapEngine.handleCycleNorthReferenceMode()
  1452. }
  1453. },
  1454. handleAutoRotateCalibrate() {
  1455. if (mapEngine) {
  1456. mapEngine.handleAutoRotateCalibrate()
  1457. }
  1458. },
  1459. handleToggleGpsTracking() {
  1460. if (mapEngine) {
  1461. mapEngine.handleToggleGpsTracking()
  1462. }
  1463. },
  1464. handleSetRealLocationMode() {
  1465. if (mapEngine) {
  1466. mapEngine.handleSetRealLocationMode()
  1467. }
  1468. },
  1469. handleSetMockLocationMode() {
  1470. if (mapEngine) {
  1471. mapEngine.handleSetMockLocationMode()
  1472. }
  1473. },
  1474. handleConnectMockLocationBridge() {
  1475. if (mapEngine) {
  1476. mapEngine.handleConnectMockLocationBridge()
  1477. }
  1478. },
  1479. handleConnectAllMockSources() {
  1480. if (!mapEngine) {
  1481. return
  1482. }
  1483. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  1484. mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
  1485. mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  1486. mapEngine.handleConnectMockLocationBridge()
  1487. mapEngine.handleSetMockLocationMode()
  1488. mapEngine.handleSetMockHeartRateMode()
  1489. mapEngine.handleConnectMockHeartRateBridge()
  1490. mapEngine.handleConnectMockDebugLogBridge()
  1491. },
  1492. handleOpenWebViewTest() {
  1493. wx.navigateTo({
  1494. url: '/pages/webview-test/webview-test',
  1495. })
  1496. },
  1497. handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
  1498. this.setData({
  1499. mockBridgeUrlDraft: event.detail.value,
  1500. })
  1501. },
  1502. handleSaveMockBridgeUrl() {
  1503. if (mapEngine) {
  1504. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  1505. }
  1506. },
  1507. handleDisconnectMockLocationBridge() {
  1508. if (mapEngine) {
  1509. mapEngine.handleDisconnectMockLocationBridge()
  1510. }
  1511. },
  1512. handleSetRealHeartRateMode() {
  1513. if (mapEngine) {
  1514. mapEngine.handleSetRealHeartRateMode()
  1515. }
  1516. },
  1517. handleSetMockHeartRateMode() {
  1518. if (mapEngine) {
  1519. mapEngine.handleSetMockHeartRateMode()
  1520. }
  1521. },
  1522. handleMockHeartRateBridgeUrlInput(event: WechatMiniprogram.Input) {
  1523. this.setData({
  1524. mockHeartRateBridgeUrlDraft: event.detail.value,
  1525. })
  1526. },
  1527. handleSaveMockHeartRateBridgeUrl() {
  1528. if (mapEngine) {
  1529. mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
  1530. }
  1531. },
  1532. handleMockDebugLogBridgeUrlInput(event: WechatMiniprogram.Input) {
  1533. this.setData({
  1534. mockDebugLogBridgeUrlDraft: event.detail.value,
  1535. })
  1536. },
  1537. handleSaveMockDebugLogBridgeUrl() {
  1538. if (mapEngine) {
  1539. mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
  1540. }
  1541. },
  1542. handleConnectMockDebugLogBridge() {
  1543. if (mapEngine) {
  1544. mapEngine.handleConnectMockDebugLogBridge()
  1545. }
  1546. },
  1547. handleDisconnectMockDebugLogBridge() {
  1548. if (mapEngine) {
  1549. mapEngine.handleDisconnectMockDebugLogBridge()
  1550. }
  1551. },
  1552. handleConnectMockHeartRateBridge() {
  1553. if (mapEngine) {
  1554. mapEngine.handleConnectMockHeartRateBridge()
  1555. }
  1556. },
  1557. handleDisconnectMockHeartRateBridge() {
  1558. if (mapEngine) {
  1559. mapEngine.handleDisconnectMockHeartRateBridge()
  1560. }
  1561. },
  1562. handleConnectHeartRate() {
  1563. if (mapEngine) {
  1564. mapEngine.handleConnectHeartRate()
  1565. }
  1566. },
  1567. handleDisconnectHeartRate() {
  1568. if (mapEngine) {
  1569. mapEngine.handleDisconnectHeartRate()
  1570. }
  1571. },
  1572. handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
  1573. if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
  1574. mapEngine.handleConnectHeartRateDevice(event.currentTarget.dataset.deviceId)
  1575. }
  1576. },
  1577. handleClearPreferredHeartRateDevice() {
  1578. if (this.data.lockHeartRateDevice) {
  1579. return
  1580. }
  1581. if (mapEngine) {
  1582. mapEngine.handleClearPreferredHeartRateDevice()
  1583. }
  1584. },
  1585. handleDebugHeartRateBlue() {
  1586. if (mapEngine) {
  1587. mapEngine.handleDebugHeartRateTone('blue')
  1588. }
  1589. },
  1590. handleDebugHeartRatePurple() {
  1591. if (mapEngine) {
  1592. mapEngine.handleDebugHeartRateTone('purple')
  1593. }
  1594. },
  1595. handleDebugHeartRateGreen() {
  1596. if (mapEngine) {
  1597. mapEngine.handleDebugHeartRateTone('green')
  1598. }
  1599. },
  1600. handleDebugHeartRateYellow() {
  1601. if (mapEngine) {
  1602. mapEngine.handleDebugHeartRateTone('yellow')
  1603. }
  1604. },
  1605. handleDebugHeartRateOrange() {
  1606. if (mapEngine) {
  1607. mapEngine.handleDebugHeartRateTone('orange')
  1608. }
  1609. },
  1610. handleDebugHeartRateRed() {
  1611. if (mapEngine) {
  1612. mapEngine.handleDebugHeartRateTone('red')
  1613. }
  1614. },
  1615. handleClearDebugHeartRate() {
  1616. if (mapEngine) {
  1617. mapEngine.handleClearDebugHeartRate()
  1618. }
  1619. },
  1620. handleToggleOsmReference() {
  1621. if (mapEngine) {
  1622. mapEngine.handleToggleOsmReference()
  1623. }
  1624. },
  1625. handleStartGame() {
  1626. if (mapEngine) {
  1627. mapEngine.handleStartGame()
  1628. }
  1629. },
  1630. handleLoadClassicConfig() {
  1631. this.loadMapConfigFromRemote(CLASSIC_REMOTE_GAME_CONFIG_URL, '顺序赛配置')
  1632. },
  1633. handleLoadScoreOConfig() {
  1634. this.loadMapConfigFromRemote(SCORE_O_REMOTE_GAME_CONFIG_URL, '积分赛配置')
  1635. },
  1636. handleForceExitGame() {
  1637. if (!mapEngine || this.data.gameSessionStatus !== 'running') {
  1638. return
  1639. }
  1640. wx.showModal({
  1641. title: '确认退出',
  1642. content: '确认强制结束当前对局并返回开始前状态?',
  1643. confirmText: '确认退出',
  1644. cancelText: '取消',
  1645. success: (result) => {
  1646. if (result.confirm && mapEngine) {
  1647. mapEngine.handleForceExitGame()
  1648. }
  1649. },
  1650. })
  1651. },
  1652. handleSkipAction() {
  1653. if (!mapEngine || !this.data.skipButtonEnabled) {
  1654. return
  1655. }
  1656. if (!mapEngine.shouldConfirmSkipAction()) {
  1657. mapEngine.handleSkipAction()
  1658. return
  1659. }
  1660. wx.showModal({
  1661. title: '确认跳点',
  1662. content: '确认跳过当前检查点并切换到下一个目标点?',
  1663. confirmText: '确认跳过',
  1664. cancelText: '取消',
  1665. success: (result) => {
  1666. if (result.confirm && mapEngine) {
  1667. mapEngine.handleSkipAction()
  1668. }
  1669. },
  1670. })
  1671. },
  1672. handleClearMapTestArtifacts() {
  1673. if (mapEngine) {
  1674. mapEngine.handleClearMapTestArtifacts()
  1675. }
  1676. },
  1677. syncGameInfoPanelSnapshot() {
  1678. if (!mapEngine) {
  1679. return
  1680. }
  1681. const snapshot = mapEngine.getGameInfoSnapshot()
  1682. const localRows = snapshot.localRows.concat([
  1683. { label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
  1684. { label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
  1685. { label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
  1686. { label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
  1687. { label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
  1688. { label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
  1689. { label: '比例尺高度', value: `${this.data.centerScaleRulerHeightPx}px` },
  1690. { label: '比例尺主刻度数', value: String(this.data.centerScaleRulerMajorMarks.length) },
  1691. ])
  1692. this.setData({
  1693. gameInfoTitle: snapshot.title,
  1694. gameInfoSubtitle: snapshot.subtitle,
  1695. gameInfoLocalRows: localRows,
  1696. gameInfoGlobalRows: snapshot.globalRows,
  1697. })
  1698. },
  1699. syncResultSceneSnapshot() {
  1700. if (!mapEngine) {
  1701. return
  1702. }
  1703. const snapshot = mapEngine.getResultSceneSnapshot()
  1704. this.setData({
  1705. resultSceneTitle: snapshot.title,
  1706. resultSceneSubtitle: snapshot.subtitle,
  1707. resultSceneHeroLabel: snapshot.heroLabel,
  1708. resultSceneHeroValue: snapshot.heroValue,
  1709. resultSceneRows: snapshot.rows,
  1710. })
  1711. },
  1712. scheduleGameInfoPanelSnapshotSync() {
  1713. if (!this.data.showGameInfoPanel) {
  1714. clearGameInfoPanelSyncTimer()
  1715. return
  1716. }
  1717. if (gameInfoPanelSyncTimer) {
  1718. return
  1719. }
  1720. gameInfoPanelSyncTimer = setTimeout(() => {
  1721. gameInfoPanelSyncTimer = 0
  1722. if (this.data.showGameInfoPanel) {
  1723. this.syncGameInfoPanelSnapshot()
  1724. }
  1725. }, 400) as unknown as number
  1726. },
  1727. handleOpenGameInfoPanel() {
  1728. clearGameInfoPanelSyncTimer()
  1729. this.syncGameInfoPanelSnapshot()
  1730. this.setData({
  1731. showDebugPanel: false,
  1732. showSystemSettingsPanel: false,
  1733. showGameInfoPanel: true,
  1734. ...buildSideButtonState({
  1735. sideButtonMode: this.data.sideButtonMode,
  1736. showGameInfoPanel: true,
  1737. showSystemSettingsPanel: false,
  1738. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1739. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1740. skipButtonEnabled: this.data.skipButtonEnabled,
  1741. gameSessionStatus: this.data.gameSessionStatus,
  1742. gpsLockEnabled: this.data.gpsLockEnabled,
  1743. gpsLockAvailable: this.data.gpsLockAvailable,
  1744. }),
  1745. })
  1746. },
  1747. handleCloseGameInfoPanel() {
  1748. clearGameInfoPanelSyncTimer()
  1749. this.setData({
  1750. showGameInfoPanel: false,
  1751. ...buildSideButtonState({
  1752. sideButtonMode: this.data.sideButtonMode,
  1753. showGameInfoPanel: false,
  1754. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  1755. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1756. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1757. skipButtonEnabled: this.data.skipButtonEnabled,
  1758. gameSessionStatus: this.data.gameSessionStatus,
  1759. gpsLockEnabled: this.data.gpsLockEnabled,
  1760. gpsLockAvailable: this.data.gpsLockAvailable,
  1761. }),
  1762. })
  1763. },
  1764. handleGameInfoPanelTap() {},
  1765. handleResultSceneTap() {},
  1766. handleCloseResultScene() {
  1767. this.setData({
  1768. showResultScene: false,
  1769. })
  1770. },
  1771. handleRestartFromResult() {
  1772. if (!mapEngine) {
  1773. return
  1774. }
  1775. this.setData({
  1776. showResultScene: false,
  1777. }, () => {
  1778. if (mapEngine) {
  1779. mapEngine.handleStartGame()
  1780. }
  1781. })
  1782. },
  1783. handleOpenSystemSettingsPanel() {
  1784. clearGameInfoPanelSyncTimer()
  1785. this.setData({
  1786. showDebugPanel: false,
  1787. showGameInfoPanel: false,
  1788. showSystemSettingsPanel: true,
  1789. ...buildSideButtonState({
  1790. sideButtonMode: this.data.sideButtonMode,
  1791. showGameInfoPanel: false,
  1792. showSystemSettingsPanel: true,
  1793. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1794. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1795. skipButtonEnabled: this.data.skipButtonEnabled,
  1796. gameSessionStatus: this.data.gameSessionStatus,
  1797. gpsLockEnabled: this.data.gpsLockEnabled,
  1798. gpsLockAvailable: this.data.gpsLockAvailable,
  1799. }),
  1800. })
  1801. },
  1802. handleCloseSystemSettingsPanel() {
  1803. this.setData({
  1804. showSystemSettingsPanel: false,
  1805. ...buildSideButtonState({
  1806. sideButtonMode: this.data.sideButtonMode,
  1807. showGameInfoPanel: this.data.showGameInfoPanel,
  1808. showSystemSettingsPanel: false,
  1809. showCenterScaleRuler: this.data.showCenterScaleRuler,
  1810. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  1811. skipButtonEnabled: this.data.skipButtonEnabled,
  1812. gameSessionStatus: this.data.gameSessionStatus,
  1813. gpsLockEnabled: this.data.gpsLockEnabled,
  1814. gpsLockAvailable: this.data.gpsLockAvailable,
  1815. }),
  1816. })
  1817. },
  1818. handleSystemSettingsPanelTap() {},
  1819. handleSetAnimationLevelStandard() {
  1820. if (this.data.lockAnimationLevel || !mapEngine) {
  1821. return
  1822. }
  1823. mapEngine.handleSetAnimationLevel('standard')
  1824. persistStoredUserSettings({
  1825. ...loadStoredUserSettings(),
  1826. animationLevel: 'standard',
  1827. })
  1828. },
  1829. handleSetAnimationLevelLite() {
  1830. if (this.data.lockAnimationLevel || !mapEngine) {
  1831. return
  1832. }
  1833. mapEngine.handleSetAnimationLevel('lite')
  1834. persistStoredUserSettings({
  1835. ...loadStoredUserSettings(),
  1836. animationLevel: 'lite',
  1837. })
  1838. },
  1839. handleSetTrackModeNone() {
  1840. if (this.data.lockTrackMode || !mapEngine) {
  1841. return
  1842. }
  1843. mapEngine.handleSetTrackMode('none')
  1844. persistStoredUserSettings({
  1845. ...loadStoredUserSettings(),
  1846. trackDisplayMode: 'none',
  1847. })
  1848. },
  1849. handleSetTrackModeTail() {
  1850. if (this.data.lockTrackMode || !mapEngine) {
  1851. return
  1852. }
  1853. mapEngine.handleSetTrackMode('tail')
  1854. persistStoredUserSettings({
  1855. ...loadStoredUserSettings(),
  1856. trackDisplayMode: 'tail',
  1857. })
  1858. },
  1859. handleSetTrackModeFull() {
  1860. if (this.data.lockTrackMode || !mapEngine) {
  1861. return
  1862. }
  1863. mapEngine.handleSetTrackMode('full')
  1864. persistStoredUserSettings({
  1865. ...loadStoredUserSettings(),
  1866. trackDisplayMode: 'full',
  1867. })
  1868. },
  1869. handleSetTrackTailLengthShort() {
  1870. if (this.data.lockTrackTailLength || !mapEngine) {
  1871. return
  1872. }
  1873. mapEngine.handleSetTrackTailLength('short')
  1874. persistStoredUserSettings({
  1875. ...loadStoredUserSettings(),
  1876. trackTailLength: 'short',
  1877. })
  1878. },
  1879. handleSetTrackTailLengthMedium() {
  1880. if (this.data.lockTrackTailLength || !mapEngine) {
  1881. return
  1882. }
  1883. mapEngine.handleSetTrackTailLength('medium')
  1884. persistStoredUserSettings({
  1885. ...loadStoredUserSettings(),
  1886. trackTailLength: 'medium',
  1887. })
  1888. },
  1889. handleSetTrackTailLengthLong() {
  1890. if (this.data.lockTrackTailLength || !mapEngine) {
  1891. return
  1892. }
  1893. mapEngine.handleSetTrackTailLength('long')
  1894. persistStoredUserSettings({
  1895. ...loadStoredUserSettings(),
  1896. trackTailLength: 'long',
  1897. })
  1898. },
  1899. handleSetTrackColorPreset(event: WechatMiniprogram.TouchEvent) {
  1900. if (this.data.lockTrackColor || !mapEngine) {
  1901. return
  1902. }
  1903. const color = event.currentTarget.dataset.color as TrackColorPreset | undefined
  1904. if (!color) {
  1905. return
  1906. }
  1907. mapEngine.handleSetTrackColorPreset(color)
  1908. persistStoredUserSettings({
  1909. ...loadStoredUserSettings(),
  1910. trackColorPreset: color,
  1911. })
  1912. },
  1913. handleSetTrackStyleClassic() {
  1914. if (this.data.lockTrackStyle || !mapEngine) {
  1915. return
  1916. }
  1917. mapEngine.handleSetTrackStyleProfile('classic')
  1918. persistStoredUserSettings({
  1919. ...loadStoredUserSettings(),
  1920. trackStyleProfile: 'classic',
  1921. })
  1922. },
  1923. handleSetTrackStyleNeon() {
  1924. if (this.data.lockTrackStyle || !mapEngine) {
  1925. return
  1926. }
  1927. mapEngine.handleSetTrackStyleProfile('neon')
  1928. persistStoredUserSettings({
  1929. ...loadStoredUserSettings(),
  1930. trackStyleProfile: 'neon',
  1931. })
  1932. },
  1933. handleSetGpsMarkerVisibleOn() {
  1934. if (this.data.lockGpsMarkerVisible || !mapEngine) {
  1935. return
  1936. }
  1937. mapEngine.handleSetGpsMarkerVisible(true)
  1938. persistStoredUserSettings({
  1939. ...loadStoredUserSettings(),
  1940. gpsMarkerVisible: true,
  1941. })
  1942. },
  1943. handleSetGpsMarkerVisibleOff() {
  1944. if (this.data.lockGpsMarkerVisible || !mapEngine) {
  1945. return
  1946. }
  1947. mapEngine.handleSetGpsMarkerVisible(false)
  1948. persistStoredUserSettings({
  1949. ...loadStoredUserSettings(),
  1950. gpsMarkerVisible: false,
  1951. })
  1952. },
  1953. handleSetGpsMarkerStyleDot() {
  1954. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  1955. return
  1956. }
  1957. mapEngine.handleSetGpsMarkerStyle('dot')
  1958. persistStoredUserSettings({
  1959. ...loadStoredUserSettings(),
  1960. gpsMarkerStyle: 'dot',
  1961. })
  1962. },
  1963. handleSetGpsMarkerStyleBeacon() {
  1964. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  1965. return
  1966. }
  1967. mapEngine.handleSetGpsMarkerStyle('beacon')
  1968. persistStoredUserSettings({
  1969. ...loadStoredUserSettings(),
  1970. gpsMarkerStyle: 'beacon',
  1971. })
  1972. },
  1973. handleSetGpsMarkerStyleDisc() {
  1974. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  1975. return
  1976. }
  1977. mapEngine.handleSetGpsMarkerStyle('disc')
  1978. persistStoredUserSettings({
  1979. ...loadStoredUserSettings(),
  1980. gpsMarkerStyle: 'disc',
  1981. })
  1982. },
  1983. handleSetGpsMarkerStyleBadge() {
  1984. if (this.data.lockGpsMarkerStyle || !mapEngine) {
  1985. return
  1986. }
  1987. mapEngine.handleSetGpsMarkerStyle('badge')
  1988. persistStoredUserSettings({
  1989. ...loadStoredUserSettings(),
  1990. gpsMarkerStyle: 'badge',
  1991. })
  1992. },
  1993. handleSetGpsMarkerSizeSmall() {
  1994. if (this.data.lockGpsMarkerSize || !mapEngine) {
  1995. return
  1996. }
  1997. mapEngine.handleSetGpsMarkerSize('small')
  1998. persistStoredUserSettings({
  1999. ...loadStoredUserSettings(),
  2000. gpsMarkerSize: 'small',
  2001. })
  2002. },
  2003. handleSetGpsMarkerSizeMedium() {
  2004. if (this.data.lockGpsMarkerSize || !mapEngine) {
  2005. return
  2006. }
  2007. mapEngine.handleSetGpsMarkerSize('medium')
  2008. persistStoredUserSettings({
  2009. ...loadStoredUserSettings(),
  2010. gpsMarkerSize: 'medium',
  2011. })
  2012. },
  2013. handleSetGpsMarkerSizeLarge() {
  2014. if (this.data.lockGpsMarkerSize || !mapEngine) {
  2015. return
  2016. }
  2017. mapEngine.handleSetGpsMarkerSize('large')
  2018. persistStoredUserSettings({
  2019. ...loadStoredUserSettings(),
  2020. gpsMarkerSize: 'large',
  2021. })
  2022. },
  2023. handleSetGpsMarkerColorPreset(event: WechatMiniprogram.TouchEvent) {
  2024. if (this.data.lockGpsMarkerColor || !mapEngine) {
  2025. return
  2026. }
  2027. const color = event.currentTarget.dataset.color as GpsMarkerColorPreset | undefined
  2028. if (!color) {
  2029. return
  2030. }
  2031. mapEngine.handleSetGpsMarkerColorPreset(color)
  2032. persistStoredUserSettings({
  2033. ...loadStoredUserSettings(),
  2034. gpsMarkerColorPreset: color,
  2035. })
  2036. },
  2037. handleSetSideButtonPlacementLeft() {
  2038. if (this.data.lockSideButtonPlacement) {
  2039. return
  2040. }
  2041. this.setData({
  2042. sideButtonPlacement: 'left',
  2043. })
  2044. persistStoredUserSettings({
  2045. ...loadStoredUserSettings(),
  2046. sideButtonPlacement: 'left',
  2047. })
  2048. },
  2049. handleSetSideButtonPlacementRight() {
  2050. if (this.data.lockSideButtonPlacement) {
  2051. return
  2052. }
  2053. this.setData({
  2054. sideButtonPlacement: 'right',
  2055. })
  2056. persistStoredUserSettings({
  2057. ...loadStoredUserSettings(),
  2058. sideButtonPlacement: 'right',
  2059. })
  2060. },
  2061. handleSetAutoRotateEnabledOn() {
  2062. if (this.data.lockAutoRotate || !mapEngine) {
  2063. return
  2064. }
  2065. mapEngine.handleSetHeadingUpMode()
  2066. persistStoredUserSettings({
  2067. ...loadStoredUserSettings(),
  2068. autoRotateEnabled: true,
  2069. })
  2070. },
  2071. handleSetAutoRotateEnabledOff() {
  2072. if (this.data.lockAutoRotate || !mapEngine) {
  2073. return
  2074. }
  2075. mapEngine.handleSetManualMode()
  2076. persistStoredUserSettings({
  2077. ...loadStoredUserSettings(),
  2078. autoRotateEnabled: false,
  2079. })
  2080. },
  2081. handleSetCompassTuningSmooth() {
  2082. if (this.data.lockCompassTuning || !mapEngine) {
  2083. return
  2084. }
  2085. mapEngine.handleSetCompassTuningProfile('smooth')
  2086. persistStoredUserSettings({
  2087. ...loadStoredUserSettings(),
  2088. compassTuningProfile: 'smooth',
  2089. })
  2090. },
  2091. handleSetCompassTuningBalanced() {
  2092. if (this.data.lockCompassTuning || !mapEngine) {
  2093. return
  2094. }
  2095. mapEngine.handleSetCompassTuningProfile('balanced')
  2096. persistStoredUserSettings({
  2097. ...loadStoredUserSettings(),
  2098. compassTuningProfile: 'balanced',
  2099. })
  2100. },
  2101. handleSetCompassTuningResponsive() {
  2102. if (this.data.lockCompassTuning || !mapEngine) {
  2103. return
  2104. }
  2105. mapEngine.handleSetCompassTuningProfile('responsive')
  2106. persistStoredUserSettings({
  2107. ...loadStoredUserSettings(),
  2108. compassTuningProfile: 'responsive',
  2109. })
  2110. },
  2111. handleSetNorthReferenceMagnetic() {
  2112. if (this.data.lockNorthReference || !mapEngine) {
  2113. return
  2114. }
  2115. mapEngine.handleSetNorthReferenceMode('magnetic')
  2116. persistStoredUserSettings({
  2117. ...loadStoredUserSettings(),
  2118. northReferenceMode: 'magnetic',
  2119. })
  2120. },
  2121. handleSetNorthReferenceTrue() {
  2122. if (this.data.lockNorthReference || !mapEngine) {
  2123. return
  2124. }
  2125. mapEngine.handleSetNorthReferenceMode('true')
  2126. persistStoredUserSettings({
  2127. ...loadStoredUserSettings(),
  2128. northReferenceMode: 'true',
  2129. })
  2130. },
  2131. handleToggleSettingLock(event: WechatMiniprogram.TouchEvent) {
  2132. const key = event.currentTarget.dataset.key as SettingLockKey | undefined
  2133. if (!key) {
  2134. return
  2135. }
  2136. const nextValue = !this.data[key]
  2137. this.setData({
  2138. [key]: nextValue,
  2139. } as Record<string, boolean>)
  2140. persistStoredUserSettings(toggleStoredSettingLock(loadStoredUserSettings(), key))
  2141. },
  2142. handleOverlayTouch() {},
  2143. handlePunchAction() {
  2144. if (!this.data.punchButtonEnabled) {
  2145. return
  2146. }
  2147. if (mapEngine) {
  2148. mapEngine.handlePunchAction()
  2149. }
  2150. },
  2151. handleOpenPendingContentCard() {
  2152. if (mapEngine) {
  2153. mapEngine.openPendingContentCard()
  2154. }
  2155. },
  2156. handleOpenContentCardAction(event: WechatMiniprogram.BaseEvent) {
  2157. if (!mapEngine) {
  2158. return
  2159. }
  2160. wx.showToast({
  2161. title: '点击CTA',
  2162. icon: 'none',
  2163. duration: 900,
  2164. })
  2165. const actionType = event.currentTarget.dataset.type
  2166. const action = typeof actionType === 'string' ? mapEngine.openCurrentContentCardAction(actionType) : null
  2167. if (action === 'detail') {
  2168. wx.showToast({
  2169. title: '打开详情',
  2170. icon: 'none',
  2171. duration: 900,
  2172. })
  2173. return
  2174. }
  2175. if (action === 'quiz') {
  2176. return
  2177. }
  2178. if (action === 'photo') {
  2179. wx.chooseMedia({
  2180. count: 1,
  2181. mediaType: ['image'],
  2182. sourceType: ['camera'],
  2183. success: () => {
  2184. if (mapEngine) {
  2185. mapEngine.handleContentCardPhotoCaptured()
  2186. }
  2187. },
  2188. })
  2189. return
  2190. }
  2191. if (action === 'audio') {
  2192. if (!contentAudioRecorder) {
  2193. contentAudioRecorder = wx.getRecorderManager()
  2194. contentAudioRecorder.onStop(() => {
  2195. contentAudioRecording = false
  2196. if (mapEngine) {
  2197. mapEngine.handleContentCardAudioRecorded()
  2198. }
  2199. })
  2200. }
  2201. const recorder = contentAudioRecorder
  2202. if (!contentAudioRecording) {
  2203. contentAudioRecording = true
  2204. recorder.start({
  2205. duration: 8000,
  2206. format: 'mp3',
  2207. } as any)
  2208. wx.showToast({
  2209. title: '开始录音',
  2210. icon: 'none',
  2211. duration: 800,
  2212. })
  2213. } else {
  2214. recorder.stop()
  2215. }
  2216. }
  2217. },
  2218. handleContentQuizAnswer(event: WechatMiniprogram.BaseEvent) {
  2219. if (!mapEngine) {
  2220. return
  2221. }
  2222. const optionKey = event.currentTarget.dataset.key
  2223. if (typeof optionKey === 'string') {
  2224. mapEngine.handleContentCardQuizAnswer(optionKey)
  2225. }
  2226. },
  2227. handleContentCardTap() {},
  2228. openH5Experience(request: H5ExperienceRequest) {
  2229. wx.navigateTo({
  2230. url: '/pages/experience-webview/experience-webview',
  2231. success: (result) => {
  2232. const eventChannel = result.eventChannel
  2233. eventChannel.on('fallback', (payload: H5ExperienceFallbackPayload) => {
  2234. if (mapEngine) {
  2235. mapEngine.handleH5ExperienceFallback(payload)
  2236. }
  2237. })
  2238. eventChannel.on('close', () => {
  2239. if (mapEngine) {
  2240. mapEngine.handleH5ExperienceClosed()
  2241. }
  2242. })
  2243. eventChannel.on('submitResult', () => {
  2244. if (mapEngine) {
  2245. mapEngine.handleH5ExperienceClosed()
  2246. }
  2247. })
  2248. eventChannel.emit('init', request)
  2249. },
  2250. fail: () => {
  2251. if (mapEngine) {
  2252. mapEngine.handleH5ExperienceFallback(request.fallback)
  2253. }
  2254. },
  2255. })
  2256. },
  2257. handleCloseContentCard() {
  2258. if (mapEngine) {
  2259. mapEngine.closeContentCard()
  2260. }
  2261. },
  2262. handleClosePunchHint() {
  2263. clearPunchHintDismissTimer()
  2264. this.setData({
  2265. showPunchHintBanner: false,
  2266. })
  2267. },
  2268. handlePunchHintTap() {},
  2269. handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
  2270. this.setData({
  2271. hudPanelIndex: event.detail.current || 0,
  2272. })
  2273. },
  2274. handleCycleSideButtons() {
  2275. const nextMode = getNextSideButtonMode(this.data.sideButtonMode)
  2276. this.setData({
  2277. ...buildSideButtonVisibility(nextMode),
  2278. ...buildSideButtonState({
  2279. sideButtonMode: nextMode,
  2280. showGameInfoPanel: this.data.showGameInfoPanel,
  2281. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  2282. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2283. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2284. skipButtonEnabled: this.data.skipButtonEnabled,
  2285. gameSessionStatus: this.data.gameSessionStatus,
  2286. gpsLockEnabled: this.data.gpsLockEnabled,
  2287. gpsLockAvailable: this.data.gpsLockAvailable,
  2288. }),
  2289. })
  2290. },
  2291. handleToggleGpsLock() {
  2292. if (mapEngine) {
  2293. mapEngine.handleToggleGpsLock()
  2294. }
  2295. },
  2296. handleToggleMapRotateMode() {
  2297. if (!mapEngine || this.data.lockAutoRotate) {
  2298. return
  2299. }
  2300. if (this.data.orientationMode === 'heading-up') {
  2301. mapEngine.handleSetManualMode()
  2302. persistStoredUserSettings({
  2303. ...loadStoredUserSettings(),
  2304. autoRotateEnabled: false,
  2305. })
  2306. return
  2307. }
  2308. mapEngine.handleSetHeadingUpMode()
  2309. persistStoredUserSettings({
  2310. ...loadStoredUserSettings(),
  2311. autoRotateEnabled: true,
  2312. })
  2313. },
  2314. handleToggleDebugPanel() {
  2315. const nextShowDebugPanel = !this.data.showDebugPanel
  2316. if (!nextShowDebugPanel) {
  2317. clearGameInfoPanelSyncTimer()
  2318. }
  2319. if (mapEngine) {
  2320. mapEngine.setDiagnosticUiEnabled(nextShowDebugPanel)
  2321. }
  2322. this.setData({
  2323. showDebugPanel: nextShowDebugPanel,
  2324. showGameInfoPanel: false,
  2325. showSystemSettingsPanel: false,
  2326. ...buildSideButtonState({
  2327. sideButtonMode: this.data.sideButtonMode,
  2328. showGameInfoPanel: false,
  2329. showSystemSettingsPanel: false,
  2330. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2331. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2332. skipButtonEnabled: this.data.skipButtonEnabled,
  2333. gameSessionStatus: this.data.gameSessionStatus,
  2334. gpsLockEnabled: this.data.gpsLockEnabled,
  2335. gpsLockAvailable: this.data.gpsLockAvailable,
  2336. }),
  2337. })
  2338. },
  2339. handleCloseDebugPanel() {
  2340. if (mapEngine) {
  2341. mapEngine.setDiagnosticUiEnabled(false)
  2342. }
  2343. this.setData({
  2344. showDebugPanel: false,
  2345. ...buildSideButtonState({
  2346. sideButtonMode: this.data.sideButtonMode,
  2347. showGameInfoPanel: this.data.showGameInfoPanel,
  2348. showSystemSettingsPanel: this.data.showSystemSettingsPanel,
  2349. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2350. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2351. skipButtonEnabled: this.data.skipButtonEnabled,
  2352. gameSessionStatus: this.data.gameSessionStatus,
  2353. gpsLockEnabled: this.data.gpsLockEnabled,
  2354. gpsLockAvailable: this.data.gpsLockAvailable,
  2355. }),
  2356. })
  2357. },
  2358. applyCenterScaleRulerSettings(nextEnabled: boolean, nextAnchorMode: CenterScaleRulerAnchorMode) {
  2359. this.data.showCenterScaleRuler = nextEnabled
  2360. this.data.centerScaleRulerAnchorMode = nextAnchorMode
  2361. clearCenterScaleRulerSyncTimer()
  2362. clearCenterScaleRulerUpdateTimer()
  2363. const syncRulerFromEngine = () => {
  2364. if (!mapEngine) {
  2365. return
  2366. }
  2367. const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
  2368. updateCenterScaleRulerInputCache(engineSnapshot)
  2369. const mergedData = {
  2370. ...centerScaleRulerInputCache,
  2371. ...this.data,
  2372. showCenterScaleRuler: nextEnabled,
  2373. centerScaleRulerAnchorMode: nextAnchorMode,
  2374. } as MapPageData
  2375. this.setData({
  2376. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
  2377. showCenterScaleRuler: nextEnabled,
  2378. centerScaleRulerAnchorMode: nextAnchorMode,
  2379. ...buildCenterScaleRulerPatch(mergedData),
  2380. ...buildSideButtonState(mergedData),
  2381. })
  2382. }
  2383. if (!nextEnabled) {
  2384. syncRulerFromEngine()
  2385. return
  2386. }
  2387. this.setData({
  2388. showCenterScaleRuler: true,
  2389. centerScaleRulerAnchorMode: nextAnchorMode,
  2390. ...buildSideButtonState({
  2391. ...this.data,
  2392. showCenterScaleRuler: true,
  2393. centerScaleRulerAnchorMode: nextAnchorMode,
  2394. } as MapPageData),
  2395. })
  2396. this.measureStageAndCanvas(() => {
  2397. syncRulerFromEngine()
  2398. })
  2399. centerScaleRulerSyncTimer = setTimeout(() => {
  2400. centerScaleRulerSyncTimer = 0
  2401. if (!this.data.showCenterScaleRuler) {
  2402. return
  2403. }
  2404. syncRulerFromEngine()
  2405. }, 96) as unknown as number
  2406. },
  2407. handleSetCenterScaleRulerVisibleOn() {
  2408. if (this.data.lockScaleRulerVisible) {
  2409. return
  2410. }
  2411. this.applyCenterScaleRulerSettings(true, this.data.centerScaleRulerAnchorMode)
  2412. persistStoredUserSettings({
  2413. ...loadStoredUserSettings(),
  2414. showCenterScaleRuler: true,
  2415. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2416. })
  2417. },
  2418. handleSetCenterScaleRulerVisibleOff() {
  2419. if (this.data.lockScaleRulerVisible) {
  2420. return
  2421. }
  2422. this.applyCenterScaleRulerSettings(false, this.data.centerScaleRulerAnchorMode)
  2423. persistStoredUserSettings({
  2424. ...loadStoredUserSettings(),
  2425. showCenterScaleRuler: false,
  2426. centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
  2427. })
  2428. },
  2429. handleSetCenterScaleRulerAnchorScreenCenter() {
  2430. if (this.data.lockScaleRulerAnchor) {
  2431. return
  2432. }
  2433. this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'screen-center')
  2434. persistStoredUserSettings({
  2435. ...loadStoredUserSettings(),
  2436. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2437. centerScaleRulerAnchorMode: 'screen-center',
  2438. })
  2439. },
  2440. handleSetCenterScaleRulerAnchorCompassCenter() {
  2441. if (this.data.lockScaleRulerAnchor) {
  2442. return
  2443. }
  2444. this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'compass-center')
  2445. persistStoredUserSettings({
  2446. ...loadStoredUserSettings(),
  2447. showCenterScaleRuler: this.data.showCenterScaleRuler,
  2448. centerScaleRulerAnchorMode: 'compass-center',
  2449. })
  2450. },
  2451. handleToggleCenterScaleRulerAnchor() {
  2452. if (!this.data.showCenterScaleRuler) {
  2453. return
  2454. }
  2455. const nextAnchorMode: CenterScaleRulerAnchorMode = this.data.centerScaleRulerAnchorMode === 'screen-center'
  2456. ? 'compass-center'
  2457. : 'screen-center'
  2458. const engineSnapshot = mapEngine ? (mapEngine.getInitialData() as Partial<MapPageData>) : {}
  2459. updateCenterScaleRulerInputCache(engineSnapshot)
  2460. this.data.centerScaleRulerAnchorMode = nextAnchorMode
  2461. const mergedData = {
  2462. ...centerScaleRulerInputCache,
  2463. ...this.data,
  2464. centerScaleRulerAnchorMode: nextAnchorMode,
  2465. } as MapPageData
  2466. this.setData({
  2467. ...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, true),
  2468. centerScaleRulerAnchorMode: nextAnchorMode,
  2469. ...buildCenterScaleRulerPatch(mergedData),
  2470. ...buildSideButtonState(mergedData),
  2471. })
  2472. if (this.data.showGameInfoPanel) {
  2473. this.syncGameInfoPanelSnapshot()
  2474. }
  2475. },
  2476. handleDebugPanelTap() {},
  2477. })