// // ItineraryDetailVC.swift // QuickLocation // // Created by 八条 on 2026/6/26. // import UIKit import RxSwift import RxCocoa import ObjectMapper import SwiftyUserDefaults #if !targetEnvironment(simulator) import AMapNaviKit import AMapSearchKit #endif class ItineraryDetailVC: BaseViewController { fileprivate var rootView: ItineraryDetailView! private var points: [SchedulePointModel] = [] private let routeSearch = AMapSearchAPI() private var routeOverlays: [MAPolyline] = [] private var pointAnnotations: [MAPointAnnotation] = [] init(scheduleJson: [String: Any]) { let model = ScheduleModel(JSON: scheduleJson) self.points = model?.points ?? [] super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { rootView = ItineraryDetailView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() setupMap() addPointAnnotations() requestRoute() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if isMovingFromParent || isBeingDismissed { rootView.cleanupMap() } } // MARK: - Map private func setupMap() { #if !targetEnvironment(simulator) rootView.mapView.delegate = self rootView.mapView.showsUserLocation = false routeSearch?.delegate = self if let lat = Defaults[\.currentLatitude], let lon = Defaults[\.currentLongitude] { let coord = CLLocationCoordinate2D(latitude: lat, longitude: lon) if CLLocationCoordinate2DIsValid(coord) { rootView.mapView.setCenter(coord, animated: false) rootView.mapView.setZoomLevel(14, animated: false) } } #endif } private func addPointAnnotations() { #if !targetEnvironment(simulator) for ann in pointAnnotations { rootView.mapView.removeAnnotation(ann) } pointAnnotations.removeAll() // lat/lon 都为 0 的视为无效坐标,跳过 let validPoints = points.filter { guard let lat = $0.latitude, let lon = $0.longitude else { return false } return abs(lat) > 0.0001 && abs(lon) > 0.0001 } for (i, p) in validPoints.enumerated() { let ann = MAPointAnnotation() ann.coordinate = CLLocationCoordinate2D(latitude: p.latitude!, longitude: p.longitude!) ann.title = "\(i + 1)" let timeStr = view.getDateInterval2String(date: "\(p.expected_timestamp / 1000)", dateFormat: "yyyy-MM-dd HH:mm") ann.subtitle = "\(p.street)|\(timeStr)" rootView.mapView.addAnnotation(ann) pointAnnotations.append(ann) } // 缩放到包含所有标注点 if !pointAnnotations.isEmpty { rootView.mapView.showAnnotations(pointAnnotations, animated: true) } #endif } private func requestRoute() { #if !targetEnvironment(simulator) let validPoints = points.filter { ($0.latitude ?? 0) != 0 || ($0.longitude ?? 0) != 0 } guard validPoints.count >= 2 else { return } let request = AMapDrivingRouteSearchRequest() request.origin = AMapGeoPoint.location(withLatitude: CGFloat(validPoints[0].latitude ?? 0), longitude: CGFloat(validPoints[0].longitude ?? 0)) request.destination = AMapGeoPoint.location(withLatitude: CGFloat(validPoints.last?.latitude ?? 0), longitude: CGFloat(validPoints.last?.longitude ?? 0)) if validPoints.count > 2 { var waypoints: [AMapGeoPoint] = [] for i in 1.. UIImage? { let size = CGSize(width: 28, height: 28) let rect = CGRect(origin: .zero, size: size) UIGraphicsBeginImageContextWithOptions(size, false, 0) guard let ctx = UIGraphicsGetCurrentContext() else { return nil } ctx.setLineWidth(1) ctx.setStrokeColor(UIColor.white.cgColor) ctx.setFillColor(UIColor(hexStr: "#16B3FF").cgColor) let path = UIBezierPath(ovalIn: rect) path.fill() path.stroke() let text = "\(num)" as NSString let attrs: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 13), .foregroundColor: UIColor.white] let strSize = text.size(withAttributes: attrs) text.draw(at: CGPoint(x: (size.width - strSize.width) / 2, y: (size.height - strSize.height) / 2)) let img = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return img } /// 生成 callout 背景图 private static func calloutImage() -> UIImage? { let size = CGSize(width: 200, height: 50) UIGraphicsBeginImageContextWithOptions(size, false, 0) guard let ctx = UIGraphicsGetCurrentContext() else { return nil } ctx.setFillColor(UIColor.white.cgColor) let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 8) path.fill() ctx.setStrokeColor(UIColor(hexStr: "#16B3FF").cgColor) ctx.setLineWidth(1) path.stroke() let img = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return img } } #if !targetEnvironment(simulator) // MARK: - MAMapViewDelegate extension ItineraryDetailVC: MAMapViewDelegate { func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! { guard !(annotation is MAUserLocation) else { return nil } guard let pointAnn = annotation as? MAPointAnnotation else { return nil } // 行程点标注 if let num = Int(pointAnn.title ?? "") { let id = "ItineraryPin" var view = mapView.dequeueReusableAnnotationView(withIdentifier: id) if view == nil { view = MAAnnotationView(annotation: annotation, reuseIdentifier: id) } else { view?.annotation = annotation } view?.image = Self.numberImage(num) view?.centerOffset = CGPoint(x: 0, y: -14) // callout let callout = CalloutView(frame: CGRect(x: 0, y: 0, width: 200, height: 50)) if let subtitle = pointAnn.subtitle { let parts = subtitle.components(separatedBy: "|") if parts.count == 2 { callout.nameLab.text = parts[0] callout.timeLab.text = parts[1] } } view?.customCalloutView = callout return view } return nil } func mapView(_ mapView: MAMapView!, didSelect view: MAAnnotationView!) { // 显示自定义 callout if let customCallout = view.customCalloutView { view.addSubview(customCallout) customCallout.frame = CGRect(x: (view.bounds.width - 200) / 2, y: -55, width: 200, height: 50) } } func mapView(_ mapView: MAMapView!, didDeselect view: MAAnnotationView!) { view.customCalloutView?.removeFromSuperview() } } // MARK: - AMapSearchDelegate extension ItineraryDetailVC: AMapSearchDelegate { func onRouteSearchDone(_ request: AMapRouteSearchBaseRequest!, response: AMapRouteSearchResponse!) { guard let path = response.route?.paths?.first as? AMapPath else { return } var coords: [CLLocationCoordinate2D] = [] for step in path.steps { guard let polylineStr = step.polyline else { continue } for point in polylineStr.components(separatedBy: ";") { let latLon = point.components(separatedBy: ",") if latLon.count == 2, let lon = Double(latLon[0]), let lat = Double(latLon[1]) { coords.append(CLLocationCoordinate2D(latitude: lat, longitude: lon)) } } } guard coords.count > 1 else { return } var mutableCoords = coords if let polyline = MAPolyline(coordinates: &mutableCoords, count: UInt(coords.count)) { rootView.mapView.add(polyline) routeOverlays.append(polyline) } } func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) { print("Route search error: \(error.localizedDescription)") } func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! { if let polyline = overlay as? MAPolyline { let r = MAPolylineRenderer(polyline: polyline) r?.strokeColor = UIColor(hexStr: "#16B3FF") r?.lineWidth = 4 r?.lineDashType = kMALineDashTypeSquare return r } return nil } } // MARK: - MAAnnotationView + customCalloutView private var calloutViewKey: UInt8 = 0 extension MAAnnotationView { var customCalloutView: UIView? { get { objc_getAssociatedObject(self, &calloutViewKey) as? UIView } set { objc_setAssociatedObject(self, &calloutViewKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } // MARK: - CalloutView class CalloutView: UIView { let nameLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 13, weight: .medium) l.textColor = UIColor(hexStr: "#333333") l.textAlignment = .center return l }() let timeLab: UILabel = { let l = UILabel() l.font = .systemFont(ofSize: 11, weight: .regular) l.textColor = UIColor(hexStr: "#999999") l.textAlignment = .center return l }() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .white layer.cornerRadius = 8 layer.shadowColor = UIColor.black.withAlphaComponent(0.15).cgColor layer.shadowOffset = .zero layer.shadowRadius = 4 layer.shadowOpacity = 1 addSubview(nameLab) addSubview(timeLab) nameLab.layoutChain.top(8).centerX().edgesHorzontal(10) timeLab.layoutChain.topToBottomOfView(nameLab, offset: 4).centerX().edgesHorzontal(10).bottom(8) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } #endif