Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Commit

Permalink
[ios, osx] Direction formatter
Browse files Browse the repository at this point in the history
Implemented an NSFormatter for formatting CLLocationDirection values as localizable display strings. The formatter supports both absolute compass directions, such as “south”, and relative “clock” directions, such as “6 o’clock”.
  • Loading branch information
1ec5 committed Apr 22, 2016
1 parent e2d20c5 commit c2accc0
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .jazzy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ custom_categories:
- MGLCoordinateSpanMake
- MGLCoordinateSpanZero
- MGLDegreesFromRadians
- MGLDirectionFormatter
- MGLDirectionFormatterOrigin
- MGLRadiansFromDegrees
- MGLStringFromCoordinateBounds
- NSValue(MGLGeometryAdditions)
77 changes: 77 additions & 0 deletions platform/darwin/include/MGLDirectionFormatter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>

#import "MGLTypes.h"

NS_ASSUME_NONNULL_BEGIN

/**
The reference point for an `MGLDirectionFormatter`.
*/
typedef NS_ENUM(NSUInteger, MGLDirectionFormatterOrigin) {
/**
Directions are assumed to be relative to true north and are given as
compass directions, such as “south” for a value of `90`.
*/
MGLDirectionFormatterOriginNorth = 0,
/**
Directions are assumed to be relative to the direction in which the user is
facing and are given as “clock directions”, such as “6 o’clock” for a value
of `90`.
*/
MGLDirectionFormatterOriginStraightAhead,
};

/**
The `MGLDirectionFormatter` class provides properly formatted descriptions of
absolute or relative headings. Use this class to create localized heading
strings when displaying directional information to users.
*/
@interface MGLDirectionFormatter : NSFormatter

/**
The receiver’s reference point. The receiver’s input is assumed to be relative
to this reference point, and its output is given in the conventional form for
directions with this reference point.
This class does not convert between different reference points. To convert an
`CLLocationDirection` with respect to true north into a `CLLocationDirection`
with respect to the direction in which the user is currently facing, use Core
Location to determine the user’s current heading.
The default value of this property is `MGLDirectionFormatterOriginNorth`, which
means a value of `0` is formatted as “north” in the receiver’s locale.
*/
@property (nonatomic) MGLDirectionFormatterOrigin origin;

/**
The unit style used by this formatter.
This property defaults to `NSFormattingUnitStyleMedium`.
*/
@property (nonatomic) NSFormattingUnitStyle unitStyle;

/**
The locale of the receiver.
The locale determines the output language as well as the numeral system used
when the `relativeToUser` property is set to `YES`.
*/
@property (copy) NSLocale *locale;

/**
Returns a heading string for the provided value.
@param direction The heading, measured in degrees.
@return The heading string appropriately formatted for the formatter’s locale.
*/
- (NSString *)stringFromDirection:(CLLocationDirection)direction;

/**
This method is not supported for the `MGLDirectionFormatter` class.
*/
- (BOOL)getObjectValue:(out id __nullable * __nullable)obj forString:(NSString *)string errorDescription:(out NSString * __nullable * __nullable)error;

@end

NS_ASSUME_NONNULL_END
167 changes: 167 additions & 0 deletions platform/darwin/src/MGLDirectionFormatter.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#import "MGLDirectionFormatter.h"

#define wrap(value, min, max) \
(fmod((fmod((value - min), (max - min)) + (max - min)), (max - min)) + min)

@implementation MGLDirectionFormatter {
NSNumberFormatter *_numberFormatter;
}

- (instancetype)init {
if (self = [super init]) {
_unitStyle = NSFormattingUnitStyleMedium;
_numberFormatter = [[NSNumberFormatter alloc] init];
}
return self;
}

- (NSLocale *)locale {
return _numberFormatter.locale;
}

- (void)setLocale:(NSLocale *)locale {
_numberFormatter.locale = locale;
}

- (NSString *)stringFromDirection:(CLLocationDirection)direction {
if (self.origin == MGLDirectionFormatterOriginNorth) {
return [self stringFromAbsoluteDirection:direction];
} else {
return [self stringFromRelativeDirection:direction];
}
}

- (NSString *)stringFromRelativeDirection:(CLLocationDirection)direction {
NSInteger hour = round(-wrap(-direction, -360, 0) / 360 * 12);
NSString *format;
NSNumberFormatterStyle style = NSNumberFormatterDecimalStyle;
switch (self.unitStyle) {
case NSFormattingUnitStyleShort:
format = NSLocalizedString(@"%@:00", @"Relative heading format, short style");
break;

case NSFormattingUnitStyleMedium:
format = NSLocalizedString(@"%@ o’clock", @"Relative heading format, medium style");

break;

case NSFormattingUnitStyleLong:
format = NSLocalizedString(@"%@ o’clock", @"Relative heading format, long style");
style = NSNumberFormatterSpellOutStyle;
break;

default:
break;
}
_numberFormatter.numberStyle = style;
return [NSString stringWithFormat:format, [_numberFormatter stringFromNumber:@(hour)]];
}

- (NSString *)stringFromAbsoluteDirection:(CLLocationDirection)direction {
static NS_ARRAY_OF(NSString *) *shortStrings;
static NS_ARRAY_OF(NSString *) *longStrings;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shortStrings = @[
NSLocalizedString(@"N", @"North, short"),
NSLocalizedString(@"N×E", @"North by east, short"),
NSLocalizedString(@"NNE", @"North-northeast, short"),
NSLocalizedString(@"NE×N", @"Northeast by north, short"),
NSLocalizedString(@"NE", @"Northeast, short"),
NSLocalizedString(@"NE×E", @"Northeast by east, short"),
NSLocalizedString(@"ENE", @"East-northeast, short"),
NSLocalizedString(@"E×N", @"East by north, short"),

NSLocalizedString(@"E", @"East, short"),
NSLocalizedString(@"E×S", @"East by south, short"),
NSLocalizedString(@"ESE", @"East-southeast, short"),
NSLocalizedString(@"SE×E", @"Southeast by east, short"),
NSLocalizedString(@"SE", @"Southeast, short"),
NSLocalizedString(@"SE×S", @"Southeast by south, short"),
NSLocalizedString(@"SSE", @"South-southeast, short"),
NSLocalizedString(@"S×E", @"South by east, short"),

NSLocalizedString(@"S", @"South, short"),
NSLocalizedString(@"S×W", @"South by west, short"),
NSLocalizedString(@"SSW", @"South-southwest, short"),
NSLocalizedString(@"SW×S", @"Southwest by south, short"),
NSLocalizedString(@"SW", @"Southwest, short"),
NSLocalizedString(@"SW×W", @"Southwest by west, short"),
NSLocalizedString(@"WSW", @"West-southwest, short"),
NSLocalizedString(@"W×S", @"West by south, short"),

NSLocalizedString(@"W", @"West, short"),
NSLocalizedString(@"W×N", @"West by north, short"),
NSLocalizedString(@"WNW", @"West-northwest, short"),
NSLocalizedString(@"NW×W", @"Northwest by west, short"),
NSLocalizedString(@"NW", @"Northwest, short"),
NSLocalizedString(@"NW×N", @"Northwest by north, short"),
NSLocalizedString(@"NNW", @"North-northwest, short"),
NSLocalizedString(@"N×W", @"North by west, short"),
];

longStrings = @[
NSLocalizedString(@"north", @"North, long"),
NSLocalizedString(@"north by east", @"North by east, long"),
NSLocalizedString(@"north-northeast", @"North-northeast, long"),
NSLocalizedString(@"northeast by north", @"Northeast by north, long"),
NSLocalizedString(@"northeast", @"Northeast, long"),
NSLocalizedString(@"northeast by east", @"Northeast by east, long"),
NSLocalizedString(@"east-northeast", @"East-northeast, long"),
NSLocalizedString(@"east by north", @"East by north, long"),

NSLocalizedString(@"east", @"East, long"),
NSLocalizedString(@"east by south", @"East by south, long"),
NSLocalizedString(@"east-southeast", @"East-southeast, long"),
NSLocalizedString(@"southeast by east", @"Southeast by east, long"),
NSLocalizedString(@"southeast", @"Southeast, long"),
NSLocalizedString(@"southeast by south", @"Southeast by south, long"),
NSLocalizedString(@"south-southeast", @"South-southeast, long"),
NSLocalizedString(@"south by east", @"South by east, long"),

NSLocalizedString(@"south", @"South, long"),
NSLocalizedString(@"south by west", @"South by west, long"),
NSLocalizedString(@"south-southwest", @"South-southwest, long"),
NSLocalizedString(@"southwest by south", @"Southwest by south, long"),
NSLocalizedString(@"southwest", @"Southwest, long"),
NSLocalizedString(@"southwest by west", @"Southwest by west, long"),
NSLocalizedString(@"west-southwest", @"West-southwest, long"),
NSLocalizedString(@"west by south", @"West by south, long"),

NSLocalizedString(@"west", @"West, long"),
NSLocalizedString(@"west by north", @"West by north, long"),
NSLocalizedString(@"west-northwest", @"West-northwest, long"),
NSLocalizedString(@"northwest by west", @"Northwest by west, long"),
NSLocalizedString(@"northwest", @"Northwest, long"),
NSLocalizedString(@"northwest by north", @"Northwest by north, long"),
NSLocalizedString(@"north-northwest", @"North-northwest, long"),
NSLocalizedString(@"north by west", @"North by west, long"),
];

NSAssert(shortStrings.count == longStrings.count, @"Long and short direction string arrays must have the same size.");
});

