[iOS][CarPlay] Base 1.0 CarPlay Integration

This commit is contained in:
Zoia Pribytkova 2019-03-01 14:09:01 +03:00 committed by Aleksey Belousov
parent 9015e77f7f
commit 33e5054401
187 changed files with 4084 additions and 283 deletions

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14109" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -53,7 +53,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HgY-nM-JMn">
<rect key="frame" x="16" y="13" width="42" height="21"/>
<rect key="frame" x="16" y="13.5" width="42" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -123,7 +123,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BbP-rE-XH6">
<rect key="frame" x="16" y="13" width="343" height="21"/>
<rect key="frame" x="16" y="13.5" width="343" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>

View file

@ -299,7 +299,7 @@ final class BookmarksSharingViewController: MWMTableViewController {
}
private func performAfterValidation(anchor: UIView, action: @escaping MWMVoidBlock) {
if MWMFrameworkHelper.isNetworkConnected() {
if FrameworkHelper.isNetworkConnected() {
signup(anchor: anchor, onComplete: { success in
if success {
action()

View file

@ -10,6 +10,7 @@
#import "MyTrackerSDK/MRMyTracker.h"
#import "Pushwoosh/PushNotificationManager.h"
#import "UIKit/UIKit.h"
#import "CarPlay/CarPlay.h"
#import "3party/Alohalytics/src/alohalytics_objc.h"
#import "MPNativeAd+MWM.h"
@ -82,9 +83,17 @@
#import "UIViewController+Navigation.h"
#import "WebViewController.h"
#import "MWMCategory.h"
#import "MWMCarPlayBookmarkObject.h"
#import "MWMCatalogCommon.h"
#import "MWMEye.h"
#import "MWMPurchaseManager.h"
#import "MWMPurchaseValidation.h"
#import "MWMTag.h"
#import "MWMTagGroup.h"
#import "EAGLView.h"
#import "MWMCarPlaySearchService.h"
#import "MWMCarPlaySearchResultObject.h"
#import "MWMRoutingManager.h"
#import "MWMRouterResultCode.h"
#import "MWMLocationModeListener.h"
#import "MWMSpeedCameraManagerMode.h"

View file

@ -48,6 +48,11 @@
+ (UIColor *)ratingLightGreen;
+ (UIColor *)ratingGreen;
+ (UIColor *)border;
+ (UIColor *)speedLimitRed;
+ (UIColor *)speedLimitGeen;
+ (UIColor *)speedLimitWhite;
+ (UIColor *)speedLimitLightGray;
+ (UIColor *)speedLimitDarkGray;
+ (UIColor *)colorWithName:(NSString *)colorName;
+ (UIColor *)colorFromHexString:(NSString *)hexString;

View file

@ -374,4 +374,25 @@ UIColor * color(SEL cmd)
+ (UIColor *)facebookButtonBackgroundDisabled {
return [self.facebookButtonBackground colorWithAlphaComponent:alpha70];
}
+ (UIColor *)speedLimitRed {
return [UIColor colorWithRed:scaled(224) green:scaled(31) blue:scaled(31) alpha:alpha100];
}
+ (UIColor *)speedLimitGeen {
return [UIColor colorWithRed:scaled(1) green:scaled(104) blue:scaled(44) alpha:alpha100];
}
+ (UIColor *)speedLimitWhite {
return [UIColor colorWithRed:scaled(255) green:scaled(255) blue:scaled(255) alpha:alpha80];
}
+ (UIColor *)speedLimitLightGray {
return [UIColor colorWithRed:scaled(0) green:scaled(0) blue:scaled(0) alpha:alpha20];
}
+ (UIColor *)speedLimitDarkGray {
return [UIColor colorWithRed:scaled(51) green:scaled(51) blue:scaled(50) alpha:alpha100];
}
@end

View file

@ -1,5 +1,6 @@
static CGFloat const alpha04 = 0.04;
static CGFloat const alpha12 = 0.12;
static CGFloat const alpha20 = 0.20;
static CGFloat const alpha26 = 0.26;
static CGFloat const alpha30 = 0.3;
static CGFloat const alpha32 = 0.32;

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,347 @@
import CarPlay
import Contacts
@available(iOS 12.0, *)
protocol CarPlayRouterListener: class {
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)
}
@available(iOS 12.0, *)
@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
@available(iOS 12.0, *)
extension CarPlayRouter {
func startNavigationSession(forTrip trip: CPTrip, template: CPMapTemplate) {
routeSession = template.startNavigationSession(for: trip)
routeSession?.pauseTrip(for: .loading, description: nil)
updateUpcomingManeuvers()
}
func cancelTrip() {
routeSession?.cancelTrip()
routeSession = nil
completeRouteAndRemovePoints()
}
func finishTrip() {
routeSession?.finishTrip()
routeSession = nil
completeRouteAndRemovePoints()
}
func updateUpcomingManeuvers() {
let maneuvers = createUpcomingManeuvers()
routeSession?.upcomingManeuvers = maneuvers
}
private func createUpcomingManeuvers() -> [CPManeuver] {
guard let routeInfo = RoutingManager.routingManager.routeInfo else {
return []
}
var maneuvers = [CPManeuver]()
let primaryManeuver = CPManeuver()
primaryManeuver.userInfo = CPConstants.Maneuvers.primary
primaryManeuver.instructionVariants = [routeInfo.streetName]
if let imageName = routeInfo.turnImageName,
let symbol = UIImage(named: imageName) {
primaryManeuver.symbolSet = CPImageSet(lightContentImage: symbol,
darkContentImage: symbol)
}
if let distance = Double(routeInfo.distanceToTurn) {
let measurement = Measurement(value: distance,
unit: routeInfo.turnUnits)
let estimates = CPTravelEstimates(distanceRemaining: measurement,
timeRemaining: 0.0)
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.symbolSet = CPImageSet(lightContentImage: symbol,
darkContentImage: 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
@available(iOS 12.0, *)
extension CarPlayRouter: RoutingManagerListener {
func updateCameraInfo(isCameraOnRoute: Bool, speedLimit limit: String?) {
CarPlayService.shared.updateCameraUI(isCameraOnRoute: isCameraOnRoute, speedLimit: 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)
})
}
}
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,685 @@
import CarPlay
import Contacts
@available(iOS 12.0, *)
@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] = []
@objc func setup(window: CPWindow, interfaceController: CPInterfaceController) {
isCarplayActivated = true
self.window = window
self.interfaceController = interfaceController
self.interfaceController?.delegate = self
sessionConfiguration = CPSessionConfiguration(delegate: self)
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)
}
ThemeManager.invalidate()
}
@objc func destroy() {
if let carplayVC = carplayVC {
carplayVC.removeMapView()
}
MapViewController.shared()?.disableCarPlayRepresentation()
MapViewController.shared()?.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()
}
@objc func interfaceStyle() -> UIUserInterfaceStyle {
if let window = window,
window.traitCollection.userInterfaceIdiom == .carPlay {
return window.traitCollection.userInterfaceStyle
}
return .unspecified
}
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
interfaceController?.setRootTemplate(mapTemplate, animated: true)
}
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.updateEstimates(estimates, for: trip)
}
if let carplayVC = carplayVC {
carplayVC.updateCurrentSpeed(routeInfo.speed)
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 cancelCurrentTrip() {
router?.cancelTrip()
if let carplayVC = carplayVC {
carplayVC.hideSpeedControl()
}
updateMapTemplateUIToBase()
}
func updateCameraUI(isCameraOnRoute: Bool, speedLimit limit: String?) {
if let carplayVC = carplayVC {
let speedLimit = limit == nil ? nil : Int(limit!)
carplayVC.updateCameraInfo(isCameraOnRoute: isCameraOnRoute, speedLimit: speedLimit)
}
}
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)
}
func updateMapTemplateUIToTripFinished(_ trip: CPTrip) {
guard let mapTemplate = rootMapTemplate else {
return
}
mapTemplate.leadingNavigationBarButtons = []
mapTemplate.trailingNavigationBarButtons = []
mapTemplate.mapButtons = []
let doneAction = CPAlertAction(title: L("done"), style: .cancel) { [unowned self] _ in
self.updateMapTemplateUIToBase()
}
var subtitle = ""
if let locationName = trip.destination.name {
subtitle = locationName
}
if let address = trip.destination.placemark.addressDictionary?[CNPostalAddressStreetKey] as? String {
subtitle = subtitle + "\n" + address
}
let alert = CPNavigationAlert(titleVariants: [L("trip_finished")],
subtitleVariants: [subtitle],
imageSet: 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]
interfaceController?.dismissTemplate(animated: true)
interfaceController?.presentTemplate(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
@available(iOS 12.0, *)
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
@available(iOS 12.0, *)
extension CarPlayService: CPSessionConfigurationDelegate {
func sessionConfiguration(_ sessionConfiguration: CPSessionConfiguration,
limitedUserInterfacesChanged limitedUserInterfaces: CPLimitableUserInterface) {
}
}
// MARK: - CPMapTemplateDelegate implementation
@available(iOS 12.0, *)
extension CarPlayService: CPMapTemplateDelegate {
public func mapTemplateDidShowPanningInterface(_ mapTemplate: CPMapTemplate) {
MapTemplateBuilder.configurePanUI(mapTemplate: mapTemplate)
FrameworkHelper.stopLocationFollow()
}
public func mapTemplateWillDismissPanningInterface(_ 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()
}
func mapTemplate(_ mapTemplate: CPMapTemplate, panEndedWith direction: CPMapTemplate.PanDirection) {
var offset = UIOffset(horizontal: 0.0, vertical: 0.0)
let offsetStep: CGFloat = 0.25
if direction.contains(.up) { offset.vertical -= offsetStep }
if direction.contains(.down) { offset.vertical += offsetStep }
if direction.contains(.left) { offset.horizontal += offsetStep }
if direction.contains(.right) { offset.horizontal -= offsetStep }
FrameworkHelper.moveMap(offset)
}
func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {
var offset = UIOffset(horizontal: 0.0, vertical: 0.0)
let offsetStep: CGFloat = 0.1
if direction.contains(.up) { offset.vertical -= offsetStep }
if direction.contains(.down) { offset.vertical += offsetStep }
if direction.contains(.left) { offset.horizontal += offsetStep }
if direction.contains(.right) { offset.horizontal -= offsetStep }
FrameworkHelper.moveMap(offset)
}
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)
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.speed)
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)
return
}
mapTemplate.updateEstimates(estimates, for: trip)
}
}
// MARK: - CPListTemplateDelegate implementation
@available(iOS 12.0, *)
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
@available(iOS 12.0, *)
extension CarPlayService: CPSearchTemplateDelegate {
func searchTemplate(_ searchTemplate: CPSearchTemplate, updatedSearchText searchText: String, completionHandler: @escaping ([CPListItem]) -> Void) {
let locale = window?.textInputMode?.primaryLanguage ?? "en"
guard let searchService = searchService else {
completionHandler([])
return
}
searchService.searchText(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()
}
}
// MARK: - CarPlayRouterListener implementation
@available(iOS 12.0, *)
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.speed)
}
guard let router = router,
let template = rootMapTemplate else {
return
}
router.updateUpcomingManeuvers()
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
@available(iOS 12.0, *)
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:
MapTemplateBuilder.setupDestinationButton(mapTemplate: rootMapTemplate)
case .notFollow:
if !rootMapTemplate.isPanningInterfaceVisible {
MapTemplateBuilder.setupRecenterButton(mapTemplate: rootMapTemplate)
}
default:
break
}
}
}
// MARK: - Alerts and Trip Previews
@available(iOS 12.0, *)
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)
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? {
if let distance = Double(routeInfo.targetDistance) {
let measurement = Measurement(value: distance,
unit: routeInfo.targetUnits)
let estimates = CPTravelEstimates(distanceRemaining: measurement,
timeRemaining: routeInfo.timeToTarget)
return estimates
}
return nil
}
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("redirect_route_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("redirect_route_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]
interfaceController?.presentTemplate(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])
interfaceController?.presentTemplate(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:
titleVariants = ["\(L("dialog_routing_system_error_carplay"))"]
default:
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])
interfaceController?.presentTemplate(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
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]
interfaceController?.presentTemplate(alert, animated: true)
}
}

View file

