295 lines
11 KiB
Swift
295 lines
11 KiB
Swift
//
|
||
// 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..<validPoints.count - 1 {
|
||
let p = validPoints[i]
|
||
if let wp = AMapGeoPoint.location(withLatitude: CGFloat(p.latitude ?? 0),
|
||
longitude: CGFloat(p.longitude ?? 0)) {
|
||
waypoints.append(wp)
|
||
}
|
||
}
|
||
request.waypoints = waypoints
|
||
}
|
||
request.strategy = 0
|
||
routeSearch?.aMapDrivingRouteSearch(request)
|
||
#endif
|
||
}
|
||
|
||
/// 生成数字图标
|
||
private static func numberImage(_ num: Int) -> 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
|