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.

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