Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
|
|
@ -0,0 +1,57 @@
|
|||
@objc(MWMBackgroundFetchScheduler)
|
||||
final class BackgroundFetchScheduler: NSObject {
|
||||
typealias FetchResultHandler = (UIBackgroundFetchResult) -> Void
|
||||
|
||||
private let completionHandler: FetchResultHandler
|
||||
private let tasks: [BackgroundFetchTask]
|
||||
private var tasksLeft: Int
|
||||
private var bestResultSoFar = UIBackgroundFetchResult.noData
|
||||
|
||||
@objc init(tasks: [BackgroundFetchTask], completionHandler: @escaping FetchResultHandler) {
|
||||
self.tasks = tasks
|
||||
self.completionHandler = completionHandler
|
||||
tasksLeft = tasks.count
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc func run() {
|
||||
fullfillFrameworkRequirements()
|
||||
|
||||
let completionHandler: FetchResultHandler = { [weak self] result in
|
||||
self?.finishTask(result: result)
|
||||
}
|
||||
|
||||
tasks.forEach { $0.start(completion: completionHandler) }
|
||||
}
|
||||
|
||||
private func fullfillFrameworkRequirements() {
|
||||
minFrameworkTypeRequired().create()
|
||||
}
|
||||
|
||||
private func minFrameworkTypeRequired() -> BackgroundFetchTaskFrameworkType {
|
||||
return tasks.reduce(.none) { max($0, $1.frameworkType) }
|
||||
}
|
||||
|
||||
private func finishTask(result: UIBackgroundFetchResult) {
|
||||
updateFetchResult(result)
|
||||
tasksLeft -= 1
|
||||
if tasksLeft <= 0 {
|
||||
completionHandler(bestResultSoFar)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateFetchResult(_ result: UIBackgroundFetchResult) {
|
||||
if resultPriority(bestResultSoFar) < resultPriority(result) {
|
||||
bestResultSoFar = result
|
||||
}
|
||||
}
|
||||
|
||||
private func resultPriority(_ result: UIBackgroundFetchResult) -> Int {
|
||||
switch result {
|
||||
case .newData: return 3
|
||||
case .noData: return 1
|
||||
case .failed: return 2
|
||||
@unknown default: fatalError("Unexpected case in UIBackgroundFetchResult switch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
@objc class BackgroundFetchTask: NSObject {
|
||||
var frameworkType: BackgroundFetchTaskFrameworkType { return .none }
|
||||
|
||||
private var backgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
||||
|
||||
private var completionHandler: BackgroundFetchScheduler.FetchResultHandler?
|
||||
|
||||
func start(completion: @escaping BackgroundFetchScheduler.FetchResultHandler) {
|
||||
completionHandler = completion
|
||||
backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName:description,
|
||||
expirationHandler: {
|
||||
self.finish(.failed)
|
||||
})
|
||||
if backgroundTaskIdentifier != UIBackgroundTaskIdentifier.invalid { fire() }
|
||||
}
|
||||
|
||||
fileprivate func fire() {
|
||||
finish(.failed)
|
||||
}
|
||||
|
||||
fileprivate func finish(_ result: UIBackgroundFetchResult) {
|
||||
guard backgroundTaskIdentifier != UIBackgroundTaskIdentifier.invalid else { return }
|
||||
UIApplication.shared.endBackgroundTask(UIBackgroundTaskIdentifier(rawValue: backgroundTaskIdentifier.rawValue))
|
||||
backgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(MWMBackgroundEditsUpload)
|
||||
final class BackgroundEditsUpload: BackgroundFetchTask {
|
||||
override fileprivate func fire() {
|
||||
MWMEditorHelper.uploadEdits(self.finish)
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "Edits upload"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
@objc enum BackgroundFetchTaskFrameworkType: Int {
|
||||
case none
|
||||
case full
|
||||
|
||||
func create() {
|
||||
switch self {
|
||||
case .none: return
|
||||
case .full: FrameworkHelper.createFramework()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BackgroundFetchTaskFrameworkType: Equatable {
|
||||
static func ==(lhs: BackgroundFetchTaskFrameworkType, rhs: BackgroundFetchTaskFrameworkType) -> Bool {
|
||||
return lhs.rawValue == rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension BackgroundFetchTaskFrameworkType: Comparable {
|
||||
static func <(lhs: BackgroundFetchTaskFrameworkType, rhs: BackgroundFetchTaskFrameworkType) -> Bool {
|
||||
return lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
159
iphone/Maps/Core/DeepLink/DeepLinkHandler.swift
Normal file
159
iphone/Maps/Core/DeepLink/DeepLinkHandler.swift
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
@objc @objcMembers class DeepLinkHandler: NSObject {
|
||||
static let shared = DeepLinkHandler()
|
||||
|
||||
private(set) var isLaunchedByDeeplink = false
|
||||
private(set) var isLaunchedByUniversalLink = false
|
||||
private(set) var url: URL?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ options: [UIApplication.LaunchOptionsKey : Any]? = nil) {
|
||||
if let launchDeeplink = options?[UIApplication.LaunchOptionsKey.url] as? URL {
|
||||
isLaunchedByDeeplink = true
|
||||
url = launchDeeplink
|
||||
}
|
||||
}
|
||||
|
||||
func applicationDidOpenUrl(_ url: URL) -> Bool {
|
||||
// File reading should be processed synchronously to avoid permission issues (the Files app will close the file for reading when the application:openURL:options returns).
|
||||
if url.isFileURL {
|
||||
return handleFileImport(url: url)
|
||||
}
|
||||
|
||||
// On the cold start, isLaunchedByDeeplink is set and handleDeepLink() call is delayed
|
||||
// until the map view will be fully initialized.
|
||||
guard !isLaunchedByDeeplink else { return true }
|
||||
|
||||
// On the hot start, link can be processed immediately.
|
||||
self.url = url
|
||||
return handleDeepLink(url: url)
|
||||
}
|
||||
|
||||
func applicationDidReceiveUniversalLink(_ universalLink: URL) -> Bool {
|
||||
// Convert http(s)://comaps.at/ENCODEDCOORDS/NAME to cm://ENCODEDCOORDS/NAME
|
||||
self.url = URL(string: universalLink.absoluteString
|
||||
.replacingOccurrences(of: "http://comaps.at", with: "cm:/")
|
||||
.replacingOccurrences(of: "https://comaps.at", with: "cm:/"))
|
||||
isLaunchedByUniversalLink = true
|
||||
return handleDeepLink(url: self.url!)
|
||||
}
|
||||
|
||||
func reset() {
|
||||
isLaunchedByDeeplink = false
|
||||
isLaunchedByUniversalLink = false
|
||||
url = nil
|
||||
}
|
||||
|
||||
func getBackUrl() -> String? {
|
||||
guard let urlString = url?.absoluteString else { return nil }
|
||||
guard let url = URLComponents(string: urlString) else { return nil }
|
||||
return (url.queryItems?.first(where: { $0.name == "backurl" })?.value ?? nil)
|
||||
}
|
||||
|
||||
func getInAppFeatureHighlightData() -> DeepLinkInAppFeatureHighlightData? {
|
||||
guard (isLaunchedByUniversalLink || isLaunchedByDeeplink), let url else { return nil }
|
||||
reset()
|
||||
return DeepLinkInAppFeatureHighlightData(DeepLinkParser.parseAndSetApiURL(url))
|
||||
}
|
||||
|
||||
func handleDeepLinkAndReset() -> Bool {
|
||||
if let url {
|
||||
let result = handleDeepLink(url: url)
|
||||
reset()
|
||||
return result
|
||||
}
|
||||
LOG(.error, "handleDeepLink is called with nil URL")
|
||||
return false
|
||||
}
|
||||
|
||||
private func handleFileImport(url: URL) -> Bool {
|
||||
LOG(.info, "handleFileImport: \(url)")
|
||||
let fileCoordinator = NSFileCoordinator()
|
||||
var error: NSError?
|
||||
fileCoordinator.coordinate(readingItemAt: url, options: [], error: &error) { fileURL in
|
||||
DeepLinkParser.addBookmarksFile(fileURL)
|
||||
}
|
||||
if let error {
|
||||
LOG(.error, "Failed to read file: \(error)")
|
||||
}
|
||||
reset()
|
||||
return true
|
||||
}
|
||||
|
||||
private func handleDeepLink(url: URL) -> Bool {
|
||||
LOG(.info, "handleDeepLink: \(url)")
|
||||
// TODO(AB): Rewrite API so iOS and Android will call only one C++ method to clear/set API state.
|
||||
// This call is also required for DeepLinkParser.showMap, and it also clears old API points...
|
||||
let urlType = DeepLinkParser.parseAndSetApiURL(url)
|
||||
LOG(.info, "URL type: \(urlType)")
|
||||
switch urlType {
|
||||
case .route:
|
||||
if let adapter = DeepLinkRouteStrategyAdapter(url) {
|
||||
MWMRouter.buildApiRoute(with: adapter.type, start: adapter.p1, finish: adapter.p2)
|
||||
MapsAppDelegate.theApp().showMap()
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
case .map:
|
||||
DeepLinkParser.executeMapApiRequest()
|
||||
MapsAppDelegate.theApp().showMap()
|
||||
return true
|
||||
case .search:
|
||||
let sd = DeepLinkSearchData();
|
||||
let kSearchInViewportZoom: Int32 = 16;
|
||||
// Set viewport only when cll parameter was provided in url.
|
||||
// Equator and Prime Meridian are perfectly valid separately.
|
||||
if (sd.hasValidCenterLatLon()) {
|
||||
MapViewController.setViewport(sd.centerLat, lon: sd.centerLon, zoomLevel: kSearchInViewportZoom)
|
||||
// Need to update viewport for search API manually because Drape engine
|
||||
// will not notify subscribers when search view is shown.
|
||||
if (!sd.isSearchOnMap) {
|
||||
sd.onViewportChanged(kSearchInViewportZoom)
|
||||
}
|
||||
}
|
||||
let searchQuery = SearchQuery(sd.query, locale: sd.locale, source: .deeplink)
|
||||
if (sd.isSearchOnMap) {
|
||||
MWMMapViewControlsManager.manager()?.search(onMap: searchQuery)
|
||||
} else {
|
||||
MWMMapViewControlsManager.manager()?.search(searchQuery)
|
||||
}
|
||||
return true
|
||||
case .menu:
|
||||
MapsAppDelegate.theApp().mapViewController.openMenu()
|
||||
return true
|
||||
case .settings:
|
||||
MapsAppDelegate.theApp().mapViewController.openSettings()
|
||||
return true
|
||||
case .crosshair:
|
||||
// Not supported on iOS.
|
||||
return false;
|
||||
case .oAuth2:
|
||||
var components = url.absoluteString.components(separatedBy: "cm://oauth2/osm/callback?code=")
|
||||
components.removeAll { component in
|
||||
component.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
if let code = components.first {
|
||||
Task(priority: .userInitiated) {
|
||||
await Profile.saveAuthorizationToken(from: code)
|
||||
DispatchQueue.main.sync {
|
||||
NotificationCenter.default.post(name: SafariView.dismissNotificationName, object: nil)
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .incorrect:
|
||||
if url.absoluteString.starts(with: "cm://oauth2/osm/callback") {
|
||||
NotificationCenter.default.post(name: SafariView.dismissNotificationName, object: nil)
|
||||
}
|
||||
// Invalid URL or API parameters.
|
||||
return false;
|
||||
@unknown default:
|
||||
LOG(.critical, "Unknown URL type: \(urlType)")
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import "MWMRouterType.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class MWMRoutePoint;
|
||||
@interface DeepLinkRouteStrategyAdapter : NSObject
|
||||
|
||||
@property(nonatomic, readonly) MWMRoutePoint* p1;
|
||||
@property(nonatomic, readonly) MWMRoutePoint* p2;
|
||||
@property(nonatomic, readonly) MWMRouterType type;
|
||||
|
||||
- (nullable instancetype)init:(NSURL*)url;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#import "DeepLinkRouteStrategyAdapter.h"
|
||||
#import <CoreApi/Framework.h>
|
||||
#import "MWMCoreRouterType.h"
|
||||
#import "MWMRoutePoint+CPP.h"
|
||||
|
||||
@implementation DeepLinkRouteStrategyAdapter
|
||||
|
||||
- (instancetype)init:(NSURL *)url {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
auto const parsedData = GetFramework().GetParsedRoutingData();
|
||||
auto const points = parsedData.m_points;
|
||||
|
||||
if (points.size() == 2) {
|
||||
_p1 = [[MWMRoutePoint alloc] initWithURLSchemeRoutePoint:points.front()
|
||||
type:MWMRoutePointTypeStart
|
||||
intermediateIndex:0];
|
||||
_p2 = [[MWMRoutePoint alloc] initWithURLSchemeRoutePoint:points.back()
|
||||
type:MWMRoutePointTypeFinish
|
||||
intermediateIndex:0];
|
||||
_type = routerType(parsedData.m_type);
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
5
iphone/Maps/Core/Editor/MWMEditorHelper.h
Normal file
5
iphone/Maps/Core/Editor/MWMEditorHelper.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@interface MWMEditorHelper : NSObject
|
||||
|
||||
+ (void)uploadEdits:(void (^)(UIBackgroundFetchResult))completionHandler;
|
||||
|
||||
@end
|
||||
50
iphone/Maps/Core/Editor/MWMEditorHelper.mm
Normal file
50
iphone/Maps/Core/Editor/MWMEditorHelper.mm
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#import "MWMEditorHelper.h"
|
||||
#import <CoreApi/AppInfo.h>
|
||||
#import "SwiftBridge.h"
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include "editor/osm_editor.hpp"
|
||||
|
||||
@implementation MWMEditorHelper
|
||||
|
||||
+ (void)uploadEdits:(void (^)(UIBackgroundFetchResult))completionHandler
|
||||
{
|
||||
if (!Profile.isExisting ||
|
||||
Platform::EConnectionType::CONNECTION_NONE == Platform::ConnectionStatus())
|
||||
{
|
||||
completionHandler(UIBackgroundFetchResultFailed);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const lambda = [completionHandler](osm::Editor::UploadResult result) {
|
||||
switch (result)
|
||||
{
|
||||
case osm::Editor::UploadResult::Success:
|
||||
completionHandler(UIBackgroundFetchResultNewData);
|
||||
break;
|
||||
case osm::Editor::UploadResult::Error:
|
||||
completionHandler(UIBackgroundFetchResultFailed);
|
||||
break;
|
||||
case osm::Editor::UploadResult::NothingToUpload:
|
||||
completionHandler(UIBackgroundFetchResultNoData);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
NSString *authorizationToken = Profile.authorizationToken;
|
||||
if (authorizationToken == nil) {
|
||||
authorizationToken = @"";
|
||||
}
|
||||
std::string const oauthToken = std::string([authorizationToken UTF8String]);
|
||||
osm::Editor::Instance().UploadChanges(
|
||||
oauthToken,
|
||||
{{"created_by",
|
||||
std::string("CoMaps " OMIM_OS_NAME " ") + AppInfo.sharedInfo.bundleVersion.UTF8String},
|
||||
{"bundle_id", NSBundle.mainBundle.bundleIdentifier.UTF8String}},
|
||||
lambda);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
60
iphone/Maps/Core/EventListening/ListenerContainer.swift
Normal file
60
iphone/Maps/Core/EventListening/ListenerContainer.swift
Normal 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<TT> {
|
||||
private weak var weakValue: AnyObject?
|
||||
|
||||
init(value: TT) {
|
||||
self.weakValue = value as AnyObject?
|
||||
}
|
||||
|
||||
var value: TT? {
|
||||
return weakValue as? TT
|
||||
}
|
||||
}
|
||||
|
||||
// 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?)
|
||||
}
|
||||
|
||||
20
iphone/Maps/Core/Framework/MWMFrameworkListener.h
Normal file
20
iphone/Maps/Core/Framework/MWMFrameworkListener.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#import "MWMFrameworkObserver.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MWMFrameworkListener : NSObject
|
||||
|
||||
+ (MWMFrameworkListener *)listener;
|
||||
+ (void)addObserver:(id<MWMFrameworkObserver>)observer;
|
||||
+ (void)removeObserver:(id<MWMFrameworkObserver>)observer;
|
||||
|
||||
- (instancetype)init __attribute__((unavailable("call +listener instead")));
|
||||
- (instancetype)copy __attribute__((unavailable("call +listener instead")));
|
||||
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +listener instead")));
|
||||
+ (instancetype)allocWithZone:(struct _NSZone *)zone
|
||||
__attribute__((unavailable("call +listener instead")));
|
||||
+ (instancetype) new __attribute__((unavailable("call +listener instead")));
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
156
iphone/Maps/Core/Framework/MWMFrameworkListener.mm
Normal file
156
iphone/Maps/Core/Framework/MWMFrameworkListener.mm
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#import "MWMFrameworkListener.h"
|
||||
#import "MWMFrameworkObservers.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
#include "platform/downloader_defines.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using Observer = id<MWMFrameworkObserver>;
|
||||
using TRouteBuildingObserver = id<MWMFrameworkRouteBuilderObserver>;
|
||||
using TDrapeObserver = id<MWMFrameworkDrapeObserver>;
|
||||
|
||||
using Observers = NSHashTable<Observer>;
|
||||
|
||||
Protocol * pRouteBuildingObserver = @protocol(MWMFrameworkRouteBuilderObserver);
|
||||
Protocol * pDrapeObserver = @protocol(MWMFrameworkDrapeObserver);
|
||||
|
||||
using TLoopBlock = void (^)(__kindof Observer observer);
|
||||
|
||||
void loopWrappers(Observers * observers, TLoopBlock block)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
for (Observer observer in observers)
|
||||
{
|
||||
if (observer)
|
||||
block(observer);
|
||||
}
|
||||
});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@interface MWMFrameworkListener ()
|
||||
|
||||
@property(nonatomic) Observers * routeBuildingObservers;
|
||||
@property(nonatomic) Observers * drapeObservers;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMFrameworkListener
|
||||
|
||||
+ (MWMFrameworkListener *)listener
|
||||
{
|
||||
static MWMFrameworkListener * listener;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
listener = [[super alloc] initListener];
|
||||
});
|
||||
return listener;
|
||||
}
|
||||
|
||||
+ (void)addObserver:(Observer)observer
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
MWMFrameworkListener * listener = [MWMFrameworkListener listener];
|
||||
if ([observer conformsToProtocol:pRouteBuildingObserver])
|
||||
[listener.routeBuildingObservers addObject:observer];
|
||||
if ([observer conformsToProtocol:pDrapeObserver])
|
||||
[listener.drapeObservers addObject:observer];
|
||||
});
|
||||
}
|
||||
|
||||
+ (void)removeObserver:(Observer)observer
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
MWMFrameworkListener * listener = [MWMFrameworkListener listener];
|
||||
[listener.routeBuildingObservers removeObject:observer];
|
||||
[listener.drapeObservers removeObject:observer];
|
||||
});
|
||||
}
|
||||
|
||||
- (instancetype)initListener
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_routeBuildingObservers = [Observers weakObjectsHashTable];
|
||||
_drapeObservers = [Observers weakObjectsHashTable];
|
||||
|
||||
[self registerRouteBuilderListener];
|
||||
[self registerDrapeObserver];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - MWMFrameworkRouteBuilderObserver
|
||||
|
||||
- (void)registerRouteBuilderListener
|
||||
{
|
||||
using namespace routing;
|
||||
using namespace storage;
|
||||
Observers * observers = self.routeBuildingObservers;
|
||||
auto & rm = GetFramework().GetRoutingManager();
|
||||
rm.SetRouteBuildingListener(
|
||||
[observers](RouterResultCode code, CountriesSet const & absentCountries) {
|
||||
loopWrappers(observers, [code, absentCountries](TRouteBuildingObserver observer) {
|
||||
[observer processRouteBuilderEvent:code countries:absentCountries];
|
||||
});
|
||||
});
|
||||
rm.SetRouteProgressListener([observers](float progress) {
|
||||
loopWrappers(observers, [progress](TRouteBuildingObserver observer) {
|
||||
if ([observer respondsToSelector:@selector(processRouteBuilderProgress:)])
|
||||
[observer processRouteBuilderProgress:progress];
|
||||
});
|
||||
});
|
||||
rm.SetRouteRecommendationListener([observers](RoutingManager::Recommendation recommendation) {
|
||||
MWMRouterRecommendation rec;
|
||||
switch (recommendation)
|
||||
{
|
||||
case RoutingManager::Recommendation::RebuildAfterPointsLoading:
|
||||
rec = MWMRouterRecommendationRebuildAfterPointsLoading;
|
||||
break;
|
||||
}
|
||||
loopWrappers(observers, [rec](TRouteBuildingObserver observer) {
|
||||
if ([observer respondsToSelector:@selector(processRouteRecommendation:)])
|
||||
[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 - MWMFrameworkDrapeObserver
|
||||
|
||||
- (void)registerDrapeObserver
|
||||
{
|
||||
Observers * observers = self.drapeObservers;
|
||||
auto & f = GetFramework();
|
||||
f.SetCurrentCountryChangedListener([observers](CountryId const & countryId) {
|
||||
for (TDrapeObserver observer in observers)
|
||||
{
|
||||
if ([observer respondsToSelector:@selector(processViewportCountryEvent:)])
|
||||
[observer processViewportCountryEvent:countryId];
|
||||
}
|
||||
});
|
||||
|
||||
f.SetViewportListener([observers](ScreenBase const & screen) {
|
||||
for (TDrapeObserver observer in observers)
|
||||
{
|
||||
if ([observer respondsToSelector:@selector(processViewportChangedEvent)])
|
||||
[observer processViewportChangedEvent];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
3
iphone/Maps/Core/Framework/MWMFrameworkObserver.h
Normal file
3
iphone/Maps/Core/Framework/MWMFrameworkObserver.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@protocol MWMFrameworkObserver<NSObject>
|
||||
|
||||
@end
|
||||
35
iphone/Maps/Core/Framework/MWMFrameworkObservers.h
Normal file
35
iphone/Maps/Core/Framework/MWMFrameworkObservers.h
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#import "MWMFrameworkObserver.h"
|
||||
#import "MWMRouterRecommendation.h"
|
||||
|
||||
#include "routing/router.hpp"
|
||||
#include "routing/routing_callbacks.hpp"
|
||||
|
||||
#include "storage/storage.hpp"
|
||||
#include "storage/storage_defines.hpp"
|
||||
|
||||
#include "platform/downloader_defines.hpp"
|
||||
|
||||
using namespace storage;
|
||||
|
||||
@protocol MWMFrameworkRouteBuilderObserver<MWMFrameworkObserver>
|
||||
|
||||
- (void)processRouteBuilderEvent:(routing::RouterResultCode)code
|
||||
countries:(storage::CountriesSet const &)absentCountries;
|
||||
|
||||
@optional
|
||||
|
||||
- (void)processRouteBuilderProgress:(CGFloat)progress;
|
||||
- (void)processRouteRecommendation:(MWMRouterRecommendation)recommendation;
|
||||
- (void)speedCameraShowedUpOnRoute:(double)speedLimitKMph;
|
||||
- (void)speedCameraLeftVisibleArea;
|
||||
|
||||
@end
|
||||
|
||||
@protocol MWMFrameworkDrapeObserver<MWMFrameworkObserver>
|
||||
|
||||
@optional
|
||||
|
||||
- (void)processViewportCountryEvent:(storage::CountryId const &)countryId;
|
||||
- (void)processViewportChangedEvent;
|
||||
|
||||
@end
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
typedef NS_CLOSED_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);
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
#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 speedLimitMps:(double)limit NS_SWIFT_NAME(updateCameraInfo(isCameraOnRoute:speedLimitMps:));
|
||||
@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;
|
||||
|
||||
- (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;
|
||||
- (void)setOnNewTurnCallback:(MWMVoidBlock)callback;
|
||||
- (void)resetOnNewTurnCallback;
|
||||
|
||||
- (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
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
#import "MWMRoutingManager.h"
|
||||
#import "MWMLocationManager.h"
|
||||
#import "MWMLocationObserver.h"
|
||||
#import "MWMFrameworkListener.h"
|
||||
#import "MWMFrameworkObservers.h"
|
||||
#import "MWMCoreRouterType.h"
|
||||
#import "MWMRoutePoint+CPP.h"
|
||||
#import "MWMCoreUnits.h"
|
||||
#import "SwiftBridge.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
@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;
|
||||
routing::FollowingInfo info;
|
||||
self.rm.GetRouteFollowingInfo(info);
|
||||
if (!info.IsValid())
|
||||
return nil;
|
||||
CLLocation * lastLocation = [MWMLocationManager lastLocation];
|
||||
double speedMps = 0;
|
||||
if (lastLocation && lastLocation.speed >= 0)
|
||||
speedMps = lastLocation.speed;
|
||||
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.GetDistance()
|
||||
targetUnitsIndex:static_cast<UInt8>(info.m_distToTarget.GetUnits())
|
||||
distanceToTurn:info.m_distToTurn.GetDistance()
|
||||
turnUnitsIndex:static_cast<UInt8>(info.m_distToTurn.GetUnits())
|
||||
streetName:@(info.m_nextStreetName.c_str())
|
||||
turnImageName:[self turnImageName:info.m_turn isPrimary:YES]
|
||||
nextTurnImageName:[self turnImageName:info.m_nextTurn isPrimary:NO]
|
||||
speedMps:speedMps
|
||||
speedLimitMps:info.m_speedLimitMps
|
||||
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();
|
||||
} else {
|
||||
if (errorPtr) {
|
||||
if (pointsCount == 0) {
|
||||
*errorPtr = [NSError errorWithDomain:@"comaps.app.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:@"comaps.app.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);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setOnNewTurnCallback:(MWMVoidBlock)callback {
|
||||
self.rm.RoutingSession().SetOnNewTurnCallback([callback] {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
- (void)resetOnNewTurnCallback {
|
||||
self.rm.RoutingSession().SetOnNewTurnCallback(nullptr);
|
||||
}
|
||||
|
||||
#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 speedLimitMps:-1];
|
||||
} else {
|
||||
auto const metersPerSecond = measurement_utils::KmphToMps(speedLimit);
|
||||
[object updateCameraInfo:YES speedLimitMps:metersPerSecond];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)speedCameraLeftVisibleArea {
|
||||
NSArray<id<MWMRoutingManagerListener>> * objects = self.listeners.allObjects;
|
||||
for (id<MWMRoutingManagerListener> object in objects) {
|
||||
[object updateCameraInfo:NO speedLimitMps:-1];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - MWMLocationObserver implementation
|
||||
|
||||
- (void)onLocationUpdate:(CLLocation *)location {
|
||||
NSMutableArray<NSString *> * turnNotifications = [NSMutableArray array];
|
||||
std::vector<std::string> notifications;
|
||||
auto announceStreets = [NSUserDefaults.standardUserDefaults boolForKey:@"UserDefaultsNeedToEnableStreetNamesTTS"];
|
||||
self.rm.GenerateNotifications(notifications, announceStreets);
|
||||
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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
typedef NS_ENUM(NSUInteger, MWMSpeedCameraManagerMode) {
|
||||
MWMSpeedCameraManagerModeAuto,
|
||||
MWMSpeedCameraManagerModeAlways,
|
||||
MWMSpeedCameraManagerModeNever
|
||||
} NS_SWIFT_NAME(SpeedCameraManagerMode);
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
final class BillingPendingTransaction: NSObject, IBillingPendingTransaction {
|
||||
private var pendingTransaction: SKPaymentTransaction?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
SKPaymentQueue.default().add(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
SKPaymentQueue.default().remove(self)
|
||||
}
|
||||
|
||||
var status: TransactionStatus {
|
||||
let routeTransactions = SKPaymentQueue.default().transactions.filter {
|
||||
var isOk = !Subscription.legacyProductIds.contains($0.payment.productIdentifier) &&
|
||||
!Subscription.productIds.contains($0.payment.productIdentifier)
|
||||
if isOk && $0.transactionState == .purchasing {
|
||||
isOk = false
|
||||
Statistics.logEvent("Pending_purchasing_transaction",
|
||||
withParameters: ["productId" : $0.payment.productIdentifier])
|
||||
}
|
||||
return isOk
|
||||
}
|
||||
|
||||
if routeTransactions.count > 1 {
|
||||
pendingTransaction = routeTransactions.last
|
||||
routeTransactions.prefix(routeTransactions.count - 1).forEach {
|
||||
SKPaymentQueue.default().finishTransaction($0)
|
||||
}
|
||||
} else if routeTransactions.count == 1 {
|
||||
pendingTransaction = routeTransactions[0]
|
||||
} else {
|
||||
return .none
|
||||
}
|
||||
|
||||
switch pendingTransaction!.transactionState {
|
||||
case .purchasing, .failed:
|
||||
return .failed
|
||||
case .purchased, .restored, .deferred:
|
||||
return .paid
|
||||
}
|
||||
}
|
||||
|
||||
func finishTransaction() {
|
||||
guard let transaction = pendingTransaction else {
|
||||
assert(false, "There is no pending transactions")
|
||||
return
|
||||
}
|
||||
|
||||
SKPaymentQueue.default().finishTransaction(transaction)
|
||||
pendingTransaction = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension BillingPendingTransaction: SKPaymentTransactionObserver {
|
||||
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
|
||||
// Do nothing. Only for SKPaymentQueue.default().transactions to work
|
||||
}
|
||||
}
|
||||
121
iphone/Maps/Core/InappPurchase/Impl/InAppBilling.swift
Normal file
121
iphone/Maps/Core/InappPurchase/Impl/InAppBilling.swift
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
fileprivate struct BillingProduct: IBillingProduct {
|
||||
var productId: String {
|
||||
return product.productIdentifier
|
||||
}
|
||||
|
||||
var localizedName: String {
|
||||
return product.localizedTitle
|
||||
}
|
||||
|
||||
var price: NSDecimalNumber {
|
||||
return product.price
|
||||
}
|
||||
|
||||
var priceLocale: Locale {
|
||||
return product.priceLocale
|
||||
}
|
||||
|
||||
let product: SKProduct
|
||||
|
||||
init(_ product: SKProduct) {
|
||||
self.product = product
|
||||
}
|
||||
}
|
||||
|
||||
final class InAppBilling: NSObject, IInAppBilling {
|
||||
private var productsCompletion: ProductsCompletion?
|
||||
private var paymentCompletion: PaymentCompletion?
|
||||
private var productRequest: SKProductsRequest?
|
||||
private var billingProduct: BillingProduct?
|
||||
private var pendingTransaction: SKPaymentTransaction?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
SKPaymentQueue.default().add(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
productRequest?.cancel()
|
||||
productRequest?.delegate = nil
|
||||
SKPaymentQueue.default().remove(self)
|
||||
}
|
||||
|
||||
func requestProducts(_ productIds: Set<String>, completion: @escaping ProductsCompletion) {
|
||||
productsCompletion = completion
|
||||
productRequest = SKProductsRequest(productIdentifiers: productIds)
|
||||
productRequest!.delegate = self
|
||||
productRequest!.start()
|
||||
}
|
||||
|
||||
func makePayment(_ product: IBillingProduct, completion: @escaping PaymentCompletion) {
|
||||
guard let billingProduct = product as? BillingProduct else {
|
||||
assert(false, "Wrong product type")
|
||||
return
|
||||
}
|
||||
|
||||
paymentCompletion = completion
|
||||
self.billingProduct = billingProduct
|
||||
SKPaymentQueue.default().add(SKPayment(product: billingProduct.product))
|
||||
}
|
||||
|
||||
func finishTransaction() {
|
||||
guard let transaction = pendingTransaction else {
|
||||
assert(false, "You must call makePayment() first")
|
||||
return
|
||||
}
|
||||
|
||||
SKPaymentQueue.default().finishTransaction(transaction)
|
||||
billingProduct = nil
|
||||
pendingTransaction = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension InAppBilling: SKProductsRequestDelegate {
|
||||
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
let products = response.products.map { BillingProduct($0) }
|
||||
self?.productsCompletion?(products, nil)
|
||||
self?.productsCompletion = nil
|
||||
self?.productRequest = nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ request: SKRequest, didFailWithError error: Error) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.productsCompletion?(nil, error)
|
||||
self?.productsCompletion = nil
|
||||
self?.productRequest = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension InAppBilling: SKPaymentTransactionObserver {
|
||||
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
|
||||
guard let productId = billingProduct?.productId else { return }
|
||||
transactions.forEach {
|
||||
if ($0.payment.productIdentifier != productId) { return }
|
||||
self.pendingTransaction = $0
|
||||
|
||||
switch $0.transactionState {
|
||||
case .purchasing:
|
||||
break
|
||||
case .purchased:
|
||||
paymentCompletion?(.success, nil)
|
||||
break
|
||||
case .failed:
|
||||
if ($0.error?._code == SKError.paymentCancelled.rawValue) {
|
||||
paymentCompletion?(.userCancelled, $0.error)
|
||||
} else {
|
||||
paymentCompletion?(.failed, $0.error)
|
||||
}
|
||||
break
|
||||
case .restored:
|
||||
break
|
||||
case .deferred:
|
||||
paymentCompletion?(.deferred, nil)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
iphone/Maps/Core/Location/MWMLocationHelpers.h
Normal file
38
iphone/Maps/Core/Location/MWMLocationHelpers.h
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#import "MWMMyPositionMode.h"
|
||||
|
||||
#include "platform/localization.hpp"
|
||||
#include "platform/location.hpp"
|
||||
#include "platform/distance.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
namespace location_helpers
|
||||
{
|
||||
|
||||
static inline NSString * formattedDistance(double const & meters) {
|
||||
if (meters < 0.)
|
||||
return nil;
|
||||
|
||||
return @(platform::Distance::CreateFormatted(meters).ToString().c_str());
|
||||
}
|
||||
|
||||
static inline ms::LatLon ToLatLon(m2::PointD const & p) { return mercator::ToLatLon(p); }
|
||||
|
||||
static inline m2::PointD ToMercator(CLLocationCoordinate2D const & l)
|
||||
{
|
||||
return mercator::FromLatLon(l.latitude, l.longitude);
|
||||
}
|
||||
|
||||
static inline m2::PointD ToMercator(ms::LatLon const & l) { return mercator::FromLatLon(l); }
|
||||
static inline MWMMyPositionMode mwmMyPositionMode(location::EMyPositionMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case location::EMyPositionMode::PendingPosition: return MWMMyPositionModePendingPosition;
|
||||
case location::EMyPositionMode::NotFollowNoPosition: return MWMMyPositionModeNotFollowNoPosition;
|
||||
case location::EMyPositionMode::NotFollow: return MWMMyPositionModeNotFollow;
|
||||
case location::EMyPositionMode::Follow: return MWMMyPositionModeFollow;
|
||||
case location::EMyPositionMode::FollowAndRotate: return MWMMyPositionModeFollowAndRotate;
|
||||
}
|
||||
}
|
||||
} // namespace location_helpers
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
extension LocationManager {
|
||||
@objc static func speedSymbolFor(_ speed: Double) -> String {
|
||||
switch max(speed, 0) {
|
||||
case 0 ..< 1: return "🐢"
|
||||
case 1 ..< 2: return "🚶"
|
||||
case 2 ..< 5: return "🏃"
|
||||
case 5 ..< 10: return "🚲"
|
||||
case 10 ..< 36: return "🚗"
|
||||
case 36 ..< 120: return "🚄"
|
||||
case 120 ..< 278: return "🛩"
|
||||
default: return "🚀"
|
||||
}
|
||||
}
|
||||
}
|
||||
42
iphone/Maps/Core/Location/MWMLocationManager.h
Normal file
42
iphone/Maps/Core/Location/MWMLocationManager.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#import "MWMMyPositionMode.h"
|
||||
#import "MWMLocationObserver.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol LocationService
|
||||
|
||||
+ (BOOL)isLocationProhibited;
|
||||
+ (void)checkLocationStatus;
|
||||
|
||||
@end
|
||||
|
||||
NS_SWIFT_NAME(LocationManager)
|
||||
@interface MWMLocationManager : NSObject<LocationService>
|
||||
|
||||
+ (void)start;
|
||||
+ (void)stop;
|
||||
+ (BOOL)isStarted;
|
||||
|
||||
+ (void)addObserver:(id<MWMLocationObserver>)observer NS_SWIFT_NAME(add(observer:));
|
||||
+ (void)removeObserver:(id<MWMLocationObserver>)observer NS_SWIFT_NAME(remove(observer:));
|
||||
|
||||
+ (void)setMyPositionMode:(MWMMyPositionMode)mode;
|
||||
|
||||
+ (nullable CLLocation *)lastLocation;
|
||||
+ (nullable CLHeading *)lastHeading;
|
||||
|
||||
+ (void)applicationDidBecomeActive;
|
||||
+ (void)applicationWillResignActive;
|
||||
|
||||
+ (void)enableLocationAlert;
|
||||
|
||||
- (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
|
||||
618
iphone/Maps/Core/Location/MWMLocationManager.mm
Normal file
618
iphone/Maps/Core/Location/MWMLocationManager.mm
Normal file
|
|
@ -0,0 +1,618 @@
|
|||
#import "MWMLocationManager.h"
|
||||
#import "MWMAlertViewController.h"
|
||||
#import "MWMLocationObserver.h"
|
||||
#import "MWMLocationPredictor.h"
|
||||
#import "MWMRouter.h"
|
||||
#import "SwiftBridge.h"
|
||||
#import "location_util.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
#include "map/gps_tracker.hpp"
|
||||
|
||||
#if TARGET_OS_SIMULATOR
|
||||
#include "MountainElevationGenerator.hpp"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
using Observer = id<MWMLocationObserver>;
|
||||
using Observers = NSHashTable<Observer>;
|
||||
|
||||
enum class GeoMode
|
||||
{
|
||||
Pending,
|
||||
InPosition,
|
||||
NotInPosition,
|
||||
FollowAndRotate,
|
||||
VehicleRouting,
|
||||
PedestrianRouting,
|
||||
BicycleRouting
|
||||
};
|
||||
|
||||
std::string DebugPrint(GeoMode geoMode) {
|
||||
using enum GeoMode;
|
||||
switch (geoMode) {
|
||||
case Pending: return "Pending";
|
||||
case InPosition: return "InPosition";
|
||||
case NotInPosition: return "NotInPosition";
|
||||
case FollowAndRotate: return "FollowAndRotate";
|
||||
case VehicleRouting: return "VehicleRouting";
|
||||
case PedestrianRouting: return "PedestrianRouting";
|
||||
case BicycleRouting: return "BicycleRouting";
|
||||
}
|
||||
CHECK(false, ("Unsupported value", static_cast<int>(geoMode)));
|
||||
}
|
||||
|
||||
std::string DebugPrint(MWMMyPositionMode mode) {
|
||||
switch (mode) {
|
||||
case MWMMyPositionModePendingPosition: return "MWMMyPositionModePendingPosition";
|
||||
case MWMMyPositionModeNotFollowNoPosition: return "MWMMyPositionModeNotFollowNoPosition";
|
||||
case MWMMyPositionModeNotFollow: return "MWMMyPositionModeNotFollow";
|
||||
case MWMMyPositionModeFollow: return "MWMMyPositionModeFollow";
|
||||
case MWMMyPositionModeFollowAndRotate: return "MWMMyPositionModeFollowAndRotate";
|
||||
}
|
||||
CHECK(false, ("Unsupported value", static_cast<int>(mode)));
|
||||
}
|
||||
|
||||
std::string DebugPrint(MWMLocationStatus status) {
|
||||
switch (status) {
|
||||
case MWMLocationStatusNoError: return "MWMLocationStatusNoError";
|
||||
case MWMLocationStatusNotSupported: return "MWMLocationStatusNotSupported";
|
||||
case MWMLocationStatusDenied: return "MWMLocationStatusDenied";
|
||||
case MWMLocationStatusGPSIsOff: return "MWMLocationStatusGPSIsOff";
|
||||
case MWMLocationStatusTimeout: return "MWMLocationStatusTimeout";
|
||||
}
|
||||
CHECK(false, ("Unsupported value", static_cast<int>(status)));
|
||||
}
|
||||
|
||||
std::string DebugPrint(CLAuthorizationStatus status) {
|
||||
switch (status) {
|
||||
case kCLAuthorizationStatusNotDetermined: return "kCLAuthorizationStatusNotDetermined";
|
||||
case kCLAuthorizationStatusRestricted: return "kCLAuthorizationStatusRestricted";
|
||||
case kCLAuthorizationStatusDenied: return "kCLAuthorizationStatusDenied";
|
||||
case kCLAuthorizationStatusAuthorizedAlways: return "kCLAuthorizationStatusAuthorizedAlways";
|
||||
case kCLAuthorizationStatusAuthorizedWhenInUse: return "kCLAuthorizationStatusAuthorizedWhenInUse";
|
||||
}
|
||||
CHECK(false, ("Unsupported value", static_cast<int>(status)));
|
||||
}
|
||||
|
||||
struct DesiredAccuracy
|
||||
{
|
||||
CLLocationAccuracy charging;
|
||||
CLLocationAccuracy battery;
|
||||
};
|
||||
|
||||
struct GeoModeSettings
|
||||
{
|
||||
CLLocationDistance distanceFilter;
|
||||
DesiredAccuracy accuracy;
|
||||
};
|
||||
|
||||
std::map<GeoMode, GeoModeSettings> const kGeoSettings{
|
||||
{GeoMode::Pending,
|
||||
{.distanceFilter = kCLDistanceFilterNone,
|
||||
.accuracy = {.charging = kCLLocationAccuracyBestForNavigation,
|
||||
.battery = kCLLocationAccuracyBestForNavigation}}},
|
||||
{GeoMode::InPosition,
|
||||
{.distanceFilter = 2,
|
||||
.accuracy = {.charging = kCLLocationAccuracyBestForNavigation,
|
||||
.battery = kCLLocationAccuracyBest}}},
|
||||
{GeoMode::NotInPosition,
|
||||
{.distanceFilter = 5,
|
||||
.accuracy = {.charging = kCLLocationAccuracyBestForNavigation,
|
||||
.battery = kCLLocationAccuracyBest}}},
|
||||
{GeoMode::FollowAndRotate,
|
||||
{.distanceFilter = 2,
|
||||
.accuracy = {.charging = kCLLocationAccuracyBestForNavigation,
|
||||
.battery = kCLLocationAccuracyBest}}},
|
||||
{GeoMode::VehicleRouting,
|
||||
{.distanceFilter = kCLDistanceFilterNone,
|
||||
.accuracy = {.charging = kCLLocationAccuracyBestForNavigation,
|
||||
.battery = kCLLocationAccuracyBest}}},
|
||||
{GeoMode::PedestrianRouting,
|
||||
{.distanceFilter = 2,
|
||||
.accuracy = {.charging = kCLLocationAccuracyBestForNavigation,
|
||||
.battery = kCLLocationAccuracyBest}}},
|
||||
{GeoMode::BicycleRouting,
|
||||
{.distanceFilter = 2,
|
||||
.accuracy = {.charging = kCLLocationAccuracyBestForNavigation,
|
||||
.battery = kCLLocationAccuracyBest}}}};
|
||||
|
||||
BOOL keepRunningInBackground()
|
||||
{
|
||||
if (GpsTracker::Instance().IsEnabled())
|
||||
return YES;
|
||||
|
||||
auto const isOnRoute = [MWMRouter isOnRoute];
|
||||
auto const isRouteFinished = [MWMRouter isRouteFinished];
|
||||
if (isOnRoute && !isRouteFinished)
|
||||
return YES;
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString * const kLocationPermissionRequestedKey = @"kLocationPermissionRequestedKey";
|
||||
NSString * const kLocationAlertNeedShowKey = @"kLocationAlertNeedShowKey";
|
||||
|
||||
BOOL needShowLocationAlert() {
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
if ([ud objectForKey:kLocationAlertNeedShowKey] == nil)
|
||||
return YES;
|
||||
return [ud boolForKey:kLocationAlertNeedShowKey];
|
||||
}
|
||||
|
||||
void setShowLocationAlert(BOOL needShow) {
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setBool:needShow forKey:kLocationAlertNeedShowKey];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@interface MWMLocationManager ()<CLLocationManagerDelegate>
|
||||
|
||||
@property(nonatomic) BOOL started;
|
||||
@property(nonatomic) CLLocationManager * locationManager;
|
||||
@property(nonatomic) GeoMode geoMode;
|
||||
@property(nonatomic) CLHeading * lastHeadingInfo;
|
||||
@property(nonatomic) CLLocation * lastLocationInfo;
|
||||
@property(nonatomic) MWMLocationStatus lastLocationStatus;
|
||||
@property(nonatomic) MWMLocationPredictor * predictor;
|
||||
@property(nonatomic) Observers * observers;
|
||||
@property(nonatomic) location::TLocationSource locationSource;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMLocationManager
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
+ (MWMLocationManager *)manager
|
||||
{
|
||||
static MWMLocationManager * manager;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
manager = [[self alloc] initManager];
|
||||
});
|
||||
return manager;
|
||||
}
|
||||
|
||||
- (instancetype)initManager
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_observers = [Observers weakObjectsHashTable];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[NSNotificationCenter.defaultCenter removeObserver:self];
|
||||
self.locationManager.delegate = nil;
|
||||
}
|
||||
|
||||
+ (void)start { [self manager].started = YES; }
|
||||
|
||||
+ (void)stop { [self manager].started = NO; }
|
||||
|
||||
+ (BOOL)isStarted { return [self manager].started; }
|
||||
|
||||
#pragma mark - Add/Remove Observers
|
||||
|
||||
+ (void)addObserver:(Observer)observer
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
MWMLocationManager * manager = [self manager];
|
||||
[manager.observers addObject:observer];
|
||||
[manager processLocationUpdate:manager.lastLocationInfo];
|
||||
});
|
||||
}
|
||||
|
||||
+ (void)removeObserver:(Observer)observer
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[self manager].observers removeObject:observer];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - App Life Cycle
|
||||
|
||||
+ (void)applicationDidBecomeActive
|
||||
{
|
||||
[self start];
|
||||
}
|
||||
|
||||
+ (void)applicationWillResignActive
|
||||
{
|
||||
BOOL const keepRunning = keepRunningInBackground();
|
||||
MWMLocationManager * manager = [self manager];
|
||||
CLLocationManager * locationManager = manager.locationManager;
|
||||
if ([locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)])
|
||||
[locationManager setAllowsBackgroundLocationUpdates:keepRunning];
|
||||
manager.started = keepRunning;
|
||||
}
|
||||
|
||||
#pragma mark - Getters
|
||||
|
||||
+ (CLLocation *)lastLocation
|
||||
{
|
||||
MWMLocationManager * manager = [self manager];
|
||||
if (!manager.started || !manager.lastLocationInfo ||
|
||||
manager.lastLocationInfo.horizontalAccuracy < 0 ||
|
||||
manager.lastLocationStatus != MWMLocationStatusNoError)
|
||||
return nil;
|
||||
return manager.lastLocationInfo;
|
||||
}
|
||||
|
||||
+ (BOOL)isLocationProhibited
|
||||
{
|
||||
auto const status = [self manager].lastLocationStatus;
|
||||
return status == MWMLocationStatusDenied ||
|
||||
status == MWMLocationStatusGPSIsOff;
|
||||
}
|
||||
|
||||
+ (CLHeading *)lastHeading
|
||||
{
|
||||
MWMLocationManager * manager = [self manager];
|
||||
if (!manager.started || !manager.lastHeadingInfo || manager.lastHeadingInfo.headingAccuracy < 0)
|
||||
return nil;
|
||||
return manager.lastHeadingInfo;
|
||||
}
|
||||
|
||||
#pragma mark - Observer notifications
|
||||
|
||||
- (void)processLocationStatus:(MWMLocationStatus)locationStatus
|
||||
{
|
||||
LOG(LINFO, ("Location status updated from", DebugPrint(self.lastLocationStatus), "to", DebugPrint(locationStatus)));
|
||||
self.lastLocationStatus = locationStatus;
|
||||
if (self.lastLocationStatus != MWMLocationStatusNoError)
|
||||
GetFramework().OnLocationError((location::TLocationError)self.lastLocationStatus);
|
||||
for (Observer observer in self.observers)
|
||||
{
|
||||
if ([observer respondsToSelector:@selector(onLocationError:)])
|
||||
[observer onLocationError:self.lastLocationStatus];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)processHeadingUpdate:(CLHeading *)headingInfo
|
||||
{
|
||||
self.lastHeadingInfo = headingInfo;
|
||||
GetFramework().OnCompassUpdate(location_util::compassInfoFromHeading(headingInfo));
|
||||
for (Observer observer in self.observers)
|
||||
{
|
||||
if ([observer respondsToSelector:@selector(onHeadingUpdate:)])
|
||||
[observer onHeadingUpdate:headingInfo];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)processLocationUpdate:(CLLocation *)locationInfo
|
||||
{
|
||||
if (!locationInfo || self.lastLocationStatus != MWMLocationStatusNoError)
|
||||
return;
|
||||
[self onLocationUpdate:locationInfo source:self.locationSource];
|
||||
if (![self.lastLocationInfo isEqual:locationInfo])
|
||||
[self.predictor reset:locationInfo];
|
||||
}
|
||||
|
||||
- (void)onLocationUpdate:(CLLocation *)locationInfo source:(location::TLocationSource)source
|
||||
{
|
||||
location::GpsInfo const gpsInfo = location_util::gpsInfoFromLocation(locationInfo, source);
|
||||
GpsTracker::Instance().OnLocationUpdated(gpsInfo);
|
||||
GetFramework().OnLocationUpdate(gpsInfo);
|
||||
|
||||
self.lastLocationInfo = locationInfo;
|
||||
self.locationSource = source;
|
||||
for (Observer observer in self.observers)
|
||||
{
|
||||
if ([observer respondsToSelector:@selector(onLocationUpdate:)])
|
||||
[observer onLocationUpdate:locationInfo];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Location Status
|
||||
|
||||
- (void)setLastLocationStatus:(MWMLocationStatus)lastLocationStatus
|
||||
{
|
||||
_lastLocationStatus = lastLocationStatus;
|
||||
switch (lastLocationStatus)
|
||||
{
|
||||
case MWMLocationStatusNoError:
|
||||
break;
|
||||
case MWMLocationStatusNotSupported:
|
||||
[[MWMAlertViewController activeAlertController] presentLocationServiceNotSupportedAlert];
|
||||
break;
|
||||
case MWMLocationStatusDenied:
|
||||
if (needShowLocationAlert()) {
|
||||
[[MWMAlertViewController activeAlertController] presentLocationAlertWithCancelBlock:^{
|
||||
setShowLocationAlert(NO);
|
||||
}];
|
||||
}
|
||||
break;
|
||||
case MWMLocationStatusGPSIsOff:
|
||||
if (needShowLocationAlert()) {
|
||||
[[MWMAlertViewController activeAlertController] presentLocationServicesDisabledAlert];
|
||||
setShowLocationAlert(NO);
|
||||
}
|
||||
break;
|
||||
case MWMLocationStatusTimeout:
|
||||
CHECK(false, ("MWMLocationStatusTimeout is only used in Qt/Desktop builds"));
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - My Position
|
||||
|
||||
+ (void)setMyPositionMode:(MWMMyPositionMode)mode
|
||||
{
|
||||
LOG(LINFO, ("MyPositionMode updated to", DebugPrint(mode)));
|
||||
MWMLocationManager * manager = [self manager];
|
||||
[manager.predictor setMyPositionMode:mode];
|
||||
[manager processLocationStatus:manager.lastLocationStatus];
|
||||
if ([MWMRouter isRoutingActive])
|
||||
{
|
||||
switch ([MWMRouter type])
|
||||
{
|
||||
case MWMRouterTypeVehicle: manager.geoMode = GeoMode::VehicleRouting; break;
|
||||
case MWMRouterTypePublicTransport:
|
||||
case MWMRouterTypePedestrian: manager.geoMode = GeoMode::PedestrianRouting; break;
|
||||
case MWMRouterTypeBicycle: manager.geoMode = GeoMode::BicycleRouting; break;
|
||||
case MWMRouterTypeRuler: break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case MWMMyPositionModePendingPosition: manager.geoMode = GeoMode::Pending; break;
|
||||
case MWMMyPositionModeNotFollowNoPosition:
|
||||
case MWMMyPositionModeNotFollow: manager.geoMode = GeoMode::NotInPosition; break;
|
||||
case MWMMyPositionModeFollow: manager.geoMode = GeoMode::InPosition; break;
|
||||
case MWMMyPositionModeFollowAndRotate: manager.geoMode = GeoMode::FollowAndRotate; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)checkLocationStatus
|
||||
{
|
||||
setShowLocationAlert(YES);
|
||||
[self.manager processLocationStatus:self.manager.lastLocationStatus];
|
||||
}
|
||||
|
||||
#pragma mark - Prediction
|
||||
|
||||
- (MWMLocationPredictor *)predictor
|
||||
{
|
||||
if (!_predictor)
|
||||
{
|
||||
__weak MWMLocationManager * weakSelf = self;
|
||||
_predictor = [[MWMLocationPredictor alloc] initWithOnPredictionBlock:^(CLLocation * location) {
|
||||
[weakSelf onLocationUpdate:location source:location::EPredictor];
|
||||
}];
|
||||
}
|
||||
return _predictor;
|
||||
}
|
||||
|
||||
#pragma mark - Device notifications
|
||||
|
||||
- (void)orientationChanged
|
||||
{
|
||||
self.locationManager.headingOrientation = (CLDeviceOrientation)UIDevice.currentDevice.orientation;
|
||||
}
|
||||
|
||||
- (void)batteryStateChangedNotification:(NSNotification *)notification
|
||||
{
|
||||
[MWMLocationManager refreshGeoModeSettingsFor:self.locationManager geoMode:self.geoMode];
|
||||
}
|
||||
|
||||
#pragma mark - Location manager
|
||||
|
||||
- (void)setGeoMode:(GeoMode)geoMode
|
||||
{
|
||||
LOG(LINFO, ("GeoMode updated to", geoMode));
|
||||
if (_geoMode == geoMode)
|
||||
return;
|
||||
_geoMode = geoMode;
|
||||
|
||||
CLLocationManager * locationManager = self.locationManager;
|
||||
switch (geoMode)
|
||||
{
|
||||
case GeoMode::Pending:
|
||||
case GeoMode::InPosition:
|
||||
case GeoMode::NotInPosition:
|
||||
case GeoMode::FollowAndRotate:
|
||||
locationManager.activityType = CLActivityTypeOther;
|
||||
break;
|
||||
case GeoMode::VehicleRouting:
|
||||
locationManager.activityType = CLActivityTypeAutomotiveNavigation;
|
||||
break;
|
||||
case GeoMode::PedestrianRouting:
|
||||
case GeoMode::BicycleRouting:
|
||||
locationManager.activityType = CLActivityTypeOtherNavigation;
|
||||
break;
|
||||
}
|
||||
|
||||
[MWMLocationManager refreshGeoModeSettingsFor:self.locationManager geoMode:self.geoMode];
|
||||
}
|
||||
|
||||
+ (void)refreshGeoModeSettingsFor:(CLLocationManager *)locationManager geoMode:(GeoMode)geoMode
|
||||
{
|
||||
UIDeviceBatteryState const state = UIDevice.currentDevice.batteryState;
|
||||
BOOL const isCharging =
|
||||
(state == UIDeviceBatteryStateCharging || state == UIDeviceBatteryStateFull);
|
||||
GeoModeSettings const settings = kGeoSettings.at(geoMode);
|
||||
locationManager.desiredAccuracy =
|
||||
isCharging ? settings.accuracy.charging : settings.accuracy.battery;
|
||||
locationManager.distanceFilter = settings.distanceFilter;
|
||||
LOG(LINFO, ("Refreshed GeoMode settings: accuracy", locationManager.desiredAccuracy,
|
||||
"distance filter", locationManager.distanceFilter, "charging", isCharging));
|
||||
}
|
||||
|
||||
- (CLLocationManager *)locationManager
|
||||
{
|
||||
if (!_locationManager)
|
||||
{
|
||||
_locationManager = [[CLLocationManager alloc] init];
|
||||
_locationManager.delegate = self;
|
||||
[MWMLocationManager refreshGeoModeSettingsFor:_locationManager geoMode:self.geoMode];
|
||||
_locationManager.pausesLocationUpdatesAutomatically = NO;
|
||||
_locationManager.headingFilter = 3.0;
|
||||
}
|
||||
return _locationManager;
|
||||
}
|
||||
|
||||
#pragma mark - CLLocationManagerDelegate
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)heading
|
||||
{
|
||||
[self processHeadingUpdate:heading];
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager
|
||||
didUpdateLocations:(NSArray<CLLocation *> *)locations
|
||||
{
|
||||
CLLocation * location = locations.lastObject;
|
||||
// According to documentation, lat and lon are valid only if horizontalAccuracy is non-negative.
|
||||
// So we filter out such events completely.
|
||||
if (location.horizontalAccuracy < 0.)
|
||||
return;
|
||||
|
||||
#if TARGET_OS_SIMULATOR
|
||||
// There is no simulator < 15.0 in the new XCode.
|
||||
if (@available(iOS 15.0, *))
|
||||
{
|
||||
// iOS Simulator doesn't provide any elevation in its locations. Mock it.
|
||||
static MountainElevationGenerator generator;
|
||||
location = [[CLLocation alloc] initWithCoordinate:location.coordinate
|
||||
altitude:generator.NextElevation()
|
||||
horizontalAccuracy:location.horizontalAccuracy
|
||||
verticalAccuracy:location.horizontalAccuracy
|
||||
course:location.course
|
||||
courseAccuracy:location.courseAccuracy
|
||||
speed:location.speed
|
||||
speedAccuracy:location.speedAccuracy
|
||||
timestamp:location.timestamp
|
||||
sourceInfo:location.sourceInformation];
|
||||
}
|
||||
#endif
|
||||
|
||||
self.lastLocationStatus = MWMLocationStatusNoError;
|
||||
self.locationSource = location::EAppleNative;
|
||||
[self processLocationUpdate:location];
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
|
||||
{
|
||||
LOG(LWARNING, ("CLLocationManagerDelegate: Did fail with error:", error.localizedDescription.UTF8String));
|
||||
if (self.lastLocationStatus == MWMLocationStatusNoError && error.code == kCLErrorDenied)
|
||||
[self processLocationStatus:MWMLocationStatusDenied];
|
||||
}
|
||||
|
||||
// Delegate's method didChangeAuthorizationStatus is used to handle the authorization status when the application finishes launching
|
||||
// or user changes location access in the application settings.
|
||||
- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager
|
||||
{
|
||||
LOG(LWARNING, ("CLLocationManagerDelegate: Authorization status has changed to", DebugPrint(manager.authorizationStatus)));
|
||||
switch (manager.authorizationStatus) {
|
||||
case kCLAuthorizationStatusAuthorizedWhenInUse:
|
||||
case kCLAuthorizationStatusAuthorizedAlways:
|
||||
[self startUpdatingLocationFor:manager];
|
||||
break;
|
||||
case kCLAuthorizationStatusNotDetermined:
|
||||
[manager requestWhenInUseAuthorization];
|
||||
break;
|
||||
case kCLAuthorizationStatusRestricted:
|
||||
case kCLAuthorizationStatusDenied:
|
||||
if ([CLLocationManager locationServicesEnabled])
|
||||
[self processLocationStatus:MWMLocationStatusDenied];
|
||||
else
|
||||
[self processLocationStatus:MWMLocationStatusGPSIsOff];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)locationManagerDidPauseLocationUpdates:(CLLocationManager *)manager
|
||||
{
|
||||
LOG(LINFO, ("CLLocationManagerDelegate: Location updates were paused"));
|
||||
}
|
||||
|
||||
- (void)locationManagerDidResumeLocationUpdates:(CLLocationManager *)manager
|
||||
{
|
||||
LOG(LINFO, ("CLLocationManagerDelegate: Location updates were resumed"));
|
||||
}
|
||||
|
||||
#pragma mark - Start / Stop
|
||||
|
||||
- (void)setStarted:(BOOL)started
|
||||
{
|
||||
if (_started == started)
|
||||
return;
|
||||
NSNotificationCenter * notificationCenter = NSNotificationCenter.defaultCenter;
|
||||
if (started) {
|
||||
_started = [self start];
|
||||
if (_started) {
|
||||
[notificationCenter addObserver:self
|
||||
selector:@selector(orientationChanged)
|
||||
name:UIDeviceOrientationDidChangeNotification
|
||||
object:nil];
|
||||
[notificationCenter addObserver:self
|
||||
selector:@selector(batteryStateChangedNotification:)
|
||||
name:UIDeviceBatteryStateDidChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
} else {
|
||||
_started = NO;
|
||||
[self stop];
|
||||
[notificationCenter removeObserver:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startUpdatingLocationFor:(CLLocationManager *)manager
|
||||
{
|
||||
LOG(LINFO, ("Start updating location"));
|
||||
[manager startUpdatingLocation];
|
||||
if ([CLLocationManager headingAvailable])
|
||||
[manager startUpdatingHeading];
|
||||
}
|
||||
|
||||
- (BOOL)start
|
||||
{
|
||||
if ([CLLocationManager locationServicesEnabled])
|
||||
{
|
||||
CLLocationManager * locationManager = self.locationManager;
|
||||
switch (locationManager.authorizationStatus)
|
||||
{
|
||||
case kCLAuthorizationStatusAuthorizedWhenInUse:
|
||||
case kCLAuthorizationStatusAuthorizedAlways:
|
||||
[self startUpdatingLocationFor:locationManager];
|
||||
return YES;
|
||||
break;
|
||||
case kCLAuthorizationStatusNotDetermined:
|
||||
[locationManager requestWhenInUseAuthorization];
|
||||
return YES;
|
||||
break;
|
||||
case kCLAuthorizationStatusRestricted:
|
||||
case kCLAuthorizationStatusDenied:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
{
|
||||
LOG(LINFO, ("Stop updating location"));
|
||||
CLLocationManager * locationManager = self.locationManager;
|
||||
[locationManager stopUpdatingLocation];
|
||||
if ([CLLocationManager headingAvailable])
|
||||
[locationManager stopUpdatingHeading];
|
||||
}
|
||||
|
||||
#pragma mark - Location alert
|
||||
|
||||
+ (void)enableLocationAlert {
|
||||
setShowLocationAlert(YES);
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
@end
|
||||
22
iphone/Maps/Core/Location/MWMLocationObserver.h
Normal file
22
iphone/Maps/Core/Location/MWMLocationObserver.h
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//#include "platform/location.hpp"
|
||||
|
||||
typedef NS_ENUM(NSInteger, MWMLocationStatus) {
|
||||
MWMLocationStatusNoError,
|
||||
MWMLocationStatusNotSupported,
|
||||
MWMLocationStatusDenied,
|
||||
MWMLocationStatusGPSIsOff,
|
||||
MWMLocationStatusTimeout // Unused on iOS, (only used on Qt)
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol MWMLocationObserver<NSObject>
|
||||
|
||||
@optional
|
||||
- (void)onHeadingUpdate:(CLHeading *)heading;
|
||||
- (void)onLocationUpdate:(CLLocation *)location;
|
||||
- (void)onLocationError:(MWMLocationStatus)locationError;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
13
iphone/Maps/Core/Location/MWMLocationPredictor.h
Normal file
13
iphone/Maps/Core/Location/MWMLocationPredictor.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#import "MWMMyPositionMode.h"
|
||||
|
||||
#include "platform/location.hpp"
|
||||
|
||||
using TPredictionBlock = void (^)(CLLocation *);
|
||||
|
||||
@interface MWMLocationPredictor : NSObject
|
||||
|
||||
- (instancetype)initWithOnPredictionBlock:(TPredictionBlock)onPredictBlock;
|
||||
- (void)reset:(CLLocation *)info;
|
||||
- (void)setMyPositionMode:(MWMMyPositionMode)mode;
|
||||
|
||||
@end
|
||||
93
iphone/Maps/Core/Location/MWMLocationPredictor.mm
Normal file
93
iphone/Maps/Core/Location/MWMLocationPredictor.mm
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#import "MWMLocationPredictor.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
NSTimeInterval constexpr kPredictionIntervalInSeconds = 0.5;
|
||||
NSUInteger constexpr kMaxPredictionCount = 20;
|
||||
} // namespace
|
||||
|
||||
@interface MWMLocationPredictor ()
|
||||
|
||||
@property(copy, nonatomic) CLLocation * lastLocation;
|
||||
@property(nonatomic) BOOL isLastLocationValid;
|
||||
@property (nonatomic) BOOL isLastPositionModeValid;
|
||||
@property (nonatomic) NSUInteger predictionsCount;
|
||||
@property (copy, nonatomic) TPredictionBlock onPredictionBlock;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMLocationPredictor
|
||||
|
||||
- (instancetype)initWithOnPredictionBlock:(TPredictionBlock)onPredictionBlock
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
_onPredictionBlock = [onPredictionBlock copy];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setMyPositionMode:(MWMMyPositionMode)mode
|
||||
{
|
||||
self.isLastPositionModeValid = (mode == MWMMyPositionModeFollowAndRotate);
|
||||
[self restart];
|
||||
}
|
||||
|
||||
- (void)reset:(CLLocation *)location
|
||||
{
|
||||
self.isLastLocationValid = (location.speed >= 0.0 && location.course >= 0.0);
|
||||
if (self.isLastLocationValid)
|
||||
self.lastLocation = location;
|
||||
|
||||
[self restart];
|
||||
}
|
||||
|
||||
- (BOOL)isActive
|
||||
{
|
||||
return self.isLastLocationValid && self.isLastPositionModeValid &&
|
||||
self.predictionsCount < kMaxPredictionCount;
|
||||
}
|
||||
|
||||
- (void)restart
|
||||
{
|
||||
self.predictionsCount = 0;
|
||||
[self schedule];
|
||||
}
|
||||
|
||||
- (void)schedule
|
||||
{
|
||||
SEL const predict = @selector(predict);
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:predict object:nil];
|
||||
[self performSelector:predict withObject:nil afterDelay:kPredictionIntervalInSeconds];
|
||||
}
|
||||
|
||||
- (void)predict
|
||||
{
|
||||
if (!self.isActive)
|
||||
return;
|
||||
|
||||
self.predictionsCount++;
|
||||
|
||||
CLLocation * l = self.lastLocation;
|
||||
CLLocationCoordinate2D coordinate = l.coordinate;
|
||||
CLLocationDistance altitude = l.altitude;
|
||||
CLLocationAccuracy hAccuracy = l.horizontalAccuracy;
|
||||
CLLocationAccuracy vAccuracy = l.verticalAccuracy;
|
||||
CLLocationDirection course = l.course;
|
||||
CLLocationSpeed speed = l.speed;
|
||||
NSDate * timestamp = [NSDate date];
|
||||
Framework::PredictLocation(coordinate.latitude, coordinate.longitude, hAccuracy, course, speed,
|
||||
timestamp.timeIntervalSince1970 - l.timestamp.timeIntervalSince1970);
|
||||
CLLocation * location = [[CLLocation alloc] initWithCoordinate:coordinate
|
||||
altitude:altitude
|
||||
horizontalAccuracy:hAccuracy
|
||||
verticalAccuracy:vAccuracy
|
||||
course:course
|
||||
speed:speed
|
||||
timestamp:timestamp];
|
||||
self.onPredictionBlock(location);
|
||||
[self schedule];
|
||||
}
|
||||
|
||||
@end
|
||||
7
iphone/Maps/Core/Location/MWMMyPositionMode.h
Normal file
7
iphone/Maps/Core/Location/MWMMyPositionMode.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
typedef NS_CLOSED_ENUM(NSUInteger, MWMMyPositionMode) {
|
||||
MWMMyPositionModePendingPosition,
|
||||
MWMMyPositionModeNotFollowNoPosition,
|
||||
MWMMyPositionModeNotFollow,
|
||||
MWMMyPositionModeFollow,
|
||||
MWMMyPositionModeFollowAndRotate
|
||||
};
|
||||
67
iphone/Maps/Core/Location/MountainElevationGenerator.hpp
Normal file
67
iphone/Maps/Core/Location/MountainElevationGenerator.hpp
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#pragma once
|
||||
|
||||
#include <ctime>
|
||||
#include <random>
|
||||
|
||||
class MountainElevationGenerator
|
||||
{
|
||||
static double constexpr kRandom{-1.};
|
||||
|
||||
std::mt19937_64 rng;
|
||||
|
||||
double const minElevation;
|
||||
double const maxElevation;
|
||||
double const maxSlopeChange;
|
||||
|
||||
std::normal_distribution<double> slopeChangeDist;
|
||||
|
||||
double currentElevation;
|
||||
double currentSlope;
|
||||
|
||||
double ValueOrRandomInRange(double value, double min, double max)
|
||||
{
|
||||
if (value != kRandom)
|
||||
return value;
|
||||
|
||||
return std::uniform_int_distribution<>(min, max)(rng);
|
||||
}
|
||||
|
||||
public:
|
||||
MountainElevationGenerator(double minElevation = kRandom, double maxElevation = kRandom,
|
||||
double startElevation = kRandom, double maxSlopeChange = kRandom,
|
||||
time_t seed = std::time(nullptr))
|
||||
: rng(seed)
|
||||
, minElevation(ValueOrRandomInRange(minElevation, 0., 2000.))
|
||||
, maxElevation(ValueOrRandomInRange(maxElevation, 3000., 7000.))
|
||||
, maxSlopeChange(ValueOrRandomInRange(maxSlopeChange, 1., 5.))
|
||||
, slopeChangeDist(0.0, maxSlopeChange)
|
||||
, currentElevation(ValueOrRandomInRange(startElevation, minElevation, maxElevation))
|
||||
, currentSlope(0.0)
|
||||
{}
|
||||
|
||||
double NextElevation()
|
||||
{
|
||||
// Change the slope gradually
|
||||
currentSlope += slopeChangeDist(rng);
|
||||
|
||||
// Limit maximum steepness
|
||||
currentSlope = std::max(-maxSlopeChange, std::min(maxSlopeChange, currentSlope));
|
||||
|
||||
// Update elevation based on current slope
|
||||
currentElevation += currentSlope;
|
||||
|
||||
// Ensure we stay within elevation bounds
|
||||
if (currentElevation < minElevation)
|
||||
{
|
||||
currentElevation = minElevation;
|
||||
currentSlope = std::abs(currentSlope) * 0.5; // Bounce back up
|
||||
}
|
||||
if (currentElevation > maxElevation)
|
||||
{
|
||||
currentElevation = maxElevation;
|
||||
currentSlope = -std::abs(currentSlope) * 0.5; // Start going down
|
||||
}
|
||||
|
||||
return currentElevation;
|
||||
}
|
||||
};
|
||||
41
iphone/Maps/Core/Location/location_util.h
Normal file
41
iphone/Maps/Core/Location/location_util.h
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
namespace location_util {
|
||||
|
||||
static location::GpsInfo gpsInfoFromLocation(CLLocation * l, location::TLocationSource source)
|
||||
{
|
||||
location::GpsInfo info;
|
||||
info.m_source = source;
|
||||
|
||||
info.m_latitude = l.coordinate.latitude;
|
||||
info.m_longitude = l.coordinate.longitude;
|
||||
info.m_timestamp = l.timestamp.timeIntervalSince1970;
|
||||
|
||||
if (l.horizontalAccuracy >= 0.0)
|
||||
info.m_horizontalAccuracy = l.horizontalAccuracy;
|
||||
|
||||
if (l.verticalAccuracy >= 0.0)
|
||||
{
|
||||
info.m_verticalAccuracy = l.verticalAccuracy;
|
||||
info.m_altitude = l.altitude;
|
||||
}
|
||||
|
||||
if (l.course >= 0.0)
|
||||
info.m_bearing = l.course;
|
||||
|
||||
if (l.speed >= 0.0)
|
||||
info.m_speed = l.speed;
|
||||
return info;
|
||||
}
|
||||
|
||||
static location::CompassInfo compassInfoFromHeading(CLHeading * h)
|
||||
{
|
||||
location::CompassInfo info;
|
||||
if (h.trueHeading >= 0.0)
|
||||
info.m_bearing = math::DegToRad(h.trueHeading);
|
||||
else if (h.headingAccuracy >= 0.0)
|
||||
info.m_bearing = math::DegToRad(h.magneticHeading);
|
||||
return info;
|
||||
}
|
||||
|
||||
} // namespace location_util
|
||||
12
iphone/Maps/Core/NetworkPolicy/MWMNetworkPolicy+UI.h
Normal file
12
iphone/Maps/Core/NetworkPolicy/MWMNetworkPolicy+UI.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#import <CoreApi/MWMNetworkPolicy.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MWMNetworkPolicy (UI)
|
||||
|
||||
- (void)callOnlineApi:(MWMBoolBlock)onlineCall;
|
||||
- (void)callOnlineApi:(MWMBoolBlock)onlineCall forceAskPermission:(BOOL)askPermission;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
76
iphone/Maps/Core/NetworkPolicy/MWMNetworkPolicy+UI.m
Normal file
76
iphone/Maps/Core/NetworkPolicy/MWMNetworkPolicy+UI.m
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#import "MWMNetworkPolicy+UI.h"
|
||||
#import "MWMAlertViewController.h"
|
||||
|
||||
@implementation MWMNetworkPolicy (UI)
|
||||
|
||||
- (BOOL)isTempPermissionValid {
|
||||
return [self.permissionExpirationDate compare:[NSDate date]] == NSOrderedDescending;
|
||||
}
|
||||
|
||||
- (void)askPermissionWithCompletion:(MWMBoolBlock)completion {
|
||||
MWMAlertViewController * alertController = [MWMAlertViewController activeAlertController];
|
||||
[alertController presentMobileInternetAlertWithBlock:^(MWMMobileInternetAlertResult result) {
|
||||
switch (result) {
|
||||
case MWMMobileInternetAlertResultAlways:
|
||||
self.permission = MWMNetworkPolicyPermissionAlways;
|
||||
completion(YES);
|
||||
break;
|
||||
case MWMMobileInternetAlertResultToday:
|
||||
self.permission = MWMNetworkPolicyPermissionToday;
|
||||
completion(YES);
|
||||
break;
|
||||
case MWMMobileInternetAlertResultNotToday:
|
||||
self.permission = MWMNetworkPolicyPermissionNotToday;
|
||||
completion(NO);
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)callOnlineApi:(MWMBoolBlock)onlineCall {
|
||||
[self callOnlineApi:onlineCall forceAskPermission:NO];
|
||||
}
|
||||
|
||||
- (void)callOnlineApi:(MWMBoolBlock)onlineCall forceAskPermission:(BOOL)askPermission {
|
||||
switch (self.connectionType) {
|
||||
case MWMConnectionTypeNone:
|
||||
onlineCall(NO);
|
||||
break;
|
||||
case MWMConnectionTypeWifi:
|
||||
onlineCall(YES);
|
||||
break;
|
||||
case MWMConnectionTypeCellular:
|
||||
switch (self.permission) {
|
||||
case MWMNetworkPolicyPermissionAsk:
|
||||
[self askPermissionWithCompletion:onlineCall];
|
||||
break;
|
||||
case MWMNetworkPolicyPermissionAlways:
|
||||
onlineCall(YES);
|
||||
break;
|
||||
case MWMNetworkPolicyPermissionNever:
|
||||
if (askPermission) {
|
||||
[self askPermissionWithCompletion:onlineCall];
|
||||
} else {
|
||||
onlineCall(NO);
|
||||
}
|
||||
break;
|
||||
case MWMNetworkPolicyPermissionToday:
|
||||
if (self.isTempPermissionValid) {
|
||||
onlineCall(YES);
|
||||
} else {
|
||||
[self askPermissionWithCompletion:onlineCall];
|
||||
}
|
||||
break;
|
||||
case MWMNetworkPolicyPermissionNotToday:
|
||||
if (!self.isTempPermissionValid || askPermission) {
|
||||
[self askPermissionWithCompletion:onlineCall];
|
||||
} else {
|
||||
onlineCall(NO);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
33
iphone/Maps/Core/Routing/MWMCoreRouterType.h
Normal file
33
iphone/Maps/Core/Routing/MWMCoreRouterType.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#import "MWMRouterType.h"
|
||||
|
||||
#include "routing/router.hpp"
|
||||
|
||||
static inline routing::RouterType coreRouterType(MWMRouterType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MWMRouterTypeVehicle: return routing::RouterType::Vehicle;
|
||||
case MWMRouterTypePedestrian: return routing::RouterType::Pedestrian;
|
||||
case MWMRouterTypePublicTransport: return routing::RouterType::Transit;
|
||||
case MWMRouterTypeBicycle: return routing::RouterType::Bicycle;
|
||||
case MWMRouterTypeRuler: return routing::RouterType::Ruler;
|
||||
default:
|
||||
ASSERT(false, ("Invalid routing type"));
|
||||
return routing::RouterType::Vehicle;
|
||||
}
|
||||
}
|
||||
|
||||
static inline MWMRouterType routerType(routing::RouterType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case routing::RouterType::Vehicle: return MWMRouterTypeVehicle;
|
||||
case routing::RouterType::Transit: return MWMRouterTypePublicTransport;
|
||||
case routing::RouterType::Pedestrian: return MWMRouterTypePedestrian;
|
||||
case routing::RouterType::Bicycle: return MWMRouterTypeBicycle;
|
||||
case routing::RouterType::Ruler: return MWMRouterTypeRuler;
|
||||
default:
|
||||
ASSERT(false, ("Invalid routing type"));
|
||||
return MWMRouterTypeVehicle;
|
||||
}
|
||||
}
|
||||
20
iphone/Maps/Core/Routing/MWMRoutePoint+CPP.h
Normal file
20
iphone/Maps/Core/Routing/MWMRoutePoint+CPP.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#import "MWMRoutePoint.h"
|
||||
|
||||
#include "map/mwm_url.hpp"
|
||||
#include "map/routing_mark.hpp"
|
||||
|
||||
@interface MWMRoutePoint (CPP)
|
||||
|
||||
@property(nonatomic, readonly) RouteMarkData routeMarkData;
|
||||
|
||||
- (instancetype)initWithURLSchemeRoutePoint:(url_scheme::RoutePoint const &)point
|
||||
type:(MWMRoutePointType)type
|
||||
intermediateIndex:(size_t)intermediateIndex;
|
||||
- (instancetype)initWithRouteMarkData:(RouteMarkData const &)point;
|
||||
- (instancetype)initWithPoint:(m2::PointD const &)point
|
||||
title:(NSString *)title
|
||||
subtitle:(NSString *)subtitle
|
||||
type:(MWMRoutePointType)type
|
||||
intermediateIndex:(size_t)intermediateIndex;
|
||||
|
||||
@end
|
||||
28
iphone/Maps/Core/Routing/MWMRoutePoint.h
Normal file
28
iphone/Maps/Core/Routing/MWMRoutePoint.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
typedef NS_CLOSED_ENUM(NSUInteger, MWMRoutePointType) {
|
||||
MWMRoutePointTypeStart,
|
||||
MWMRoutePointTypeIntermediate,
|
||||
MWMRoutePointTypeFinish
|
||||
};
|
||||
|
||||
@interface MWMRoutePoint : NSObject
|
||||
|
||||
- (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;
|
||||
@property(nonatomic, readonly) BOOL isMyPosition;
|
||||
@property(nonatomic) MWMRoutePointType type;
|
||||
@property(nonatomic) size_t intermediateIndex;
|
||||
|
||||
@property(nonatomic, readonly) double latitude;
|
||||
@property(nonatomic, readonly) double longitude;
|
||||
|
||||
@end
|
||||
167
iphone/Maps/Core/Routing/MWMRoutePoint.mm
Normal file
167
iphone/Maps/Core/Routing/MWMRoutePoint.mm
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
#import "MWMRoutePoint.h"
|
||||
#import "CLLocation+Mercator.h"
|
||||
#import "MWMLocationManager.h"
|
||||
#import "MWMRoutePoint+CPP.h"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "platform/measurement_utils.hpp"
|
||||
|
||||
@interface MWMRoutePoint ()
|
||||
|
||||
@property(nonatomic, readonly) m2::PointD point;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMRoutePoint
|
||||
|
||||
- (instancetype)initWithLastLocationAndType:(MWMRoutePointType)type
|
||||
intermediateIndex:(size_t)intermediateIndex
|
||||
{
|
||||
auto lastLocation = [MWMLocationManager lastLocation];
|
||||
if (!lastLocation)
|
||||
return nil;
|
||||
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_point = lastLocation.mercator;
|
||||
_title = L(@"p2p_your_location");
|
||||
_subtitle = @"";
|
||||
_isMyPosition = YES;
|
||||
_type = type;
|
||||
_intermediateIndex = intermediateIndex;
|
||||
|
||||
[self validatePoint];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithURLSchemeRoutePoint:(url_scheme::RoutePoint const &)point
|
||||
type:(MWMRoutePointType)type
|
||||
intermediateIndex:(size_t)intermediateIndex
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_point = point.m_org;
|
||||
_title = @(point.m_name.c_str());
|
||||
_subtitle = @"";
|
||||
_isMyPosition = NO;
|
||||
_type = type;
|
||||
_intermediateIndex = intermediateIndex;
|
||||
|
||||
[self validatePoint];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithRouteMarkData:(RouteMarkData const &)point
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_point = point.m_position;
|
||||
_title = @(point.m_title.c_str());
|
||||
_subtitle = @(point.m_subTitle.c_str());
|
||||
_isMyPosition = point.m_isMyPosition;
|
||||
_intermediateIndex = point.m_intermediateIndex;
|
||||
switch (point.m_pointType)
|
||||
{
|
||||
case RouteMarkType::Start: _type = MWMRoutePointTypeStart; break;
|
||||
case RouteMarkType::Intermediate: _type = MWMRoutePointTypeIntermediate; break;
|
||||
case RouteMarkType::Finish: _type = MWMRoutePointTypeFinish; break;
|
||||
}
|
||||
|
||||
[self validatePoint];
|
||||
}
|
||||
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
|
||||
type:(MWMRoutePointType)type
|
||||
intermediateIndex:(size_t)intermediateIndex
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_point = point;
|
||||
_title = title;
|
||||
_subtitle = subtitle ?: @"";
|
||||
_isMyPosition = NO;
|
||||
_type = type;
|
||||
_intermediateIndex = intermediateIndex;
|
||||
|
||||
[self validatePoint];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)validatePoint
|
||||
{
|
||||
// Sync with RoutePointsLayout::kMaxIntermediatePointsCount constant.
|
||||
NSAssert(_intermediateIndex >= 0 && _intermediateIndex <= 100, @"Invalid intermediateIndex");
|
||||
}
|
||||
|
||||
- (double)latitude { return mercator::YToLat(self.point.y); }
|
||||
- (double)longitude { return mercator::XToLon(self.point.x); }
|
||||
|
||||
- (NSString *)latLonString
|
||||
{
|
||||
return @(measurement_utils::FormatLatLon(self.latitude, self.longitude, true).c_str());
|
||||
}
|
||||
|
||||
- (RouteMarkData)routeMarkData
|
||||
{
|
||||
[self validatePoint];
|
||||
|
||||
RouteMarkData pt;
|
||||
switch (self.type)
|
||||
{
|
||||
case MWMRoutePointTypeStart: pt.m_pointType = RouteMarkType::Start; break;
|
||||
case MWMRoutePointTypeIntermediate: pt.m_pointType = RouteMarkType::Intermediate; break;
|
||||
case MWMRoutePointTypeFinish: pt.m_pointType = RouteMarkType::Finish; break;
|
||||
}
|
||||
pt.m_position = self.point;
|
||||
pt.m_isMyPosition = self.isMyPosition;
|
||||
pt.m_title = self.title.UTF8String;
|
||||
pt.m_subTitle = self.subtitle.UTF8String;
|
||||
pt.m_intermediateIndex = self.intermediateIndex;
|
||||
return pt;
|
||||
}
|
||||
|
||||
- (NSString *)debugDescription
|
||||
{
|
||||
NSString * type = nil;
|
||||
switch (_type)
|
||||
{
|
||||
case MWMRoutePointTypeStart: type = @"Start"; break;
|
||||
case MWMRoutePointTypeIntermediate: type = @"Intermediate"; break;
|
||||
case MWMRoutePointTypeFinish: type = @"Finish"; break;
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:@"<%@: %p> Position: [%@, %@] | IsMyPosition: %@ | Type: %@ | "
|
||||
@"IntermediateIndex: %@ | Title: %@ | Subtitle: %@",
|
||||
[self class], self, @(_point.x), @(_point.y),
|
||||
_isMyPosition ? @"true" : @"false", type, @(_intermediateIndex),
|
||||
_title, _subtitle];
|
||||
}
|
||||
|
||||
@end
|
||||
53
iphone/Maps/Core/Routing/MWMRouter+RouteManager.mm
Normal file
53
iphone/Maps/Core/Routing/MWMRouter+RouteManager.mm
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#import "MWMRouter.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
@interface MWMRouter ()
|
||||
|
||||
@property(nonatomic) uint32_t routeManagerTransactionId;
|
||||
|
||||
+ (MWMRouter *)router;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMRouter (RouteManager)
|
||||
|
||||
+ (void)openRouteManagerTransaction
|
||||
{
|
||||
auto router = [MWMRouter router];
|
||||
router.routeManagerTransactionId =
|
||||
GetFramework().GetRoutingManager().OpenRoutePointsTransaction();
|
||||
}
|
||||
|
||||
+ (void)applyRouteManagerTransaction
|
||||
{
|
||||
auto router = [MWMRouter router];
|
||||
if (router.routeManagerTransactionId == RoutingManager::InvalidRoutePointsTransactionId())
|
||||
return;
|
||||
GetFramework().GetRoutingManager().ApplyRoutePointsTransaction(router.routeManagerTransactionId);
|
||||
router.routeManagerTransactionId = RoutingManager::InvalidRoutePointsTransactionId();
|
||||
}
|
||||
|
||||
+ (void)cancelRouteManagerTransaction
|
||||
{
|
||||
auto router = [MWMRouter router];
|
||||
if (router.routeManagerTransactionId == RoutingManager::InvalidRoutePointsTransactionId())
|
||||
return;
|
||||
auto & rm = GetFramework().GetRoutingManager();
|
||||
rm.CancelRoutePointsTransaction(router.routeManagerTransactionId);
|
||||
router.routeManagerTransactionId = RoutingManager::InvalidRoutePointsTransactionId();
|
||||
rm.CancelPreviewMode();
|
||||
}
|
||||
|
||||
+ (void)movePointAtIndex:(NSInteger)index toIndex:(NSInteger)newIndex
|
||||
{
|
||||
NSAssert(index != newIndex, @"Route manager moves point to its' current position.");
|
||||
GetFramework().GetRoutingManager().MoveRoutePoint(index, newIndex);
|
||||
}
|
||||
|
||||
+ (void)updatePreviewMode
|
||||
{
|
||||
GetFramework().GetRoutingManager().UpdatePreviewMode();
|
||||
}
|
||||
|
||||
@end
|
||||
94
iphone/Maps/Core/Routing/MWMRouter.h
Normal file
94
iphone/Maps/Core/Routing/MWMRouter.h
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#import "MWMRoutePoint.h"
|
||||
#import "MWMRouterType.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, MWMRoadType) {
|
||||
MWMRoadTypeToll,
|
||||
MWMRoadTypeDirty,
|
||||
MWMRoadTypeFerry,
|
||||
MWMRoadTypeMotorway,
|
||||
MWMRoadTypeSteps,
|
||||
MWMRoadTypePaved
|
||||
};
|
||||
|
||||
typedef void (^MWMImageHeightBlock)(UIImage *, NSString *, NSString *);
|
||||
|
||||
@interface MWMRouter : NSObject
|
||||
|
||||
+ (void)subscribeToEvents;
|
||||
+ (void)unsubscribeFromEvents;
|
||||
|
||||
+ (BOOL)isRoutingActive;
|
||||
+ (BOOL)isRouteBuilt;
|
||||
+ (BOOL)isRouteFinished;
|
||||
+ (BOOL)isRouteRebuildingOnly;
|
||||
+ (BOOL)isOnRoute;
|
||||
|
||||
+ (BOOL)isSpeedCamLimitExceeded;
|
||||
|
||||
+ (BOOL)canAddIntermediatePoint;
|
||||
|
||||
+ (void)startRouting;
|
||||
+ (void)stopRouting;
|
||||
|
||||
+ (NSArray<MWMRoutePoint *> *)points;
|
||||
+ (NSInteger)pointsCount;
|
||||
+ (MWMRoutePoint *)startPoint;
|
||||
+ (MWMRoutePoint *)finishPoint;
|
||||
|
||||
+ (void)enableAutoAddLastLocation:(BOOL)enable;
|
||||
|
||||
+ (void)setType:(MWMRouterType)type;
|
||||
+ (MWMRouterType)type;
|
||||
|
||||
+ (void)disableFollowMode;
|
||||
|
||||
+ (void)enableTurnNotifications:(BOOL)active;
|
||||
+ (BOOL)areTurnNotificationsEnabled;
|
||||
+ (void)setTurnNotificationsLocale:(NSString *)locale;
|
||||
+ (NSArray<NSString *> *)turnNotifications;
|
||||
|
||||
+ (void)addPoint:(MWMRoutePoint *)point;
|
||||
+ (void)removePoint:(MWMRoutePoint *)point;
|
||||
+ (void)addPointAndRebuild:(MWMRoutePoint *)point;
|
||||
+ (void)removePointAndRebuild:(MWMRoutePoint *)point;
|
||||
+ (void)removePoints;
|
||||
|
||||
+ (void)buildFromPoint:(MWMRoutePoint *)start bestRouter:(BOOL)bestRouter;
|
||||
+ (void)buildToPoint:(MWMRoutePoint *)finish bestRouter:(BOOL)bestRouter;
|
||||
+ (void)buildApiRouteWithType:(MWMRouterType)type
|
||||
startPoint:(MWMRoutePoint *)startPoint
|
||||
finishPoint:(MWMRoutePoint *)finishPoint;
|
||||
+ (void)rebuildWithBestRouter:(BOOL)bestRouter;
|
||||
|
||||
+ (BOOL)hasRouteAltitude;
|
||||
+ (void)routeAltitudeImageForSize:(CGSize)size completion:(MWMImageHeightBlock)block;
|
||||
|
||||
+ (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
|
||||
|
||||
@interface MWMRouter (RouteManager)
|
||||
|
||||
+ (void)openRouteManagerTransaction;
|
||||
+ (void)applyRouteManagerTransaction;
|
||||
+ (void)cancelRouteManagerTransaction;
|
||||
+ (void)movePointAtIndex:(NSInteger)index toIndex:(NSInteger)newIndex;
|
||||
+ (void)updatePreviewMode;
|
||||
|
||||
@end
|
||||
620
iphone/Maps/Core/Routing/MWMRouter.mm
Normal file
620
iphone/Maps/Core/Routing/MWMRouter.mm
Normal file
|
|
@ -0,0 +1,620 @@
|
|||
#import "MWMRouter.h"
|
||||
#import "MWMAlertViewController+CPP.h"
|
||||
#import "MWMCoreRouterType.h"
|
||||
#import "MWMFrameworkListener.h"
|
||||
#import "MWMFrameworkObservers.h"
|
||||
#import "MWMLocationHelpers.h"
|
||||
#import "MWMLocationObserver.h"
|
||||
#import "MWMMapViewControlsManager.h"
|
||||
#import "MWMNavigationDashboardManager+Entity.h"
|
||||
#import "MWMRoutePoint+CPP.h"
|
||||
#import "MWMStorage+UI.h"
|
||||
#import "MapsAppDelegate.h"
|
||||
#import "SwiftBridge.h"
|
||||
#import "UIImage+RGBAData.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
#include "platform/localization.hpp"
|
||||
#include "platform/distance.hpp"
|
||||
|
||||
using namespace routing;
|
||||
|
||||
@interface MWMRouter () <MWMLocationObserver, MWMFrameworkRouteBuilderObserver>
|
||||
|
||||
@property(nonatomic) NSMutableDictionary<NSValue *, NSData *> *altitudeImagesData;
|
||||
@property(nonatomic) NSString *totalAscent;
|
||||
@property(nonatomic) NSString *totalDescent;
|
||||
@property(nonatomic) dispatch_queue_t renderAltitudeImagesQueue;
|
||||
@property(nonatomic) uint32_t routeManagerTransactionId;
|
||||
@property(nonatomic) BOOL canAutoAddLastLocation;
|
||||
@property(nonatomic) BOOL isAPICall;
|
||||
@property(nonatomic) BOOL isRestoreProcessCompleted;
|
||||
@property(strong, nonatomic) MWMRoutingOptions *routingOptions;
|
||||
|
||||
+ (MWMRouter *)router;
|
||||
|
||||
@end
|
||||
|
||||
namespace {
|
||||
char const *kRenderAltitudeImagesQueueLabel = "mapsme.mwmrouter.renderAltitudeImagesQueue";
|
||||
} // namespace
|
||||
|
||||
@implementation MWMRouter
|
||||
|
||||
+ (MWMRouter *)router {
|
||||
static MWMRouter *router;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
router = [[self alloc] initRouter];
|
||||
});
|
||||
return router;
|
||||
}
|
||||
|
||||
+ (BOOL)hasRouteAltitude {
|
||||
switch ([self type]) {
|
||||
case MWMRouterTypeVehicle:
|
||||
case MWMRouterTypePublicTransport:
|
||||
case MWMRouterTypeRuler:
|
||||
return NO;
|
||||
case MWMRouterTypePedestrian:
|
||||
case MWMRouterTypeBicycle:
|
||||
return GetFramework().GetRoutingManager().HasRouteAltitude();
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)startRouting {
|
||||
[self start];
|
||||
}
|
||||
|
||||
+ (void)stopRouting {
|
||||
[self stop:YES];
|
||||
}
|
||||
|
||||
+ (BOOL)isRoutingActive {
|
||||
return GetFramework().GetRoutingManager().IsRoutingActive();
|
||||
}
|
||||
+ (BOOL)isRouteBuilt {
|
||||
return GetFramework().GetRoutingManager().IsRouteBuilt();
|
||||
}
|
||||
+ (BOOL)isRouteFinished {
|
||||
return GetFramework().GetRoutingManager().IsRouteFinished();
|
||||
}
|
||||
+ (BOOL)isRouteRebuildingOnly {
|
||||
return GetFramework().GetRoutingManager().IsRouteRebuildingOnly();
|
||||
}
|
||||
+ (BOOL)isOnRoute {
|
||||
return GetFramework().GetRoutingManager().IsRoutingFollowing();
|
||||
}
|
||||
+ (BOOL)IsRouteValid {
|
||||
return GetFramework().GetRoutingManager().IsRouteValid();
|
||||
}
|
||||
|
||||
+ (BOOL)isSpeedCamLimitExceeded
|
||||
{
|
||||
return GetFramework().GetRoutingManager().IsSpeedCamLimitExceeded();
|
||||
}
|
||||
|
||||
+ (NSArray<MWMRoutePoint *> *)points {
|
||||
NSMutableArray<MWMRoutePoint *> *points = [@[] mutableCopy];
|
||||
auto const routePoints = GetFramework().GetRoutingManager().GetRoutePoints();
|
||||
for (auto const &routePoint : routePoints)
|
||||
[points addObject:[[MWMRoutePoint alloc] initWithRouteMarkData:routePoint]];
|
||||
return [points copy];
|
||||
}
|
||||
|
||||
+ (NSInteger)pointsCount {
|
||||
return GetFramework().GetRoutingManager().GetRoutePointsCount();
|
||||
}
|
||||
+ (MWMRoutePoint *)startPoint {
|
||||
auto const routePoints = GetFramework().GetRoutingManager().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 *)finishPoint {
|
||||
auto const routePoints = GetFramework().GetRoutingManager().GetRoutePoints();
|
||||
if (routePoints.empty())
|
||||
return nil;
|
||||
auto const &routePoint = routePoints.back();
|
||||
if (routePoint.m_pointType == RouteMarkType::Finish)
|
||||
return [[MWMRoutePoint alloc] initWithRouteMarkData:routePoint];
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (void)enableAutoAddLastLocation:(BOOL)enable {
|
||||
[MWMRouter router].canAutoAddLastLocation = enable;
|
||||
}
|
||||
|
||||
+ (BOOL)canAddIntermediatePoint {
|
||||
return GetFramework().GetRoutingManager().CouldAddIntermediatePoint();
|
||||
}
|
||||
|
||||
- (instancetype)initRouter {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.altitudeImagesData = [@{} mutableCopy];
|
||||
self.renderAltitudeImagesQueue = dispatch_queue_create(kRenderAltitudeImagesQueueLabel, DISPATCH_QUEUE_SERIAL);
|
||||
self.routeManagerTransactionId = RoutingManager::InvalidRoutePointsTransactionId();
|
||||
[MWMLocationManager addObserver:self];
|
||||
[MWMFrameworkListener addObserver:self];
|
||||
_canAutoAddLastLocation = YES;
|
||||
_routingOptions = [MWMRoutingOptions new];
|
||||
_isRestoreProcessCompleted = NO;
|
||||
|
||||
[NSNotificationCenter.defaultCenter addObserverForName:@"RoutingOptionsChanged" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull notification) {
|
||||
[MWMRouter updateRoute];
|
||||
}];
|
||||
}
|
||||
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)
|
||||
return;
|
||||
|
||||
[self doStop:NO];
|
||||
GetFramework().GetRoutingManager().SetRouter(coreRouterType(type));
|
||||
}
|
||||
|
||||
+ (MWMRouterType)type {
|
||||
return routerType(GetFramework().GetRoutingManager().GetRouter());
|
||||
}
|
||||
+ (void)disableFollowMode {
|
||||
GetFramework().GetRoutingManager().DisableFollowMode();
|
||||
}
|
||||
+ (void)enableTurnNotifications:(BOOL)active {
|
||||
GetFramework().GetRoutingManager().EnableTurnNotifications(active);
|
||||
}
|
||||
|
||||
+ (BOOL)areTurnNotificationsEnabled {
|
||||
return GetFramework().GetRoutingManager().AreTurnNotificationsEnabled();
|
||||
}
|
||||
|
||||
+ (void)setTurnNotificationsLocale:(NSString *)locale {
|
||||
GetFramework().GetRoutingManager().SetTurnNotificationsLocale(locale.UTF8String);
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)turnNotifications {
|
||||
NSMutableArray<NSString *> *turnNotifications = [@[] mutableCopy];
|
||||
std::vector<std::string> notifications;
|
||||
auto announceStreets = [NSUserDefaults.standardUserDefaults boolForKey:@"UserDefaultsNeedToEnableStreetNamesTTS"];
|
||||
GetFramework().GetRoutingManager().GenerateNotifications(notifications, announceStreets);
|
||||
|
||||
for (auto const &text : notifications)
|
||||
[turnNotifications addObject:@(text.c_str())];
|
||||
return [turnNotifications copy];
|
||||
}
|
||||
|
||||
+ (void)removePoint:(MWMRoutePoint *)point {
|
||||
RouteMarkData pt = point.routeMarkData;
|
||||
GetFramework().GetRoutingManager().RemoveRoutePoint(pt.m_pointType, pt.m_intermediateIndex);
|
||||
[[MWMNavigationDashboardManager sharedManager] onRoutePointsUpdated];
|
||||
}
|
||||
|
||||
+ (void)removePointAndRebuild:(MWMRoutePoint *)point {
|
||||
if (!point)
|
||||
return;
|
||||
[self removePoint:point];
|
||||
[self rebuildWithBestRouter:NO];
|
||||
}
|
||||
|
||||
+ (void)removePoints {
|
||||
GetFramework().GetRoutingManager().RemoveRoutePoints();
|
||||
}
|
||||
+ (void)addPoint:(MWMRoutePoint *)point {
|
||||
if (!point) {
|
||||
NSAssert(NO, @"Point can not be nil");
|
||||
return;
|
||||
}
|
||||
|
||||
RouteMarkData pt = point.routeMarkData;
|
||||
GetFramework().GetRoutingManager().AddRoutePoint(std::move(pt));
|
||||
[[MWMNavigationDashboardManager sharedManager] onRoutePointsUpdated];
|
||||
}
|
||||
|
||||
+ (void)addPointAndRebuild:(MWMRoutePoint *)point {
|
||||
if (!point)
|
||||
return;
|
||||
[self addPoint:point];
|
||||
[self rebuildWithBestRouter:NO];
|
||||
}
|
||||
|
||||
+ (void)buildFromPoint:(MWMRoutePoint *)startPoint bestRouter:(BOOL)bestRouter {
|
||||
if (!startPoint)
|
||||
return;
|
||||
[self addPoint:startPoint];
|
||||
[self rebuildWithBestRouter:bestRouter];
|
||||
}
|
||||
|
||||
+ (void)buildToPoint:(MWMRoutePoint *)finishPoint bestRouter:(BOOL)bestRouter {
|
||||
if (!finishPoint)
|
||||
return;
|
||||
[self addPoint:finishPoint];
|
||||
if (![self startPoint] && [MWMLocationManager lastLocation] && [MWMRouter router].canAutoAddLastLocation) {
|
||||
[self addPoint:[[MWMRoutePoint alloc] initWithLastLocationAndType:MWMRoutePointTypeStart intermediateIndex:0]];
|
||||
}
|
||||
if ([self startPoint] && [self finishPoint])
|
||||
[self rebuildWithBestRouter:bestRouter];
|
||||
}
|
||||
|
||||
+ (void)buildApiRouteWithType:(MWMRouterType)type
|
||||
startPoint:(MWMRoutePoint *)startPoint
|
||||
finishPoint:(MWMRoutePoint *)finishPoint {
|
||||
if (!startPoint || !finishPoint)
|
||||
return;
|
||||
|
||||
[MWMRouter setType:type];
|
||||
|
||||
auto router = [MWMRouter router];
|
||||
router.isAPICall = YES;
|
||||
[self addPoint:startPoint];
|
||||
[self addPoint:finishPoint];
|
||||
router.isAPICall = NO;
|
||||
|
||||
[self rebuildWithBestRouter:NO];
|
||||
}
|
||||
|
||||
+ (void)rebuildWithBestRouter:(BOOL)bestRouter {
|
||||
[self clearAltitudeImagesData];
|
||||
|
||||
auto &rm = GetFramework().GetRoutingManager();
|
||||
auto const &points = rm.GetRoutePoints();
|
||||
auto const pointsCount = points.size();
|
||||
if (pointsCount < 2) {
|
||||
[self doStop:NO];
|
||||
[[MWMMapViewControlsManager manager] onRoutePrepare];
|
||||
return;
|
||||
}
|
||||
if (bestRouter)
|
||||
self.type = routerType(rm.GetBestRouter(points.front().m_position, points.back().m_position));
|
||||
|
||||
[[MWMMapViewControlsManager manager] onRouteRebuild];
|
||||
rm.BuildRoute();
|
||||
}
|
||||
|
||||
+ (void)start {
|
||||
[self saveRoute];
|
||||
auto const doStart = ^{
|
||||
auto &rm = GetFramework().GetRoutingManager();
|
||||
auto const routePoints = rm.GetRoutePoints();
|
||||
if (routePoints.size() >= 2)
|
||||
{
|
||||
auto p1 = [[MWMRoutePoint alloc] initWithRouteMarkData:routePoints.front()];
|
||||
auto p2 = [[MWMRoutePoint alloc] initWithRouteMarkData:routePoints.back()];
|
||||
|
||||
CLLocation *lastLocation = [MWMLocationManager lastLocation];
|
||||
if (p1.isMyPosition && lastLocation)
|
||||
{
|
||||
rm.FollowRoute();
|
||||
[[MWMMapViewControlsManager manager] onRouteStart];
|
||||
[MWMThemeManager setAutoUpdates:YES];
|
||||
}
|
||||
else
|
||||
{
|
||||
BOOL const needToRebuild = lastLocation && [MWMLocationManager isStarted] && !p2.isMyPosition;
|
||||
|
||||
[[MWMAlertViewController activeAlertController]
|
||||
presentPoint2PointAlertWithOkBlock:^{
|
||||
[self buildFromPoint:[[MWMRoutePoint alloc] initWithLastLocationAndType:MWMRoutePointTypeStart
|
||||
intermediateIndex:0]
|
||||
bestRouter:NO];
|
||||
}
|
||||
needToRebuild:needToRebuild];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ([MWMSettings routingDisclaimerApproved]) {
|
||||
doStart();
|
||||
} else {
|
||||
[[MWMAlertViewController activeAlertController] presentRoutingDisclaimerAlertWithOkBlock:^{
|
||||
doStart();
|
||||
[MWMSettings setRoutingDisclaimerApproved];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)stop:(BOOL)removeRoutePoints {
|
||||
[self doStop:removeRoutePoints];
|
||||
[self hideNavigationMapControls];
|
||||
[MWMRouter router].canAutoAddLastLocation = YES;
|
||||
}
|
||||
|
||||
+ (void)doStop:(BOOL)removeRoutePoints {
|
||||
[self clearAltitudeImagesData];
|
||||
GetFramework().GetRoutingManager().CloseRouting(removeRoutePoints);
|
||||
if (removeRoutePoints)
|
||||
GetFramework().GetRoutingManager().DeleteSavedRoutePoints();
|
||||
[MWMThemeManager setAutoUpdates:NO];
|
||||
}
|
||||
|
||||
- (void)updateFollowingInfo {
|
||||
if (![MWMRouter isRoutingActive])
|
||||
return;
|
||||
auto const &rm = GetFramework().GetRoutingManager();
|
||||
routing::FollowingInfo info;
|
||||
rm.GetRouteFollowingInfo(info);
|
||||
if (!info.IsValid())
|
||||
return;
|
||||
auto navManager = [MWMNavigationDashboardManager sharedManager];
|
||||
if ([MWMRouter type] == MWMRouterTypePublicTransport)
|
||||
[navManager updateTransitInfo:rm.GetTransitRouteInfo()];
|
||||
else
|
||||
[navManager updateFollowingInfo:info routePoints:[MWMRouter points] type:[MWMRouter type]];
|
||||
}
|
||||
|
||||
+ (void)routeAltitudeImageForSize:(CGSize)size completion:(MWMImageHeightBlock)block {
|
||||
if (![self hasRouteAltitude])
|
||||
return;
|
||||
|
||||
auto altitudes = std::make_shared<RoutingManager::DistanceAltitude>();
|
||||
if (!GetFramework().GetRoutingManager().GetRouteAltitudesAndDistancesM(*altitudes))
|
||||
return;
|
||||
|
||||
// |altitudes| should not be used in the method after line below.
|
||||
dispatch_async(self.router.renderAltitudeImagesQueue, [=]() {
|
||||
auto router = self.router;
|
||||
CGFloat const screenScale = [UIScreen mainScreen].scale;
|
||||
CGSize const scaledSize = {size.width * screenScale, size.height * screenScale};
|
||||
CHECK_GREATER_OR_EQUAL(scaledSize.width, 0.0, ());
|
||||
CHECK_GREATER_OR_EQUAL(scaledSize.height, 0.0, ());
|
||||
uint32_t const width = static_cast<uint32_t>(scaledSize.width);
|
||||
uint32_t const height = static_cast<uint32_t>(scaledSize.height);
|
||||
if (width == 0 || height == 0)
|
||||
return;
|
||||
|
||||
NSValue *sizeValue = [NSValue valueWithCGSize:scaledSize];
|
||||
NSData *imageData = router.altitudeImagesData[sizeValue];
|
||||
if (!imageData)
|
||||
{
|
||||
altitudes->Simplify();
|
||||
|
||||
std::vector<uint8_t> imageRGBAData;
|
||||
if (!altitudes->GenerateRouteAltitudeChart(width, height, imageRGBAData))
|
||||
return;
|
||||
if (imageRGBAData.empty())
|
||||
return;
|
||||
imageData = [NSData dataWithBytes:imageRGBAData.data() length:imageRGBAData.size()];
|
||||
router.altitudeImagesData[sizeValue] = imageData;
|
||||
|
||||
uint32_t totalAscentM, totalDescentM;
|
||||
altitudes->CalculateAscentDescent(totalAscentM, totalDescentM);
|
||||
|
||||
auto const localizedUnits = platform::GetLocalizedAltitudeUnits();
|
||||
router.totalAscent = @(platform::Distance::FormatAltitude(totalAscentM).c_str());
|
||||
router.totalDescent = @(platform::Distance::FormatAltitude(totalDescentM).c_str());
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIImage *altitudeImage = [UIImage imageWithRGBAData:imageData width:width height:height];
|
||||
if (altitudeImage)
|
||||
block(altitudeImage, router.totalAscent, router.totalDescent);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+ (void)clearAltitudeImagesData {
|
||||
auto router = self.router;
|
||||
dispatch_async(router.renderAltitudeImagesQueue, ^{
|
||||
[router.altitudeImagesData removeAllObjects];
|
||||
router.totalAscent = nil;
|
||||
router.totalDescent = nil;
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - MWMLocationObserver
|
||||
|
||||
- (void)onLocationUpdate:(CLLocation *)location {
|
||||
if (![MWMRouter isRoutingActive])
|
||||
return;
|
||||
auto tts = [MWMTextToSpeech tts];
|
||||
NSArray<NSString *> *turnNotifications = [MWMRouter turnNotifications];
|
||||
if ([MWMRouter isOnRoute] && tts.active) {
|
||||
[tts playTurnNotifications:turnNotifications];
|
||||
[tts playWarningSound];
|
||||
}
|
||||
|
||||
[self updateFollowingInfo];
|
||||
}
|
||||
|
||||
#pragma mark - MWMFrameworkRouteBuilderObserver
|
||||
|
||||
- (void)onRouteReady:(BOOL)hasWarnings {
|
||||
self.routingOptions = [MWMRoutingOptions new];
|
||||
GetFramework().DeactivateMapSelection();
|
||||
|
||||
auto startPoint = [MWMRouter startPoint];
|
||||
if (!startPoint || !startPoint.isMyPosition) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[MWMRouter disableFollowMode];
|
||||
});
|
||||
}
|
||||
|
||||
[[MWMMapViewControlsManager manager] onRouteReady:hasWarnings];
|
||||
[self updateFollowingInfo];
|
||||
}
|
||||
|
||||
- (void)processRouteBuilderEvent:(routing::RouterResultCode)code
|
||||
countries:(storage::CountriesSet const &)absentCountries {
|
||||
MWMMapViewControlsManager *mapViewControlsManager = [MWMMapViewControlsManager manager];
|
||||
switch (code) {
|
||||
case routing::RouterResultCode::NoError:
|
||||
[self onRouteReady:NO];
|
||||
break;
|
||||
case routing::RouterResultCode::HasWarnings:
|
||||
[self onRouteReady:YES];
|
||||
break;
|
||||
case routing::RouterResultCode::RouteFileNotExist:
|
||||
case routing::RouterResultCode::InconsistentMWMandRoute:
|
||||
case routing::RouterResultCode::NeedMoreMaps:
|
||||
case routing::RouterResultCode::FileTooOld:
|
||||
case routing::RouterResultCode::RouteNotFound:
|
||||
self.routingOptions = [MWMRoutingOptions new];
|
||||
[self presentDownloaderAlert:code countries:absentCountries];
|
||||
[[MWMNavigationDashboardManager sharedManager] onRouteError:L(@"routing_planning_error")];
|
||||
break;
|
||||
case routing::RouterResultCode::Cancelled:
|
||||
[mapViewControlsManager onRoutePrepare];
|
||||
break;
|
||||
case routing::RouterResultCode::StartPointNotFound:
|
||||
case routing::RouterResultCode::EndPointNotFound:
|
||||
case routing::RouterResultCode::NoCurrentPosition:
|
||||
case routing::RouterResultCode::PointsInDifferentMWM:
|
||||
case routing::RouterResultCode::InternalError:
|
||||
case routing::RouterResultCode::IntermediatePointNotFound:
|
||||
case routing::RouterResultCode::TransitRouteNotFoundNoNetwork:
|
||||
case routing::RouterResultCode::TransitRouteNotFoundTooLongPedestrian:
|
||||
case routing::RouterResultCode::RouteNotFoundRedressRouteError:
|
||||
[[MWMAlertViewController activeAlertController] presentAlert:code];
|
||||
[[MWMNavigationDashboardManager sharedManager] onRouteError:L(@"routing_planning_error")];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)processRouteBuilderProgress:(CGFloat)progress {
|
||||
[[MWMNavigationDashboardManager sharedManager] setRouteBuilderProgress:progress];
|
||||
}
|
||||
|
||||
- (void)processRouteRecommendation:(MWMRouterRecommendation)recommendation {
|
||||
switch (recommendation) {
|
||||
case MWMRouterRecommendationRebuildAfterPointsLoading:
|
||||
[MWMRouter addPointAndRebuild:[[MWMRoutePoint alloc] initWithLastLocationAndType:MWMRoutePointTypeStart
|
||||
intermediateIndex:0]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Alerts
|
||||
|
||||
- (void)presentDownloaderAlert:(routing::RouterResultCode)code countries:(storage::CountriesSet const &)countries {
|
||||
MWMAlertViewController *activeAlertController = [MWMAlertViewController activeAlertController];
|
||||
if (!countries.empty()) {
|
||||
[activeAlertController presentDownloaderAlertWithCountries:countries
|
||||
code:code
|
||||
cancelBlock:^{
|
||||
if (code != routing::RouterResultCode::NeedMoreMaps)
|
||||
[MWMRouter stopRouting];
|
||||
}
|
||||
downloadBlock:^(storage::CountriesVec const &downloadCountries, MWMVoidBlock onSuccess) {
|
||||
NSMutableArray *array = [NSMutableArray arrayWithCapacity:downloadCountries.size()];
|
||||
for (auto const &cid : downloadCountries) {
|
||||
[array addObject:@(cid.c_str())];
|
||||
}
|
||||
[[MWMStorage sharedStorage] downloadNodes:array onSuccess:onSuccess];
|
||||
}
|
||||
downloadCompleteBlock:^{
|
||||
[MWMRouter rebuildWithBestRouter:NO];
|
||||
}];
|
||||
} else if ([MWMRouter hasActiveDrivingOptions]) {
|
||||
[activeAlertController presentDefaultAlertWithTitle:L(@"unable_to_calc_alert_title")
|
||||
message:L(@"unable_to_calc_alert_subtitle")
|
||||
rightButtonTitle:L(@"settings")
|
||||
leftButtonTitle:L(@"cancel")
|
||||
rightButtonAction:^{
|
||||
[[MapViewController sharedController] openDrivingOptions];
|
||||
}];
|
||||
} else {
|
||||
[activeAlertController presentAlert:code];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Save / Load route points
|
||||
|
||||
+ (void)saveRoute {
|
||||
GetFramework().GetRoutingManager().SaveRoutePoints();
|
||||
}
|
||||
|
||||
+ (void)saveRouteIfNeeded {
|
||||
if ([self isOnRoute])
|
||||
[self saveRoute];
|
||||
}
|
||||
|
||||
+ (void)restoreRouteIfNeeded {
|
||||
if ([MapsAppDelegate theApp].isDrapeEngineCreated) {
|
||||
auto &rm = GetFramework().GetRoutingManager();
|
||||
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 {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self restoreRouteIfNeeded];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)isRestoreProcessCompleted {
|
||||
return self.router.isRestoreProcessCompleted;
|
||||
}
|
||||
|
||||
+ (BOOL)hasSavedRoute {
|
||||
return GetFramework().GetRoutingManager().HasSavedRoutePoints();
|
||||
}
|
||||
|
||||
+ (void)updateRoute {
|
||||
MWMRoutingOptions *newOptions = [MWMRoutingOptions new];
|
||||
if (self.isRoutingActive && !self.isOnRoute && ![newOptions isEqual:[self router].routingOptions]) {
|
||||
[self rebuildWithBestRouter:YES];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)hasActiveDrivingOptions {
|
||||
return [MWMRoutingOptions new].hasOptions && self.type != MWMRouterTypeRuler;
|
||||
}
|
||||
|
||||
+ (void)avoidRoadTypeAndRebuild:(MWMRoadType)type {
|
||||
MWMRoutingOptions *options = [MWMRoutingOptions new];
|
||||
switch (type) {
|
||||
case MWMRoadTypeToll:
|
||||
options.avoidToll = YES;
|
||||
break;
|
||||
case MWMRoadTypeDirty:
|
||||
options.avoidDirty = YES;
|
||||
break;
|
||||
case MWMRoadTypePaved:
|
||||
options.avoidPaved = YES;
|
||||
break;
|
||||
case MWMRoadTypeFerry:
|
||||
options.avoidFerry = YES;
|
||||
break;
|
||||
case MWMRoadTypeMotorway:
|
||||
options.avoidMotorway = YES;
|
||||
break;
|
||||
case MWMRoadTypeSteps:
|
||||
options.avoidSteps = YES;
|
||||
break;
|
||||
}
|
||||
[options save];
|
||||
[self rebuildWithBestRouter:YES];
|
||||
}
|
||||
|
||||
+ (void)showNavigationMapControls {
|
||||
[[MWMMapViewControlsManager manager] onRouteStart];
|
||||
}
|
||||
|
||||
+ (void)hideNavigationMapControls {
|
||||
[[MWMMapViewControlsManager manager] onRouteStop];
|
||||
}
|
||||
|
||||
@end
|
||||
3
iphone/Maps/Core/Routing/MWMRouterRecommendation.h
Normal file
3
iphone/Maps/Core/Routing/MWMRouterRecommendation.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
typedef NS_ENUM(NSUInteger, MWMRouterRecommendation) {
|
||||
MWMRouterRecommendationRebuildAfterPointsLoading
|
||||
};
|
||||
12
iphone/Maps/Core/Routing/MWMRouterTransitStepInfo.h
Normal file
12
iphone/Maps/Core/Routing/MWMRouterTransitStepInfo.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#import "MWMRouterTransitType.h"
|
||||
|
||||
@interface MWMRouterTransitStepInfo : NSObject
|
||||
|
||||
@property(nonatomic, readwrite) MWMRouterTransitType type;
|
||||
@property(copy, nonatomic, readwrite) NSString * distance;
|
||||
@property(copy, nonatomic, readwrite) NSString * distanceUnits;
|
||||
@property(copy, nonatomic, readwrite) NSString * number;
|
||||
@property(nonatomic, readwrite) UIColor * color;
|
||||
@property(nonatomic, readwrite) NSInteger intermediateIndex;
|
||||
|
||||
@end
|
||||
54
iphone/Maps/Core/Routing/MWMRouterTransitStepInfo.mm
Normal file
54
iphone/Maps/Core/Routing/MWMRouterTransitStepInfo.mm
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#import "MWMRouterTransitStepInfo.h"
|
||||
|
||||
#include "map/routing_manager.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
MWMRouterTransitType convertType(TransitType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TransitType::IntermediatePoint: return MWMRouterTransitTypeIntermediatePoint;
|
||||
case TransitType::Pedestrian: return MWMRouterTransitTypePedestrian;
|
||||
case TransitType::Subway: return MWMRouterTransitTypeSubway;
|
||||
case TransitType::Train: return MWMRouterTransitTypeTrain;
|
||||
case TransitType::LightRail: return MWMRouterTransitTypeLightRail;
|
||||
case TransitType::Monorail: return MWMRouterTransitTypeMonorail;
|
||||
}
|
||||
|
||||
// This is temporary solution for compiling iOS project after adding new
|
||||
// TransitType values. When these values will be approved we'll add them
|
||||
// above in switch(type) and remove this line.
|
||||
// TODO(o.khlopkova) Replace this return with more cases when transit
|
||||
// types are ready.
|
||||
return MWMRouterTransitTypePedestrian;
|
||||
}
|
||||
|
||||
UIColor * convertColor(uint32_t colorARGB)
|
||||
{
|
||||
CGFloat const alpha = CGFloat((colorARGB >> 24) & 0xFF) / 255;
|
||||
CGFloat const red = CGFloat((colorARGB >> 16) & 0xFF) / 255;
|
||||
CGFloat const green = CGFloat((colorARGB >> 8) & 0xFF) / 255;
|
||||
CGFloat const blue = CGFloat(colorARGB & 0xFF) / 255;
|
||||
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@implementation MWMRouterTransitStepInfo
|
||||
|
||||
- (instancetype)initWithStepInfo:(TransitStepInfo const &)info
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_type = convertType(info.m_type);
|
||||
_distance = @(info.m_distanceStr.c_str());
|
||||
_distanceUnits = @(info.m_distanceUnitsSuffix.c_str());
|
||||
_number = @(info.m_number.c_str());
|
||||
_color = convertColor(info.m_colorARGB);
|
||||
_intermediateIndex = info.m_intermediateIndex;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
9
iphone/Maps/Core/Routing/MWMRouterTransitType.h
Normal file
9
iphone/Maps/Core/Routing/MWMRouterTransitType.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
typedef NS_CLOSED_ENUM(NSUInteger, MWMRouterTransitType) {
|
||||
MWMRouterTransitTypeIntermediatePoint,
|
||||
MWMRouterTransitTypePedestrian,
|
||||
MWMRouterTransitTypeSubway,
|
||||
MWMRouterTransitTypeTrain,
|
||||
MWMRouterTransitTypeLightRail,
|
||||
MWMRouterTransitTypeMonorail,
|
||||
MWMRouterTransitTypeRuler
|
||||
};
|
||||
7
iphone/Maps/Core/Routing/MWMRouterType.h
Normal file
7
iphone/Maps/Core/Routing/MWMRouterType.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
typedef NS_ENUM(NSUInteger, MWMRouterType) {
|
||||
MWMRouterTypeVehicle,
|
||||
MWMRouterTypePedestrian,
|
||||
MWMRouterTypePublicTransport,
|
||||
MWMRouterTypeBicycle,
|
||||
MWMRouterTypeRuler,
|
||||
};
|
||||
7
iphone/Maps/Core/Search/MWMSearch+CoreSpotlight.h
Normal file
7
iphone/Maps/Core/Search/MWMSearch+CoreSpotlight.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#import "MWMSearch.h"
|
||||
|
||||
@interface MWMSearch (CoreSpotlight)
|
||||
|
||||
+ (void)addCategoriesToSpotlight;
|
||||
|
||||
@end
|
||||
78
iphone/Maps/Core/Search/MWMSearch+CoreSpotlight.mm
Normal file
78
iphone/Maps/Core/Search/MWMSearch+CoreSpotlight.mm
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#import <CoreSpotlight/CoreSpotlight.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import <CoreApi/Framework.h>
|
||||
#import <CoreApi/AppInfo.h>
|
||||
#import <CoreApi/MWMCommon.h>
|
||||
#import "MWMSearch+CoreSpotlight.h"
|
||||
#import "MWMSettings.h"
|
||||
|
||||
@implementation MWMSearch (CoreSpotlight)
|
||||
|
||||
+ (void)addCategoriesToSpotlight
|
||||
{
|
||||
if (isIOSVersionLessThan(9) || ![CSSearchableIndex isIndexingAvailable])
|
||||
return;
|
||||
|
||||
NSString * localeLanguageId = [[AppInfo sharedInfo] languageId];
|
||||
if ([localeLanguageId isEqualToString:[MWMSettings spotlightLocaleLanguageId]])
|
||||
return;
|
||||
|
||||
auto const & categories = GetFramework().GetDisplayedCategories();
|
||||
auto const & categoriesKeys = categories.GetKeys();
|
||||
NSMutableArray<CSSearchableItem *> * items = [@[] mutableCopy];
|
||||
|
||||
for (auto const & categoryKey : categoriesKeys)
|
||||
{
|
||||
CSSearchableItemAttributeSet * attrSet = [[CSSearchableItemAttributeSet alloc]
|
||||
initWithItemContentType: UTTypeItem.identifier];
|
||||
|
||||
NSString * categoryName = nil;
|
||||
NSMutableDictionary<NSString *, NSString *> * localizedStrings = [@{} mutableCopy];
|
||||
|
||||
categories.ForEachSynonym(categoryKey, [&localizedStrings, &localeLanguageId, &categoryName](
|
||||
std::string const & name, std::string const & locale) {
|
||||
NSString * nsName = @(name.c_str());
|
||||
NSString * nsLocale = @(locale.c_str());
|
||||
if ([localeLanguageId isEqualToString:nsLocale])
|
||||
categoryName = nsName;
|
||||
localizedStrings[nsLocale] = nsName;
|
||||
});
|
||||
attrSet.alternateNames = localizedStrings.allValues;
|
||||
attrSet.keywords = localizedStrings.allValues;
|
||||
attrSet.title = categoryName;
|
||||
attrSet.displayName = [[CSLocalizedString alloc] initWithLocalizedStrings:localizedStrings];
|
||||
|
||||
NSString * categoryKeyString = @(categoryKey.c_str());
|
||||
NSString * imageName = [NSString stringWithFormat:@"Search/Categories/%@", [categoryKeyString stringByReplacingOccurrencesOfString: @"category_" withString:@""]];
|
||||
UIImage * image = [UIImage imageNamed:imageName inBundle:nil compatibleWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle: UIUserInterfaceStyleLight]];
|
||||
UIGraphicsBeginImageContext(CGSizeMake(360, 360));
|
||||
[image drawInRect:CGRectMake(0, 0, 360, 360)];
|
||||
UIImage * resizedImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext() ;
|
||||
attrSet.thumbnailData = UIImagePNGRepresentation(resizedImage);
|
||||
|
||||
CSSearchableItem * item =
|
||||
[[CSSearchableItem alloc] initWithUniqueIdentifier:categoryKeyString
|
||||
domainIdentifier:@"comaps.app.categories"
|
||||
attributeSet:attrSet];
|
||||
[items addObject:item];
|
||||
}
|
||||
|
||||
[[CSSearchableIndex defaultSearchableIndex]
|
||||
indexSearchableItems:items
|
||||
completionHandler:^(NSError * _Nullable error) {
|
||||
if (error)
|
||||
{
|
||||
NSError * err = error;
|
||||
LOG(LERROR,
|
||||
("addCategoriesToSpotlight failed: ", err.localizedDescription.UTF8String));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LINFO, ("addCategoriesToSpotlight succeded"));
|
||||
[MWMSettings setSpotlightLocaleLanguageId:localeLanguageId];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
58
iphone/Maps/Core/Search/MWMSearch.h
Normal file
58
iphone/Maps/Core/Search/MWMSearch.h
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#import "MWMSearchObserver.h"
|
||||
#import "SearchItemType.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, SearchTextSource) {
|
||||
SearchTextSourceTypedText,
|
||||
SearchTextSourceCategory,
|
||||
SearchTextSourceHistory,
|
||||
SearchTextSourceSuggestion,
|
||||
SearchTextSourceDeeplink
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, SearchMode) {
|
||||
SearchModeEverywhere,
|
||||
SearchModeViewport,
|
||||
SearchModeEverywhereAndViewport
|
||||
};
|
||||
|
||||
@class SearchResult;
|
||||
@class SearchQuery;
|
||||
|
||||
@protocol SearchManager
|
||||
|
||||
+ (void)addObserver:(id<MWMSearchObserver>)observer;
|
||||
+ (void)removeObserver:(id<MWMSearchObserver>)observer;
|
||||
|
||||
+ (void)saveQuery:(SearchQuery *)query;
|
||||
+ (void)searchQuery:(SearchQuery *)query;
|
||||
|
||||
+ (void)showResultAtIndex:(NSUInteger)index;
|
||||
+ (SearchMode)searchMode;
|
||||
+ (void)setSearchMode:(SearchMode)mode;
|
||||
|
||||
+ (NSArray<SearchResult *> *)getResults;
|
||||
|
||||
+ (void)clear;
|
||||
@end
|
||||
|
||||
NS_SWIFT_NAME(Search)
|
||||
@interface MWMSearch : NSObject<SearchManager>
|
||||
|
||||
+ (SearchItemType)resultTypeWithRow:(NSUInteger)row;
|
||||
+ (NSUInteger)containerIndexWithRow:(NSUInteger)row;
|
||||
+ (SearchResult *)resultWithContainerIndex:(NSUInteger)index;
|
||||
|
||||
+ (NSUInteger)suggestionsCount;
|
||||
+ (NSUInteger)resultsCount;
|
||||
|
||||
- (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
|
||||
297
iphone/Maps/Core/Search/MWMSearch.mm
Normal file
297
iphone/Maps/Core/Search/MWMSearch.mm
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
#import "MWMSearch.h"
|
||||
#import "MWMFrameworkListener.h"
|
||||
#import "MWMFrameworkObservers.h"
|
||||
#import "SearchResult+Core.h"
|
||||
#import "SwiftBridge.h"
|
||||
|
||||
#include <CoreApi/MWMTypes.h>
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
#include "platform/network_policy.hpp"
|
||||
|
||||
namespace {
|
||||
using Observer = id<MWMSearchObserver>;
|
||||
using Observers = NSHashTable<Observer>;
|
||||
} // namespace
|
||||
|
||||
@interface MWMSearch () <MWMFrameworkDrapeObserver>
|
||||
|
||||
@property(nonatomic) NSUInteger suggestionsCount;
|
||||
@property(nonatomic) SearchMode searchMode;
|
||||
@property(nonatomic) BOOL textChanged;
|
||||
@property(nonatomic) Observers * observers;
|
||||
@property(nonatomic) NSUInteger lastSearchTimestamp;
|
||||
@property(nonatomic) SearchIndex * itemsIndex;
|
||||
@property(nonatomic) NSInteger searchCount;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMSearch {
|
||||
std::string m_query;
|
||||
std::string m_locale;
|
||||
bool m_isCategory;
|
||||
search::Results m_everywhereResults;
|
||||
search::Results m_viewportResults;
|
||||
}
|
||||
|
||||
#pragma mark - Instance
|
||||
|
||||
+ (MWMSearch *)manager {
|
||||
static MWMSearch *manager;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
manager = [[self alloc] initManager];
|
||||
});
|
||||
return manager;
|
||||
}
|
||||
|
||||
- (instancetype)initManager {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_observers = [Observers weakObjectsHashTable];
|
||||
[MWMFrameworkListener addObserver:self];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)searchEverywhere {
|
||||
self.lastSearchTimestamp += 1;
|
||||
NSUInteger const timestamp = self.lastSearchTimestamp;
|
||||
|
||||
search::EverywhereSearchParams params{
|
||||
m_query, m_locale, {} /* default timeout */, m_isCategory,
|
||||
// m_onResults
|
||||
[self, timestamp](search::Results results, std::vector<search::ProductInfo> productInfo)
|
||||
{
|
||||
// Store the flag first, because we will make move next.
|
||||
bool const isEndMarker = results.IsEndMarker();
|
||||
|
||||
if (timestamp == self.lastSearchTimestamp)
|
||||
{
|
||||
self.suggestionsCount = results.GetSuggestsCount();
|
||||
self->m_everywhereResults = std::move(results);
|
||||
|
||||
[self onSearchResultsUpdated];
|
||||
}
|
||||
|
||||
if (isEndMarker)
|
||||
self.searchCount -= 1;
|
||||
}
|
||||
};
|
||||
|
||||
GetFramework().GetSearchAPI().SearchEverywhere(std::move(params));
|
||||
self.searchCount += 1;
|
||||
}
|
||||
|
||||
- (void)searchInViewport {
|
||||
search::ViewportSearchParams params {
|
||||
m_query, m_locale, {} /* default timeout */, m_isCategory,
|
||||
// m_onStarted
|
||||
{},
|
||||
// m_onCompleted
|
||||
[self](search::Results results)
|
||||
{
|
||||
if (!results.IsEndMarker())
|
||||
return;
|
||||
if (!results.IsEndedCancelled())
|
||||
self->m_viewportResults = std::move(results);
|
||||
}
|
||||
};
|
||||
|
||||
GetFramework().GetSearchAPI().SearchInViewport(std::move(params));
|
||||
}
|
||||
|
||||
- (void)update {
|
||||
if (m_query.empty())
|
||||
return;
|
||||
|
||||
switch (self.searchMode) {
|
||||
case SearchModeEverywhere:
|
||||
[self searchEverywhere];
|
||||
break;
|
||||
case SearchModeViewport:
|
||||
[self searchInViewport];
|
||||
break;
|
||||
case SearchModeEverywhereAndViewport:
|
||||
[self searchEverywhere];
|
||||
[self searchInViewport];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Add/Remove Observers
|
||||
|
||||
+ (void)addObserver:(id<MWMSearchObserver>)observer {
|
||||
[[MWMSearch manager].observers addObject:observer];
|
||||
}
|
||||
|
||||
+ (void)removeObserver:(id<MWMSearchObserver>)observer {
|
||||
[[MWMSearch manager].observers removeObject:observer];
|
||||
}
|
||||
|
||||
#pragma mark - Methods
|
||||
|
||||
+ (void)saveQuery:(SearchQuery *)query {
|
||||
if (!query.text || query.text.length == 0)
|
||||
return;
|
||||
|
||||
std::string locale = (!query.locale || query.locale == 0)
|
||||
? [MWMSearch manager]->m_locale
|
||||
: query.locale.UTF8String;
|
||||
std::string text = query.text.UTF8String;
|
||||
GetFramework().GetSearchAPI().SaveSearchQuery({std::move(locale), std::move(text)});
|
||||
}
|
||||
|
||||
+ (void)searchQuery:(SearchQuery *)query {
|
||||
if (!query.text)
|
||||
return;
|
||||
|
||||
MWMSearch *manager = [MWMSearch manager];
|
||||
if (query.locale.length != 0)
|
||||
manager->m_locale = query.locale.UTF8String;
|
||||
|
||||
// Pass input query as-is without any normalization (precomposedStringWithCompatibilityMapping).
|
||||
// Otherwise № -> No, and it's unexpectable for the search index.
|
||||
manager->m_query = query.text.UTF8String;
|
||||
manager->m_isCategory = (query.source == SearchTextSourceCategory);
|
||||
manager.textChanged = YES;
|
||||
|
||||
[manager reset];
|
||||
[manager update];
|
||||
}
|
||||
|
||||
+ (void)showResultAtIndex:(NSUInteger)index {
|
||||
auto const & result = [MWMSearch manager]->m_everywhereResults[index];
|
||||
GetFramework().StopLocationFollow();
|
||||
GetFramework().SelectSearchResult(result, true);
|
||||
}
|
||||
|
||||
+ (SearchResult *)resultWithContainerIndex:(NSUInteger)index {
|
||||
SearchResult * result = [[SearchResult alloc] initWithResult:[MWMSearch manager]->m_everywhereResults[index]
|
||||
itemType:[MWMSearch resultTypeWithRow:index]
|
||||
index:index];
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (NSArray<SearchResult *> *)getResults {
|
||||
NSMutableArray<SearchResult *> * results = [[NSMutableArray alloc] initWithCapacity:MWMSearch.resultsCount];
|
||||
for (NSUInteger i = 0; i < MWMSearch.resultsCount; ++i) {
|
||||
SearchResult * result = [MWMSearch resultWithContainerIndex:i];
|
||||
[results addObject:result];
|
||||
}
|
||||
return [results copy];
|
||||
}
|
||||
|
||||
+ (SearchItemType)resultTypeWithRow:(NSUInteger)row {
|
||||
auto itemsIndex = [MWMSearch manager].itemsIndex;
|
||||
return [itemsIndex resultTypeWithRow:row];
|
||||
}
|
||||
|
||||
+ (NSUInteger)containerIndexWithRow:(NSUInteger)row {
|
||||
auto itemsIndex = [MWMSearch manager].itemsIndex;
|
||||
return [itemsIndex resultContainerIndexWithRow:row];
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
self.lastSearchTimestamp += 1;
|
||||
GetFramework().GetSearchAPI().CancelAllSearches();
|
||||
|
||||
m_everywhereResults.Clear();
|
||||
m_viewportResults.Clear();
|
||||
|
||||
[self onSearchResultsUpdated];
|
||||
}
|
||||
|
||||
+ (void)clear {
|
||||
auto manager = [MWMSearch manager];
|
||||
manager->m_query.clear();
|
||||
manager.suggestionsCount = 0;
|
||||
[manager reset];
|
||||
}
|
||||
|
||||
+ (SearchMode)searchMode {
|
||||
return [MWMSearch manager].searchMode;
|
||||
}
|
||||
|
||||
+ (void)setSearchMode:(SearchMode)mode {
|
||||
MWMSearch * manager = [MWMSearch manager];
|
||||
if (manager.searchMode == mode)
|
||||
return;
|
||||
manager.searchMode = mode;
|
||||
[manager update];
|
||||
}
|
||||
|
||||
+ (NSUInteger)suggestionsCount {
|
||||
return [MWMSearch manager].suggestionsCount;
|
||||
}
|
||||
|
||||
+ (NSUInteger)resultsCount {
|
||||
return [MWMSearch manager].itemsIndex.count;
|
||||
}
|
||||
|
||||
- (void)updateItemsIndexWithBannerReload:(BOOL)reloadBanner {
|
||||
auto const resultsCount = self->m_everywhereResults.GetCount();
|
||||
auto const itemsIndex = [[SearchIndex alloc] initWithSuggestionsCount:self.suggestionsCount
|
||||
resultsCount:resultsCount];
|
||||
[itemsIndex build];
|
||||
self.itemsIndex = itemsIndex;
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)onSearchStarted {
|
||||
for (Observer observer in self.observers) {
|
||||
if ([observer respondsToSelector:@selector(onSearchStarted)])
|
||||
[observer onSearchStarted];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onSearchCompleted {
|
||||
[self updateItemsIndexWithBannerReload:YES];
|
||||
for (Observer observer in self.observers) {
|
||||
if ([observer respondsToSelector:@selector(onSearchCompleted)])
|
||||
[observer onSearchCompleted];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onSearchResultsUpdated {
|
||||
[self updateItemsIndexWithBannerReload:NO];
|
||||
for (Observer observer in self.observers) {
|
||||
if ([observer respondsToSelector:@selector(onSearchResultsUpdated)])
|
||||
[observer onSearchResultsUpdated];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - MWMFrameworkDrapeObserver
|
||||
|
||||
- (void)processViewportChangedEvent {
|
||||
if (!GetFramework().GetSearchAPI().IsViewportSearchActive())
|
||||
return;
|
||||
|
||||
BOOL const isSearchCompleted = self.searchCount == 0;
|
||||
if (!isSearchCompleted)
|
||||
return;
|
||||
|
||||
switch (self.searchMode) {
|
||||
case SearchModeEverywhere:
|
||||
case SearchModeViewport:
|
||||
break;
|
||||
case SearchModeEverywhereAndViewport:
|
||||
[self searchEverywhere];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (void)setSearchCount:(NSInteger)searchCount {
|
||||
NSAssert((searchCount >= 0) && ((_searchCount == searchCount - 1) || (_searchCount == searchCount + 1)),
|
||||
@"Invalid search count update");
|
||||
if (searchCount > 0)
|
||||
[self onSearchStarted];
|
||||
else if (searchCount == 0)
|
||||
[self onSearchCompleted];
|
||||
_searchCount = searchCount;
|
||||
}
|
||||
|
||||
@end
|
||||
8
iphone/Maps/Core/Search/MWMSearchObserver.h
Normal file
8
iphone/Maps/Core/Search/MWMSearchObserver.h
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
@protocol MWMSearchObserver<NSObject>
|
||||
|
||||
@optional
|
||||
- (void)onSearchStarted;
|
||||
- (void)onSearchCompleted;
|
||||
- (void)onSearchResultsUpdated;
|
||||
|
||||
@end
|
||||
39
iphone/Maps/Core/Search/SearchBanners.swift
Normal file
39
iphone/Maps/Core/Search/SearchBanners.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
@objc(MWMSearchBanners)
|
||||
final class SearchBanners: NSObject {
|
||||
private var banners: [MWMBanner] = []
|
||||
|
||||
weak var searchIndex: SearchIndex?
|
||||
|
||||
@objc init(searchIndex: SearchIndex) {
|
||||
self.searchIndex = searchIndex
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc func add(_ banner: MWMBanner) {
|
||||
guard let searchIndex = searchIndex else { return }
|
||||
banners.append(banner)
|
||||
let type: MWMSearchItemType
|
||||
let prefferedPosition: Int
|
||||
switch banner.mwmType {
|
||||
case .mopub:
|
||||
type = .mopub
|
||||
prefferedPosition = 2
|
||||
case .facebook:
|
||||
type = .facebook
|
||||
prefferedPosition = 2
|
||||
default:
|
||||
assert(false, "Unsupported banner type")
|
||||
type = .regular
|
||||
prefferedPosition = 0
|
||||
}
|
||||
searchIndex.addItem(type: type, prefferedPosition: prefferedPosition, containerIndex: banners.count - 1)
|
||||
}
|
||||
|
||||
@objc func banner(atIndex index: Int) -> MWMBanner {
|
||||
return banners[index]
|
||||
}
|
||||
|
||||
deinit {
|
||||
banners.forEach { BannersCache.cache.bannerIsOutOfScreen(coreBanner: $0) }
|
||||
}
|
||||
}
|
||||
87
iphone/Maps/Core/Search/SearchIndex.swift
Normal file
87
iphone/Maps/Core/Search/SearchIndex.swift
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
@objc
|
||||
final class SearchIndex: NSObject {
|
||||
fileprivate struct Item {
|
||||
let type: SearchItemType
|
||||
let containerIndex: Int
|
||||
}
|
||||
|
||||
fileprivate struct PositionItem {
|
||||
let item: Item
|
||||
var position: Int
|
||||
}
|
||||
|
||||
private var positionItems: [PositionItem] = []
|
||||
private var items: [Item] = []
|
||||
|
||||
@objc var count: Int {
|
||||
return items.count
|
||||
}
|
||||
|
||||
@objc init(suggestionsCount: Int, resultsCount: Int) {
|
||||
for index in 0 ..< resultsCount {
|
||||
let type: SearchItemType = index < suggestionsCount ? .suggestion : .regular
|
||||
let item = Item(type: type, containerIndex: index)
|
||||
positionItems.append(PositionItem(item: item, position: index))
|
||||
}
|
||||
super.init()
|
||||
}
|
||||
|
||||
func addItem(type: SearchItemType, prefferedPosition: Int, containerIndex: Int) {
|
||||
assert(type != .suggestion && type != .regular)
|
||||
let item = Item(type: type, containerIndex: containerIndex)
|
||||
positionItems.append(PositionItem(item: item, position: prefferedPosition))
|
||||
}
|
||||
|
||||
@objc func build() {
|
||||
positionItems.sort(by: >)
|
||||
var itemsDict: [Int: Item] = [:]
|
||||
positionItems.forEach { item in
|
||||
var position = item.position
|
||||
while itemsDict[position] != nil {
|
||||
position += 1
|
||||
}
|
||||
itemsDict[position] = item.item
|
||||
}
|
||||
|
||||
items.removeAll()
|
||||
let keys = itemsDict.keys.sorted()
|
||||
for index in 0 ..< keys.count {
|
||||
let key = keys[index]
|
||||
if index == key {
|
||||
items.append(itemsDict[key]!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func resultType(row: Int) -> SearchItemType {
|
||||
return items[row].type
|
||||
}
|
||||
|
||||
@objc func resultContainerIndex(row: Int) -> Int {
|
||||
return items[row].containerIndex
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchIndex.PositionItem: Equatable {
|
||||
static func ==(lhs: SearchIndex.PositionItem, rhs: SearchIndex.PositionItem) -> Bool {
|
||||
let lhsCache = lhs.item
|
||||
let rhsCache = rhs.item
|
||||
return lhsCache.type == rhsCache.type &&
|
||||
lhs.position == rhs.position &&
|
||||
lhsCache.containerIndex == rhsCache.containerIndex
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchIndex.PositionItem: Comparable {
|
||||
static func <(lhs: SearchIndex.PositionItem, rhs: SearchIndex.PositionItem) -> Bool {
|
||||
let lhsCache = lhs.item
|
||||
let rhsCache = rhs.item
|
||||
guard lhsCache.type == rhsCache.type else {
|
||||
return lhsCache.type.rawValue < rhsCache.type.rawValue
|
||||
}
|
||||
guard lhs.position == rhs.position else {
|
||||
return lhs.position > rhs.position
|
||||
}
|
||||
return lhsCache.containerIndex < rhsCache.containerIndex
|
||||
}
|
||||
}
|
||||
4
iphone/Maps/Core/Search/SearchItemType.h
Normal file
4
iphone/Maps/Core/Search/SearchItemType.h
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
typedef NS_ENUM(NSInteger, SearchItemType) {
|
||||
SearchItemTypeRegular,
|
||||
SearchItemTypeSuggestion
|
||||
};
|
||||
13
iphone/Maps/Core/Search/SearchResult+Core.h
Normal file
13
iphone/Maps/Core/Search/SearchResult+Core.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#import "SearchResult.h"
|
||||
|
||||
#import "search/result.hpp"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SearchResult (Core)
|
||||
|
||||
- (instancetype)initWithResult:(const search::Result &)result itemType:(SearchItemType)itemType index:(NSUInteger)index;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
32
iphone/Maps/Core/Search/SearchResult.h
Normal file
32
iphone/Maps/Core/Search/SearchResult.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#import "SearchItemType.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SearchResult : NSObject
|
||||
|
||||
@property (nonatomic, readonly) NSUInteger index;
|
||||
@property (nonatomic, readonly) NSString * titleText;
|
||||
@property (nonatomic, readonly, nullable) NSString * branchText;
|
||||
@property (nonatomic, readonly) NSString * iconImageName;
|
||||
@property (nonatomic, readonly) NSString * addressText;
|
||||
@property (nonatomic, readonly) NSString * infoText;
|
||||
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
|
||||
@property (nonatomic, readonly) CGPoint point;
|
||||
@property (nonatomic, readonly, nullable) NSString * distanceText;
|
||||
@property (nonatomic, readonly, nullable) NSString * openStatusText;
|
||||
@property (nonatomic, readonly) UIColor * openStatusColor;
|
||||
@property (nonatomic, readonly) BOOL isPopularHidden;
|
||||
@property (nonatomic, readonly) NSString * suggestion;
|
||||
@property (nonatomic, readonly) BOOL isPureSuggest;
|
||||
@property (nonatomic, readonly) NSArray<NSValue *> * highlightRanges;
|
||||
@property (nonatomic, readonly) SearchItemType itemType;
|
||||
|
||||
/// This initializer is intended only for testing purposes.
|
||||
- (instancetype)initWithTitleText:(NSString *)titleText type:(SearchItemType)type suggestion:(NSString *)suggestion;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
118
iphone/Maps/Core/Search/SearchResult.mm
Normal file
118
iphone/Maps/Core/Search/SearchResult.mm
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#import "SearchResult+Core.h"
|
||||
#import "CLLocation+Mercator.h"
|
||||
#import "MWMLocationManager.h"
|
||||
#import "SwiftBridge.h"
|
||||
|
||||
#import "platform/localization.hpp"
|
||||
#import "platform/distance.hpp"
|
||||
|
||||
#include "map/bookmark_helpers.hpp"
|
||||
|
||||
#import "geometry/mercator.hpp"
|
||||
|
||||
@implementation SearchResult
|
||||
|
||||
- (instancetype)initWithTitleText:(NSString *)titleText type:(SearchItemType)type suggestion:(NSString *)suggestion {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_titleText = titleText;
|
||||
_itemType = type;
|
||||
_suggestion = suggestion;
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SearchResult(Core)
|
||||
|
||||
- (instancetype)initWithResult:(const search::Result &)result itemType:(SearchItemType)itemType index:(NSUInteger)index {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_index = index;
|
||||
_titleText = result.GetString().empty() ? @(result.GetLocalizedFeatureType().c_str()) : @(result.GetString().c_str());
|
||||
_addressText = @(result.GetAddress().c_str());
|
||||
_infoText = @(result.GetFeatureDescription().c_str());
|
||||
_branchText = result.GetBranch().empty() ? nil : @(result.GetBranch().c_str());
|
||||
if (result.IsSuggest())
|
||||
_suggestion = @(result.GetSuggestionString().c_str());
|
||||
|
||||
_distanceText = nil;
|
||||
if (result.HasPoint()) {
|
||||
auto const center = result.GetFeatureCenter();
|
||||
_point = CGPointMake(center.x, center.y);
|
||||
auto const [centerLat, centerLon] = mercator::ToLatLon(center);
|
||||
_coordinate = CLLocationCoordinate2DMake(centerLat, centerLon);
|
||||
|
||||
CLLocation * lastLocation = [MWMLocationManager lastLocation];
|
||||
if (lastLocation) {
|
||||
double const distanceM = mercator::DistanceOnEarth(lastLocation.mercator, center);
|
||||
std::string const distanceStr = platform::Distance::CreateFormatted(distanceM).ToString();
|
||||
_distanceText = @(distanceStr.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
switch (result.IsOpenNow()) {
|
||||
case osm::Yes: {
|
||||
const int minutes = result.GetMinutesUntilClosed();
|
||||
if (minutes < 60) { // less than 1 hour
|
||||
_openStatusColor = [UIColor colorNamed:@"Base Colors/Yellow Color"];
|
||||
NSString * time = [NSString stringWithFormat:@"%d %@", minutes, L(@"minute")];
|
||||
_openStatusText = [NSString stringWithFormat:L(@"closes_in"), time];
|
||||
} else {
|
||||
_openStatusColor = [UIColor colorNamed:@"Base Colors/Green Color"];
|
||||
_openStatusText = L(@"editor_time_open");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case osm::No: {
|
||||
const int minutes = result.GetMinutesUntilOpen();
|
||||
if (minutes < 15) { // less than 15 minutes
|
||||
_openStatusColor = [UIColor colorNamed:@"Base Colors/Yellow Color"];
|
||||
NSString * time = [NSString stringWithFormat:@"%d %@", minutes, L(@"minute")];
|
||||
_openStatusText = [NSString stringWithFormat:L(@"opens_in"), time];
|
||||
}
|
||||
else if (minutes < 60) { // less than an hour (but more than 15 mins)
|
||||
_openStatusColor = [UIColor colorNamed:@"Base Colors/Red Color"];
|
||||
NSString * time = [NSString stringWithFormat:@"%d %@", minutes, L(@"minute")];
|
||||
_openStatusText = [NSString stringWithFormat:L(@"opens_in"), time];
|
||||
}
|
||||
else { // opens later or schedule is unknown
|
||||
_openStatusColor = [UIColor colorNamed:@"Base Colors/Red Color"];
|
||||
_openStatusText = L(@"closed");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case osm::Unknown: {
|
||||
_openStatusText = nil;
|
||||
_openStatusColor = UIColor.clearColor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_isPopularHidden = YES; // Restore logic in the future when popularity is available.
|
||||
_isPureSuggest = result.GetResultType() == search::Result::Type::PureSuggest;
|
||||
|
||||
NSMutableArray<NSValue *> * ranges = [NSMutableArray array];
|
||||
size_t const rangesCount = result.GetHighlightRangesCount();
|
||||
for (size_t i = 0; i < rangesCount; ++i) {
|
||||
auto const &range = result.GetHighlightRange(i);
|
||||
NSRange nsRange = NSMakeRange(range.first, range.second);
|
||||
[ranges addObject:[NSValue valueWithRange:nsRange]];
|
||||
}
|
||||
_highlightRanges = [ranges copy];
|
||||
|
||||
_itemType = itemType;
|
||||
|
||||
if (result.GetResultType() == search::Result::Type::Feature) {
|
||||
auto const featureType = result.GetFeatureType();
|
||||
auto const bookmarkImage = GetBookmarkIconByFeatureType(featureType);
|
||||
_iconImageName = [NSString stringWithFormat:@"%@%@",
|
||||
@"ic_bm_",
|
||||
[@(kml::ToString(bookmarkImage).c_str()) lowercaseString]];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
19
iphone/Maps/Core/Settings/MWMCoreUnits.h
Normal file
19
iphone/Maps/Core/Settings/MWMCoreUnits.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#include "platform/measurement_utils.hpp"
|
||||
|
||||
static inline measurement_utils::Units coreUnits(MWMUnits units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case MWMUnitsMetric: return measurement_utils::Units::Metric;
|
||||
case MWMUnitsImperial: return measurement_utils::Units::Imperial;
|
||||
}
|
||||
}
|
||||
|
||||
static inline MWMUnits mwmUnits(measurement_utils::Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case measurement_utils::Units::Metric: return MWMUnitsMetric;
|
||||
case measurement_utils::Units::Imperial: return MWMUnitsImperial;
|
||||
}
|
||||
}
|
||||
20
iphone/Maps/Core/Settings/MWMRoutingOptions.h
Normal file
20
iphone/Maps/Core/Settings/MWMRoutingOptions.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NS_SWIFT_NAME(RoutingOptions)
|
||||
@interface MWMRoutingOptions : NSObject
|
||||
|
||||
@property(nonatomic) BOOL avoidToll;
|
||||
@property(nonatomic) BOOL avoidDirty;
|
||||
@property(nonatomic) BOOL avoidPaved;
|
||||
@property(nonatomic) BOOL avoidFerry;
|
||||
@property(nonatomic) BOOL avoidMotorway;
|
||||
@property(nonatomic) BOOL avoidSteps;
|
||||
@property(nonatomic, readonly) BOOL hasOptions;
|
||||
|
||||
- (void)save;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
94
iphone/Maps/Core/Settings/MWMRoutingOptions.mm
Normal file
94
iphone/Maps/Core/Settings/MWMRoutingOptions.mm
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#import "MWMRoutingOptions.h"
|
||||
|
||||
#include "routing/routing_options.hpp"
|
||||
|
||||
@interface MWMRoutingOptions() {
|
||||
routing::RoutingOptions _options;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMRoutingOptions
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_options = routing::RoutingOptions::LoadCarOptionsFromSettings();
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)avoidToll {
|
||||
return _options.Has(routing::RoutingOptions::Road::Toll);
|
||||
}
|
||||
|
||||
- (void)setAvoidToll:(BOOL)avoid {
|
||||
[self setOption:(routing::RoutingOptions::Road::Toll) enabled:avoid];
|
||||
}
|
||||
|
||||
- (BOOL)avoidDirty {
|
||||
return _options.Has(routing::RoutingOptions::Road::Dirty);
|
||||
}
|
||||
|
||||
- (void)setAvoidDirty:(BOOL)avoid {
|
||||
[self setOption:(routing::RoutingOptions::Road::Dirty) enabled:avoid];
|
||||
}
|
||||
|
||||
- (BOOL)avoidPaved {
|
||||
return _options.Has(routing::RoutingOptions::Road::Paved);
|
||||
}
|
||||
|
||||
- (void)setAvoidPaved:(BOOL)avoid {
|
||||
[self setOption:(routing::RoutingOptions::Road::Paved) enabled:avoid];
|
||||
}
|
||||
|
||||
- (BOOL)avoidFerry {
|
||||
return _options.Has(routing::RoutingOptions::Road::Ferry);
|
||||
}
|
||||
|
||||
- (void)setAvoidFerry:(BOOL)avoid {
|
||||
[self setOption:(routing::RoutingOptions::Road::Ferry) enabled:avoid];
|
||||
}
|
||||
|
||||
- (BOOL)avoidMotorway {
|
||||
return _options.Has(routing::RoutingOptions::Road::Motorway);
|
||||
}
|
||||
|
||||
- (void)setAvoidMotorway:(BOOL)avoid {
|
||||
[self setOption:(routing::RoutingOptions::Road::Motorway) enabled:avoid];
|
||||
}
|
||||
|
||||
- (BOOL)avoidSteps {
|
||||
return _options.Has(routing::RoutingOptions::Road::Steps);
|
||||
}
|
||||
|
||||
- (void)setAvoidSteps:(BOOL)avoid {
|
||||
[self setOption:(routing::RoutingOptions::Road::Steps) enabled:avoid];
|
||||
}
|
||||
|
||||
- (BOOL)hasOptions {
|
||||
return self.avoidToll || self.avoidDirty || self.avoidPaved|| self.avoidFerry || self.avoidMotorway || self.avoidSteps;
|
||||
}
|
||||
|
||||
- (void)save {
|
||||
routing::RoutingOptions::SaveCarOptionsToSettings(_options);
|
||||
}
|
||||
|
||||
- (void)setOption:(routing::RoutingOptions::Road)option enabled:(BOOL)enabled {
|
||||
if (enabled) {
|
||||
_options.Add(option);
|
||||
} else {
|
||||
_options.Remove(option);
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
if (![object isMemberOfClass:self.class]) {
|
||||
return NO;
|
||||
}
|
||||
MWMRoutingOptions *another = (MWMRoutingOptions *)object;
|
||||
return another.avoidToll == self.avoidToll && another.avoidDirty == self.avoidDirty && another.avoidPaved == self.avoidPaved && another.avoidFerry == self.avoidFerry && another.avoidMotorway == self.avoidMotorway && another.avoidSteps == self.avoidSteps;
|
||||
}
|
||||
|
||||
@end
|
||||
60
iphone/Maps/Core/Settings/MWMSettings.h
Normal file
60
iphone/Maps/Core/Settings/MWMSettings.h
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
NS_SWIFT_NAME(SettingsBridge)
|
||||
@interface MWMSettings : NSObject
|
||||
|
||||
+ (BOOL)buildings3dViewEnabled;
|
||||
+ (void)setBuildings3dViewEnabled:(BOOL)buildings3dViewEnabled;
|
||||
|
||||
+ (BOOL)perspectiveViewEnabled;
|
||||
+ (void)setPerspectiveViewEnabled:(BOOL)perspectiveViewEnabled;
|
||||
|
||||
+ (BOOL)autoZoomEnabled;
|
||||
+ (void)setAutoZoomEnabled:(BOOL)autoZoomEnabled;
|
||||
|
||||
+ (BOOL)autoDownloadEnabled;
|
||||
+ (void)setAutoDownloadEnabled:(BOOL)autoDownloadEnabled;
|
||||
|
||||
+ (MWMUnits)measurementUnits;
|
||||
+ (void)setMeasurementUnits:(MWMUnits)measurementUnits;
|
||||
|
||||
+ (BOOL)zoomButtonsEnabled;
|
||||
+ (void)setZoomButtonsEnabled:(BOOL)zoomButtonsEnabled;
|
||||
|
||||
+ (BOOL)compassCalibrationEnabled;
|
||||
+ (void)setCompassCalibrationEnabled:(BOOL)compassCalibrationEnabled;
|
||||
|
||||
+ (MWMTheme)theme;
|
||||
+ (void)setTheme:(MWMTheme)theme;
|
||||
|
||||
+ (NSInteger)powerManagement;
|
||||
+ (void)setPowerManagement:(NSInteger)powerManagement;
|
||||
|
||||
+ (BOOL)routingDisclaimerApproved;
|
||||
+ (void)setRoutingDisclaimerApproved;
|
||||
|
||||
+ (NSString *)spotlightLocaleLanguageId;
|
||||
+ (void)setSpotlightLocaleLanguageId:(NSString *)spotlightLocaleLanguageId;
|
||||
|
||||
+ (BOOL)largeFontSize;
|
||||
+ (void)setLargeFontSize:(BOOL)largeFontSize;
|
||||
|
||||
+ (NSDictionary<NSString *, NSString *> *)availableMapLanguages;
|
||||
+ (NSString *)mapLanguageCode;
|
||||
+ (void)setMapLanguageCode:(NSString *)mapLanguageCode;
|
||||
|
||||
+ (BOOL)transliteration;
|
||||
+ (void)setTransliteration:(BOOL)transliteration;
|
||||
|
||||
+ (BOOL)isTrackWarningAlertShown;
|
||||
+ (void)setTrackWarningAlertShown:(BOOL)shown;
|
||||
|
||||
+ (NSString *)donateUrl;
|
||||
|
||||
+ (BOOL)iCLoudSynchronizationEnabled;
|
||||
+ (void)setICLoudSynchronizationEnabled:(BOOL)iCLoudSyncEnabled;
|
||||
|
||||
+ (void)initializeLogging;
|
||||
+ (BOOL)isFileLoggingEnabled;
|
||||
+ (void)setFileLoggingEnabled:(BOOL)fileLoggingEnabled;
|
||||
+ (NSInteger)logFileSize;
|
||||
|
||||
@end
|
||||
296
iphone/Maps/Core/Settings/MWMSettings.mm
Normal file
296
iphone/Maps/Core/Settings/MWMSettings.mm
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
#import "MWMSettings.h"
|
||||
#import "MWMCoreUnits.h"
|
||||
#import "MWMMapViewControlsManager.h"
|
||||
#import "SwiftBridge.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
#include <CoreApi/Logger.h>
|
||||
|
||||
using namespace power_management;
|
||||
|
||||
namespace
|
||||
{
|
||||
char const * kAutoDownloadEnabledKey = "AutoDownloadEnabled";
|
||||
char const * kZoomButtonsEnabledKey = "ZoomButtonsEnabled";
|
||||
char const * kCompassCalibrationEnabledKey = "CompassCalibrationEnabled";
|
||||
char const * kMapLanguageCode = "MapLanguageCode";
|
||||
char const * kRoutingDisclaimerApprovedKey = "IsDisclaimerApproved";
|
||||
|
||||
// TODO(igrechuhin): Remove outdated kUDAutoNightModeOff
|
||||
NSString * const kUDAutoNightModeOff = @"AutoNightModeOff";
|
||||
NSString * const kThemeMode = @"ThemeMode";
|
||||
NSString * const kSpotlightLocaleLanguageId = @"SpotlightLocaleLanguageId";
|
||||
NSString * const kUDTrackWarningAlertWasShown = @"TrackWarningAlertWasShown";
|
||||
NSString * const kiCLoudSynchronizationEnabledKey = @"iCLoudSynchronizationEnabled";
|
||||
NSString * const kUDFileLoggingEnabledKey = @"FileLoggingEnabledKey";
|
||||
} // namespace
|
||||
|
||||
@implementation MWMSettings
|
||||
|
||||
+ (BOOL)buildings3dViewEnabled;
|
||||
{
|
||||
bool _ = true, on = true;
|
||||
GetFramework().Load3dMode(_, on);
|
||||
if (GetFramework().GetPowerManager().GetScheme() == power_management::Scheme::EconomyMaximum) {
|
||||
return false;
|
||||
} else {
|
||||
return on;
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)setBuildings3dViewEnabled:(BOOL)buildings3dViewEnabled;
|
||||
{
|
||||
auto &f = GetFramework();
|
||||
bool _ = true, is3dBuildings = true;
|
||||
f.Load3dMode(_, is3dBuildings);
|
||||
is3dBuildings = static_cast<bool>(buildings3dViewEnabled);
|
||||
f.Save3dMode(_, is3dBuildings);
|
||||
f.Allow3dMode(_, is3dBuildings);
|
||||
}
|
||||
|
||||
+ (BOOL)perspectiveViewEnabled;
|
||||
{
|
||||
bool _ = true, on = true;
|
||||
auto &f = GetFramework();
|
||||
f.Load3dMode(on, _);
|
||||
return on;
|
||||
}
|
||||
|
||||
+ (void)setPerspectiveViewEnabled:(BOOL)perspectiveViewEnabled;
|
||||
{
|
||||
auto &f = GetFramework();
|
||||
bool is3d = true, _ = true;
|
||||
f.Load3dMode(is3d, _);
|
||||
is3d = static_cast<bool>(perspectiveViewEnabled);
|
||||
f.Save3dMode(is3d, _);
|
||||
f.Allow3dMode(is3d, _);
|
||||
}
|
||||
|
||||
+ (BOOL)autoZoomEnabled
|
||||
{
|
||||
return GetFramework().LoadAutoZoom();
|
||||
}
|
||||
|
||||
+ (void)setAutoZoomEnabled:(BOOL)autoZoomEnabled
|
||||
{
|
||||
auto &f = GetFramework();
|
||||
f.AllowAutoZoom(autoZoomEnabled);
|
||||
f.SaveAutoZoom(autoZoomEnabled);
|
||||
}
|
||||
|
||||
+ (BOOL)autoDownloadEnabled
|
||||
{
|
||||
bool autoDownloadEnabled = true;
|
||||
UNUSED_VALUE(settings::Get(kAutoDownloadEnabledKey, autoDownloadEnabled));
|
||||
return autoDownloadEnabled;
|
||||
}
|
||||
|
||||
+ (void)setAutoDownloadEnabled:(BOOL)autoDownloadEnabled
|
||||
{
|
||||
settings::Set(kAutoDownloadEnabledKey, static_cast<bool>(autoDownloadEnabled));
|
||||
}
|
||||
|
||||
+ (MWMUnits)measurementUnits
|
||||
{
|
||||
auto units = measurement_utils::Units::Metric;
|
||||
UNUSED_VALUE(settings::Get(settings::kMeasurementUnits, units));
|
||||
return mwmUnits(units);
|
||||
}
|
||||
|
||||
+ (void)setMeasurementUnits:(MWMUnits)measurementUnits
|
||||
{
|
||||
settings::Set(settings::kMeasurementUnits, coreUnits(measurementUnits));
|
||||
GetFramework().SetupMeasurementSystem();
|
||||
}
|
||||
|
||||
+ (BOOL)zoomButtonsEnabled
|
||||
{
|
||||
bool enabled = true;
|
||||
UNUSED_VALUE(settings::Get(kZoomButtonsEnabledKey, enabled));
|
||||
return enabled;
|
||||
}
|
||||
|
||||
+ (void)setZoomButtonsEnabled:(BOOL)zoomButtonsEnabled
|
||||
{
|
||||
settings::Set(kZoomButtonsEnabledKey, static_cast<bool>(zoomButtonsEnabled));
|
||||
[MWMMapViewControlsManager manager].zoomHidden = !zoomButtonsEnabled;
|
||||
}
|
||||
|
||||
+ (BOOL)compassCalibrationEnabled
|
||||
{
|
||||
bool enabled = true;
|
||||
UNUSED_VALUE(settings::Get(kCompassCalibrationEnabledKey, enabled));
|
||||
return enabled;
|
||||
}
|
||||
|
||||
+ (void)setCompassCalibrationEnabled:(BOOL)compassCalibrationEnabled
|
||||
{
|
||||
settings::Set(kCompassCalibrationEnabledKey, static_cast<bool>(compassCalibrationEnabled));
|
||||
}
|
||||
|
||||
+ (MWMTheme)theme
|
||||
{
|
||||
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;
|
||||
return static_cast<MWMTheme>([ud integerForKey:kThemeMode]);
|
||||
}
|
||||
|
||||
+ (void)setTheme:(MWMTheme)theme
|
||||
{
|
||||
if ([self theme] == theme)
|
||||
return;
|
||||
auto ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setInteger:theme forKey:kThemeMode];
|
||||
BOOL const autoOff = theme != MWMThemeAuto;
|
||||
[ud setBool:autoOff forKey:kUDAutoNightModeOff];
|
||||
[MWMThemeManager invalidate];
|
||||
}
|
||||
|
||||
+ (NSInteger)powerManagement
|
||||
{
|
||||
Scheme scheme = GetFramework().GetPowerManager().GetScheme();
|
||||
if (scheme == Scheme::EconomyMaximum) {
|
||||
return 2;
|
||||
} else if (scheme == Scheme::Auto) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+ (void)setPowerManagement:(NSInteger)powerManagement
|
||||
{
|
||||
Scheme scheme = Scheme::Normal;
|
||||
if (powerManagement == 2) {
|
||||
scheme = Scheme::EconomyMaximum;
|
||||
} else if (powerManagement == 1) {
|
||||
scheme = Scheme::Auto;
|
||||
}
|
||||
GetFramework().GetPowerManager().SetScheme(scheme);
|
||||
}
|
||||
|
||||
+ (BOOL)routingDisclaimerApproved
|
||||
{
|
||||
bool enabled = false;
|
||||
UNUSED_VALUE(settings::Get(kRoutingDisclaimerApprovedKey, enabled));
|
||||
return enabled;
|
||||
}
|
||||
|
||||
+ (void)setRoutingDisclaimerApproved { settings::Set(kRoutingDisclaimerApprovedKey, true); }
|
||||
+ (NSString *)spotlightLocaleLanguageId
|
||||
{
|
||||
return [NSUserDefaults.standardUserDefaults stringForKey:kSpotlightLocaleLanguageId];
|
||||
}
|
||||
|
||||
+ (void)setSpotlightLocaleLanguageId:(NSString *)spotlightLocaleLanguageId
|
||||
{
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setObject:spotlightLocaleLanguageId forKey:kSpotlightLocaleLanguageId];
|
||||
}
|
||||
|
||||
+ (BOOL)largeFontSize { return GetFramework().LoadLargeFontsSize(); }
|
||||
+ (void)setLargeFontSize:(BOOL)largeFontSize
|
||||
{
|
||||
GetFramework().SetLargeFontsSize(static_cast<bool>(largeFontSize));
|
||||
}
|
||||
|
||||
+ (NSDictionary<NSString *, NSString *> *)availableMapLanguages;
|
||||
{
|
||||
NSMutableDictionary<NSString *, NSString *> *availableLanguages = [[NSMutableDictionary alloc] init];
|
||||
auto const & v = StringUtf8Multilang::GetSupportedLanguages(false);
|
||||
for (auto i: v) {
|
||||
[availableLanguages setObject:@(std::string(i.m_name).c_str()) forKey:@(std::string(i.m_code).c_str())];
|
||||
}
|
||||
return availableLanguages;
|
||||
}
|
||||
|
||||
+ (NSString *)mapLanguageCode;
|
||||
{
|
||||
std::string mapLanguageCode;
|
||||
bool hasMapLanguageCode = settings::Get(kMapLanguageCode, mapLanguageCode);
|
||||
if (hasMapLanguageCode) {
|
||||
return @(mapLanguageCode.c_str());
|
||||
}
|
||||
|
||||
return @"auto";
|
||||
}
|
||||
|
||||
+ (void)setMapLanguageCode:(NSString *)mapLanguageCode;
|
||||
{
|
||||
auto &f = GetFramework();
|
||||
if ([mapLanguageCode isEqual: @"auto"]) {
|
||||
f.ResetMapLanguageCode();
|
||||
} else {
|
||||
f.SetMapLanguageCode(std::string([mapLanguageCode UTF8String]));
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)transliteration { return GetFramework().LoadTransliteration(); }
|
||||
+ (void)setTransliteration:(BOOL)transliteration
|
||||
{
|
||||
bool const isTransliteration = static_cast<bool>(transliteration);
|
||||
auto & f = GetFramework();
|
||||
f.SaveTransliteration(isTransliteration);
|
||||
f.AllowTransliteration(isTransliteration);
|
||||
}
|
||||
|
||||
+ (BOOL)isTrackWarningAlertShown
|
||||
{
|
||||
return [NSUserDefaults.standardUserDefaults boolForKey:kUDTrackWarningAlertWasShown];
|
||||
}
|
||||
|
||||
+ (void)setTrackWarningAlertShown:(BOOL)shown
|
||||
{
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setBool:shown forKey:kUDTrackWarningAlertWasShown];
|
||||
}
|
||||
|
||||
+ (NSString *)donateUrl
|
||||
{
|
||||
std::string url;
|
||||
return settings::Get(settings::kDonateUrl, url) ? @(url.c_str()) : nil;
|
||||
}
|
||||
|
||||
+ (BOOL)iCLoudSynchronizationEnabled
|
||||
{
|
||||
return [NSUserDefaults.standardUserDefaults boolForKey:kiCLoudSynchronizationEnabledKey];
|
||||
}
|
||||
|
||||
+ (void)setICLoudSynchronizationEnabled:(BOOL)iCLoudSyncEnabled
|
||||
{
|
||||
[NSUserDefaults.standardUserDefaults setBool:iCLoudSyncEnabled forKey:kiCLoudSynchronizationEnabledKey];
|
||||
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.iCloudSynchronizationDidChangeEnabledState object:nil];
|
||||
}
|
||||
|
||||
+ (void)initializeLogging {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
[self setFileLoggingEnabled:[self isFileLoggingEnabled]];
|
||||
});
|
||||
}
|
||||
|
||||
+ (BOOL)isFileLoggingEnabled {
|
||||
return [NSUserDefaults.standardUserDefaults boolForKey:kUDFileLoggingEnabledKey];
|
||||
}
|
||||
|
||||
+ (void)setFileLoggingEnabled:(BOOL)fileLoggingEnabled {
|
||||
[NSUserDefaults.standardUserDefaults setBool:fileLoggingEnabled forKey:kUDFileLoggingEnabledKey];
|
||||
[Logger setFileLoggingEnabled:fileLoggingEnabled];
|
||||
}
|
||||
|
||||
+ (NSInteger)logFileSize
|
||||
{
|
||||
uint64_t logFileSize = [Logger getLogFileSize];
|
||||
return logFileSize;
|
||||
}
|
||||
|
||||
@end
|
||||
16
iphone/Maps/Core/Storage/MWMStorage+UI.h
Normal file
16
iphone/Maps/Core/Storage/MWMStorage+UI.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#import <CoreApi/MWMStorage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MWMStorage (UI)
|
||||
|
||||
- (void)downloadNode:(NSString *)countryId;
|
||||
- (void)downloadNode:(NSString *)countryId onSuccess:(nullable MWMVoidBlock)success;
|
||||
- (void)updateNode:(NSString *)countryId;
|
||||
- (void)updateNode:(NSString *)countryId onCancel:(nullable MWMVoidBlock)cancel;
|
||||
- (void)deleteNode:(NSString *)countryId;
|
||||
- (void)downloadNodes:(NSArray<NSString *> *)countryIds onSuccess:(nullable MWMVoidBlock)success;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
109
iphone/Maps/Core/Storage/MWMStorage+UI.m
Normal file
109
iphone/Maps/Core/Storage/MWMStorage+UI.m
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#import "MWMStorage+UI.h"
|
||||
#import "MWMAlertViewController.h"
|
||||
|
||||
@implementation MWMStorage (UI)
|
||||
|
||||
- (void)handleError:(NSError *)error {
|
||||
if (error.code == kStorageNotEnoughSpace) {
|
||||
[[MWMAlertViewController activeAlertController] presentNotEnoughSpaceAlert];
|
||||
} else if (error.code == kStorageNoConnection) {
|
||||
[[MWMAlertViewController activeAlertController] presentNoConnectionAlert];
|
||||
} else if (error.code == kStorageRoutingActive) {
|
||||
[[MWMAlertViewController activeAlertController] presentDeleteMapProhibitedAlert];
|
||||
} else {
|
||||
NSAssert(NO, @"Unknown error code");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)downloadNode:(NSString *)countryId {
|
||||
[self downloadNode:countryId onSuccess:nil];
|
||||
}
|
||||
|
||||
- (void)downloadNode:(NSString *)countryId onSuccess:(MWMVoidBlock)success {
|
||||
NSError *error;
|
||||
[self downloadNode:countryId error:&error];
|
||||
if (error) {
|
||||
if (error.code == kStorageCellularForbidden) {
|
||||
__weak __typeof(self) ws = self;
|
||||
[[MWMAlertViewController activeAlertController] presentNoWiFiAlertWithOkBlock:^{
|
||||
[self enableCellularDownload:YES];
|
||||
[ws downloadNode:countryId];
|
||||
} andCancelBlock:nil];
|
||||
} else {
|
||||
[self handleError:error];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (success) {
|
||||
success();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateNode:(NSString *)countryId {
|
||||
[self updateNode:countryId onCancel:nil];
|
||||
}
|
||||
|
||||
- (void)updateNode:(NSString *)countryId onCancel:(MWMVoidBlock)cancel {
|
||||
NSError *error;
|
||||
[self updateNode:countryId error:&error];
|
||||
if (error) {
|
||||
if (error.code == kStorageCellularForbidden) {
|
||||
__weak __typeof(self) ws = self;
|
||||
[[MWMAlertViewController activeAlertController] presentNoWiFiAlertWithOkBlock:^{
|
||||
[self enableCellularDownload:YES];
|
||||
[ws updateNode:countryId onCancel:cancel];
|
||||
} andCancelBlock:cancel];
|
||||
} else {
|
||||
[self handleError:error];
|
||||
if (cancel) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)deleteNode:(NSString *)countryId {
|
||||
[self deleteNode:countryId ignoreUnsavedEdits:NO];
|
||||
}
|
||||
|
||||
- (void)deleteNode:(NSString *)countryId ignoreUnsavedEdits:(BOOL)force {
|
||||
NSError *error;
|
||||
[self deleteNode:countryId ignoreUnsavedEdits:force error:&error];
|
||||
if (error) {
|
||||
__weak __typeof(self) ws = self;
|
||||
if (error.code == kStorageCellularForbidden) {
|
||||
[[MWMAlertViewController activeAlertController] presentNoWiFiAlertWithOkBlock:^{
|
||||
[self enableCellularDownload:YES];
|
||||
[ws deleteNode:countryId];
|
||||
} andCancelBlock:nil];
|
||||
} else if (error.code == kStorageHaveUnsavedEdits) {
|
||||
[[MWMAlertViewController activeAlertController] presentUnsavedEditsAlertWithOkBlock:^ {
|
||||
[ws deleteNode:countryId ignoreUnsavedEdits:YES];
|
||||
}];
|
||||
} else {
|
||||
[self handleError:error];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)downloadNodes:(NSArray<NSString *> *)countryIds onSuccess:(nullable MWMVoidBlock)success {
|
||||
NSError *error;
|
||||
[self downloadNodes:countryIds error:&error];
|
||||
if (error) {
|
||||
if (error.code == kStorageCellularForbidden) {
|
||||
__weak __typeof(self) ws = self;
|
||||
[[MWMAlertViewController activeAlertController] presentNoWiFiAlertWithOkBlock:^{
|
||||
[self enableCellularDownload:YES];
|
||||
[ws downloadNodes:countryIds onSuccess:success];
|
||||
} andCancelBlock:nil];
|
||||
} else {
|
||||
[self handleError:error];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (success) {
|
||||
success();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
19
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech+CPP.h
Normal file
19
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech+CPP.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#import "MWMTextToSpeech.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@interface MWMTextToSpeech (CPP)
|
||||
|
||||
// Returns a list of available languages in the following format:
|
||||
// * name in bcp47;
|
||||
// * localized name;
|
||||
- (std::vector<std::pair<std::string, std::string>>)availableLanguages;
|
||||
- (std::pair<std::string, std::string>)standardLanguage;
|
||||
|
||||
@end
|
||||
|
||||
namespace tts
|
||||
{
|
||||
std::string translateLocale(std::string const & localeString);
|
||||
} // namespace tts
|
||||
37
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech.h
Normal file
37
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#import "MWMTextToSpeechObserver.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface MWMTextToSpeech : NSObject
|
||||
|
||||
+ (MWMTextToSpeech *)tts;
|
||||
- (AVSpeechSynthesisVoice *)voice;
|
||||
+ (BOOL)isTTSEnabled;
|
||||
+ (void)setTTSEnabled:(BOOL)enabled;
|
||||
+ (BOOL)isStreetNamesTTSEnabled;
|
||||
+ (void)setStreetNamesTTSEnabled:(BOOL)enabled;
|
||||
+ (NSDictionary<NSString *, NSString *> *)availableLanguages;
|
||||
+ (NSString *)selectedLanguage;
|
||||
+ (NSString *)savedLanguage;
|
||||
+ (NSInteger)speedCameraMode;
|
||||
+ (void)setSpeedCameraMode:(NSInteger)speedCameraMode;
|
||||
+ (void)playTest;
|
||||
|
||||
+ (void)addObserver:(id<MWMTextToSpeechObserver>)observer;
|
||||
+ (void)removeObserver:(id<MWMTextToSpeechObserver>)observer;
|
||||
|
||||
+ (void)applicationDidBecomeActive;
|
||||
|
||||
@property(nonatomic) BOOL active;
|
||||
- (void)setNotificationsLocale:(NSString *)locale;
|
||||
- (void)playTurnNotifications:(NSArray<NSString *> *)turnNotifications;
|
||||
- (void)playWarningSound;
|
||||
- (void)play:(NSString *)text;
|
||||
|
||||
- (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")));
|
||||
|
||||
@end
|
||||
392
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech.mm
Normal file
392
iphone/Maps/Core/TextToSpeech/MWMTextToSpeech.mm
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
#import <AVFoundation/AVFoundation.h>
|
||||
#import "MWMRouter.h"
|
||||
#import "MWMTextToSpeech+CPP.h"
|
||||
#import "SwiftBridge.h"
|
||||
#import "TTSTester.h"
|
||||
|
||||
#include "LocaleTranslator.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
#include "platform/languages.hpp"
|
||||
|
||||
using namespace locale_translator;
|
||||
using namespace routing;
|
||||
|
||||
namespace
|
||||
{
|
||||
NSString * const kUserDefaultsTTSLanguageBcp47 = @"UserDefaultsTTSLanguageBcp47";
|
||||
NSString * const kIsTTSEnabled = @"UserDefaultsNeedToEnableTTS";
|
||||
NSString * const kIsStreetNamesTTSEnabled = @"UserDefaultsNeedToEnableStreetNamesTTS";
|
||||
NSString * const kDefaultLanguage = @"en-US";
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> availableLanguages()
|
||||
{
|
||||
NSArray<AVSpeechSynthesisVoice *> * voices = [AVSpeechSynthesisVoice speechVoices];
|
||||
std::vector<std::pair<std::string, std::string>> native;
|
||||
for (AVSpeechSynthesisVoice * v in voices)
|
||||
native.emplace_back(make_pair(bcp47ToTwineLanguage(v.language), v.language.UTF8String));
|
||||
|
||||
using namespace routing::turns::sound;
|
||||
std::vector<std::pair<std::string, std::string>> result;
|
||||
for (auto const & [twineRouting, _] : kLanguageList)
|
||||
{
|
||||
for (auto const & [twineVoice, bcp47Voice] : native)
|
||||
{
|
||||
if (twineVoice == twineRouting)
|
||||
{
|
||||
auto pair = std::make_pair(bcp47Voice, tts::translateLocale(bcp47Voice));
|
||||
if (std::find(result.begin(), result.end(), pair) == result.end())
|
||||
result.emplace_back(std::move(pair));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
using Observer = id<MWMTextToSpeechObserver>;
|
||||
using Observers = NSHashTable<Observer>;
|
||||
} // namespace
|
||||
|
||||
@interface MWMTextToSpeech ()<AVSpeechSynthesizerDelegate>
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> _availableLanguages;
|
||||
}
|
||||
|
||||
@property(nonatomic) AVSpeechSynthesizer * speechSynthesizer;
|
||||
@property(nonatomic) AVSpeechSynthesisVoice * speechVoice;
|
||||
@property(nonatomic) AVAudioPlayer * audioPlayer;
|
||||
|
||||
@property(nonatomic) Observers * observers;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMTextToSpeech
|
||||
|
||||
+ (MWMTextToSpeech *)tts {
|
||||
static dispatch_once_t onceToken;
|
||||
static MWMTextToSpeech * tts = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
tts = [[self alloc] initTTS];
|
||||
});
|
||||
return tts;
|
||||
}
|
||||
|
||||
+ (void)applicationDidBecomeActive {
|
||||
auto tts = [self tts];
|
||||
tts.speechSynthesizer = nil;
|
||||
tts.speechVoice = nil;
|
||||
}
|
||||
|
||||
- (instancetype)initTTS {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_availableLanguages = availableLanguages();
|
||||
_observers = [Observers weakObjectsHashTable];
|
||||
|
||||
NSString * saved = [[self class] savedLanguage];
|
||||
NSString * preferedLanguageBcp47;
|
||||
if (saved.length)
|
||||
preferedLanguageBcp47 = saved;
|
||||
else
|
||||
preferedLanguageBcp47 = [AVSpeechSynthesisVoice currentLanguageCode];
|
||||
|
||||
std::pair<std::string, std::string> const lan =
|
||||
std::make_pair(preferedLanguageBcp47.UTF8String,
|
||||
tts::translateLocale(preferedLanguageBcp47.UTF8String));
|
||||
|
||||
if (find(_availableLanguages.begin(), _availableLanguages.end(), lan) !=
|
||||
_availableLanguages.end())
|
||||
[self setNotificationsLocale:preferedLanguageBcp47];
|
||||
else
|
||||
[self setNotificationsLocale:kDefaultLanguage];
|
||||
|
||||
NSError * err = nil;
|
||||
if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
|
||||
mode:AVAudioSessionModeVoicePrompt
|
||||
options:AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers |
|
||||
AVAudioSessionCategoryOptionDuckOthers
|
||||
error:&err]) {
|
||||
LOG(LWARNING, ("Couldn't configure audio session: ", [err localizedDescription]));
|
||||
}
|
||||
|
||||
// Set initial StreetNamesTTS setting
|
||||
NSDictionary *dictionary = @{ kIsStreetNamesTTSEnabled : @NO };
|
||||
[NSUserDefaults.standardUserDefaults registerDefaults:dictionary];
|
||||
|
||||
self.active = YES;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[AVAudioSession sharedInstance] setActive:NO
|
||||
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
|
||||
error:nil];
|
||||
self.speechSynthesizer.delegate = nil;
|
||||
}
|
||||
- (std::vector<std::pair<std::string, std::string>>)availableLanguages { return _availableLanguages; }
|
||||
- (std::pair<std::string, std::string>)standardLanguage {
|
||||
return std::make_pair(kDefaultLanguage.UTF8String, tts::translateLocale(kDefaultLanguage.UTF8String));
|
||||
}
|
||||
- (void)setNotificationsLocale:(NSString *)locale {
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setObject:locale forKey:kUserDefaultsTTSLanguageBcp47];
|
||||
[self createVoice:locale];
|
||||
}
|
||||
- (AVSpeechSynthesisVoice *)voice {
|
||||
[self createVoice:[[self class] savedLanguage]];
|
||||
return self.speechVoice;
|
||||
}
|
||||
- (BOOL)isValid { return _speechSynthesizer != nil && _speechVoice != nil; }
|
||||
+ (BOOL)isTTSEnabled { return [NSUserDefaults.standardUserDefaults boolForKey:kIsTTSEnabled]; }
|
||||
+ (void)setTTSEnabled:(BOOL)enabled {
|
||||
if ([self isTTSEnabled] == enabled)
|
||||
return;
|
||||
auto tts = [self tts];
|
||||
if (!enabled)
|
||||
[tts setActive:NO];
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setBool:enabled forKey:kIsTTSEnabled];
|
||||
|
||||
[tts onTTSStatusUpdated];
|
||||
if (enabled)
|
||||
[tts setActive:YES];
|
||||
}
|
||||
+ (BOOL)isStreetNamesTTSEnabled { return [NSUserDefaults.standardUserDefaults boolForKey:kIsStreetNamesTTSEnabled]; }
|
||||
+ (void)setStreetNamesTTSEnabled:(BOOL)enabled {
|
||||
if ([self isStreetNamesTTSEnabled] == enabled)
|
||||
return;
|
||||
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setBool:enabled forKey:kIsStreetNamesTTSEnabled];
|
||||
[ud synchronize];
|
||||
}
|
||||
|
||||
- (void)setActive:(BOOL)active {
|
||||
if (![[self class] isTTSEnabled] || self.active == active)
|
||||
return;
|
||||
if (active && ![self isValid])
|
||||
[self createVoice:[[self class] savedLanguage]];
|
||||
[MWMRouter enableTurnNotifications:active];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self onTTSStatusUpdated];
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)active { return [[self class] isTTSEnabled] && [MWMRouter areTurnNotificationsEnabled]; }
|
||||
|
||||
+ (NSDictionary<NSString *, NSString *> *)availableLanguages
|
||||
{
|
||||
NSMutableDictionary<NSString *, NSString *> *availableLanguages = [[NSMutableDictionary alloc] init];
|
||||
auto const & v = [[self tts] availableLanguages];
|
||||
for (auto i: v) {
|
||||
[availableLanguages setObject:@(i.second.c_str()) forKey:@(i.first.c_str())];
|
||||
}
|
||||
return availableLanguages;
|
||||
}
|
||||
|
||||
+ (NSString *)selectedLanguage {
|
||||
if ([self savedLanguage] != nil) {
|
||||
return [self savedLanguage];
|
||||
}
|
||||
|
||||
NSString * preferedLanguageBcp47 = [AVSpeechSynthesisVoice currentLanguageCode];
|
||||
|
||||
std::pair<std::string, std::string> const lan =
|
||||
std::make_pair(preferedLanguageBcp47.UTF8String, tts::translateLocale(preferedLanguageBcp47.UTF8String));
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> const availableLanguages = [[self tts] availableLanguages];
|
||||
if (find(availableLanguages.begin(), availableLanguages.end(), lan) !=
|
||||
availableLanguages.end()) {
|
||||
return preferedLanguageBcp47;
|
||||
}
|
||||
|
||||
return kDefaultLanguage;
|
||||
}
|
||||
|
||||
+ (NSString *)savedLanguage {
|
||||
return [NSUserDefaults.standardUserDefaults stringForKey:kUserDefaultsTTSLanguageBcp47];
|
||||
}
|
||||
|
||||
+ (NSInteger)speedCameraMode
|
||||
{
|
||||
SpeedCameraManagerMode mode = GetFramework().GetRoutingManager().GetSpeedCamManager().GetMode();
|
||||
if (mode == SpeedCameraManagerMode::Auto) {
|
||||
return 2;
|
||||
} else if (mode == SpeedCameraManagerMode::Always) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+ (void)setSpeedCameraMode:(NSInteger)speedCameraMode
|
||||
{
|
||||
SpeedCameraManagerMode mode = SpeedCameraManagerMode::Never;
|
||||
if (speedCameraMode == 2) {
|
||||
mode = SpeedCameraManagerMode::Auto;
|
||||
} else if (speedCameraMode == 1) {
|
||||
mode = SpeedCameraManagerMode::Always;
|
||||
}
|
||||
GetFramework().GetRoutingManager().GetSpeedCamManager().SetMode(mode);
|
||||
}
|
||||
|
||||
- (void)createVoice:(NSString *)locale {
|
||||
if (!self.speechSynthesizer) {
|
||||
self.speechSynthesizer = [[AVSpeechSynthesizer alloc] init];
|
||||
self.speechSynthesizer.delegate = self;
|
||||
}
|
||||
|
||||
NSMutableArray<NSString *> * candidateLocales = [@[ kDefaultLanguage, @"en-GB" ] mutableCopy];
|
||||
|
||||
if (locale)
|
||||
[candidateLocales insertObject:locale atIndex:0];
|
||||
else
|
||||
LOG(LWARNING, ("locale is nil. Trying default locale."));
|
||||
|
||||
AVSpeechSynthesisVoice * voice = nil;
|
||||
for (NSString * loc in candidateLocales) {
|
||||
if (@available(iOS 16.0, *)) {
|
||||
for (AVSpeechSynthesisVoice * aVoice in [AVSpeechSynthesisVoice speechVoices]) {
|
||||
if (voice == nil && aVoice.language == loc && aVoice.quality == AVSpeechSynthesisVoiceQualityPremium) {
|
||||
voice = aVoice;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (AVSpeechSynthesisVoice * aVoice in [AVSpeechSynthesisVoice speechVoices]) {
|
||||
if (voice == nil && aVoice.language == loc && aVoice.quality == AVSpeechSynthesisVoiceQualityEnhanced) {
|
||||
voice = aVoice;
|
||||
}
|
||||
}
|
||||
if (voice == nil) {
|
||||
voice = [AVSpeechSynthesisVoice voiceWithLanguage:loc];
|
||||
}
|
||||
if (voice) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.speechVoice = voice;
|
||||
if (voice) {
|
||||
std::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 {
|
||||
LOG(LWARNING,
|
||||
("The UI language and English are not available for TTS. MWMTextToSpeech is invalid."));
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)playTest
|
||||
{
|
||||
TTSTester * ttsTester = [[TTSTester alloc] init];
|
||||
[ttsTester playRandomTestString];
|
||||
}
|
||||
|
||||
|
||||
- (void)speakOneString:(NSString *)textToSpeak {
|
||||
AVSpeechUtterance * utterance = [AVSpeechUtterance speechUtteranceWithString:textToSpeak];
|
||||
utterance.voice = self.speechVoice;
|
||||
utterance.rate = AVSpeechUtteranceDefaultSpeechRate;
|
||||
[self.speechSynthesizer speakUtterance:utterance];
|
||||
}
|
||||
|
||||
- (void)playTurnNotifications:(NSArray<NSString *> *)turnNotifications {
|
||||
auto stopSession = ^{
|
||||
if (self.speechSynthesizer.isSpeaking)
|
||||
return;
|
||||
[[AVAudioSession sharedInstance]
|
||||
setActive:NO
|
||||
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
|
||||
error:nil];
|
||||
};
|
||||
|
||||
if (![MWMRouter isOnRoute] || !self.active) {
|
||||
stopSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (![self isValid])
|
||||
[self createVoice:[[self class] savedLanguage]];
|
||||
|
||||
if (![self isValid]) {
|
||||
stopSession();
|
||||
return;
|
||||
}
|
||||
|
||||
if (turnNotifications.count == 0) {
|
||||
stopSession();
|
||||
return;
|
||||
} else {
|
||||
NSError * err = nil;
|
||||
if (![[AVAudioSession sharedInstance] setActive:YES error:&err]) {
|
||||
LOG(LWARNING, ("Couldn't activate audio session: ", [err localizedDescription]));
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString * notification in turnNotifications)
|
||||
[self speakOneString:notification];
|
||||
}
|
||||
}
|
||||
|
||||
- (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"]) {
|
||||
NSError * error = nil;
|
||||
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
|
||||
CHECK(!error, (error.localizedDescription.UTF8String));
|
||||
} else {
|
||||
CHECK(false, ("Speed warning file not found"));
|
||||
}
|
||||
}
|
||||
|
||||
return _audioPlayer;
|
||||
}
|
||||
|
||||
- (void)play:(NSString *)text {
|
||||
if (![self isValid])
|
||||
[self createVoice:[[self class] savedLanguage]];
|
||||
|
||||
[self speakOneString:text];
|
||||
}
|
||||
|
||||
#pragma mark - MWMNavigationDashboardObserver
|
||||
|
||||
- (void)onTTSStatusUpdated {
|
||||
for (Observer observer in self.observers)
|
||||
[observer onTTSStatusUpdated];
|
||||
}
|
||||
|
||||
#pragma mark - Add/Remove Observers
|
||||
|
||||
+ (void)addObserver:(id<MWMTextToSpeechObserver>)observer {
|
||||
[[self tts].observers addObject:observer];
|
||||
}
|
||||
|
||||
+ (void)removeObserver:(id<MWMTextToSpeechObserver>)observer {
|
||||
[[self tts].observers removeObject:observer];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace tts
|
||||
{
|
||||
std::string translateLocale(std::string const & localeString)
|
||||
{
|
||||
NSString * nsLocaleString = [NSString stringWithUTF8String: localeString.c_str()];
|
||||
NSLocale * locale = [[NSLocale alloc] initWithLocaleIdentifier: nsLocaleString];
|
||||
NSString * localizedName = [locale localizedStringForLocaleIdentifier:nsLocaleString];
|
||||
localizedName = [localizedName capitalizedString];
|
||||
return std::string(localizedName.UTF8String);
|
||||
}
|
||||
} // namespace tts
|
||||
5
iphone/Maps/Core/TextToSpeech/MWMTextToSpeechObserver.h
Normal file
5
iphone/Maps/Core/TextToSpeech/MWMTextToSpeechObserver.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@protocol MWMTextToSpeechObserver<NSObject>
|
||||
|
||||
- (void)onTTSStatusUpdated;
|
||||
|
||||
@end
|
||||
10
iphone/Maps/Core/TextToSpeech/TTSTester.h
Normal file
10
iphone/Maps/Core/TextToSpeech/TTSTester.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TTSTester : NSObject
|
||||
|
||||
- (void)playRandomTestString;
|
||||
- (NSArray<NSString *> *)getTestStrings:(NSString *)language;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
59
iphone/Maps/Core/TextToSpeech/TTSTester.mm
Normal file
59
iphone/Maps/Core/TextToSpeech/TTSTester.mm
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#import "TTSTester.h"
|
||||
|
||||
#include "LocaleTranslator.h"
|
||||
#include "MWMTextToSpeech.h"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
@implementation TTSTester
|
||||
|
||||
static NSString * const NotFoundDelimiter = @"__not_found__";
|
||||
|
||||
NSArray<NSString *> * testStrings;
|
||||
NSString * testStringsLanguage;
|
||||
|
||||
int testStringIndex;
|
||||
|
||||
- (void)playRandomTestString {
|
||||
NSString * currentTTSLanguage = MWMTextToSpeech.savedLanguage;
|
||||
if (testStrings == nil || ![currentTTSLanguage isEqualToString:testStringsLanguage]) {
|
||||
testStrings = [self getTestStrings:currentTTSLanguage];
|
||||
if (testStrings == nil) {
|
||||
LOG(LWARNING, ("Couldn't load TTS test strings"));
|
||||
return;
|
||||
}
|
||||
testStringsLanguage = currentTTSLanguage;
|
||||
}
|
||||
|
||||
[[MWMTextToSpeech tts] play:testStrings[testStringIndex]];
|
||||
|
||||
if (++testStringIndex >= testStrings.count)
|
||||
testStringIndex = 0;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)getTestStrings:(NSString *)language {
|
||||
NSString * twineLanguage = [NSString stringWithUTF8String:locale_translator::bcp47ToTwineLanguage(language).c_str()];
|
||||
NSString * languagePath = [NSBundle.mainBundle pathForResource:twineLanguage ofType:@"lproj"];
|
||||
if (languagePath == nil) {
|
||||
LOG(LWARNING, ("Couldn't find translation file for ", twineLanguage.UTF8String));
|
||||
return nil;
|
||||
}
|
||||
NSBundle * bundle = [NSBundle bundleWithPath:languagePath];
|
||||
|
||||
NSMutableArray * appTips = [NSMutableArray new];
|
||||
for (int idx = 0; ; idx++) {
|
||||
NSString * appTipKey = [NSString stringWithFormat:@"app_tip_%02d", idx];
|
||||
NSString * appTip = [bundle localizedStringForKey:appTipKey value:NotFoundDelimiter table:nil];
|
||||
if ([appTip isEqualToString:NotFoundDelimiter])
|
||||
break;
|
||||
[appTips addObject:appTip];
|
||||
}
|
||||
|
||||
// shuffle
|
||||
for (NSUInteger i = appTips.count; i > 1; i--)
|
||||
[appTips exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
|
||||
|
||||
return appTips;
|
||||
}
|
||||
|
||||
@end
|
||||
34
iphone/Maps/Core/Theme/BookmarksStyleSheet.swift
Normal file
34
iphone/Maps/Core/Theme/BookmarksStyleSheet.swift
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
enum BookmarksStyleSheet: String, CaseIterable {
|
||||
case bookmarksCategoryTextView = "BookmarksCategoryTextView"
|
||||
case bookmarksCategoryDeleteButton = "BookmarksCategoryDeleteButton"
|
||||
case bookmarksActionCreateIcon = "BookmarksActionCreateIcon"
|
||||
case bookmarkSharingLicense = "BookmarkSharingLicense"
|
||||
}
|
||||
|
||||
extension BookmarksStyleSheet: IStyleSheet {
|
||||
func styleResolverFor(colors: IColors, fonts: IFonts) -> Theme.StyleResolver {
|
||||
switch self {
|
||||
case .bookmarksCategoryTextView:
|
||||
return .add { s in
|
||||
s.font = fonts.regular16
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
s.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
}
|
||||
case .bookmarksCategoryDeleteButton:
|
||||
return .add { s in
|
||||
s.font = fonts.regular17
|
||||
s.fontColor = colors.red
|
||||
s.fontColorDisabled = colors.blackHintText
|
||||
}
|
||||
case .bookmarksActionCreateIcon:
|
||||
return .add { s in
|
||||
s.tintColor = colors.linkBlue
|
||||
}
|
||||
case .bookmarkSharingLicense:
|
||||
return .addFrom(GlobalStyleSheet.termsOfUseLinkText) { s in
|
||||
s.fontColor = colors.blackSecondaryText
|
||||
s.font = fonts.regular14
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
iphone/Maps/Core/Theme/Colors.swift
Normal file
125
iphone/Maps/Core/Theme/Colors.swift
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
class DayColors: IColors {
|
||||
var clear = UIColor.clear
|
||||
var primaryDark = UIColor(24, 128, 68, alpha100)
|
||||
var primary = UIColor.accent
|
||||
var secondary = UIColor(55, 101, 63, alpha100)
|
||||
// Light green color
|
||||
var primaryLight = UIColor(124, 188, 123, alpha100)
|
||||
var menuBackground = UIColor(255, 255, 255, alpha90)
|
||||
var tabBarButtonBackground = UIColor(255, 255, 255, alpha70)
|
||||
var downloadBadgeBackground = UIColor(255, 55, 35, alpha100)
|
||||
// Background color && press color
|
||||
var pressBackground = UIColor(245, 245, 245, alpha100)
|
||||
// Red color (use for status closed in place page)
|
||||
var red = UIColor(230, 15, 35, alpha100)
|
||||
var errorPink = UIColor(246, 60, 51, alpha12)
|
||||
// Orange color (use for status 15 min in place page)
|
||||
var orange = UIColor(255, 120, 5, alpha100)
|
||||
// Blue color (use for links and phone numbers)
|
||||
var linkBlue = UIColor.accent
|
||||
var linkBlueHighlighted = UIColor.accent
|
||||
var linkBlueDark = UIColor.accent
|
||||
var buttonRed = UIColor(244, 67, 67, alpha100)
|
||||
var buttonRedHighlighted = UIColor(183, 28, 28, alpha100)
|
||||
var black = UIColor(0, 0, 0, alpha100)
|
||||
var blackPrimaryText = UIColor(0, 0, 0, alpha87)
|
||||
var blackSecondaryText = UIColor(0, 0, 0, alpha54)
|
||||
var blackHintText = UIColor(0, 0, 0, alpha26)
|
||||
var blackDividers = UIColor(0, 0, 0, alpha08)
|
||||
var solidDividers = UIColor(224, 224, 224, alpha100)
|
||||
var white = UIColor(255, 255, 255, alpha100)
|
||||
var whitePrimaryText = UIColor(255, 255, 255, alpha87);
|
||||
var whitePrimaryTextHighlighted = UIColor(255, 255, 255, alpha30);
|
||||
var whiteSecondaryText = UIColor(255, 255, 255, alpha54)
|
||||
var whiteHintText = UIColor(255, 255, 255, alpha30)
|
||||
var buttonDisabledBlueText = UIColor(3, 122, 255, alpha26)
|
||||
var alertBackground = UIColor(255, 255, 255, alpha90)
|
||||
var blackOpaque = UIColor(0, 0, 0, alpha04)
|
||||
var toastBackground = UIColor(255, 255, 255, alpha87)
|
||||
var statusBarBackground = UIColor(255, 255, 255, alpha36)
|
||||
var searchPromoBackground = UIColor(249, 251, 231, alpha100)
|
||||
var border = UIColor(0, 0, 0, alpha04)
|
||||
var bookingBackground = UIColor(25, 69, 125, alpha100)
|
||||
var opentableBackground = UIColor(218, 55, 67, alpha100)
|
||||
var transparentGreen = UIColor(233, 244, 233, alpha26)
|
||||
var ratingRed = UIColor(229, 57, 53, alpha100)
|
||||
var ratingOrange = UIColor(244, 81, 30, alpha100)
|
||||
var ratingYellow = UIColor(245, 176, 39, alpha100)
|
||||
var ratingLightGreen = UIColor(124, 179, 66, alpha100)
|
||||
var ratingGreen = UIColor(67, 160, 71, alpha100)
|
||||
var fadeBackground = UIColor(0, 0, 0, alpha80)
|
||||
var blackStatusBarBackground = UIColor(0, 0, 0, alpha80)
|
||||
var elevationPreviewTint = UIColor(193, 209, 224, alpha30)
|
||||
var elevationPreviewSelector = UIColor(red: 0.757, green: 0.82, blue: 0.878, alpha: 1)
|
||||
var shadow = UIColor(0, 0, 0, alpha100)
|
||||
var chartLine = UIColor(red: 0.118, green: 0.588, blue: 0.941, alpha: 1)
|
||||
var chartShadow = UIColor(red: 0.118, green: 0.588, blue: 0.941, alpha: 0.12)
|
||||
var cityColor = UIColor(red: 0.4, green: 0.225, blue: 0.75, alpha: 1)
|
||||
var outdoorColor = UIColor(red: 0.235, green: 0.549, blue: 0.235, alpha: 1)
|
||||
var carplayPlaceholderBackground = UIColor(221, 221, 205, alpha100)
|
||||
var iconOpaqueGrayTint = UIColor(117, 117, 117, alpha100)
|
||||
var iconOpaqueGrayBackground = UIColor(231, 231, 231, alpha100)
|
||||
}
|
||||
|
||||
class NightColors: IColors {
|
||||
var clear = UIColor.clear
|
||||
var primaryDark = UIColor(25, 30, 35, alpha100)
|
||||
var primary = UIColor.accent
|
||||
var secondary = UIColor(0x25, 0x28, 0x2b, alpha100)
|
||||
// Light green color
|
||||
var primaryLight = UIColor(65, 70, 75, alpha100)
|
||||
var menuBackground = UIColor(45, 50, 55, alpha90)
|
||||
var tabBarButtonBackground = UIColor(60, 64, 68, alpha70)
|
||||
var downloadBadgeBackground = UIColor(230, 70, 60, alpha100)
|
||||
// Background color && press color
|
||||
var pressBackground = UIColor(28, 28, 30, alpha100)
|
||||
// Red color (use for status closed in place page)
|
||||
var red = UIColor(230, 70, 60, alpha100)
|
||||
var errorPink = UIColor(246, 60, 51, alpha26)
|
||||
// Orange color (use for status 15 min in place page)
|
||||
var orange = UIColor(250, 190, 10, alpha100)
|
||||
// Blue color (use for links and phone numbers)
|
||||
var linkBlue = UIColor.alternativeAccent
|
||||
var linkBlueHighlighted = UIColor.accent
|
||||
var linkBlueDark = UIColor.accent
|
||||
var buttonRed = UIColor(244, 67, 67, alpha100)
|
||||
var buttonRedHighlighted = UIColor(183, 28, 28, alpha100)
|
||||
var black = UIColor(255, 255, 255, alpha100)
|
||||
var blackPrimaryText = UIColor(255, 255, 255, alpha90)
|
||||
var blackSecondaryText = UIColor(255, 255, 255, alpha70)
|
||||
var blackHintText = UIColor(255, 255, 255, alpha30)
|
||||
var blackDividers = UIColor(255, 255, 255, alpha08)
|
||||
var solidDividers = UIColor(84, 86, 90, alpha100)
|
||||
var white = UIColor(34, 34, 36, alpha100)
|
||||
var whitePrimaryText = UIColor(255, 255, 255, alpha87)
|
||||
var whitePrimaryTextHighlighted = UIColor(255, 255, 255, alpha30)
|
||||
var whiteSecondaryText = UIColor(0, 0, 0, alpha70)
|
||||
var whiteHintText = UIColor(0, 0, 0, alpha26)
|
||||
var buttonDisabledBlueText = UIColor(255, 230, 140, alpha30)
|
||||
var alertBackground = UIColor(60, 64, 68, alpha90)
|
||||
var blackOpaque = UIColor(255, 255, 255, alpha04)
|
||||
var toastBackground = UIColor(0, 0, 0, alpha87)
|
||||
var statusBarBackground = UIColor(0, 0, 0, alpha32)
|
||||
var searchPromoBackground = UIColor(71, 75, 79, alpha100)
|
||||
var border = UIColor(255, 255, 255, alpha04)
|
||||
var bookingBackground = UIColor(25, 69, 125, alpha100)
|
||||
var opentableBackground = UIColor(218, 55, 67, alpha100)
|
||||
var transparentGreen = UIColor(233, 244, 233, alpha26)
|
||||
var ratingRed = UIColor(229, 57, 53, alpha100)
|
||||
var ratingOrange = UIColor(244, 81, 30, alpha100)
|
||||
var ratingYellow = UIColor(245, 176, 39, alpha100)
|
||||
var ratingLightGreen = UIColor(124, 179, 66, alpha100)
|
||||
var ratingGreen = UIColor(67, 160, 71, alpha100)
|
||||
var fadeBackground = UIColor(0, 0, 0, alpha80)
|
||||
var blackStatusBarBackground = UIColor(0, 0, 0, alpha80)
|
||||
var elevationPreviewTint = UIColor(0, 0, 0, alpha54)
|
||||
var elevationPreviewSelector = UIColor(red: 0.404, green: 0.439, blue: 0.475, alpha: 1)
|
||||
var shadow = UIColor.clear
|
||||
var chartLine = UIColor(red: 0.294, green: 0.725, blue: 0.902, alpha: 1)
|
||||
var chartShadow = UIColor(red: 0.294, green: 0.725, blue: 0.902, alpha: 0.12)
|
||||
var cityColor = UIColor(152, 103, 252, alpha100)
|
||||
var outdoorColor = UIColor(147, 191, 57, alpha100)
|
||||
var carplayPlaceholderBackground = UIColor(50, 54, 58, alpha100)
|
||||
var iconOpaqueGrayTint = UIColor(197, 197, 197, alpha100)
|
||||
var iconOpaqueGrayBackground = UIColor(84, 86, 90, alpha100)
|
||||
}
|
||||
73
iphone/Maps/Core/Theme/Components/IColors.swift
Normal file
73
iphone/Maps/Core/Theme/Components/IColors.swift
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
let alpha04: CGFloat = 0.04
|
||||
let alpha08: CGFloat = 0.08
|
||||
let alpha12: CGFloat = 0.12
|
||||
let alpha20: CGFloat = 0.20
|
||||
let alpha26: CGFloat = 0.26
|
||||
let alpha30: CGFloat = 0.3
|
||||
let alpha32: CGFloat = 0.32
|
||||
let alpha36: CGFloat = 0.36
|
||||
let alpha40: CGFloat = 0.4
|
||||
let alpha54: CGFloat = 0.54
|
||||
let alpha70: CGFloat = 0.7
|
||||
let alpha80: CGFloat = 0.8
|
||||
let alpha87: CGFloat = 0.87
|
||||
let alpha90: CGFloat = 0.9
|
||||
let alpha100: CGFloat = 1.0
|
||||
|
||||
@objc protocol IColors {
|
||||
var clear: UIColor { get }
|
||||
var primaryDark: UIColor { get }
|
||||
var primary: UIColor { get }
|
||||
var secondary: UIColor { get }
|
||||
var primaryLight: UIColor { get }
|
||||
var menuBackground: UIColor { get }
|
||||
var tabBarButtonBackground: UIColor { get }
|
||||
var downloadBadgeBackground: UIColor { get }
|
||||
var pressBackground: UIColor { get }
|
||||
var red: UIColor { get }
|
||||
var errorPink: UIColor { get }
|
||||
var orange: UIColor { get }
|
||||
var linkBlue: UIColor { get }
|
||||
var linkBlueHighlighted: UIColor { get }
|
||||
var linkBlueDark: UIColor { get }
|
||||
var buttonRed: UIColor { get }
|
||||
var buttonRedHighlighted: UIColor { get }
|
||||
var black: UIColor { get }
|
||||
var blackPrimaryText: UIColor { get }
|
||||
var blackSecondaryText: UIColor { get }
|
||||
var blackHintText: UIColor { get }
|
||||
var blackDividers: UIColor { get }
|
||||
var solidDividers: UIColor { get }
|
||||
var white: UIColor { get }
|
||||
var whitePrimaryText: UIColor { get }
|
||||
var whitePrimaryTextHighlighted: UIColor { get }
|
||||
var whiteSecondaryText: UIColor { get }
|
||||
var whiteHintText: UIColor { get }
|
||||
var buttonDisabledBlueText: UIColor { get }
|
||||
var alertBackground: UIColor { get }
|
||||
var blackOpaque: UIColor { get }
|
||||
var toastBackground: UIColor { get }
|
||||
var statusBarBackground: UIColor { get }
|
||||
var searchPromoBackground: UIColor { get }
|
||||
var border: UIColor { get }
|
||||
var bookingBackground: UIColor { get }
|
||||
var opentableBackground: UIColor { get }
|
||||
var transparentGreen: UIColor { get }
|
||||
var ratingRed: UIColor { get }
|
||||
var ratingOrange: UIColor { get }
|
||||
var ratingYellow: UIColor { get }
|
||||
var ratingLightGreen: UIColor { get }
|
||||
var ratingGreen: UIColor { get }
|
||||
var fadeBackground: UIColor { get }
|
||||
var blackStatusBarBackground: UIColor { get }
|
||||
var elevationPreviewSelector: UIColor { get }
|
||||
var elevationPreviewTint: UIColor { get }
|
||||
var shadow: UIColor { get }
|
||||
var chartLine: UIColor { get }
|
||||
var chartShadow: UIColor { get }
|
||||
var cityColor: UIColor { get }
|
||||
var outdoorColor: UIColor { get }
|
||||
var carplayPlaceholderBackground: UIColor { get }
|
||||
var iconOpaqueGrayTint: UIColor { get }
|
||||
var iconOpaqueGrayBackground: UIColor { get }
|
||||
}
|
||||
60
iphone/Maps/Core/Theme/Components/IFonts.swift
Normal file
60
iphone/Maps/Core/Theme/Components/IFonts.swift
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
@objc protocol IFonts {
|
||||
var regular9: UIFont { get }
|
||||
var regular10: UIFont { get }
|
||||
|
||||
var regular11: UIFont { get }
|
||||
var regular12: UIFont { get }
|
||||
var regular13: UIFont { get }
|
||||
var regular14: UIFont { get }
|
||||
var regular15: UIFont { get }
|
||||
var regular16: UIFont { get }
|
||||
var regular17: UIFont { get }
|
||||
var regular18: UIFont { get }
|
||||
var regular20: UIFont { get }
|
||||
var regular24: UIFont { get }
|
||||
var regular32: UIFont { get }
|
||||
var regular52: UIFont { get }
|
||||
var medium9: UIFont { get }
|
||||
var medium10: UIFont { get }
|
||||
var medium12: UIFont { get }
|
||||
var medium13: UIFont { get }
|
||||
var medium14: UIFont { get }
|
||||
var medium16: UIFont { get }
|
||||
var medium17: UIFont { get }
|
||||
var medium18: UIFont { get }
|
||||
var medium20: UIFont { get }
|
||||
var medium24: UIFont { get }
|
||||
var medium28: UIFont { get }
|
||||
var medium36: UIFont { get }
|
||||
var medium40: UIFont { get }
|
||||
var medium44: UIFont { get }
|
||||
var light10: UIFont { get }
|
||||
var light12: UIFont { get }
|
||||
var light16: UIFont { get }
|
||||
var light17: UIFont { get }
|
||||
var bold12: UIFont { get }
|
||||
var bold14: UIFont { get }
|
||||
var bold16: UIFont { get }
|
||||
var bold17: UIFont { get }
|
||||
var bold18: UIFont { get }
|
||||
var bold20: UIFont { get }
|
||||
var bold22: UIFont { get }
|
||||
var bold24: UIFont { get }
|
||||
var bold28: UIFont { get }
|
||||
var bold34: UIFont { get }
|
||||
var bold36: UIFont { get }
|
||||
var bold48: UIFont { get }
|
||||
var header: UIFont { get }
|
||||
var heavy17: UIFont { get }
|
||||
var heavy20: UIFont { get }
|
||||
var heavy32: UIFont { get }
|
||||
var heavy38: UIFont { get }
|
||||
var italic12: UIFont { get }
|
||||
var italic16: UIFont { get }
|
||||
var semibold12: UIFont { get }
|
||||
var semibold14: UIFont { get }
|
||||
var semibold15: UIFont { get }
|
||||
var semibold16: UIFont { get }
|
||||
var semibold18: UIFont { get }
|
||||
var semibold20: UIFont { get }
|
||||
}
|
||||
9
iphone/Maps/Core/Theme/Core/IStyleSheet.swift
Normal file
9
iphone/Maps/Core/Theme/Core/IStyleSheet.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
protocol IStyleSheet: CaseIterable, RawRepresentable, StyleStringRepresentable {
|
||||
static func register(theme: Theme, colors: IColors, fonts: IFonts)
|
||||
}
|
||||
|
||||
extension IStyleSheet {
|
||||
static func register(theme: Theme, colors: IColors, fonts: IFonts) {
|
||||
allCases.forEach { theme.add($0, $0.styleResolverFor(colors: colors, fonts: fonts)) }
|
||||
}
|
||||
}
|
||||
333
iphone/Maps/Core/Theme/Core/Style.swift
Normal file
333
iphone/Maps/Core/Theme/Core/Style.swift
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
class Style: ExpressibleByDictionaryLiteral {
|
||||
enum Parameter: Hashable{
|
||||
case backgroundColor
|
||||
case borderColor
|
||||
case borderWidth
|
||||
case cornerRadius
|
||||
case maskedCorners
|
||||
case shadowColor
|
||||
case shadowOpacity
|
||||
case shadowOffset
|
||||
case shadowRadius
|
||||
case clip
|
||||
case round
|
||||
|
||||
case font
|
||||
case fontColor
|
||||
case fontDetailed
|
||||
case fontColorDetailed
|
||||
case tintColor
|
||||
case tintColorDisabled
|
||||
case onTintColor
|
||||
case offTintColor
|
||||
case image
|
||||
case mwmImage
|
||||
case color
|
||||
case attributes
|
||||
case linkAttributes
|
||||
|
||||
case backgroundImage
|
||||
case backgroundColorSelected
|
||||
case backgroundColorHighlighted
|
||||
case backgroundColorDisabled
|
||||
case fontColorSelected
|
||||
case fontColorHighlighted
|
||||
case fontColorDisabled
|
||||
case barTintColor
|
||||
case shadowImage
|
||||
case textAlignment
|
||||
case textContainerInset
|
||||
case separatorColor
|
||||
case pageIndicatorTintColor
|
||||
case currentPageIndicatorTintColor
|
||||
|
||||
case coloring
|
||||
case colors
|
||||
case images
|
||||
case exclusions
|
||||
case unknown
|
||||
|
||||
case gridColor
|
||||
case previewSelectorColor
|
||||
case previewTintColor
|
||||
case infoBackground
|
||||
}
|
||||
|
||||
typealias Key = Parameter
|
||||
typealias Value = Any?
|
||||
|
||||
var params:[Key: Value] = [:]
|
||||
var isEmpty: Bool {
|
||||
return params.isEmpty
|
||||
}
|
||||
|
||||
required init(dictionaryLiteral elements: (Style.Parameter, Any?)...) {
|
||||
for (key, value) in elements {
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
subscript(keyname: Key) -> Value {
|
||||
get { return params[keyname] ?? nil }
|
||||
}
|
||||
|
||||
func append(_ style: Style) {
|
||||
params.merge(style.params) { (a, b) -> Style.Value in
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
func append(_ styles: [Style]) {
|
||||
styles.forEach { (style) in
|
||||
params.merge(style.params) { (a, b) -> Style.Value in
|
||||
return a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasExclusion(view: UIView) -> Bool {
|
||||
guard let exclusions = exclusions else {
|
||||
return false
|
||||
}
|
||||
var superView:UIView? = view
|
||||
while (superView != nil) {
|
||||
if exclusions.contains(String(describing: type(of: superView!))) {
|
||||
return true
|
||||
}
|
||||
superView = superView?.superview
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
extension Style {
|
||||
var backgroundColor: UIColor? {
|
||||
get { return self[.backgroundColor] as? UIColor }
|
||||
set { params[.backgroundColor] = newValue }
|
||||
}
|
||||
|
||||
var borderColor: UIColor? {
|
||||
get { return self[.borderColor] as? UIColor }
|
||||
set { params[.borderColor] = newValue }
|
||||
}
|
||||
|
||||
var borderWidth: CGFloat? {
|
||||
get { return self[.borderWidth] as? CGFloat }
|
||||
set { params[.borderWidth] = newValue }
|
||||
}
|
||||
|
||||
var cornerRadius: CornerRadius? {
|
||||
get { return self[.cornerRadius] as? CornerRadius }
|
||||
set { params[.cornerRadius] = newValue }
|
||||
}
|
||||
|
||||
var maskedCorners: CACornerMask? {
|
||||
get { return self[.maskedCorners] as? CACornerMask }
|
||||
set { params[.maskedCorners] = newValue }
|
||||
}
|
||||
|
||||
var shadowColor: UIColor? {
|
||||
get { return self[.shadowColor] as? UIColor }
|
||||
set { params[.shadowColor] = newValue }
|
||||
}
|
||||
|
||||
var shadowOpacity: Float? {
|
||||
get { return self[.shadowOpacity] as? Float }
|
||||
set { params[.shadowOpacity] = newValue }
|
||||
}
|
||||
|
||||
var shadowOffset: CGSize? {
|
||||
get { return self[.shadowOffset] as? CGSize }
|
||||
set { params[.shadowOffset] = newValue }
|
||||
}
|
||||
|
||||
var shadowRadius: CGFloat? {
|
||||
get { return self[.shadowRadius] as? CGFloat }
|
||||
set { params[.shadowRadius] = newValue }
|
||||
}
|
||||
|
||||
var clip: Bool? {
|
||||
get { return self[.clip] as? Bool }
|
||||
set { params[.clip] = newValue }
|
||||
}
|
||||
|
||||
var round: Bool? {
|
||||
get { return self[.round] as? Bool }
|
||||
set { params[.round] = newValue }
|
||||
}
|
||||
|
||||
var font: UIFont? {
|
||||
get { return self[.font] as? UIFont }
|
||||
set { params[.font] = newValue }
|
||||
}
|
||||
|
||||
var fontColor: UIColor? {
|
||||
get { return self[.fontColor] as? UIColor }
|
||||
set { params[.fontColor] = newValue }
|
||||
}
|
||||
|
||||
var fontDetailed: UIFont? {
|
||||
get { return self[.fontDetailed] as? UIFont }
|
||||
set { params[.fontDetailed] = newValue }
|
||||
}
|
||||
|
||||
var fontColorDetailed: UIColor? {
|
||||
get { return self[.fontColorDetailed] as? UIColor }
|
||||
set { params[.fontColorDetailed] = newValue }
|
||||
}
|
||||
|
||||
var tintColor: UIColor? {
|
||||
get { return self[.tintColor] as? UIColor }
|
||||
set { params[.tintColor] = newValue }
|
||||
}
|
||||
|
||||
var tintColorDisabled: UIColor? {
|
||||
get { return self[.tintColorDisabled] as? UIColor }
|
||||
set { params[.tintColorDisabled] = newValue }
|
||||
}
|
||||
|
||||
var onTintColor: UIColor? {
|
||||
get { return self[.onTintColor] as? UIColor }
|
||||
set { params[.onTintColor] = newValue }
|
||||
}
|
||||
|
||||
var offTintColor: UIColor? {
|
||||
get { return self[.offTintColor] as? UIColor }
|
||||
set { params[.offTintColor] = newValue }
|
||||
}
|
||||
|
||||
var image: String? {
|
||||
get { return self[.image] as? String }
|
||||
set { params[.image] = newValue }
|
||||
}
|
||||
|
||||
var mwmImage: String? {
|
||||
get { return self[.mwmImage] as? String }
|
||||
set { params[.mwmImage] = newValue }
|
||||
}
|
||||
|
||||
var color: UIColor? {
|
||||
get { return self[.color] as? UIColor }
|
||||
set { params[.color] = newValue }
|
||||
}
|
||||
|
||||
var attributes: [NSAttributedString.Key : Any]? {
|
||||
get { return self[.attributes] as? [NSAttributedString.Key : Any] }
|
||||
set { params[.attributes] = newValue }
|
||||
}
|
||||
|
||||
var linkAttributes: [NSAttributedString.Key : Any]? {
|
||||
get { return self[.linkAttributes] as? [NSAttributedString.Key : Any] }
|
||||
set { params[.linkAttributes] = newValue }
|
||||
}
|
||||
|
||||
var backgroundImage: UIImage? {
|
||||
get { return self[.backgroundImage] as? UIImage }
|
||||
set { params[.backgroundImage] = newValue }
|
||||
}
|
||||
|
||||
var barTintColor: UIColor? {
|
||||
get { return self[.barTintColor] as? UIColor }
|
||||
set { params[.barTintColor] = newValue }
|
||||
}
|
||||
|
||||
var backgroundColorSelected: UIColor? {
|
||||
get { return self[.backgroundColorSelected] as? UIColor }
|
||||
set { params[.backgroundColorSelected] = newValue }
|
||||
}
|
||||
|
||||
var backgroundColorHighlighted: UIColor? {
|
||||
get { return self[.backgroundColorHighlighted] as? UIColor }
|
||||
set { params[.backgroundColorHighlighted] = newValue }
|
||||
}
|
||||
|
||||
var backgroundColorDisabled: UIColor? {
|
||||
get { return self[.backgroundColorDisabled] as? UIColor }
|
||||
set { params[.backgroundColorDisabled] = newValue }
|
||||
}
|
||||
|
||||
var fontColorSelected: UIColor? {
|
||||
get { return self[.fontColorSelected] as? UIColor }
|
||||
set { params[.fontColorSelected] = newValue }
|
||||
}
|
||||
|
||||
var fontColorHighlighted: UIColor? {
|
||||
get { return self[.fontColorHighlighted] as? UIColor }
|
||||
set { params[.fontColorHighlighted] = newValue }
|
||||
}
|
||||
|
||||
var fontColorDisabled: UIColor? {
|
||||
get { return self[.fontColorDisabled] as? UIColor }
|
||||
set { params[.fontColorDisabled] = newValue }
|
||||
}
|
||||
|
||||
var shadowImage: UIImage? {
|
||||
get { return self[.shadowImage] as? UIImage }
|
||||
set { params[.shadowImage] = newValue }
|
||||
}
|
||||
|
||||
var textAlignment: NSTextAlignment? {
|
||||
get { return self[.textAlignment] as? NSTextAlignment }
|
||||
set { params[.textAlignment] = newValue }
|
||||
}
|
||||
|
||||
var textContainerInset: UIEdgeInsets? {
|
||||
get { return self[.textContainerInset] as? UIEdgeInsets }
|
||||
set { params[.textContainerInset] = newValue }
|
||||
}
|
||||
|
||||
var separatorColor: UIColor? {
|
||||
get { return self[.separatorColor] as? UIColor }
|
||||
set { params[.separatorColor] = newValue }
|
||||
}
|
||||
|
||||
var pageIndicatorTintColor: UIColor? {
|
||||
get { return self[.pageIndicatorTintColor] as? UIColor }
|
||||
set { params[.pageIndicatorTintColor] = newValue }
|
||||
}
|
||||
|
||||
var currentPageIndicatorTintColor: UIColor? {
|
||||
get { return self[.currentPageIndicatorTintColor] as? UIColor }
|
||||
set { params[.currentPageIndicatorTintColor] = newValue }
|
||||
}
|
||||
|
||||
var colors: [UIColor]? {
|
||||
get { return self[.colors] as? [UIColor] }
|
||||
set { params[.colors] = newValue }
|
||||
}
|
||||
|
||||
var images: [String]? {
|
||||
get { return self[.images] as? [String] }
|
||||
set { params[.images] = newValue }
|
||||
}
|
||||
|
||||
var coloring: MWMButtonColoring? {
|
||||
get { return self[.coloring] as? MWMButtonColoring }
|
||||
set { params[.coloring] = newValue }
|
||||
}
|
||||
|
||||
var exclusions: Set<String>? {
|
||||
get { return self[.exclusions] as? Set<String> }
|
||||
set { params[.exclusions] = newValue }
|
||||
}
|
||||
|
||||
var gridColor: UIColor? {
|
||||
get { return self[.gridColor] as? UIColor }
|
||||
set { params[.gridColor] = newValue }
|
||||
}
|
||||
|
||||
var previewSelectorColor: UIColor? {
|
||||
get { return self[.previewSelectorColor] as? UIColor }
|
||||
set { params[.previewSelectorColor] = newValue }
|
||||
}
|
||||
|
||||
var previewTintColor: UIColor? {
|
||||
get { return self[.previewTintColor] as? UIColor }
|
||||
set { params[.previewTintColor] = newValue }
|
||||
}
|
||||
|
||||
var infoBackground: UIColor? {
|
||||
get { return self[.infoBackground] as? UIColor }
|
||||
set { params[.infoBackground] = newValue }
|
||||
}
|
||||
}
|
||||
80
iphone/Maps/Core/Theme/Core/StyleManager.swift
Normal file
80
iphone/Maps/Core/Theme/Core/StyleManager.swift
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
@objc protocol ThemeListener {
|
||||
@objc func applyTheme()
|
||||
}
|
||||
|
||||
@objc class StyleManager: NSObject {
|
||||
@objc static var shared = StyleManager()
|
||||
@objc private(set) var theme: Theme?
|
||||
private var listeners: [Weak<ThemeListener>] = []
|
||||
|
||||
override private init() {
|
||||
super.init()
|
||||
SwizzleStyle.swizzle()
|
||||
}
|
||||
|
||||
func setTheme (_ theme: Theme) {
|
||||
self.theme = theme;
|
||||
update()
|
||||
}
|
||||
|
||||
func hasTheme () -> Bool {
|
||||
return theme != nil
|
||||
}
|
||||
|
||||
func update () {
|
||||
for scene in UIApplication.shared.connectedScenes {
|
||||
if let windowsScene = scene as? UIWindowScene {
|
||||
for window in windowsScene.windows {
|
||||
updateView(window.rootViewController?.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let appDelegate = UIApplication.shared.delegate as! MapsAppDelegate
|
||||
if let vc = appDelegate.window.rootViewController?.presentedViewController {
|
||||
vc.applyTheme()
|
||||
updateView(vc.view)
|
||||
} else if let vcs = appDelegate.window.rootViewController?.children {
|
||||
for vc in vcs {
|
||||
vc.applyTheme()
|
||||
}
|
||||
}
|
||||
|
||||
for container in listeners {
|
||||
if let listener = container.value {
|
||||
listener.applyTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateView(_ view: UIView?) {
|
||||
guard let view = view else {
|
||||
return
|
||||
}
|
||||
view.isStyleApplied = false
|
||||
for subview in view.subviews {
|
||||
self.updateView(subview)
|
||||
}
|
||||
view.applyTheme()
|
||||
view.isStyleApplied = true;
|
||||
}
|
||||
|
||||
func getStyle(_ styleName: String) -> [Style]{
|
||||
return theme?.get(styleName) ?? [Style]()
|
||||
}
|
||||
|
||||
@objc func addListener(_ themeListener: ThemeListener) {
|
||||
if theme != nil {
|
||||
themeListener.applyTheme()
|
||||
}
|
||||
if !listeners.contains(where: { themeListener === $0.value }) {
|
||||
listeners.append(Weak(value: themeListener))
|
||||
}
|
||||
}
|
||||
|
||||
@objc func removeListener(_ themeListener: ThemeListener) {
|
||||
listeners.removeAll { (container) -> Bool in
|
||||
return container.value === themeListener
|
||||
}
|
||||
}
|
||||
}
|
||||
76
iphone/Maps/Core/Theme/Core/Theme.swift
Normal file
76
iphone/Maps/Core/Theme/Core/Theme.swift
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
@objc class Theme: NSObject {
|
||||
enum ThemeType {
|
||||
case dark
|
||||
case light
|
||||
}
|
||||
typealias StyleName = String
|
||||
typealias Resolver = ((Style) -> (Void))
|
||||
|
||||
@objc let colors: IColors
|
||||
@objc let fonts: IFonts
|
||||
private var themeType: ThemeType
|
||||
private var components: [StyleName: Style] = [:]
|
||||
private var resolvers: [StyleName: Resolver] = [:]
|
||||
private var dependencies: [StyleName: StyleName] = [:]
|
||||
|
||||
init (type: ThemeType, colors: IColors, fonts: IFonts) {
|
||||
self.colors = colors
|
||||
self.fonts = fonts
|
||||
self.themeType = type
|
||||
super.init()
|
||||
self.register()
|
||||
}
|
||||
|
||||
func registerStyleSheet<U: IStyleSheet> (_ type: U.Type) {
|
||||
U.register(theme: self, colors: colors, fonts: fonts)
|
||||
}
|
||||
|
||||
func add(styleName: StyleName, _ resolver:@escaping Resolver) {
|
||||
resolvers[styleName] = resolver
|
||||
}
|
||||
|
||||
func add(styleName: StyleName, from: StyleName, _ resolver:@escaping Resolver) {
|
||||
resolvers[styleName] = resolver
|
||||
dependencies[styleName] = from
|
||||
}
|
||||
|
||||
func add(styleName: StyleName, forType: ThemeType, _ resolver:@escaping Resolver) {
|
||||
guard themeType == forType else {
|
||||
return
|
||||
}
|
||||
resolvers[styleName] = resolver
|
||||
}
|
||||
|
||||
func add(styleName: StyleName, from: StyleName, forType: ThemeType, _ resolver:@escaping Resolver) {
|
||||
guard themeType == forType else {
|
||||
return
|
||||
}
|
||||
resolvers[styleName] = resolver
|
||||
dependencies[styleName] = from
|
||||
}
|
||||
|
||||
func get(_ styleName: StyleName) -> [Style] {
|
||||
let styleNames = styleName.split(separator: ":")
|
||||
var result = [Style]()
|
||||
for name in styleNames {
|
||||
let strName = String(name)
|
||||
if let style = components[strName] {
|
||||
result.append(style)
|
||||
} else if let resolver = resolvers[strName] {
|
||||
let style = Style()
|
||||
resolver(style)
|
||||
if let dependency = dependencies[strName] {
|
||||
style.append(self.get(dependency))
|
||||
}
|
||||
result.append(style)
|
||||
} else {
|
||||
assertionFailure("Style Not found:\(name)")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func register() {
|
||||
fatalError("You should register stylesheets in subclass")
|
||||
}
|
||||
}
|
||||
103
iphone/Maps/Core/Theme/Core/ThemeManager.swift
Normal file
103
iphone/Maps/Core/Theme/Core/ThemeManager.swift
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
@objc(MWMThemeManager)
|
||||
final class ThemeManager: NSObject {
|
||||
private static let autoUpdatesInterval: TimeInterval = 30 * 60 // 30 minutes in seconds
|
||||
|
||||
private static let instance = ThemeManager()
|
||||
private weak var timer: Timer?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
private func update(theme: MWMTheme) {
|
||||
updateSystemUserInterfaceStyle(theme)
|
||||
|
||||
let actualTheme: MWMTheme = { theme in
|
||||
let isVehicleRouting = MWMRouter.isRoutingActive() && (MWMRouter.type() == .vehicle) && MWMRouter.hasSavedRoute()
|
||||
switch theme {
|
||||
case .day: fallthrough
|
||||
case .vehicleDay: return isVehicleRouting ? .vehicleDay : .day
|
||||
case .night: fallthrough
|
||||
case .vehicleNight: return isVehicleRouting ? .vehicleNight : .night
|
||||
case .auto:
|
||||
let isDarkModeEnabled = UIScreen.main.traitCollection.userInterfaceStyle == .dark
|
||||
guard isVehicleRouting else { return isDarkModeEnabled ? .night : .day }
|
||||
return isDarkModeEnabled ? .vehicleNight : .vehicleDay
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}(theme)
|
||||
|
||||
let nightMode = UIColor.isNightMode()
|
||||
let newNightMode: Bool = { theme in
|
||||
switch theme {
|
||||
case .day: fallthrough
|
||||
case .vehicleDay: return false
|
||||
case .night: fallthrough
|
||||
case .vehicleNight: return true
|
||||
case .auto: assert(false); return false
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}(actualTheme)
|
||||
|
||||
if Settings.mapAppearance == .light {
|
||||
if actualTheme == .vehicleDay || actualTheme == .vehicleNight {
|
||||
FrameworkHelper.setTheme(.vehicleDay)
|
||||
} else {
|
||||
FrameworkHelper.setTheme(.day)
|
||||
}
|
||||
} else if Settings.mapAppearance == .dark {
|
||||
if actualTheme == .vehicleDay || actualTheme == .vehicleNight {
|
||||
FrameworkHelper.setTheme(.vehicleNight)
|
||||
} else {
|
||||
FrameworkHelper.setTheme(.night)
|
||||
}
|
||||
} else {
|
||||
FrameworkHelper.setTheme(actualTheme)
|
||||
}
|
||||
if nightMode != newNightMode || StyleManager.shared.hasTheme() == false{
|
||||
UIColor.setNightMode(newNightMode)
|
||||
if newNightMode {
|
||||
StyleManager.shared.setTheme(MainTheme(type: .dark, colors: NightColors(), fonts: Fonts()))
|
||||
} else {
|
||||
StyleManager.shared.setTheme(MainTheme(type: .light, colors: DayColors(), fonts: Fonts()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc static func invalidate() {
|
||||
instance.update(theme: SettingsBridge.theme())
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
private func updateSystemUserInterfaceStyle(_ theme: MWMTheme) {
|
||||
let userInterfaceStyle: UIUserInterfaceStyle = { theme in
|
||||
switch theme {
|
||||
case .day: fallthrough
|
||||
case .vehicleDay: return .light
|
||||
case .night: fallthrough
|
||||
case .vehicleNight: return .dark
|
||||
case .auto: return .unspecified
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}(theme)
|
||||
UIApplication.shared.delegate?.window??.overrideUserInterfaceStyle = userInterfaceStyle
|
||||
}
|
||||
|
||||
@available(iOS, deprecated:13.0)
|
||||
@objc static var autoUpdates: Bool {
|
||||
get {
|
||||
return instance.timer != nil
|
||||
}
|
||||
set {
|
||||
if newValue {
|
||||
instance.timer = Timer.scheduledTimer(timeInterval: autoUpdatesInterval, target: self, selector: #selector(invalidate), userInfo: nil, repeats: true)
|
||||
} else {
|
||||
instance.timer?.invalidate()
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
21
iphone/Maps/Core/Theme/CornerRadius.swift
Normal file
21
iphone/Maps/Core/Theme/CornerRadius.swift
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
enum CornerRadius {
|
||||
case modalSheet
|
||||
case buttonDefault
|
||||
case buttonDefaultSmall
|
||||
case buttonSmall
|
||||
case grabber
|
||||
case custom(CGFloat)
|
||||
}
|
||||
|
||||
extension CornerRadius {
|
||||
var value: CGFloat {
|
||||
switch self {
|
||||
case .modalSheet: return 12
|
||||
case .buttonDefault: return 8
|
||||
case .buttonDefaultSmall: return 6
|
||||
case .buttonSmall: return 4
|
||||
case .grabber: return 2.5
|
||||
case .custom(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
46
iphone/Maps/Core/Theme/Extensions/UIColor+hexString.swift
Normal file
46
iphone/Maps/Core/Theme/Extensions/UIColor+hexString.swift
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
extension UIColor {
|
||||
private func convertToHEX(component: CGFloat) -> Int {
|
||||
return lroundf(Float(component * 255));
|
||||
}
|
||||
|
||||
private var hexColorComponentTemplate: String {
|
||||
get {
|
||||
return "%02lX";
|
||||
}
|
||||
}
|
||||
|
||||
private var hexColorTemplate: String {
|
||||
get {
|
||||
return "#\(hexColorComponentTemplate)\(hexColorComponentTemplate)\(hexColorComponentTemplate)";
|
||||
}
|
||||
}
|
||||
|
||||
var hexString: String {
|
||||
get {
|
||||
let cgColorInRGB = cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil)!
|
||||
let colorRef = cgColorInRGB.components
|
||||
let r = colorRef?[0] ?? 0
|
||||
let g = colorRef?[1] ?? 0
|
||||
let b = ((colorRef?.count ?? 0) > 2 ? colorRef?[2] : g) ?? 0
|
||||
let alpha = cgColor.alpha
|
||||
|
||||
var color = String(
|
||||
format: hexColorTemplate,
|
||||
convertToHEX(component: r),
|
||||
convertToHEX(component: g),
|
||||
convertToHEX(component: b)
|
||||
)
|
||||
|
||||
if (alpha < 1) {
|
||||
color += String(format: hexColorComponentTemplate, convertToHEX(component: alpha))
|
||||
}
|
||||
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
var sRGBColor: UIColor {
|
||||
let cgColorInRGB = cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil)!
|
||||
return UIColor(cgColor: cgColorInRGB)
|
||||
}
|
||||
}
|
||||
11
iphone/Maps/Core/Theme/Extensions/UIColor+image.swift
Normal file
11
iphone/Maps/Core/Theme/Extensions/UIColor+image.swift
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
extension UIColor {
|
||||
func getImage() -> UIImage? {
|
||||
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
|
||||
UIGraphicsBeginImageContext(rect.size)
|
||||
self.setFill()
|
||||
UIRectFill(rect)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image
|
||||
}
|
||||
}
|
||||
5
iphone/Maps/Core/Theme/Extensions/UIColor+rgba.swift
Normal file
5
iphone/Maps/Core/Theme/Extensions/UIColor+rgba.swift
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
extension UIColor {
|
||||
convenience init(_ r: CGFloat, _ g: CGFloat, _ b :CGFloat, _ a: CGFloat) {
|
||||
self.init(red: CGFloat(r/255.0), green: CGFloat(g/255.0), blue: CGFloat(b/255.0), alpha: a)
|
||||
}
|
||||
}
|
||||
18
iphone/Maps/Core/Theme/Extensions/UIFont+monospaced.swift
Normal file
18
iphone/Maps/Core/Theme/Extensions/UIFont+monospaced.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import UIKit
|
||||
|
||||
extension UIFont {
|
||||
|
||||
/// Creates a UIFont object with monospaced numbers keeping other font descriptors like size and weight
|
||||
@objc var monospaced: UIFont {
|
||||
let attributes: [UIFontDescriptor.AttributeName: Any] = [
|
||||
.featureSettings: [
|
||||
[
|
||||
UIFontDescriptor.FeatureKey.type: kNumberSpacingType,
|
||||
UIFontDescriptor.FeatureKey.selector: kMonospacedNumbersSelector
|
||||
]
|
||||
]
|
||||
]
|
||||
let monospacedNumbersFontDescriptor = fontDescriptor.addingAttributes(attributes)
|
||||
return UIFont(descriptor: monospacedNumbersFontDescriptor, size: pointSize)
|
||||
}
|
||||
}
|
||||
23
iphone/Maps/Core/Theme/Extensions/UILabel+SetFontStyle.swift
Normal file
23
iphone/Maps/Core/Theme/Extensions/UILabel+SetFontStyle.swift
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
extension UILabel {
|
||||
func setFontStyle(_ font: FontStyleSheet, color: TextColorStyleSheet? = nil) {
|
||||
var name = font.rawValue
|
||||
if let color {
|
||||
name += ":\(color.rawValue)"
|
||||
}
|
||||
styleName = name
|
||||
}
|
||||
|
||||
func setFontStyle(_ color: TextColorStyleSheet) {
|
||||
styleName = color.rawValue
|
||||
}
|
||||
|
||||
func setFontStyleAndApply(_ font: FontStyleSheet, color: TextColorStyleSheet? = nil) {
|
||||
setFontStyle(font, color: color)
|
||||
applyTheme()
|
||||
}
|
||||
|
||||
func setFontStyleAndApply(_ color: TextColorStyleSheet) {
|
||||
setFontStyle(color)
|
||||
applyTheme()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
fileprivate struct AssociatedKeys {
|
||||
static var styleName: UInt8 = 0
|
||||
static var isStyleApplied: UInt8 = 1
|
||||
}
|
||||
|
||||
@objc extension UINavigationItem: StyleApplicable {
|
||||
@objc var styleName: String {
|
||||
get {
|
||||
isStyleApplied = false
|
||||
guard let value = objc_getAssociatedObject(self, &AssociatedKeys.styleName) as? String else {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
}
|
||||
set(newValue) {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.styleName, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
@objc var isStyleApplied: Bool {
|
||||
get {
|
||||
guard let value = objc_getAssociatedObject(self, &AssociatedKeys.isStyleApplied) as? Bool else {
|
||||
return false
|
||||
}
|
||||
return value
|
||||
}
|
||||
set(newValue) {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.isStyleApplied, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
}
|
||||
46
iphone/Maps/Core/Theme/Extensions/UIView+SetStyle.swift
Normal file
46
iphone/Maps/Core/Theme/Extensions/UIView+SetStyle.swift
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
fileprivate struct AssociatedKeys {
|
||||
static var styleName: UInt8 = 0
|
||||
static var isStyleApplied: UInt8 = 1
|
||||
}
|
||||
|
||||
@objc extension UIView: StyleApplicable {
|
||||
@objc func sw_didMoveToWindow() {
|
||||
guard MapsAppDelegate.theApp().window === window else {
|
||||
sw_didMoveToWindow();
|
||||
return
|
||||
}
|
||||
applyTheme()
|
||||
isStyleApplied = true
|
||||
sw_didMoveToWindow();
|
||||
}
|
||||
|
||||
@objc var styleName: String {
|
||||
get {
|
||||
isStyleApplied = false
|
||||
guard let value = objc_getAssociatedObject(self, &AssociatedKeys.styleName) as? String else {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
}
|
||||
set(newValue) {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.styleName, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
@objc var isStyleApplied: Bool {
|
||||
get {
|
||||
guard let value = objc_getAssociatedObject(self, &AssociatedKeys.isStyleApplied) as? Bool else {
|
||||
return false
|
||||
}
|
||||
return value
|
||||
}
|
||||
set(newValue) {
|
||||
objc_setAssociatedObject(self, &AssociatedKeys.isStyleApplied, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setStyleNameAndApply(_ styleName: String) {
|
||||
self.styleName = styleName
|
||||
applyTheme()
|
||||
}
|
||||
}
|
||||
136
iphone/Maps/Core/Theme/FontStyleSheet.swift
Normal file
136
iphone/Maps/Core/Theme/FontStyleSheet.swift
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
enum FontStyleSheet: String, CaseIterable {
|
||||
case regular9
|
||||
case regular10
|
||||
case regular11
|
||||
case regular12
|
||||
case regular13
|
||||
case regular14
|
||||
case regular15
|
||||
case regular16
|
||||
case regular17
|
||||
case regular18
|
||||
case regular20
|
||||
case regular24
|
||||
case regular32
|
||||
case regular52
|
||||
|
||||
case medium9
|
||||
case medium10
|
||||
case medium12
|
||||
case medium13
|
||||
case medium14
|
||||
case medium16
|
||||
case medium17
|
||||
case medium18
|
||||
case medium20
|
||||
case medium24
|
||||
case medium28
|
||||
case medium36
|
||||
case medium40
|
||||
case medium44
|
||||
|
||||
case light10
|
||||
case light12
|
||||
case light16
|
||||
case light17
|
||||
|
||||
case bold12
|
||||
case bold14
|
||||
case bold16
|
||||
case bold17
|
||||
case bold18
|
||||
case bold20
|
||||
case bold22
|
||||
case bold24
|
||||
case bold28
|
||||
case bold34
|
||||
case bold36
|
||||
case bold48
|
||||
|
||||
case heavy17
|
||||
case heavy20
|
||||
case heavy32
|
||||
case heavy38
|
||||
|
||||
case italic12
|
||||
case italic16
|
||||
|
||||
case semibold12
|
||||
case semibold14
|
||||
case semibold15
|
||||
case semibold16
|
||||
case semibold18
|
||||
case semibold20
|
||||
}
|
||||
|
||||
extension FontStyleSheet: IStyleSheet {
|
||||
func styleResolverFor(colors: IColors, fonts: IFonts) -> Theme.StyleResolver {
|
||||
let font: UIFont = {
|
||||
switch self {
|
||||
case .regular9: return fonts.regular9
|
||||
case .regular10: return fonts.regular10
|
||||
case .regular11: return fonts.regular11
|
||||
case .regular12: return fonts.regular12
|
||||
case .regular13: return fonts.regular13
|
||||
case .regular14: return fonts.regular14
|
||||
case .regular15: return fonts.regular15
|
||||
case .regular16: return fonts.regular16
|
||||
case .regular17: return fonts.regular17
|
||||
case .regular18: return fonts.regular18
|
||||
case .regular20: return fonts.regular20
|
||||
case .regular24: return fonts.regular24
|
||||
case .regular32: return fonts.regular32
|
||||
case .regular52: return fonts.regular52
|
||||
|
||||
case .medium9: return fonts.medium9
|
||||
case .medium10: return fonts.medium10
|
||||
case .medium12: return fonts.medium12
|
||||
case .medium13: return fonts.medium13
|
||||
case .medium14: return fonts.medium14
|
||||
case .medium16: return fonts.medium16
|
||||
case .medium17: return fonts.medium17
|
||||
case .medium18: return fonts.medium18
|
||||
case .medium20: return fonts.medium20
|
||||
case .medium24: return fonts.medium24
|
||||
case .medium28: return fonts.medium28
|
||||
case .medium36: return fonts.medium36
|
||||
case .medium40: return fonts.medium40
|
||||
case .medium44: return fonts.medium44
|
||||
|
||||
case .light10: return fonts.light10
|
||||
case .light12: return fonts.light12
|
||||
case .light16: return fonts.light16
|
||||
case .light17: return fonts.light17
|
||||
|
||||
case .bold12: return fonts.bold12
|
||||
case .bold14: return fonts.bold14
|
||||
case .bold16: return fonts.bold16
|
||||
case .bold17: return fonts.bold17
|
||||
case .bold18: return fonts.bold18
|
||||
case .bold20: return fonts.bold20
|
||||
case .bold22: return fonts.bold22
|
||||
case .bold24: return fonts.bold24
|
||||
case .bold28: return fonts.bold28
|
||||
case .bold34: return fonts.bold34
|
||||
case .bold36: return fonts.bold36
|
||||
case .bold48: return fonts.bold48
|
||||
|
||||
case .heavy17: return fonts.heavy17
|
||||
case .heavy20: return fonts.heavy20
|
||||
case .heavy32: return fonts.heavy32
|
||||
case .heavy38: return fonts.heavy38
|
||||
|
||||
case .italic12: return fonts.italic12
|
||||
case .italic16: return fonts.italic16
|
||||
|
||||
case .semibold12: return fonts.semibold12
|
||||
case .semibold14: return fonts.semibold14
|
||||
case .semibold15: return fonts.semibold15
|
||||
case .semibold16: return fonts.semibold16
|
||||
case .semibold18: return fonts.semibold18
|
||||
case .semibold20: return fonts.semibold20
|
||||
}
|
||||
}()
|
||||
return .add { s in s.font = font }
|
||||
}
|
||||
}
|
||||
59
iphone/Maps/Core/Theme/Fonts.swift
Normal file
59
iphone/Maps/Core/Theme/Fonts.swift
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
class Fonts: IFonts {
|
||||
var regular9 = UIFont.systemFont(ofSize: 9)
|
||||
var regular10 = UIFont.systemFont(ofSize: 10)
|
||||
var regular11 = UIFont.systemFont(ofSize: 11)
|
||||
var regular12 = UIFont.systemFont(ofSize: 12)
|
||||
var regular13 = UIFont.systemFont(ofSize: 13)
|
||||
var regular14 = UIFont.systemFont(ofSize: 14)
|
||||
var regular15 = UIFont.systemFont(ofSize: 15)
|
||||
var regular16 = UIFont.systemFont(ofSize: 16)
|
||||
var regular17 = UIFont.systemFont(ofSize: 17)
|
||||
var regular18 = UIFont.systemFont(ofSize: 18)
|
||||
var regular20 = UIFont.systemFont(ofSize: 20)
|
||||
var regular24 = UIFont.systemFont(ofSize: 24)
|
||||
var regular32 = UIFont.systemFont(ofSize: 32)
|
||||
var regular52 = UIFont.systemFont(ofSize: 52)
|
||||
var medium9 = UIFont.systemFont(ofSize: 9, weight:UIFont.Weight.medium)
|
||||
var medium10 = UIFont.systemFont(ofSize: 10, weight:UIFont.Weight.medium)
|
||||
var medium12 = UIFont.systemFont(ofSize: 12, weight:UIFont.Weight.medium)
|
||||
var medium13 = UIFont.systemFont(ofSize: 13, weight:UIFont.Weight.medium)
|
||||
var medium14 = UIFont.systemFont(ofSize: 14, weight:UIFont.Weight.medium)
|
||||
var medium16 = UIFont.systemFont(ofSize: 16, weight:UIFont.Weight.medium)
|
||||
var medium17 = UIFont.systemFont(ofSize: 17, weight:UIFont.Weight.medium)
|
||||
var medium18 = UIFont.systemFont(ofSize: 18, weight:UIFont.Weight.medium)
|
||||
var medium20 = UIFont.systemFont(ofSize: 20, weight:UIFont.Weight.medium)
|
||||
var medium24 = UIFont.systemFont(ofSize: 24, weight:UIFont.Weight.medium)
|
||||
var medium28 = UIFont.systemFont(ofSize: 28, weight:UIFont.Weight.medium)
|
||||
var medium36 = UIFont.systemFont(ofSize: 36, weight:UIFont.Weight.medium)
|
||||
var medium40 = UIFont.systemFont(ofSize: 40, weight:UIFont.Weight.medium)
|
||||
var medium44 = UIFont.systemFont(ofSize: 44, weight:UIFont.Weight.medium)
|
||||
var light10 = UIFont.systemFont(ofSize: 10, weight:UIFont.Weight.light)
|
||||
var light12 = UIFont.systemFont(ofSize: 12, weight:UIFont.Weight.light)
|
||||
var light16 = UIFont.systemFont(ofSize: 16, weight:UIFont.Weight.light)
|
||||
var light17 = UIFont.systemFont(ofSize: 17, weight:UIFont.Weight.light)
|
||||
var bold12 = UIFont.systemFont(ofSize: 12, weight:UIFont.Weight.bold)
|
||||
var bold14 = UIFont.systemFont(ofSize: 14, weight:UIFont.Weight.bold)
|
||||
var bold16 = UIFont.systemFont(ofSize: 16, weight:UIFont.Weight.bold)
|
||||
var bold17 = UIFont.systemFont(ofSize: 17, weight:UIFont.Weight.bold)
|
||||
var bold18 = UIFont.systemFont(ofSize: 18, weight:UIFont.Weight.bold)
|
||||
var bold20 = UIFont.systemFont(ofSize: 20, weight:UIFont.Weight.bold)
|
||||
var bold22 = UIFont.systemFont(ofSize: 22, weight:UIFont.Weight.bold)
|
||||
var bold24 = UIFont.systemFont(ofSize: 24, weight:UIFont.Weight.bold)
|
||||
var bold28 = UIFont.systemFont(ofSize: 28, weight:UIFont.Weight.bold)
|
||||
var bold34 = UIFont.systemFont(ofSize: 34, weight:UIFont.Weight.bold)
|
||||
var bold36 = UIFont.systemFont(ofSize: 36, weight:UIFont.Weight.bold)
|
||||
var bold48 = UIFont.systemFont(ofSize: 48, weight:UIFont.Weight.bold)
|
||||
var header = UIFont.preferredFont(forTextStyle: .headline)
|
||||
var heavy17 = UIFont.systemFont(ofSize: 17, weight:UIFont.Weight.heavy)
|
||||
var heavy20 = UIFont.systemFont(ofSize: 20, weight:UIFont.Weight.heavy)
|
||||
var heavy32 = UIFont.systemFont(ofSize: 32, weight:UIFont.Weight.heavy)
|
||||
var heavy38 = UIFont.systemFont(ofSize: 38, weight:UIFont.Weight.heavy)
|
||||
var italic12 = UIFont.italicSystemFont(ofSize: 12)
|
||||
var italic16 = UIFont.italicSystemFont(ofSize: 16)
|
||||
var semibold12 = UIFont.systemFont(ofSize: 12, weight:UIFont.Weight.semibold)
|
||||
var semibold14 = UIFont.systemFont(ofSize: 14, weight:UIFont.Weight.semibold)
|
||||
var semibold15 = UIFont.systemFont(ofSize: 15, weight:UIFont.Weight.semibold)
|
||||
var semibold16 = UIFont.systemFont(ofSize: 16, weight:UIFont.Weight.semibold)
|
||||
var semibold18 = UIFont.systemFont(ofSize: 18, weight:UIFont.Weight.semibold)
|
||||
var semibold20 = UIFont.systemFont(ofSize: 20, weight:UIFont.Weight.semibold)
|
||||
}
|
||||
476
iphone/Maps/Core/Theme/GlobalStyleSheet.swift
Normal file
476
iphone/Maps/Core/Theme/GlobalStyleSheet.swift
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
enum GlobalStyleSheet: String, CaseIterable {
|
||||
case tableView = "TableView"
|
||||
case tableCell = "TableCell"
|
||||
case tableViewCell = "MWMTableViewCell"
|
||||
case defaultTableViewCell
|
||||
case tableViewHeaderFooterView = "TableViewHeaderFooterView"
|
||||
case defaultSearchBar
|
||||
case searchBar = "SearchBar"
|
||||
case navigationBar = "NavigationBar"
|
||||
case navigationBarItem = "NavigationBarItem"
|
||||
case checkmark = "Checkmark"
|
||||
case `switch` = "Switch"
|
||||
case pageControl = "PageControl"
|
||||
case starRatingView = "StarRatingView"
|
||||
case difficultyView = "DifficultyView"
|
||||
case divider = "Divider"
|
||||
case solidDivider = "SolidDivider"
|
||||
case background = "Background"
|
||||
case pressBackground = "PressBackground"
|
||||
case primaryBackground = "PrimaryBackground"
|
||||
case secondaryBackground = "SecondaryBackground"
|
||||
case menuBackground = "MenuBackground"
|
||||
case bottomTabBarButton = "BottomTabBarButton"
|
||||
case trackRecordingWidgetButton = "TrackRecordingWidgetButton"
|
||||
case blackOpaqueBackground = "BlackOpaqueBackground"
|
||||
case blueBackground = "BlueBackground"
|
||||
case fadeBackground = "FadeBackground"
|
||||
case errorBackground = "ErrorBackground"
|
||||
case blackStatusBarBackground = "BlackStatusBarBackground"
|
||||
case presentationBackground = "PresentationBackground"
|
||||
case clearBackground = "ClearBackground"
|
||||
case border = "Border"
|
||||
case tabView = "TabView"
|
||||
case dialogView = "DialogView"
|
||||
case alertView = "AlertView"
|
||||
case alertViewTextFieldContainer = "AlertViewTextFieldContainer"
|
||||
case alertViewTextField = "AlertViewTextField"
|
||||
case searchStatusBarView = "SearchStatusBarView"
|
||||
case flatNormalButton = "FlatNormalButton"
|
||||
case flatNormalButtonBig = "FlatNormalButtonBig"
|
||||
case flatNormalTransButton = "FlatNormalTransButton"
|
||||
case flatNormalTransButtonBig = "FlatNormalTransButtonBig"
|
||||
case flatGrayTransButton = "FlatGrayTransButton"
|
||||
case flatPrimaryTransButton = "FlatPrimaryTransButton"
|
||||
case flatRedTransButton = "FlatRedTransButton"
|
||||
case flatRedTransButtonBig = "FlatRedTransButtonBig"
|
||||
case flatRedButton = "FlatRedButton"
|
||||
case moreButton = "MoreButton"
|
||||
case editButton = "EditButton"
|
||||
case rateAppButton = "RateAppButton"
|
||||
case termsOfUseLinkText = "TermsOfUseLinkText"
|
||||
case termsOfUseGrayButton = "TermsOfUseGrayButton"
|
||||
case badge = "Badge"
|
||||
case blue = "MWMBlue"
|
||||
case black = "MWMBlack"
|
||||
case other = "MWMOther"
|
||||
case gray = "MWMGray"
|
||||
case separator = "MWMSeparator"
|
||||
case white = "MWMWhite"
|
||||
case datePickerView = "DatePickerView"
|
||||
case valueStepperView = "ValueStepperView"
|
||||
case grabber
|
||||
case modalSheetBackground
|
||||
case modalSheetContent
|
||||
case toastBackground
|
||||
case toastLabel
|
||||
}
|
||||
|
||||
extension GlobalStyleSheet: IStyleSheet {
|
||||
func styleResolverFor(colors: IColors, fonts: IFonts) -> Theme.StyleResolver {
|
||||
switch self {
|
||||
case .tableView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.separatorColor = colors.blackDividers
|
||||
s.exclusions = [String(describing: UIDatePicker.self)]
|
||||
}
|
||||
case .tableCell:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
s.tintColor = colors.linkBlue
|
||||
s.fontColorDetailed = colors.blackSecondaryText
|
||||
s.backgroundColorSelected = colors.pressBackground
|
||||
s.exclusions = [String(describing: UIDatePicker.self), "_UIActivityUserDefaultsActivityCell"]
|
||||
}
|
||||
case .tableViewCell:
|
||||
return .addFrom(Self.tableCell) { s in
|
||||
}
|
||||
case .defaultTableViewCell:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
}
|
||||
case .tableViewHeaderFooterView:
|
||||
return .add { s in
|
||||
s.font = fonts.medium14
|
||||
s.fontColor = colors.blackSecondaryText
|
||||
}
|
||||
case .defaultSearchBar:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.pressBackground
|
||||
s.barTintColor = colors.clear
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
s.fontColorDetailed = UIColor.white
|
||||
s.tintColor = colors.blackSecondaryText
|
||||
}
|
||||
case .searchBar:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.barTintColor = colors.primary
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
s.fontColorDetailed = UIColor.white
|
||||
s.tintColor = colors.blackSecondaryText
|
||||
}
|
||||
case .navigationBar:
|
||||
return .add { s in
|
||||
s.barTintColor = colors.primary
|
||||
s.tintColor = colors.whitePrimaryText
|
||||
s.backgroundImage = UIImage()
|
||||
s.shadowImage = UIImage()
|
||||
s.font = fonts.header
|
||||
s.fontColor = colors.whitePrimaryText
|
||||
}
|
||||
case .navigationBarItem:
|
||||
return .add { s in
|
||||
s.font = fonts.regular18
|
||||
s.fontColor = colors.whitePrimaryText
|
||||
s.fontColorDisabled = UIColor.lightGray
|
||||
s.fontColorHighlighted = colors.whitePrimaryTextHighlighted
|
||||
s.tintColor = colors.whitePrimaryText
|
||||
}
|
||||
case .checkmark:
|
||||
return .add { s in
|
||||
s.onTintColor = colors.linkBlue
|
||||
s.offTintColor = colors.blackHintText
|
||||
}
|
||||
case .switch:
|
||||
return .add { s in
|
||||
s.onTintColor = UIColor.accent
|
||||
}
|
||||
case .pageControl:
|
||||
return .add { s in
|
||||
s.pageIndicatorTintColor = colors.blackHintText
|
||||
s.currentPageIndicatorTintColor = colors.blackSecondaryText
|
||||
s.backgroundColor = colors.white
|
||||
}
|
||||
case .starRatingView:
|
||||
return .add { s in
|
||||
s.onTintColor = colors.ratingYellow
|
||||
s.offTintColor = colors.blackDividers
|
||||
}
|
||||
case .difficultyView:
|
||||
return .add { s in
|
||||
s.colors = [colors.blackSecondaryText, colors.ratingGreen, colors.ratingYellow, colors.ratingRed]
|
||||
s.offTintColor = colors.blackSecondaryText
|
||||
s.backgroundColor = colors.clear
|
||||
}
|
||||
case .divider:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.blackDividers
|
||||
}
|
||||
case .solidDivider:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.solidDividers
|
||||
}
|
||||
case .background:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.backgroundColorSelected = colors.pressBackground
|
||||
}
|
||||
case .pressBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.pressBackground
|
||||
}
|
||||
case .primaryBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.primary
|
||||
}
|
||||
case .secondaryBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.secondary
|
||||
}
|
||||
case .menuBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.menuBackground
|
||||
}
|
||||
case .bottomTabBarButton:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.tabBarButtonBackground
|
||||
s.tintColor = colors.blackSecondaryText
|
||||
s.coloring = MWMButtonColoring.black
|
||||
s.cornerRadius = .buttonDefault
|
||||
s.shadowColor = UIColor(0,0,0,alpha20)
|
||||
s.shadowOpacity = 1
|
||||
s.shadowOffset = CGSize(width: 0, height: 1)
|
||||
s.onTintColor = .red
|
||||
}
|
||||
case .trackRecordingWidgetButton:
|
||||
return .addFrom(Self.bottomTabBarButton) { s in
|
||||
s.cornerRadius = .custom(23)
|
||||
s.coloring = .red
|
||||
}
|
||||
case .blackOpaqueBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.blackOpaque
|
||||
}
|
||||
case .blueBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.linkBlue
|
||||
}
|
||||
case .fadeBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.fadeBackground
|
||||
}
|
||||
case .errorBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.errorPink
|
||||
}
|
||||
case .blackStatusBarBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.blackStatusBarBackground
|
||||
}
|
||||
case .presentationBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = UIColor.black.withAlphaComponent(alpha40)
|
||||
}
|
||||
case .clearBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.clear
|
||||
}
|
||||
case .border:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.border
|
||||
}
|
||||
case .tabView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.barTintColor = colors.white
|
||||
s.tintColor = colors.linkBlue
|
||||
s.fontColor = colors.blackSecondaryText
|
||||
s.fontColorHighlighted = colors.linkBlue
|
||||
s.font = fonts.medium14
|
||||
}
|
||||
case .dialogView:
|
||||
return .add { s in
|
||||
s.cornerRadius = .buttonDefault
|
||||
s.shadowRadius = 2
|
||||
s.shadowColor = UIColor(0,0,0,alpha26)
|
||||
s.shadowOpacity = 1
|
||||
s.shadowOffset = CGSize(width: 0, height: 1)
|
||||
s.backgroundColor = colors.white
|
||||
s.clip = true
|
||||
}
|
||||
case .alertView:
|
||||
return .add { s in
|
||||
s.cornerRadius = .modalSheet
|
||||
s.shadowRadius = 6
|
||||
s.shadowColor = UIColor(0,0,0,alpha20)
|
||||
s.shadowOpacity = 1
|
||||
s.shadowOffset = CGSize(width: 0, height: 3)
|
||||
s.backgroundColor = colors.alertBackground
|
||||
s.clip = true
|
||||
}
|
||||
case .alertViewTextFieldContainer:
|
||||
return .add { s in
|
||||
s.borderColor = colors.blackDividers
|
||||
s.borderWidth = 0.5
|
||||
s.backgroundColor = colors.white
|
||||
}
|
||||
case .alertViewTextField:
|
||||
return .add { s in
|
||||
s.font = fonts.regular14
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
s.tintColor = colors.blackSecondaryText
|
||||
}
|
||||
case .searchStatusBarView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.primary
|
||||
s.shadowRadius = 2
|
||||
s.shadowColor = colors.blackDividers
|
||||
s.shadowOpacity = 1
|
||||
s.shadowOffset = CGSize(width: 0, height: 0)
|
||||
}
|
||||
case .flatNormalButton:
|
||||
return .add { s in
|
||||
s.font = fonts.medium14
|
||||
s.cornerRadius = .buttonDefault
|
||||
s.clip = true
|
||||
s.fontColor = colors.whitePrimaryText
|
||||
s.backgroundColor = colors.linkBlue
|
||||
s.fontColorHighlighted = colors.whitePrimaryTextHighlighted
|
||||
s.fontColorDisabled = colors.whitePrimaryTextHighlighted
|
||||
s.backgroundColorHighlighted = colors.linkBlueHighlighted
|
||||
}
|
||||
case .flatNormalButtonBig:
|
||||
return .addFrom(Self.flatNormalButton) { s in
|
||||
s.font = fonts.regular17
|
||||
}
|
||||
case .flatNormalTransButton:
|
||||
return .add { s in
|
||||
s.font = fonts.medium14
|
||||
s.cornerRadius = .buttonDefault
|
||||
s.clip = true
|
||||
s.fontColor = colors.linkBlue
|
||||
s.backgroundColor = colors.clear
|
||||
s.fontColorHighlighted = colors.linkBlueHighlighted
|
||||
s.fontColorDisabled = colors.blackHintText
|
||||
s.backgroundColorHighlighted = colors.clear
|
||||
}
|
||||
case .flatNormalTransButtonBig:
|
||||
return .addFrom(Self.flatNormalTransButton) { s in
|
||||
s.font = fonts.regular17
|
||||
}
|
||||
case .flatGrayTransButton:
|
||||
return .add { s in
|
||||
s.font = fonts.medium14
|
||||
s.fontColor = colors.blackSecondaryText
|
||||
s.backgroundColor = colors.clear
|
||||
s.fontColorHighlighted = colors.linkBlueHighlighted
|
||||
}
|
||||
case .flatPrimaryTransButton:
|
||||
return .add { s in
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
s.backgroundColor = colors.clear
|
||||
s.fontColorHighlighted = colors.linkBlueHighlighted
|
||||
}
|
||||
case .flatRedTransButton:
|
||||
return .add { s in
|
||||
s.font = fonts.medium14
|
||||
s.fontColor = colors.red
|
||||
s.backgroundColor = colors.clear
|
||||
s.fontColorHighlighted = colors.red
|
||||
}
|
||||
case .flatRedTransButtonBig:
|
||||
return .add { s in
|
||||
s.font = fonts.regular17
|
||||
s.fontColor = colors.red
|
||||
s.backgroundColor = colors.clear
|
||||
s.fontColorHighlighted = colors.red
|
||||
}
|
||||
case .flatRedButton:
|
||||
return .add { s in
|
||||
s.font = fonts.medium14
|
||||
s.cornerRadius = .buttonDefault
|
||||
s.fontColor = colors.whitePrimaryText
|
||||
s.backgroundColor = colors.buttonRed
|
||||
s.fontColorHighlighted = colors.buttonRedHighlighted
|
||||
}
|
||||
case .moreButton:
|
||||
return .add { s in
|
||||
s.fontColor = colors.linkBlue
|
||||
s.fontColorHighlighted = colors.linkBlueHighlighted
|
||||
s.backgroundColor = colors.clear
|
||||
s.font = fonts.regular16
|
||||
}
|
||||
case .editButton:
|
||||
return .add { s in
|
||||
s.font = fonts.regular14
|
||||
s.fontColor = colors.linkBlue
|
||||
s.cornerRadius = .buttonDefault
|
||||
s.borderColor = colors.linkBlue
|
||||
s.borderWidth = 1
|
||||
s.fontColorHighlighted = colors.linkBlueHighlighted
|
||||
s.backgroundColor = colors.clear
|
||||
}
|
||||
case .rateAppButton:
|
||||
return .add { s in
|
||||
s.font = fonts.medium17
|
||||
s.fontColor = colors.linkBlue
|
||||
s.fontColorHighlighted = colors.white
|
||||
s.borderColor = colors.linkBlue
|
||||
s.cornerRadius = .buttonDefault
|
||||
s.borderWidth = 1
|
||||
s.backgroundColor = colors.clear
|
||||
s.backgroundColorHighlighted = colors.linkBlue
|
||||
}
|
||||
case .termsOfUseLinkText:
|
||||
return .add { s in
|
||||
s.font = fonts.regular16
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
|
||||
s.linkAttributes = [NSAttributedString.Key.font: fonts.regular16,
|
||||
NSAttributedString.Key.foregroundColor: colors.linkBlue,
|
||||
NSAttributedString.Key.underlineColor: UIColor.clear]
|
||||
s.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
}
|
||||
case .termsOfUseGrayButton:
|
||||
return .add { s in
|
||||
s.font = fonts.medium10
|
||||
s.fontColor = colors.blackSecondaryText
|
||||
s.fontColorHighlighted = colors.blackHintText
|
||||
}
|
||||
case .badge:
|
||||
return .add { s in
|
||||
s.round = true
|
||||
s.backgroundColor = colors.downloadBadgeBackground
|
||||
}
|
||||
case .blue:
|
||||
return .add { s in
|
||||
s.tintColor = colors.linkBlue
|
||||
s.coloring = MWMButtonColoring.blue
|
||||
}
|
||||
case .black:
|
||||
return .add { s in
|
||||
s.tintColor = colors.blackSecondaryText
|
||||
s.coloring = MWMButtonColoring.black
|
||||
}
|
||||
case .other:
|
||||
return .add { s in
|
||||
s.tintColor = colors.white
|
||||
s.coloring = MWMButtonColoring.other
|
||||
}
|
||||
case .gray:
|
||||
return .add { s in
|
||||
s.tintColor = colors.blackHintText
|
||||
s.coloring = MWMButtonColoring.gray
|
||||
}
|
||||
case .separator:
|
||||
return .add { s in
|
||||
s.tintColor = colors.blackDividers
|
||||
s.coloring = MWMButtonColoring.black
|
||||
}
|
||||
case .white:
|
||||
return .add { s in
|
||||
s.tintColor = colors.white
|
||||
s.coloring = MWMButtonColoring.white
|
||||
}
|
||||
case .datePickerView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
s.fontColorSelected = colors.whitePrimaryText
|
||||
s.backgroundColorSelected = colors.linkBlue
|
||||
s.backgroundColorHighlighted = colors.linkBlueHighlighted
|
||||
s.fontColorDisabled = colors.blackSecondaryText
|
||||
}
|
||||
case .valueStepperView:
|
||||
return .add { s in
|
||||
s.font = fonts.regular16
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
s.coloring = MWMButtonColoring.blue
|
||||
}
|
||||
case .grabber:
|
||||
return .addFrom(Self.background) { s in
|
||||
s.cornerRadius = .grabber
|
||||
}
|
||||
case .modalSheetBackground:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.shadowColor = UIColor.black
|
||||
s.shadowOffset = CGSize(width: 0, height: 1)
|
||||
s.shadowOpacity = 0.3
|
||||
s.shadowRadius = 6
|
||||
s.cornerRadius = .modalSheet
|
||||
s.clip = false
|
||||
s.maskedCorners = isiPad ? [] : [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
}
|
||||
case .modalSheetContent:
|
||||
return .addFrom(Self.modalSheetBackground) { s in
|
||||
s.backgroundColor = colors.clear
|
||||
s.clip = true
|
||||
}
|
||||
case .toastBackground:
|
||||
return .add { s in
|
||||
s.cornerRadius = .modalSheet
|
||||
s.clip = true
|
||||
}
|
||||
case .toastLabel:
|
||||
return .add { s in
|
||||
s.font = fonts.regular16
|
||||
s.fontColor = colors.whitePrimaryText
|
||||
s.textAlignment = .center
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
iphone/Maps/Core/Theme/MainTheme.swift
Normal file
12
iphone/Maps/Core/Theme/MainTheme.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
class MainTheme: Theme {
|
||||
override func register() {
|
||||
registerStyleSheet(GlobalStyleSheet.self)
|
||||
registerStyleSheet(PlacePageStyleSheet.self)
|
||||
registerStyleSheet(MapStyleSheet.self)
|
||||
registerStyleSheet(BookmarksStyleSheet.self)
|
||||
registerStyleSheet(SearchStyleSheet.self)
|
||||
registerStyleSheet(FontStyleSheet.self)
|
||||
registerStyleSheet(TextColorStyleSheet.self)
|
||||
}
|
||||
}
|
||||
|
||||
123
iphone/Maps/Core/Theme/MapStyleSheet.swift
Normal file
123
iphone/Maps/Core/Theme/MapStyleSheet.swift
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
enum MapStyleSheet: String, CaseIterable {
|
||||
case mapMenuButtonDisabled = "MenuButtonDisabled"
|
||||
case mapMenuButtonEnabled = "MenuButtonEnabled"
|
||||
case mapStreetNameBackgroundView = "StreetNameBackgroundView"
|
||||
case mapButtonZoomIn = "ButtonZoomIn"
|
||||
case mapButtonZoomOut = "ButtonZoomOut"
|
||||
case mapButtonPending = "ButtonPending"
|
||||
case mapButtonGetPosition = "ButtonGetPosition"
|
||||
case mapButtonFollow = "ButtonFollow"
|
||||
case mapButtonFollowAndRotate = "ButtonFollowAndRotate"
|
||||
case mapButtonMapBookmarks = "ButtonMapBookmarks"
|
||||
case mapPromoDiscoveryButton = "PromoDiscroveryButton"
|
||||
case mapButtonBookmarksBack = "ButtonBookmarksBack"
|
||||
case mapButtonBookmarksBackOpaque = "ButtonBookmarksBackOpaque"
|
||||
case mapFirstTurnView = "FirstTurnView"
|
||||
case mapSecondTurnView = "SecondTurnView"
|
||||
case mapAutoupdateView = "MapAutoupdateView"
|
||||
case mapGuidesNavigationBar = "GuidesNavigationBar"
|
||||
}
|
||||
|
||||
extension MapStyleSheet: IStyleSheet {
|
||||
func styleResolverFor(colors: IColors, fonts: IFonts) -> Theme.StyleResolver {
|
||||
switch self {
|
||||
case .mapMenuButtonDisabled:
|
||||
return .add { s in
|
||||
s.fontColor = colors.blackSecondaryText
|
||||
s.font = fonts.regular10
|
||||
s.backgroundColor = colors.clear
|
||||
s.borderColor = colors.clear
|
||||
s.borderWidth = 0
|
||||
s.cornerRadius = .buttonDefaultSmall
|
||||
}
|
||||
case .mapMenuButtonEnabled:
|
||||
return .add { s in
|
||||
s.fontColor = colors.linkBlue
|
||||
s.font = fonts.regular10
|
||||
s.backgroundColor = colors.linkBlue
|
||||
s.borderColor = colors.linkBlue
|
||||
s.borderWidth = 2
|
||||
s.cornerRadius = .buttonDefaultSmall
|
||||
}
|
||||
case .mapStreetNameBackgroundView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.shadowRadius = 2
|
||||
s.shadowColor = UIColor(0, 0, 0, alpha26)
|
||||
s.shadowOpacity = 1
|
||||
s.shadowOffset = CGSize(width: 0, height: 1)
|
||||
}
|
||||
case .mapButtonZoomIn:
|
||||
return .add { s in
|
||||
s.mwmImage = "btn_zoom_in"
|
||||
}
|
||||
case .mapButtonZoomOut:
|
||||
return .add { s in
|
||||
s.mwmImage = "btn_zoom_out"
|
||||
}
|
||||
case .mapButtonPending:
|
||||
return .add { s in
|
||||
s.mwmImage = "btn_pending"
|
||||
}
|
||||
case .mapButtonGetPosition:
|
||||
return .add { s in
|
||||
s.mwmImage = "btn_get_position"
|
||||
}
|
||||
case .mapButtonFollow:
|
||||
return .add { s in
|
||||
s.mwmImage = "btn_follow"
|
||||
}
|
||||
case .mapButtonFollowAndRotate:
|
||||
return .add { s in
|
||||
s.mwmImage = "btn_follow_and_rotate"
|
||||
}
|
||||
case .mapButtonMapBookmarks:
|
||||
return .add { s in
|
||||
s.mwmImage = "ic_routing_bookmark"
|
||||
}
|
||||
case .mapPromoDiscoveryButton:
|
||||
return .add { s in
|
||||
s.mwmImage = "promo_discovery_button"
|
||||
}
|
||||
case .mapButtonBookmarksBack:
|
||||
return .add { s in
|
||||
s.mwmImage = "btn_back"
|
||||
}
|
||||
case .mapButtonBookmarksBackOpaque:
|
||||
return .add { s in
|
||||
s.mwmImage = "btn_back_opaque"
|
||||
}
|
||||
case .mapFirstTurnView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.linkBlue
|
||||
s.cornerRadius = .buttonSmall
|
||||
s.shadowRadius = 2
|
||||
s.shadowColor = colors.shadow
|
||||
s.shadowOpacity = 0.2
|
||||
s.shadowOffset = CGSize(width: 0, height: 2)
|
||||
}
|
||||
case .mapSecondTurnView:
|
||||
return .addFrom(Self.mapFirstTurnView) { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.shadowColor = colors.blackPrimaryText
|
||||
}
|
||||
case .mapAutoupdateView:
|
||||
return .add { s in
|
||||
s.shadowOffset = CGSize(width: 0, height: 3)
|
||||
s.shadowRadius = 6
|
||||
s.cornerRadius = .buttonSmall
|
||||
s.shadowOpacity = 1
|
||||
s.backgroundColor = colors.white
|
||||
}
|
||||
case .mapGuidesNavigationBar:
|
||||
return .add { s in
|
||||
s.barTintColor = colors.white
|
||||
s.tintColor = colors.linkBlue
|
||||
s.backgroundImage = UIImage()
|
||||
s.shadowImage = UIImage()
|
||||
s.font = fonts.regular18
|
||||
s.fontColor = colors.blackPrimaryText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
191
iphone/Maps/Core/Theme/PlacePageStyleSheet.swift
Normal file
191
iphone/Maps/Core/Theme/PlacePageStyleSheet.swift
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
enum PlacePageStyleSheet: String, CaseIterable {
|
||||
case ppTitlePopularView = "PPTitlePopularView"
|
||||
case ppActionBarTitle = "PPActionBarTitle"
|
||||
case ppActionBarTitlePartner = "PPActionBarTitlePartner"
|
||||
case ppElevationProfileDescriptionCell = "ElevationProfileDescriptionCell"
|
||||
case ppElevationProfileExtendedDifficulty = "ElevationProfileExtendedDifficulty"
|
||||
case ppRouteBasePreview = "RouteBasePreview"
|
||||
case ppRoutePreview = "RoutePreview"
|
||||
case ppRatingSummaryView24 = "RatingSummaryView24"
|
||||
case ppRatingSummaryView12 = "RatingSummaryView12"
|
||||
case ppRatingSummaryView12User = "RatingSummaryView12User"
|
||||
case ppHeaderView = "PPHeaderView"
|
||||
case ppNavigationShadowView = "PPNavigationShadowView"
|
||||
case ppBackgroundView = "PPBackgroundView"
|
||||
case ppView = "PPView"
|
||||
case ppHeaderCircleIcon = "PPHeaderCircleIcon"
|
||||
case ppChartView = "ChartView"
|
||||
case ppRatingView = "PPRatingView"
|
||||
case ppRatingHorrible = "PPRatingHorrible"
|
||||
case ppRatingBad = "PPRatingBad"
|
||||
case ppRatingNormal = "PPRatingNormal"
|
||||
case ppRatingGood = "PPRatingGood"
|
||||
case ppRatingExcellent = "PPRatingExellent"
|
||||
case ppButton = "PPButton"
|
||||
}
|
||||
|
||||
extension PlacePageStyleSheet: IStyleSheet {
|
||||
func styleResolverFor(colors: IColors, fonts: IFonts) -> Theme.StyleResolver {
|
||||
switch self {
|
||||
case .ppTitlePopularView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.linkBlueHighlighted
|
||||
s.cornerRadius = .custom(10)
|
||||
}
|
||||
case .ppActionBarTitle:
|
||||
return .add { s in
|
||||
s.font = fonts.regular10
|
||||
s.fontColor = colors.blackSecondaryText
|
||||
}
|
||||
case .ppActionBarTitlePartner:
|
||||
return .add { s in
|
||||
s.font = fonts.regular10
|
||||
s.fontColor = UIColor.white
|
||||
}
|
||||
case .ppElevationProfileDescriptionCell:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.blackOpaque
|
||||
s.cornerRadius = .buttonDefault
|
||||
}
|
||||
case .ppElevationProfileExtendedDifficulty:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.blackSecondaryText
|
||||
s.fontColor = colors.white
|
||||
s.font = fonts.medium14
|
||||
s.textContainerInset = UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6)
|
||||
}
|
||||
case .ppRouteBasePreview:
|
||||
return .add { s in
|
||||
s.borderColor = colors.blackDividers
|
||||
s.borderWidth = 1
|
||||
s.backgroundColor = colors.white
|
||||
}
|
||||
case .ppRoutePreview:
|
||||
return .add { s in
|
||||
s.shadowRadius = 2
|
||||
s.shadowColor = colors.blackDividers
|
||||
s.shadowOpacity = 1
|
||||
s.shadowOffset = CGSize(width: 3, height: 0)
|
||||
s.backgroundColor = colors.pressBackground
|
||||
}
|
||||
case .ppRatingSummaryView24:
|
||||
return .add { s in
|
||||
s.font = fonts.bold16
|
||||
s.fontColorHighlighted = colors.ratingYellow
|
||||
s.fontColorDisabled = colors.blackDividers
|
||||
s.colors = [
|
||||
colors.blackSecondaryText,
|
||||
colors.ratingRed,
|
||||
colors.ratingOrange,
|
||||
colors.ratingYellow,
|
||||
colors.ratingLightGreen,
|
||||
colors.ratingGreen
|
||||
]
|
||||
s.images = [
|
||||
"ic_24px_rating_normal",
|
||||
"ic_24px_rating_horrible",
|
||||
"ic_24px_rating_bad",
|
||||
"ic_24px_rating_normal",
|
||||
"ic_24px_rating_good",
|
||||
"ic_24px_rating_excellent"
|
||||
]
|
||||
}
|
||||
case .ppRatingSummaryView12:
|
||||
return .addFrom(Self.ppRatingSummaryView24) { s in
|
||||
s.font = fonts.bold12
|
||||
s.images = [
|
||||
"ic_12px_rating_normal",
|
||||
"ic_12px_rating_horrible",
|
||||
"ic_12px_rating_bad",
|
||||
"ic_12px_rating_normal",
|
||||
"ic_12px_rating_good",
|
||||
"ic_12px_rating_excellent"
|
||||
]
|
||||
}
|
||||
case .ppRatingSummaryView12User:
|
||||
return .addFrom(Self.ppRatingSummaryView12) { s in
|
||||
s.colors?[0] = colors.linkBlue
|
||||
s.images?[0] = "ic_12px_radio_on"
|
||||
}
|
||||
case .ppHeaderView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.cornerRadius = .modalSheet
|
||||
s.clip = true
|
||||
}
|
||||
case .ppNavigationShadowView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.shadowColor = UIColor.black
|
||||
s.shadowOffset = CGSize(width: 0, height: 1)
|
||||
s.shadowOpacity = 0.4
|
||||
s.shadowRadius = 1
|
||||
s.clip = false
|
||||
}
|
||||
case .ppBackgroundView:
|
||||
return .addFrom(GlobalStyleSheet.modalSheetBackground) { s in
|
||||
s.backgroundColor = colors.pressBackground
|
||||
s.maskedCorners = isiPad ? CACornerMask.all : [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
s.clip = false
|
||||
}
|
||||
case .ppView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.clear
|
||||
s.cornerRadius = .modalSheet
|
||||
s.clip = true
|
||||
}
|
||||
case .ppHeaderCircleIcon:
|
||||
return .add { s in
|
||||
s.tintColor = colors.iconOpaqueGrayTint
|
||||
s.backgroundColor = colors.iconOpaqueGrayBackground
|
||||
}
|
||||
case .ppChartView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.white
|
||||
s.fontColor = colors.blackSecondaryText
|
||||
s.font = fonts.regular12
|
||||
s.gridColor = colors.blackDividers
|
||||
s.previewSelectorColor = colors.elevationPreviewSelector
|
||||
s.previewTintColor = colors.elevationPreviewTint
|
||||
s.shadowOpacity = 0.25
|
||||
s.shadowColor = colors.shadow
|
||||
s.infoBackground = colors.pressBackground
|
||||
}
|
||||
case .ppRatingView:
|
||||
return .add { s in
|
||||
s.backgroundColor = colors.blackOpaque
|
||||
s.round = true
|
||||
}
|
||||
case .ppRatingHorrible:
|
||||
return .add { s in
|
||||
s.image = "ic_24px_rating_horrible"
|
||||
s.tintColor = colors.ratingRed
|
||||
}
|
||||
case .ppRatingBad:
|
||||
return .add { s in
|
||||
s.image = "ic_24px_rating_bad"
|
||||
s.tintColor = colors.ratingOrange
|
||||
}
|
||||
case .ppRatingNormal:
|
||||
return .add { s in
|
||||
s.image = "ic_24px_rating_normal"
|
||||
s.tintColor = colors.ratingYellow
|
||||
}
|
||||
case .ppRatingGood:
|
||||
return .add { s in
|
||||
s.image = "ic_24px_rating_good"
|
||||
s.tintColor = colors.ratingLightGreen
|
||||
}
|
||||
case .ppRatingExcellent:
|
||||
return .add { s in
|
||||
s.image = "ic_24px_rating_excellent"
|
||||
s.tintColor = colors.ratingGreen
|
||||
}
|
||||
case .ppButton:
|
||||
return .addFrom(GlobalStyleSheet.flatNormalTransButtonBig) { s in
|
||||
s.borderColor = colors.linkBlue
|
||||
s.borderWidth = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
extension BottomMenuLayerButton {
|
||||
@objc override func applyTheme() {
|
||||
for style in StyleManager.shared.getStyle(styleName)
|
||||
where !style.isEmpty && !style.hasExclusion(view: self) {
|
||||
BottomMenuLayerButtonRenderer.render(self, style: style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BottomMenuLayerButtonRenderer {
|
||||
class func render(_ control: BottomMenuLayerButton, style: Style) {
|
||||
if let font = style.font {
|
||||
control.titleLabel.font = font
|
||||
}
|
||||
|
||||
if let fontColor = style.fontColor {
|
||||
control.titleLabel.textColor = fontColor
|
||||
}
|
||||
|
||||
UIImageViewRenderer.render(control.imageView, style: style)
|
||||
}
|
||||
}
|
||||
26
iphone/Maps/Core/Theme/Renderers/ChartViewRenderer.swift
Normal file
26
iphone/Maps/Core/Theme/Renderers/ChartViewRenderer.swift
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import Chart
|
||||
|
||||
extension ChartView {
|
||||
override func applyTheme() {
|
||||
if styleName.isEmpty {
|
||||
setStyle(.ppChartView)
|
||||
}
|
||||
for style in StyleManager.shared.getStyle(styleName) where !style.isEmpty && !style.hasExclusion(view: self) {
|
||||
ChartViewRenderer.render(self, style: style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate final class ChartViewRenderer {
|
||||
class func render(_ control: ChartView, style: Style) {
|
||||
control.backgroundColor = style.backgroundColor
|
||||
control.textColor = style.fontColor!
|
||||
control.font = style.font!
|
||||
control.gridColor = style.gridColor!
|
||||
control.previewSelectorColor = style.previewSelectorColor!
|
||||
control.previewTintColor = style.previewTintColor!
|
||||
control.infoBackgroundColor = style.infoBackground!
|
||||
control.infoShadowColor = style.shadowColor!
|
||||
control.infoShadowOpacity = style.shadowOpacity!
|
||||
}
|
||||
}
|
||||
25
iphone/Maps/Core/Theme/Renderers/CheckmarkRenderer.swift
Normal file
25
iphone/Maps/Core/Theme/Renderers/CheckmarkRenderer.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import Foundation
|
||||
extension Checkmark {
|
||||
@objc override func applyTheme() {
|
||||
if styleName.isEmpty {
|
||||
setStyle(.checkmark)
|
||||
}
|
||||
for style in StyleManager.shared.getStyle(styleName)
|
||||
where !style.isEmpty && !style.hasExclusion(view: self) {
|
||||
CheckmarkRenderer.render(self, style: style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CheckmarkRenderer {
|
||||
class func render(_ control: Checkmark, style: Style) {
|
||||
if let onTintColor = style.onTintColor {
|
||||
control.onTintColor = onTintColor
|
||||
}
|
||||
|
||||
if let offTintColor = style.offTintColor {
|
||||
control.offTintColor = offTintColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import DatePicker
|
||||
|
||||
extension DatePickerView {
|
||||
override func applyTheme() {
|
||||
if styleName.isEmpty {
|
||||
setStyle(.datePickerView)
|
||||
}
|
||||
for style in StyleManager.shared.getStyle(styleName) where !style.isEmpty && !style.hasExclusion(view: self) {
|
||||
DatePickerViewRenderer.render(self, style: style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate final class DatePickerViewRenderer {
|
||||
class func render(_ control: DatePickerView, style: Style) {
|
||||
control.backgroundColor = style.backgroundColor
|
||||
|
||||
var theme = DatePickerViewTheme()
|
||||
theme.monthHeaderBackgroundColor = style.backgroundColor!
|
||||
theme.monthHeaderColor = style.fontColorDisabled!
|
||||
theme.weekdaySymbolsColor = style.fontColorDisabled!
|
||||
theme.dayColor = style.fontColor!
|
||||
theme.selectedDayColor = style.fontColorSelected!
|
||||
theme.selectedDayBackgroundColor = style.backgroundColorSelected!
|
||||
theme.selectedRangeBackgroundColor = style.backgroundColorHighlighted!
|
||||
theme.inactiveDayColor = style.fontColorDisabled!
|
||||
control.theme = theme
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import Foundation
|
||||
extension DifficultyView {
|
||||
@objc override func applyTheme() {
|
||||
if styleName.isEmpty {
|
||||
setStyle(.difficultyView)
|
||||
}
|
||||
for style in StyleManager.shared.getStyle(styleName)
|
||||
where !style.isEmpty && !style.hasExclusion(view: self) {
|
||||
DifficultyViewRenderer.render(self, style: style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DifficultyViewRenderer: UIViewRenderer {
|
||||
class func render(_ control: DifficultyView, style: Style) {
|
||||
super.render(control, style: style)
|
||||
if let colors = style.colors {
|
||||
control.colors = colors
|
||||
}
|
||||
if let emptyColor = style.offTintColor {
|
||||
control.emptyColor = emptyColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
iphone/Maps/Core/Theme/Renderers/InsetsLabelRenderer.swift
Normal file
17
iphone/Maps/Core/Theme/Renderers/InsetsLabelRenderer.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
extension InsetsLabel {
|
||||
@objc override func applyTheme() {
|
||||
for style in StyleManager.shared.getStyle(styleName)
|
||||
where !style.isEmpty && !style.hasExclusion(view: self) {
|
||||
InsetsLabelRenderer.render(self, style: style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InsetsLabelRenderer: UILabelRenderer {
|
||||
class func render(_ control: InsetsLabel, style: Style) {
|
||||
super.render(control, style: style)
|
||||
if let insets = style.textContainerInset {
|
||||
control.insets = insets
|
||||
}
|
||||
}
|
||||
}
|
||||
21
iphone/Maps/Core/Theme/Renderers/MWMButtonRenderer.swift
Normal file
21
iphone/Maps/Core/Theme/Renderers/MWMButtonRenderer.swift
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
extension MWMButton {
|
||||
@objc override func applyTheme() {
|
||||
for style in StyleManager.shared.getStyle(styleName)
|
||||
where !style.isEmpty && !style.hasExclusion(view: self) {
|
||||
MWMButtonRenderer.render(self, style: style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MWMButtonRenderer {
|
||||
class func render(_ control: MWMButton, style: Style) {
|
||||
UIButtonRenderer.render(control, style: style)
|
||||
if let coloring = style.coloring {
|
||||
control.coloring = coloring
|
||||
}
|
||||
if let imageName = style.mwmImage {
|
||||
control.imageName = imageName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import Foundation
|
||||
extension MWMTableViewCell {
|
||||
@objc override func applyTheme() {
|
||||
if styleName.isEmpty {
|
||||
setStyle(.tableViewCell)
|
||||
}
|
||||
for style in StyleManager.shared.getStyle(styleName)
|
||||
where !style.isEmpty && !style.hasExclusion(view: self) {
|
||||
MWMTableViewCellRenderer.render(self, style: style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MWMTableViewCellRenderer: UITableViewCellRenderer {
|
||||
class func render(_ control: MWMTableViewCell, style: Style) {
|
||||
super.render(control, style: style)
|
||||
}
|
||||
}
|
||||
33
iphone/Maps/Core/Theme/Renderers/TabViewRenderer.swift
Normal file
33
iphone/Maps/Core/Theme/Renderers/TabViewRenderer.swift
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
extension TabView {
|
||||
@objc override func applyTheme() {
|
||||
if styleName.isEmpty {
|
||||
setStyle(.tabView)
|
||||
}
|
||||
for style in StyleManager.shared.getStyle(styleName)
|
||||
where !style.isEmpty && !style.hasExclusion(view: self) {
|
||||
TabViewRenderer.render(self, style: style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TabViewRenderer {
|
||||
class func render(_ control: TabView, style: Style) {
|
||||
if let backgroundColor = style.backgroundColor {
|
||||
control.backgroundColor = backgroundColor
|
||||
}
|
||||
if let barTintColor = style.barTintColor {
|
||||
control.barTintColor = barTintColor
|
||||
}
|
||||
if let tintColor = style.tintColor {
|
||||
control.tintColor = tintColor
|
||||
}
|
||||
if let font = style.font, let fontColor = style.fontColorHighlighted {
|
||||
control.selectedHeaderTextAttributes = [.foregroundColor: fontColor,
|
||||
.font: font]
|
||||
}
|
||||
if let font = style.font, let fontColor = style.fontColor {
|
||||
control.deselectedHeaderTextAttributes = [.foregroundColor: fontColor,
|
||||
.font: font]
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue