Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
38
iphone/Maps/Classes/CarPlay/CPConstants.swift
Normal file
38
iphone/Maps/Classes/CarPlay/CPConstants.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import Foundation
|
||||
|
||||
struct CPConstants {
|
||||
struct TemplateKey {
|
||||
static let map = "map_type"
|
||||
static let alert = "alert_type"
|
||||
static let list = "list_type"
|
||||
}
|
||||
struct TemplateType {
|
||||
static let main = "main"
|
||||
static let navigation = "navigation"
|
||||
static let preview = "preview"
|
||||
static let previewAccepted = "preview_accepted"
|
||||
static let previewSettings = "preview_settings"
|
||||
static let redirectRoute = "redirect_route"
|
||||
static let restoreRoute = "restore_route"
|
||||
static let downloadMap = "download_map"
|
||||
}
|
||||
|
||||
struct ListItemType {
|
||||
static let history = "history"
|
||||
static let bookmarks = "bookmarks"
|
||||
static let bookmarkLists = "bookmark_lists"
|
||||
static let searchResults = "search_results"
|
||||
}
|
||||
|
||||
struct Maneuvers {
|
||||
static let primary = "primary"
|
||||
static let secondary = "secondary"
|
||||
}
|
||||
|
||||
struct Trip {
|
||||
static let start = "start_point"
|
||||
static let end = "end_point"
|
||||
static let errorCode = "error_code"
|
||||
static let missedCountries = "countries"
|
||||
}
|
||||
}
|
||||
7
iphone/Maps/Classes/CarPlay/CPViewPortState.swift
Normal file
7
iphone/Maps/Classes/CarPlay/CPViewPortState.swift
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
enum CPViewPortState: Int {
|
||||
case `default`
|
||||
case preview
|
||||
case navigation
|
||||
}
|
||||
366
iphone/Maps/Classes/CarPlay/CarPlayRouter.swift
Normal file
366
iphone/Maps/Classes/CarPlay/CarPlayRouter.swift
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
import CarPlay
|
||||
import Contacts
|
||||
|
||||
protocol CarPlayRouterListener: AnyObject {
|
||||
func didCreateRoute(routeInfo: RouteInfo,
|
||||
trip: CPTrip)
|
||||
func didUpdateRouteInfo(_ routeInfo: RouteInfo, forTrip trip: CPTrip)
|
||||
func didFailureBuildRoute(forTrip trip: CPTrip, code: RouterResultCode, countries: [String])
|
||||
func routeDidFinish(_ trip: CPTrip)
|
||||
}
|
||||
|
||||
|
||||
@objc(MWMCarPlayRouter)
|
||||
final class CarPlayRouter: NSObject {
|
||||
private let listenerContainer: ListenerContainer<CarPlayRouterListener>
|
||||
private var routeSession: CPNavigationSession?
|
||||
private var initialSpeedCamSettings: SpeedCameraManagerMode
|
||||
var currentTrip: CPTrip? {
|
||||
return routeSession?.trip
|
||||
}
|
||||
var previewTrip: CPTrip?
|
||||
var speedCameraMode: SpeedCameraManagerMode {
|
||||
return RoutingManager.routingManager.speedCameraMode
|
||||
}
|
||||
|
||||
override init() {
|
||||
listenerContainer = ListenerContainer<CarPlayRouterListener>()
|
||||
initialSpeedCamSettings = RoutingManager.routingManager.speedCameraMode
|
||||
super.init()
|
||||
}
|
||||
|
||||
func addListener(_ listener: CarPlayRouterListener) {
|
||||
listenerContainer.addListener(listener)
|
||||
}
|
||||
|
||||
func removeListener(_ listener: CarPlayRouterListener) {
|
||||
listenerContainer.removeListener(listener)
|
||||
}
|
||||
|
||||
func subscribeToEvents() {
|
||||
RoutingManager.routingManager.add(self)
|
||||
}
|
||||
|
||||
func unsubscribeFromEvents() {
|
||||
RoutingManager.routingManager.remove(self)
|
||||
}
|
||||
|
||||
func completeRouteAndRemovePoints() {
|
||||
let manager = RoutingManager.routingManager
|
||||
manager.stopRoutingAndRemoveRoutePoints(true)
|
||||
manager.deleteSavedRoutePoints()
|
||||
manager.apply(routeType: .vehicle)
|
||||
previewTrip = nil
|
||||
}
|
||||
|
||||
func rebuildRoute() {
|
||||
guard let trip = previewTrip else { return }
|
||||
do {
|
||||
try RoutingManager.routingManager.buildRoute()
|
||||
} catch let error as NSError {
|
||||
listenerContainer.forEach({
|
||||
let code = RouterResultCode(rawValue: UInt(error.code)) ?? .internalError
|
||||
$0.didFailureBuildRoute(forTrip: trip, code: code, countries: [])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildRoute(trip: CPTrip) {
|
||||
completeRouteAndRemovePoints()
|
||||
previewTrip = trip
|
||||
guard let info = trip.userInfo as? [String: MWMRoutePoint] else {
|
||||
listenerContainer.forEach({
|
||||
$0.didFailureBuildRoute(forTrip: trip, code: .routeNotFound, countries: [])
|
||||
})
|
||||
return
|
||||
}
|
||||
guard let startPoint = info[CPConstants.Trip.start],
|
||||
let endPoint = info[CPConstants.Trip.end] else {
|
||||
listenerContainer.forEach({
|
||||
var code: RouterResultCode!
|
||||
if info[CPConstants.Trip.end] == nil {
|
||||
code = .endPointNotFound
|
||||
} else {
|
||||
code = .startPointNotFound
|
||||
}
|
||||
$0.didFailureBuildRoute(forTrip: trip, code: code, countries: [])
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let manager = RoutingManager.routingManager
|
||||
manager.add(routePoint: startPoint)
|
||||
manager.add(routePoint: endPoint)
|
||||
|
||||
do {
|
||||
try manager.buildRoute()
|
||||
} catch let error as NSError {
|
||||
listenerContainer.forEach({
|
||||
let code = RouterResultCode(rawValue: UInt(error.code)) ?? .internalError
|
||||
$0.didFailureBuildRoute(forTrip: trip, code: code, countries: [])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateStartPointAndRebuild(trip: CPTrip) {
|
||||
let manager = RoutingManager.routingManager
|
||||
previewTrip = trip
|
||||
guard let info = trip.userInfo as? [String: MWMRoutePoint] else {
|
||||
listenerContainer.forEach({
|
||||
$0.didFailureBuildRoute(forTrip: trip, code: .routeNotFound, countries: [])
|
||||
})
|
||||
return
|
||||
}
|
||||
guard let startPoint = info[CPConstants.Trip.start] else {
|
||||
listenerContainer.forEach({
|
||||
$0.didFailureBuildRoute(forTrip: trip, code: .startPointNotFound, countries: [])
|
||||
})
|
||||
return
|
||||
}
|
||||
manager.add(routePoint: startPoint)
|
||||
manager.apply(routeType: .vehicle)
|
||||
do {
|
||||
try manager.buildRoute()
|
||||
} catch let error as NSError {
|
||||
listenerContainer.forEach({
|
||||
let code = RouterResultCode(rawValue: UInt(error.code)) ?? .internalError
|
||||
$0.didFailureBuildRoute(forTrip: trip, code: code, countries: [])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func startRoute() {
|
||||
let manager = RoutingManager.routingManager
|
||||
manager.startRoute()
|
||||
}
|
||||
|
||||
func setupCarPlaySpeedCameraMode() {
|
||||
if case .auto = initialSpeedCamSettings {
|
||||
RoutingManager.routingManager.speedCameraMode = .always
|
||||
}
|
||||
}
|
||||
|
||||
func setupInitialSpeedCameraMode() {
|
||||
RoutingManager.routingManager.speedCameraMode = initialSpeedCamSettings
|
||||
}
|
||||
|
||||
func updateSpeedCameraMode(_ mode: SpeedCameraManagerMode) {
|
||||
initialSpeedCamSettings = mode
|
||||
RoutingManager.routingManager.speedCameraMode = mode
|
||||
}
|
||||
|
||||
func restoreTripPreviewOnCarplay(beforeRootTemplateDidAppear: Bool) {
|
||||
guard MWMRouter.isRestoreProcessCompleted() else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.restoreTripPreviewOnCarplay(beforeRootTemplateDidAppear: false)
|
||||
}
|
||||
return
|
||||
}
|
||||
let manager = RoutingManager.routingManager
|
||||
MWMRouter.hideNavigationMapControls()
|
||||
guard manager.isRoutingActive,
|
||||
let startPoint = manager.startPoint,
|
||||
let endPoint = manager.endPoint else {
|
||||
completeRouteAndRemovePoints()
|
||||
return
|
||||
}
|
||||
let trip = createTrip(startPoint: startPoint,
|
||||
endPoint: endPoint,
|
||||
routeInfo: manager.routeInfo)
|
||||
previewTrip = trip
|
||||
if manager.type != .vehicle {
|
||||
CarPlayService.shared.showRecoverRouteAlert(trip: trip, isTypeCorrect: false)
|
||||
return
|
||||
}
|
||||
if !startPoint.isMyPosition {
|
||||
CarPlayService.shared.showRecoverRouteAlert(trip: trip, isTypeCorrect: true)
|
||||
return
|
||||
}
|
||||
if beforeRootTemplateDidAppear {
|
||||
CarPlayService.shared.preparedToPreviewTrips = [trip]
|
||||
} else {
|
||||
CarPlayService.shared.preparePreview(trips: [trip])
|
||||
}
|
||||
}
|
||||
|
||||
func restoredNavigationSession() -> (CPTrip, RouteInfo)? {
|
||||
let manager = RoutingManager.routingManager
|
||||
if manager.isOnRoute,
|
||||
manager.type == .vehicle,
|
||||
let startPoint = manager.startPoint,
|
||||
let endPoint = manager.endPoint,
|
||||
let routeInfo = manager.routeInfo {
|
||||
MWMRouter.hideNavigationMapControls()
|
||||
let trip = createTrip(startPoint: startPoint,
|
||||
endPoint: endPoint,
|
||||
routeInfo: routeInfo)
|
||||
previewTrip = trip
|
||||
return (trip, routeInfo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Navigation session management
|
||||
extension CarPlayRouter {
|
||||
func startNavigationSession(forTrip trip: CPTrip, template: CPMapTemplate) {
|
||||
routeSession = template.startNavigationSession(for: trip)
|
||||
routeSession?.pauseTrip(for: .loading, description: nil)
|
||||
updateUpcomingManeuvers()
|
||||
RoutingManager.routingManager.setOnNewTurnCallback { [weak self] in
|
||||
self?.updateUpcomingManeuvers()
|
||||
}
|
||||
}
|
||||
|
||||
func cancelTrip() {
|
||||
routeSession?.cancelTrip()
|
||||
routeSession = nil
|
||||
completeRouteAndRemovePoints()
|
||||
RoutingManager.routingManager.resetOnNewTurnCallback()
|
||||
}
|
||||
|
||||
func finishTrip() {
|
||||
routeSession?.finishTrip()
|
||||
routeSession = nil
|
||||
completeRouteAndRemovePoints()
|
||||
RoutingManager.routingManager.resetOnNewTurnCallback()
|
||||
}
|
||||
|
||||
func updateUpcomingManeuvers() {
|
||||
let maneuvers = createUpcomingManeuvers()
|
||||
routeSession?.upcomingManeuvers = maneuvers
|
||||
}
|
||||
|
||||
func updateEstimates() {
|
||||
guard let routeSession = routeSession,
|
||||
let routeInfo = RoutingManager.routingManager.routeInfo,
|
||||
let primaryManeuver = routeSession.upcomingManeuvers.first,
|
||||
let estimates = createEstimates(routeInfo) else {
|
||||
return
|
||||
}
|
||||
routeSession.updateEstimates(estimates, for: primaryManeuver)
|
||||
}
|
||||
|
||||
private func createEstimates(_ routeInfo: RouteInfo) -> CPTravelEstimates? {
|
||||
let measurement = Measurement(value: routeInfo.distanceToTurn, unit: routeInfo.turnUnits)
|
||||
return CPTravelEstimates(distanceRemaining: measurement, timeRemaining: 0.0)
|
||||
}
|
||||
|
||||
private func createUpcomingManeuvers() -> [CPManeuver] {
|
||||
guard let routeInfo = RoutingManager.routingManager.routeInfo else {
|
||||
return []
|
||||
}
|
||||
var maneuvers = [CPManeuver]()
|
||||
let primaryManeuver = CPManeuver()
|
||||
primaryManeuver.userInfo = CPConstants.Maneuvers.primary
|
||||
var instructionVariant = routeInfo.streetName
|
||||
if routeInfo.roundExitNumber != 0 {
|
||||
let ordinalExitNumber = NumberFormatter.localizedString(from: NSNumber(value: routeInfo.roundExitNumber),
|
||||
number: .ordinal)
|
||||
let exitNumber = String(format: L("carplay_roundabout_exit"),
|
||||
arguments: [ordinalExitNumber])
|
||||
instructionVariant = instructionVariant.isEmpty ? exitNumber : (exitNumber + ", " + instructionVariant)
|
||||
}
|
||||
primaryManeuver.instructionVariants = [instructionVariant]
|
||||
if let imageName = routeInfo.turnImageName,
|
||||
let symbol = UIImage(named: imageName) {
|
||||
primaryManeuver.symbolImage = symbol
|
||||
}
|
||||
if let estimates = createEstimates(routeInfo) {
|
||||
primaryManeuver.initialTravelEstimates = estimates
|
||||
}
|
||||
maneuvers.append(primaryManeuver)
|
||||
if let imageName = routeInfo.nextTurnImageName,
|
||||
let symbol = UIImage(named: imageName) {
|
||||
let secondaryManeuver = CPManeuver()
|
||||
secondaryManeuver.userInfo = CPConstants.Maneuvers.secondary
|
||||
secondaryManeuver.instructionVariants = [L("then_turn")]
|
||||
secondaryManeuver.symbolImage = symbol
|
||||
maneuvers.append(secondaryManeuver)
|
||||
}
|
||||
return maneuvers
|
||||
}
|
||||
|
||||
func createTrip(startPoint: MWMRoutePoint, endPoint: MWMRoutePoint, routeInfo: RouteInfo? = nil) -> CPTrip {
|
||||
let startPlacemark = MKPlacemark(coordinate: CLLocationCoordinate2D(latitude: startPoint.latitude,
|
||||
longitude: startPoint.longitude))
|
||||
let endPlacemark = MKPlacemark(coordinate: CLLocationCoordinate2D(latitude: endPoint.latitude,
|
||||
longitude: endPoint.longitude),
|
||||
addressDictionary: [CNPostalAddressStreetKey: endPoint.subtitle ?? ""])
|
||||
let startItem = MKMapItem(placemark: startPlacemark)
|
||||
let endItem = MKMapItem(placemark: endPlacemark)
|
||||
endItem.name = endPoint.title
|
||||
|
||||
let routeChoice = CPRouteChoice(summaryVariants: [" "], additionalInformationVariants: [], selectionSummaryVariants: [])
|
||||
routeChoice.userInfo = routeInfo
|
||||
|
||||
let trip = CPTrip(origin: startItem, destination: endItem, routeChoices: [routeChoice])
|
||||
trip.userInfo = [CPConstants.Trip.start: startPoint, CPConstants.Trip.end: endPoint]
|
||||
return trip
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RoutingManagerListener implementation
|
||||
extension CarPlayRouter: RoutingManagerListener {
|
||||
func updateCameraInfo(isCameraOnRoute: Bool, speedLimitMps limit: Double) {
|
||||
CarPlayService.shared.updateCameraUI(isCameraOnRoute: isCameraOnRoute, speedLimitMps: limit < 0 ? nil : limit)
|
||||
}
|
||||
|
||||
func processRouteBuilderEvent(with code: RouterResultCode, countries: [String]) {
|
||||
guard let trip = previewTrip else {
|
||||
return
|
||||
}
|
||||
switch code {
|
||||
case .noError, .hasWarnings:
|
||||
let manager = RoutingManager.routingManager
|
||||
if manager.isRouteFinished {
|
||||
listenerContainer.forEach({
|
||||
$0.routeDidFinish(trip)
|
||||
})
|
||||
return
|
||||
}
|
||||
if let info = manager.routeInfo {
|
||||
previewTrip?.routeChoices.first?.userInfo = info
|
||||
if routeSession == nil {
|
||||
listenerContainer.forEach({
|
||||
$0.didCreateRoute(routeInfo: info,
|
||||
trip: trip)
|
||||
})
|
||||
} else {
|
||||
listenerContainer.forEach({
|
||||
$0.didUpdateRouteInfo(info, forTrip: trip)
|
||||
})
|
||||
updateUpcomingManeuvers()
|
||||
}
|
||||
}
|
||||
default:
|
||||
listenerContainer.forEach({
|
||||
$0.didFailureBuildRoute(forTrip: trip, code: code, countries: countries)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func didLocationUpdate(_ notifications: [String]) {
|
||||
guard let trip = previewTrip else { return }
|
||||
|
||||
let manager = RoutingManager.routingManager
|
||||
if manager.isRouteFinished {
|
||||
listenerContainer.forEach({
|
||||
$0.routeDidFinish(trip)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
guard let routeInfo = manager.routeInfo,
|
||||
manager.isRoutingActive else { return }
|
||||
listenerContainer.forEach({
|
||||
$0.didUpdateRouteInfo(routeInfo, forTrip: trip)
|
||||
})
|
||||
|
||||
let tts = MWMTextToSpeech.tts()!
|
||||
if manager.isOnRoute && tts.active {
|
||||
tts.playTurnNotifications(notifications)
|
||||
tts.playWarningSound()
|
||||
}
|
||||
}
|
||||
}
|
||||
805
iphone/Maps/Classes/CarPlay/CarPlayService.swift
Normal file
805
iphone/Maps/Classes/CarPlay/CarPlayService.swift
Normal file
|
|
@ -0,0 +1,805 @@
|
|||
import CarPlay
|
||||
import Contacts
|
||||
|
||||
@objc(MWMCarPlayService)
|
||||
final class CarPlayService: NSObject {
|
||||
@objc static let shared = CarPlayService()
|
||||
@objc var isCarplayActivated: Bool = false
|
||||
private var searchService: CarPlaySearchService?
|
||||
private var router: CarPlayRouter?
|
||||
private var window: CPWindow?
|
||||
private var interfaceController: CPInterfaceController?
|
||||
private var sessionConfiguration: CPSessionConfiguration?
|
||||
var currentPositionMode: MWMMyPositionMode = .pendingPosition
|
||||
var isSpeedCamActivated: Bool {
|
||||
set {
|
||||
router?.updateSpeedCameraMode(newValue ? .always: .never)
|
||||
}
|
||||
get {
|
||||
let mode: SpeedCameraManagerMode = router?.speedCameraMode ?? .never
|
||||
return mode == .always ? true : false
|
||||
}
|
||||
}
|
||||
var isKeyboardLimited: Bool {
|
||||
return sessionConfiguration?.limitedUserInterfaces.contains(.keyboard) ?? false
|
||||
}
|
||||
private var carplayVC: CarPlayMapViewController? {
|
||||
return window?.rootViewController as? CarPlayMapViewController
|
||||
}
|
||||
private var rootMapTemplate: CPMapTemplate? {
|
||||
return interfaceController?.rootTemplate as? CPMapTemplate
|
||||
}
|
||||
var preparedToPreviewTrips: [CPTrip] = []
|
||||
var isUserPanMap: Bool = false
|
||||
private var searchText = ""
|
||||
|
||||
@objc func setup(window: CPWindow, interfaceController: CPInterfaceController) {
|
||||
isCarplayActivated = true
|
||||
self.window = window
|
||||
self.interfaceController = interfaceController
|
||||
self.interfaceController?.delegate = self
|
||||
let configuration = CPSessionConfiguration(delegate: self)
|
||||
sessionConfiguration = configuration
|
||||
searchService = CarPlaySearchService()
|
||||
let router = CarPlayRouter()
|
||||
router.addListener(self)
|
||||
router.subscribeToEvents()
|
||||
router.setupCarPlaySpeedCameraMode()
|
||||
self.router = router
|
||||
MWMRouter.unsubscribeFromEvents()
|
||||
applyRootViewController()
|
||||
if let sessionData = router.restoredNavigationSession() {
|
||||
applyNavigationRootTemplate(trip: sessionData.0, routeInfo: sessionData.1)
|
||||
} else {
|
||||
applyBaseRootTemplate()
|
||||
router.restoreTripPreviewOnCarplay(beforeRootTemplateDidAppear: true)
|
||||
}
|
||||
updateContentStyle(configuration.contentStyle)
|
||||
FrameworkHelper.updatePositionArrowOffset(false, offset: (Int32(window.height * window.screen.scale)/3))
|
||||
|
||||
CarPlayWindowScaleAdjuster.updateAppearance(
|
||||
fromWindow: MapsAppDelegate.theApp().window,
|
||||
toWindow: window,
|
||||
isCarplayActivated: true
|
||||
)
|
||||
}
|
||||
|
||||
private var savedInterfaceController: CPInterfaceController?
|
||||
@objc func showOnPhone() {
|
||||
savedInterfaceController = interfaceController
|
||||
switchScreenToPhone()
|
||||
showPhoneModeAlert()
|
||||
}
|
||||
|
||||
@objc func showOnCarplay() {
|
||||
if let window, let savedInterfaceController {
|
||||
setup(window: window, interfaceController: savedInterfaceController)
|
||||
}
|
||||
}
|
||||
|
||||
private func showPhoneModeAlert() {
|
||||
let switchToCarAction = CPAlertAction(
|
||||
title: L("car_continue_in_the_car"),
|
||||
style: .default,
|
||||
handler: { [weak self] _ in
|
||||
self?.savedInterfaceController?.dismissTemplate(animated: false)
|
||||
self?.showOnCarplay()
|
||||
}
|
||||
)
|
||||
let alert = CPAlertTemplate(
|
||||
titleVariants: [L("car_used_on_the_phone_screen")],
|
||||
actions: [switchToCarAction]
|
||||
)
|
||||
savedInterfaceController?.dismissTemplate(animated: false)
|
||||
savedInterfaceController?.presentTemplate(alert, animated: false)
|
||||
}
|
||||
|
||||
private func switchScreenToPhone() {
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.removeMapView()
|
||||
}
|
||||
if let mvc = MapViewController.shared() {
|
||||
mvc.disableCarPlayRepresentation()
|
||||
mvc.remove(self)
|
||||
}
|
||||
router?.removeListener(self)
|
||||
router?.unsubscribeFromEvents()
|
||||
router?.setupInitialSpeedCameraMode()
|
||||
MWMRouter.subscribeToEvents()
|
||||
isCarplayActivated = false
|
||||
if router?.currentTrip != nil {
|
||||
MWMRouter.showNavigationMapControls()
|
||||
} else if router?.previewTrip != nil {
|
||||
MWMRouter.rebuild(withBestRouter: true)
|
||||
}
|
||||
searchService = nil
|
||||
router = nil
|
||||
sessionConfiguration = nil
|
||||
interfaceController = nil
|
||||
ThemeManager.invalidate()
|
||||
FrameworkHelper.updatePositionArrowOffset(true, offset: 0)
|
||||
|
||||
if let window {
|
||||
CarPlayWindowScaleAdjuster.updateAppearance(
|
||||
fromWindow: window,
|
||||
toWindow: MapsAppDelegate.theApp().window,
|
||||
isCarplayActivated: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func destroy() {
|
||||
if isCarplayActivated {
|
||||
switchScreenToPhone()
|
||||
}
|
||||
savedInterfaceController = nil
|
||||
}
|
||||
|
||||
@objc func interfaceStyle() -> UIUserInterfaceStyle {
|
||||
if let window = window,
|
||||
window.traitCollection.userInterfaceIdiom == .carPlay {
|
||||
return rootTemplateStyle == .dark ? .dark : .light
|
||||
}
|
||||
return .unspecified
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
private func updateContentStyle(_ contentStyle: CPContentStyle) {
|
||||
rootTemplateStyle = contentStyle == .dark ? .dark : .light
|
||||
// Update the current map style in accordance with the CarPLay content theme.
|
||||
ThemeManager.invalidate()
|
||||
}
|
||||
|
||||
private var rootTemplateStyle: CPTripEstimateStyle = .light {
|
||||
didSet {
|
||||
(interfaceController?.rootTemplate as? CPMapTemplate)?.tripEstimateStyle = rootTemplateStyle
|
||||
}
|
||||
}
|
||||
|
||||
private func applyRootViewController() {
|
||||
guard let window = window else { return }
|
||||
let carplaySotyboard = UIStoryboard.instance(.carPlay)
|
||||
let carplayVC = carplaySotyboard.instantiateInitialViewController() as! CarPlayMapViewController
|
||||
window.rootViewController = carplayVC
|
||||
if let mapVC = MapViewController.shared() {
|
||||
currentPositionMode = mapVC.currentPositionMode
|
||||
mapVC.enableCarPlayRepresentation()
|
||||
carplayVC.addMapView(mapVC.mapView, mapButtonSafeAreaLayoutGuide: window.mapButtonSafeAreaLayoutGuide)
|
||||
mapVC.add(self)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyBaseRootTemplate() {
|
||||
let mapTemplate = MapTemplateBuilder.buildBaseTemplate(positionMode: currentPositionMode)
|
||||
mapTemplate.mapDelegate = self
|
||||
mapTemplate.tripEstimateStyle = rootTemplateStyle
|
||||
interfaceController?.setRootTemplate(mapTemplate, animated: true)
|
||||
FrameworkHelper.rotateMap(0.0, animated: false)
|
||||
}
|
||||
|
||||
private func applyNavigationRootTemplate(trip: CPTrip, routeInfo: RouteInfo) {
|
||||
let mapTemplate = MapTemplateBuilder.buildNavigationTemplate()
|
||||
mapTemplate.mapDelegate = self
|
||||
interfaceController?.setRootTemplate(mapTemplate, animated: true)
|
||||
router?.startNavigationSession(forTrip: trip, template: mapTemplate)
|
||||
if let estimates = createEstimates(routeInfo: routeInfo) {
|
||||
mapTemplate.tripEstimateStyle = rootTemplateStyle
|
||||
mapTemplate.updateEstimates(estimates, for: trip)
|
||||
}
|
||||
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.updateCurrentSpeed(routeInfo.speedMps, speedLimitMps: routeInfo.speedLimitMps)
|
||||
carplayVC.showSpeedControl()
|
||||
}
|
||||
}
|
||||
|
||||
func pushTemplate(_ templateToPush: CPTemplate, animated: Bool) {
|
||||
if let interfaceController = interfaceController {
|
||||
switch templateToPush {
|
||||
case let list as CPListTemplate:
|
||||
list.delegate = self
|
||||
case let search as CPSearchTemplate:
|
||||
search.delegate = self
|
||||
case let map as CPMapTemplate:
|
||||
map.mapDelegate = self
|
||||
default:
|
||||
break
|
||||
}
|
||||
interfaceController.pushTemplate(templateToPush, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func popTemplate(animated: Bool) {
|
||||
interfaceController?.popTemplate(animated: animated)
|
||||
}
|
||||
|
||||
func presentAlert(_ template: CPAlertTemplate, animated: Bool) {
|
||||
interfaceController?.dismissTemplate(animated: false)
|
||||
interfaceController?.presentTemplate(template, animated: animated)
|
||||
}
|
||||
|
||||
func cancelCurrentTrip() {
|
||||
router?.cancelTrip()
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.hideSpeedControl()
|
||||
}
|
||||
updateMapTemplateUIToBase()
|
||||
}
|
||||
|
||||
func updateCameraUI(isCameraOnRoute: Bool, speedLimitMps limit: Double?) {
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.updateCameraInfo(isCameraOnRoute: isCameraOnRoute, speedLimitMps: limit)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMapTemplateUIToBase() {
|
||||
guard let mapTemplate = rootMapTemplate else {
|
||||
return
|
||||
}
|
||||
MapTemplateBuilder.configureBaseUI(mapTemplate: mapTemplate)
|
||||
if currentPositionMode == .pendingPosition {
|
||||
mapTemplate.leadingNavigationBarButtons = []
|
||||
} else if currentPositionMode == .follow || currentPositionMode == .followAndRotate {
|
||||
MapTemplateBuilder.setupDestinationButton(mapTemplate: mapTemplate)
|
||||
} else {
|
||||
MapTemplateBuilder.setupRecenterButton(mapTemplate: mapTemplate)
|
||||
}
|
||||
updateVisibleViewPortState(.default)
|
||||
FrameworkHelper.rotateMap(0.0, animated: true)
|
||||
}
|
||||
|
||||
func updateMapTemplateUIToTripFinished(_ trip: CPTrip) {
|
||||
guard let mapTemplate = rootMapTemplate else {
|
||||
return
|
||||
}
|
||||
mapTemplate.leadingNavigationBarButtons = []
|
||||
mapTemplate.trailingNavigationBarButtons = []
|
||||
mapTemplate.mapButtons = []
|
||||
let doneAction = CPAlertAction(title: L("done"), style: .default) { [unowned self] _ in
|
||||
self.updateMapTemplateUIToBase()
|
||||
}
|
||||
var subtitle = ""
|
||||
if let locationName = trip.destination.name {
|
||||
subtitle = locationName
|
||||
}
|
||||
if let address = trip.destination.placemark.postalAddress?.street {
|
||||
subtitle = subtitle + "\n" + address
|
||||
}
|
||||
|
||||
let alert = CPNavigationAlert(titleVariants: [L("trip_finished")],
|
||||
subtitleVariants: [subtitle],
|
||||
image: nil,
|
||||
primaryAction: doneAction,
|
||||
secondaryAction: nil,
|
||||
duration: 0)
|
||||
mapTemplate.present(navigationAlert: alert, animated: true)
|
||||
}
|
||||
|
||||
func updateVisibleViewPortState(_ state: CPViewPortState) {
|
||||
guard let carplayVC = carplayVC else {
|
||||
return
|
||||
}
|
||||
carplayVC.updateVisibleViewPortState(state)
|
||||
}
|
||||
|
||||
func updateRouteAfterChangingSettings() {
|
||||
router?.rebuildRoute()
|
||||
}
|
||||
|
||||
@objc func showNoMapAlert() {
|
||||
guard let mapTemplate = interfaceController?.topTemplate as? CPMapTemplate,
|
||||
let info = mapTemplate.userInfo as? MapInfo,
|
||||
info.type == CPConstants.TemplateType.main else {
|
||||
return
|
||||
}
|
||||
let alert = CPAlertTemplate(titleVariants: [L("download_map_carplay")], actions: [])
|
||||
alert.userInfo = [CPConstants.TemplateKey.alert: CPConstants.TemplateType.downloadMap]
|
||||
presentAlert(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc func hideNoMapAlert() {
|
||||
if let presentedTemplate = interfaceController?.presentedTemplate,
|
||||
let info = presentedTemplate.userInfo as? [String: String],
|
||||
let alertType = info[CPConstants.TemplateKey.alert],
|
||||
alertType == CPConstants.TemplateType.downloadMap {
|
||||
interfaceController?.dismissTemplate(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CPInterfaceControllerDelegate implementation
|
||||
extension CarPlayService: CPInterfaceControllerDelegate {
|
||||
func templateWillAppear(_ aTemplate: CPTemplate, animated: Bool) {
|
||||
guard let info = aTemplate.userInfo as? MapInfo else {
|
||||
return
|
||||
}
|
||||
switch info.type {
|
||||
case CPConstants.TemplateType.main:
|
||||
updateVisibleViewPortState(.default)
|
||||
case CPConstants.TemplateType.preview:
|
||||
updateVisibleViewPortState(.preview)
|
||||
case CPConstants.TemplateType.navigation:
|
||||
updateVisibleViewPortState(.navigation)
|
||||
case CPConstants.TemplateType.previewSettings:
|
||||
aTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.preview)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func templateDidAppear(_ aTemplate: CPTemplate, animated: Bool) {
|
||||
guard let mapTemplate = aTemplate as? CPMapTemplate,
|
||||
let info = aTemplate.userInfo as? MapInfo else {
|
||||
return
|
||||
}
|
||||
if !preparedToPreviewTrips.isEmpty && info.type == CPConstants.TemplateType.main {
|
||||
preparePreview(trips: preparedToPreviewTrips)
|
||||
preparedToPreviewTrips = []
|
||||
return
|
||||
}
|
||||
|
||||
if info.type == CPConstants.TemplateType.preview, let trips = info.trips {
|
||||
showPreview(mapTemplate: mapTemplate, trips: trips)
|
||||
}
|
||||
}
|
||||
|
||||
func templateWillDisappear(_ aTemplate: CPTemplate, animated: Bool) {
|
||||
guard let info = aTemplate.userInfo as? MapInfo else {
|
||||
return
|
||||
}
|
||||
if info.type == CPConstants.TemplateType.preview {
|
||||
router?.completeRouteAndRemovePoints()
|
||||
}
|
||||
}
|
||||
|
||||
func templateDidDisappear(_ aTemplate: CPTemplate, animated: Bool) {
|
||||
guard !preparedToPreviewTrips.isEmpty,
|
||||
let info = aTemplate.userInfo as? [String: String],
|
||||
let alertType = info[CPConstants.TemplateKey.alert],
|
||||
alertType == CPConstants.TemplateType.redirectRoute ||
|
||||
alertType == CPConstants.TemplateType.restoreRoute else {
|
||||
return
|
||||
}
|
||||
preparePreview(trips: preparedToPreviewTrips)
|
||||
preparedToPreviewTrips = []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CPSessionConfigurationDelegate implementation
|
||||
extension CarPlayService: CPSessionConfigurationDelegate {
|
||||
func sessionConfiguration(_ sessionConfiguration: CPSessionConfiguration,
|
||||
limitedUserInterfacesChanged limitedUserInterfaces: CPLimitableUserInterface) {
|
||||
|
||||
}
|
||||
@available(iOS 13.0, *)
|
||||
func sessionConfiguration(_ sessionConfiguration: CPSessionConfiguration,
|
||||
contentStyleChanged contentStyle: CPContentStyle) {
|
||||
// Handle the CarPlay content style changing triggered by the 'Always Show Dark Maps' toggle.
|
||||
updateContentStyle(contentStyle)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CPMapTemplateDelegate implementation
|
||||
extension CarPlayService: CPMapTemplateDelegate {
|
||||
public func mapTemplateDidShowPanningInterface(_ mapTemplate: CPMapTemplate) {
|
||||
isUserPanMap = false
|
||||
MapTemplateBuilder.configurePanUI(mapTemplate: mapTemplate)
|
||||
FrameworkHelper.stopLocationFollow()
|
||||
}
|
||||
|
||||
public func mapTemplateDidDismissPanningInterface(_ mapTemplate: CPMapTemplate) {
|
||||
if let info = mapTemplate.userInfo as? MapInfo,
|
||||
info.type == CPConstants.TemplateType.navigation {
|
||||
MapTemplateBuilder.configureNavigationUI(mapTemplate: mapTemplate)
|
||||
} else {
|
||||
MapTemplateBuilder.configureBaseUI(mapTemplate: mapTemplate)
|
||||
}
|
||||
FrameworkHelper.switchMyPositionMode()
|
||||
}
|
||||
|
||||
@objc(mapTemplate:panEndedWithDirection:)
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate, panEndedWith direction: Int) {
|
||||
var offset = UIOffset(horizontal: 0.0, vertical: 0.0)
|
||||
let offsetStep: CGFloat = 0.25
|
||||
let panDirection = CPMapTemplate.PanDirection(rawValue: direction)
|
||||
if panDirection.contains(.up) { offset.vertical -= offsetStep }
|
||||
if panDirection.contains(.down) { offset.vertical += offsetStep }
|
||||
if panDirection.contains(.left) { offset.horizontal += offsetStep }
|
||||
if panDirection.contains(.right) { offset.horizontal -= offsetStep }
|
||||
FrameworkHelper.moveMap(offset)
|
||||
isUserPanMap = true
|
||||
}
|
||||
|
||||
|
||||
@objc(mapTemplate:panWithDirection:)
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: Int) {
|
||||
var offset = UIOffset(horizontal: 0.0, vertical: 0.0)
|
||||
let offsetStep: CGFloat = 0.1
|
||||
let panDirection = CPMapTemplate.PanDirection(rawValue: direction)
|
||||
if panDirection.contains(.up) { offset.vertical -= offsetStep }
|
||||
if panDirection.contains(.down) { offset.vertical += offsetStep }
|
||||
if panDirection.contains(.left) { offset.horizontal += offsetStep }
|
||||
if panDirection.contains(.right) { offset.horizontal -= offsetStep }
|
||||
FrameworkHelper.moveMap(offset)
|
||||
isUserPanMap = true
|
||||
}
|
||||
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate, didUpdatePanGestureWithTranslation translation: CGPoint, velocity: CGPoint) {
|
||||
let scaleFactor = self.carplayVC?.mapView?.contentScaleFactor ?? 1
|
||||
FrameworkHelper.scrollMap(toDistanceX:-scaleFactor * translation.x, andY:-scaleFactor * translation.y);
|
||||
}
|
||||
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate, startedTrip trip: CPTrip, using routeChoice: CPRouteChoice) {
|
||||
guard let info = routeChoice.userInfo as? RouteInfo else {
|
||||
if let info = routeChoice.userInfo as? [String: Any],
|
||||
let code = info[CPConstants.Trip.errorCode] as? RouterResultCode,
|
||||
let countries = info[CPConstants.Trip.missedCountries] as? [String] {
|
||||
showErrorAlert(code: code, countries: countries)
|
||||
}
|
||||
return
|
||||
}
|
||||
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.previewAccepted)
|
||||
mapTemplate.hideTripPreviews()
|
||||
|
||||
guard let router = router,
|
||||
let interfaceController = interfaceController,
|
||||
let rootMapTemplate = rootMapTemplate else {
|
||||
return
|
||||
}
|
||||
|
||||
MapTemplateBuilder.configureNavigationUI(mapTemplate: rootMapTemplate)
|
||||
|
||||
if interfaceController.templates.count > 1 {
|
||||
interfaceController.popToRootTemplate(animated: false)
|
||||
}
|
||||
router.startNavigationSession(forTrip: trip, template: rootMapTemplate)
|
||||
router.startRoute()
|
||||
if let estimates = createEstimates(routeInfo: info) {
|
||||
rootMapTemplate.updateEstimates(estimates, for: trip)
|
||||
}
|
||||
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.updateCurrentSpeed(info.speedMps, speedLimitMps: info.speedLimitMps)
|
||||
carplayVC.showSpeedControl()
|
||||
}
|
||||
updateVisibleViewPortState(.navigation)
|
||||
}
|
||||
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate, displayStyleFor maneuver: CPManeuver) -> CPManeuverDisplayStyle {
|
||||
if let type = maneuver.userInfo as? String,
|
||||
type == CPConstants.Maneuvers.secondary {
|
||||
return .trailingSymbol
|
||||
}
|
||||
return .leadingSymbol
|
||||
}
|
||||
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate,
|
||||
selectedPreviewFor trip: CPTrip,
|
||||
using routeChoice: CPRouteChoice) {
|
||||
guard let previewTrip = router?.previewTrip, previewTrip == trip else {
|
||||
applyUndefinedEstimates(template: mapTemplate, trip: trip)
|
||||
router?.buildRoute(trip: trip)
|
||||
return
|
||||
}
|
||||
guard let info = routeChoice.userInfo as? RouteInfo,
|
||||
let estimates = createEstimates(routeInfo: info) else {
|
||||
applyUndefinedEstimates(template: mapTemplate, trip: trip)
|
||||
router?.rebuildRoute()
|
||||
return
|
||||
}
|
||||
mapTemplate.updateEstimates(estimates, for: trip)
|
||||
routeChoice.userInfo = nil
|
||||
router?.rebuildRoute()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - CPListTemplateDelegate implementation
|
||||
extension CarPlayService: CPListTemplateDelegate {
|
||||
func listTemplate(_ listTemplate: CPListTemplate, didSelect item: CPListItem, completionHandler: @escaping () -> Void) {
|
||||
if let userInfo = item.userInfo as? ListItemInfo {
|
||||
switch userInfo.type {
|
||||
case CPConstants.ListItemType.history:
|
||||
let locale = window?.textInputMode?.primaryLanguage ?? "en"
|
||||
guard let searchService = searchService else {
|
||||
completionHandler()
|
||||
return
|
||||
}
|
||||
searchService.searchText(item.text ?? "", forInputLocale: locale, completionHandler: { [weak self] results in
|
||||
guard let self = self else { return }
|
||||
let template = ListTemplateBuilder.buildListTemplate(for: .searchResults(results: results))
|
||||
completionHandler()
|
||||
self.pushTemplate(template, animated: true)
|
||||
})
|
||||
case CPConstants.ListItemType.bookmarkLists where userInfo.metadata is CategoryInfo:
|
||||
let metadata = userInfo.metadata as! CategoryInfo
|
||||
let template = ListTemplateBuilder.buildListTemplate(for: .bookmarks(category: metadata.category))
|
||||
completionHandler()
|
||||
pushTemplate(template, animated: true)
|
||||
case CPConstants.ListItemType.bookmarks where userInfo.metadata is BookmarkInfo:
|
||||
let metadata = userInfo.metadata as! BookmarkInfo
|
||||
let bookmark = MWMCarPlayBookmarkObject(bookmarkId: metadata.bookmarkId)
|
||||
preparePreview(forBookmark: bookmark)
|
||||
completionHandler()
|
||||
case CPConstants.ListItemType.searchResults where userInfo.metadata is SearchResultInfo:
|
||||
let metadata = userInfo.metadata as! SearchResultInfo
|
||||
preparePreviewForSearchResults(selectedRow: metadata.originalRow)
|
||||
completionHandler()
|
||||
default:
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CPSearchTemplateDelegate implementation
|
||||
extension CarPlayService: CPSearchTemplateDelegate {
|
||||
func searchTemplate(_ searchTemplate: CPSearchTemplate, updatedSearchText searchText: String, completionHandler: @escaping ([CPListItem]) -> Void) {
|
||||
self.searchText = searchText
|
||||
let locale = window?.textInputMode?.primaryLanguage ?? "en"
|
||||
guard let searchService = searchService else {
|
||||
completionHandler([])
|
||||
return
|
||||
}
|
||||
searchService.searchText(self.searchText, forInputLocale: locale, completionHandler: { results in
|
||||
var items = [CPListItem]()
|
||||
for object in results {
|
||||
let item = CPListItem(text: object.title, detailText: object.address)
|
||||
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.searchResults,
|
||||
metadata: SearchResultInfo(originalRow: object.originalRow))
|
||||
items.append(item)
|
||||
}
|
||||
completionHandler(items)
|
||||
})
|
||||
}
|
||||
|
||||
func searchTemplate(_ searchTemplate: CPSearchTemplate, selectedResult item: CPListItem, completionHandler: @escaping () -> Void) {
|
||||
searchService?.saveLastQuery()
|
||||
if let info = item.userInfo as? ListItemInfo,
|
||||
let metadata = info.metadata as? SearchResultInfo {
|
||||
preparePreviewForSearchResults(selectedRow: metadata.originalRow)
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
func searchTemplateSearchButtonPressed(_ searchTemplate: CPSearchTemplate) {
|
||||
let locale = window?.textInputMode?.primaryLanguage ?? "en"
|
||||
guard let searchService = searchService else {
|
||||
return
|
||||
}
|
||||
searchService.searchText(searchText, forInputLocale: locale, completionHandler: { [weak self] results in
|
||||
guard let self = self else { return }
|
||||
let template = ListTemplateBuilder.buildListTemplate(for: .searchResults(results: results))
|
||||
self.pushTemplate(template, animated: true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CarPlayRouterListener implementation
|
||||
extension CarPlayService: CarPlayRouterListener {
|
||||
func didCreateRoute(routeInfo: RouteInfo, trip: CPTrip) {
|
||||
guard let currentTemplate = interfaceController?.topTemplate as? CPMapTemplate,
|
||||
let info = currentTemplate.userInfo as? MapInfo,
|
||||
info.type == CPConstants.TemplateType.preview else {
|
||||
return
|
||||
}
|
||||
if let estimates = createEstimates(routeInfo: routeInfo) {
|
||||
currentTemplate.updateEstimates(estimates, for: trip)
|
||||
}
|
||||
}
|
||||
|
||||
func didUpdateRouteInfo(_ routeInfo: RouteInfo, forTrip trip: CPTrip) {
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.updateCurrentSpeed(routeInfo.speedMps, speedLimitMps: routeInfo.speedLimitMps)
|
||||
}
|
||||
guard let router = router,
|
||||
let template = rootMapTemplate else {
|
||||
return
|
||||
}
|
||||
router.updateEstimates()
|
||||
if let estimates = createEstimates(routeInfo: routeInfo) {
|
||||
template.updateEstimates(estimates, for: trip)
|
||||
}
|
||||
trip.routeChoices.first?.userInfo = routeInfo
|
||||
}
|
||||
|
||||
func didFailureBuildRoute(forTrip trip: CPTrip, code: RouterResultCode, countries: [String]) {
|
||||
guard let template = interfaceController?.topTemplate as? CPMapTemplate else { return }
|
||||
trip.routeChoices.first?.userInfo = [CPConstants.Trip.errorCode: code, CPConstants.Trip.missedCountries: countries]
|
||||
applyUndefinedEstimates(template: template, trip: trip)
|
||||
}
|
||||
|
||||
func routeDidFinish(_ trip: CPTrip) {
|
||||
if router?.currentTrip == nil { return }
|
||||
router?.finishTrip()
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.hideSpeedControl()
|
||||
}
|
||||
updateMapTemplateUIToTripFinished(trip)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LocationModeListener implementation
|
||||
extension CarPlayService: LocationModeListener {
|
||||
func processMyPositionStateModeEvent(_ mode: MWMMyPositionMode) {
|
||||
currentPositionMode = mode
|
||||
guard let rootMapTemplate = rootMapTemplate,
|
||||
let info = rootMapTemplate.userInfo as? MapInfo,
|
||||
info.type == CPConstants.TemplateType.main else {
|
||||
return
|
||||
}
|
||||
switch mode {
|
||||
case .follow, .followAndRotate:
|
||||
if !rootMapTemplate.isPanningInterfaceVisible {
|
||||
MapTemplateBuilder.setupDestinationButton(mapTemplate: rootMapTemplate)
|
||||
}
|
||||
case .notFollow:
|
||||
if !rootMapTemplate.isPanningInterfaceVisible {
|
||||
MapTemplateBuilder.setupRecenterButton(mapTemplate: rootMapTemplate)
|
||||
}
|
||||
case .pendingPosition, .notFollowNoPosition:
|
||||
rootMapTemplate.leadingNavigationBarButtons = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Alerts and Trip Previews
|
||||
extension CarPlayService {
|
||||
func preparePreviewForSearchResults(selectedRow row: Int) {
|
||||
var results = searchService?.lastResults ?? []
|
||||
if let currentItemIndex = results.firstIndex(where: { $0.originalRow == row }) {
|
||||
let item = results.remove(at: currentItemIndex)
|
||||
results.insert(item, at: 0)
|
||||
} else {
|
||||
results.insert(MWMCarPlaySearchResultObject(forRow: row), at: 0)
|
||||
}
|
||||
if let router = router,
|
||||
let startPoint = MWMRoutePoint(lastLocationAndType: .start,
|
||||
intermediateIndex: 0) {
|
||||
let endPoints = results.compactMap({ MWMRoutePoint(cgPoint: $0.mercatorPoint,
|
||||
title: $0.title,
|
||||
subtitle: $0.address,
|
||||
type: .finish,
|
||||
intermediateIndex: 0) })
|
||||
let trips = endPoints.map({ router.createTrip(startPoint: startPoint, endPoint: $0) })
|
||||
if router.currentTrip == nil {
|
||||
preparePreview(trips: trips)
|
||||
} else {
|
||||
showRerouteAlert(trips: trips)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func preparePreview(forBookmark bookmark: MWMCarPlayBookmarkObject) {
|
||||
if let router = router,
|
||||
let startPoint = MWMRoutePoint(lastLocationAndType: .start,
|
||||
intermediateIndex: 0),
|
||||
let endPoint = MWMRoutePoint(cgPoint: bookmark.mercatorPoint,
|
||||
title: bookmark.prefferedName,
|
||||
subtitle: bookmark.address,
|
||||
type: .finish,
|
||||
intermediateIndex: 0) {
|
||||
let trip = router.createTrip(startPoint: startPoint, endPoint: endPoint)
|
||||
if router.currentTrip == nil {
|
||||
preparePreview(trips: [trip])
|
||||
} else {
|
||||
showRerouteAlert(trips: [trip])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func preparePreview(trips: [CPTrip]) {
|
||||
let mapTemplate = MapTemplateBuilder.buildTripPreviewTemplate(forTrips: trips)
|
||||
if let interfaceController = interfaceController {
|
||||
mapTemplate.mapDelegate = self
|
||||
|
||||
if interfaceController.templates.count > 1 {
|
||||
interfaceController.popToRootTemplate(animated: false)
|
||||
}
|
||||
interfaceController.pushTemplate(mapTemplate, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func showPreview(mapTemplate: CPMapTemplate, trips: [CPTrip]) {
|
||||
let tripTextConfig = CPTripPreviewTextConfiguration(startButtonTitle: L("trip_start"),
|
||||
additionalRoutesButtonTitle: nil,
|
||||
overviewButtonTitle: nil)
|
||||
mapTemplate.showTripPreviews(trips, textConfiguration: tripTextConfig)
|
||||
}
|
||||
|
||||
func createEstimates(routeInfo: RouteInfo) -> CPTravelEstimates? {
|
||||
let measurement = Measurement(value: routeInfo.targetDistance, unit: routeInfo.targetUnits)
|
||||
return CPTravelEstimates(distanceRemaining: measurement, timeRemaining: routeInfo.timeToTarget)
|
||||
}
|
||||
|
||||
func applyUndefinedEstimates(template: CPMapTemplate, trip: CPTrip) {
|
||||
let measurement = Measurement(value: -1,
|
||||
unit: UnitLength.meters)
|
||||
let estimates = CPTravelEstimates(distanceRemaining: measurement,
|
||||
timeRemaining: -1)
|
||||
template.updateEstimates(estimates, for: trip)
|
||||
}
|
||||
|
||||
func showRerouteAlert(trips: [CPTrip]) {
|
||||
let yesAction = CPAlertAction(title: L("yes"), style: .default, handler: { [unowned self] _ in
|
||||
self.router?.cancelTrip()
|
||||
self.updateMapTemplateUIToBase()
|
||||
self.preparedToPreviewTrips = trips
|
||||
self.interfaceController?.dismissTemplate(animated: true)
|
||||
})
|
||||
let noAction = CPAlertAction(title: L("no"), style: .cancel, handler: { [unowned self] _ in
|
||||
self.interfaceController?.dismissTemplate(animated: true)
|
||||
})
|
||||
let alert = CPAlertTemplate(titleVariants: [L("redirect_route_alert")], actions: [noAction, yesAction])
|
||||
alert.userInfo = [CPConstants.TemplateKey.alert: CPConstants.TemplateType.redirectRoute]
|
||||
presentAlert(alert, animated: true)
|
||||
}
|
||||
|
||||
func showKeyboardAlert() {
|
||||
let okAction = CPAlertAction(title: L("ok"), style: .default, handler: { [unowned self] _ in
|
||||
self.interfaceController?.dismissTemplate(animated: true)
|
||||
})
|
||||
let alert = CPAlertTemplate(titleVariants: [L("keyboard_availability_alert")], actions: [okAction])
|
||||
presentAlert(alert, animated: true)
|
||||
}
|
||||
|
||||
func showErrorAlert(code: RouterResultCode, countries: [String]) {
|
||||
var titleVariants = [String]()
|
||||
switch code {
|
||||
case .noCurrentPosition:
|
||||
titleVariants = ["\(L("dialog_routing_check_gps_carplay"))"]
|
||||
case .startPointNotFound:
|
||||
titleVariants = ["\(L("dialog_routing_change_start_carplay"))"]
|
||||
case .endPointNotFound:
|
||||
titleVariants = ["\(L("dialog_routing_change_end_carplay"))"]
|
||||
case .routeNotFoundRedressRouteError,
|
||||
.routeNotFound,
|
||||
.inconsistentMWMandRoute:
|
||||
titleVariants = ["\(L("dialog_routing_unable_locate_route_carplay"))"]
|
||||
case .routeFileNotExist,
|
||||
.fileTooOld,
|
||||
.needMoreMaps,
|
||||
.pointsInDifferentMWM:
|
||||
titleVariants = ["\(L("dialog_routing_download_files_carplay"))"]
|
||||
case .internalError,
|
||||
.intermediatePointNotFound:
|
||||
titleVariants = ["\(L("dialog_routing_system_error_carplay"))"]
|
||||
case .noError,
|
||||
.cancelled,
|
||||
.hasWarnings,
|
||||
.transitRouteNotFoundNoNetwork,
|
||||
.transitRouteNotFoundTooLongPedestrian:
|
||||
return
|
||||
}
|
||||
|
||||
let okAction = CPAlertAction(title: L("ok"), style: .cancel, handler: { [unowned self] _ in
|
||||
self.interfaceController?.dismissTemplate(animated: true)
|
||||
})
|
||||
let alert = CPAlertTemplate(titleVariants: titleVariants, actions: [okAction])
|
||||
presentAlert(alert, animated: true)
|
||||
}
|
||||
|
||||
func showRecoverRouteAlert(trip: CPTrip, isTypeCorrect: Bool) {
|
||||
let yesAction = CPAlertAction(title: L("ok"), style: .default, handler: { [unowned self] _ in
|
||||
var info = trip.userInfo as? [String: MWMRoutePoint]
|
||||
|
||||
if let startPoint = MWMRoutePoint(lastLocationAndType: .start,
|
||||
intermediateIndex: 0) {
|
||||
info?[CPConstants.Trip.start] = startPoint
|
||||
}
|
||||
trip.userInfo = info
|
||||
self.preparedToPreviewTrips = [trip]
|
||||
self.router?.updateStartPointAndRebuild(trip: trip)
|
||||
self.interfaceController?.dismissTemplate(animated: true)
|
||||
})
|
||||
let noAction = CPAlertAction(title: L("cancel"), style: .cancel, handler: { [unowned self] _ in
|
||||
FrameworkHelper.rotateMap(0.0, animated: false)
|
||||
self.router?.completeRouteAndRemovePoints()
|
||||
self.interfaceController?.dismissTemplate(animated: true)
|
||||
})
|
||||
let title = isTypeCorrect ? L("dialog_routing_rebuild_from_current_location_carplay") : L("dialog_routing_rebuild_for_vehicle_carplay")
|
||||
let alert = CPAlertTemplate(titleVariants: [title], actions: [noAction, yesAction])
|
||||
alert.userInfo = [CPConstants.TemplateKey.alert: CPConstants.TemplateType.restoreRoute]
|
||||
presentAlert(alert, animated: true)
|
||||
}
|
||||
}
|
||||
51
iphone/Maps/Classes/CarPlay/CarPlayWindowScaleAdjuster.swift
Normal file
51
iphone/Maps/Classes/CarPlay/CarPlayWindowScaleAdjuster.swift
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum CarPlayWindowScaleAdjuster {
|
||||
|
||||
static func updateAppearance(
|
||||
fromWindow sourceWindow: UIWindow,
|
||||
toWindow destinationWindow: UIWindow,
|
||||
isCarplayActivated: Bool
|
||||
) {
|
||||
|
||||
let sourceContentScale = sourceWindow.screen.scale;
|
||||
let destinationContentScale = destinationWindow.screen.scale;
|
||||
|
||||
if abs(sourceContentScale - destinationContentScale) > 0.1 {
|
||||
if isCarplayActivated {
|
||||
updateVisualScale(to: destinationContentScale)
|
||||
} else {
|
||||
updateVisualScaleToMain()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func updateVisualScale(to scale: CGFloat) {
|
||||
if isGraphicContextInitialized {
|
||||
mapViewController?.mapView.updateVisualScale(to: scale)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
updateVisualScale(to: scale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func updateVisualScaleToMain() {
|
||||
if isGraphicContextInitialized {
|
||||
mapViewController?.mapView.updateVisualScaleToMain()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
updateVisualScaleToMain()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static var isGraphicContextInitialized: Bool {
|
||||
return mapViewController?.mapView.graphicContextInitialized ?? false
|
||||
}
|
||||
|
||||
private static var mapViewController: MapViewController? {
|
||||
return MapViewController.shared()
|
||||
}
|
||||
}
|
||||
93
iphone/Maps/Classes/CarPlay/CarplayPlaceholderView.swift
Normal file
93
iphone/Maps/Classes/CarPlay/CarplayPlaceholderView.swift
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import Foundation
|
||||
|
||||
class CarplayPlaceholderView: UIView {
|
||||
private let containerView = UIView()
|
||||
private let imageView = UIImageView()
|
||||
private let descriptionLabel = UILabel()
|
||||
private let switchButton = UIButton(type: .system);
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
addSubview(containerView)
|
||||
|
||||
imageView.image = UIImage(named: "ic_carplay_activated")
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
containerView.addSubview(imageView)
|
||||
|
||||
descriptionLabel.text = L("car_used_on_the_car_screen")
|
||||
descriptionLabel.font = UIFont.bold24()
|
||||
descriptionLabel.textAlignment = .center
|
||||
descriptionLabel.numberOfLines = 0
|
||||
containerView.addSubview(descriptionLabel)
|
||||
|
||||
switchButton.setTitle(L("car_continue_on_the_phone"), for: .normal)
|
||||
switchButton.addTarget(self, action: #selector(onSwitchButtonTap), for: .touchUpInside)
|
||||
switchButton.titleLabel?.font = UIFont.medium16()
|
||||
switchButton.titleLabel?.lineBreakMode = .byWordWrapping
|
||||
switchButton.titleLabel?.textAlignment = .center
|
||||
switchButton.layer.cornerRadius = 8
|
||||
containerView.addSubview(switchButton)
|
||||
|
||||
updateColors()
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
override func applyTheme() {
|
||||
super.applyTheme()
|
||||
updateColors()
|
||||
}
|
||||
|
||||
private func updateColors() {
|
||||
backgroundColor = UIColor.carplayPlaceholderBackground()
|
||||
descriptionLabel.textColor = UIColor.blackSecondaryText()
|
||||
switchButton.backgroundColor = UIColor.linkBlue()
|
||||
switchButton.setTitleColor(UIColor.whitePrimaryText(), for: .normal)
|
||||
}
|
||||
|
||||
@objc private func onSwitchButtonTap(_ sender: UIButton) {
|
||||
CarPlayService.shared.showOnPhone()
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
switchButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let horizontalPadding: CGFloat = 24
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
containerView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
containerView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor),
|
||||
containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
containerView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
|
||||
|
||||
imageView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
imageView.widthAnchor.constraint(equalToConstant: 160),
|
||||
imageView.heightAnchor.constraint(equalToConstant: 160),
|
||||
|
||||
descriptionLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 32),
|
||||
descriptionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalPadding),
|
||||
descriptionLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalPadding),
|
||||
|
||||
switchButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 24),
|
||||
switchButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalPadding),
|
||||
switchButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalPadding),
|
||||
switchButton.heightAnchor.constraint(equalToConstant: 48),
|
||||
switchButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
}
|
||||
13
iphone/Maps/Classes/CarPlay/MWMCarPlaySearchResultObject.h
Normal file
13
iphone/Maps/Classes/CarPlay/MWMCarPlaySearchResultObject.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MWMCarPlaySearchResultObject : NSObject
|
||||
@property(assign, nonatomic, readonly) NSInteger originalRow;
|
||||
@property(strong, nonatomic, readonly) NSString *title;
|
||||
@property(strong, nonatomic, readonly) NSString *address;
|
||||
@property(assign, nonatomic, readonly) CLLocationCoordinate2D coordinate;
|
||||
@property(assign, nonatomic, readonly) CGPoint mercatorPoint;
|
||||
|
||||
- (instancetype)initForRow:(NSInteger)row;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
43
iphone/Maps/Classes/CarPlay/MWMCarPlaySearchResultObject.mm
Normal file
43
iphone/Maps/Classes/CarPlay/MWMCarPlaySearchResultObject.mm
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#import "MWMCarPlaySearchResultObject.h"
|
||||
#import "MWMSearch.h"
|
||||
#import "SearchResult.h"
|
||||
#import "SwiftBridge.h"
|
||||
|
||||
#include "search/result.hpp"
|
||||
|
||||
#include "indexer/classificator.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "platform/localization.hpp"
|
||||
|
||||
@interface MWMCarPlaySearchResultObject()
|
||||
@property(assign, nonatomic, readwrite) NSInteger originalRow;
|
||||
@property(strong, nonatomic, readwrite) NSString *title;
|
||||
@property(strong, nonatomic, readwrite) NSString *address;
|
||||
@property(assign, nonatomic, readwrite) CLLocationCoordinate2D coordinate;
|
||||
@property(assign, nonatomic, readwrite) CGPoint mercatorPoint;
|
||||
@end
|
||||
|
||||
@implementation MWMCarPlaySearchResultObject
|
||||
|
||||
- (instancetype)initForRow:(NSInteger)row {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.originalRow = row;
|
||||
NSInteger containerIndex = [MWMSearch containerIndexWithRow:row];
|
||||
SearchItemType type = [MWMSearch resultTypeWithRow:row];
|
||||
if (type == SearchItemTypeRegular) {
|
||||
auto const & result = [MWMSearch resultWithContainerIndex:containerIndex];
|
||||
self.title = result.titleText;
|
||||
self.address = result.addressText;
|
||||
self.coordinate = result.coordinate;
|
||||
auto const pivot = mercator::FromLatLon(result.coordinate.latitude, result.coordinate.longitude);
|
||||
self.mercatorPoint = CGPointMake(pivot.x, pivot.y);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
17
iphone/Maps/Classes/CarPlay/MWMCarPlaySearchService.h
Normal file
17
iphone/Maps/Classes/CarPlay/MWMCarPlaySearchService.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
@class MWMCarPlaySearchResultObject;
|
||||
|
||||
API_AVAILABLE(ios(12.0))
|
||||
NS_SWIFT_NAME(CarPlaySearchService)
|
||||
@interface MWMCarPlaySearchService : NSObject
|
||||
@property(strong, nonatomic, readonly) NSArray<MWMCarPlaySearchResultObject *> *lastResults;
|
||||
|
||||
- (instancetype)init;
|
||||
|
||||
- (void)searchText:(NSString *)text
|
||||
forInputLocale:(NSString *)inputLocale
|
||||
completionHandler:(void (^)(NSArray<MWMCarPlaySearchResultObject *> *searchResults))completionHandler;
|
||||
- (void)saveLastQuery;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
65
iphone/Maps/Classes/CarPlay/MWMCarPlaySearchService.mm
Normal file
65
iphone/Maps/Classes/CarPlay/MWMCarPlaySearchService.mm
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#import "MWMCarPlaySearchService.h"
|
||||
#import "MWMCarPlaySearchResultObject.h"
|
||||
#import "MWMSearch.h"
|
||||
|
||||
#import "SwiftBridge.h"
|
||||
|
||||
API_AVAILABLE(ios(12.0))
|
||||
@interface MWMCarPlaySearchService ()<MWMSearchObserver>
|
||||
@property(strong, nonatomic, nullable) void (^completionHandler)(NSArray<MWMCarPlaySearchResultObject *> *searchResults);
|
||||
@property(strong, nonatomic, nullable) NSString *lastQuery;
|
||||
@property(strong, nonatomic, nullable) NSString *inputLocale;
|
||||
@property(strong, nonatomic, readwrite) NSArray<MWMCarPlaySearchResultObject *> *lastResults;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMCarPlaySearchService
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
[MWMSearch addObserver:self];
|
||||
self.lastResults = @[];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)searchText:(NSString *)text
|
||||
forInputLocale:(NSString *)inputLocale
|
||||
completionHandler:(void (^)(NSArray<MWMCarPlaySearchResultObject *> *searchResults))completionHandler {
|
||||
self.lastQuery = text;
|
||||
self.inputLocale = inputLocale;
|
||||
self.lastResults = @[];
|
||||
self.completionHandler = completionHandler;
|
||||
/// @todo Didn't find pure category request in CarPlay.
|
||||
[MWMSearch setSearchMode:SearchModeEverywhere];
|
||||
SearchQuery * query = [[SearchQuery alloc] init:text locale:inputLocale source:SearchTextSourceTypedText];
|
||||
[MWMSearch searchQuery:query];
|
||||
}
|
||||
|
||||
- (void)saveLastQuery {
|
||||
if (self.lastQuery != nil && self.inputLocale != nil) {
|
||||
SearchQuery * query = [[SearchQuery alloc] init:self.lastQuery locale:self.inputLocale source:SearchTextSourceTypedText];
|
||||
[MWMSearch saveQuery:query];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - MWMSearchObserver
|
||||
|
||||
- (void)onSearchCompleted {
|
||||
void (^completionHandler)(NSArray<MWMCarPlaySearchResultObject *> *searchResults) = self.completionHandler;
|
||||
if (completionHandler == nil) { return; }
|
||||
|
||||
NSMutableArray<MWMCarPlaySearchResultObject *> *results = [NSMutableArray array];
|
||||
NSInteger count = [MWMSearch resultsCount];
|
||||
for (NSInteger row = 0; row < count; row++) {
|
||||
MWMCarPlaySearchResultObject *result = [[MWMCarPlaySearchResultObject alloc] initForRow:row];
|
||||
if (result != nil) { [results addObject:result]; }
|
||||
}
|
||||
|
||||
self.lastResults = results;
|
||||
completionHandler(results);
|
||||
self.completionHandler = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import CarPlay
|
||||
|
||||
final class ListTemplateBuilder {
|
||||
enum ListTemplateType {
|
||||
case history
|
||||
case bookmarkLists
|
||||
case bookmarks(category: BookmarkGroup)
|
||||
case searchResults(results: [MWMCarPlaySearchResultObject])
|
||||
}
|
||||
|
||||
enum BarButtonType {
|
||||
case bookmarks
|
||||
case search
|
||||
}
|
||||
|
||||
// MARK: - CPListTemplate builder
|
||||
class func buildListTemplate(for type: ListTemplateType) -> CPListTemplate {
|
||||
var title = ""
|
||||
var trailingNavigationBarButtons = [CPBarButton]()
|
||||
switch type {
|
||||
case .history:
|
||||
title = L("pick_destination")
|
||||
let bookmarksButton = buildBarButton(type: .bookmarks) { _ in
|
||||
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .bookmarkLists)
|
||||
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
|
||||
}
|
||||
let searchButton = buildBarButton(type: .search) { _ in
|
||||
if CarPlayService.shared.isKeyboardLimited {
|
||||
CarPlayService.shared.showKeyboardAlert()
|
||||
} else {
|
||||
let searchTemplate = SearchTemplateBuilder.buildSearchTemplate()
|
||||
CarPlayService.shared.pushTemplate(searchTemplate, animated: true)
|
||||
}
|
||||
}
|
||||
trailingNavigationBarButtons = [searchButton, bookmarksButton]
|
||||
case .bookmarkLists:
|
||||
title = L("bookmarks")
|
||||
case .searchResults:
|
||||
title = L("search_results")
|
||||
case .bookmarks(let category):
|
||||
title = category.title
|
||||
}
|
||||
|
||||
let sections = buildSectionsForType(type)
|
||||
let template = CPListTemplate(title: title, sections: sections)
|
||||
template.trailingNavigationBarButtons = trailingNavigationBarButtons
|
||||
return template
|
||||
}
|
||||
|
||||
private class func buildSectionsForType(_ type: ListTemplateType) -> [CPListSection] {
|
||||
switch type {
|
||||
case .history:
|
||||
return buildHistorySections()
|
||||
case .bookmarks(let category):
|
||||
return buildBookmarksSections(categoryId: category.categoryId)
|
||||
case .bookmarkLists:
|
||||
return buildBookmarkListsSections()
|
||||
case .searchResults(let results):
|
||||
return buildSearchResultsSections(results)
|
||||
}
|
||||
}
|
||||
|
||||
private class func buildHistorySections() -> [CPListSection] {
|
||||
let searchQueries = FrameworkHelper.obtainLastSearchQueries()
|
||||
let items = searchQueries.map({ (text) -> CPListItem in
|
||||
let item = CPListItem(text: text, detailText: nil, image: UIImage(named: "recent"))
|
||||
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.history,
|
||||
metadata: nil)
|
||||
return item
|
||||
})
|
||||
return [CPListSection(items: items)]
|
||||
}
|
||||
|
||||
private class func buildBookmarkListsSections() -> [CPListSection] {
|
||||
let bookmarkManager = BookmarksManager.shared()
|
||||
let categories = bookmarkManager.sortedUserCategories()
|
||||
let items: [CPListItem] = categories.compactMap({ category in
|
||||
if category.bookmarksCount == 0 { return nil }
|
||||
let placesString = category.placesCountTitle()
|
||||
let item = CPListItem(text: category.title, detailText: placesString)
|
||||
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.bookmarkLists,
|
||||
metadata: CategoryInfo(category: category))
|
||||
return item
|
||||
})
|
||||
return [CPListSection(items: items)]
|
||||
}
|
||||
|
||||
private class func buildBookmarksSections(categoryId: MWMMarkGroupID) -> [CPListSection] {
|
||||
let bookmarkManager = BookmarksManager.shared()
|
||||
let bookmarks = bookmarkManager.bookmarks(forCategory: categoryId)
|
||||
var items = bookmarks.map({ (bookmark) -> CPListItem in
|
||||
let item = CPListItem(text: bookmark.prefferedName, detailText: bookmark.address)
|
||||
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.bookmarks,
|
||||
metadata: BookmarkInfo(categoryId: categoryId,
|
||||
bookmarkId: bookmark.bookmarkId))
|
||||
return item
|
||||
})
|
||||
let maxItemCount = CPListTemplate.maximumItemCount - 1
|
||||
if items.count >= maxItemCount {
|
||||
items = Array(items.prefix(maxItemCount))
|
||||
let cropWarning = CPListItem(text: L("not_all_shown_bookmarks_carplay"), detailText: L("switch_to_phone_bookmarks_carplay"))
|
||||
cropWarning.isEnabled = false
|
||||
items.append(cropWarning)
|
||||
}
|
||||
return [CPListSection(items: items)]
|
||||
}
|
||||
|
||||
private class func buildSearchResultsSections(_ results: [MWMCarPlaySearchResultObject]) -> [CPListSection] {
|
||||
var items = [CPListItem]()
|
||||
for object in results {
|
||||
let item = CPListItem(text: object.title, detailText: object.address)
|
||||
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.searchResults,
|
||||
metadata: SearchResultInfo(originalRow: object.originalRow))
|
||||
items.append(item)
|
||||
}
|
||||
return [CPListSection(items: items)]
|
||||
}
|
||||
|
||||
|
||||
// MARK: - CPBarButton builder
|
||||
private class func buildBarButton(type: BarButtonType, action: ((CPBarButton) -> Void)?) -> CPBarButton {
|
||||
switch type {
|
||||
case .bookmarks:
|
||||
return CPBarButton(image: UIImage(systemName: "list.star")!, handler: action)
|
||||
case .search:
|
||||
return CPBarButton(image: UIImage(systemName: "keyboard.fill")!, handler: action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
import CarPlay
|
||||
|
||||
final class MapTemplateBuilder {
|
||||
enum MapButtonType {
|
||||
case startPanning
|
||||
case zoomIn
|
||||
case zoomOut
|
||||
}
|
||||
enum BarButtonType {
|
||||
case dismissPaning
|
||||
case destination
|
||||
case recenter
|
||||
case settings
|
||||
case mute
|
||||
case unmute
|
||||
case redirectRoute
|
||||
case endRoute
|
||||
}
|
||||
|
||||
private enum Constants {
|
||||
static let carPlayGuidanceBackgroundColor = UIColor(46, 100, 51, 1.0)
|
||||
}
|
||||
|
||||
// MARK: - CPMapTemplate builders
|
||||
class func buildBaseTemplate(positionMode: MWMMyPositionMode) -> CPMapTemplate {
|
||||
let mapTemplate = CPMapTemplate()
|
||||
mapTemplate.hidesButtonsWithNavigationBar = false
|
||||
configureBaseUI(mapTemplate: mapTemplate)
|
||||
if positionMode == .pendingPosition {
|
||||
mapTemplate.leadingNavigationBarButtons = []
|
||||
} else if positionMode == .follow || positionMode == .followAndRotate {
|
||||
setupDestinationButton(mapTemplate: mapTemplate)
|
||||
} else {
|
||||
setupRecenterButton(mapTemplate: mapTemplate)
|
||||
}
|
||||
return mapTemplate
|
||||
}
|
||||
|
||||
class func buildNavigationTemplate() -> CPMapTemplate {
|
||||
let mapTemplate = CPMapTemplate()
|
||||
mapTemplate.hidesButtonsWithNavigationBar = false
|
||||
configureNavigationUI(mapTemplate: mapTemplate)
|
||||
return mapTemplate
|
||||
}
|
||||
|
||||
class func buildTripPreviewTemplate(forTrips trips: [CPTrip]) -> CPMapTemplate {
|
||||
let mapTemplate = CPMapTemplate()
|
||||
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.preview, trips: trips)
|
||||
mapTemplate.mapButtons = []
|
||||
mapTemplate.leadingNavigationBarButtons = []
|
||||
let settingsButton = buildBarButton(type: .settings) { _ in
|
||||
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.previewSettings)
|
||||
let gridTemplate = SettingsTemplateBuilder.buildGridTemplate()
|
||||
CarPlayService.shared.pushTemplate(gridTemplate, animated: true)
|
||||
}
|
||||
mapTemplate.trailingNavigationBarButtons = [settingsButton]
|
||||
return mapTemplate
|
||||
}
|
||||
|
||||
// MARK: - MapTemplate UI configs
|
||||
class func configureBaseUI(mapTemplate: CPMapTemplate) {
|
||||
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.main)
|
||||
let panningButton = buildMapButton(type: .startPanning) { _ in
|
||||
mapTemplate.showPanningInterface(animated: true)
|
||||
}
|
||||
let zoomInButton = buildMapButton(type: .zoomIn) { _ in
|
||||
FrameworkHelper.zoomMap(.in)
|
||||
}
|
||||
let zoomOutButton = buildMapButton(type: .zoomOut) { _ in
|
||||
FrameworkHelper.zoomMap(.out)
|
||||
}
|
||||
mapTemplate.mapButtons = [panningButton, zoomInButton, zoomOutButton]
|
||||
|
||||
let settingsButton = buildBarButton(type: .settings) { _ in
|
||||
let gridTemplate = SettingsTemplateBuilder.buildGridTemplate()
|
||||
CarPlayService.shared.pushTemplate(gridTemplate, animated: true)
|
||||
}
|
||||
mapTemplate.trailingNavigationBarButtons = [settingsButton]
|
||||
}
|
||||
|
||||
class func configurePanUI(mapTemplate: CPMapTemplate) {
|
||||
let zoomInButton = buildMapButton(type: .zoomIn) { _ in
|
||||
FrameworkHelper.zoomMap(.in)
|
||||
}
|
||||
let zoomOutButton = buildMapButton(type: .zoomOut) { _ in
|
||||
FrameworkHelper.zoomMap(.out)
|
||||
}
|
||||
mapTemplate.mapButtons = [zoomInButton, zoomOutButton]
|
||||
|
||||
let doneButton = buildBarButton(type: .dismissPaning) { _ in
|
||||
mapTemplate.dismissPanningInterface(animated: true)
|
||||
}
|
||||
mapTemplate.leadingNavigationBarButtons = []
|
||||
mapTemplate.trailingNavigationBarButtons = [doneButton]
|
||||
}
|
||||
|
||||
class func configureNavigationUI(mapTemplate: CPMapTemplate) {
|
||||
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.navigation)
|
||||
let panningButton = buildMapButton(type: .startPanning) { _ in
|
||||
mapTemplate.showPanningInterface(animated: true)
|
||||
}
|
||||
mapTemplate.mapButtons = [panningButton]
|
||||
setupMuteAndRedirectButtons(template: mapTemplate)
|
||||
let endButton = buildBarButton(type: .endRoute) { _ in
|
||||
CarPlayService.shared.cancelCurrentTrip()
|
||||
}
|
||||
mapTemplate.trailingNavigationBarButtons = [endButton]
|
||||
mapTemplate.guidanceBackgroundColor = Constants.carPlayGuidanceBackgroundColor
|
||||
}
|
||||
|
||||
// MARK: - Conditional navigation buttons
|
||||
class func setupDestinationButton(mapTemplate: CPMapTemplate) {
|
||||
let destinationButton = buildBarButton(type: .destination) { _ in
|
||||
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .history)
|
||||
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
|
||||
}
|
||||
mapTemplate.leadingNavigationBarButtons = [destinationButton]
|
||||
}
|
||||
|
||||
class func setupRecenterButton(mapTemplate: CPMapTemplate) {
|
||||
let recenterButton = buildBarButton(type: .recenter) { _ in
|
||||
FrameworkHelper.switchMyPositionMode()
|
||||
}
|
||||
mapTemplate.leadingNavigationBarButtons = [recenterButton]
|
||||
}
|
||||
|
||||
private class func setupMuteAndRedirectButtons(template: CPMapTemplate) {
|
||||
let redirectButton = buildBarButton(type: .redirectRoute) { _ in
|
||||
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .history)
|
||||
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
|
||||
}
|
||||
if MWMTextToSpeech.isTTSEnabled() {
|
||||
let muteButton = buildBarButton(type: .mute) { _ in
|
||||
MWMTextToSpeech.tts().active = false
|
||||
setupUnmuteAndRedirectButtons(template: template)
|
||||
}
|
||||
template.leadingNavigationBarButtons = [muteButton, redirectButton]
|
||||
} else {
|
||||
template.leadingNavigationBarButtons = [redirectButton]
|
||||
}
|
||||
}
|
||||
|
||||
private class func setupUnmuteAndRedirectButtons(template: CPMapTemplate) {
|
||||
let redirectButton = buildBarButton(type: .redirectRoute) { _ in
|
||||
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .history)
|
||||
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
|
||||
}
|
||||
if MWMTextToSpeech.isTTSEnabled() {
|
||||
let unmuteButton = buildBarButton(type: .unmute) { _ in
|
||||
MWMTextToSpeech.tts().active = true
|
||||
setupMuteAndRedirectButtons(template: template)
|
||||
}
|
||||
template.leadingNavigationBarButtons = [unmuteButton, redirectButton]
|
||||
} else {
|
||||
template.leadingNavigationBarButtons = [redirectButton]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CPMapButton builder
|
||||
private class func buildMapButton(type: MapButtonType, action: ((CPMapButton) -> Void)?) -> CPMapButton {
|
||||
let button = CPMapButton(handler: action)
|
||||
switch type {
|
||||
case .startPanning:
|
||||
button.image = UIImage(systemName: "arrow.up.and.down.and.arrow.left.and.right")
|
||||
case .zoomIn:
|
||||
button.image = UIImage(systemName: "plus")
|
||||
case .zoomOut:
|
||||
button.image = UIImage(systemName: "minus")
|
||||
}
|
||||
// Remove code below once Apple has fixed its issue with the button background
|
||||
if #unavailable(iOS 26) {
|
||||
switch type {
|
||||
case .startPanning:
|
||||
button.focusedImage = UIImage(systemName: "smallcircle.filled.circle.fill")
|
||||
case .zoomIn:
|
||||
button.focusedImage = UIImage(systemName: "plus.circle.fill")
|
||||
case .zoomOut:
|
||||
button.focusedImage = UIImage(systemName: "minus.circle.fill")
|
||||
}
|
||||
}
|
||||
return button
|
||||
}
|
||||
|
||||
// MARK: - CPBarButton builder
|
||||
private class func buildBarButton(type: BarButtonType, action: ((CPBarButton) -> Void)?) -> CPBarButton {
|
||||
switch type {
|
||||
case .dismissPaning:
|
||||
return CPBarButton(title: L("done"), handler: action)
|
||||
case .destination:
|
||||
return CPBarButton(title: L("pick_destination"), handler: action)
|
||||
case .recenter:
|
||||
return CPBarButton(title: L("follow_my_position"), handler: action)
|
||||
case .settings:
|
||||
return CPBarButton(image: UIImage(systemName: "gearshape.fill")!, handler: action)
|
||||
case .mute:
|
||||
return CPBarButton(image: UIImage(systemName: "speaker.wave.3")!, handler: action)
|
||||
case .unmute:
|
||||
return CPBarButton(image: UIImage(systemName: "speaker.slash")!, handler: action)
|
||||
case .redirectRoute:
|
||||
return CPBarButton(image: UIImage(named: "ic_carplay_redirect_route")!, handler: action)
|
||||
case .endRoute:
|
||||
return CPBarButton(title: L("navigation_stop_button").capitalized, handler: action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import CarPlay
|
||||
|
||||
final class SearchTemplateBuilder {
|
||||
// MARK: - CPSearchTemplate builder
|
||||
class func buildSearchTemplate() -> CPSearchTemplate {
|
||||
let template = CPSearchTemplate()
|
||||
return template
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import CarPlay
|
||||
|
||||
final class SettingsTemplateBuilder {
|
||||
// MARK: - CPGridTemplate builder
|
||||
class func buildGridTemplate() -> CPGridTemplate {
|
||||
let actions = SettingsTemplateBuilder.buildGridButtons()
|
||||
let gridTemplate = CPGridTemplate(title: L("settings"),
|
||||
gridButtons: actions)
|
||||
|
||||
return gridTemplate
|
||||
}
|
||||
|
||||
private class func buildGridButtons() -> [CPGridButton] {
|
||||
let options = RoutingOptions()
|
||||
return [createTollButton(options: options),
|
||||
createUnpavedButton(options: options),
|
||||
createPavedButton(options: options),
|
||||
createMotorwayButton(options: options),
|
||||
createFerryButton(options: options),
|
||||
createStepsButton(options: options),
|
||||
createSpeedcamButton()]
|
||||
}
|
||||
|
||||
// MARK: - CPGridButton builders
|
||||
private class func createTollButton(options: RoutingOptions) -> CPGridButton {
|
||||
var tollIconName = "tolls.circle"
|
||||
if options.avoidToll { tollIconName += ".slash" }
|
||||
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
|
||||
var image = UIImage(named: tollIconName, in: nil, with: configuration)!
|
||||
if #unavailable(iOS 26) {
|
||||
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
|
||||
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
let tollButton = CPGridButton(titleVariants: [L("avoid_tolls")], image: image) { _ in
|
||||
options.avoidToll = !options.avoidToll
|
||||
options.save()
|
||||
CarPlayService.shared.updateRouteAfterChangingSettings()
|
||||
CarPlayService.shared.popTemplate(animated: true)
|
||||
}
|
||||
return tollButton
|
||||
}
|
||||
|
||||
private class func createUnpavedButton(options: RoutingOptions) -> CPGridButton {
|
||||
var unpavedIconName = "unpaved.circle"
|
||||
if options.avoidDirty && !options.avoidPaved { unpavedIconName += ".slash" }
|
||||
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
|
||||
var image = UIImage(named: unpavedIconName, in: nil, with: configuration)!
|
||||
if #unavailable(iOS 26) {
|
||||
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
|
||||
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
let unpavedButton = CPGridButton(titleVariants: [L("avoid_unpaved")], image: image) { _ in
|
||||
options.avoidDirty = !options.avoidDirty
|
||||
if options.avoidDirty {
|
||||
options.avoidPaved = false
|
||||
}
|
||||
options.save()
|
||||
CarPlayService.shared.updateRouteAfterChangingSettings()
|
||||
CarPlayService.shared.popTemplate(animated: true)
|
||||
}
|
||||
unpavedButton.isEnabled = !options.avoidPaved
|
||||
return unpavedButton
|
||||
}
|
||||
|
||||
private class func createPavedButton(options: RoutingOptions) -> CPGridButton {
|
||||
var pavedIconName = "paved.circle"
|
||||
if options.avoidPaved && !options.avoidDirty { pavedIconName += ".slash" }
|
||||
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
|
||||
var image = UIImage(named: pavedIconName, in: nil, with: configuration)!
|
||||
if #unavailable(iOS 26) {
|
||||
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
|
||||
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
let pavedButton = CPGridButton(titleVariants: [L("avoid_paved")], image: image) { _ in
|
||||
options.avoidPaved = !options.avoidPaved
|
||||
if options.avoidPaved {
|
||||
options.avoidDirty = false
|
||||
}
|
||||
options.save()
|
||||
CarPlayService.shared.updateRouteAfterChangingSettings()
|
||||
CarPlayService.shared.popTemplate(animated: true)
|
||||
}
|
||||
pavedButton.isEnabled = !options.avoidDirty
|
||||
return pavedButton
|
||||
}
|
||||
|
||||
private class func createMotorwayButton(options: RoutingOptions) -> CPGridButton {
|
||||
var motorwayIconName = "motorways.circle"
|
||||
if options.avoidMotorway { motorwayIconName += ".slash" }
|
||||
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
|
||||
var image = UIImage(named: motorwayIconName, in: nil, with: configuration)!
|
||||
if #unavailable(iOS 26) {
|
||||
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
|
||||
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
let motorwayButton = CPGridButton(titleVariants: [L("avoid_motorways")], image: image) { _ in
|
||||
options.avoidMotorway = !options.avoidMotorway
|
||||
options.save()
|
||||
CarPlayService.shared.updateRouteAfterChangingSettings()
|
||||
CarPlayService.shared.popTemplate(animated: true)
|
||||
}
|
||||
return motorwayButton
|
||||
}
|
||||
|
||||
private class func createFerryButton(options: RoutingOptions) -> CPGridButton {
|
||||
var ferryIconName = "ferries.circle"
|
||||
if options.avoidFerry { ferryIconName += ".slash" }
|
||||
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
|
||||
var image = UIImage(named: ferryIconName, in: nil, with: configuration)!
|
||||
if #unavailable(iOS 26) {
|
||||
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
|
||||
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
let ferryButton = CPGridButton(titleVariants: [L("avoid_ferry")], image: image) { _ in
|
||||
options.avoidFerry = !options.avoidFerry
|
||||
options.save()
|
||||
CarPlayService.shared.updateRouteAfterChangingSettings()
|
||||
CarPlayService.shared.popTemplate(animated: true)
|
||||
}
|
||||
return ferryButton
|
||||
}
|
||||
|
||||
private class func createStepsButton(options: RoutingOptions) -> CPGridButton {
|
||||
var stepsIconName = "steps.circle"
|
||||
if options.avoidSteps { stepsIconName += ".slash" }
|
||||
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
|
||||
var image = UIImage(named: stepsIconName, in: nil, with: configuration)!
|
||||
if #unavailable(iOS 26) {
|
||||
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
|
||||
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
let stepsButton = CPGridButton(titleVariants: [L("avoid_steps")], image: image) { _ in
|
||||
options.avoidSteps = !options.avoidSteps
|
||||
options.save()
|
||||
CarPlayService.shared.updateRouteAfterChangingSettings()
|
||||
CarPlayService.shared.popTemplate(animated: true)
|
||||
}
|
||||
return stepsButton
|
||||
}
|
||||
|
||||
private class func createSpeedcamButton() -> CPGridButton {
|
||||
var speedcamIconName = "speedcamera"
|
||||
let isSpeedCamActivated = CarPlayService.shared.isSpeedCamActivated
|
||||
if !isSpeedCamActivated { speedcamIconName += ".slash" }
|
||||
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
|
||||
var image = UIImage(named: speedcamIconName, in: nil, with: configuration)!
|
||||
if #unavailable(iOS 26) {
|
||||
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
|
||||
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
let speedButton = CPGridButton(titleVariants: [L("speedcams_alert_title_carplay_1"), L("speedcams_alert_title_carplay_2")], image: image) { _ in
|
||||
CarPlayService.shared.isSpeedCamActivated = !isSpeedCamActivated
|
||||
CarPlayService.shared.popTemplate(animated: true)
|
||||
}
|
||||
return speedButton
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
struct BookmarkInfo: InfoMetadata {
|
||||
let categoryId: UInt64
|
||||
let bookmarkId: UInt64
|
||||
|
||||
init(categoryId: UInt64, bookmarkId: UInt64) {
|
||||
self.categoryId = categoryId
|
||||
self.bookmarkId = bookmarkId
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
struct CategoryInfo: InfoMetadata {
|
||||
let category: BookmarkGroup
|
||||
|
||||
init(category: BookmarkGroup) {
|
||||
self.category = category
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
protocol InfoMetadata {}
|
||||
|
||||
struct ListItemInfo {
|
||||
let type: String
|
||||
let metadata: InfoMetadata?
|
||||
|
||||
init(type: String, metadata: InfoMetadata?) {
|
||||
self.type = type
|
||||
self.metadata = metadata
|
||||
}
|
||||
}
|
||||
9
iphone/Maps/Classes/CarPlay/Templates Data/MapInfo.swift
Normal file
9
iphone/Maps/Classes/CarPlay/Templates Data/MapInfo.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
struct MapInfo {
|
||||
let type: String
|
||||
let trips: [CPTrip]?
|
||||
|
||||
init(type: String, trips: [CPTrip]? = nil) {
|
||||
self.type = type
|
||||
self.trips = trips
|
||||
}
|
||||
}
|
||||
57
iphone/Maps/Classes/CarPlay/Templates Data/RouteInfo.swift
Normal file
57
iphone/Maps/Classes/CarPlay/Templates Data/RouteInfo.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
@objc(MWMRouteInfo)
|
||||
class RouteInfo: NSObject {
|
||||
let timeToTarget: TimeInterval
|
||||
let targetDistance: Double
|
||||
let targetUnits: UnitLength
|
||||
let distanceToTurn: Double
|
||||
let turnUnits: UnitLength
|
||||
let streetName: String
|
||||
let turnImageName: String?
|
||||
let nextTurnImageName: String?
|
||||
let speedMps: Double
|
||||
let speedLimitMps: Double?
|
||||
let roundExitNumber: Int
|
||||
|
||||
@objc init(timeToTarget: TimeInterval,
|
||||
targetDistance: Double,
|
||||
targetUnitsIndex: UInt8,
|
||||
distanceToTurn: Double,
|
||||
turnUnitsIndex: UInt8,
|
||||
streetName: String,
|
||||
turnImageName: String?,
|
||||
nextTurnImageName: String?,
|
||||
speedMps: Double,
|
||||
speedLimitMps: Double,
|
||||
roundExitNumber: Int) {
|
||||
self.timeToTarget = timeToTarget
|
||||
self.targetDistance = targetDistance
|
||||
self.targetUnits = RouteInfo.unitLength(for: targetUnitsIndex)
|
||||
self.distanceToTurn = distanceToTurn
|
||||
self.turnUnits = RouteInfo.unitLength(for: turnUnitsIndex)
|
||||
self.streetName = streetName;
|
||||
self.turnImageName = turnImageName
|
||||
self.nextTurnImageName = nextTurnImageName
|
||||
self.speedMps = speedMps
|
||||
// speedLimitMps >= 0 means known limited speed.
|
||||
self.speedLimitMps = speedLimitMps < 0 ? nil : speedLimitMps
|
||||
self.roundExitNumber = roundExitNumber
|
||||
}
|
||||
|
||||
|
||||
/// > Warning: Order of enum values MUST BE the same with
|
||||
/// > native ``Distance::Units`` enum (see platform/distance.hpp for details).
|
||||
class func unitLength(for targetUnitsIndex: UInt8) -> UnitLength {
|
||||
switch targetUnitsIndex {
|
||||
case 0:
|
||||
return .meters
|
||||
case 1:
|
||||
return .kilometers
|
||||
case 2:
|
||||
return .feet
|
||||
case 3:
|
||||
return .miles
|
||||
default:
|
||||
return .meters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
struct SearchResultInfo: InfoMetadata {
|
||||
let originalRow: Int
|
||||
|
||||
init(originalRow: Int) {
|
||||
self.originalRow = originalRow
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue