Repo created

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

View file

@ -0,0 +1,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")
}
}
}

View file

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

View file

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

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
@interface MWMEditorHelper : NSObject
+ (void)uploadEdits:(void (^)(UIBackgroundFetchResult))completionHandler;
@end

View 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

View file

@ -0,0 +1,60 @@
import Foundation
// TODO: define directly T as AnyObject after Swift version update
final class ListenerContainer<T> {
// MARK: - WeakWrapper for listeners
private class WeakWrapper<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?)
}

View 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

View 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

View file

@ -0,0 +1,3 @@
@protocol MWMFrameworkObserver<NSObject>
@end

View 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

View file

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

View file

@ -0,0 +1,19 @@
typedef NS_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);

View file

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

View file

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

View file

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

View file

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

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

View 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

View file

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

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,7 @@
typedef NS_CLOSED_ENUM(NSUInteger, MWMMyPositionMode) {
MWMMyPositionModePendingPosition,
MWMMyPositionModeNotFollowNoPosition,
MWMMyPositionModeNotFollow,
MWMMyPositionModeFollow,
MWMMyPositionModeFollowAndRotate
};

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

View 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

View 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

View 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

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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,3 @@
typedef NS_ENUM(NSUInteger, MWMRouterRecommendation) {
MWMRouterRecommendationRebuildAfterPointsLoading
};

View 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

View 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

View file

@ -0,0 +1,9 @@
typedef NS_CLOSED_ENUM(NSUInteger, MWMRouterTransitType) {
MWMRouterTransitTypeIntermediatePoint,
MWMRouterTransitTypePedestrian,
MWMRouterTransitTypeSubway,
MWMRouterTransitTypeTrain,
MWMRouterTransitTypeLightRail,
MWMRouterTransitTypeMonorail,
MWMRouterTransitTypeRuler
};

View file

@ -0,0 +1,7 @@
typedef NS_ENUM(NSUInteger, MWMRouterType) {
MWMRouterTypeVehicle,
MWMRouterTypePedestrian,
MWMRouterTypePublicTransport,
MWMRouterTypeBicycle,
MWMRouterTypeRuler,
};

View file

@ -0,0 +1,7 @@
#import "MWMSearch.h"
@interface MWMSearch (CoreSpotlight)
+ (void)addCategoriesToSpotlight;
@end

View 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

View 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

View 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

View file

@ -0,0 +1,8 @@
@protocol MWMSearchObserver<NSObject>
@optional
- (void)onSearchStarted;
- (void)onSearchCompleted;
- (void)onSearchResultsUpdated;
@end

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

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

View file

@ -0,0 +1,4 @@
typedef NS_ENUM(NSInteger, SearchItemType) {
SearchItemTypeRegular,
SearchItemTypeSuggestion
};

View 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

View 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

View 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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,5 @@
@protocol MWMTextToSpeechObserver<NSObject>
- (void)onTTSStatusUpdated;
@end

View file

@ -0,0 +1,10 @@
NS_ASSUME_NONNULL_BEGIN
@interface TTSTester : NSObject
- (void)playRandomTestString;
- (NSArray<NSString *> *)getTestStrings:(NSString *)language;
@end
NS_ASSUME_NONNULL_END

View 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

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

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

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

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

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

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

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

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

View file

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

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

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

View file

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

View file

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

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

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

View file

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

View 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