map.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. import { MapEngine, type MapEngineStageRect, type MapEngineViewState } from '../../engine/map/mapEngine'
  2. import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
  3. type CompassTickData = {
  4. angle: number
  5. long: boolean
  6. major: boolean
  7. }
  8. type CompassLabelData = {
  9. text: string
  10. angle: number
  11. rotateBack: number
  12. radius: number
  13. className: string
  14. }
  15. type SideButtonMode = 'all' | 'left' | 'right' | 'hidden'
  16. type MapPageData = MapEngineViewState & {
  17. showDebugPanel: boolean
  18. statusBarHeight: number
  19. topInsetHeight: number
  20. hudPanelIndex: number
  21. mockBridgeUrlDraft: string
  22. panelTimerText: string
  23. panelMileageText: string
  24. panelDistanceValueText: string
  25. panelProgressText: string
  26. panelSpeedValueText: string
  27. compassTicks: CompassTickData[]
  28. compassLabels: CompassLabelData[]
  29. sideButtonMode: SideButtonMode
  30. showLeftButtonGroup: boolean
  31. showRightButtonGroups: boolean
  32. showBottomDebugButton: boolean
  33. }
  34. const INTERNAL_BUILD_VERSION = 'map-build-195'
  35. const REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/wxmini/test/game.json'
  36. let mapEngine: MapEngine | null = null
  37. let stageCanvasAttached = false
  38. function buildSideButtonVisibility(mode: SideButtonMode) {
  39. return {
  40. sideButtonMode: mode,
  41. showLeftButtonGroup: mode === 'all' || mode === 'left' || mode === 'right',
  42. showRightButtonGroups: mode === 'all' || mode === 'right',
  43. showBottomDebugButton: mode !== 'hidden',
  44. }
  45. }
  46. function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
  47. if (currentMode === 'all') {
  48. return 'left'
  49. }
  50. if (currentMode === 'left') {
  51. return 'right'
  52. }
  53. if (currentMode === 'right') {
  54. return 'hidden'
  55. }
  56. return 'left'
  57. }
  58. function buildCompassTicks(): CompassTickData[] {
  59. const ticks: CompassTickData[] = []
  60. for (let angle = 0; angle < 360; angle += 5) {
  61. ticks.push({
  62. angle,
  63. long: angle % 15 === 0,
  64. major: angle % 45 === 0,
  65. })
  66. }
  67. return ticks
  68. }
  69. function buildCompassLabels(): CompassLabelData[] {
  70. return [
  71. { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
  72. { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
  73. { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  74. { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  75. { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  76. { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
  77. { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
  78. { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
  79. ]
  80. }
  81. function getFallbackStageRect(): MapEngineStageRect {
  82. const systemInfo = wx.getSystemInfoSync()
  83. const width = Math.max(320, systemInfo.windowWidth)
  84. const height = Math.max(280, systemInfo.windowHeight)
  85. return {
  86. width,
  87. height,
  88. left: 0,
  89. top: 0,
  90. }
  91. }
  92. Page({
  93. data: {
  94. showDebugPanel: false,
  95. statusBarHeight: 0,
  96. topInsetHeight: 12,
  97. hudPanelIndex: 0,
  98. panelTimerText: '00:00:00',
  99. panelMileageText: '0m',
  100. panelActionTagText: '目标',
  101. panelDistanceTagText: '点距',
  102. panelDistanceValueText: '--',
  103. panelDistanceUnitText: '',
  104. panelProgressText: '0/0',
  105. gameSessionStatus: 'idle',
  106. gameModeText: '顺序赛',
  107. locationSourceMode: 'real',
  108. locationSourceText: '真实定位',
  109. mockBridgeConnected: false,
  110. mockBridgeStatusText: '未连接',
  111. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  112. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  113. mockCoordText: '--',
  114. mockSpeedText: '--',
  115. heartRateScanText: '未扫描',
  116. heartRateDiscoveredDevices: [],
  117. panelSpeedValueText: '0',
  118. panelTelemetryTone: 'blue',
  119. panelHeartRateZoneNameText: '--',
  120. panelHeartRateZoneRangeText: '',
  121. heartRateConnected: false,
  122. heartRateStatusText: '心率带未连接',
  123. heartRateDeviceText: '--',
  124. panelHeartRateValueText: '--',
  125. panelHeartRateUnitText: '',
  126. panelCaloriesValueText: '0',
  127. panelCaloriesUnitText: 'kcal',
  128. panelAverageSpeedValueText: '0',
  129. panelAverageSpeedUnitText: 'km/h',
  130. panelAccuracyValueText: '--',
  131. panelAccuracyUnitText: '',
  132. punchButtonText: '打点',
  133. punchButtonEnabled: false,
  134. punchHintText: '等待进入检查点范围',
  135. punchFeedbackVisible: false,
  136. punchFeedbackText: '',
  137. punchFeedbackTone: 'neutral',
  138. contentCardVisible: false,
  139. contentCardTitle: '',
  140. contentCardBody: '',
  141. punchButtonFxClass: '',
  142. punchFeedbackFxClass: '',
  143. contentCardFxClass: '',
  144. mapPulseVisible: false,
  145. mapPulseLeftPx: 0,
  146. mapPulseTopPx: 0,
  147. mapPulseFxClass: '',
  148. stageFxVisible: false,
  149. stageFxClass: '',
  150. compassTicks: buildCompassTicks(),
  151. compassLabels: buildCompassLabels(),
  152. ...buildSideButtonVisibility('left'),
  153. } as unknown as MapPageData,
  154. onLoad() {
  155. const systemInfo = wx.getSystemInfoSync()
  156. const statusBarHeight = systemInfo.statusBarHeight || 0
  157. const menuButtonRect = wx.getMenuButtonBoundingClientRect()
  158. const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
  159. mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
  160. onData: (patch) => {
  161. const nextPatch = patch as Partial<MapPageData>
  162. if (
  163. typeof nextPatch.mockBridgeUrlText === 'string'
  164. && this.data.mockBridgeUrlDraft === this.data.mockBridgeUrlText
  165. ) {
  166. this.setData({
  167. ...nextPatch,
  168. mockBridgeUrlDraft: nextPatch.mockBridgeUrlText,
  169. })
  170. return
  171. }
  172. this.setData(nextPatch)
  173. },
  174. })
  175. this.setData({
  176. ...mapEngine.getInitialData(),
  177. showDebugPanel: false,
  178. statusBarHeight,
  179. topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
  180. hudPanelIndex: 0,
  181. panelTimerText: '00:00:00',
  182. panelMileageText: '0m',
  183. panelActionTagText: '目标',
  184. panelDistanceTagText: '点距',
  185. panelDistanceValueText: '--',
  186. panelDistanceUnitText: '',
  187. panelProgressText: '0/0',
  188. gameSessionStatus: 'idle',
  189. gameModeText: '顺序赛',
  190. locationSourceMode: 'real',
  191. locationSourceText: '真实定位',
  192. mockBridgeConnected: false,
  193. mockBridgeStatusText: '未连接',
  194. mockBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
  195. mockBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
  196. mockCoordText: '--',
  197. mockSpeedText: '--',
  198. panelSpeedValueText: '0',
  199. panelTelemetryTone: 'blue',
  200. panelHeartRateZoneNameText: '--',
  201. panelHeartRateZoneRangeText: '',
  202. heartRateConnected: false,
  203. heartRateStatusText: '心率带未连接',
  204. heartRateDeviceText: '--',
  205. panelHeartRateValueText: '--',
  206. panelHeartRateUnitText: '',
  207. panelCaloriesValueText: '0',
  208. panelCaloriesUnitText: 'kcal',
  209. panelAverageSpeedValueText: '0',
  210. panelAverageSpeedUnitText: 'km/h',
  211. panelAccuracyValueText: '--',
  212. panelAccuracyUnitText: '',
  213. punchButtonText: '打点',
  214. punchButtonEnabled: false,
  215. punchHintText: '等待进入检查点范围',
  216. punchFeedbackVisible: false,
  217. punchFeedbackText: '',
  218. punchFeedbackTone: 'neutral',
  219. contentCardVisible: false,
  220. contentCardTitle: '',
  221. contentCardBody: '',
  222. punchButtonFxClass: '',
  223. punchFeedbackFxClass: '',
  224. contentCardFxClass: '',
  225. mapPulseVisible: false,
  226. mapPulseLeftPx: 0,
  227. mapPulseTopPx: 0,
  228. mapPulseFxClass: '',
  229. stageFxVisible: false,
  230. stageFxClass: '',
  231. compassTicks: buildCompassTicks(),
  232. compassLabels: buildCompassLabels(),
  233. ...buildSideButtonVisibility('left'),
  234. })
  235. },
  236. onReady() {
  237. stageCanvasAttached = false
  238. this.measureStageAndCanvas()
  239. this.loadMapConfigFromRemote()
  240. },
  241. onShow() {
  242. if (mapEngine) {
  243. mapEngine.handleAppShow()
  244. }
  245. },
  246. onHide() {
  247. if (mapEngine) {
  248. mapEngine.handleAppHide()
  249. }
  250. },
  251. onUnload() {
  252. if (mapEngine) {
  253. mapEngine.destroy()
  254. mapEngine = null
  255. }
  256. stageCanvasAttached = false
  257. },
  258. loadMapConfigFromRemote() {
  259. const currentEngine = mapEngine
  260. if (!currentEngine) {
  261. return
  262. }
  263. loadRemoteMapConfig(REMOTE_GAME_CONFIG_URL)
  264. .then((config) => {
  265. if (mapEngine !== currentEngine) {
  266. return
  267. }
  268. currentEngine.applyRemoteMapConfig(config)
  269. })
  270. .catch((error) => {
  271. if (mapEngine !== currentEngine) {
  272. return
  273. }
  274. const errorMessage = error && error.message ? error.message : '未知错误'
  275. this.setData({
  276. configStatusText: `载入失败: ${errorMessage}`,
  277. statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
  278. })
  279. })
  280. },
  281. measureStageAndCanvas() {
  282. const page = this
  283. const applyStage = (rawRect?: Partial<WechatMiniprogram.BoundingClientRectCallbackResult>) => {
  284. const fallbackRect = getFallbackStageRect()
  285. const rect: MapEngineStageRect = {
  286. width: rawRect && typeof rawRect.width === 'number' ? rawRect.width : fallbackRect.width,
  287. height: rawRect && typeof rawRect.height === 'number' ? rawRect.height : fallbackRect.height,
  288. left: rawRect && typeof rawRect.left === 'number' ? rawRect.left : fallbackRect.left,
  289. top: rawRect && typeof rawRect.top === 'number' ? rawRect.top : fallbackRect.top,
  290. }
  291. const currentEngine = mapEngine
  292. if (!currentEngine) {
  293. return
  294. }
  295. currentEngine.setStage(rect)
  296. if (stageCanvasAttached) {
  297. return
  298. }
  299. const canvasQuery = wx.createSelectorQuery().in(page)
  300. canvasQuery.select('#mapCanvas').fields({ node: true, size: true })
  301. canvasQuery.select('#routeLabelCanvas').fields({ node: true, size: true })
  302. canvasQuery.exec((canvasRes) => {
  303. const canvasRef = canvasRes[0] as any
  304. const labelCanvasRef = canvasRes[1] as any
  305. if (!canvasRef || !canvasRef.node) {
  306. page.setData({
  307. statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
  308. })
  309. return
  310. }
  311. const dpr = wx.getSystemInfoSync().pixelRatio || 1
  312. try {
  313. currentEngine.attachCanvas(
  314. canvasRef.node,
  315. rect.width,
  316. rect.height,
  317. dpr,
  318. labelCanvasRef && labelCanvasRef.node ? labelCanvasRef.node : undefined,
  319. )
  320. stageCanvasAttached = true
  321. } catch (error) {
  322. page.setData({
  323. statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
  324. })
  325. }
  326. })
  327. }
  328. const query = wx.createSelectorQuery().in(page)
  329. query.select('.map-stage').boundingClientRect()
  330. query.exec((res) => {
  331. const rect = res[0] as WechatMiniprogram.BoundingClientRectCallbackResult | undefined
  332. applyStage(rect)
  333. })
  334. },
  335. handleTouchStart(event: WechatMiniprogram.TouchEvent) {
  336. if (mapEngine) {
  337. mapEngine.handleTouchStart(event)
  338. }
  339. },
  340. handleTouchMove(event: WechatMiniprogram.TouchEvent) {
  341. if (mapEngine) {
  342. mapEngine.handleTouchMove(event)
  343. }
  344. },
  345. handleTouchEnd(event: WechatMiniprogram.TouchEvent) {
  346. if (mapEngine) {
  347. mapEngine.handleTouchEnd(event)
  348. }
  349. },
  350. handleTouchCancel() {
  351. if (mapEngine) {
  352. mapEngine.handleTouchCancel()
  353. }
  354. },
  355. handleRecenter() {
  356. if (mapEngine) {
  357. mapEngine.handleRecenter()
  358. }
  359. },
  360. handleRotateStep() {
  361. if (mapEngine) {
  362. mapEngine.handleRotateStep()
  363. }
  364. },
  365. handleRotationReset() {
  366. if (mapEngine) {
  367. mapEngine.handleRotationReset()
  368. }
  369. },
  370. handleSetManualMode() {
  371. if (mapEngine) {
  372. mapEngine.handleSetManualMode()
  373. }
  374. },
  375. handleSetNorthUpMode() {
  376. if (mapEngine) {
  377. mapEngine.handleSetNorthUpMode()
  378. }
  379. },
  380. handleSetHeadingUpMode() {
  381. if (mapEngine) {
  382. mapEngine.handleSetHeadingUpMode()
  383. }
  384. },
  385. handleCycleNorthReferenceMode() {
  386. if (mapEngine) {
  387. mapEngine.handleCycleNorthReferenceMode()
  388. }
  389. },
  390. handleAutoRotateCalibrate() {
  391. if (mapEngine) {
  392. mapEngine.handleAutoRotateCalibrate()
  393. }
  394. },
  395. handleToggleGpsTracking() {
  396. if (mapEngine) {
  397. mapEngine.handleToggleGpsTracking()
  398. }
  399. },
  400. handleSetRealLocationMode() {
  401. if (mapEngine) {
  402. mapEngine.handleSetRealLocationMode()
  403. }
  404. },
  405. handleSetMockLocationMode() {
  406. if (mapEngine) {
  407. mapEngine.handleSetMockLocationMode()
  408. }
  409. },
  410. handleConnectMockLocationBridge() {
  411. if (mapEngine) {
  412. mapEngine.handleConnectMockLocationBridge()
  413. }
  414. },
  415. handleMockBridgeUrlInput(event: WechatMiniprogram.Input) {
  416. this.setData({
  417. mockBridgeUrlDraft: event.detail.value,
  418. })
  419. },
  420. handleSaveMockBridgeUrl() {
  421. if (mapEngine) {
  422. mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
  423. }
  424. },
  425. handleDisconnectMockLocationBridge() {
  426. if (mapEngine) {
  427. mapEngine.handleDisconnectMockLocationBridge()
  428. }
  429. },
  430. handleConnectHeartRate() {
  431. if (mapEngine) {
  432. mapEngine.handleConnectHeartRate()
  433. }
  434. },
  435. handleDisconnectHeartRate() {
  436. if (mapEngine) {
  437. mapEngine.handleDisconnectHeartRate()
  438. }
  439. },
  440. handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
  441. if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
  442. mapEngine.handleConnectHeartRateDevice(event.currentTarget.dataset.deviceId)
  443. }
  444. },
  445. handleClearPreferredHeartRateDevice() {
  446. if (mapEngine) {
  447. mapEngine.handleClearPreferredHeartRateDevice()
  448. }
  449. },
  450. handleDebugHeartRateBlue() {
  451. if (mapEngine) {
  452. mapEngine.handleDebugHeartRateTone('blue')
  453. }
  454. },
  455. handleDebugHeartRatePurple() {
  456. if (mapEngine) {
  457. mapEngine.handleDebugHeartRateTone('purple')
  458. }
  459. },
  460. handleDebugHeartRateGreen() {
  461. if (mapEngine) {
  462. mapEngine.handleDebugHeartRateTone('green')
  463. }
  464. },
  465. handleDebugHeartRateYellow() {
  466. if (mapEngine) {
  467. mapEngine.handleDebugHeartRateTone('yellow')
  468. }
  469. },
  470. handleDebugHeartRateOrange() {
  471. if (mapEngine) {
  472. mapEngine.handleDebugHeartRateTone('orange')
  473. }
  474. },
  475. handleDebugHeartRateRed() {
  476. if (mapEngine) {
  477. mapEngine.handleDebugHeartRateTone('red')
  478. }
  479. },
  480. handleClearDebugHeartRate() {
  481. if (mapEngine) {
  482. mapEngine.handleClearDebugHeartRate()
  483. }
  484. },
  485. handleToggleOsmReference() {
  486. if (mapEngine) {
  487. mapEngine.handleToggleOsmReference()
  488. }
  489. },
  490. handleStartGame() {
  491. if (mapEngine) {
  492. mapEngine.handleStartGame()
  493. }
  494. },
  495. handleSetClassicMode() {
  496. if (mapEngine) {
  497. mapEngine.handleSetGameMode('classic-sequential')
  498. }
  499. },
  500. handleSetScoreOMode() {
  501. if (mapEngine) {
  502. mapEngine.handleSetGameMode('score-o')
  503. }
  504. },
  505. handleClearMapTestArtifacts() {
  506. if (mapEngine) {
  507. mapEngine.handleClearMapTestArtifacts()
  508. }
  509. },
  510. handleOverlayTouch() {},
  511. handlePunchAction() {
  512. if (!this.data.punchButtonEnabled) {
  513. return
  514. }
  515. if (mapEngine) {
  516. mapEngine.handlePunchAction()
  517. }
  518. },
  519. handleCloseContentCard() {
  520. if (mapEngine) {
  521. mapEngine.closeContentCard()
  522. }
  523. },
  524. handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
  525. this.setData({
  526. hudPanelIndex: event.detail.current || 0,
  527. })
  528. },
  529. handleCycleSideButtons() {
  530. this.setData(buildSideButtonVisibility(getNextSideButtonMode(this.data.sideButtonMode)))
  531. },
  532. handleToggleMapRotateMode() {
  533. if (!mapEngine) {
  534. return
  535. }
  536. if (this.data.orientationMode === 'heading-up') {
  537. mapEngine.handleSetManualMode()
  538. return
  539. }
  540. mapEngine.handleSetHeadingUpMode()
  541. },
  542. handleToggleDebugPanel() {
  543. this.setData({
  544. showDebugPanel: !this.data.showDebugPanel,
  545. })
  546. },
  547. handleCloseDebugPanel() {
  548. this.setData({
  549. showDebugPanel: false,
  550. })
  551. },
  552. handleDebugPanelTap() {},
  553. })