jsdw_ios/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVC.swift

294 lines
11 KiB
Swift

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