// // ScheduleDetailVC.swift // QuickLocation // // Created by 八条 on 2026/6/25. // import UIKit import RxSwift import RxCocoa import ObjectMapper import SwiftyUserDefaults import AMapNaviKit #if !targetEnvironment(simulator) import AMapSearchKit #endif final class ScheduleDetailVC: BaseViewController { fileprivate var rootView: ScheduleDetailView! private let viewModel: ScheduleDetailViewModel private var routeOverlays: [MAPolyline] = [] private var pointAnnotations: [ScheduleRouteAnnotation] = [] private var validCoordinates: [CLLocationCoordinate2D] = [] private var hasDrawnFallbackRoute = false private let routeSearch = AMapSearchAPI() override func loadView() { rootView = ScheduleDetailView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() rootView.tableView.dataSource = self configureData() bindActions() setupMap() addPointAnnotations() requestRoute() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if isMovingFromParent || isBeingDismissed { rootView.cleanupMap() } } private func configureData() { guard let model = viewModel.scheduModel else { return } rootView.configure(model) rootView.tableView.reloadData() } private func bindActions() { rootView.editBtn.rx.tap .subscribe(onNext: { [weak self] in guard let model = self?.viewModel.scheduModel, model.is_own else { return } AppRouter.push(Route.createSchedule, userInfo: ["scheduleJson": model.toJSON()]) }) .disposed(by: disposeBag) } private func setupMap() { #if !targetEnvironment(simulator) rootView.mapView.delegate = self routeSearch?.delegate = self if let latitude = Defaults[\.currentLatitude], let longitude = Defaults[\.currentLongitude] { let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) if CLLocationCoordinate2DIsValid(coordinate) { rootView.mapView.setCenter(coordinate, animated: false) } } #endif } private func addPointAnnotations() { #if !targetEnvironment(simulator) pointAnnotations.forEach { rootView.mapView.removeAnnotation($0) } pointAnnotations.removeAll() let validPoints = viewModel.orderedPoints.filter { guard let latitude = $0.latitude, let longitude = $0.longitude else { return false } return abs(latitude) > 0.0001 && abs(longitude) > 0.0001 } validCoordinates = validPoints.compactMap { guard let latitude = $0.latitude, let longitude = $0.longitude else { return nil } return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } for (index, coordinate) in validCoordinates.enumerated() { let annotation = ScheduleRouteAnnotation() annotation.coordinate = coordinate if validCoordinates.count == 1 { annotation.role = validPoints[0].type == 2 ? .end : .start } else if index == 0 { annotation.role = .start } else if index == validCoordinates.count - 1 { annotation.role = .end } else { annotation.role = .waypoint } rootView.mapView.addAnnotation(annotation) pointAnnotations.append(annotation) } fitMapContent() #endif } private func requestRoute() { #if !targetEnvironment(simulator) guard validCoordinates.count >= 2 else { return } let request = AMapDrivingRouteSearchRequest() request.origin = AMapGeoPoint.location( withLatitude: CGFloat(validCoordinates[0].latitude), longitude: CGFloat(validCoordinates[0].longitude) ) if let last = validCoordinates.last { request.destination = AMapGeoPoint.location( withLatitude: CGFloat(last.latitude), longitude: CGFloat(last.longitude) ) } if validCoordinates.count > 2 { request.waypoints = validCoordinates.dropFirst().dropLast().compactMap { AMapGeoPoint.location(withLatitude: CGFloat($0.latitude), longitude: CGFloat($0.longitude)) } } request.strategy = 0 routeSearch?.aMapDrivingRouteSearch(request) #endif } private func drawRoute(_ coordinates: [CLLocationCoordinate2D], fallback: Bool) { #if !targetEnvironment(simulator) guard coordinates.count > 1 else { return } if !fallback { routeOverlays.forEach { rootView.mapView.remove($0) } routeOverlays.removeAll() } else if !routeOverlays.isEmpty || hasDrawnFallbackRoute { return } var mutableCoordinates = coordinates guard let polyline = MAPolyline(coordinates: &mutableCoordinates, count: UInt(mutableCoordinates.count)) else { return } rootView.mapView.add(polyline) routeOverlays.append(polyline) hasDrawnFallbackRoute = fallback fitMapContent() #endif } private func fitMapContent() { #if !targetEnvironment(simulator) rootView.layoutIfNeeded() let padding = UIEdgeInsets( top: kNaviHeight + 18, left: 36, bottom: rootView.mapBottomPadding, right: 36 ) if !routeOverlays.isEmpty { rootView.mapView.showOverlays(routeOverlays, edgePadding: padding, animated: true) } else if !pointAnnotations.isEmpty { rootView.mapView.showAnnotations(pointAnnotations, edgePadding: padding, animated: true) } #endif } private static func markerImage(for role: ScheduleTimelineRole) -> UIImage? { let size = role == .waypoint ? CGSize(width: 16, height: 16) : CGSize(width: 28, height: 28) UIGraphicsBeginImageContextWithOptions(size, false, 0) defer { UIGraphicsEndImageContext() } guard let context = UIGraphicsGetCurrentContext() else { return nil } if role == .waypoint { let markerRect = CGRect(origin: .zero, size: size).insetBy(dx: 1, dy: 1) context.setFillColor(UIColor(hexStr: "#00ADFE").cgColor) context.setStrokeColor(UIColor.white.cgColor) context.setLineWidth(2) context.fillEllipse(in: markerRect) context.strokeEllipse(in: markerRect) context.setFillColor(UIColor.white.cgColor) context.fillEllipse(in: CGRect(x: 7, y: 7, width: 4, height: 4)) } else { let color = role == .start ? UIColor(hexStr: "#35C8F4") : UIColor(hexStr: "#FF5665") context.setFillColor(color.cgColor) context.fillEllipse(in: CGRect(origin: .zero, size: size)) let text = role == .start ? "始" : "终" let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 13, weight: .bold), .foregroundColor: UIColor.white ] let textSize = text.size(withAttributes: attributes) text.draw( at: CGPoint(x: (size.width - textSize.width) / 2, y: (size.height - textSize.height) / 2), withAttributes: attributes ) } return UIGraphicsGetImageFromCurrentImageContext() } init(routeId: String, scheduleJson: [String: Any]) { viewModel = ScheduleDetailViewModel( routeId: routeId, model: scheduleJson.isEmpty ? nil : ScheduleModel(JSON: scheduleJson) ) super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension ScheduleDetailVC: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { viewModel.timelineItems.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: ScheduleTimelineCell = tableView.dequeueReusableCell(for: indexPath) cell.configure(viewModel.timelineItems[indexPath.row]) return cell } } private final class ScheduleRouteAnnotation: MAPointAnnotation { var role: ScheduleTimelineRole = .waypoint } #if !targetEnvironment(simulator) extension ScheduleDetailVC: MAMapViewDelegate { func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! { guard !(annotation is MAUserLocation), let routeAnnotation = annotation as? ScheduleRouteAnnotation else { return nil } let identifier = "ScheduleRouteMarker" var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) if annotationView == nil { annotationView = MAAnnotationView(annotation: annotation, reuseIdentifier: identifier) } else { annotationView?.annotation = annotation } annotationView?.image = Self.markerImage(for: routeAnnotation.role) annotationView?.centerOffset = .zero annotationView?.canShowCallout = false return annotationView } 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: "#58EDFF") renderer?.lineWidth = 6 renderer?.lineJoinType = kMALineJoinRound renderer?.lineCapType = kMALineCapRound return renderer } } extension ScheduleDetailVC: AMapSearchDelegate { func onRouteSearchDone(_ request: AMapRouteSearchBaseRequest!, response: AMapRouteSearchResponse!) { guard let path = response.route?.paths?.first as? AMapPath else { drawRoute(validCoordinates, fallback: true) return } var coordinates: [CLLocationCoordinate2D] = [] for step in path.steps { guard let polyline = step.polyline else { continue } for point in polyline.components(separatedBy: ";") { let values = point.components(separatedBy: ",") if values.count == 2, let longitude = Double(values[0]), let latitude = Double(values[1]) { coordinates.append(CLLocationCoordinate2D(latitude: latitude, longitude: longitude)) } } } if coordinates.count > 1 { drawRoute(coordinates, fallback: false) } else { drawRoute(validCoordinates, fallback: true) } } func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) { drawRoute(validCoordinates, fallback: true) } } #endif