track_day_detail_controller.dart 16 KB

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