Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

View 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"
}
}

View file

@ -0,0 +1,7 @@
import Foundation
enum CPViewPortState: Int {
case `default`
case preview
case navigation
}

View 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()
}
}
}

View 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)
}
}

View 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()
}
}

View 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)
])
}
}

View 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

View 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

View 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

View 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

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -0,0 +1,9 @@
import CarPlay
final class SearchTemplateBuilder {
// MARK: - CPSearchTemplate builder
class func buildSearchTemplate() -> CPSearchTemplate {
let template = CPSearchTemplate()
return template
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,9 @@
struct BookmarkInfo: InfoMetadata {
let categoryId: UInt64
let bookmarkId: UInt64
init(categoryId: UInt64, bookmarkId: UInt64) {
self.categoryId = categoryId
self.bookmarkId = bookmarkId
}
}

View file

@ -0,0 +1,7 @@
struct CategoryInfo: InfoMetadata {
let category: BookmarkGroup
init(category: BookmarkGroup) {
self.category = category
}
}

View file

@ -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
}
}

View 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
}
}

View 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
}
}
}

View file

@ -0,0 +1,7 @@
struct SearchResultInfo: InfoMetadata {
let originalRow: Int
init(originalRow: Int) {
self.originalRow = originalRow
}
}