map.ts 13 KB

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