@ -0,0 +1,13 @@
NS_ASSUME_NONNULL_BEGIN
@interface MWMCarPlayBookmarkObject : NSObject
@property(assign, nonatomic, readonly) MWMMarkID bookmarkId;
@property(strong, nonatomic, readonly) NSString *prefferedName;
@property(strong, nonatomic, readonly) NSString *address;
@property(assign, nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(assign, nonatomic, readonly) CGPoint mercatorPoint;
- (instancetype)initWithBookmarkId:(MWMMarkID)bookmarkId;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,31 @@
#import "MWMCarPlayBookmarkObject.h"
#include "Framework.h"
#include "geometry/mercator.hpp"
@interface MWMCarPlayBookmarkObject()
@property(assign, nonatomic, readwrite) MWMMarkID bookmarkId;
@property(strong, nonatomic, readwrite) NSString *prefferedName;
@property(strong, nonatomic, readwrite) NSString *address;
@property(assign, nonatomic, readwrite) CLLocationCoordinate2D coordinate;
@property(assign, nonatomic, readwrite) CGPoint mercatorPoint;
@end
@implementation MWMCarPlayBookmarkObject
- (instancetype)initWithBookmarkId:(MWMMarkID)bookmarkId {
self = [super init];
if (self) {
self.bookmarkId = bookmarkId;
auto const & bm = GetFramework().GetBookmarkManager();
Bookmark const * bookmark = bm.GetBookmark(bookmarkId);
self.prefferedName = @(bookmark->GetPreferredName().c_str());
auto const pivot = bookmark->GetPivot();
self.mercatorPoint = CGPointMake(pivot.x, pivot.y);
auto const & address = GetFramework().GetAddressAtPoint(pivot);
self.address = @(address.FormatAddress().c_str());
auto const location = MercatorBounds::ToLatLon(pivot);
self.coordinate = CLLocationCoordinate2DMake(location.m_lat, location.m_lon);
}
return self;
}
@end

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,42 @@
#import "MWMCarPlaySearchResultObject.h"
#import "MWMSearch.h"
#include "platform/localization.hpp"
#include "search/result.hpp"
#include "map/everywhere_search_callback.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];
MWMSearchItemType type = [MWMSearch resultTypeWithRow:row];
if (type == MWMSearchItemTypeRegular) {
auto const & result = [MWMSearch resultWithContainerIndex:containerIndex];
NSString *localizedTypeName = @"";
if (result.GetResultType() == search::Result::Type::Feature) {
auto const readableType = classif().GetReadableObjectName(result.GetFeatureType());
localizedTypeName = @(platform::GetLocalizedTypeName(readableType).c_str());
}
self.title = result.GetString().empty() ? localizedTypeName : @(result.GetString().c_str());
self.address = @(result.GetAddress().c_str());
auto const pivot = result.GetFeatureCenter();
self.mercatorPoint = CGPointMake(pivot.x, pivot.y);
auto const location = MercatorBounds::ToLatLon(pivot);
self.coordinate = CLLocationCoordinate2DMake(location.m_lat, location.m_lon);
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,60 @@
#import "MWMCarPlaySearchService.h"
#import "MWMCarPlaySearchResultObject.h"
#import "MWMSearch.h"
#import <CarPlay/CarPlay.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;
[MWMSearch searchQuery:text forInputLocale:inputLocale];
}
- (void)saveLastQuery {
if (self.lastQuery != nil && self.inputLocale != nil) {
[MWMSearch saveQuery:self.lastQuery forInputLocale:self.inputLocale];
}
}
#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
@available(iOS 12.0, *)
final class ListTemplateBuilder {
enum ListTemplateType {
case history
case bookmarkLists
case bookmarks(category: MWMCategory)
case searchResults(results: [MWMCarPlaySearchResultObject])
}
enum BarButtonType {
case bookmarks
case search
}
// MARK: - CPListTemplate bilder
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 template = CPListTemplate(title: title, sections: [])
template.trailingNavigationBarButtons = trailingNavigationBarButtons
obtainResources(for: type, template: template)
return template
}
private class func obtainResources(for type: ListTemplateType, template: CPListTemplate) {
switch type {
case .history:
obtainHistory(template: template)
case .bookmarks(let category):
obtainBookmarks(template: template, categoryId: category.categoryId)
case .bookmarkLists:
obtainCategories(template: template)
case .searchResults(let results):
convertSearchResults(results, template: template)
}
}
private class func obtainHistory(template: CPListTemplate) {
let searchQueries = FrameworkHelper.obtainLastSearchQueries() ?? []
let items = searchQueries.map({ (text) -> CPListItem in
let item = CPListItem(text: text, detailText: nil, image: UIImage(named: "ic_carplay_recent"))
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.history,
metadata: nil)
return item
})
let section = CPListSection(items: items)
template.updateSections([section])
}
private class func obtainCategories(template: CPListTemplate) {
let bookmarkManager = MWMBookmarksManager.shared()
let categories = bookmarkManager.userCategories()
let items: [CPListItem] = categories.compactMap({ category in
if category.bookmarksCount == 0 { return nil }
let placesString = String(format: L("bookmarks_places"), category.bookmarksCount)
let item = CPListItem(text: category.title, detailText: placesString)
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.bookmarkLists,
metadata: CategoryInfo(category: category))
return item
})
let section = CPListSection(items: items)
template.updateSections([section])
}
private class func obtainBookmarks(template: CPListTemplate, categoryId: MWMMarkGroupID) {
let bookmarkManager = MWMBookmarksManager.shared()
let bookmarks = bookmarkManager.bookmarks(forCategory: categoryId)
let 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 section = CPListSection(items: items)
template.updateSections([section])
}
private class func convertSearchResults(_ results: [MWMCarPlaySearchResultObject], template: CPListTemplate) {
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)
}
let section = CPListSection(items: items)
template.updateSections([section])
}
// MARK: - CPBarButton builder
private class func buildBarButton(type: BarButtonType, action: ((CPBarButton) -> Void)?) -> CPBarButton {
switch type {
case .bookmarks:
let button = CPBarButton(type: .image, handler: action)
button.image = UIImage(named: "ic_carplay_bookmark")
return button
case .search:
let button = CPBarButton(type: .image, handler: action)
button.image = UIImage(named: "ic_carplay_keyboard")
return button
}
}
}

View file

@ -0,0 +1,197 @@
import CarPlay
@available(iOS 12.0, *)
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
}
// 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]
}
// 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 muteButton = buildBarButton(type: .mute) { _ in
MWMTextToSpeech.setTTSEnabled(false)
setupUnmuteAndRedirectButtons(template: template)
}
let redirectButton = buildBarButton(type: .redirectRoute) { _ in
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .history)
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
}
template.leadingNavigationBarButtons = [muteButton, redirectButton]
}
private class func setupUnmuteAndRedirectButtons(template: CPMapTemplate) {
let unmuteButton = buildBarButton(type: .unmute) { _ in
MWMTextToSpeech.setTTSEnabled(true)
setupMuteAndRedirectButtons(template: template)
}
let redirectButton = buildBarButton(type: .redirectRoute) { _ in
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .history)
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
}
template.leadingNavigationBarButtons = [unmuteButton, 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(named: "btn_carplay_pan_light")
case .zoomIn:
button.image = UIImage(named: "btn_zoom_in_light")
case .zoomOut:
button.image = UIImage(named: "btn_zoom_out_light")
}
return button
}
// MARK: - CPBarButton builder
private class func buildBarButton(type: BarButtonType, action: ((CPBarButton) -> Void)?) -> CPBarButton {
switch type {
case .dismissPaning:
let button = CPBarButton(type: .text, handler: action)
button.title = L("done")
return button
case .destination:
let button = CPBarButton(type: .text, handler: action)
button.title = L("pick_destination")
return button
case .recenter:
let button = CPBarButton(type: .text, handler: action)
button.title = L("follow_my_position")
return button
case .settings:
let button = CPBarButton(type: .image, handler: action)
button.image = UIImage(named: "ic_carplay_settings")
return button
case .mute:
let button = CPBarButton(type: .image, handler: action)
button.image = UIImage(named: "ic_carplay_unmuted")
return button
case .unmute:
let button = CPBarButton(type: .image, handler: action)
button.image = UIImage(named: "ic_carplay_muted")
return button
case .redirectRoute:
let button = CPBarButton(type: .image, handler: action)
button.image = UIImage(named: "ic_carplay_redirect_route")
return button
case .endRoute:
let button = CPBarButton(type: .text, handler: action)
button.title = L("navigation_stop_button").capitalized
return button
}
}
}

View file

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

View file

