jsdw_ios/QuickLocation/Section/Schedule/ItineraryDetail/ItineraryDetailVC.swift

295 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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