import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; import 'package:get/get_core/src/get_main.dart'; import 'package:location/base/base_controller.dart'; import 'package:location/data/bean/track_daily_bean.dart'; import 'package:location/data/consts/constants.dart'; import 'package:location/data/repositories/account_repository.dart' show AccountRepository; import 'package:location/data/repositories/track_repository.dart'; import 'package:location/dialog/loading_dialog.dart'; import 'package:location/handler/error_handler.dart'; import 'package:location/module/analyse/location_analyse_page.dart'; import 'package:location/module/track/track_controller.dart'; import 'package:location/module/track/track_day_detail/time_proportion/pie_chat_data.dart'; import 'package:location/module/track/track_util.dart'; import 'package:location/resource/string.gen.dart'; import 'package:location/sdk/wechat/wechat_share_util.dart'; import 'package:location/utils/async_util.dart'; import 'package:location/utils/common_expand.dart'; import 'package:location/utils/pair.dart'; import 'package:location/utils/toast_util.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../data/api/response/track_daily_summary_response.dart'; import '../../../data/bean/atmob_track_point.dart'; import '../../../data/bean/stream_chat_origin_data.dart'; import '../../../data/bean/track_days.dart'; import '../../../utils/http_handler.dart'; import '../../../widget/gradually_print_text.dart'; import '../track_status.dart'; class TrackDayDetailController extends BaseController { final TrackDays days; late TrackRepository trackRepository; late final AccountRepository accountRepository; final RxBool _trackNoData = RxBool(false); final RxList trackDailyList = RxList(); final RxBool _isExpanded = RxBool(false); final RxBool _isRequested = RxBool(false); final RxBool _isHideExpand = RxBool(false); bool get isHideExpand => _isHideExpand.value; bool get isRequested => _isRequested.value; bool get isExpanded => _isExpanded.value; bool get trackNoData => _trackNoData.value; final Rxn> _expandSituation = Rxn(); Pair? get expandSituation => _expandSituation.value; final RxList pieChatData = RxList(); int indexPieChatColor = 0; final TrackController trackController = Get.find(); final RxBool _isShowGradually = RxBool(false); bool get isShowGradually => _isShowGradually.value; final Rxn _trackDailySummary = Rxn(); TrackDailySummaryResponse? get trackDailySummary => _trackDailySummary.value; final GraduallyController graduallyController = GraduallyController(); StreamSubscription? _streamChatSubscription; StreamSubscription? _currentTrackDaySubscription; final RxnString _summaryError = RxnString(); RxDouble get _trackBottomHeight => trackController.trackBottomHeight; double get trackBottomHeight => _trackBottomHeight.value; CancelableFuture? requestTrackFuture; String? get summaryError => _summaryError.value; final GlobalKey shareGlobalKey = GlobalKey(); CancelableFuture? summaryFuture; //轨迹相关 List? trackOriginPoints; //原始轨迹点 List points = []; //根据停留情况合并的轨迹点 List markers = []; // 停留时间较长的标记点 TrackDayDetailController(this.days, bool isExpand) { trackRepository = TrackRepository.getInstance(); accountRepository = AccountRepository.getInstance(); _isExpanded.value = isExpand; } @override void onReady() { _currentTrackDaySubscription = trackController.currentTrackDay.listen((dat) { if (dat == days) { _requestTrackData(); } }); _requestTrackDailySummary(); _recordNumberTrajectoryViewed(); } ///记录查看轨迹的次数 void _recordNumberTrajectoryViewed() { if (accountRepository.memberStatusInfo.value?.trialed == true && accountRepository.memberStatusInfo.value?.level == 20) { trackRepository.refreshMemberTrailTrack(); } } _requestTrackData() { if (isRequested) { trackController.showMapTrack(points, markers); return; } requestTrackFuture?.cancel(); CustomLoadingDialog.show(loadingTxt: StringName.trackLoadingTxt); requestTrackFuture = AsyncUtil.waitForAll( [_requestTrackHistoryPoints(), _requestTrackDaily()]); requestTrackFuture!.then((_) { //组装地图新的数据 points.clear(); markers.clear(); for (int i = 0; i < trackDailyList.length; i++) { final bean = trackDailyList[i]; double? markerLatitude; double? markerLongitude; if (bean.status == TrackStatus.stay) { //停留 markerLatitude = bean.lat; markerLongitude = bean.lng; points.add( LatLng(latitude: markerLatitude, longitude: markerLongitude)); } else if (bean.status == TrackStatus.moving) { //移动 final movePoints = TrackUtil.getTrackMovePoints( trackOriginPoints, bean.start, bean.end); points.addAll(movePoints); continue; } else if (bean.status == TrackStatus.error) { continue; } MarkerType markerType; String? markerName; String? customAvatarUrl; if (i == 0) { //起点 markerType = MarkerType.traceStartPoint; } else if (i == trackDailyList.length - 1) { //终点 final userInfo = trackController.userInfo; markerName = userInfo?.getUserNickName(); markerType = userInfo?.isMine == true ? MarkerType.traceEndMinePoint : MarkerType.traceEndMinePoint; customAvatarUrl = userInfo?.avatar; } else { markerType = MarkerType.tracePassingPoint; } markers.add(Marker( id: '${bean.start}', markerName: markerName ?? '', longitude: markerLongitude, latitude: markerLatitude, markerType: markerType, customAvatarUrl: customAvatarUrl)); } if (markers.isNotEmpty && (markers.last.markerType != MarkerType.traceEndMinePoint || markers.last.markerType != MarkerType.traceEndFriendPoint)) { markers.last.markerType = trackController.userInfo?.isMine == true ? MarkerType.traceEndMinePoint : MarkerType.traceEndFriendPoint; markers.last.markerName = trackController.userInfo?.getUserNickName() ?? ''; markers.last.customAvatarUrl = trackController.userInfo?.avatar; } //如果只有一个点,则添加一个起点和终点相同的点,绘制线需要2点 if (points.length == 1) { points.add(points.first); } trackController.showMapTrack(points, markers); }).whenComplete(() { CustomLoadingDialog.hide(); }); } Future _requestTrackHistoryPoints() { final userInfo = trackController.userInfo; trackOriginPoints = null; return Future.value().then((_) { if (userInfo?.virtual == true) { return trackRepository.queryVirtualTrack(); } else { return trackRepository.queryTrack( startTime: days.start, endTime: days.end, userId: userInfo?.id); } }).then((data) { trackOriginPoints = data.trackPoints; }); } void _requestTrackDailySummary() { summaryFuture?.cancel(); summaryFuture = AsyncUtil.retry( () => _requestTrackSummary(), Duration(seconds: 2), maxRetry: 5); summaryFuture!.catchError((error) { ToastUtil.show(StringName.trackDetailDailySummaryGetFail); }); } Future _requestTrackSummary() { return trackRepository .trackDailySummary( startTime: days.start, endTime: days.end, userId: trackController.userInfo?.id) .then((response) { _trackDailySummary.value = response; return response; }); } Future _requestTrackDaily() { _isRequested.value = false; return trackRepository .trackDailyQuery( startTime: days.start, endTime: days.end, userId: trackController.userInfo?.id) .then((list) { _isRequested.value = true; _trackNoData.value = (list == null || list.isEmpty == true); trackDailyList.assignAll(list ?? []); if (list != null && list.length == 1) { _isExpanded.value = true; _isHideExpand.value = true; } _dealPieChatData(); _dealTrackExpandData(); }).catchError((error) { _isRequested.value = false; ErrorHandler.toastError(error); }); } void _dealPieChatData() { pieChatData.clear(); final list = trackDailyList; if (list.isEmpty) { return; } indexPieChatColor = 0; int totalDuration = 0; final Map addrMap = {}; for (var bean in list) { if (bean.status == TrackStatus.error) { continue; } final addr = bean.addr; if (addr == null) { continue; } if (indexPieChatColor >= pieChatColors.length) { indexPieChatColor = 0; } totalDuration += bean.duration; if (addrMap.containsKey(addr)) { addrMap[addr]!.duration += bean.duration; } else { addrMap[addr] = PieChatData( address: addr, duration: bean.duration, color: pieChatColors[indexPieChatColor++], ); } } for (var data in addrMap.values) { data.proportion = (data.duration / totalDuration * 100).toFormattedDouble(1); } pieChatData.addAll(addrMap.values.toList()); //如果当数据为空,填入一个默认文案数据 if (pieChatData.isEmpty) { PieChatData noStayData = PieChatData( address: StringName.trackNoStayData, duration: 0, proportion: 100, color: pieChatColors[0], ); pieChatData.add(noStayData); } } void _dealTrackExpandData() { final list = trackDailyList; if (list.isNotEmpty && list.length > 1) { TrackExpandType? expandType; TrackDailyBean? expandBean; for (var i = 0; i < list.length; i++) { final bean = list[i]; if (bean.status == TrackStatus.error) { expandType = TrackExpandType.error; expandBean = bean; break; } else if (bean.status == TrackStatus.stay) { if (expandBean == null) { expandType = TrackExpandType.stay; expandBean = bean; } continue; } } if (expandType != null && expandBean != null) { if (expandType == TrackExpandType.error && list.indexOf(expandBean) == list.length - 1) { expandType = TrackExpandType.errorNow; } else if (expandType == TrackExpandType.stay && list.indexOf(expandBean) == list.length - 1) { expandType = TrackExpandType.stayNow; } _expandSituation.value = Pair(expandType, expandBean); } } } void onTrackDetailFoldClick() { _isExpanded.value = !_isExpanded.value; } void checkGraduallyPrintTextVisible(double visibleFraction) { if (!isShowGradually && visibleFraction >= 0.3) { _isShowGradually.value = true; //流式获取轨迹情况 _requestStreamTrackDaily(); } } void _requestStreamTrackDaily() { graduallyController.dispose(); _streamChatSubscription?.cancel(); trackRepository .streamDailySummary( startTime: days.start, endTime: days.end, userId: trackController.userInfo?.id) .then((stream) { // graduallyController.setGraduallyFinishedListener(() { _trackDailySummary.value?.trackSituation = graduallyController.graduallyTxt; _trackDailySummary.refresh(); }); _streamChatSubscription = stream.listen((event) { try { Map json = jsonDecode(event.data); if (json.isEmpty) { return; } StreamChatOriginData data = StreamChatOriginData.fromJson(json); if (data.choices == null || data.choices!.isEmpty) { return; } Delta? delta = data.choices![0].delta; if (delta == null) { return; } graduallyController.append(delta.content ?? ""); } catch (ignore) {} }, onDone: () { graduallyController.appendDone(); }, onError: (error) { _summaryError.value = "网络错误,请检查网络连接"; debugPrint("error: $error"); debugPrintStack(); }); }).catchError((error) { if (error is ServerErrorException) { _summaryError.value = error.message ?? "服务出错,请稍后再试"; } else { _summaryError.value = "网络错误,请检查网络连接"; debugPrint("error: $error"); debugPrintStack(); } }); } void onPhoneCallClick() async { final Uri uri = Uri.parse('tel:${trackController.userInfo?.phoneNumber}'); if (await canLaunchUrl(uri)) { await launchUrl(uri); } else { ToastUtil.show(StringName.trackDailySkipCallPhoneFail); } } void onShareClick() async { WechatShareUtil.shareWidgetToWeChat(shareGlobalKey); } void onHistoryTrackItemClick(TrackDailyBean bean) { if (trackController.selectedTrackDailyBean == null) { if (bean.status == TrackStatus.stay) { //停留状态 Marker selectMarker = Marker( id: Constants.tracePopupId, markerName: '', latitude: bean.lat, longitude: bean.lng, markerType: MarkerType.tracePopupPoint, tags: Popup( title: bean.addr ?? '', desc: TrackUtil.formatDurationFromMillis(bean.duration), ), ); trackController.showSelectMarker(points, selectMarker); } else if (bean.status == TrackStatus.moving) { final movingPoints = TrackUtil.getTrackMovePoints( trackOriginPoints, bean.start, bean.end); trackController.showMovingTrack(movingPoints); } else if (bean.status == TrackStatus.error) { final errorPoints = TrackUtil.getTrackMovePoints( trackOriginPoints, bean.start, bean.end); if (errorPoints.isEmpty) { return; } Marker errorMarker = Marker( id: Constants.traceErrorId, markerName: '', latitude: errorPoints.first.latitude, longitude: errorPoints.first.longitude, markerType: MarkerType.trackErrorPoint); trackController.showTrackError(errorPoints, errorMarker); } trackController.selectedTrackDailyBean = bean; } else { //如果当前已经有选中的轨迹,则清除选中状态 trackController.clearSelectMapMarker(); trackController.selectedTrackDailyBean = null; } } void onAIAnalyseClick(TrackDailyBean bean) { final userInfo = trackController.userInfo; if (userInfo == null) { return; } LocationAnalysePage.start(errorData: bean, userInfo: userInfo); } @override void onClose() { super.onClose(); _streamChatSubscription?.cancel(); _currentTrackDaySubscription?.cancel(); graduallyController.dispose(); requestTrackFuture?.cancel(); } }