[ios] Red for over speed; Speed limit for Carplay #3047
|
@ -1202,7 +1202,7 @@ Java_com_mapswithme_maps_Framework_nativeGetRouteFollowingInfo(JNIEnv * env, jcl
|
|||
}
|
||||
|
||||
auto const & rm = frm()->GetRoutingManager();
|
||||
auto const isSpeedLimitExceeded = rm.IsRoutingActive() ? rm.IsSpeedLimitExceeded() : false;
|
||||
auto const isSpeedCamLimitExceeded = rm.IsRoutingActive() ? rm.IsSpeedCamLimitExceeded() : false;
|
||||
auto const shouldPlaySignal = frm()->GetRoutingManager().GetSpeedCamManager().ShouldPlayBeepSignal();
|
||||
jobject const result = env->NewObject(
|
||||
klass, ctorRouteInfoID, jni::ToJavaString(env, info.m_distToTarget),
|
||||
|
@ -1210,7 +1210,7 @@ Java_com_mapswithme_maps_Framework_nativeGetRouteFollowingInfo(JNIEnv * env, jcl
|
|||
jni::ToJavaString(env, info.m_turnUnitsSuffix), jni::ToJavaString(env, info.m_sourceName),
|
||||
jni::ToJavaString(env, info.m_displayedStreetName), info.m_completionPercent, info.m_turn,
|
||||
info.m_nextTurn, info.m_pedestrianTurn, info.m_exitNum, info.m_time, jLanes,
|
||||
static_cast<jboolean>(isSpeedLimitExceeded), static_cast<jboolean>(shouldPlaySignal));
|
||||
static_cast<jboolean>(isSpeedCamLimitExceeded), static_cast<jboolean>(shouldPlaySignal));
|
||||
ASSERT(result, (jni::DescribeException()));
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,17 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface Measure : NSObject
|
||||
|
||||
@property(nonatomic, readonly) double value;
|
||||
@property(nonatomic, readonly) NSString* valueAsString;
|
||||
|
||||
@property(nonatomic, readonly) NSString* unit;
|
||||
|
||||
- (instancetype) initAsSpeed:(double) mps;
|
||||
|
||||
@end
|
||||
|
||||
NS_SWIFT_NAME(GeoUtil)
|
||||
@interface MWMGeoUtil : NSObject
|
||||
|
||||
|
|
|
@ -3,6 +3,54 @@
|
|||
#include "geometry/mercator.hpp"
|
||||
#include "geometry/angles.hpp"
|
||||
|
||||
#include "platform/localization.hpp"
|
||||
#include "platform/settings.hpp"
|
||||
#include "platform/measurement_utils.hpp"
|
||||
|
||||
@implementation Measure
|
||||
|
||||
// Alternative native implementation.
|
||||
// It has the issue: some localized unit are too long even in .short style. E.g. speed for RU.
|
||||
/*
|
||||
let imperial = Settings.measurementUnits() == .imperial
|
||||
var speedMeasurement = Measurement(value: speed, unit: UnitSpeed.metersPerSecond)
|
||||
speedMeasurement.convert(to: imperial ? UnitSpeed.milesPerHour : UnitSpeed.kilometersPerHour)
|
||||
|
||||
let formatter = MeasurementFormatter()
|
||||
formatter.unitOptions = .providedUnit
|
||||
formatter.numberFormatter.maximumFractionDigits = 0
|
||||
formatter.unitStyle = .short
|
||||
|
||||
if speedMeasurement.value < 10
|
||||
{
|
||||
formatter.numberFormatter.minimumFractionDigits = 1
|
||||
formatter.numberFormatter.maximumFractionDigits = 1
|
||||
}
|
||||
|
||||
let speedString = formatter.string(from: speedMeasurement)
|
||||
*/
|
||||
|
||||
- (NSString*) valueAsString {
|
||||
if (self.value > 9.999)
|
||||
return [NSString stringWithFormat:@"%.0f", self.value];
|
||||
else
|
||||
return [NSString stringWithFormat:@"%.1f", self.value];
|
||||
}
|
||||
|
||||
- (instancetype)initAsSpeed:(double) mps {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
auto units = measurement_utils::Units::Metric;
|
||||
settings::TryGet(settings::kMeasurementUnits, units);
|
||||
_value = measurement_utils::MpsToUnits(mps, units);
|
||||
|
||||
_unit = @(platform::GetLocalizedSpeedUnits(units).c_str());
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMGeoUtil
|
||||
|
||||
+ (float)angleAtPoint:(CLLocationCoordinate2D)p1 toPoint:(CLLocationCoordinate2D)p2 {
|
||||
|
|
|
@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
+ (UIColor *)opentableBackground;
|
||||
+ (UIColor *)transparentGreen;
|
||||
+ (UIColor *)speedLimitRed;
|
||||
+ (UIColor *)speedLimitGeen;
|
||||
+ (UIColor *)speedLimitGreen;
|
||||
+ (UIColor *)speedLimitWhite;
|
||||
+ (UIColor *)speedLimitLightGray;
|
||||
+ (UIColor *)speedLimitDarkGray;
|
||||
|
|
|
@ -259,7 +259,7 @@ static NSDictionary<NSString *, UIColor *> *night;
|
|||
return [UIColor colorWithRed:scaled(224) green:scaled(31) blue:scaled(31) alpha:alpha100];
|
||||
}
|
||||
|
||||
+ (UIColor *)speedLimitGeen {
|
||||
+ (UIColor *)speedLimitGreen {
|
||||
return [UIColor colorWithRed:scaled(1) green:scaled(104) blue:scaled(44) alpha:alpha100];
|
||||
}
|
||||
|
||||
|
|
|
@ -22,29 +22,29 @@ final class CarPlayRouter: NSObject {
|
|||
var speedCameraMode: SpeedCameraManagerMode {
|
||||
return RoutingManager.routingManager.speedCameraMode
|
||||
}
|
||||
|
||||
|
||||
override init() {
|
||||
listenerContainer = ListenerContainer<CarPlayRouterListener>()
|
||||
initialSpeedCamSettings = RoutingManager.routingManager.speedCameraMode
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
||||
func addListener(_ listener: CarPlayRouterListener) {
|
||||
listenerContainer.addListener(listener)
|
||||
}
|
||||
|
||||
|
||||
func removeListener(_ listener: CarPlayRouterListener) {
|
||||
listenerContainer.removeListener(listener)
|
||||
}
|
||||
|
||||
|
||||
func subscribeToEvents() {
|
||||
RoutingManager.routingManager.add(self)
|
||||
}
|
||||
|
||||
|
||||
func unsubscribeFromEvents() {
|
||||
RoutingManager.routingManager.remove(self)
|
||||
}
|
||||
|
||||
|
||||
func completeRouteAndRemovePoints() {
|
||||
let manager = RoutingManager.routingManager
|
||||
manager.stopRoutingAndRemoveRoutePoints(true)
|
||||
|
@ -52,7 +52,7 @@ final class CarPlayRouter: NSObject {
|
|||
manager.apply(routeType: .vehicle)
|
||||
previewTrip = nil
|
||||
}
|
||||
|
||||
|
||||
func rebuildRoute() {
|
||||
guard let trip = previewTrip else { return }
|
||||
do {
|
||||
|
@ -64,7 +64,7 @@ final class CarPlayRouter: NSObject {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func buildRoute(trip: CPTrip) {
|
||||
completeRouteAndRemovePoints()
|
||||
previewTrip = trip
|
||||
|
@ -87,11 +87,11 @@ final class CarPlayRouter: NSObject {
|
|||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let manager = RoutingManager.routingManager
|
||||
manager.add(routePoint: startPoint)
|
||||
manager.add(routePoint: endPoint)
|
||||
|
||||
|
||||
do {
|
||||
try manager.buildRoute()
|
||||
} catch let error as NSError {
|
||||
|
@ -101,7 +101,7 @@ final class CarPlayRouter: NSObject {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateStartPointAndRebuild(trip: CPTrip) {
|
||||
let manager = RoutingManager.routingManager
|
||||
previewTrip = trip
|
||||
|
@ -128,27 +128,27 @@ final class CarPlayRouter: NSObject {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func startRoute() {
|
||||
let manager = RoutingManager.routingManager
|
||||
manager.startRoute()
|
||||
}
|
||||
|
||||
|
||||
func setupCarPlaySpeedCameraMode() {
|
||||
if case .auto = initialSpeedCamSettings {
|
||||
RoutingManager.routingManager.speedCameraMode = .always
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func setupInitialSpeedCameraMode() {
|
||||
RoutingManager.routingManager.speedCameraMode = initialSpeedCamSettings
|
||||
}
|
||||
|
||||
|
||||
func updateSpeedCameraMode(_ mode: SpeedCameraManagerMode) {
|
||||
initialSpeedCamSettings = mode
|
||||
RoutingManager.routingManager.speedCameraMode = mode
|
||||
}
|
||||
|
||||
|
||||
func restoreTripPreviewOnCarplay(beforeRootTemplateDidAppear: Bool) {
|
||||
guard MWMRouter.isRestoreProcessCompleted() else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
|
@ -182,7 +182,7 @@ final class CarPlayRouter: NSObject {
|
|||
CarPlayService.shared.preparePreview(trips: [trip])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func restoredNavigationSession() -> (CPTrip, RouteInfo)? {
|
||||
let manager = RoutingManager.routingManager
|
||||
if manager.isOnRoute,
|
||||
|
@ -211,21 +211,21 @@ extension CarPlayRouter {
|
|||
self?.updateUpcomingManeuvers()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func cancelTrip() {
|
||||
routeSession?.cancelTrip()
|
||||
routeSession = nil
|
||||
completeRouteAndRemovePoints()
|
||||
RoutingManager.routingManager.resetOnNewTurnCallback()
|
||||
}
|
||||
|
||||
|
||||
func finishTrip() {
|
||||
routeSession?.finishTrip()
|
||||
routeSession = nil
|
||||
completeRouteAndRemovePoints()
|
||||
RoutingManager.routingManager.resetOnNewTurnCallback()
|
||||
}
|
||||
|
||||
|
||||
func updateUpcomingManeuvers() {
|
||||
let maneuvers = createUpcomingManeuvers()
|
||||
routeSession?.upcomingManeuvers = maneuvers
|
||||
|
@ -249,7 +249,7 @@ extension CarPlayRouter {
|
|||
let measurement = Measurement(value: distance, unit: routeInfo.turnUnits)
|
||||
return CPTravelEstimates(distanceRemaining: measurement, timeRemaining: 0.0)
|
||||
}
|
||||
|
||||
|
||||
private func createUpcomingManeuvers() -> [CPManeuver] {
|
||||
guard let routeInfo = RoutingManager.routingManager.routeInfo else {
|
||||
return []
|
||||
|
@ -286,7 +286,7 @@ extension CarPlayRouter {
|
|||
}
|
||||
return maneuvers
|
||||
}
|
||||
|
||||
|
||||
func createTrip(startPoint: MWMRoutePoint, endPoint: MWMRoutePoint, routeInfo: RouteInfo? = nil) -> CPTrip {
|
||||
let startPlacemark = MKPlacemark(coordinate: CLLocationCoordinate2D(latitude: startPoint.latitude,
|
||||
longitude: startPoint.longitude))
|
||||
|
@ -296,10 +296,10 @@ extension CarPlayRouter {
|
|||
let startItem = MKMapItem(placemark: startPlacemark)
|
||||
let endItem = MKMapItem(placemark: endPlacemark)
|
||||
endItem.name = endPoint.title
|
||||
|
||||
|
||||
let routeChoice = CPRouteChoice(summaryVariants: [" "], additionalInformationVariants: [], selectionSummaryVariants: [])
|
||||
routeChoice.userInfo = routeInfo
|
||||
|
||||
|
||||
let trip = CPTrip(origin: startItem, destination: endItem, routeChoices: [routeChoice])
|
||||
trip.userInfo = [CPConstants.Trip.start: startPoint, CPConstants.Trip.end: endPoint]
|
||||
return trip
|
||||
|
@ -308,10 +308,10 @@ extension CarPlayRouter {
|
|||
|
||||
// MARK: - RoutingManagerListener implementation
|
||||
extension CarPlayRouter: RoutingManagerListener {
|
||||
func updateCameraInfo(isCameraOnRoute: Bool, speedLimit limit: String?) {
|
||||
CarPlayService.shared.updateCameraUI(isCameraOnRoute: isCameraOnRoute, speedLimit: limit)
|
||||
func updateCameraInfo(isCameraOnRoute: Bool, speedLimitMps limit: Double) {
|
||||
CarPlayService.shared.updateCameraUI(isCameraOnRoute: isCameraOnRoute, speedLimitMps: limit < 0 ? nil : limit)
|
||||
}
|
||||
|
||||
|
||||
func processRouteBuilderEvent(with code: RouterResultCode, countries: [String]) {
|
||||
guard let trip = previewTrip else {
|
||||
return
|
||||
|
@ -345,10 +345,10 @@ extension CarPlayRouter: RoutingManagerListener {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func didLocationUpdate(_ notifications: [String]) {
|
||||
guard let trip = previewTrip else { return }
|
||||
|
||||
|
||||
let manager = RoutingManager.routingManager
|
||||
if manager.isRouteFinished {
|
||||
listenerContainer.forEach({
|
||||
|
@ -356,13 +356,13 @@ extension CarPlayRouter: RoutingManagerListener {
|
|||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let routeInfo = manager.routeInfo,
|
||||
manager.isRoutingActive else { return }
|
||||
listenerContainer.forEach({
|
||||
$0.didUpdateRouteInfo(routeInfo, forTrip: trip)
|
||||
})
|
||||
|
||||
|
||||
let tts = MWMTextToSpeech.tts()!
|
||||
if manager.isOnRoute && tts.active {
|
||||
tts.playTurnNotifications(notifications)
|
||||
|
|
|
@ -31,7 +31,7 @@ final class CarPlayService: NSObject {
|
|||
}
|
||||
var preparedToPreviewTrips: [CPTrip] = []
|
||||
var isUserPanMap: Bool = false
|
||||
|
||||
|
||||
@objc func setup(window: CPWindow, interfaceController: CPInterfaceController) {
|
||||
isCarplayActivated = true
|
||||
self.window = window
|
||||
|
@ -63,7 +63,7 @@ final class CarPlayService: NSObject {
|
|||
ThemeManager.invalidate()
|
||||
FrameworkHelper.updatePositionArrowOffset(false, offset: 5)
|
||||
}
|
||||
|
||||
|
||||
@objc func destroy() {
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.removeMapView()
|
||||
|
@ -89,7 +89,7 @@ final class CarPlayService: NSObject {
|
|||
ThemeManager.invalidate()
|
||||
FrameworkHelper.updatePositionArrowOffset(true, offset: 0)
|
||||
}
|
||||
|
||||
|
||||
@objc func interfaceStyle() -> UIUserInterfaceStyle {
|
||||
if let window = window,
|
||||
window.traitCollection.userInterfaceIdiom == .carPlay {
|
||||
|
@ -97,7 +97,7 @@ final class CarPlayService: NSObject {
|
|||
}
|
||||
return .unspecified
|
||||
}
|
||||
|
||||
|
||||
private func applyRootViewController() {
|
||||
guard let window = window else { return }
|
||||
let carplaySotyboard = UIStoryboard.instance(.carPlay)
|
||||
|
@ -110,14 +110,14 @@ final class CarPlayService: NSObject {
|
|||
mapVC.add(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func applyBaseRootTemplate() {
|
||||
let mapTemplate = MapTemplateBuilder.buildBaseTemplate(positionMode: currentPositionMode)
|
||||
mapTemplate.mapDelegate = self
|
||||
interfaceController?.setRootTemplate(mapTemplate, animated: true)
|
||||
FrameworkHelper.rotateMap(0.0, animated: false)
|
||||
}
|
||||
|
||||
|
||||
private func applyNavigationRootTemplate(trip: CPTrip, routeInfo: RouteInfo) {
|
||||
let mapTemplate = MapTemplateBuilder.buildNavigationTemplate()
|
||||
mapTemplate.mapDelegate = self
|
||||
|
@ -126,13 +126,13 @@ final class CarPlayService: NSObject {
|
|||
if let estimates = createEstimates(routeInfo: routeInfo) {
|
||||
mapTemplate.updateEstimates(estimates, for: trip)
|
||||
}
|
||||
|
||||
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.updateCurrentSpeed(routeInfo.speed)
|
||||
carplayVC.updateCurrentSpeed(routeInfo.speedMps, speedLimitMps: routeInfo.speedLimitMps)
|
||||
carplayVC.showSpeedControl()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func pushTemplate(_ templateToPush: CPTemplate, animated: Bool) {
|
||||
if let interfaceController = interfaceController {
|
||||
switch templateToPush {
|
||||
|
@ -148,16 +148,16 @@ final class CarPlayService: NSObject {
|
|||
interfaceController.pushTemplate(templateToPush, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func popTemplate(animated: Bool) {
|
||||
interfaceController?.popTemplate(animated: animated)
|
||||
}
|
||||
|
||||
|
||||
func presentAlert(_ template: CPAlertTemplate, animated: Bool) {
|
||||
interfaceController?.dismissTemplate(animated: false)
|
||||
interfaceController?.presentTemplate(template, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
func cancelCurrentTrip() {
|
||||
router?.cancelTrip()
|
||||
if let carplayVC = carplayVC {
|
||||
|
@ -165,14 +165,13 @@ final class CarPlayService: NSObject {
|
|||
}
|
||||
updateMapTemplateUIToBase()
|
||||
}
|
||||
|
||||
func updateCameraUI(isCameraOnRoute: Bool, speedLimit limit: String?) {
|
||||
|
||||
func updateCameraUI(isCameraOnRoute: Bool, speedLimitMps limit: Double?) {
|
||||
if let carplayVC = carplayVC {
|
||||
let speedLimit = limit == nil ? nil : Int(limit!)
|
||||
carplayVC.updateCameraInfo(isCameraOnRoute: isCameraOnRoute, speedLimit: speedLimit)
|
||||
carplayVC.updateCameraInfo(isCameraOnRoute: isCameraOnRoute, speedLimitMps: limit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateMapTemplateUIToBase() {
|
||||
guard let mapTemplate = rootMapTemplate else {
|
||||
return
|
||||
|
@ -188,7 +187,7 @@ final class CarPlayService: NSObject {
|
|||
updateVisibleViewPortState(.default)
|
||||
FrameworkHelper.rotateMap(0.0, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func updateMapTemplateUIToTripFinished(_ trip: CPTrip) {
|
||||
guard let mapTemplate = rootMapTemplate else {
|
||||
return
|
||||
|
@ -206,7 +205,7 @@ final class CarPlayService: NSObject {
|
|||
if let address = trip.destination.placemark.postalAddress?.street {
|
||||
subtitle = subtitle + "\n" + address
|
||||
}
|
||||
|
||||
|
||||
let alert = CPNavigationAlert(titleVariants: [L("trip_finished")],
|
||||
subtitleVariants: [subtitle],
|
||||
imageSet: nil,
|
||||
|
@ -215,18 +214,18 @@ final class CarPlayService: NSObject {
|
|||
duration: 0)
|
||||
mapTemplate.present(navigationAlert: alert, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func updateVisibleViewPortState(_ state: CPViewPortState) {
|
||||
guard let carplayVC = carplayVC else {
|
||||
return
|
||||
}
|
||||
carplayVC.updateVisibleViewPortState(state)
|
||||
}
|
||||
|
||||
|
||||
func updateRouteAfterChangingSettings() {
|
||||
router?.rebuildRoute()
|
||||
}
|
||||
|
||||
|
||||
@objc func showNoMapAlert() {
|
||||
guard let mapTemplate = interfaceController?.topTemplate as? CPMapTemplate,
|
||||
let info = mapTemplate.userInfo as? MapInfo,
|
||||
|
@ -237,7 +236,7 @@ final class CarPlayService: NSObject {
|
|||
alert.userInfo = [CPConstants.TemplateKey.alert: CPConstants.TemplateType.downloadMap]
|
||||
presentAlert(alert, animated: true)
|
||||
}
|
||||
|
||||
|
||||
@objc func hideNoMapAlert() {
|
||||
if let presentedTemplate = interfaceController?.presentedTemplate,
|
||||
let info = presentedTemplate.userInfo as? [String: String],
|
||||
|
@ -267,7 +266,7 @@ extension CarPlayService: CPInterfaceControllerDelegate {
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func templateDidAppear(_ aTemplate: CPTemplate, animated: Bool) {
|
||||
guard let mapTemplate = aTemplate as? CPMapTemplate,
|
||||
let info = aTemplate.userInfo as? MapInfo else {
|
||||
|
@ -278,12 +277,12 @@ extension CarPlayService: CPInterfaceControllerDelegate {
|
|||
preparedToPreviewTrips = []
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if info.type == CPConstants.TemplateType.preview, let trips = info.trips {
|
||||
showPreview(mapTemplate: mapTemplate, trips: trips)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func templateWillDisappear(_ aTemplate: CPTemplate, animated: Bool) {
|
||||
guard let info = aTemplate.userInfo as? MapInfo else {
|
||||
return
|
||||
|
@ -292,7 +291,7 @@ extension CarPlayService: CPInterfaceControllerDelegate {
|
|||
router?.completeRouteAndRemovePoints()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func templateDidDisappear(_ aTemplate: CPTemplate, animated: Bool) {
|
||||
guard !preparedToPreviewTrips.isEmpty,
|
||||
let info = aTemplate.userInfo as? [String: String],
|
||||
|
@ -310,7 +309,7 @@ extension CarPlayService: CPInterfaceControllerDelegate {
|
|||
extension CarPlayService: CPSessionConfigurationDelegate {
|
||||
func sessionConfiguration(_ sessionConfiguration: CPSessionConfiguration,
|
||||
limitedUserInterfacesChanged limitedUserInterfaces: CPLimitableUserInterface) {
|
||||
|
||||
|
||||
}
|
||||
@available(iOS 13.0, *)
|
||||
func sessionConfiguration(_ sessionConfiguration: CPSessionConfiguration,
|
||||
|
@ -326,7 +325,7 @@ extension CarPlayService: CPMapTemplateDelegate {
|
|||
MapTemplateBuilder.configurePanUI(mapTemplate: mapTemplate)
|
||||
FrameworkHelper.stopLocationFollow()
|
||||
}
|
||||
|
||||
|
||||
public func mapTemplateDidDismissPanningInterface(_ mapTemplate: CPMapTemplate) {
|
||||
if let info = mapTemplate.userInfo as? MapInfo,
|
||||
info.type == CPConstants.TemplateType.navigation {
|
||||
|
@ -336,7 +335,7 @@ extension CarPlayService: CPMapTemplateDelegate {
|
|||
}
|
||||
FrameworkHelper.switchMyPositionMode()
|
||||
}
|
||||
|
||||
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate, panEndedWith direction: CPMapTemplate.PanDirection) {
|
||||
var offset = UIOffset(horizontal: 0.0, vertical: 0.0)
|
||||
let offsetStep: CGFloat = 0.25
|
||||
|
@ -347,7 +346,7 @@ extension CarPlayService: CPMapTemplateDelegate {
|
|||
FrameworkHelper.moveMap(offset)
|
||||
isUserPanMap = true
|
||||
}
|
||||
|
||||
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {
|
||||
var offset = UIOffset(horizontal: 0.0, vertical: 0.0)
|
||||
let offsetStep: CGFloat = 0.1
|
||||
|
@ -358,7 +357,7 @@ extension CarPlayService: CPMapTemplateDelegate {
|
|||
FrameworkHelper.moveMap(offset)
|
||||
isUserPanMap = true
|
||||
}
|
||||
|
||||
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate, startedTrip trip: CPTrip, using routeChoice: CPRouteChoice) {
|
||||
guard let info = routeChoice.userInfo as? RouteInfo else {
|
||||
if let info = routeChoice.userInfo as? [String: Any],
|
||||
|
@ -370,13 +369,13 @@ extension CarPlayService: CPMapTemplateDelegate {
|
|||
}
|
||||
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.previewAccepted)
|
||||
mapTemplate.hideTripPreviews()
|
||||
|
||||
|
||||
guard let router = router,
|
||||
let interfaceController = interfaceController,
|
||||
let rootMapTemplate = rootMapTemplate else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
MapTemplateBuilder.configureNavigationUI(mapTemplate: rootMapTemplate)
|
||||
|
||||
if interfaceController.templates.count > 1 {
|
||||
|
@ -387,14 +386,14 @@ extension CarPlayService: CPMapTemplateDelegate {
|
|||
if let estimates = createEstimates(routeInfo: info) {
|
||||
rootMapTemplate.updateEstimates(estimates, for: trip)
|
||||
}
|
||||
|
||||
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.updateCurrentSpeed(info.speed)
|
||||
carplayVC.updateCurrentSpeed(info.speedMps, speedLimitMps: info.speedLimitMps)
|
||||
carplayVC.showSpeedControl()
|
||||
}
|
||||
updateVisibleViewPortState(.navigation)
|
||||
}
|
||||
|
||||
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate, displayStyleFor maneuver: CPManeuver) -> CPManeuverDisplayStyle {
|
||||
if let type = maneuver.userInfo as? String,
|
||||
type == CPConstants.Maneuvers.secondary {
|
||||
|
@ -402,7 +401,7 @@ extension CarPlayService: CPMapTemplateDelegate {
|
|||
}
|
||||
return .leadingSymbol
|
||||
}
|
||||
|
||||
|
||||
func mapTemplate(_ mapTemplate: CPMapTemplate,
|
||||
selectedPreviewFor trip: CPTrip,
|
||||
using routeChoice: CPRouteChoice) {
|
||||
|
@ -481,7 +480,7 @@ extension CarPlayService: CPSearchTemplateDelegate {
|
|||
completionHandler(items)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func searchTemplate(_ searchTemplate: CPSearchTemplate, selectedResult item: CPListItem, completionHandler: @escaping () -> Void) {
|
||||
searchService?.saveLastQuery()
|
||||
if let info = item.userInfo as? ListItemInfo,
|
||||
|
@ -504,10 +503,10 @@ extension CarPlayService: CarPlayRouterListener {
|
|||
currentTemplate.updateEstimates(estimates, for: trip)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func didUpdateRouteInfo(_ routeInfo: RouteInfo, forTrip trip: CPTrip) {
|
||||
if let carplayVC = carplayVC {
|
||||
carplayVC.updateCurrentSpeed(routeInfo.speed)
|
||||
carplayVC.updateCurrentSpeed(routeInfo.speedMps, speedLimitMps: routeInfo.speedLimitMps)
|
||||
}
|
||||
guard let router = router,
|
||||
let template = rootMapTemplate else {
|
||||
|
@ -519,13 +518,13 @@ extension CarPlayService: CarPlayRouterListener {
|
|||
}
|
||||
trip.routeChoices.first?.userInfo = routeInfo
|
||||
}
|
||||
|
||||
|
||||
func didFailureBuildRoute(forTrip trip: CPTrip, code: RouterResultCode, countries: [String]) {
|
||||
guard let template = interfaceController?.topTemplate as? CPMapTemplate else { return }
|
||||
trip.routeChoices.first?.userInfo = [CPConstants.Trip.errorCode: code, CPConstants.Trip.missedCountries: countries]
|
||||
applyUndefinedEstimates(template: template, trip: trip)
|
||||
}
|
||||
|
||||
|
||||
func routeDidFinish(_ trip: CPTrip) {
|
||||
if router?.currentTrip == nil { return }
|
||||
router?.finishTrip()
|
||||
|
@ -558,7 +557,7 @@ extension CarPlayService: LocationModeListener {
|
|||
rootMapTemplate.leadingNavigationBarButtons = []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func processMyPositionPendingTimeout() {
|
||||
}
|
||||
}
|
||||
|
@ -589,7 +588,7 @@ extension CarPlayService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func preparePreview(forBookmark bookmark: MWMCarPlayBookmarkObject) {
|
||||
if let router = router,
|
||||
let startPoint = MWMRoutePoint(lastLocationAndType: .start,
|
||||
|
@ -607,7 +606,7 @@ extension CarPlayService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func preparePreview(trips: [CPTrip]) {
|
||||
let mapTemplate = MapTemplateBuilder.buildTripPreviewTemplate(forTrips: trips)
|
||||
if let interfaceController = interfaceController {
|
||||
|
@ -619,14 +618,14 @@ extension CarPlayService {
|
|||
interfaceController.pushTemplate(mapTemplate, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func showPreview(mapTemplate: CPMapTemplate, trips: [CPTrip]) {
|
||||
let tripTextConfig = CPTripPreviewTextConfiguration(startButtonTitle: L("trip_start"),
|
||||
additionalRoutesButtonTitle: nil,
|
||||
overviewButtonTitle: nil)
|
||||
mapTemplate.showTripPreviews(trips, textConfiguration: tripTextConfig)
|
||||
}
|
||||
|
||||
|
||||
func createEstimates(routeInfo: RouteInfo) -> CPTravelEstimates? {
|
||||
if let distance = Double(routeInfo.targetDistance) {
|
||||
let measurement = Measurement(value: distance,
|
||||
|
@ -637,7 +636,7 @@ extension CarPlayService {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func applyUndefinedEstimates(template: CPMapTemplate, trip: CPTrip) {
|
||||
let measurement = Measurement(value: -1,
|
||||
unit: UnitLength.meters)
|
||||
|
@ -645,7 +644,7 @@ extension CarPlayService {
|
|||
timeRemaining: -1)
|
||||
template.updateEstimates(estimates, for: trip)
|
||||
}
|
||||
|
||||
|
||||
func showRerouteAlert(trips: [CPTrip]) {
|
||||
let yesAction = CPAlertAction(title: L("yes"), style: .default, handler: { [unowned self] _ in
|
||||
self.router?.cancelTrip()
|
||||
|
@ -660,7 +659,7 @@ extension CarPlayService {
|
|||
alert.userInfo = [CPConstants.TemplateKey.alert: CPConstants.TemplateType.redirectRoute]
|
||||
presentAlert(alert, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func showKeyboardAlert() {
|
||||
let okAction = CPAlertAction(title: L("ok"), style: .default, handler: { [unowned self] _ in
|
||||
self.interfaceController?.dismissTemplate(animated: true)
|
||||
|
@ -668,7 +667,7 @@ extension CarPlayService {
|
|||
let alert = CPAlertTemplate(titleVariants: [L("keyboard_availability_alert")], actions: [okAction])
|
||||
presentAlert(alert, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func showErrorAlert(code: RouterResultCode, countries: [String]) {
|
||||
var titleVariants = [String]()
|
||||
switch code {
|
||||
|
@ -697,18 +696,18 @@ extension CarPlayService {
|
|||
.transitRouteNotFoundTooLongPedestrian:
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let okAction = CPAlertAction(title: L("ok"), style: .cancel, handler: { [unowned self] _ in
|
||||
self.interfaceController?.dismissTemplate(animated: true)
|
||||
})
|
||||
let alert = CPAlertTemplate(titleVariants: titleVariants, actions: [okAction])
|
||||
presentAlert(alert, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func showRecoverRouteAlert(trip: CPTrip, isTypeCorrect: Bool) {
|
||||
let yesAction = CPAlertAction(title: L("ok"), style: .default, handler: { [unowned self] _ in
|
||||
var info = trip.userInfo as? [String: MWMRoutePoint]
|
||||
|
||||
|
||||
if let startPoint = MWMRoutePoint(lastLocationAndType: .start,
|
||||
intermediateIndex: 0) {
|
||||
info?[CPConstants.Trip.start] = startPoint
|
||||
|
|
|
@ -8,9 +8,10 @@ class RouteInfo: NSObject {
|
|||
let turnUnits: UnitLength
|
||||
let turnImageName: String?
|
||||
let nextTurnImageName: String?
|
||||
let speed: Int
|
||||
let speedMps: Double
|
||||
let speedLimitMps: Double?
|
||||
let roundExitNumber: Int
|
||||
|
||||
|
||||
@objc init(timeToTarget: TimeInterval,
|
||||
targetDistance: String,
|
||||
targetUnits: String,
|
||||
|
@ -19,7 +20,8 @@ class RouteInfo: NSObject {
|
|||
turnUnits: String,
|
||||
turnImageName: String?,
|
||||
nextTurnImageName: String?,
|
||||
speed: Int,
|
||||
speedMps: Double,
|
||||
speedLimitMps: Double,
|
||||
roundExitNumber: Int) {
|
||||
self.timeToTarget = timeToTarget
|
||||
self.targetDistance = targetDistance
|
||||
|
@ -29,10 +31,12 @@ class RouteInfo: NSObject {
|
|||
self.turnUnits = RouteInfo.unitLength(for: turnUnits)
|
||||
self.turnImageName = turnImageName
|
||||
self.nextTurnImageName = nextTurnImageName
|
||||
self.speed = speed
|
||||
self.speedMps = speedMps
|
||||
// speedLimitMps >= 0 means known limited speed.
|
||||
self.speedLimitMps = speedLimitMps < 0 ? nil : speedLimitMps
|
||||
self.roundExitNumber = roundExitNumber
|
||||
}
|
||||
|
||||
|
||||
class func unitLength(for targetUnits: String) -> UnitLength {
|
||||
switch targetUnits {
|
||||
case "mi":
|
||||
|
|
|
@ -2,25 +2,24 @@
|
|||
|
||||
@interface MWMNavigationDashboardEntity : NSObject
|
||||
|
||||
@property(copy, nonatomic, readonly) NSArray<MWMRouterTransitStepInfo *> * transitSteps;
|
||||
@property(copy, nonatomic, readonly) NSAttributedString * estimate;
|
||||
@property(copy, nonatomic, readonly) NSAttributedString * estimateDot;
|
||||
@property(copy, nonatomic, readonly) NSString * distanceToTurn;
|
||||
@property(copy, nonatomic, readonly) NSString * streetName;
|
||||
@property(copy, nonatomic, readonly) NSString * targetDistance;
|
||||
@property(copy, nonatomic, readonly) NSString * targetUnits;
|
||||
@property(copy, nonatomic, readonly) NSString * turnUnits;
|
||||
@property(copy, nonatomic, readonly) NSString * speedLimit;
|
||||
@property(copy, nonatomic, readonly) NSArray<MWMRouterTransitStepInfo *> *transitSteps;
|
||||
@property(copy, nonatomic, readonly) NSAttributedString *estimate;
|
||||
@property(copy, nonatomic, readonly) NSString *distanceToTurn;
|
||||
@property(copy, nonatomic, readonly) NSString *streetName;
|
||||
@property(copy, nonatomic, readonly) NSString *targetDistance;
|
||||
@property(copy, nonatomic, readonly) NSString *targetUnits;
|
||||
@property(copy, nonatomic, readonly) NSString *turnUnits;
|
||||
@property(nonatomic, readonly) double speedLimitMps;
|
||||
@property(nonatomic, readonly) BOOL isValid;
|
||||
@property(nonatomic, readonly) CGFloat progress;
|
||||
@property(nonatomic, readonly) NSString * arrival;
|
||||
@property(nonatomic, readonly) NSString * eta;
|
||||
@property(nonatomic, readonly) NSString * speed;
|
||||
@property(nonatomic, readonly) NSString * speedUnits;
|
||||
@property(nonatomic, readonly) NSUInteger roundExitNumber;
|
||||
@property(nonatomic, readonly) UIImage * nextTurnImage;
|
||||
@property(nonatomic, readonly) UIImage * turnImage;
|
||||
@property(nonatomic, readonly) BOOL isSpeedLimitExceeded;
|
||||
@property(nonatomic, readonly) NSUInteger timeToTarget;
|
||||
@property(nonatomic, readonly) UIImage *nextTurnImage;
|
||||
@property(nonatomic, readonly) UIImage *turnImage;
|
||||
|
||||
@property(nonatomic, readonly) NSString * arrival;
|
||||
|
||||
+ (NSAttributedString *) estimateDot;
|
||||
|
||||
+ (instancetype) new __attribute__((unavailable("init is not available")));
|
||||
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
#import "MWMNavigationDashboardEntity.h"
|
||||
#import "MWMCoreUnits.h"
|
||||
#import "MWMLocationManager.h"
|
||||
#import "MWMRouterTransitStepInfo.h"
|
||||
#import "MWMSettings.h"
|
||||
#import "SwiftBridge.h"
|
||||
|
||||
#include <CoreApi/Framework.h>
|
||||
|
||||
#include "map/routing_manager.hpp"
|
||||
|
||||
#include "platform/localization.hpp"
|
||||
#include "platform/measurement_utils.hpp"
|
||||
|
||||
@interface MWMNavigationDashboardEntity ()
|
||||
|
||||
@property(copy, nonatomic, readwrite) NSArray<MWMRouterTransitStepInfo *> * transitSteps;
|
||||
@property(copy, nonatomic, readwrite) NSAttributedString * estimate;
|
||||
@property(copy, nonatomic, readwrite) NSString * distanceToTurn;
|
||||
@property(copy, nonatomic, readwrite) NSString * streetName;
|
||||
@property(copy, nonatomic, readwrite) NSString * targetDistance;
|
||||
@property(copy, nonatomic, readwrite) NSString * targetUnits;
|
||||
@property(copy, nonatomic, readwrite) NSString * turnUnits;
|
||||
@property(copy, nonatomic, readwrite) NSString * speedLimit;
|
||||
@property(nonatomic, readwrite) BOOL isValid;
|
||||
@property(nonatomic, readwrite) CGFloat progress;
|
||||
@property(nonatomic, readwrite) NSUInteger roundExitNumber;
|
||||
@property(nonatomic, readwrite) NSUInteger timeToTarget;
|
||||
@property(nonatomic, readwrite) UIImage * nextTurnImage;
|
||||
@property(nonatomic, readwrite) UIImage * turnImage;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMNavigationDashboardEntity
|
||||
|
||||
- (NSString *)speed
|
||||
{
|
||||
CLLocation * lastLocation = [MWMLocationManager lastLocation];
|
||||
if (!lastLocation || lastLocation.speed < 0)
|
||||
return nil;
|
||||
auto const units = coreUnits([MWMSettings measurementUnits]);
|
||||
return @(measurement_utils::FormatSpeedNumeric(lastLocation.speed, units).c_str());
|
||||
}
|
||||
|
||||
- (BOOL)isSpeedLimitExceeded
|
||||
{
|
||||
return GetFramework().GetRoutingManager().IsSpeedLimitExceeded();
|
||||
}
|
||||
|
||||
- (NSString *)speedUnits
|
||||
{
|
||||
return @(platform::GetLocalizedSpeedUnits().c_str());
|
||||
}
|
||||
|
||||
- (NSString *)eta { return [NSDateComponentsFormatter etaStringFrom:self.timeToTarget]; }
|
||||
- (NSString *)arrival
|
||||
{
|
||||
auto arrivalDate = [[NSDate date] dateByAddingTimeInterval:self.timeToTarget];
|
||||
return [NSDateFormatter localizedStringFromDate:arrivalDate
|
||||
dateStyle:NSDateFormatterNoStyle
|
||||
timeStyle:NSDateFormatterShortStyle];
|
||||
}
|
||||
|
||||
- (NSAttributedString *)estimateDot
|
||||
{
|
||||
auto attributes = @{
|
||||
NSForegroundColorAttributeName: [UIColor blackSecondaryText],
|
||||
NSFontAttributeName: [UIFont medium17]
|
||||
};
|
||||
return [[NSAttributedString alloc] initWithString:@" • " attributes:attributes];
|
||||
}
|
||||
|
||||
@end
|
|
@ -68,11 +68,11 @@ UIImage *image(routing::turns::PedestrianDirection t) {
|
|||
return [UIImage imageNamed:imageName];
|
||||
}
|
||||
|
||||
NSAttributedString *estimate(NSTimeInterval time, NSAttributedString *dot, NSString *distance, NSString *distanceUnits,
|
||||
NSAttributedString *estimate(NSTimeInterval time, NSString *distance, NSString *distanceUnits,
|
||||
NSDictionary *primaryAttributes, NSDictionary *secondaryAttributes, BOOL isWalk) {
|
||||
NSString *eta = [NSDateComponentsFormatter etaStringFrom:time];
|
||||
auto result = [[NSMutableAttributedString alloc] initWithString:eta attributes:primaryAttributes];
|
||||
[result appendAttributedString:dot];
|
||||
[result appendAttributedString:MWMNavigationDashboardEntity.estimateDot];
|
||||
|
||||
if (isWalk) {
|
||||
UIFont *font = primaryAttributes[NSFontAttributeName];
|
||||
|
@ -106,7 +106,7 @@ NSAttributedString *estimate(NSTimeInterval time, NSAttributedString *dot, NSStr
|
|||
@property(copy, nonatomic, readwrite) NSString *targetDistance;
|
||||
@property(copy, nonatomic, readwrite) NSString *targetUnits;
|
||||
@property(copy, nonatomic, readwrite) NSString *turnUnits;
|
||||
@property(copy, nonatomic, readwrite) NSString *speedLimit;
|
||||
@property(nonatomic, readwrite) double speedLimitMps;
|
||||
@property(nonatomic, readwrite) BOOL isValid;
|
||||
@property(nonatomic, readwrite) CGFloat progress;
|
||||
@property(nonatomic, readwrite) NSUInteger roundExitNumber;
|
||||
|
@ -116,6 +116,27 @@ NSAttributedString *estimate(NSTimeInterval time, NSAttributedString *dot, NSStr
|
|||
|
||||
@end
|
||||
|
||||
@implementation MWMNavigationDashboardEntity
|
||||
|
||||
- (NSString *)arrival
|
||||
{
|
||||
auto arrivalDate = [[NSDate date] dateByAddingTimeInterval:self.timeToTarget];
|
||||
return [NSDateFormatter localizedStringFromDate:arrivalDate
|
||||
dateStyle:NSDateFormatterNoStyle
|
||||
timeStyle:NSDateFormatterShortStyle];
|
||||
}
|
||||
|
||||
+ (NSAttributedString *)estimateDot
|
||||
{
|
||||
auto attributes = @{
|
||||
NSForegroundColorAttributeName: [UIColor blackSecondaryText],
|
||||
NSFontAttributeName: [UIFont medium17]
|
||||
};
|
||||
return [[NSAttributedString alloc] initWithString:@" • " attributes:attributes];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MWMRouterTransitStepInfo ()
|
||||
|
||||
- (instancetype)initWithStepInfo:(TransitStepInfo const &)info;
|
||||
|
@ -150,9 +171,9 @@ NSAttributedString *estimate(NSTimeInterval time, NSAttributedString *dot, NSStr
|
|||
entity.distanceToTurn = @(info.m_distToTurn.c_str());
|
||||
entity.turnUnits = [self localizedUnitLength:@(info.m_turnUnitsSuffix.c_str())];
|
||||
entity.streetName = @(info.m_displayedStreetName.c_str());
|
||||
entity.speedLimit = @(info.m_speedLimit.c_str());
|
||||
entity.speedLimitMps = info.m_speedLimitMps;
|
||||
|
||||
entity.estimate = estimate(entity.timeToTarget, entity.estimateDot, entity.targetDistance, entity.targetUnits,
|
||||
entity.estimate = estimate(entity.timeToTarget, entity.targetDistance, entity.targetUnits,
|
||||
self.etaAttributes, self.etaSecondaryAttributes, NO);
|
||||
|
||||
if (type == MWMRouterTypePedestrian) {
|
||||
|
@ -178,7 +199,7 @@ NSAttributedString *estimate(NSTimeInterval time, NSAttributedString *dot, NSStr
|
|||
if (auto entity = self.entity) {
|
||||
entity.isValid = YES;
|
||||
entity.estimate =
|
||||
estimate(info.m_totalTimeInSec, entity.estimateDot, @(info.m_totalPedestrianDistanceStr.c_str()),
|
||||
estimate(info.m_totalTimeInSec, @(info.m_totalPedestrianDistanceStr.c_str()),
|
||||
@(info.m_totalPedestrianUnitsSuffix.c_str()), self.etaAttributes, self.etaSecondaryAttributes, YES);
|
||||
NSMutableArray<MWMRouterTransitStepInfo *> *transitSteps = [@[] mutableCopy];
|
||||
for (auto const &stepInfo : info.m_steps)
|
||||
|
|
|
@ -136,7 +136,7 @@ final class NavigationControlView: SolidTouchView, MWMTextToSpeechObserver, MapO
|
|||
]
|
||||
|
||||
if timePageControl.currentPage == 0 {
|
||||
timeLabel.text = info.eta
|
||||
timeLabel.text = DateComponentsFormatter.etaString(from: TimeInterval(info.timeToTarget))
|
||||
} else {
|
||||
timeLabel.text = info.arrival
|
||||
}
|
||||
|
@ -155,23 +155,35 @@ final class NavigationControlView: SolidTouchView, MWMTextToSpeechObserver, MapO
|
|||
}
|
||||
}
|
||||
|
||||
var speed = info.speed ?? "0"
|
||||
if (info.speedLimit != "") {
|
||||
speed += " / " + info.speedLimit;
|
||||
var speedMps = 0.0
|
||||
if let s = LocationManager.lastLocation()?.speed, s > 0 {
|
||||
speedMps = s
|
||||
}
|
||||
|
||||
let speedMeasure = Measure(asSpeed: speedMps)
|
||||
var speed = speedMeasure.valueAsString;
|
||||
// speedLimitMps >= 0 means known limited speed.
|
||||
if (info.speedLimitMps >= 0) {
|
||||
let speedLimitMeasure = Measure(asSpeed: info.speedLimitMps)
|
||||
// speedLimitMps == 0 means unlimited speed.
|
||||
speed += " / " + (info.speedLimitMps == 0 ? "∞" : speedLimitMeasure.valueAsString);
|
||||
}
|
||||
|
||||
speedLabel.text = speed
|
||||
![]() https://developer.apple.com/documentation/corelocation/cllocation/1423798-speed https://developer.apple.com/documentation/corelocation/cllocation/1423798-speed
A negative value indicates an invalid speed.
|
||||
speedLegendLabel.text = info.speedUnits
|
||||
speedLegendLabel.text = speedMeasure.unit
|
||||
let speedWithLegend = NSMutableAttributedString(string: speed, attributes: routingNumberAttributes)
|
||||
speedWithLegend.append(NSAttributedString(string: info.speedUnits, attributes: routingLegendAttributes))
|
||||
speedWithLegend.append(NSAttributedString(string: speedMeasure.unit, attributes: routingLegendAttributes))
|
||||
speedWithLegendLabel.attributedText = speedWithLegend
|
||||
|
||||
let speedLimitExceeded = info.isSpeedLimitExceeded
|
||||
let textColor = speedLimitExceeded ? UIColor.white() : UIColor.blackPrimaryText()
|
||||
speedBackground.backgroundColor = speedLimitExceeded ? UIColor.buttonRed() : UIColor.clear
|
||||
speedLabel.textColor = textColor
|
||||
speedLegendLabel.textColor = textColor
|
||||
speedWithLegendLabel.textColor = textColor
|
||||
if MWMRouter.isSpeedCamLimitExceeded() {
|
||||
speedLabel.textColor = UIColor.white()
|
||||
speedBackground.backgroundColor = UIColor.buttonRed()
|
||||
} else {
|
||||
let isSpeedLimitExceeded = info.speedLimitMps > 0 && speedMps > info.speedLimitMps
|
||||
speedLabel.textColor = isSpeedLimitExceeded ? UIColor.buttonRed() : UIColor.blackPrimaryText()
|
||||
speedBackground.backgroundColor = UIColor.clear
|
||||
}
|
||||
speedLegendLabel.textColor = speedLabel.textColor
|
||||
speedWithLegendLabel.textColor = speedLabel.textColor
|
||||
![]() Использование класса-враппера только для удобства конвертации не обосновано, слишком много оверхеда. Предлагаю сделать метод в C++, который потом можно будет переиспользовать и в Android, чтобы было единообразно. Использование класса-враппера только для удобства конвертации не обосновано, слишком много оверхеда. Предлагаю сделать метод в C++, который потом можно будет переиспользовать и в Android, чтобы было единообразно.
![]() В самом крайнем случае, если не получится, просто сделать кусок кода на С++ внутри .mm файла, и пометить комментарием его потом переиспользовать в ядре для Андроида и других платформ. В самом крайнем случае, если не получится, просто сделать кусок кода на С++ внутри .mm файла, и пометить комментарием его потом переиспользовать в ядре для Андроида и других платформ.
![]() Да, просто C++ -> ObjC функция для использования в Swift. Да, просто C++ -> ObjC функция для использования в Swift.
![]() На крайний случай valueAsString настолько проста, что достаточно в ObjC иметь простую именованную функцию типа SpeedToString На крайний случай valueAsString настолько проста, что достаточно в ObjC иметь простую именованную функцию типа SpeedToString
![]() Хотелось универсальное решение втч и для расстояния, когда в зависимости от длины используются крупные или мелкие юниты. А для этого логично получать пару размера и юнитов одновременно. Кстати, сейчас есть такое, например: В Java сейчас такой подход (но только нужно число, а не строка): Хотелось универсальное решение втч и для расстояния, когда в зависимости от длины используются крупные или мелкие юниты. А для этого логично получать пару размера и юнитов одновременно.
А что именно с классом не нравится? Вызов 3 C++ функций вместо одной?
Где можно почитать про overhead в аналогичных ситуациях?
Кстати, сейчас есть такое, например:
https://github.com/organicmaps/organicmaps/blob/c2c1f979ac9d9422eae5ae72dad1975b49a47061/iphone/Maps/UI/PlacePage/Components/ElevationProfile/ElevationProfilePresenter.swift#L243-L244
В Java сейчас такой подход (но только нужно число, а не строка):
https://github.com/organicmaps/organicmaps/blob/c2c1f979ac9d9422eae5ae72dad1975b49a47061/android/jni/com/mapswithme/util/StringUtils.cpp#L57-L60
![]() Замерил быстродействие:
Если убрать запрос к platform::GetLocalizedSpeedUnits(units):
результат одинаков Замерил быстродействие:
`Measure.init(asSpeed: v)`
vs
```
let imperial = Settings.measurementUnits() == .imperial
var altMeasurement = Measurement(value: v, unit: UnitLength.meters)
altMeasurement.convert(to: imperial ? UnitLength.feet : UnitLength.meters)
```
Если убрать запрос к platform::GetLocalizedSpeedUnits(units):
```
auto units = measurement_utils::Units::Metric;
settings::TryGet(settings::kMeasurementUnits, units);
_value = measurement_utils::MpsToUnits(mps, units);
_unit = @(units == measurement_utils::Units::Metric ? "kmph" : "mph");
```
результат одинаков
а если его оставить - разница в 20 раз.
Вывод - bottleneck в запросе к локализации строки (что ожидаемо), а не в конструировании объекта.
![]()
Динамическая аллокация, которой лучше избегать в часто повторяемых операциях. Такой же объект, созданный на стеке, вообще бы не вызывал вопросов.
Вот в т.ч. и из-за подобных велосипедов на изолиниях теперь всегда метры рисуются.
Если пользователь идёт в настройки и меняет юниты (крайне редкая операция), то диалоги пересоздаются. Вывод: строки/юниты можно и нужно инициализировать один раз для сессии диалога, и на ведре, и на айосе. > А что именно с классом не нравится? Вызов 3 C++ функций вместо одной?
Динамическая аллокация, которой лучше избегать в часто повторяемых операциях. Такой же объект, созданный на стеке, вообще бы не вызывал вопросов.
> Кстати, сейчас есть такое, например:
Вот в т.ч. и из-за подобных велосипедов на изолиниях теперь всегда метры рисуются.
> Вывод - bottleneck в запросе к локализации строки (что ожидаемо), а не в конструировании объекта.
Если пользователь идёт в настройки и меняет юниты (крайне редкая операция), то диалоги пересоздаются. Вывод: строки/юниты можно и нужно инициализировать один раз для сессии диалога, и на ведре, и на айосе.
![]() Я бы предпочел просто использовать статические переменные вместо того чтобы засорять классы ненужными переменными. По тестам быстродействие вполне нормальное (сравнимое со встроенным Measurement). Я бы предпочел просто использовать статические переменные вместо того чтобы засорять классы ненужными переменными. По тестам быстродействие вполне нормальное (сравнимое со встроенным Measurement).
https://github.com/AntonM030481/organicmaps/blob/6d5d5ad891141932f9263f66556e9e052ec55f58/platform/localization.cpp#L19-L45
|
||||
|
||||
routingProgress.constant = progressView.width * info.progress / 100
|
||||
}
|
||||
|
|
|
@ -7,9 +7,6 @@ typedef NS_ENUM(NSInteger, MWMDrivingOptionsState) {
|
|||
MWMDrivingOptionsStateChange
|
||||
};
|
||||
|
||||
@class MWMNavigationDashboardEntity;
|
||||
@class MWMNavigationDashboardManager;
|
||||
@class MWMTaxiCollectionView;
|
||||
@class MWMRoutePreview;
|
||||
|
||||
@protocol MWMRoutePreviewDelegate
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
#import "MWMRoutePreview.h"
|
||||
#import "MWMCircularProgress.h"
|
||||
#import "MWMLocationManager.h"
|
||||
#import "MWMNavigationDashboardEntity.h"
|
||||
#import "MWMNavigationDashboardManager.h"
|
||||
#import "MWMRouter.h"
|
||||
#import "UIButton+Orientation.h"
|
||||
#import "UIImageView+Coloring.h"
|
||||
|
|
|
@ -140,7 +140,7 @@ final class BaseRoutePreviewStatus: SolidTouchView {
|
|||
|
||||
if let result = info.estimate.mutableCopy() as? NSMutableAttributedString {
|
||||
if let elevation = self.elevation {
|
||||
result.append(info.estimateDot)
|
||||
result.append(MWMNavigationDashboardEntity.estimateDot())
|
||||
result.append(elevation)
|
||||
}
|
||||
resultLabel.attributedText = result
|
||||
|
|
|
@ -20,7 +20,7 @@ using namespace storage;
|
|||
|
||||
- (void)processRouteBuilderProgress:(CGFloat)progress;
|
||||
- (void)processRouteRecommendation:(MWMRouterRecommendation)recommendation;
|
||||
- (void)speedCameraShowedUpOnRoute:(double)speedLimit;
|
||||
- (void)speedCameraShowedUpOnRoute:(double)speedLimitKMph;
|
||||
- (void)speedCameraLeftVisibleArea;
|
||||
|
||||
@end
|
||||
|
|
|
@ -11,7 +11,7 @@ NS_SWIFT_NAME(RoutingManagerListener)
|
|||
- (void)processRouteBuilderEventWithCode:(MWMRouterResultCode)code
|
||||
countries:(NSArray<NSString *> *)absentCountries;
|
||||
- (void)didLocationUpdate:(NSArray<NSString *> *)notifications;
|
||||
- (void)updateCameraInfo:(BOOL)isCameraOnRoute speedLimit:(nullable NSString *)limit NS_SWIFT_NAME(updateCameraInfo(isCameraOnRoute:speedLimit:));
|
||||
- (void)updateCameraInfo:(BOOL)isCameraOnRoute speedLimitMps:(double)limit NS_SWIFT_NAME(updateCameraInfo(isCameraOnRoute:speedLimitMps:));
|
||||
@end
|
||||
|
||||
NS_SWIFT_NAME(RoutingManager)
|
||||
|
|
|
@ -82,10 +82,9 @@
|
|||
self.rm.GetRouteFollowingInfo(info);
|
||||
if (!info.IsValid()) { return nil; }
|
||||
CLLocation * lastLocation = [MWMLocationManager lastLocation];
|
||||
NSString *speed = @"0";
|
||||
double speedMps = 0;
|
||||
if (lastLocation && lastLocation.speed >= 0) {
|
||||
auto const units = coreUnits([MWMSettings measurementUnits]);
|
||||
speed = @(measurement_utils::FormatSpeedNumeric(lastLocation.speed, units).c_str());
|
||||
speedMps = lastLocation.speed;
|
||||
}
|
||||
NSInteger roundExitNumber = 0;
|
||||
if (info.m_turn == routing::turns::CarDirection::EnterRoundAbout ||
|
||||
|
@ -93,7 +92,7 @@
|
|||
info.m_turn == routing::turns::CarDirection::LeaveRoundAbout) {
|
||||
roundExitNumber = info.m_exitNum;
|
||||
}
|
||||
|
||||
|
||||
MWMRouteInfo *objCInfo = [[MWMRouteInfo alloc] initWithTimeToTarget:info.m_time
|
||||
targetDistance:@(info.m_distToTarget.c_str())
|
||||
targetUnits:@(info.m_targetUnitsSuffix.c_str())
|
||||
|
@ -102,8 +101,9 @@
|
|||
turnUnits:@(info.m_turnUnitsSuffix.c_str())
|
||||
turnImageName:[self turnImageName:info.m_turn isPrimary:YES]
|
||||
nextTurnImageName:[self turnImageName:info.m_nextTurn isPrimary:NO]
|
||||
speed:[speed integerValue]
|
||||
roundExitNumber: roundExitNumber];
|
||||
speedMps:speedMps
|
||||
speedLimitMps:info.m_speedLimitMps
|
||||
roundExitNumber:roundExitNumber];
|
||||
return objCInfo;
|
||||
}
|
||||
|
||||
|
@ -143,7 +143,7 @@
|
|||
- (void)buildRouteWithDidFailError:(NSError * __autoreleasing __nullable *)errorPtr {
|
||||
auto const & points = self.rm.GetRoutePoints();
|
||||
auto const pointsCount = points.size();
|
||||
|
||||
|
||||
if (pointsCount > 1) {
|
||||
self.rm.BuildRoute();
|
||||
} else {
|
||||
|
@ -229,12 +229,10 @@
|
|||
NSArray<id<MWMRoutingManagerListener>> * objects = self.listeners.allObjects;
|
||||
for (id<MWMRoutingManagerListener> object in objects) {
|
||||
if (speedLimit == routing::SpeedCameraOnRoute::kNoSpeedInfo) {
|
||||
[object updateCameraInfo:YES speedLimit:nil];
|
||||
[object updateCameraInfo:YES speedLimitMps:-1];
|
||||
} else {
|
||||
auto const metersPerSecond = measurement_utils::KmphToMps(speedLimit);
|
||||
|
||||
NSString *limit = @(measurement_utils::FormatSpeed(metersPerSecond).c_str());
|
||||
[object updateCameraInfo:YES speedLimit:limit];
|
||||
[object updateCameraInfo:YES speedLimitMps:metersPerSecond];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +240,7 @@
|
|||
- (void)speedCameraLeftVisibleArea {
|
||||
NSArray<id<MWMRoutingManagerListener>> * objects = self.listeners.allObjects;
|
||||
for (id<MWMRoutingManagerListener> object in objects) {
|
||||
[object updateCameraInfo:NO speedLimit:nil];
|
||||
[object updateCameraInfo:NO speedLimitMps:-1];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ typedef void (^MWMImageHeightBlock)(UIImage *, NSString *);
|
|||
+ (BOOL)isRouteRebuildingOnly;
|
||||
+ (BOOL)isOnRoute;
|
||||
|
||||
+ (BOOL)isSpeedCamLimitExceeded;
|
||||
|
||||
+ (BOOL)canAddIntermediatePoint;
|
||||
|
||||
+ (void)startRouting;
|
||||
|
|
|
@ -87,6 +87,12 @@ char const *kRenderAltitudeImagesQueueLabel = "mapsme.mwmrouter.renderAltitudeIm
|
|||
+ (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();
|
||||
|
|
|
@ -99,7 +99,6 @@
|
|||
34AB39C21D2BD8310021857D /* MWMStopButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 34AB39C01D2BD8310021857D /* MWMStopButton.m */; };
|
||||
34AB66051FC5AA320078E451 /* MWMNavigationDashboardManager+Entity.mm in Sources */ = {isa = PBXBuildFile; fileRef = 34AB65C51FC5AA320078E451 /* MWMNavigationDashboardManager+Entity.mm */; };
|
||||
34AB66081FC5AA320078E451 /* MWMNavigationDashboardManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 34AB65C61FC5AA320078E451 /* MWMNavigationDashboardManager.mm */; };
|
||||
34AB660B1FC5AA320078E451 /* MWMNavigationDashboardEntity.mm in Sources */ = {isa = PBXBuildFile; fileRef = 34AB65CB1FC5AA320078E451 /* MWMNavigationDashboardEntity.mm */; };
|
||||
34AB660E1FC5AA320078E451 /* NavigationControlView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 34AB65CD1FC5AA320078E451 /* NavigationControlView.xib */; };
|
||||
34AB66111FC5AA320078E451 /* NavigationTurnsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34AB65CE1FC5AA320078E451 /* NavigationTurnsView.swift */; };
|
||||
34AB66141FC5AA320078E451 /* MWMNavigationInfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 34AB65D01FC5AA320078E451 /* MWMNavigationInfoView.xib */; };
|
||||
|
@ -855,7 +854,6 @@
|
|||
34AB65C71FC5AA320078E451 /* MWMNavigationDashboardManager+Entity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MWMNavigationDashboardManager+Entity.h"; sourceTree = "<group>"; };
|
||||
34AB65C91FC5AA320078E451 /* MWMNavigationDashboardEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MWMNavigationDashboardEntity.h; sourceTree = "<group>"; };
|
||||
34AB65CA1FC5AA320078E451 /* MWMNavigationDashboardManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MWMNavigationDashboardManager.h; sourceTree = "<group>"; };
|
||||
34AB65CB1FC5AA320078E451 /* MWMNavigationDashboardEntity.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MWMNavigationDashboardEntity.mm; sourceTree = "<group>"; };
|
||||
34AB65CD1FC5AA320078E451 /* NavigationControlView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NavigationControlView.xib; sourceTree = "<group>"; };
|
||||
34AB65CE1FC5AA320078E451 /* NavigationTurnsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationTurnsView.swift; sourceTree = "<group>"; };
|
||||
34AB65CF1FC5AA320078E451 /* MWMNavigationInfoView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MWMNavigationInfoView.h; sourceTree = "<group>"; };
|
||||
|
@ -2267,7 +2265,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
34AB65C91FC5AA320078E451 /* MWMNavigationDashboardEntity.h */,
|
||||
34AB65CB1FC5AA320078E451 /* MWMNavigationDashboardEntity.mm */,
|
||||
34AB65CA1FC5AA320078E451 /* MWMNavigationDashboardManager.h */,
|
||||
34AB65C61FC5AA320078E451 /* MWMNavigationDashboardManager.mm */,
|
||||
34AB65C71FC5AA320078E451 /* MWMNavigationDashboardManager+Entity.h */,
|
||||
|
@ -4325,7 +4322,6 @@
|
|||
993DF10923F6BDB100AC231A /* IFonts.swift in Sources */,
|
||||
47699A0821F08E37009E6585 /* NSDate+TimeDistance.m in Sources */,
|
||||
34845DB31E165E24003D55B9 /* SearchNoResultsViewController.swift in Sources */,
|
||||
34AB660B1FC5AA320078E451 /* MWMNavigationDashboardEntity.mm in Sources */,
|
||||
47E3C72B2111E62A008B3B27 /* FadeOutAnimatedTransitioning.swift in Sources */,
|
||||
471A7BC4248471BE00A0D4C1 /* BookmarkUIUtils.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
final class CarPlayMapViewController: MWMViewController {
|
||||
private(set) var mapView: EAGLView?
|
||||
@IBOutlet var speedInfoView: UIView!
|
||||
@IBOutlet var speedLimitContainer: UIView!
|
||||
@IBOutlet var speedCamLimitContainer: UIView!
|
||||
@IBOutlet var speedCamImageView: UIImageView!
|
||||
@IBOutlet var speedLimitLabel: UILabel!
|
||||
@IBOutlet var speedCamLimitLabel: UILabel!
|
||||
@IBOutlet var currentSpeedView: UIView!
|
||||
@IBOutlet var currentSpeedLabel: UILabel!
|
||||
private var currentSpeed: Int = 0
|
||||
private var speedLimit: Int?
|
||||
private var currentSpeedMps: Double = 0.0
|
||||
private var speedLimitMps: Double?
|
||||
private var speedCamLimitMps: Double?
|
||||
private var isCameraOnRoute: Bool = false
|
||||
private var viewPortState: CPViewPortState = .default
|
||||
private var isSpeedCamBlinking: Bool = false
|
||||
private var isLeftWheelCar: Bool {
|
||||
return self.speedInfoView.frame.origin.x > self.view.frame.midX
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if mapView?.drapeEngineCreated == false {
|
||||
|
@ -25,11 +27,11 @@ final class CarPlayMapViewController: MWMViewController {
|
|||
}
|
||||
updateVisibleViewPortState(viewPortState)
|
||||
}
|
||||
|
||||
|
||||
func addMapView(_ mapView: EAGLView, mapButtonSafeAreaLayoutGuide: UILayoutGuide) {
|
||||
mapView.translatesAutoresizingMaskIntoConstraints = false
|
||||
removeMapView()
|
||||
|
||||
|
||||
self.mapView = mapView
|
||||
mapView.frame = view.bounds
|
||||
view.insertSubview(mapView, at: 0)
|
||||
|
@ -38,73 +40,124 @@ final class CarPlayMapViewController: MWMViewController {
|
|||
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
|
||||
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
|
||||
speedInfoView.trailingAnchor.constraint(equalTo: mapButtonSafeAreaLayoutGuide.trailingAnchor).isActive = true
|
||||
|
||||
speedCamLimitContainer.layer.borderWidth = 2.0
|
||||
}
|
||||
|
||||
|
||||
func removeMapView() {
|
||||
if let mapView = mapView {
|
||||
if let mapView = self.mapView {
|
||||
mapView.removeFromSuperview()
|
||||
self.mapView = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func hideSpeedControl() {
|
||||
if !speedInfoView.isHidden {
|
||||
speedInfoView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func showSpeedControl() {
|
||||
if speedInfoView.isHidden {
|
||||
speedInfoView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func updateCurrentSpeed(_ speed: Int) {
|
||||
self.currentSpeed = speed
|
||||
|
||||
func updateCurrentSpeed(_ speedMps: Double, speedLimitMps: Double?) {
|
||||
self.currentSpeedMps = speedMps
|
||||
self.speedLimitMps = speedLimitMps
|
||||
updateSpeedControl()
|
||||
}
|
||||
|
||||
func updateCameraInfo(isCameraOnRoute: Bool, speedLimit: Int?) {
|
||||
|
||||
func updateCameraInfo(isCameraOnRoute: Bool, speedLimitMps: Double?) {
|
||||
self.isCameraOnRoute = isCameraOnRoute
|
||||
self.speedLimit = speedLimit
|
||||
self.speedCamLimitMps = speedLimitMps
|
||||
updateSpeedControl()
|
||||
}
|
||||
|
||||
|
||||
private func BlinkSpeedCamLimit(blink: Bool)
|
||||
{
|
||||
if blink {
|
||||
if !isSpeedCamBlinking {
|
||||
speedCamLimitLabel.alpha = 0
|
||||
speedCamImageView.alpha = 1
|
||||
UIView.animate(withDuration: 0.5,
|
||||
delay:0.0,
|
||||
options:[.repeat, .autoreverse, .curveEaseOut],
|
||||
animations: { self.speedCamImageView.alpha = 0; self.speedCamLimitLabel.alpha = 1 })
|
||||
isSpeedCamBlinking = true
|
||||
}
|
||||
} else {
|
||||
if (isSpeedCamBlinking) {
|
||||
speedCamLimitLabel.layer.removeAllAnimations()
|
||||
speedCamImageView.layer.removeAllAnimations()
|
||||
isSpeedCamBlinking = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSpeedControl() {
|
||||
currentSpeedLabel.text = "\(currentSpeed)"
|
||||
let speedMeasure = Measure.init(asSpeed: currentSpeedMps)
|
||||
currentSpeedLabel.text = speedMeasure.valueAsString
|
||||
|
||||
if isCameraOnRoute {
|
||||
speedLimitContainer.layer.borderColor = UIColor.speedLimitRed().cgColor
|
||||
speedLimitContainer.layer.borderWidth = 2.0
|
||||
if let speedLimit = speedLimit {
|
||||
speedCamImageView.alpha = 0.0
|
||||
speedLimitLabel.textColor = UIColor.speedLimitDarkGray()
|
||||
speedLimitLabel.text = "\(speedLimit)"
|
||||
speedLimitLabel.alpha = 1.0
|
||||
speedCamLimitContainer.layer.borderColor = UIColor.speedLimitRed().cgColor
|
||||
speedCamImageView.tintColor = UIColor.speedLimitRed()
|
||||
|
||||
// self.speedCamLimitMps comes from SpeedCamManager and is based on
|
||||
// the nearest speed camera info when it is close enough.
|
||||
// If it's unknown self.speedLimitMps is used, which is based on current road speed limit.
|
||||
if let speedCamLimitMps = (self.speedCamLimitMps ?? self.speedLimitMps) {
|
||||
BlinkSpeedCamLimit(blink: true)
|
||||
let speedCamLimitMeasure = Measure.init(asSpeed: speedCamLimitMps)
|
||||
speedCamLimitLabel.text = speedCamLimitMeasure.valueAsString
|
||||
speedCamLimitLabel.textColor = UIColor.speedLimitDarkGray()
|
||||
|
||||
currentSpeedLabel.textColor = UIColor.white
|
||||
if speedLimit >= currentSpeed {
|
||||
currentSpeedView.backgroundColor = UIColor.speedLimitGeen()
|
||||
if speedCamLimitMps >= currentSpeedMps {
|
||||
currentSpeedView.backgroundColor = UIColor.speedLimitGreen()
|
||||
} else {
|
||||
currentSpeedView.backgroundColor = UIColor.speedLimitRed()
|
||||
}
|
||||
} else {
|
||||
speedLimitLabel.alpha = 0.0
|
||||
BlinkSpeedCamLimit(blink: false)
|
||||
speedCamLimitLabel.alpha = 0.0
|
||||
speedCamImageView.tintColor = UIColor.speedLimitRed()
|
||||
speedCamImageView.alpha = 1.0
|
||||
|
||||
currentSpeedLabel.textColor = UIColor.speedLimitDarkGray()
|
||||
currentSpeedView.backgroundColor = UIColor.speedLimitWhite()
|
||||
}
|
||||
} else {
|
||||
speedLimitContainer.layer.borderColor = UIColor.speedLimitLightGray().cgColor
|
||||
speedLimitContainer.layer.borderWidth = 2.0
|
||||
speedLimitLabel.alpha = 0.0
|
||||
speedCamImageView.tintColor = UIColor.speedLimitLightGray()
|
||||
speedCamImageView.alpha = 1.0
|
||||
} else { // !isCameraOnRoute
|
||||
BlinkSpeedCamLimit(blink: false)
|
||||
currentSpeedLabel.textColor = UIColor.speedLimitDarkGray()
|
||||
if let speedLimitMps = self.speedLimitMps {
|
||||
speedCamImageView.alpha = 0.0
|
||||
let speedLimitMeasure = Measure.init(asSpeed: speedLimitMps)
|
||||
speedCamLimitLabel.textColor = UIColor.speedLimitDarkGray()
|
||||
// speedLimitMps == 0 means unlimited speed.
|
||||
if speedLimitMeasure.value == 0 {
|
||||
speedCamLimitLabel.text = "🚀" //"∞"
|
||||
}
|
||||
else {
|
||||
speedCamLimitLabel.text = speedLimitMeasure.valueAsString;
|
||||
}
|
||||
speedCamLimitLabel.alpha = 1.0
|
||||
speedCamLimitContainer.layer.borderColor = UIColor.speedLimitRed().cgColor
|
||||
if currentSpeedMps > speedLimitMps {
|
||||
currentSpeedLabel.textColor = UIColor.speedLimitRed()
|
||||
}
|
||||
} else {
|
||||
speedCamImageView.tintColor = UIColor.speedLimitLightGray()
|
||||
speedCamImageView.alpha = 1.0
|
||||
speedCamLimitLabel.alpha = 0.0
|
||||
speedCamLimitContainer.layer.borderColor = UIColor.speedLimitLightGray().cgColor
|
||||
}
|
||||
currentSpeedView.backgroundColor = UIColor.speedLimitWhite()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateVisibleViewPortState(_ state: CPViewPortState) {
|
||||
viewPortState = state
|
||||
switch viewPortState {
|
||||
|
@ -116,7 +169,7 @@ final class CarPlayMapViewController: MWMViewController {
|
|||
updateVisibleViewPortToNavigationState()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func updateVisibleViewPortToPreviewState() {
|
||||
let viewBounds = view.bounds
|
||||
let previewWidth = self.view.frame.width * 0.45
|
||||
|
@ -128,7 +181,7 @@ final class CarPlayMapViewController: MWMViewController {
|
|||
height: viewBounds.height - origin.y)
|
||||
FrameworkHelper.setVisibleViewport(frame, scaleFactor: mapView?.contentScaleFactor ?? 1)
|
||||
}
|
||||
|
||||
|
||||
private func updateVisibleViewPortToNavigationState() {
|
||||
let viewBounds = view.bounds
|
||||
let previewWidth = viewBounds.width * 0.45
|
||||
|
@ -141,11 +194,11 @@ final class CarPlayMapViewController: MWMViewController {
|
|||
height: viewBounds.height - origin.y)
|
||||
FrameworkHelper.setVisibleViewport(frame, scaleFactor: mapView?.contentScaleFactor ?? 1)
|
||||
}
|
||||
|
||||
|
||||
private func updateVisibleViewPortToDefaultState() {
|
||||
FrameworkHelper.setVisibleViewport(view.bounds, scaleFactor: mapView?.contentScaleFactor ?? 1)
|
||||
}
|
||||
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
ThemeManager.invalidate()
|
||||
|
|
|
@ -36,7 +36,7 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout {
|
|||
vc.placePagePreviewData = placePageData.previewData
|
||||
return vc
|
||||
} ()
|
||||
|
||||
|
||||
lazy var wikiDescriptionViewController: WikiDescriptionViewController = {
|
||||
let vc = storyboard.instantiateViewController(ofType: WikiDescriptionViewController.self)
|
||||
vc.view.isHidden = true
|
||||
|
@ -50,14 +50,14 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout {
|
|||
vc.delegate = interactor
|
||||
return vc
|
||||
} ()
|
||||
|
||||
|
||||
lazy var infoViewController: PlacePageInfoViewController = {
|
||||
let vc = storyboard.instantiateViewController(ofType: PlacePageInfoViewController.self)
|
||||
vc.placePageInfoData = placePageData.infoData
|
||||
vc.delegate = interactor
|
||||
return vc
|
||||
} ()
|
||||
|
||||
|
||||
lazy var buttonsViewController: PlacePageButtonsViewController = {
|
||||
let vc = storyboard.instantiateViewController(ofType: PlacePageButtonsViewController.self)
|
||||
vc.buttonsData = placePageData.buttonsData!
|
||||
|
@ -65,7 +65,7 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout {
|
|||
vc.delegate = interactor
|
||||
return vc
|
||||
} ()
|
||||
|
||||
|
||||
lazy var actionBarViewController: ActionBarViewController = {
|
||||
let vc = storyboard.instantiateViewController(ofType: ActionBarViewController.self)
|
||||
vc.placePageData = placePageData
|
||||
|
@ -82,13 +82,13 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout {
|
|||
lazy var placePageNavigationViewController: PlacePageHeaderViewController = {
|
||||
return PlacePageHeaderBuilder.build(data: placePageData.previewData, delegate: interactor, headerType: .fixed)
|
||||
} ()
|
||||
|
||||
|
||||
init(interactor: PlacePageInteractor, storyboard: UIStoryboard, data: PlacePageData) {
|
||||
self.interactor = interactor
|
||||
self.storyboard = storyboard
|
||||
self.placePageData = data
|
||||
}
|
||||
|
||||
|
||||
private func configureViewControllers() -> [UIViewController] {
|
||||
var viewControllers = [UIViewController]()
|
||||
viewControllers.append(previewViewController)
|
||||
|
@ -113,7 +113,7 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout {
|
|||
if placePageData.buttonsData != nil {
|
||||
viewControllers.append(buttonsViewController)
|
||||
}
|
||||
|
||||
|
||||
placePageData.onBookmarkStatusUpdate = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.placePageData.bookmarkData == nil {
|
||||
|
@ -212,9 +212,8 @@ extension PlacePageCommonLayout: MWMLocationObserver {
|
|||
let altString = "▲ \(unitsFormatter.string(from: altMeasurement))"
|
||||
|
||||
if location.speed > 0 && location.timestamp.timeIntervalSinceNow >= -2 {
|
||||
let speed = imperial ? location.speed * 2.237 : location.speed * 3.6
|
||||
let speedMeasurement = Measurement(value: speed.rounded(), unit: imperial ? UnitSpeed.milesPerHour: UnitSpeed.kilometersPerHour)
|
||||
let speedString = "\(LocationManager.speedSymbolFor(location.speed))\(unitsFormatter.string(from: speedMeasurement))"
|
||||
let speedMeasure = Measure.init(asSpeed: location.speed)
|
||||
let speedString = "\(LocationManager.speedSymbolFor(location.speed))\(speedMeasure.valueAsString) \(speedMeasure.unit)"
|
||||
previewViewController.updateSpeedAndAltitude("\(altString) \(speedString)")
|
||||
} else {
|
||||
previewViewController.updateSpeedAndAltitude(altString)
|
||||
|
|
|
@ -53,11 +53,11 @@
|
|||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_carplay_camera" translatesAutoresizingMaskIntoConstraints="NO" id="Vip-QN-Smh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="30" height="30"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" white="1" alpha="0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="60" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ifo-0s-Ynd">
|
||||
<rect key="frame" x="0.0" y="0.0" width="30" height="30"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="backgroundColor" white="1" alpha="0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
|
@ -107,8 +107,8 @@
|
|||
<outlet property="currentSpeedView" destination="1hD-QK-pwq" id="Mbg-8c-EWp"/>
|
||||
<outlet property="speedCamImageView" destination="Vip-QN-Smh" id="ASc-eP-fXa"/>
|
||||
<outlet property="speedInfoView" destination="dAY-9Y-UMN" id="k1g-9g-oKS"/>
|
||||
<outlet property="speedLimitContainer" destination="uhn-dc-aMW" id="AQt-AB-ZyL"/>
|
||||
<outlet property="speedLimitLabel" destination="ifo-0s-Ynd" id="zgD-Di-6YZ"/>
|
||||
<outlet property="speedCamLimitContainer" destination="uhn-dc-aMW" id="AQt-AB-ZyL"/>
|
||||
<outlet property="speedCamLimitLabel" destination="ifo-0s-Ynd" id="zgD-Di-6YZ"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="6hr-Cq-l0z" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
|
|
|
@ -1475,7 +1475,7 @@ void RoutingManager::SetSubroutesVisibility(bool visible)
|
|||
lock.Get()->SetSubrouteVisibility(subrouteId, visible);
|
||||
}
|
||||
|
||||
bool RoutingManager::IsSpeedLimitExceeded() const
|
||||
bool RoutingManager::IsSpeedCamLimitExceeded() const
|
||||
{
|
||||
return m_routingSession.IsSpeedLimitExceeded();
|
||||
return m_routingSession.IsSpeedCamLimitExceeded();
|
||||
}
|
||||
|
|
|
@ -255,7 +255,7 @@ public:
|
|||
void OnLocationUpdate(location::GpsInfo const & info);
|
||||
|
||||
routing::SpeedCameraManager & GetSpeedCamManager() { return m_routingSession.GetSpeedCamManager(); }
|
||||
bool IsSpeedLimitExceeded() const;
|
||||
bool IsSpeedCamLimitExceeded() const;
|
||||
|
||||
void SetTurnNotificationsUnits(measurement_utils::Units const units)
|
||||
{
|
||||
|
|
|
@ -16,23 +16,29 @@ enum class MeasurementType
|
|||
Altitude
|
||||
};
|
||||
|
||||
LocalizedUnits GetLocalizedUnits(measurement_utils::Units units, MeasurementType measurementType)
|
||||
const LocalizedUnits & GetLocalizedUnits(measurement_utils::Units units, MeasurementType measurementType)
|
||||
{
|
||||
static LocalizedUnits UnitsLenghImperial = {GetLocalizedString("foot"), GetLocalizedString("mile")};
|
||||
static LocalizedUnits UnitsLenghMetric = {GetLocalizedString("meter"), GetLocalizedString("kilometer")};
|
||||
|
||||
static LocalizedUnits UnitsSpeedImperial = {GetLocalizedString("foot"), GetLocalizedString("miles_per_hour")};
|
||||
static LocalizedUnits UnitsSpeedMetric = {GetLocalizedString("meter"), GetLocalizedString("kilometers_per_hour")};
|
||||
|
||||
switch (measurementType)
|
||||
{
|
||||
case MeasurementType::Distance:
|
||||
case MeasurementType::Altitude:
|
||||
switch (units)
|
||||
{
|
||||
case measurement_utils::Units::Imperial: return {GetLocalizedString("foot"), GetLocalizedString("mile")};
|
||||
case measurement_utils::Units::Metric: return {GetLocalizedString("meter"), GetLocalizedString("kilometer")};
|
||||
case measurement_utils::Units::Imperial: return UnitsLenghImperial;
|
||||
case measurement_utils::Units::Metric: return UnitsLenghMetric;
|
||||
}
|
||||
break;
|
||||
case MeasurementType::Speed:
|
||||
switch (units)
|
||||
{
|
||||
case measurement_utils::Units::Imperial: return {"", GetLocalizedString("miles_per_hour")};
|
||||
case measurement_utils::Units::Metric: return {"", GetLocalizedString("kilometers_per_hour")};
|
||||
case measurement_utils::Units::Imperial: return UnitsSpeedImperial;
|
||||
case measurement_utils::Units::Metric: return UnitsSpeedMetric;
|
||||
}
|
||||
}
|
||||
UNREACHABLE();
|
||||
|
@ -55,11 +61,16 @@ LocalizedUnits GetLocalizedAltitudeUnits()
|
|||
return GetLocalizedUnits(units, MeasurementType::Altitude);
|
||||
}
|
||||
|
||||
const std::string & GetLocalizedSpeedUnits(measurement_utils::Units units)
|
||||
{
|
||||
return GetLocalizedUnits(units, MeasurementType::Speed).m_high;
|
||||
}
|
||||
|
||||
std::string GetLocalizedSpeedUnits()
|
||||
{
|
||||
auto units = measurement_utils::Units::Metric;
|
||||
settings::TryGet(settings::kMeasurementUnits, units);
|
||||
|
||||
return GetLocalizedUnits(units, MeasurementType::Speed).m_high;
|
||||
return GetLocalizedSpeedUnits(units);
|
||||
}
|
||||
} // namespace platform
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
#include <string>
|
||||
|
||||
namespace measurement_utils
|
||||
{
|
||||
enum class Units;
|
||||
}
|
||||
|
||||
namespace platform
|
||||
{
|
||||
struct LocalizedUnits
|
||||
|
@ -18,5 +23,7 @@ extern std::string GetLocalizedMyPositionBookmarkName();
|
|||
|
||||
extern LocalizedUnits GetLocalizedDistanceUnits();
|
||||
extern LocalizedUnits GetLocalizedAltitudeUnits();
|
||||
|
||||
extern const std::string & GetLocalizedSpeedUnits(measurement_utils::Units units);
|
||||
extern std::string GetLocalizedSpeedUnits();
|
||||
} // namespace platform
|
||||
|
|
|
@ -73,7 +73,7 @@ std::string DebugPrint(Units units)
|
|||
UNREACHABLE();
|
||||
}
|
||||
|
||||
double ToSpeedKmPH(double speed, measurement_utils::Units units)
|
||||
double ToSpeedKmPH(double speed, Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
|
@ -224,14 +224,19 @@ string FormatSpeed(double metersPerSecond)
|
|||
return FormatSpeedNumeric(metersPerSecond, units) + " " + FormatSpeedUnits(units);
|
||||
}
|
||||
|
||||
string FormatSpeedNumeric(double metersPerSecond, Units units)
|
||||
double MpsToUnits(double metersPerSecond, Units units)
|
||||
{
|
||||
double unitsPerHour;
|
||||
switch (units)
|
||||
{
|
||||
case Units::Imperial: unitsPerHour = KmphToMiph(MpsToKmph(metersPerSecond)); break;
|
||||
case Units::Metric: unitsPerHour = MpsToKmph(metersPerSecond); break;
|
||||
case Units::Imperial: return KmphToMiph(MpsToKmph(metersPerSecond)); break;
|
||||
case Units::Metric: return MpsToKmph(metersPerSecond); break;
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
string FormatSpeedNumeric(double metersPerSecond, Units units)
|
||||
{
|
||||
double const unitsPerHour = MpsToUnits(metersPerSecond, units);
|
||||
return ToStringPrecision(unitsPerHour, unitsPerHour >= 10.0 ? 0 : 1);
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ inline double InchesToMeters(double in) { return in / 39.370; }
|
|||
inline double NauticalMilesToMeters(double nmi) { return nmi * 1852; }
|
||||
![]() Мы же тут уже внутри namespace measurement_utils Мы же тут уже внутри namespace measurement_utils
![]() OK OK
|
||||
inline double KmphToMps(double kmph) { return kmph * 1000 / 3600; }
|
||||
|
||||
double ToSpeedKmPH(double speed, measurement_utils::Units units);
|
||||
double ToSpeedKmPH(double speed, Units units);
|
||||
double MpsToUnits(double mps, Units units);
|
||||
|
||||
/// Takes into an account user settings [metric, imperial]
|
||||
/// @param[in] m meters
|
||||
|
|
|
@ -486,7 +486,7 @@ void DrawWidget::SubmitFakeLocationPoint(m2::PointD const & pt)
|
|||
else
|
||||
{
|
||||
LOG(LDEBUG, ("Distance:", loc.m_distToTarget + loc.m_targetUnitsSuffix, "Time:", loc.m_time,
|
||||
loc.m_speedLimitUnitsSuffix.empty() ? "" : "SpeedLimit: " + loc.m_speedLimit + loc.m_speedLimitUnitsSuffix,
|
||||
loc.m_speedLimitMps < 0 ? "" : "SpeedLimit: " + measurement_utils::FormatSpeed(loc.m_speedLimitMps),
|
||||
GetTurnString(loc.m_turn), (loc.m_exitNum != 0 ? ":" + std::to_string(loc.m_exitNum) : ""),
|
||||
"in", loc.m_distToTurn + loc.m_turnUnitsSuffix,
|
||||
loc.m_targetName.empty() ? "" : "to " + loc.m_targetName ));
|
||||
|
|
|
@ -84,9 +84,8 @@ public:
|
|||
turns::PedestrianDirection m_pedestrianTurn;
|
||||
//@}
|
||||
|
||||
// Current speed limit.
|
||||
// If no info about speed limit then m_speedLimit == "" and m_speedLimitUnitsSuffix == "".
|
||||
std::string m_speedLimit;
|
||||
std::string m_speedLimitUnitsSuffix;
|
||||
// Current speed limit in meters per second.
|
||||
// If no info about speed limit then m_speedLimitMps < 0.
|
||||
double m_speedLimitMps = -1.0;
|
||||
};
|
||||
} // namespace routing
|
||||
|
|
|
@ -48,16 +48,6 @@ void FormatDistance(double dist, string & value, string & suffix)
|
|||
value.erase(delim);
|
||||
};
|
||||
|
||||
void FormatSpeed(double speedKmPH, string & value, string & suffix)
|
||||
{
|
||||
value = measurement_utils::FormatSpeed(measurement_utils::KmphToMps(speedKmPH));
|
||||
|
||||
size_t const delim = value.find(' ');
|
||||
ASSERT(delim != string::npos, ());
|
||||
suffix = value.substr(delim + 1);
|
||||
value.erase(delim);
|
||||
};
|
||||
|
||||
RoutingSession::RoutingSession()
|
||||
: m_router(nullptr)
|
||||
, m_route(make_shared<Route>(string() /* router */, 0 /* route id */))
|
||||
|
@ -416,8 +406,12 @@ void RoutingSession::GetRouteFollowingInfo(FollowingInfo & info) const
|
|||
|
||||
SpeedInUnits speedLimit;
|
||||
m_route->GetCurrentSpeedLimit(speedLimit);
|
||||
if (speedLimit.IsValid())
|
||||
FormatSpeed(speedLimit.GetSpeedKmPH(), info.m_speedLimit, info.m_speedLimitUnitsSuffix);
|
||||
if (speedLimit.IsNumeric())
|
||||
info.m_speedLimitMps = measurement_utils::KmphToMps(speedLimit.GetSpeedKmPH());
|
||||
else if (speedLimit.GetSpeed() == kNoneMaxSpeed)
|
||||
info.m_speedLimitMps = 0;
|
||||
else
|
||||
info.m_speedLimitMps = -1.0;
|
||||
|
||||
// The turn after the next one.
|
||||
if (m_routingSettings.m_showTurnAfterNext)
|
||||
|
|
|
@ -161,7 +161,7 @@ public:
|
|||
|
||||
void AssignRouteForTesting(std::shared_ptr<Route> route, RouterResultCode e) { AssignRoute(route, e); }
|
||||
|
||||
bool IsSpeedLimitExceeded() const { return m_speedCameraManager.IsSpeedLimitExceeded(); }
|
||||
bool IsSpeedCamLimitExceeded() const { return m_speedCameraManager.IsSpeedLimitExceeded(); }
|
||||
SpeedCameraManager & GetSpeedCamManager() { return m_speedCameraManager; }
|
||||
SpeedCameraManager const & GetSpeedCamManager() const { return m_speedCameraManager; }
|
||||
|
||||
|
|
Проверка
s > 0
лишняя, если значение не может быть отрицательным. А если может, то почему?