NSInteger cardinalPoint = round(wrap(direction, 0, 360) / 360 * shortStrings.count);
switch (self.unitStyle) {
case NSFormattingUnitStyleShort:
return shortStrings[cardinalPoint];

case NSFormattingUnitStyleMedium:
case NSFormattingUnitStyleLong:
return longStrings[cardinalPoint];
}
}

- (nullable NSString *)stringForObjectValue:(id)obj {
if (![obj isKindOfClass:[NSValue class]]) {
return nil;
}
return [self stringFromDirection:[obj doubleValue]];
}

- (BOOL)getObjectValue:(out id __nullable * __nullable)obj forString:(NSString *)string errorDescription:(out NSString * __nullable * __nullable)error {
NSAssert(NO, @"-getObjectValue:forString:errorDescription: has not been implemented");
return NO;
}

@end
111 changes: 111 additions & 0 deletions platform/darwin/test/MGLDirectionFormatterTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#import <Mapbox/Mapbox.h>
#import <XCTest/XCTest.h>

static NSString * const MGLTestLocaleIdentifier = @"en-US";

@interface MGLDirectionFormatterTests : XCTestCase

@end

@implementation MGLDirectionFormatterTests

- (void)testAbsoluteDirections {
MGLDirectionFormatter *shortFormatter = [[MGLDirectionFormatter alloc] init];
XCTAssertEqual(shortFormatter.origin, MGLDirectionFormatterOriginNorth, @"Reference point should be north by default.");
shortFormatter.unitStyle = NSFormattingUnitStyleShort;
shortFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier];

MGLDirectionFormatter *mediumFormatter = [[MGLDirectionFormatter alloc] init];
XCTAssertEqual(mediumFormatter.unitStyle, NSFormattingUnitStyleMedium, @"Unit style should be medium by default.");
mediumFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier];

MGLDirectionFormatter *longFormatter = [[MGLDirectionFormatter alloc] init];
longFormatter.unitStyle = NSFormattingUnitStyleLong;
longFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier];

XCTAssertEqualObjects(@"NW", [shortFormatter stringFromDirection:-45]);
XCTAssertEqualObjects(@"northwest", [mediumFormatter stringFromDirection:-45]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:-45], [longFormatter stringFromDirection:-45]);

XCTAssertEqualObjects(@"N", [shortFormatter stringFromDirection:0]);
XCTAssertEqualObjects(@"north", [mediumFormatter stringFromDirection:0]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:0], [longFormatter stringFromDirection:0]);

XCTAssertEqualObjects(@"N", [shortFormatter stringFromDirection:1]);
XCTAssertEqualObjects(@"north", [mediumFormatter stringFromDirection:1]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:1], [longFormatter stringFromDirection:1]);

XCTAssertEqualObjects(@"N×E", [shortFormatter stringFromDirection:10]);
XCTAssertEqualObjects(@"north by east", [mediumFormatter stringFromDirection:10]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:10], [longFormatter stringFromDirection:10]);

XCTAssertEqualObjects(@"NNE", [shortFormatter stringFromDirection:20]);
XCTAssertEqualObjects(@"north-northeast", [mediumFormatter stringFromDirection:20]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:20], [longFormatter stringFromDirection:20]);

XCTAssertEqualObjects(@"NE", [shortFormatter stringFromDirection:45]);
XCTAssertEqualObjects(@"northeast", [mediumFormatter stringFromDirection:45]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:45], [longFormatter stringFromDirection:45]);

XCTAssertEqualObjects(@"E", [shortFormatter stringFromDirection:90]);
XCTAssertEqualObjects(@"east", [mediumFormatter stringFromDirection:90]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:90], [longFormatter stringFromDirection:90]);

XCTAssertEqualObjects(@"S", [shortFormatter stringFromDirection:180]);
XCTAssertEqualObjects(@"south", [mediumFormatter stringFromDirection:180]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:180], [longFormatter stringFromDirection:180]);

XCTAssertEqualObjects(@"W", [shortFormatter stringFromDirection:270]);
XCTAssertEqualObjects(@"west", [mediumFormatter stringFromDirection:270]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:270], [longFormatter stringFromDirection:270]);

XCTAssertEqualObjects(@"N", [shortFormatter stringFromDirection:360]);
XCTAssertEqualObjects(@"north", [mediumFormatter stringFromDirection:360]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:360], [longFormatter stringFromDirection:360]);

XCTAssertEqualObjects(@"N", [shortFormatter stringFromDirection:720]);
XCTAssertEqualObjects(@"north", [mediumFormatter stringFromDirection:720]);
XCTAssertEqualObjects([mediumFormatter stringFromDirection:720], [longFormatter stringFromDirection:720]);
}

