map.ts 43 KB

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