@ -0,0 +1,86 @@
import CarPlay
@available(iOS 12.0, *)
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 [createUnpavedButton(options: options),
createTollButton(options: options),
createFerryButton(options: options),
createTrafficButton(),
createSpeedcamButton()]
}
// MARK: - CPGridButton builders
private class func createTollButton(options: RoutingOptions) -> CPGridButton {
var tollIconName = "ic_carplay_toll"
if options.avoidToll { tollIconName += "_active" }
let tollButton = CPGridButton(titleVariants: [L("avoid_tolls")],
image: UIImage(named: tollIconName)!) { _ 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 = "ic_carplay_unpaved"
if options.avoidDirty { unpavedIconName += "_active" }
let unpavedButton = CPGridButton(titleVariants: [L("avoid_unpaved")],
image: UIImage(named: unpavedIconName)!) { _ in
options.avoidDirty = !options.avoidDirty
options.save()
CarPlayService.shared.updateRouteAfterChangingSettings()
CarPlayService.shared.popTemplate(animated: true)
}
return unpavedButton
}
private class func createFerryButton(options: RoutingOptions) -> CPGridButton {
var ferryIconName = "ic_carplay_ferry"
if options.avoidFerry { ferryIconName += "_active" }
let ferryButton = CPGridButton(titleVariants: [L("avoid_ferry")],
image: UIImage(named: ferryIconName)!) { _ in
options.avoidFerry = !options.avoidFerry
options.save()
CarPlayService.shared.updateRouteAfterChangingSettings()
CarPlayService.shared.popTemplate(animated: true)
}
return ferryButton
}
private class func createTrafficButton() -> CPGridButton {
var trafficIconName = "ic_carplay_trafficlight"
let isTrafficEnabled = MWMTrafficManager.trafficEnabled()
if isTrafficEnabled { trafficIconName += "_active" }
let trafficButton = CPGridButton(titleVariants: [L("button_layer_traffic")],
image: UIImage(named: trafficIconName)!) { _ in
MWMTrafficManager.setTrafficEnabled(!isTrafficEnabled)
CarPlayService.shared.popTemplate(animated: true)
}
return trafficButton
}
private class func createSpeedcamButton() -> CPGridButton {
var speedcamIconName = "ic_carplay_speedcam"
let isSpeedCamActivated = CarPlayService.shared.isSpeedCamActivated
if isSpeedCamActivated { speedcamIconName += "_active" }
let speedButton = CPGridButton(titleVariants: [L("speedcams_alert_title")],
image: UIImage(named: speedcamIconName)!) { _ 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: MWMCategory
init(category: MWMCategory) {
self.category = category
}
}

View file

@ -0,0 +1,13 @@
@available(iOS 12.0, *)
protocol InfoMetadata {}
@available(iOS 12.0, *)
struct ListItemInfo {
let type: String
let metadata: InfoMetadata?
init(type: String, metadata: InfoMetadata?) {
self.type = type
self.metadata = metadata
}
}

View file

@ -0,0 +1,10 @@
@available(iOS 12.0, *)
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,50 @@
@objc(MWMRouteInfo)
class RouteInfo: NSObject {
let timeToTarget: TimeInterval
let targetDistance: String
let targetUnits: UnitLength
let distanceToTurn: String
let streetName: String
let turnUnits: UnitLength
let turnImageName: String?
let nextTurnImageName: String?
let speed: Int
let roundExitNumber: Int
@objc init(timeToTarget: TimeInterval,
targetDistance: String,
targetUnits: String,
distanceToTurn: String,
streetName: String,
turnUnits: String,
turnImageName: String?,
nextTurnImageName: String?,
speed: Int,
roundExitNumber: Int) {
self.timeToTarget = timeToTarget
self.targetDistance = targetDistance
self.targetUnits = RouteInfo.unitLength(for: targetUnits)
self.distanceToTurn = distanceToTurn
self.streetName = streetName;
self.turnUnits = RouteInfo.unitLength(for: turnUnits)
self.turnImageName = turnImageName
self.nextTurnImageName = nextTurnImageName
self.speed = speed
self.roundExitNumber = roundExitNumber
}
class func unitLength(for targetUnits: String) -> UnitLength {
switch targetUnits {
case "mi":
return .miles
case "ft":
return .feet
case "km":
return .kilometers
case "m":
return .meters
default:
return .meters
}
}
}

View file

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

View file

@ -288,7 +288,7 @@ extern NSString * const kAlohalyticsTapEventKey;
- (MWMSideButtons *)sideButtons
{
if (!_sideButtons)
_sideButtons = [[MWMSideButtons alloc] initWithParentView:self.ownerController.view];
_sideButtons = [[MWMSideButtons alloc] initWithParentView:self.ownerController.controlsView];
return _sideButtons;
}
@ -319,7 +319,7 @@ extern NSString * const kAlohalyticsTapEventKey;
{
if (!_navigationManager)
_navigationManager =
[[MWMNavigationDashboardManager alloc] initWithParentView:self.ownerController.view];
[[MWMNavigationDashboardManager alloc] initWithParentView:self.ownerController.controlsView];
return _navigationManager;
}
@ -450,7 +450,7 @@ extern NSString * const kAlohalyticsTapEventKey;
[ownerController addChildViewController:self.tutorialViewContoller];
self.tutorialViewContoller.view.frame = ownerController.view.bounds;
self.tutorialViewContoller.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[ownerController.view addSubview:self.tutorialViewContoller.view];
[ownerController.controlsView addSubview:self.tutorialViewContoller.view];
[self.tutorialViewContoller didMoveToParentViewController:ownerController];
}

View file

@ -54,7 +54,7 @@ NSArray<UIImage *> * imagesWithName(NSString * name)
{
MapViewController * ovc = [MapViewController sharedController];
[ovc addChildViewController:self];
[ovc.view addSubview:self.view];
[ovc.controlsView addSubview:self.view];
[self configLayout];
[self refreshAppearance];
[MWMTrafficManager addObserver:self];

View file

@ -49,7 +49,7 @@
- (NSString *)speedUnits
{
auto const units = coreUnits([MWMSettings measurementUnits]);
return @(measurement_utils::FormatSpeedUnits(units).c_str());
return [self localizedUnitSpeed:@(measurement_utils::FormatSpeedUnits(units).c_str())];
}
- (NSString *)eta { return [NSDateComponentsFormatter etaStringFrom:self.timeToTarget]; }
@ -70,4 +70,13 @@
return [[NSAttributedString alloc] initWithString:@" • " attributes:attributes];
}
- (NSString *)localizedUnitSpeed:(NSString *)speedUnits {
if ([speedUnits isEqualToString:@"mph"]) {
return L(@"miles_per_hour");
} else if ([speedUnits isEqualToString:@"km/h"]) {
return L(@"kilometers_per_hour");
}
return speedUnits;
}
@end

View file

@ -135,10 +135,10 @@ NSAttributedString * estimate(NSTimeInterval time, NSAttributedString * dot, NSS
entity.isValid = YES;
entity.timeToTarget = info.m_time;
entity.targetDistance = @(info.m_distToTarget.c_str());
entity.targetUnits = @(info.m_targetUnitsSuffix.c_str());
entity.targetUnits = [self localizedUnitLength:@(info.m_targetUnitsSuffix.c_str())];
entity.progress = info.m_completionPercent;
entity.distanceToTurn = @(info.m_distToTurn.c_str());
entity.turnUnits = @(info.m_turnUnitsSuffix.c_str());
entity.turnUnits = [self localizedUnitLength:@(info.m_turnUnitsSuffix.c_str())];
entity.streetName = @(info.m_displayedStreetName.c_str());
entity.nextTurnImage = image(info.m_nextTurn, true);
@ -178,4 +178,17 @@ NSAttributedString * estimate(NSTimeInterval time, NSAttributedString * dot, NSS
[self onNavigationInfoUpdated];
}
- (NSString *)localizedUnitLength:(NSString *)targetUnits {
if ([targetUnits isEqualToString:@"mi"]) {
return L(@"mile");
} else if ([targetUnits isEqualToString:@"km"]) {
return L(@"kilometer");
} else if ([targetUnits isEqualToString:@"ft"]) {
return L(@"foot");
} else if ([targetUnits isEqualToString:@"m"]) {
return L(@"meter");
}
return targetUnits;
}
@end

View file

@ -18,6 +18,7 @@ static CGFloat const kDrivingOptionsHeight = 48;
@interface MWMRoutePreview ()<MWMCircularProgressProtocol>
@property(nonatomic) BOOL isVisible;
@property(nonatomic) BOOL actualVisibilityValue;
@property(weak, nonatomic) IBOutlet UIButton * backButton;
@property(weak, nonatomic) IBOutlet UIView * bicycle;
@property(weak, nonatomic) IBOutlet UIView * contentView;
@ -38,6 +39,7 @@ static CGFloat const kDrivingOptionsHeight = 48;
- (void)awakeFromNib
{
[super awakeFromNib];
self.actualVisibilityValue = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
[self setupProgresses];
[self.backButton matchInterfaceOrientation];
@ -174,12 +176,16 @@ static CGFloat const kDrivingOptionsHeight = 48;
return;
[superview addSubview:self];
[self setupConstraints];
self.actualVisibilityValue = YES;
dispatch_async(dispatch_get_main_queue(), ^{
self.isVisible = YES;
});
}
- (void)remove { self.isVisible = NO; }
- (void)remove {
self.actualVisibilityValue = NO;
self.isVisible = NO;
}
- (void)setupConstraints {}
@ -209,6 +215,7 @@ static CGFloat const kDrivingOptionsHeight = 48;
- (void)setIsVisible:(BOOL)isVisible
{
if (isVisible != self.actualVisibilityValue) { return; }
_isVisible = isVisible;
auto sv = self.superview;
[sv setNeedsLayout];

View file

@ -1,34 +1,22 @@
#include "drape/pointers.hpp"
#include "drape/drape_global.hpp"
@class MWMMapWidgets;
namespace dp
{
class GraphicsContextFactory;
}
// This class wraps the CAEAGLLayer from CoreAnimation into a convenient UIView subclass.
// The view content is basically an EAGL surface you render your OpenGL scene into.
// Note that setting the view non-opaque will only work if the EAGL surface has an alpha channel.
@interface EAGLView : UIView
{
dp::ApiVersion m_apiVersion;
drape_ptr<dp::GraphicsContextFactory> m_factory;
// Do not call onSize from layoutSubViews when real size wasn't changed.
// It's possible when we add/remove subviews (bookmark balloons) and it hangs the map without this check
CGRect m_lastViewSize;
bool m_presentAvailable;
}
@property(nonatomic) MWMMapWidgets * widgetsManager;
@property(nonatomic, readonly) BOOL drapeEngineCreated;
@property(nonatomic, getter=isLaunchByDeepLink) BOOL launchByDeepLink;
@property(nonatomic, readonly) m2::PointU pixelSize;
@property(nonatomic, readonly) CGSize pixelSize;
@property(nonatomic, readonly) BOOL graphicContextInitialized;
- (void)createDrapeEngine;
- (void)deallocateNative;
- (void)setPresentAvailable:(BOOL)available;
- (void)updateVisualScaleTo:(CGFloat)visualScale;
- (void)updateVisualScaleToMain;
@end

View file

@ -5,19 +5,39 @@
#import "3party/Alohalytics/src/alohalytics_objc.h"
#include "Framework.h"
#include "drape/drape_global.hpp"
#include "drape/visual_scale.hpp"
#include "base/assert.hpp"
#include "base/logging.hpp"
#include "drape/drape_global.hpp"
#include "drape/pointers.hpp"
#include "drape/visual_scale.hpp"
#include "drape_frontend/visual_params.hpp"
#include "Framework.h"
#ifdef OMIM_METAL_AVAILABLE
#import "MetalContextFactory.h"
#import <MetalKit/MetalKit.h>
#endif
namespace dp
{
class GraphicsContextFactory;
}
@interface EAGLView()
{
dp::ApiVersion m_apiVersion;
drape_ptr<dp::GraphicsContextFactory> m_factory;
// Do not call onSize from layoutSubViews when real size wasn't changed.
// It's possible when we add/remove subviews (bookmark balloons) and it hangs the map without this check
CGRect m_lastViewSize;
bool m_presentAvailable;
double main_visualScale;
}
@property(nonatomic, readwrite) BOOL graphicContextInitialized;
@end
@implementation EAGLView
namespace
@ -119,11 +139,16 @@ double getExactDPI(double contentScaleFactor)
layer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking : @NO,
kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8};
}
auto & f = GetFramework();
f.SetGraphicsContextInitializationHandler([self]() {
self.graphicContextInitialized = YES;
});
}
- (void)createDrapeEngine
{
m2::PointU const s = [self pixelSize];
CGSize const objcSize = [self pixelSize];
m2::PointU const s = m2::PointU(static_cast<uint32_t>(objcSize.width), static_cast<uint32_t>(objcSize.height));
if (m_apiVersion == dp::ApiVersion::Metal)
{
@ -172,12 +197,13 @@ double getExactDPI(double contentScaleFactor)
}
}
- (m2::PointU)pixelSize
- (CGSize)pixelSize
{
CGSize const s = self.bounds.size;
uint32_t const w = static_cast<uint32_t>(s.width * self.contentScaleFactor);
uint32_t const h = static_cast<uint32_t>(s.height * self.contentScaleFactor);
return m2::PointU(w, h);
CGFloat const w = s.width * self.contentScaleFactor;
CGFloat const h = s.height * self.contentScaleFactor;
return CGSizeMake(w, h);
}
- (void)layoutSubviews
@ -185,9 +211,10 @@ double getExactDPI(double contentScaleFactor)
if (!CGRectEqualToRect(m_lastViewSize, self.frame))
{
m_lastViewSize = self.frame;
m2::PointU const s = [self pixelSize];
CGSize const objcSize = [self pixelSize];
m2::PointU const s = m2::PointU(static_cast<uint32_t>(objcSize.width), static_cast<uint32_t>(objcSize.height));
GetFramework().OnSize(s.x, s.y);
[self.widgetsManager resize:CGSizeMake(s.x, s.y)];
[self.widgetsManager resize:objcSize];
}
[super layoutSubviews];
}
@ -212,4 +239,13 @@ double getExactDPI(double contentScaleFactor)
return _widgetsManager;
}
- (void)updateVisualScaleTo:(CGFloat)visualScale {
main_visualScale = df::VisualParams::Instance().GetVisualScale();
GetFramework().UpdateVisualScale(visualScale);
}
- (void)updateVisualScaleToMain {
GetFramework().UpdateVisualScale(main_visualScale);
}
@end

View file

@ -1,14 +1,19 @@
#import "MWMMapDownloaderMode.h"
#import "MWMViewController.h"
#import "MWMMyPositionMode.h"
@class MWMWelcomePageController;
@class MWMMapViewControlsManager;
@class MWMAPIBar;
@class MWMPlacePageData;
@class EAGLView;
@protocol MWMLocationModeListener;
@interface MapViewController : MWMViewController
+ (MapViewController *)sharedController;
- (void)addListener:(id<MWMLocationModeListener>)listener;
- (void)removeListener:(id<MWMLocationModeListener>)listener;
// called when app is terminated by system
- (void)onTerminate;
@ -38,9 +43,16 @@
+ (void)setViewport:(double)lat lon:(double)lon zoomLevel:(int)zoomlevel;
- (void)initialize;
- (void)enableCarPlayRepresentation;
- (void)disableCarPlayRepresentation;
@property(nonatomic, readonly) MWMMapViewControlsManager * controlsManager;
@property(nonatomic) MWMAPIBar * apiBar;
@property(nonatomic) MWMWelcomePageController * welcomePageController;
@property(nonatomic) MWMMyPositionMode currentPositionMode;
@property(strong, nonatomic) IBOutlet EAGLView * _Nonnull mapView;
@property(strong, nonatomic) IBOutlet UIView * _Nonnull controlsView;
@end

View file

