// // TodayTrackDetailVC.swift // QuickLocation // import UIKit import RxSwift import RxCocoa import AMapNaviKit import SwiftDate import ObjectMapper final class TodayTrackDetailVC: BaseViewController { private var rootView: TodayTrackDetailView! private let viewModel: TodayTrackDetailViewModel private var routeOverlays: [MAPolyline] = [] private var panStartHeight: CGFloat = 0 private var stayList: [StayPoint] = [] init(members: [GroupMemberModel], selectedUserId: String) { self.viewModel = TodayTrackDetailViewModel(members: members, selectedUserId: selectedUserId) super.init(nibName: nil, bundle: nil) } convenience init(userInfo: [String: Any]) { let rawMembers = userInfo["members"] let membersJson: [[String: Any]] if let arr = rawMembers as? [[String: Any]] { membersJson = arr } else if let arr = rawMembers as? [Any] { membersJson = arr.compactMap { $0 as? [String: Any] } } else { membersJson = [] } let members = membersJson.compactMap { GroupMemberModel(JSON: $0) } let selected = (userInfo["userId"] as? String) ?? members.first?.user_id ?? "" self.init(members: members, selectedUserId: selected) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { rootView = TodayTrackDetailView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() rootView.mapView.delegate = self rootView.memberCV.dataSource = self rootView.memberCV.delegate = self rootView.dateCV.dataSource = self rootView.dateCV.delegate = self rootView.timelineTV.dataSource = self rootView.timelineTV.delegate = self bindActions() bindSheetPan() rootView.memberCV.reloadData() rootView.dateCV.reloadData() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if isMovingFromParent || isBeingDismissed { rootView.cleanupMap() } } private func bindActions() { rootView.backBtn.rx.tap .subscribe(onNext: { [weak self] in self?.navigationController?.popViewController(animated: true) }) .disposed(by: disposeBag) viewModel.trajectoryPoints .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] points in self?.drawRoute(points) }) .disposed(by: disposeBag) viewModel.isEmpty .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] empty in self?.rootView.emptyLab.isHidden = !empty }) .disposed(by: disposeBag) viewModel.stayPoints .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] stays in self?.stayList = stays self?.rootView.timelineTV.reloadData() }) .disposed(by: disposeBag) viewModel.selectedMemberId .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] _ in self?.rootView.memberCV.reloadData() }) .disposed(by: disposeBag) viewModel.selectedDateKey .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] _ in self?.rootView.dateCV.reloadData() }) .disposed(by: disposeBag) } private func bindSheetPan() { let pan = UIPanGestureRecognizer() rootView.sheetView.addGestureRecognizer(pan) pan.rx.event.subscribe(onNext: { [weak self] gesture in guard let self, let constraint = self.rootView.sheetHeightConstraint else { return } let minH: CGFloat = 220 + kSafeBottomMargin let maxH: CGFloat = UIScreen.main.bounds.height * 0.7 switch gesture.state { case .began: self.panStartHeight = constraint.constant case .changed: let dy = gesture.translation(in: self.rootView).y constraint.constant = min(maxH, max(minH, self.panStartHeight - dy)) case .ended, .cancelled: let mid = (minH + maxH) / 2 let target = constraint.constant > mid ? maxH : minH + 100 UIView.animate(withDuration: 0.25) { constraint.constant = target self.rootView.layoutIfNeeded() } default: break } }).disposed(by: disposeBag) } private func drawRoute(_ points: [TrackPoint]) { rootView.mapView.removeOverlays(routeOverlays) routeOverlays.removeAll() if let anns = rootView.mapView.annotations { rootView.mapView.removeAnnotations(anns) } let coords = points .map { CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.lng) } .filter { CLLocationCoordinate2DIsValid($0) } guard coords.count >= 2 else { if let first = coords.first { rootView.mapView.setCenter(first, animated: true) } return } var mutable = coords if let polyline = MAPolyline(coordinates: &mutable, count: UInt(mutable.count)) { rootView.mapView.add(polyline) routeOverlays.append(polyline) let padding = UIEdgeInsets(top: 160, left: 40, bottom: 360, right: 40) rootView.mapView.showOverlays(routeOverlays, edgePadding: padding, animated: true) } if let start = coords.first { let ann = MAPointAnnotation() ann.coordinate = start ann.title = "始" rootView.mapView.addAnnotation(ann) } } private static func stayTitle(_ stay: StayPoint, isCurrent: Bool) -> String { let addr = stay.address.isEmpty ? "**" : stay.address if isCurrent { return "当前 \(addr)" } let time = Date(timeIntervalSince1970: TimeInterval(stay.start_time)).toFormat("HH:mm") return "\(time) \(addr)" } private static func stayTags(_ stay: StayPoint, isCurrent: Bool) -> [String] { let seconds = max(0, Int(stay.end_time - stay.start_time)) let hours = seconds / 3600 let minutes = (seconds % 3600) / 60 let duration: String if hours > 0 { duration = "停留 \(hours)小时\(minutes)分" } else { duration = "停留 \(minutes)分" } if isCurrent { return [duration, "停留中..."] } return [duration] } } // MARK: - Collection / Table extension TodayTrackDetailVC: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { if collectionView === rootView.memberCV { return viewModel.members.count } return viewModel.dateItems.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if collectionView === rootView.memberCV { let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath) let model = viewModel.members[indexPath.item] let selected = model.user_id == viewModel.selectedMemberId.value let isMe = model.user_id == AppContextManager.shared.userId cell.configure(model: model, isCurrentUser: isMe, isSelected: selected) return cell } let cell: TodayTrackDateCell = collectionView.dequeueReusableCell(for: indexPath) let item = viewModel.dateItems[indexPath.item] let selected = item.key == viewModel.selectedDateKey.value cell.configure(title: item.title, selected: selected) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if collectionView === rootView.memberCV { let model = viewModel.members[indexPath.item] viewModel.selectedMemberId.accept(model.user_id) return } let item = viewModel.dateItems[indexPath.item] viewModel.selectedDateKey.accept(item.key) } } extension TodayTrackDetailVC: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { stayList.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: TodayTrackStayCell = tableView.dequeueReusableCell(for: indexPath) let stay = stayList[indexPath.row] let isFirst = indexPath.row == 0 let isLast = indexPath.row == stayList.count - 1 cell.configure( title: Self.stayTitle(stay, isCurrent: isFirst), tags: Self.stayTags(stay, isCurrent: isFirst), isFirst: isFirst, isLast: isLast ) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let stay = stayList[indexPath.row] let coord = CLLocationCoordinate2D(latitude: stay.lat, longitude: stay.lng) guard CLLocationCoordinate2DIsValid(coord) else { return } rootView.mapView.setCenter(coord, animated: true) rootView.mapView.setZoomLevel(16, animated: true) } } extension TodayTrackDetailVC: MAMapViewDelegate { func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! { guard let polyline = overlay as? MAPolyline else { return nil } let renderer = MAPolylineRenderer(polyline: polyline) renderer?.strokeColor = UIColor(hexStr: "#5EC8FF") renderer?.lineWidth = 6 return renderer } func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! { guard !(annotation is MAUserLocation) else { return nil } let id = "todayTrack.ann" var view = mapView.dequeueReusableAnnotationView(withIdentifier: id) if view == nil { view = MAAnnotationView(annotation: annotation, reuseIdentifier: id) } view?.annotation = annotation view?.image = UIImage(systemName: "mappin.circle.fill") view?.centerOffset = CGPoint(x: 0, y: -12) return view } }