- (void)testRelativeDirections {
MGLDirectionFormatter *shortFormatter = [[MGLDirectionFormatter alloc] init];
shortFormatter.origin = MGLDirectionFormatterOriginStraightAhead;
shortFormatter.unitStyle = NSFormattingUnitStyleShort;
shortFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier];

MGLDirectionFormatter *mediumFormatter = [[MGLDirectionFormatter alloc] init];
mediumFormatter.origin = MGLDirectionFormatterOriginStraightAhead;
mediumFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier];

MGLDirectionFormatter *longFormatter = [[MGLDirectionFormatter alloc] init];
longFormatter.origin = MGLDirectionFormatterOriginStraightAhead;
longFormatter.unitStyle = NSFormattingUnitStyleLong;
longFormatter.locale = [NSLocale localeWithLocaleIdentifier:MGLTestLocaleIdentifier];

XCTAssertEqualObjects(@"9:00", [shortFormatter stringFromDirection:-90]);
XCTAssertEqualObjects(@"9 o’clock", [mediumFormatter stringFromDirection:-90]);
XCTAssertEqualObjects(@"nine o’clock", [longFormatter stringFromDirection:-90]);

XCTAssertEqualObjects(@"12:00", [shortFormatter stringFromDirection:0]);
XCTAssertEqualObjects(@"12 o’clock", [mediumFormatter stringFromDirection:0]);
XCTAssertEqualObjects(@"twelve o’clock", [longFormatter stringFromDirection:0]);

XCTAssertEqualObjects(@"2:00", [shortFormatter stringFromDirection:45]);
XCTAssertEqualObjects(@"2 o’clock", [mediumFormatter stringFromDirection:45]);
XCTAssertEqualObjects(@"two o’clock", [longFormatter stringFromDirection:45]);

XCTAssertEqualObjects(@"3:00", [shortFormatter stringFromDirection:90]);
XCTAssertEqualObjects(@"3 o’clock", [mediumFormatter stringFromDirection:90]);
XCTAssertEqualObjects(@"three o’clock", [longFormatter stringFromDirection:90]);

XCTAssertEqualObjects(@"6:00", [shortFormatter stringFromDirection:180]);
XCTAssertEqualObjects(@"6 o’clock", [mediumFormatter stringFromDirection:180]);
XCTAssertEqualObjects(@"six o’clock", [longFormatter stringFromDirection:180]);

XCTAssertEqualObjects(@"9:00", [shortFormatter stringFromDirection:270]);
XCTAssertEqualObjects(@"9 o’clock", [mediumFormatter stringFromDirection:270]);
XCTAssertEqualObjects(@"nine o’clock", [longFormatter stringFromDirection:270]);
}

@end
2 changes: 1 addition & 1 deletion platform/ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CON
- Fixed an issue preventing KVO change notifications from being generated on MGLMapView’s `userTrackingMode` key path when `-setUserTrackingMode:animated:` is called. ([#4724](https://github.com/mapbox/mapbox-gl-native/pull/4724))
- Rendering now occurs on the main thread, fixing a hang when calling `-[MGLMapView styleURL]` before the map view has fully loaded or while the application is in the background. ([#2909](https://github.com/mapbox/mapbox-gl-native/pull/2909))
- Added category methods on NSValue for converting to and from the structure types defined in MGLGeometry.h. ([#4802](https://github.com/mapbox/mapbox-gl-native/pull/4802))
- Added MGLCoordinateFormatter for converting geographic coordinates into display strings. ([#4802](https://github.com/mapbox/mapbox-gl-native/pull/4802))
- Added NSFormatter subclasses for converting geographic coordinates and directions into display strings. ([#4802](https://github.com/mapbox/mapbox-gl-native/pull/4802))
- Added a `-reloadStyle:` action to MGLMapView to force a reload of the current style. ([#4728](https://github.com/mapbox/mapbox-gl-native/pull/4728))
- A more specific user agent string is now sent with style and tile requests. ([#4012](https://github.com/mapbox/mapbox-gl-native/pull/4012))
- Mapbox Telemetry is automatically disabled while the host application is running in the iOS Simulator. ([#4726](https://github.com/mapbox/mapbox-gl-native/pull/4726))
Expand Down
1 change: 1 addition & 0 deletions platform/ios/framework/Mapbox.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ FOUNDATION_EXPORT const unsigned char MapboxVersionString[];
#import "MGLAnnotation.h"
#import "MGLAnnotationImage.h"
#import "MGLCalloutView.h"
#import "MGLDirectionFormatter.h"
#import "MGLCoordinateFormatter.h"
#import "MGLMapCamera.h"
#import "MGLGeometry.h"
Expand Down
Loading

0 comments on commit c2accc0

Please sign in to comment.