@ -18,6 +18,7 @@
#import "MWMPlacePageProtocol.h"
#import "MapsAppDelegate.h"
#import "SwiftBridge.h"
#import "MWMLocationModeListener.h"
#include "Framework.h"
@ -93,15 +94,19 @@ BOOL gIsFirstMyPositionMode = YES;
@property(nonatomic) BOOL skipForceTouch;
@property(weak, nonatomic) IBOutlet NSLayoutConstraint * visibleAreaBottom;
@property(weak, nonatomic) IBOutlet NSLayoutConstraint * visibleAreaKeyboard;
@property(weak, nonatomic) IBOutlet NSLayoutConstraint * placePageAreaKeyboard;
@property(weak, nonatomic) IBOutlet NSLayoutConstraint * sideButtonsAreaBottom;
@property(weak, nonatomic) IBOutlet NSLayoutConstraint * sideButtonsAreaKeyboard;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint * visibleAreaBottom;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint * visibleAreaKeyboard;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint * placePageAreaKeyboard;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint * sideButtonsAreaBottom;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint * sideButtonsAreaKeyboard;
@property(strong, nonatomic) IBOutlet UILabel * carplayPlaceholderLabel;
@property(strong, nonatomic) NSHashTable<id<MWMLocationModeListener>> *listeners;
@end
@implementation MapViewController
@synthesize mapView, controlsView;
+ (MapViewController *)sharedController { return [MapsAppDelegate theApp].mapViewController; }
@ -150,11 +155,14 @@ BOOL gIsFirstMyPositionMode = YES;
withTouches:(NSSet *)touches
andEvent:(UIEvent *)event
{
if (@available(iOS 12.0, *)) {
if ([MWMCarPlayService shared].isCarplayActivated) { return; }
}
NSArray * allTouches = [[event allTouches] allObjects];
if ([allTouches count] < 1)
return;
UIView * v = self.view;
UIView * v = self.mapView;
CGFloat const scaleFactor = v.contentScaleFactor;
df::TouchEvent e;
@ -236,8 +244,8 @@ BOOL gIsFirstMyPositionMode = YES;
[super didReceiveMemoryWarning];
}
- (void)onTerminate { [(EAGLView *)self.view deallocateNative]; }
- (void)onGetFocus:(BOOL)isOnFocus { [(EAGLView *)self.view setPresentAvailable:isOnFocus]; }
- (void)onTerminate { [self.mapView deallocateNative]; }
- (void)onGetFocus:(BOOL)isOnFocus { [self.mapView setPresentAvailable:isOnFocus]; }
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
@ -256,11 +264,12 @@ BOOL gIsFirstMyPositionMode = YES;
{
[super viewDidLoad];
[(EAGLView *)self.view setLaunchByDeepLink:DeepLinkHandler.shared.isLaunchedByDeeplink];
[self.mapView setLaunchByDeepLink:DeepLinkHandler.shared.isLaunchedByDeeplink];
[MWMRouter restoreRouteIfNeeded];
self.view.clipsToBounds = YES;
[self processMyPositionStateModeEvent:MWMMyPositionModePendingPosition];
self.currentPositionMode = MWMMyPositionModePendingPosition;
[self processMyPositionStateModeEvent:self.currentPositionMode];
[MWMKeyboard addObserver:self];
self.welcomePageController = [MWMWelcomePageController controllerWithParent:self];
if ([MWMNavigationDashboardManager manager].state == MWMNavigationDashboardStateHidden)
@ -285,9 +294,8 @@ BOOL gIsFirstMyPositionMode = YES;
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
EAGLView * renderingView = (EAGLView *)self.view;
if (!renderingView.drapeEngineCreated)
[renderingView createDrapeEngine];
if (!self.mapView.drapeEngineCreated)
[self.mapView createDrapeEngine];
}
- (void)mwm_refreshUI
@ -368,6 +376,7 @@ BOOL gIsFirstMyPositionMode = YES;
- (void)initialize
{
self.listeners = [NSHashTable<id<MWMLocationModeListener>> weakObjectsHashTable];
Framework & f = GetFramework();
// TODO: Review and improve this code.
f.SetMapSelectionListeners(
@ -386,6 +395,14 @@ BOOL gIsFirstMyPositionMode = YES;
[MWMFrameworkListener addObserver:self];
}
- (void)addListener:(id<MWMLocationModeListener>)listener {
[self.listeners addObject:listener];
}
- (void)removeListener:(id<MWMLocationModeListener>)listener {
[self.listeners removeObject:listener];
}
#pragma mark - Open controllers
- (void)openMigration { [self performSegueWithIdentifier:kMigrationSegue sender:self]; }
@ -529,12 +546,17 @@ BOOL gIsFirstMyPositionMode = YES;
- (void)processMyPositionStateModeEvent:(MWMMyPositionMode)mode
{
self.currentPositionMode = mode;
[MWMLocationManager setMyPositionMode:mode];
[[MWMSideButtons buttons] processMyPositionStateModeEvent:mode];
NSArray<id<MWMLocationModeListener>> * objects = self.listeners.allObjects;
for (id<MWMLocationModeListener> object in objects) {
[object processMyPositionStateModeEvent:mode];
}
self.disableStandbyOnLocationStateMode = NO;
switch (mode)
{
case location::NotFollowNoPosition:
case MWMMyPositionModeNotFollowNoPosition:
{
BOOL const hasLocation = [MWMLocationManager lastLocation] != nil;
if (hasLocation)
@ -558,10 +580,10 @@ BOOL gIsFirstMyPositionMode = YES;
}
break;
}
case location::PendingPosition:
case location::NotFollow: break;
case location::Follow:
case location::FollowAndRotate: self.disableStandbyOnLocationStateMode = YES; break;
case MWMMyPositionModePendingPosition:
case MWMMyPositionModeNotFollow: break;
case MWMMyPositionModeFollow:
case MWMMyPositionModeFollowAndRotate: self.disableStandbyOnLocationStateMode = YES; break;
}
gIsFirstMyPositionMode = NO;
}
@ -774,4 +796,33 @@ BOOL gIsFirstMyPositionMode = YES;
f.SetViewportCenter(center, zoomLevel, false);
}
#pragma mark - CarPlay map append/remove
- (void)disableCarPlayRepresentation
{
self.carplayPlaceholderLabel.hidden = YES;
self.mapView.frame = self.view.bounds;
[self.view insertSubview:self.mapView atIndex:0];
[[self.mapView.topAnchor constraintEqualToAnchor:self.view.topAnchor] setActive:YES];
[[self.mapView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor] setActive:YES];
[[self.mapView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor] setActive:YES];
[[self.mapView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor] setActive:YES];
self.controlsView.hidden = NO;
[MWMFrameworkHelper setVisibleViewport:self.view.bounds];
}
- (void)enableCarPlayRepresentation
{
UIViewController *presentedController = self.presentedViewController;
if (presentedController != nil) {
[presentedController dismissViewControllerAnimated:NO completion:nil];
}
[[MapsAppDelegate theApp] showMap];
[self.mapView removeFromSuperview];
if (!self.controlsView.isHidden) {
self.controlsView.hidden = YES;
}
self.carplayPlaceholderLabel.hidden = NO;
}
@end

View file

@ -3,6 +3,7 @@
#import "MWMNavigationController.h"
@class MapViewController;
@class MWMCarPlayService;
NS_ASSUME_NONNULL_BEGIN
@ -14,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic) UIWindow * window;
@property(nonatomic, readonly) MWMCarPlayService *carplayService API_AVAILABLE(ios(12.0));
@property(nonatomic, readonly) MapViewController * mapViewController;
@property(nonatomic, readonly) BOOL isDrapeEngineCreated;

View file

@ -24,6 +24,7 @@
#import <CoreSpotlight/CoreSpotlight.h>
#import <FBSDKCoreKit/FBSDKCoreKit.h>
#import <UserNotifications/UserNotifications.h>
#import <CarPlay/CarPlay.h>
#import <AppsFlyerLib/AppsFlyerTracker.h>
#import <Crashlytics/Crashlytics.h>
@ -107,7 +108,10 @@ void OverrideUserAgent()
using namespace osm_auth_ios;
@interface MapsAppDelegate ()<MWMFrameworkStorageObserver, NotificationManagerDelegate, AppsFlyerTrackerDelegate>
@interface MapsAppDelegate ()<MWMFrameworkStorageObserver,
NotificationManagerDelegate,
AppsFlyerTrackerDelegate,
CPApplicationDelegate>
@property(nonatomic) NSInteger standbyCounter;
@property(nonatomic) MWMBackgroundFetchScheduler * backgroundFetchScheduler;
@ -153,7 +157,12 @@ using namespace osm_auth_ios;
- (BOOL)isDrapeEngineCreated
{
return ((EAGLView *)self.mapViewController.view).drapeEngineCreated;
return self.mapViewController.mapView.drapeEngineCreated;
}
- (BOOL)isGraphicContextInitialized
{
return self.mapViewController.mapView.graphicContextInitialized;
}
- (void)searchText:(NSString *)searchString
@ -413,8 +422,8 @@ using namespace osm_auth_ios;
// because of new OpenGL driver powered by Metal.
if ([AppInfo sharedInfo].openGLDriver == MWMOpenGLDriverMetalPre103)
{
m2::PointU const size = ((EAGLView *)self.mapViewController.view).pixelSize;
f.OnRecoverSurface(static_cast<int>(size.x), static_cast<int>(size.y), true /* recreateContextDependentResources */);
CGSize const objcSize = self.mapViewController.mapView.pixelSize;
f.OnRecoverSurface(static_cast<int>(objcSize.width), static_cast<int>(objcSize.height), true /* recreateContextDependentResources */);
}
[MWMLocationManager applicationDidBecomeActive];
[MWMSearch addCategoriesToSpotlight];
@ -643,6 +652,10 @@ continueUserActivity:(NSUserActivity *)userActivity
return [(UINavigationController *)self.window.rootViewController viewControllers].firstObject;
}
- (MWMCarPlayService *)carplayService {
return [MWMCarPlayService shared];
}
#pragma mark - TTS
- (void)enableTTSForTheFirstTime
@ -815,4 +828,59 @@ continueUserActivity:(NSUserActivity *)userActivity
});
}
#pragma mark - CPApplicationDelegate implementation
- (void)application:(UIApplication *)application
didConnectCarInterfaceController:(CPInterfaceController *)interfaceController
toWindow:(CPWindow *)window API_AVAILABLE(ios(12.0)) {
[self.carplayService setupWithWindow:window
interfaceController:interfaceController];
[self updateVisualScaleFromWindow:self.window
toWindow:window
isCarplayActivated:YES];
}
- (void)application:(UIApplication *)application
didDisconnectCarInterfaceController:(CPInterfaceController *)interfaceController
fromWindow:(CPWindow *)window API_AVAILABLE(ios(12.0)) {
[self.carplayService destroy];
[self updateVisualScaleFromWindow:window
toWindow:self.window
isCarplayActivated:NO];
}
- (void)updateVisualScaleFromWindow:(UIWindow *)sourceWindow
toWindow:(UIWindow *)destinationWindow
isCarplayActivated:(BOOL)isCarplayActivated {
CGFloat sourceContentScale = sourceWindow.screen.nativeScale;
CGFloat destinationContentScale = destinationWindow.screen.nativeScale;
if (ABS(sourceContentScale - destinationContentScale) > 0.1) {
if (isCarplayActivated) {
[self updateVisualScale:destinationContentScale];
} else {
[self updateVisualScaleToMain];
}
}
}
- (void)updateVisualScale:(CGFloat)scale {
if ([self isGraphicContextInitialized]) {
[self.mapViewController.mapView updateVisualScaleTo:scale];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateVisualScale:scale];
});
}
}
- (void)updateVisualScaleToMain {
if ([self isGraphicContextInitialized]) {
[self.mapViewController.mapView updateVisualScaleToMain];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateVisualScaleToMain];
});
}
}
@end

View file

