You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

597 lines
19 KiB

5 months ago
5 months ago
3 months ago
5 months ago
3 months ago
2 months ago
5 months ago
3 months ago
3 months ago
5 months ago
5 months ago
4 months ago
4 months ago
4 months ago
3 months ago
4 months ago
3 months ago
3 months ago
3 months ago
3 months ago
4 months ago
3 months ago
3 months ago
3 months ago
2 months ago
4 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
5 months ago
3 months ago
3 months ago
4 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
5 months ago
  1. import StepItem from '../components/StepItem';
  2. import { useNavigate } from 'react-router-dom';
  3. import CustomNavBar from '../components/CustomNavBar';
  4. import MeasurementCanvas, {
  5. BenchmarkShape,
  6. MeasurementCanvasRef,
  7. Point,
  8. } from '../components/konva/MeasurementCanvas';
  9. import { useEffect, useRef, useState } from 'react';
  10. import RailTypeBtn from '../components/RailTypeBtn';
  11. import { Cascader, Dialog, Mask, Picker, SpinLoading, Toast } from 'antd-mobile';
  12. import { useAppDispatch, useAppSelector } from '../utils/hooks';
  13. import { updateMeasureData, updateTaskState } from '../store/features/measureSlice';
  14. import Bridge from '../utils/bridge';
  15. import { selectLabeledKtjOrgs, updateRailPoints } from '../store/features/baseData';
  16. import { updateOrg } from '../store/features/contextSlice';
  17. import { selectOrgTextArr } from '../store';
  18. import icon_left from '../assets/icon_left.svg';
  19. import icon_right from '../assets/icon_right.svg';
  20. import icon_up from '../assets/icon_up.svg';
  21. import icon_down from '../assets/icon_down.svg';
  22. import icon_leftR from '../assets/icon_leftR.svg';
  23. import icon_rightR from '../assets/icon_rightR.svg';
  24. export default function Measure() {
  25. const navigate = useNavigate();
  26. const dispatch = useAppDispatch();
  27. const labeledKtjOrgs = useAppSelector(selectLabeledKtjOrgs);
  28. const orgTextArr = useAppSelector(selectOrgTextArr);
  29. const measureState = useAppSelector((state) => state.measure);
  30. const contextState = useAppSelector((state) => state.context);
  31. const baseState = useAppSelector((state) => state.baseData);
  32. const [railPickerVisible, setRailPickerVisible] = useState(false);
  33. const [railId, setRailId] = useState<(number | string | null)[]>([]);
  34. const canvasRef = useRef<MeasurementCanvasRef>(null);
  35. const [railSize, setRailSize] = useState<number | string | null>();
  36. const iconWidth = 35;
  37. const [showStandard, setShowStandard] = useState(false);
  38. // 默认选中第一个轨型
  39. useEffect(() => {
  40. if (baseState.railTypes.length > 0) {
  41. let railData = baseState.railTypes[0];
  42. setRailId([railData.id]);
  43. setRailSize(railData.code);
  44. }
  45. }, [baseState.railTypes]);
  46. function drawRailBaseLine(points: string) {
  47. const benchmarkShapes = JSON.parse(points) as BenchmarkShape[];
  48. if (canvasRef.current) {
  49. canvasRef.current.setBenchmarkData(benchmarkShapes);
  50. }
  51. }
  52. // 检查轨型有没有坐标,如果有,绘制轨型基准线,如果没,拉取再绘制其线
  53. useEffect(() => {
  54. if (railId.length > 0) {
  55. const r = baseState.railTypes.find((rail) => rail.id === railId[0]);
  56. if (!r) return;
  57. if (!!r.points) {
  58. drawRailBaseLine(r.points);
  59. return;
  60. }
  61. Bridge.getTrackPoint({ code: r.code }).then((res) => {
  62. if (res.success) {
  63. dispatch(updateRailPoints(res.data));
  64. drawRailBaseLine(res.data.points!);
  65. } else {
  66. Toast.show(res.message);
  67. }
  68. });
  69. }
  70. }, [baseState.railTypes, dispatch, railId]);
  71. // 绘制测量坐标线
  72. useEffect(() => {
  73. if (canvasRef.current) {
  74. canvasRef.current.setMeasurementDataLeft(measureState.leftPoints);
  75. }
  76. }, [measureState.leftPoints]);
  77. useEffect(() => {
  78. if (canvasRef.current) {
  79. canvasRef.current.setMeasurementDataRight(measureState.rightPoints);
  80. }
  81. }, [measureState.rightPoints]);
  82. // 左右两测量完成,转换后的测量线(两线合一线)
  83. useEffect(() => {
  84. if (canvasRef.current && measureState.measureFinishData.length) {
  85. canvasRef.current?.setMeasurementCalibrationData(measureState.measureFinishData);
  86. // setshowCalibration(true);
  87. setShowMeasureFinish(true)
  88. setCaloading(false)
  89. }
  90. }, [measureState.measureFinishData])
  91. useEffect(() => {
  92. if(measureState.measureStatus === 'FINISH_RECORD'){
  93. setCaloading(true)
  94. }
  95. else {
  96. setCaloading(false)
  97. }
  98. }, [measureState.measureStatus])
  99. const onSaveClick = () => {
  100. dispatch(updateMeasureData(newMeasureData));
  101. navigate('/measure/save');
  102. };
  103. const [caloading, setCaloading] = useState(false);
  104. const [showCalibration, setshowCalibration] = useState(false);
  105. const [showMeasureFinish, setShowMeasureFinish] = useState(false);
  106. const onCalibrationBtnClick = () => {
  107. setCaloading(true);
  108. Bridge.alignPoints({ railSize: railSize || 'GX-60' })
  109. .then((res) => {
  110. if (res.success) {
  111. setshowCalibration(true);
  112. canvasRef.current?.setMeasurementCalibrationData(res.data);
  113. } else {
  114. }
  115. setCaloading(false);
  116. })
  117. .catch((e) => {
  118. setCaloading(false);
  119. Toast.show({
  120. content: <span></span>,
  121. position: 'top',
  122. });
  123. });
  124. };
  125. const [initStart, setInitStart] = useState(false);
  126. const onStartClick = () => {
  127. setshowCalibration(false);
  128. setShowMeasureFinish(false)
  129. dispatch(updateMeasureData([]));
  130. if (!contextState.device.connected) {
  131. Dialog.alert({
  132. content: '蓝牙未连接,请先连接蓝牙',
  133. onConfirm: () => {
  134. navigate('/home/bluetooth');
  135. },
  136. });
  137. return;
  138. }
  139. if (baseState.ktjOrgs.length === 0) {
  140. Dialog.alert({
  141. content: '请在基础数据同步完成后重试',
  142. onConfirm: () => {
  143. navigate('/home/mine');
  144. },
  145. });
  146. return;
  147. }
  148. if (!contextState.currOrgCode) {
  149. Dialog.alert({
  150. content: '请选择铁路局/工务段/线路',
  151. onConfirm: () => {
  152. onOrgBarClick();
  153. },
  154. });
  155. return;
  156. }
  157. // if (contextState.device.power < 20) {
  158. // Toast.show("电量低于20%,请充电后测量");
  159. // return;
  160. // }
  161. Bridge.startMeasure().then((res) => {
  162. if (res.success) {
  163. dispatch(updateTaskState('START_RECORD_SIG'));
  164. } else {
  165. Toast.show(res.message);
  166. }
  167. setInitStart(true);
  168. setShowStandard(false)
  169. });
  170. };
  171. const onOrgBarClick = async () => {
  172. if (baseState.ktjOrgs.length === 0) {
  173. Dialog.alert({
  174. content: '请在基础数据同步完成后重试',
  175. onConfirm: () => {
  176. navigate('/home/mine');
  177. },
  178. });
  179. return;
  180. }
  181. const value = await Cascader.prompt({
  182. options: labeledKtjOrgs,
  183. placeholder: '请选择',
  184. });
  185. // Toast.show(value ? `你选择了 ${value.join(' - ')}` : '你没有进行选择');
  186. if (value) {
  187. dispatch(updateOrg(value as string[]));
  188. }
  189. };
  190. const [state, setState] = useState({
  191. left_ready: 'none',
  192. right_ready: 'none',
  193. left_begin: 'none',
  194. right_begin: 'none',
  195. left_end: 'none',
  196. right_end: 'none',
  197. });
  198. const [status, setStatus] = useState(0)
  199. useEffect(() => {
  200. console.log(measureState.taskState)
  201. switch (measureState.taskState) {
  202. case 'WAITING_FOR_RECORD_THE_1ST_SIDE':
  203. setStatus(1);
  204. setState({
  205. left_ready: 'ongoing',
  206. right_ready: 'none',
  207. left_begin: 'none',
  208. right_begin: 'none',
  209. left_end: 'none',
  210. right_end: 'none',
  211. });
  212. break;
  213. case 'WAITING_FOR_RECORD_THE_2ND_SIDE':
  214. setStatus(2)
  215. break;
  216. case 'START_RECORD_SIG':
  217. setState({
  218. left_ready: 'ongoing',
  219. right_ready: 'none',
  220. left_begin: 'none',
  221. right_begin: 'none',
  222. left_end: 'none',
  223. right_end: 'none',
  224. });
  225. break;
  226. case 'START_RECORD_LEFT':
  227. case 'START_RECORD_RIGHT':
  228. if (status === 1) {
  229. setState({
  230. left_ready: 'done',
  231. right_ready: 'none',
  232. left_begin: 'ongoing',
  233. right_begin: 'none',
  234. left_end: 'none',
  235. right_end: 'none',
  236. });
  237. } else if (status === 2) {
  238. setState({
  239. left_ready: 'done',
  240. right_ready: 'done',
  241. left_begin: 'done',
  242. right_begin: 'ongoing',
  243. left_end: 'done',
  244. right_end: 'none',
  245. });
  246. }
  247. break;
  248. case 'FINISH_RECORD_LEFT':
  249. case 'FINISH_RECORD_RIGHT':
  250. if (status === 1) {
  251. setState({
  252. left_ready: 'done',
  253. right_ready: 'none',
  254. left_begin: 'done',
  255. right_begin: 'none',
  256. left_end: 'done',
  257. right_end: 'none',
  258. });
  259. } else if (status === 2) {
  260. setState({
  261. left_ready: 'done',
  262. right_ready: 'done',
  263. left_begin: 'done',
  264. right_begin: 'done',
  265. left_end: 'done',
  266. right_end: 'done',
  267. });
  268. }
  269. break;
  270. case 'FINISH_RECORD':
  271. setState({
  272. left_ready: 'done',
  273. right_ready: 'done',
  274. left_begin: 'done',
  275. right_begin: 'done',
  276. left_end: 'done',
  277. right_end: 'done',
  278. });
  279. // 测量完成,显示基线
  280. setShowStandard(true)
  281. break;
  282. default:
  283. setState({
  284. left_ready: 'none',
  285. right_ready: 'none',
  286. left_begin: 'none',
  287. right_begin: 'none',
  288. left_end: 'none',
  289. right_end: 'none',
  290. });
  291. }
  292. }, [measureState.taskState, status]);
  293. function railName() {
  294. return baseState.railTypes.find((r) => r.id === railId[0])?.name || '';
  295. }
  296. function onRailSizeChange(ids: (number | string | null)[]) {
  297. if (ids && ids.length) {
  298. setRailId(ids);
  299. let id = ids[0];
  300. const codes = baseState.railTypes.map((item) => {
  301. if (item.id === id) {
  302. return item.code;
  303. }
  304. return item.code;
  305. });
  306. if (codes && codes.length) {
  307. setRailSize(codes[0]);
  308. }
  309. }
  310. }
  311. //上下移动
  312. const timerRef = useRef<NodeJS.Timeout | null>(null);
  313. const handlePressStart = (type: string) => {
  314. timerRef.current = setInterval(() => {
  315. console.log('你进行了长按操作!');
  316. onHandleMove(type);
  317. }, 500);
  318. };
  319. const handlePressEnd = () => {
  320. if (timerRef.current) {
  321. clearInterval(timerRef.current);
  322. timerRef.current = null;
  323. }
  324. };
  325. const onMoveLine = (type: string) => {
  326. console.log('这是点击');
  327. onHandleMove(type);
  328. };
  329. const onHandleMove = (type: string) => {
  330. let list = canvasRef.current?.getMeasurementCalibrationData();
  331. if (list && list.length) {
  332. list.forEach((item) => {
  333. if (type === 'up') {
  334. //向上移动,原数据减y X轴不动
  335. item.y = item.y - distance / 1000;
  336. }
  337. if (type === 'down') {
  338. //向上移动,原数据加y X轴不动
  339. item.y = item.y + distance / 1000;
  340. }
  341. if (type === 'left') {
  342. //向左移动,原数据减x Y轴不动
  343. item.x = item.x - distance / 1000;
  344. }
  345. if (type === 'right') {
  346. //向右移动,原数据加x Y轴不动
  347. item.x = item.x + distance / 1000;
  348. }
  349. });
  350. canvasRef.current?.setMeasurementCalibrationData(list);
  351. setNewMeasureData(list);
  352. }
  353. };
  354. const handleRotationPressStart = (type: string) => {
  355. timerRef.current = setInterval(() => {
  356. onRotationLine(type);
  357. }, 500);
  358. };
  359. //旋转
  360. let [measurementRotation] = useState<number>(0);
  361. let [newMeasureData, setNewMeasureData] = useState<Point[]>();
  362. let [angle] = useState<number>(5); //角度单位 分
  363. let [distance] = useState<number>(100);
  364. const onRotationLine = (type: string) => {
  365. let mrValue = 0;
  366. if (type === 'left') {
  367. //逆时针
  368. mrValue = measurementRotation - ((angle / 60) * Math.PI) / 180;
  369. }
  370. if (type === 'right') {
  371. //顺时针
  372. mrValue = measurementRotation + ((angle / 60) * Math.PI) / 180;
  373. }
  374. let list = canvasRef.current?.getMeasurementCalibrationData();
  375. if (list && list.length) {
  376. list.forEach((item, index) => {
  377. let cloneItem = rotatePoint(item, mrValue);
  378. item.x = cloneItem.x;
  379. item.y = cloneItem.y;
  380. });
  381. canvasRef.current?.setMeasurementCalibrationData(list);
  382. setNewMeasureData(list);
  383. }
  384. };
  385. const rotatePoint = (pt: { x: number; y: number }, angle: number) => {
  386. const item = {
  387. x: pt.x * Math.cos(angle) - pt.y * Math.sin(angle),
  388. y: pt.x * Math.sin(angle) + pt.y * Math.cos(angle),
  389. };
  390. return item;
  391. };
  392. const handleContextMenu = (e: any) => {
  393. e.preventDefault();
  394. };
  395. return (
  396. <>
  397. <div className="relative pt-[--navBarHeight]">
  398. <div className="absolute top-0 w-full z-10">
  399. <CustomNavBar title={'测量'}></CustomNavBar>
  400. </div>
  401. <main className="home-page-content overflow-x-hidden overflow-y-auto">
  402. <div className="relative h-0 p-0 pb-[70%]">
  403. {/**正在校准时的loading */}
  404. {caloading && (
  405. <Mask opacity="thin" className="h-[100vh] flex justify-center items-center">
  406. <div style={{ margin: '45%' }}>
  407. <SpinLoading color="#5c92b4" />
  408. <div className="w-[100px] mt-[20px] text-[#5c92b4]">...</div>
  409. </div>
  410. </Mask>
  411. )}
  412. {/**测量区 */}
  413. <div className="absolute left-0 right-0 top-0 bottom-0 bg-title">
  414. <MeasurementCanvas
  415. width={window.innerWidth}
  416. height={window.innerWidth * 0.7}
  417. logicalExtent={{ minX: -45, maxX: 45, minY: -18, maxY: 52 }}
  418. gridStep={3}
  419. origin={{ x: 0, y: 20 }}
  420. pixelPerMm={window.innerWidth / 90}
  421. maxZoom={8}
  422. showGrid={true}
  423. showBenchmark={showStandard}
  424. showAnalysis={false}
  425. showScale={false}
  426. scaleInterval={1}
  427. showCoordinates={false}
  428. showCalibration={showCalibration || showMeasureFinish}
  429. ref={canvasRef}
  430. />
  431. {/**选择轨型区 */}
  432. {railId.length > 0 && (
  433. <div className="absolute left-1 bottom-1">
  434. <RailTypeBtn text={railName()} onClick={() => setRailPickerVisible(true)} />
  435. </div>
  436. )}
  437. </div>
  438. </div>
  439. {/**局段线区 */}
  440. <section
  441. className="h-10 bg-[#e3e8f5] flex justify-between items-center px-4"
  442. onClick={onOrgBarClick}
  443. >
  444. <p className="text-text" style={{ color: contextState.currOrgCode ? '#333' : 'red' }}>
  445. {contextState.currOrgCode ? orgTextArr.join('/') : '点击此处选择铁路局和工务段'}
  446. </p>
  447. <span className="text-primary underline"></span>
  448. </section>
  449. {/**手动校准区 */}
  450. {showCalibration && (
  451. <section className="h-10 bg-[#e3e8f5] flex justify-between items-center px-4">
  452. <img
  453. width={iconWidth}
  454. src={icon_left}
  455. onClick={() => onMoveLine('left')}
  456. onTouchStart={() => handlePressStart('left')}
  457. onTouchEnd={handlePressEnd}
  458. onContextMenu={handleContextMenu}
  459. className="text-[20px] ml-[5px]"
  460. alt="左移"
  461. />
  462. <img
  463. width={iconWidth}
  464. src={icon_right}
  465. onClick={() => onMoveLine('right')}
  466. onTouchStart={() => handlePressStart('right')}
  467. onTouchEnd={handlePressEnd}
  468. onContextMenu={handleContextMenu}
  469. className="text-[20px] ml-[5px]"
  470. alt="右移"
  471. />
  472. <img
  473. width={iconWidth}
  474. src={icon_up}
  475. onClick={() => onMoveLine('up')}
  476. onTouchStart={() => handlePressStart('up')}
  477. onTouchEnd={handlePressEnd}
  478. onContextMenu={handleContextMenu}
  479. className="text-[20px] ml-[5px]"
  480. alt="上移"
  481. />
  482. <img
  483. width={iconWidth}
  484. src={icon_down}
  485. onClick={() => onMoveLine('down')}
  486. onTouchStart={() => handlePressStart('down')}
  487. onTouchEnd={handlePressEnd}
  488. onContextMenu={handleContextMenu}
  489. className="text-[20px] ml-[5px]"
  490. alt="下移"
  491. />
  492. <img
  493. width={iconWidth}
  494. src={icon_leftR}
  495. onClick={() => onRotationLine('left')}
  496. onTouchStart={() => handleRotationPressStart('left')}
  497. onTouchEnd={handlePressEnd}
  498. onContextMenu={handleContextMenu}
  499. className="text-[20px] ml-[5px]"
  500. alt="逆时针旋转"
  501. />
  502. <img
  503. width={iconWidth}
  504. src={icon_rightR}
  505. onClick={() => onRotationLine('right')}
  506. onTouchStart={() => handleRotationPressStart('right')}
  507. onTouchEnd={handlePressEnd}
  508. onContextMenu={handleContextMenu}
  509. className="text-[20px] ml-[5px]"
  510. alt="顺时针旋转"
  511. />
  512. </section>
  513. )}
  514. {/**按钮操作区 */}
  515. <section className="flex items-center gap-4 px-4 my-4">
  516. <button className="btn-contained rounded-md text-sm h-10 flex-1" onClick={onStartClick}>
  517. {!initStart ||
  518. (measureState.leftPoints.length > 0 && measureState.rightPoints.length > 0)
  519. ? '开始测量'
  520. : '重新测量'}
  521. </button>
  522. <button
  523. className="btn-contained rounded-md text-sm h-10 flex-1"
  524. disabled={measureState.taskState !== 'FINISH_RECORD'}
  525. onClick={onSaveClick}
  526. >
  527. </button>
  528. <button
  529. className="btn-contained rounded-md text-sm h-10 flex-1"
  530. disabled={measureState.taskState !== 'FINISH_RECORD'}
  531. onClick={onCalibrationBtnClick}
  532. >
  533. </button>
  534. </section>
  535. {/**测量状态区 */}
  536. <section className="grid grid-cols-2 gap-[10px]">
  537. <StepItem state={state.left_ready} text={'等待测量'} />
  538. <StepItem state={state.right_ready} text={'等待测量另一侧'} />
  539. <StepItem state={state.left_begin} text={'正在进行测量'} />
  540. <StepItem state={state.right_begin} text={'正在进行测量'} />
  541. <StepItem state={state.left_end} text={'一侧测量完成'} />
  542. <StepItem state={state.right_end} text={'测量已完成'} />
  543. </section>
  544. </main>
  545. </div>
  546. <Picker
  547. columns={[baseState.railTypes.map((t) => ({ label: t.name, value: t.id }))]}
  548. visible={railPickerVisible}
  549. onClose={() => {
  550. setRailPickerVisible(false);
  551. }}
  552. value={railId}
  553. onConfirm={(v) => {
  554. onRailSizeChange(v);
  555. }}
  556. />
  557. </>
  558. );
  559. }