track_day_detail_controller.dart 14 KB

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