@ -3,6 +3,7 @@
#import "CLLocation+Mercator.h"
#import "MWMAlertViewController.h"
#import "MWMBottomMenuViewController.h"
#import "SwiftBridge.h"
#import "MWMCircularProgress.h"
#import "MWMCommon.h"
#import "MWMFrameworkListener.h"
@ -166,6 +167,9 @@ using namespace storage;
m_autoDownloadCountryId = kInvalidCountryId;
[self showDownloadRequest];
}
if (@available(iOS 12.0, *)) {
[[MWMCarPlayService shared] showNoMapAlert];
}
break;
}
case NodeStatus::Downloading:
@ -209,6 +213,9 @@ using namespace storage;
- (void)removeFromSuperview
{
if (@available(iOS 12.0, *)) {
[[MWMCarPlayService shared] hideNoMapAlert];
}
self.progress.state = MWMCircularProgressStateNormal;
[MWMFrameworkListener removeObserver:self];
[super removeFromSuperview];

View file

@ -8,5 +8,7 @@
- (void)resize:(CGSize)size;
- (void)updateAvailableArea:(CGRect)frame;
- (void)updateLayout:(CGRect)frame;
- (void)updateLayoutForAvailableArea;
@end

View file

@ -1,6 +1,7 @@
#import "MWMMapWidgets.h"
#import "EAGLView.h"
#import "MapViewController.h"
#import "SwiftBridge.h"
@interface MWMMapWidgets ()
@ -16,7 +17,7 @@
+ (MWMMapWidgets *)widgetsManager
{
return ((EAGLView *)[MapViewController sharedController].view).widgetsManager;
return [MapViewController sharedController].mapView.widgetsManager;
}
- (void)setupWidgets:(Framework::DrapeCreationParams &)p
@ -34,7 +35,14 @@
if (m_skin != nullptr)
m_skin->Resize(size.width, size.height);
dispatch_async(dispatch_get_main_queue(), ^{
[self updateAvailableArea:self.availableArea];
if (@available(iOS 12.0, *)) {
if ([MWMCarPlayService shared].isCarplayActivated) {
CGRect bounds = [MapViewController sharedController].mapView.bounds;
[self updateLayout: bounds];
return;
}
}
[self updateLayoutForAvailableArea];
});
}
@ -43,26 +51,41 @@
if (CGRectEqualToRect(self.availableArea, frame))
return;
self.availableArea = frame;
if (@available(iOS 12.0, *)) {
if ([MWMCarPlayService shared].isCarplayActivated) {
return;
}
}
[self updateLayout:frame];
}
- (void)updateLayoutForAvailableArea
{
[self updateLayout:self.availableArea];
}
- (void)updateLayout:(CGRect)frame
{
if (m_skin == nullptr)
return;
gui::TWidgetsLayoutInfo layout;
auto const vs = self.visualScale;
auto const viewHeight = [MapViewController sharedController].view.height;
auto const viewWidth = [MapViewController sharedController].view.width;
auto const viewHeight = [MapViewController sharedController].mapView.height;
auto const viewWidth = [MapViewController sharedController].mapView.width;
auto const rulerOffset =
m2::PointF(frame.origin.x * vs, (frame.origin.y + frame.size.height - viewHeight) * vs);
m2::PointF(frame.origin.x * vs, (frame.origin.y + frame.size.height - viewHeight) * vs);
auto const compassOffset =
m2::PointF((frame.origin.x + frame.size.width - viewWidth) * vs, frame.origin.y * vs);
m2::PointF((frame.origin.x + frame.size.width - viewWidth) * vs, frame.origin.y * vs);
m_skin->ForEach([&](gui::EWidget w, gui::Position const & pos) {
m2::PointF pivot = pos.m_pixelPivot;
switch (w)
{
case gui::WIDGET_RULER:
case gui::WIDGET_WATERMARK:
case gui::WIDGET_COPYRIGHT: pivot += rulerOffset; break;
case gui::WIDGET_COMPASS: pivot += compassOffset; break;
case gui::WIDGET_SCALE_FPS_LABEL:
case gui::WIDGET_CHOOSE_POSITION_MARK: break;
case gui::WIDGET_RULER:
case gui::WIDGET_WATERMARK:
case gui::WIDGET_COPYRIGHT: pivot += rulerOffset; break;
case gui::WIDGET_COMPASS: pivot += compassOffset; break;
case gui::WIDGET_SCALE_FPS_LABEL:
case gui::WIDGET_CHOOSE_POSITION_MARK: break;
}
layout[w] = pivot;
});

View file

@ -1,5 +1,7 @@
@interface MWMMapWidgetsHelper : NSObject
+ (void)updateAvailableArea:(CGRect)frame;
+ (void)updateLayout:(CGRect)frame;
+ (void)updateLayoutForAvailableArea;
@end

View file

@ -8,4 +8,14 @@
[[MWMMapWidgets widgetsManager] updateAvailableArea:frame];
}
+ (void)updateLayout:(CGRect)frame
{
[[MWMMapWidgets widgetsManager] updateLayout:frame];
}
+ (void)updateLayoutForAvailableArea
{
[[MWMMapWidgets widgetsManager] updateLayoutForAvailableArea];
}
@end

View file

@ -9,4 +9,11 @@
+ (CGFloat)keyboardHeight;
- (instancetype)init __attribute__((unavailable("call +manager instead")));
- (instancetype)copy __attribute__((unavailable("call +manager instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +manager instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone
__attribute__((unavailable("call +manager instead")));
+ (instancetype) new __attribute__((unavailable("call +manager instead")));
@end

View file

@ -21,7 +21,7 @@ using Observers = NSHashTable<Observer>;
static MWMKeyboard * manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[super alloc] initManager];
manager = [[self alloc] initManager];
});
return manager;
}

View file

@ -5,7 +5,7 @@
func create() {
switch self {
case .none: return
case .full: MWMFrameworkHelper.createFramework()
case .full: FrameworkHelper.createFramework()
}
}
}

View file

@ -3,6 +3,7 @@
#import "MWMCatalogCommon.h"
@class MWMCategory;
@class MWMCarPlayBookmarkObject;
@class MWMTagGroup;
@class MWMTag;
@ -37,10 +38,11 @@ typedef void (^LoadTagsCompletionBlock)(NSArray<MWMTagGroup *> * _Nullable tags,
- (void)setUserCategoriesVisible:(BOOL)isVisible;
- (void)setCatalogCategoriesVisible:(BOOL)isVisible;
- (void)deleteCategory:(MWMMarkGroupID)groupId;
- (void)deleteBookmark:(MWMMarkID)bookmarkId;
- (BOOL)checkCategoryName:(NSString *)name;
- (NSArray<MWMCarPlayBookmarkObject *> *)bookmarksForCategory:(MWMMarkGroupID)categoryId;
- (void)deleteBookmark:(MWMMarkID)bookmarkId;
- (void)shareCategory:(MWMMarkGroupID)groupId;
- (NSURL *)shareCategoryURL;
- (void)finishShareCategory;
@ -90,5 +92,12 @@ typedef void (^LoadTagsCompletionBlock)(NSArray<MWMTagGroup *> * _Nullable tags,
progress:(_Nullable ProgressBlock)progress
completion:(UploadCompletionBlock)completion;
- (instancetype)init __attribute__((unavailable("call +manager instead")));
- (instancetype)copy __attribute__((unavailable("call +manager instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +manager instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone
__attribute__((unavailable("call +manager instead")));
+ (instancetype) new __attribute__((unavailable("call +manager instead")));
@end
NS_ASSUME_NONNULL_END

View file

@ -1,6 +1,7 @@
#import "MWMBookmarksManager.h"
#import "AppInfo.h"
#import "MWMCategory.h"
#import "MWMCarPlayBookmarkObject.h"
#import "MWMTag+Convenience.h"
#import "MWMTagGroup+Convenience.h"
#import "MWMCatalogObserver.h"
@ -47,7 +48,7 @@ NSString * const CloudErrorToString(Cloud::SynchronizationResult result)
static MWMBookmarksManager * manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[super alloc] initManager];
manager = [[self alloc] initManager];
});
return manager;
}
@ -383,6 +384,25 @@ NSString * const CloudErrorToString(Cloud::SynchronizationResult result)
}];
}
- (BOOL)checkCategoryName:(NSString *)name
{
return !self.bm.IsUsedCategoryName(name.UTF8String);
}
#pragma mark - Bookmarks
- (NSArray<MWMCarPlayBookmarkObject *> *)bookmarksForCategory:(MWMMarkGroupID)categoryId
{
NSMutableArray<MWMCarPlayBookmarkObject *> * result = [NSMutableArray array];
auto const & bookmarkIds = self.bm.GetUserMarkIds(categoryId);
for (auto bookmarkId : bookmarkIds)
{
MWMCarPlayBookmarkObject *bookmark = [[MWMCarPlayBookmarkObject alloc] initWithBookmarkId:bookmarkId];
[result addObject:bookmark];
}
return [result copy];
}
- (void)deleteBookmark:(MWMMarkID)bookmarkId
{
self.bm.GetEditSession().DeleteBookmark(bookmarkId);
@ -392,11 +412,6 @@ NSString * const CloudErrorToString(Cloud::SynchronizationResult result)
}];
}
- (BOOL)checkCategoryName:(NSString *)name
{
return !self.bm.IsUsedCategoryName(name.UTF8String);
}
#pragma mark - Category sharing
- (void)shareCategory:(MWMMarkGroupID)groupId

View file

@ -0,0 +1,60 @@
import Foundation
// TODO: define directly T as AnyObject after Swift version update
final class ListenerContainer<T> {
// MARK: - WeakWrapper for listeners
private class WeakWrapper<T> {
private weak var weakValue: AnyObject?
init(value: T) {
self.weakValue = value as AnyObject?
}
var value: T? {
return weakValue as? T
}
}
// MARK: - Properties
private var listeners = [WeakWrapper<T>]()
// MARK: - Public methods
func addListener(_ listener: T) {
guard isUnique(listener) else {
return
}
listeners.append(WeakWrapper(value: listener))
}
func removeListener(_ listener: T) {
listeners = listeners.filter({ weakRef in
guard let object = weakRef.value else {
return false
}
return !identical(object, listener)
})
}
func forEach(_ block: @escaping (T) -> Void) {
fetchListeners().forEach(block)
}
// MARK: - Private methods
private func isUnique(_ listener: T) -> Bool {
return !fetchListeners().contains(where: { identical($0, listener) })
}
private func fetchListeners() -> [T] {
removeNilReference()
return listeners.compactMap({ $0.value })
}
private func removeNilReference() {
listeners = listeners.filter({ $0.value != nil })
}
}
private func identical(_ lhs: Any, _ rhs: Any) -> Bool {
return (lhs as AnyObject?) === (rhs as AnyObject?)
}

View file

@ -1,3 +1,9 @@
typedef NS_ENUM(NSUInteger, MWMZoomMode) {
MWMZoomModeIn = 0,
MWMZoomModeOut
};
NS_SWIFT_NAME(FrameworkHelper)
@interface MWMFrameworkHelper : NSObject
+ (void)processFirstLaunch;
@ -20,4 +26,12 @@
+ (MWMMarkGroupID)invalidCategoryId;
+ (void)zoomMap:(MWMZoomMode)mode;
+ (void)moveMap:(UIOffset)offset;
+ (void)deactivateMapSelection:(BOOL)notifyUI NS_SWIFT_NAME(deactivateMapSelection(notifyUI:));
+ (void)switchMyPositionMode;
+ (void)stopLocationFollow;
+ (NSArray<NSString *> *)obtainLastSearchQueries;
+ (BOOL)needUpdateMaps;
@end

View file

