map.ts 15 KB

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