294 lines
11 KiB
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
|