@ -2,10 +2,17 @@
#import "MWMLocationManager.h"
#import "MapViewController.h"
#import "MWMAlertViewController.h"
#import "MWMCategory.h"
#import "Statistics.h"
#import "3party/Alohalytics/src/alohalytics_objc.h"
#import "EAGLView.h"
#include "Framework.h"
#include "platform/network_policy_ios.h"
#include "platform/local_country_file_utils.hpp"
extern NSString * const kAlohalyticsTapEventKey;
#include "base/sunrise_sunset.hpp"
@ -23,7 +30,7 @@
+ (void)setVisibleViewport:(CGRect)rect
{
CGFloat const scale = [MapViewController sharedController].view.contentScaleFactor;
CGFloat const scale = [MapViewController sharedController].mapView.contentScaleFactor;
CGFloat const x0 = rect.origin.x * scale;
CGFloat const y0 = rect.origin.y * scale;
CGFloat const x1 = x0 + rect.size.width * scale;
@ -117,4 +124,56 @@
+ (MWMMarkGroupID)invalidCategoryId { return kml::kInvalidMarkGroupId; }
+ (NSArray<NSString *> *)obtainLastSearchQueries
{
NSMutableArray * result = [NSMutableArray array];
auto const & queries = GetFramework().GetLastSearchQueries();
for (auto const & item : queries)
{
[result addObject:@(item.second.c_str())];
}
return [result copy];
}
#pragma mark - Map Interaction
+ (void)zoomMap:(MWMZoomMode)mode
{
[Statistics logEvent:kStatEventName(kStatZoom, kStatOut)];
[Alohalytics logEvent:kAlohalyticsTapEventKey withValue:@"-"];
switch(mode) {
case MWMZoomModeIn:
GetFramework().Scale(Framework::SCALE_MAG, true);
break;
case MWMZoomModeOut:
GetFramework().Scale(Framework::SCALE_MIN, true);
break;
}
}
+ (void)moveMap:(UIOffset)offset
{
GetFramework().Move(offset.horizontal, offset.vertical, true);
}
+ (void)deactivateMapSelection:(BOOL)notifyUI
{
GetFramework().DeactivateMapSelection(notifyUI);
}
+ (void)switchMyPositionMode
{
GetFramework().SwitchMyPositionNextMode();
}
+ (void)stopLocationFollow
{
GetFramework().StopLocationFollow();
}
+ (BOOL)needUpdateMaps
{
return platform::migrate::NeedMigrate();
}
@end

View file

@ -9,7 +9,6 @@
- (instancetype)init __attribute__((unavailable("call +listener instead")));
- (instancetype)copy __attribute__((unavailable("call +listener instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +listener instead")));
+ (instancetype)alloc __attribute__((unavailable("call +listener instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone
__attribute__((unavailable("call +listener instead")));
+ (instancetype) new __attribute__((unavailable("call +listener instead")));

View file

@ -124,6 +124,18 @@ void loopWrappers(Observers * observers, TLoopBlock block)
[observer processRouteRecommendation:rec];
});
});
rm.SetRouteSpeedCamShowListener([observers](m2::PointD const & point, double cameraSpeedKmPH) {
loopWrappers(observers, [cameraSpeedKmPH](TRouteBuildingObserver observer) {
if ([observer respondsToSelector:@selector(speedCameraShowedUpOnRoute:)])
[observer speedCameraShowedUpOnRoute:cameraSpeedKmPH];
});
});
rm.SetRouteSpeedCamsClearListener([observers]() {
loopWrappers(observers, ^(TRouteBuildingObserver observer) {
if ([observer respondsToSelector:@selector(speedCameraLeftVisibleArea)])
[observer speedCameraLeftVisibleArea];
});
});
}
#pragma mark - MWMFrameworkStorageObserver

View file

@ -20,6 +20,8 @@ using namespace storage;
- (void)processRouteBuilderProgress:(CGFloat)progress;
- (void)processRouteRecommendation:(MWMRouterRecommendation)recommendation;
- (void)speedCameraShowedUpOnRoute:(double)speedLimit;
- (void)speedCameraLeftVisibleArea;
@end

View file

@ -0,0 +1,10 @@
#import "MWMMyPositionMode.h"
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(LocationModeListener)
@protocol MWMLocationModeListener <NSObject>
- (void)processMyPositionStateModeEvent:(MWMMyPositionMode)mode;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,19 @@
typedef NS_ENUM(NSUInteger, MWMRouterResultCode) {
MWMRouterResultCodeNoError = 0,
MWMRouterResultCodeCancelled = 1,
MWMRouterResultCodeNoCurrentPosition = 2,
MWMRouterResultCodeInconsistentMWMandRoute = 3,
MWMRouterResultCodeRouteFileNotExist = 4,
MWMRouterResultCodeStartPointNotFound = 5,
MWMRouterResultCodeEndPointNotFound = 6,
MWMRouterResultCodePointsInDifferentMWM = 7,
MWMRouterResultCodeRouteNotFound = 8,
MWMRouterResultCodeNeedMoreMaps = 9,
MWMRouterResultCodeInternalError = 10,
MWMRouterResultCodeFileTooOld = 11,
MWMRouterResultCodeIntermediatePointNotFound = 12,
MWMRouterResultCodeTransitRouteNotFoundNoNetwork = 13,
MWMRouterResultCodeTransitRouteNotFoundTooLongPedestrian = 14,
MWMRouterResultCodeRouteNotFoundRedressRouteError = 15,
MWMRouterResultCodeHasWarnings = 16
} NS_SWIFT_NAME(RouterResultCode);

View file

@ -0,0 +1,48 @@
#import "MWMRouterType.h"
#import "MWMRoutePoint.h"
#import "MWMRouterResultCode.h"
#import "MWMSpeedCameraManagerMode.h"
@class RouteInfo;
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(RoutingManagerListener)
@protocol MWMRoutingManagerListener <NSObject>
- (void)processRouteBuilderEventWithCode:(MWMRouterResultCode)code
countries:(NSArray<NSString *> *)absentCountries;
- (void)didLocationUpdate:(NSArray<NSString *> *)notifications;
- (void)updateCameraInfo:(BOOL)isCameraOnRoute speedLimit:(nullable NSString *)limit NS_SWIFT_NAME(updateCameraInfo(isCameraOnRoute:speedLimit:));
@end
NS_SWIFT_NAME(RoutingManager)
@interface MWMRoutingManager : NSObject
@property(class, nonatomic, readonly) MWMRoutingManager *routingManager;
@property(nonatomic, readonly, nullable) MWMRoutePoint *startPoint;
@property(nonatomic, readonly, nullable) MWMRoutePoint *endPoint;
@property(nonatomic, readonly) BOOL isOnRoute;
@property(nonatomic, readonly) BOOL isRoutingActive;
@property(nonatomic, readonly) BOOL isRouteFinished;
@property(nonatomic, readonly, nullable) RouteInfo *routeInfo;
@property(nonatomic, readonly) MWMRouterType type;
@property(nonatomic) MWMSpeedCameraManagerMode speedCameraMode;
- (instancetype)init NS_UNAVAILABLE;
- (void)addListener:(id<MWMRoutingManagerListener>)listener;
- (void)removeListener:(id<MWMRoutingManagerListener>)listener;
- (void)stopRoutingAndRemoveRoutePoints:(BOOL)flag;
- (void)deleteSavedRoutePoints;
- (void)applyRouterType:(MWMRouterType)type NS_SWIFT_NAME(apply(routeType:));
- (void)addRoutePoint:(MWMRoutePoint *)point NS_SWIFT_NAME(add(routePoint:));
- (void)buildRouteWithDidFailError:(NSError **)errorPtr __attribute__((swift_error(nonnull_error))) NS_SWIFT_NAME(buildRoute());
- (void)startRoute;
- (instancetype)init __attribute__((unavailable("call +routingManager instead")));
- (instancetype)copy __attribute__((unavailable("call +routingManager instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +routingManager instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone
__attribute__((unavailable("call +routingManager instead")));
+ (instancetype) new __attribute__((unavailable("call +routingManager instead")));
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,285 @@
#import "MWMRoutingManager.h"
#import "MWMLocationManager.h"
#import "MWMLocationObserver.h"
#import "MWMFrameworkListener.h"
#import "MWMCoreRouterType.h"
#import "MWMRouterResultCode.h"
#import "MWMRoutePoint+CPP.h"
#import "MWMCoreUnits.h"
#import "SwiftBridge.h"
#include "Framework.h"
#include "routing/turns.hpp"
#include "routing/routing_session.hpp"
@interface MWMRoutingManager()<MWMFrameworkRouteBuilderObserver, MWMLocationObserver>
@property(nonatomic, readonly) RoutingManager & rm;
@property(strong, nonatomic) NSHashTable<id<MWMRoutingManagerListener>> *listeners;
@end
@implementation MWMRoutingManager
+ (MWMRoutingManager *)routingManager {
static MWMRoutingManager * routingManager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
routingManager = [[self alloc] initManager];
});
return routingManager;
}
- (instancetype)initManager {
self = [super init];
if (self) {
self.listeners = [NSHashTable<id<MWMRoutingManagerListener>> weakObjectsHashTable];
[MWMFrameworkListener addObserver:self];
[MWMLocationManager addObserver:self];
}
return self;
}
- (RoutingManager &)rm {
return GetFramework().GetRoutingManager();
}
- (routing::SpeedCameraManager &)scm {
return self.rm.GetSpeedCamManager();
}
- (MWMRoutePoint *)startPoint {
auto const routePoints = self.rm.GetRoutePoints();
if (routePoints.empty())
return nil;
auto const & routePoint = routePoints.front();
if (routePoint.m_pointType == RouteMarkType::Start)
return [[MWMRoutePoint alloc] initWithRouteMarkData:routePoint];
return nil;
}
- (MWMRoutePoint *)endPoint {
auto const routePoints = self.rm.GetRoutePoints();
if (routePoints.empty())
return nil;
auto const & routePoint = routePoints.back();
if (routePoint.m_pointType == RouteMarkType::Finish)
return [[MWMRoutePoint alloc] initWithRouteMarkData:routePoint];
return nil;
}
- (BOOL)isOnRoute {
return self.rm.IsRoutingFollowing();
}
- (BOOL)isRoutingActive {
return self.rm.IsRoutingActive();
}
- (BOOL)isRouteFinished {
return self.rm.IsRouteFinished();
}
- (MWMRouteInfo *)routeInfo {
if (!self.isRoutingActive) { return nil; }
location::FollowingInfo info;
self.rm.GetRouteFollowingInfo(info);
if (!info.IsValid()) { return nil; }
CLLocation * lastLocation = [MWMLocationManager lastLocation];
NSString *speed = @"0";
if (lastLocation && lastLocation.speed >= 0) {
auto const units = coreUnits([MWMSettings measurementUnits]);
speed = @(measurement_utils::FormatSpeed(lastLocation.speed, units).c_str());
}
NSInteger roundExitNumber = 0;
if (info.m_turn == routing::turns::CarDirection::EnterRoundAbout ||
info.m_turn == routing::turns::CarDirection::StayOnRoundAbout ||
info.m_turn == routing::turns::CarDirection::LeaveRoundAbout) {
roundExitNumber = info.m_exitNum;
}
MWMRouteInfo *objCInfo = [[MWMRouteInfo alloc] initWithTimeToTarget:info.m_time
targetDistance:@(info.m_distToTarget.c_str())
targetUnits:@(info.m_targetUnitsSuffix.c_str())
distanceToTurn:@(info.m_distToTurn.c_str())
streetName:@(info.m_displayedStreetName.c_str())
turnUnits:@(info.m_turnUnitsSuffix.c_str())
turnImageName:[self turnImageName:info.m_turn isPrimary:YES]
nextTurnImageName:[self turnImageName:info.m_nextTurn isPrimary:NO]
speed:[speed integerValue]
roundExitNumber: roundExitNumber];
return objCInfo;
}
- (MWMRouterType)type {
return routerType(self.rm.GetRouter());
}
- (void)addListener:(id<MWMRoutingManagerListener>)listener {
[self.listeners addObject:listener];
}
- (void)removeListener:(id<MWMRoutingManagerListener>)listener {
[self.listeners removeObject:listener];
}
- (void)stopRoutingAndRemoveRoutePoints:(BOOL)flag {
self.rm.CloseRouting(flag);
}
- (void)deleteSavedRoutePoints {
self.rm.DeleteSavedRoutePoints();
}
- (void)applyRouterType:(MWMRouterType)type {
self.rm.SetRouter(coreRouterType(type));
}
- (void)addRoutePoint:(MWMRoutePoint *)point {
RouteMarkData startPt = point.routeMarkData;
self.rm.AddRoutePoint(std::move(startPt));
}
- (void)saveRoute {
self.rm.SaveRoutePoints();
}
- (void)buildRouteWithDidFailError:(NSError * __autoreleasing __nullable *)errorPtr {
auto const & points = self.rm.GetRoutePoints();
auto const pointsCount = points.size();
if (pointsCount > 1) {
self.rm.BuildRoute(0 /* timeoutSec */);
} else {
if (errorPtr) {
if (pointsCount == 0) {
*errorPtr = [NSError errorWithDomain:@"maps.me.routing"
code:MWMRouterResultCodeStartPointNotFound
userInfo:nil];
} else {
auto const & routePoint = points.front();
MWMRouterResultCode code;
if (routePoint.m_pointType == RouteMarkType::Start) {
code = MWMRouterResultCodeEndPointNotFound;
} else {
code = MWMRouterResultCodeStartPointNotFound;
}
*errorPtr = [NSError errorWithDomain:@"maps.me.routing"
code:code
userInfo:nil];
}
}
}
}
- (void)startRoute {
[self saveRoute];
self.rm.FollowRoute();
}
- (MWMSpeedCameraManagerMode)speedCameraMode {
auto const mode = self.scm.GetMode();
switch (mode) {
case routing::SpeedCameraManagerMode::Auto:
return MWMSpeedCameraManagerModeAuto;
case routing::SpeedCameraManagerMode::Always:
return MWMSpeedCameraManagerModeAlways;
default:
return MWMSpeedCameraManagerModeNever;
}
}
- (void)setSpeedCameraMode:(MWMSpeedCameraManagerMode)mode {
switch (mode) {
case MWMSpeedCameraManagerModeAuto:
self.scm.SetMode(routing::SpeedCameraManagerMode::Auto);
break;
case MWMSpeedCameraManagerModeAlways:
self.scm.SetMode(routing::SpeedCameraManagerMode::Always);
break;
default:
self.scm.SetMode(routing::SpeedCameraManagerMode::Never);
}
}
#pragma mark - MWMFrameworkRouteBuilderObserver implementation
- (void)processRouteBuilderEvent:(routing::RouterResultCode)code
countries:(const storage::CountriesSet &)absentCountries {
NSArray<id<MWMRoutingManagerListener>> * objects = self.listeners.allObjects;
MWMRouterResultCode objCCode = MWMRouterResultCode(code);
NSMutableArray<NSString *> *objCAbsentCountries = [NSMutableArray new];
std::for_each(absentCountries.begin(), absentCountries.end(), ^(std::string const & str) {
id nsstr = [NSString stringWithUTF8String:str.c_str()];
[objCAbsentCountries addObject:nsstr];
});
for (id<MWMRoutingManagerListener> object in objects) {
[object processRouteBuilderEventWithCode:objCCode
countries:objCAbsentCountries];
}
}
- (void)speedCameraShowedUpOnRoute:(double)speedLimit {
NSArray<id<MWMRoutingManagerListener>> * objects = self.listeners.allObjects;
for (id<MWMRoutingManagerListener> object in objects) {
if (speedLimit == routing::SpeedCameraOnRoute::kNoSpeedInfo) {
[object updateCameraInfo:YES speedLimit:nil];
} else {
auto const units = coreUnits([MWMSettings measurementUnits]);
NSString *limit = @(measurement_utils::FormatSpeedLimit(speedLimit, units).c_str());
[object updateCameraInfo:YES speedLimit:limit];
}
}
}
- (void)speedCameraLeftVisibleArea {
NSArray<id<MWMRoutingManagerListener>> * objects = self.listeners.allObjects;
for (id<MWMRoutingManagerListener> object in objects) {
[object updateCameraInfo:NO speedLimit:nil];
}
}
#pragma mark - MWMLocationObserver implementation
- (void)onLocationUpdate:(location::GpsInfo const &)gpsInfo {
NSMutableArray<NSString *> * turnNotifications = [NSMutableArray array];
vector<string> notifications;
self.rm.GenerateNotifications(notifications);
for (auto const & text : notifications) {
[turnNotifications addObject:@(text.c_str())];
}
NSArray<id<MWMRoutingManagerListener>> * objects = self.listeners.allObjects;
for (id<MWMRoutingManagerListener> object in objects) {
[object didLocationUpdate:turnNotifications];
}
}
- (NSString *)turnImageName:(routing::turns::CarDirection)turn isPrimary:(BOOL)isPrimary {
using namespace routing::turns;
NSString *imageName = nil;
switch (turn) {
case CarDirection::ExitHighwayToRight: imageName = @"ic_cp_exit_highway_to_right"; break;
case CarDirection::TurnSlightRight: imageName = @"ic_cp_slight_right"; break;
case CarDirection::TurnRight: imageName = @"ic_cp_simple_right"; break;
case CarDirection::TurnSharpRight: imageName = @"ic_cp_sharp_right"; break;
case CarDirection::ExitHighwayToLeft: imageName = @"ic_cp_exit_highway_to_left"; break;
case CarDirection::TurnSlightLeft: imageName = @"ic_cp_slight_left"; break;
case CarDirection::TurnLeft: imageName = @"ic_cp_simple_left"; break;
case CarDirection::TurnSharpLeft: imageName = @"ic_cp_sharp_left"; break;
case CarDirection::UTurnLeft: imageName = @"ic_cp_uturn_left"; break;
case CarDirection::UTurnRight: imageName = @"ic_cp_uturn_right"; break;
case CarDirection::ReachedYourDestination: imageName = @"ic_cp_finish_point"; break;
case CarDirection::LeaveRoundAbout:
case CarDirection::EnterRoundAbout: imageName = @"ic_cp_round"; break;
case CarDirection::GoStraight: imageName = @"ic_cp_straight"; break;
case CarDirection::StartAtEndOfStreet:
case CarDirection::StayOnRoundAbout:
case CarDirection::Count:
case CarDirection::None: imageName = isPrimary ? @"ic_cp_straight" : nil; break;
}
if (!isPrimary && imageName != nil) {
imageName = [NSString stringWithFormat:@"%@_then", imageName];
}
return imageName;
}
@end

View file

@ -0,0 +1,5 @@
typedef NS_ENUM(NSUInteger, MWMSpeedCameraManagerMode) {
MWMSpeedCameraManagerModeAuto,
MWMSpeedCameraManagerModeAlways,
MWMSpeedCameraManagerModeNever
} NS_SWIFT_NAME(SpeedCameraManagerMode);

View file

@ -31,14 +31,28 @@ static inline NSString * formattedSpeedAndAltitude(CLLocation * location)
return result;
}
static inline NSString * formattedDistance(double const & meters)
{
static inline NSString * formattedDistance(double const & meters) {
if (meters < 0.)
return nil;
string s;
measurement_utils::FormatDistance(meters, s);
return @(s.c_str());
auto units = measurement_utils::Units::Metric;
settings::TryGet(settings::kMeasurementUnits, units);
string distance;
switch (units)
{
case measurement_utils::Units::Imperial:
measurement_utils::FormatDistanceWithLocalization(meters,
distance,
[[@" " stringByAppendingString:L(@"mile")] UTF8String],
[[@" " stringByAppendingString:L(@"foot")] UTF8String]);
case measurement_utils::Units::Metric:
measurement_utils::FormatDistanceWithLocalization(meters,
distance,
[[@" " stringByAppendingString:L(@"kilometer")] UTF8String],
[[@" " stringByAppendingString:L(@"meter")] UTF8String]);
}
return @(distance.c_str());
}
static inline BOOL isMyPositionPendingOrNoPosition()

View file

@ -21,7 +21,6 @@
- (instancetype)init __attribute__((unavailable("call +manager instead")));
- (instancetype)copy __attribute__((unavailable("call +manager instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +manager instead")));
+ (instancetype)alloc __attribute__((unavailable("call +manager instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone
__attribute__((unavailable("call +manager instead")));
+ (instancetype) new __attribute__((unavailable("call +manager instead")));

View file

@ -171,7 +171,7 @@ void setPermissionRequested()
static MWMLocationManager * manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[super alloc] initManager];
manager = [[self alloc] initManager];
});
return manager;
}

