track_day_detail_controller.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter_map/flutter_map.dart';
  5. import 'package:get/get.dart';
  6. import 'package:get/get_core/src/get_main.dart';
  7. import 'package:location/base/base_controller.dart';
  8. import 'package:location/data/bean/track_daily_bean.dart';
  9. import 'package:location/data/consts/constants.dart';
  10. import 'package:location/data/repositories/account_repository.dart'
  11. show AccountRepository;
  12. import 'package:location/data/repositories/track_repository.dart';
  13. import 'package:location/dialog/loading_dialog.dart';
  14. import 'package:location/handler/error_handler.dart';
  15. import 'package:location/module/analyse/location_analyse_page.dart';
  16. import 'package:location/module/track/track_controller.dart';
  17. import 'package:location/module/track/track_day_detail/time_proportion/pie_chat_data.dart';
  18. import 'package:location/module/track/track_util.dart';
  19. import 'package:location/resource/string.gen.dart';
  20. import 'package:location/utils/async_util.dart';
  21. import 'package:location/utils/common_expand.dart';
  22. import 'package:location/utils/pair.dart';
  23. import 'package:location/utils/toast_util.dart';
  24. import 'package:url_launcher/url_launcher.dart';
  25. import '../../../data/api/response/track_daily_summary_response.dart';
  26. import '../../../data/bean/atmob_track_point.dart';
  27. import '../../../data/bean/stream_chat_origin_data.dart';
  28. import '../../../data/bean/track_days.dart';
  29. import '../../../utils/http_handler.dart';
  30. import '../../../widget/gradually_print_text.dart';
  31. import '../track_status.dart';
  32. class TrackDayDetailController extends BaseController {
  33. final TrackDays days;
  34. late TrackRepository trackRepository;
  35. late final AccountRepository accountRepository;
  36. final RxBool _trackNoData = RxBool(false);
  37. final RxList<TrackDailyBean> trackDailyList = RxList<TrackDailyBean>();
  38. final RxBool _isExpanded = RxBool(false);
  39. final RxBool _isRequested = RxBool(false);
  40. final RxBool _isHideExpand = RxBool(false);
  41. bool get isHideExpand => _isHideExpand.value;
  42. bool get isRequested => _isRequested.value;
  43. bool get isExpanded => _isExpanded.value;
  44. bool get trackNoData => _trackNoData.value;
  45. final Rxn<Pair<TrackExpandType, TrackDailyBean>> _expandSituation = Rxn();
  46. Pair<TrackExpandType, TrackDailyBean>? get expandSituation =>
  47. _expandSituation.value;
  48. final RxList<PieChatData> pieChatData = RxList<PieChatData>();
  49. int indexPieChatColor = 0;
  50. final TrackController trackController = Get.find<TrackController>();
  51. final RxBool _isShowGradually = RxBool(false);
  52. bool get isShowGradually => _isShowGradually.value;
  53. final Rxn<TrackDailySummaryResponse> _trackDailySummary = Rxn();
  54. TrackDailySummaryResponse? get trackDailySummary => _trackDailySummary.value;
  55. final GraduallyController graduallyController = GraduallyController();
  56. StreamSubscription? _streamChatSubscription;
  57. StreamSubscription? _currentTrackDaySubscription;
  58. final RxnString _summaryError = RxnString();
  59. RxDouble get _trackBottomHeight => trackController.trackBottomHeight;
  60. double get trackBottomHeight => _trackBottomHeight.value;
  61. CancelableFuture? requestTrackFuture;
  62. String? get summaryError => _summaryError.value;
  63. CancelableFuture? summaryFuture;
  64. //轨迹相关
  65. List<AtmobTrackPoint>? trackOriginPoints; //原始轨迹点
  66. List<LatLng> points = []; //根据停留情况合并的轨迹点
  67. List<Marker> markers = []; // 停留时间较长的标记点
  68. TrackDayDetailController(this.days, bool isExpand) {
  69. trackRepository = TrackRepository.getInstance();
  70. accountRepository = AccountRepository.getInstance();
  71. _isExpanded.value = isExpand;
  72. }
  73. @override
  74. void onReady() {
  75. _currentTrackDaySubscription =
  76. trackController.currentTrackDay.listen((dat) {
  77. if (dat == days) {
  78. _requestTrackData();
  79. }
  80. });
  81. _requestTrackDailySummary();
  82. _recordNumberTrajectoryViewed();
  83. }
  84. ///记录查看轨迹的次数
  85. void _recordNumberTrajectoryViewed() {
  86. if (accountRepository.memberStatusInfo.value?.trialed == true &&
  87. accountRepository.memberStatusInfo.value?.level == 20) {
  88. trackRepository.refreshMemberTrailTrack();
  89. }
  90. }
  91. _requestTrackData() {
  92. if (isRequested) {
  93. trackController.showMapTrack(points, markers);
  94. return;
  95. }
  96. requestTrackFuture?.cancel();
  97. CustomLoadingDialog.show(loadingTxt: StringName.trackLoadingTxt);
  98. requestTrackFuture = AsyncUtil.waitForAll(
  99. [_requestTrackHistoryPoints(), _requestTrackDaily()]);
  100. requestTrackFuture!.then((_) {
  101. //组装地图新的数据
  102. points.clear();
  103. markers.clear();
  104. for (int i = 0; i < trackDailyList.length; i++) {
  105. final bean = trackDailyList[i];
  106. double? markerLatitude;
  107. double? markerLongitude;
  108. if (bean.status == TrackStatus.stay) {
  109. //停留
  110. markerLatitude = bean.lat;
  111. markerLongitude = bean.lng;
  112. points.add(
  113. LatLng(latitude: markerLatitude, longitude: markerLongitude));
  114. } else if (bean.status == TrackStatus.moving) {
  115. //移动
  116. final movePoints = TrackUtil.getTrackMovePoints(
  117. trackOriginPoints, bean.start, bean.end);
  118. points.addAll(movePoints);
  119. continue;
  120. } else if (bean.status == TrackStatus.error) {
  121. continue;
  122. }
  123. MarkerType markerType;
  124. String? markerName;
  125. String? customAvatarUrl;
  126. if (i == 0) {
  127. //起点
  128. markerType = MarkerType.traceStartPoint;
  129. } else if (i == trackDailyList.length - 1) {
  130. //终点
  131. final userInfo = trackController.userInfo;
  132. markerName = userInfo?.getUserNickName();
  133. markerType = userInfo?.isMine == true
  134. ? MarkerType.traceEndMinePoint
  135. : MarkerType.traceEndMinePoint;
  136. customAvatarUrl = userInfo?.avatar;
  137. } else {
  138. markerType = MarkerType.tracePassingPoint;
  139. }
  140. markers.add(Marker(
  141. id: '${bean.start}',
  142. markerName: markerName ?? '',
  143. longitude: markerLongitude,
  144. latitude: markerLatitude,
  145. markerType: markerType,
  146. customAvatarUrl: customAvatarUrl));
  147. }
  148. if (markers.isNotEmpty &&
  149. (markers.last.markerType != MarkerType.traceEndMinePoint ||
  150. markers.last.markerType != MarkerType.traceEndFriendPoint)) {
  151. markers.last.markerType = trackController.userInfo?.isMine == true
  152. ? MarkerType.traceEndMinePoint
  153. : MarkerType.traceEndFriendPoint;
  154. markers.last.markerName =
  155. trackController.userInfo?.getUserNickName() ?? '';
  156. markers.last.customAvatarUrl = trackController.userInfo?.avatar;
  157. }
  158. //如果只有一个点,则添加一个起点和终点相同的点,绘制线需要2点
  159. if (points.length == 1) {
  160. points.add(points.first);
  161. }
  162. trackController.showMapTrack(points, markers);
  163. }).whenComplete(() {
  164. CustomLoadingDialog.hide();
  165. });
  166. }
  167. Future<void> _requestTrackHistoryPoints() {
  168. final userInfo = trackController.userInfo;
  169. trackOriginPoints = null;
  170. return Future.value().then((_) {
  171. if (userInfo?.virtual == true) {
  172. return trackRepository.queryVirtualTrack();
  173. } else {
  174. return trackRepository.queryTrack(
  175. startTime: days.start, endTime: days.end, userId: userInfo?.id);
  176. }
  177. }).then((data) {
  178. trackOriginPoints = data.trackPoints;
  179. });
  180. }
  181. void _requestTrackDailySummary() {
  182. summaryFuture?.cancel();
  183. summaryFuture = AsyncUtil.retry(
  184. () => _requestTrackSummary(), Duration(seconds: 2),
  185. maxRetry: 5);
  186. summaryFuture!.catchError((error) {
  187. ToastUtil.show(StringName.trackDetailDailySummaryGetFail);
  188. });
  189. }
  190. Future<TrackDailySummaryResponse> _requestTrackSummary() {
  191. return trackRepository
  192. .trackDailySummary(
  193. startTime: days.start,
  194. endTime: days.end,
  195. userId: trackController.userInfo?.id)
  196. .then((response) {
  197. _trackDailySummary.value = response;
  198. return response;
  199. });
  200. }
  201. Future<void> _requestTrackDaily() {
  202. _isRequested.value = false;
  203. return trackRepository
  204. .trackDailyQuery(
  205. startTime: days.start,
  206. endTime: days.end,
  207. userId: trackController.userInfo?.id)
  208. .then((list) {
  209. _isRequested.value = true;
  210. _trackNoData.value = (list == null || list.isEmpty == true);
  211. trackDailyList.assignAll(list ?? []);
  212. if (list != null && list.length == 1) {
  213. _isExpanded.value = true;
  214. _isHideExpand.value = true;
  215. }
  216. _dealPieChatData();
  217. _dealTrackExpandData();
  218. }).catchError((error) {
  219. _isRequested.value = false;
  220. ErrorHandler.toastError(error);
  221. });
  222. }
  223. void _dealPieChatData() {
  224. pieChatData.clear();
  225. final list = trackDailyList;
  226. if (list.isEmpty) {
  227. return;
  228. }
  229. indexPieChatColor = 0;
  230. int totalDuration = 0;
  231. final Map<String, PieChatData> addrMap = {};
  232. for (var bean in list) {
  233. if (bean.status == TrackStatus.error) {
  234. continue;
  235. }
  236. final addr = bean.addr;
  237. if (addr == null) {
  238. continue;
  239. }
  240. if (indexPieChatColor >= pieChatColors.length) {
  241. indexPieChatColor = 0;
  242. }
  243. totalDuration += bean.duration;
  244. if (addrMap.containsKey(addr)) {
  245. addrMap[addr]!.duration += bean.duration;
  246. } else {
  247. addrMap[addr] = PieChatData(
  248. address: addr,
  249. duration: bean.duration,
  250. color: pieChatColors[indexPieChatColor++],
  251. );
  252. }
  253. }
  254. for (var data in addrMap.values) {
  255. data.proportion =
  256. (data.duration / totalDuration * 100).toFormattedDouble(1);
  257. }
  258. pieChatData.addAll(addrMap.values.toList());
  259. //如果当数据为空,填入一个默认文案数据
  260. if (pieChatData.isEmpty) {
  261. PieChatData noStayData = PieChatData(
  262. address: StringName.trackNoStayData,
  263. duration: 0,
  264. proportion: 100,
  265. color: pieChatColors[0],
  266. );
  267. pieChatData.add(noStayData);
  268. }
  269. }
  270. void _dealTrackExpandData() {
  271. final list = trackDailyList;
  272. if (list.isNotEmpty && list.length > 1) {
  273. TrackExpandType? expandType;
  274. TrackDailyBean? expandBean;
  275. for (var i = 0; i < list.length; i++) {
  276. final bean = list[i];
  277. if (bean.status == TrackStatus.error) {
  278. expandType = TrackExpandType.error;
  279. expandBean = bean;
  280. break;
  281. } else if (bean.status == TrackStatus.stay) {
  282. if (expandBean == null) {
  283. expandType = TrackExpandType.stay;
  284. expandBean = bean;
  285. }
  286. continue;
  287. }
  288. }
  289. if (expandType != null && expandBean != null) {
  290. if (expandType == TrackExpandType.error &&
  291. list.indexOf(expandBean) == list.length - 1) {
  292. expandType = TrackExpandType.errorNow;
  293. } else if (expandType == TrackExpandType.stay &&
  294. list.indexOf(expandBean) == list.length - 1) {
  295. expandType = TrackExpandType.stayNow;
  296. }
  297. _expandSituation.value = Pair(expandType, expandBean);
  298. }
  299. }
  300. }
  301. void onTrackDetailFoldClick() {
  302. _isExpanded.value = !_isExpanded.value;
  303. }
  304. void checkGraduallyPrintTextVisible(double visibleFraction) {
  305. if (!isShowGradually && visibleFraction >= 0.3) {
  306. _isShowGradually.value = true;
  307. //流式获取轨迹情况
  308. _requestStreamTrackDaily();
  309. }
  310. }
  311. void _requestStreamTrackDaily() {
  312. graduallyController.dispose();
  313. _streamChatSubscription?.cancel();
  314. trackRepository
  315. .streamDailySummary(
  316. startTime: days.start,
  317. endTime: days.end,
  318. userId: trackController.userInfo?.id)
  319. .then((stream) {
  320. //
  321. graduallyController.setGraduallyFinishedListener(() {
  322. _trackDailySummary.value?.trackSituation =
  323. graduallyController.graduallyTxt;
  324. _trackDailySummary.refresh();
  325. });
  326. _streamChatSubscription = stream.listen((event) {
  327. try {
  328. Map<String, dynamic> json = jsonDecode(event.data);
  329. if (json.isEmpty) {
  330. return;
  331. }
  332. StreamChatOriginData data = StreamChatOriginData.fromJson(json);
  333. if (data.choices == null || data.choices!.isEmpty) {
  334. return;
  335. }
  336. Delta? delta = data.choices![0].delta;
  337. if (delta == null) {
  338. return;
  339. }
  340. graduallyController.append(delta.content ?? "");
  341. } catch (ignore) {}
  342. }, onDone: () {
  343. graduallyController.appendDone();
  344. }, onError: (error) {
  345. _summaryError.value = "网络错误,请检查网络连接";
  346. debugPrint("error: $error");
  347. debugPrintStack();
  348. });
  349. }).catchError((error) {
  350. if (error is ServerErrorException) {
  351. _summaryError.value = error.message ?? "服务出错,请稍后再试";
  352. } else {
  353. _summaryError.value = "网络错误,请检查网络连接";
  354. debugPrint("error: $error");
  355. debugPrintStack();
  356. }
  357. });
  358. }
  359. void onPhoneCallClick() async {
  360. final Uri uri = Uri.parse('tel:${trackController.userInfo?.phoneNumber}');
  361. if (await canLaunchUrl(uri)) {
  362. await launchUrl(uri);
  363. } else {
  364. ToastUtil.show(StringName.trackDailySkipCallPhoneFail);
  365. }
  366. }
  367. void onShareClick() async {
  368. final summary = _trackDailySummary.value;
  369. if (summary == null) {
  370. return;
  371. }
  372. if (summary.showTrackSituation == true &&
  373. (summary.trackSituation == null ||
  374. summary.trackSituation?.isEmpty == true)) {
  375. ToastUtil.show(StringName.trackStayShareAnalysis);
  376. return;
  377. }
  378. trackController.shareTrack(summary);
  379. }
  380. void onHistoryTrackItemClick(TrackDailyBean bean) {
  381. if (trackController.selectedTrackDailyBean == null ||
  382. trackController.selectedTrackDailyBean != bean) {
  383. if (bean.status == TrackStatus.stay) {
  384. //停留状态
  385. Marker selectMarker = Marker(
  386. id: Constants.tracePopupId,
  387. markerName: '',
  388. latitude: bean.lat,
  389. longitude: bean.lng,
  390. markerType: MarkerType.tracePopupPoint,
  391. tags: Popup(
  392. title: bean.addr ?? '',
  393. desc: TrackUtil.formatDurationFromMillis(bean.duration),
  394. ),
  395. );
  396. trackController.showSelectMarker(points, selectMarker);
  397. } else if (bean.status == TrackStatus.moving) {
  398. final movingPoints = TrackUtil.getTrackMovePoints(
  399. trackOriginPoints, bean.start, bean.end);
  400. trackController.showMovingTrack(movingPoints);
  401. } else if (bean.status == TrackStatus.error) {
  402. final errorPoints = TrackUtil.getTrackMovePoints(
  403. trackOriginPoints, bean.start, bean.end);
  404. if (errorPoints.isEmpty) {
  405. return;
  406. }
  407. Marker errorMarker = Marker(
  408. id: Constants.traceErrorId,
  409. markerName: '',
  410. latitude: errorPoints.first.latitude,
  411. longitude: errorPoints.first.longitude,
  412. markerType: MarkerType.trackErrorPoint);
  413. trackController.showTrackError(errorPoints, errorMarker);
  414. }
  415. trackController.selectedTrackDailyBean = bean;
  416. } else {
  417. //如果当前已经有选中的轨迹,则清除选中状态
  418. trackController.clearSelectMapMarker();
  419. trackController.selectedTrackDailyBean = null;
  420. }
  421. }
  422. void onAIAnalyseClick(TrackDailyBean bean) {
  423. final userInfo = trackController.userInfo;
  424. if (userInfo == null) {
  425. return;
  426. }
  427. LocationAnalysePage.start(errorData: bean, userInfo: userInfo);
  428. }
  429. @override
  430. void onClose() {
  431. super.onClose();
  432. _streamChatSubscription?.cancel();
  433. _currentTrackDaySubscription?.cancel();
  434. graduallyController.dispose();
  435. requestTrackFuture?.cancel();
  436. }
  437. }