View file

@ -9,6 +9,12 @@ typedef NS_ENUM(NSUInteger, MWMRoutePointType) {
- (instancetype)initWithLastLocationAndType:(MWMRoutePointType)type
intermediateIndex:(size_t)intermediateIndex;
- (instancetype)initWithCGPoint:(CGPoint)point
title:(NSString *)title
subtitle:(NSString *)subtitle
type:(MWMRoutePointType)type
intermediateIndex:(size_t)intermediateIndex;
@property(copy, nonatomic, readonly) NSString * title;
@property(copy, nonatomic, readonly) NSString * subtitle;
@property(copy, nonatomic, readonly) NSString * latLonString;

View file

@ -79,6 +79,21 @@
return self;
}
- (instancetype)initWithCGPoint:(CGPoint)point
title:(NSString *)title
subtitle:(NSString *)subtitle
type:(MWMRoutePointType)type
intermediateIndex:(size_t)intermediateIndex
{
auto const pointD = m2::PointD(point.x, point.y);
self = [self initWithPoint:pointD
title:title
subtitle:subtitle
type:type intermediateIndex:intermediateIndex];
return self;
}
- (instancetype)initWithPoint:(m2::PointD const &)point
title:(NSString *)title
subtitle:(NSString *)subtitle

View file

@ -12,6 +12,9 @@ typedef void (^MWMImageHeightBlock)(UIImage *, NSString *);
@interface MWMRouter : NSObject
+ (void)subscribeToEvents;
+ (void)unsubscribeFromEvents;
+ (BOOL)isTaxi;
+ (BOOL)isRoutingActive;
+ (BOOL)isRouteBuilt;
@ -60,10 +63,20 @@ typedef void (^MWMImageHeightBlock)(UIImage *, NSString *);
+ (void)saveRouteIfNeeded;
+ (void)restoreRouteIfNeeded;
+ (BOOL)hasSavedRoute;
+ (BOOL)isRestoreProcessCompleted;
+ (void)updateRoute;
+ (BOOL)hasActiveDrivingOptions;
+ (void)avoidRoadTypeAndRebuild:(MWMRoadType)type;
+ (void)showNavigationMapControls;
+ (void)hideNavigationMapControls;
- (instancetype)init __attribute__((unavailable("call +router instead")));
- (instancetype)copy __attribute__((unavailable("call +router instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +router instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone
__attribute__((unavailable("call +router instead")));
+ (instancetype) new __attribute__((unavailable("call +router instead")));
@end

View file

@ -38,6 +38,7 @@ using namespace routing;
@property(nonatomic) uint32_t routeManagerTransactionId;
@property(nonatomic) BOOL canAutoAddLastLocation;
@property(nonatomic) BOOL isAPICall;
@property(nonatomic) BOOL isRestoreProcessCompleted;
@property(strong, nonatomic) MWMRoutingOptions * routingOptions;
+ (MWMRouter *)router;
@ -85,7 +86,7 @@ void logPointEvent(MWMRoutePoint * point, NSString * eventType)
static MWMRouter * router;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
router = [[super alloc] initRouter];
router = [[self alloc] initRouter];
});
return router;
}
@ -230,10 +231,23 @@ void logPointEvent(MWMRoutePoint * point, NSString * eventType)
[MWMFrameworkListener addObserver:self];
_canAutoAddLastLocation = YES;
_routingOptions = [MWMRoutingOptions new];
_isRestoreProcessCompleted = NO;
}
return self;
}
+ (void)subscribeToEvents
{
[MWMFrameworkListener addObserver:[MWMRouter router]];
[MWMLocationManager addObserver:[MWMRouter router]];
}
+ (void)unsubscribeFromEvents
{
[MWMFrameworkListener removeObserver:[MWMRouter router]];
[MWMLocationManager removeObserver:[MWMRouter router]];
}
+ (void)setType:(MWMRouterType)type
{
if (type == self.type)
@ -507,7 +521,7 @@ void logPointEvent(MWMRoutePoint * point, NSString * eventType)
// Don't save taxi routing type as default.
if ([MWMRouter isTaxi])
GetFramework().GetRoutingManager().SetRouter(routing::RouterType::Vehicle);
[[MWMMapViewControlsManager manager] onRouteStop];
[self hideNavigationMapControls];
[MWMRouter router].canAutoAddLastLocation = YES;
}
@ -612,9 +626,10 @@ void logPointEvent(MWMRoutePoint * point, NSString * eventType)
if (![MWMRouter isRoutingActive])
return;
auto tts = [MWMTextToSpeech tts];
NSArray<NSString *> *turnNotifications = [MWMRouter turnNotifications];
if ([MWMRouter isOnRoute] && tts.active)
{
[tts playTurnNotifications];
[tts playTurnNotifications:turnNotifications];
[tts playWarningSound];
}
@ -763,12 +778,15 @@ void logPointEvent(MWMRoutePoint * point, NSString * eventType)
if ([MapsAppDelegate theApp].isDrapeEngineCreated)
{
auto & rm = GetFramework().GetRoutingManager();
if ([self isRoutingActive] || ![self hasSavedRoute])
if ([self isRoutingActive] || ![self hasSavedRoute]) {
self.router.isRestoreProcessCompleted = YES;
return;
}
rm.LoadRoutePoints([self](bool success)
{
if (success)
[self rebuildWithBestRouter:YES];
self.router.isRestoreProcessCompleted = YES;
});
}
else
@ -779,6 +797,11 @@ void logPointEvent(MWMRoutePoint * point, NSString * eventType)
}
}
+ (BOOL)isRestoreProcessCompleted
{
return self.router.isRestoreProcessCompleted;
}
+ (BOOL)hasSavedRoute
{
return GetFramework().GetRoutingManager().HasSavedRoutePoints();
@ -816,4 +839,12 @@ void logPointEvent(MWMRoutePoint * point, NSString * eventType)
[self rebuildWithBestRouter:YES];
}
+ (void)showNavigationMapControls {
[[MWMMapViewControlsManager manager] onRouteStart];
}
+ (void)hideNavigationMapControls {
[[MWMMapViewControlsManager manager] onRouteStop];
}
@end

View file

@ -42,11 +42,11 @@ struct ProductInfo;
+ (MWMHotelParams *)getFilter;
+ (void)clearFilter;
- (instancetype)init __attribute__((unavailable("unavailable")));
- (instancetype)copy __attribute__((unavailable("unavailable")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("unavailable")));
+ (instancetype)alloc __attribute__((unavailable("unavailable")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable("unavailable")));
+ (instancetype) new __attribute__((unavailable("unavailable")));
- (instancetype)init __attribute__((unavailable("call +manager instead")));
- (instancetype)copy __attribute__((unavailable("call +manager instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +manager instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone
__attribute__((unavailable("call +manager instead")));
+ (instancetype) new __attribute__((unavailable("call +manager instead")));
@end

View file

@ -96,7 +96,7 @@ booking::filter::Tasks MakeBookingFilterTasks(booking::filter::Params && availab
static MWMSearch * manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[super alloc] initManager];
manager = [[self alloc] initManager];
});
return manager;
}

View file

@ -2,6 +2,7 @@
NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(RoutingOptions)
@interface MWMRoutingOptions : NSObject
@property(nonatomic) BOOL avoidToll;

View file

@ -115,6 +115,16 @@ NSString * const kCrashReportingDisabled = @"CrashReportingDisabled";
+ (MWMTheme)theme
{
if (@available(iOS 12.0, *)) {
if ([MWMCarPlayService shared].isCarplayActivated) {
UIUserInterfaceStyle style = [[MWMCarPlayService shared] interfaceStyle];
switch (style) {
case UIUserInterfaceStyleLight: return MWMThemeDay;
case UIUserInterfaceStyleDark: return MWMThemeNight;
case UIUserInterfaceStyleUnspecified: break;
}
}
}
auto ud = NSUserDefaults.standardUserDefaults;
if (![ud boolForKey:kUDAutoNightModeOff])
return MWMThemeAuto;

View file

@ -14,15 +14,14 @@
@property(nonatomic) BOOL active;
- (void)setNotificationsLocale:(NSString *)locale;
- (void)playTurnNotifications;
- (void)playTurnNotifications:(NSArray<NSString *> *)turnNotifications;
- (void)playWarningSound;
- (instancetype)init __attribute__((unavailable("call tts instead")));
- (instancetype)copy __attribute__((unavailable("call tts instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call tts instead")));
+ (instancetype)alloc __attribute__((unavailable("call tts instead")));
- (instancetype)init __attribute__((unavailable("call +tts instead")));
- (instancetype)copy __attribute__((unavailable("call +tts instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +tts instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone
__attribute__((unavailable("call tts instead")));
+ (instancetype) new __attribute__((unavailable("call tts instead")));
__attribute__((unavailable("call +tts instead")));
+ (instancetype) new __attribute__((unavailable("call +tts instead")));
@end

View file

@ -4,6 +4,7 @@
#import "MWMRouter.h"
#import "MWMTextToSpeech+CPP.h"
#import "Statistics.h"
#import "SwiftBridge.h"
#include "LocaleTranslator.h"
@ -65,28 +66,24 @@ using Observers = NSHashTable<Observer>;
@implementation MWMTextToSpeech
+ (MWMTextToSpeech *)tts
{
+ (MWMTextToSpeech *)tts {
static dispatch_once_t onceToken;
static MWMTextToSpeech * tts = nil;
dispatch_once(&onceToken, ^{
tts = [[super alloc] initTTS];
tts = [[self alloc] initTTS];
});
return tts;
}
+ (void)applicationDidBecomeActive
{
+ (void)applicationDidBecomeActive {
auto tts = [self tts];
tts.speechSynthesizer = nil;
tts.speechVoice = nil;
}
- (instancetype)initTTS
{
- (instancetype)initTTS {
self = [super init];
if (self)
{
if (self) {
_availableLanguages = availableLanguages();
_observers = [Observers weakObjectsHashTable];
@ -111,8 +108,7 @@ using Observers = NSHashTable<Observer>;
if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers |
AVAudioSessionCategoryOptionDuckOthers
error:&err])
{
error:&err]) {
LOG(LWARNING, ("[ setCategory]] error.", [err localizedDescription]));
}
@ -121,16 +117,14 @@ using Observers = NSHashTable<Observer>;
return self;
}
- (void)dealloc
{
- (void)dealloc {
[[AVAudioSession sharedInstance] setActive:NO
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
error:nil];
self.speechSynthesizer.delegate = nil;
}
- (std::vector<std::pair<string, string>>)availableLanguages { return _availableLanguages; }
- (void)setNotificationsLocale:(NSString *)locale
{
- (void)setNotificationsLocale:(NSString *)locale {
[Statistics logEvent:kStatEventName(kStatTTSSettings, kStatChangeLanguage)
withParameters:@{kStatValue : locale}];
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
@ -141,8 +135,7 @@ using Observers = NSHashTable<Observer>;
- (BOOL)isValid { return _speechSynthesizer != nil && _speechVoice != nil; }
+ (BOOL)isTTSEnabled { return [NSUserDefaults.standardUserDefaults boolForKey:kIsTTSEnabled]; }
+ (void)setTTSEnabled:(BOOL)enabled
{
+ (void)setTTSEnabled:(BOOL)enabled {
if ([self isTTSEnabled] == enabled)
return;
auto tts = [self tts];
@ -157,8 +150,7 @@ using Observers = NSHashTable<Observer>;
[tts setActive:YES];
}
- (void)setActive:(BOOL)active
{
- (void)setActive:(BOOL)active {
if (![[self class] isTTSEnabled] || self.active == active)
return;
if (active && ![self isValid])
@ -170,15 +162,12 @@ using Observers = NSHashTable<Observer>;
}
- (BOOL)active { return [[self class] isTTSEnabled] && [MWMRouter areTurnNotificationsEnabled]; }
+ (NSString *)savedLanguage
{
+ (NSString *)savedLanguage {
return [NSUserDefaults.standardUserDefaults stringForKey:kUserDefaultsTTSLanguageBcp47];
}
- (void)createVoice:(NSString *)locale
{
if (!self.speechSynthesizer)
{
- (void)createVoice:(NSString *)locale {
if (!self.speechSynthesizer) {
self.speechSynthesizer = [[AVSpeechSynthesizer alloc] init];
self.speechSynthesizer.delegate = self;
}
@ -190,53 +179,39 @@ using Observers = NSHashTable<Observer>;
else
LOG(LWARNING, ("locale is nil. Trying default locale."));
NSArray<AVSpeechSynthesisVoice *> * availTTSVoices = [AVSpeechSynthesisVoice speechVoices];
AVSpeechSynthesisVoice * voice = nil;
for (NSString * loc in candidateLocales)
{
for (NSString * loc in candidateLocales) {
if ([loc isEqualToString:@"en-US"])
voice = [AVSpeechSynthesisVoice voiceWithIdentifier:AVSpeechSynthesisVoiceIdentifierAlex];
if (voice)
break;
for (AVSpeechSynthesisVoice * ttsVoice in availTTSVoices)
{
if ([ttsVoice.language isEqualToString:loc])
{
voice = [AVSpeechSynthesisVoice voiceWithLanguage:loc];
break;
}
}
voice = [AVSpeechSynthesisVoice voiceWithLanguage:loc];
if (voice)
break;
}
self.speechVoice = voice;
if (voice)
{
if (voice) {
string const twineLang = bcp47ToTwineLanguage(voice.language);
if (twineLang.empty())
LOG(LERROR, ("Cannot convert UI locale or default locale to twine language. MWMTextToSpeech "
"is invalid."));
else
[MWMRouter setTurnNotificationsLocale:@(twineLang.c_str())];
}
else
{
} else {
LOG(LERROR,
("The UI language and English are not available for TTS. MWMTextToSpeech is invalid."));
}
}
- (void)speakOneString:(NSString *)textToSpeak
{
- (void)speakOneString:(NSString *)textToSpeak {
AVSpeechUtterance * utterance = [AVSpeechUtterance speechUtteranceWithString:textToSpeak];
utterance.voice = self.speechVoice;
utterance.rate = AVSpeechUtteranceDefaultSpeechRate;
[self.speechSynthesizer speakUtterance:utterance];
}
- (void)playTurnNotifications
{
- (void)playTurnNotifications:(NSArray<NSString *> *)turnNotifications {
auto stopSession = ^{
if (self.speechSynthesizer.isSpeaking)
return;
@ -246,8 +221,7 @@ using Observers = NSHashTable<Observer>;
error:nil];
};
if (![MWMRouter isOnRoute] || !self.active)
{
if (![MWMRouter isOnRoute] || !self.active) {
stopSession();
return;
}
@ -255,21 +229,26 @@ using Observers = NSHashTable<Observer>;
if (![self isValid])
[self createVoice:[[self class] savedLanguage]];
if (![self isValid])
{
if (![self isValid]) {
stopSession();
return;
}
NSArray<NSString *> * turnNotifications = [MWMRouter turnNotifications];
if (turnNotifications.count == 0)
{
if (turnNotifications.count == 0) {
stopSession();
return;
}
else
{
if (![[AVAudioSession sharedInstance] setActive:YES error:nil])
} else {
AVAudioSessionMode mode = AVAudioSessionModeDefault;
if (@available(iOS 12.0, *)) {
if ([MWMCarPlayService shared].isCarplayActivated) {
mode = AVAudioSessionModeVoicePrompt;
}
}
if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
mode:mode
options:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDuckOthers
error:nil] ||
![[AVAudioSession sharedInstance] setActive:YES error:nil])
return;
for (NSString * notification in turnNotifications)
@ -277,26 +256,20 @@ using Observers = NSHashTable<Observer>;
}
}
- (void)playWarningSound
{
- (void)playWarningSound {
if (!GetFramework().GetRoutingManager().GetSpeedCamManager().ShouldPlayBeepSignal())
return;
[self.audioPlayer play];
}
- (AVAudioPlayer *)audioPlayer
{
if (!_audioPlayer)
{
if (auto url = [[NSBundle mainBundle] URLForResource:@"Alert 5" withExtension:@"m4a"])
{
- (AVAudioPlayer *)audioPlayer {
if (!_audioPlayer) {
if (auto url = [[NSBundle mainBundle] URLForResource:@"Alert 5" withExtension:@"m4a"]) {
NSError * error = nil;
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
CHECK(!error, (error.localizedDescription.UTF8String));
}
else
{
} else {
CHECK(false, ("Speed warning file not found"));
}
}
@ -306,21 +279,18 @@ using Observers = NSHashTable<Observer>;
#pragma mark - MWMNavigationDashboardObserver
- (void)onTTSStatusUpdated
{
- (void)onTTSStatusUpdated {
for (Observer observer in self.observers)
[observer onTTSStatusUpdated];
}
#pragma mark - Add/Remove Observers
+ (void)addObserver:(id<MWMTextToSpeechObserver>)observer
{
+ (void)addObserver:(id<MWMTextToSpeechObserver>)observer {
[[self tts].observers addObject:observer];
}
+ (void)removeObserver:(id<MWMTextToSpeechObserver>)observer
{
+ (void)removeObserver:(id<MWMTextToSpeechObserver>)observer {
[[self tts].observers removeObject:observer];
}

View file

@ -38,4 +38,11 @@ typedef NS_ENUM(NSUInteger, MWMTransitManagerState) {
+ (void)setTrafficEnabled:(BOOL)enable;
+ (void)setTransitEnabled:(BOOL)enable;
- (instancetype)init __attribute__((unavailable("call +manager instead")));
- (instancetype)copy __attribute__((unavailable("call +manager instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +manager instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone
__attribute__((unavailable("call +manager instead")));
+ (instancetype) new __attribute__((unavailable("call +manager instead")));
@end

View file

@ -26,7 +26,7 @@ using Observers = NSHashTable<Observer>;
static MWMTrafficManager * manager;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
manager = [[super alloc] initManager];
manager = [[self alloc] initManager];
});
return manager;
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_exit_highway_to_left_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_exit_highway_to_right_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_round_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_sharp_left_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_sharp_right_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_simple_left_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_simple_right_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_slight_left_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_slight_right_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_straight_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_uturn_left_then.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_cp_uturn_right_then.pdf",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Some files were not shown because too many files have changed in this diff Show more