441 lines
14 KiB
Plaintext
441 lines
14 KiB
Plaintext
#import <Mapbox/Mapbox.h>
|
|
#import "MGLScaleBar.h"
|
|
|
|
static const CGFloat MGLFeetPerMile = 5280;
|
|
|
|
struct MGLRow {
|
|
CLLocationDistance distance;
|
|
NSUInteger numberOfBars;
|
|
};
|
|
|
|
static const MGLRow MGLMetricTable[] = {
|
|
{.distance = 1, .numberOfBars = 2},
|
|
{.distance = 2, .numberOfBars = 2},
|
|
{.distance = 4, .numberOfBars = 2},
|
|
{.distance = 10, .numberOfBars = 2},
|
|
{.distance = 20, .numberOfBars = 2},
|
|
{.distance = 50, .numberOfBars = 2},
|
|
{.distance = 75, .numberOfBars = 3},
|
|
{.distance = 100, .numberOfBars = 2},
|
|
{.distance = 150, .numberOfBars = 2},
|
|
{.distance = 200, .numberOfBars = 2},
|
|
{.distance = 300, .numberOfBars = 3},
|
|
{.distance = 500, .numberOfBars = 2},
|
|
{.distance = 1000, .numberOfBars = 2},
|
|
{.distance = 1500, .numberOfBars = 2},
|
|
{.distance = 3000, .numberOfBars = 3},
|
|
{.distance = 5000, .numberOfBars = 2},
|
|
{.distance = 10000, .numberOfBars = 2},
|
|
{.distance = 20000, .numberOfBars = 2},
|
|
{.distance = 30000, .numberOfBars = 3},
|
|
{.distance = 50000, .numberOfBars = 2},
|
|
{.distance = 100000, .numberOfBars = 2},
|
|
{.distance = 200000, .numberOfBars = 2},
|
|
{.distance = 300000, .numberOfBars = 3},
|
|
{.distance = 400000, .numberOfBars = 2},
|
|
{.distance = 500000, .numberOfBars = 2},
|
|
{.distance = 600000, .numberOfBars = 3},
|
|
{.distance = 800000, .numberOfBars = 2},
|
|
};
|
|
|
|
static const MGLRow MGLImperialTable[] ={
|
|
{.distance = 4, .numberOfBars = 2},
|
|
{.distance = 6, .numberOfBars = 2},
|
|
{.distance = 10, .numberOfBars = 2},
|
|
{.distance = 20, .numberOfBars = 2},
|
|
{.distance = 30, .numberOfBars = 2},
|
|
{.distance = 50, .numberOfBars = 2},
|
|
{.distance = 75, .numberOfBars = 3},
|
|
{.distance = 100, .numberOfBars = 2},
|
|
{.distance = 200, .numberOfBars = 2},
|
|
{.distance = 300, .numberOfBars = 3},
|
|
{.distance = 400, .numberOfBars = 2},
|
|
{.distance = 600, .numberOfBars = 3},
|
|
{.distance = 800, .numberOfBars = 2},
|
|
{.distance = 1000, .numberOfBars = 2},
|
|
{.distance = 0.25f*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 0.5f*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 1*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 2*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 3*MGLFeetPerMile, .numberOfBars = 3},
|
|
{.distance = 4*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 8*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 12*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 15*MGLFeetPerMile, .numberOfBars = 3},
|
|
{.distance = 20*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 30*MGLFeetPerMile, .numberOfBars = 3},
|
|
{.distance = 40*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 80*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 120*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 200*MGLFeetPerMile, .numberOfBars = 2},
|
|
{.distance = 300*MGLFeetPerMile, .numberOfBars = 3},
|
|
{.distance = 400*MGLFeetPerMile, .numberOfBars = 2},
|
|
};
|
|
|
|
@class MGLScaleBarLabel;
|
|
|
|
@interface MGLScaleBar()
|
|
@property (nonatomic) NSArray<UIView *> *labelViews;
|
|
@property (nonatomic) NSArray<UIView *> *bars;
|
|
@property (nonatomic) UIView *containerView;
|
|
@property (nonatomic) MGLDistanceFormatter *formatter;
|
|
@property (nonatomic, assign) MGLRow row;
|
|
@property (nonatomic) UIColor *primaryColor;
|
|
@property (nonatomic) UIColor *secondaryColor;
|
|
@property (nonatomic) CALayer *borderLayer;
|
|
@property (nonatomic, assign) CGFloat borderWidth;
|
|
@property (nonatomic) NSCache* labelImageCache;
|
|
@property (nonatomic) MGLScaleBarLabel* prototypeLabel;
|
|
|
|
|
|
@end
|
|
|
|
static const CGFloat MGLBarHeight = 4;
|
|
static const CGFloat MGLFeetPerMeter = 3.28084;
|
|
|
|
@interface MGLScaleBarLabel : UILabel
|
|
@end
|
|
|
|
@implementation MGLScaleBarLabel
|
|
|
|
- (void)drawTextInRect:(CGRect)rect {
|
|
CGSize shadowOffset = self.shadowOffset;
|
|
UIColor *textColor = self.textColor;
|
|
|
|
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
CGContextSetLineWidth(context, 2);
|
|
CGContextSetLineJoin(context, kCGLineJoinRound);
|
|
|
|
CGContextSetTextDrawingMode(context, kCGTextStroke);
|
|
self.textColor = [UIColor whiteColor];
|
|
[super drawTextInRect:rect];
|
|
|
|
CGContextSetTextDrawingMode(context, kCGTextFill);
|
|
self.textColor = textColor;
|
|
self.shadowOffset = CGSizeMake(0, 0);
|
|
[super drawTextInRect:rect];
|
|
|
|
self.shadowOffset = shadowOffset;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation MGLScaleBar
|
|
|
|
- (instancetype)initWithCoder:(NSCoder *)decoder {
|
|
if (self = [super initWithCoder:decoder]) {
|
|
[self commonInit];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame {
|
|
if (self = [super initWithFrame:frame]) {
|
|
[self commonInit];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)commonInit {
|
|
_primaryColor = [UIColor colorWithRed:18.0/255.0 green:45.0/255.0 blue:17.0/255.0 alpha:1];
|
|
_secondaryColor = [UIColor colorWithRed:247.0/255.0 green:247.0/255.0 blue:247.0/255.0 alpha:1];
|
|
_borderWidth = 1.0f;
|
|
|
|
self.clipsToBounds = NO;
|
|
self.hidden = YES;
|
|
|
|
_containerView = [[UIView alloc] init];
|
|
_containerView.clipsToBounds = YES;
|
|
_containerView.backgroundColor = self.secondaryColor;
|
|
[self addSubview:_containerView];
|
|
|
|
_borderLayer = [CAShapeLayer layer];
|
|
_borderLayer.borderColor = [self.primaryColor CGColor];
|
|
_borderLayer.borderWidth = 1.0f / [[UIScreen mainScreen] scale];
|
|
|
|
[_containerView.layer addSublayer:_borderLayer];
|
|
|
|
_formatter = [[MGLDistanceFormatter alloc] init];
|
|
|
|
// Image labels are now images
|
|
_labelImageCache = [[NSCache alloc] init];
|
|
_prototypeLabel = [[MGLScaleBarLabel alloc] init];
|
|
_prototypeLabel.font = [UIFont systemFontOfSize:8 weight:UIFontWeightMedium];
|
|
_prototypeLabel.clipsToBounds = NO;
|
|
|
|
NSUInteger numberOfLabels = 4;
|
|
NSMutableArray *labelViews = [NSMutableArray arrayWithCapacity:numberOfLabels];
|
|
|
|
for (NSUInteger i = 0; i < numberOfLabels; i++) {
|
|
UIView *view = [[UIView alloc] init];
|
|
view.bounds = CGRectZero;
|
|
view.clipsToBounds = NO;
|
|
view.contentMode = UIViewContentModeCenter;
|
|
view.hidden = YES;
|
|
[labelViews addObject:view];
|
|
[self addSubview:view];
|
|
}
|
|
_labelViews = [labelViews copy];
|
|
|
|
// Zero is a special case (no formatting)
|
|
[self addZeroLabel];
|
|
}
|
|
|
|
#pragma mark - Dimensions
|
|
|
|
- (CGSize)intrinsicContentSize {
|
|
return CGSizeMake(self.actualWidth, 16);
|
|
}
|
|
|
|
- (CGFloat)actualWidth {
|
|
CGFloat width = self.row.distance / [self unitsPerPoint];
|
|
return !isnan(width) ? width : 0;
|
|
}
|
|
|
|
- (CGFloat)maximumWidth {
|
|
CGFloat fullWidth = CGRectGetWidth(self.superview.bounds);
|
|
CGFloat padding = [self usesRightToLeftLayout] ? fullWidth - CGRectGetMaxX(self.frame) : CGRectGetMinX(self.frame);
|
|
return floorf(fullWidth / 2 - padding);
|
|
}
|
|
|
|
- (CGFloat)unitsPerPoint {
|
|
return [self usesMetricSystem] ? self.metersPerPoint : self.metersPerPoint * MGLFeetPerMeter;
|
|
}
|
|
|
|
#pragma mark - Convenience methods
|
|
|
|
- (BOOL)usesRightToLeftLayout {
|
|
return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.superview.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft;
|
|
}
|
|
|
|
- (BOOL)usesMetricSystem {
|
|
NSLocale *locale = [NSLocale currentLocale];
|
|
return [[locale objectForKey:NSLocaleUsesMetricSystem] boolValue];
|
|
}
|
|
|
|
- (MGLRow)preferredRow {
|
|
CLLocationDistance maximumDistance = [self maximumWidth] * [self unitsPerPoint];
|
|
|
|
BOOL useMetric = [self usesMetricSystem];
|
|
|
|
const MGLRow *row;
|
|
const MGLRow *table;
|
|
NSUInteger count;
|
|
|
|
if (useMetric) {
|
|
row = table = MGLMetricTable;
|
|
count = sizeof(MGLMetricTable) / sizeof(MGLMetricTable[0]);
|
|
}
|
|
else {
|
|
row = table = MGLImperialTable;
|
|
count = sizeof(MGLImperialTable) / sizeof(MGLImperialTable[0]);
|
|
}
|
|
|
|
while (row < table + count) {
|
|
if (row->distance > maximumDistance) {
|
|
// use the previous row
|
|
NSAssert(row != table, @"");
|
|
return *(row - 1);
|
|
}
|
|
++row;
|
|
}
|
|
|
|
// Didn't find it, just return the first.
|
|
return *table;
|
|
}
|
|
|
|
#pragma mark - Setters
|
|
|
|
- (void)setMetersPerPoint:(CLLocationDistance)metersPerPoint {
|
|
if (_metersPerPoint == metersPerPoint) {
|
|
return;
|
|
}
|
|
|
|
_metersPerPoint = metersPerPoint;
|
|
|
|
[self updateVisibility];
|
|
|
|
self.row = [self preferredRow];
|
|
|
|
CGSize size = self.intrinsicContentSize;
|
|
self.frame = CGRectMake(CGRectGetMinX(self.frame),
|
|
CGRectGetMinY(self.frame),
|
|
size.width,
|
|
size.height);
|
|
}
|
|
|
|
- (void)updateVisibility {
|
|
BOOL metric = [self usesMetricSystem];
|
|
|
|
NSUInteger count = metric
|
|
? sizeof(MGLMetricTable) / sizeof(MGLMetricTable[0])
|
|
: sizeof(MGLImperialTable) / sizeof(MGLImperialTable[0]);
|
|
|
|
CLLocationDistance maximumDistance = [self maximumWidth] * [self unitsPerPoint];
|
|
CLLocationDistance allowedDistance = metric
|
|
? MGLMetricTable[count-1].distance
|
|
: MGLImperialTable[count-1].distance;
|
|
|
|
CGFloat alpha = maximumDistance > allowedDistance ? .0f : 1.0f;
|
|
|
|
if (self.alpha != alpha) {
|
|
[UIView animateWithDuration:.2f delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
|
|
self.alpha = alpha;
|
|
} completion:nil];
|
|
}
|
|
}
|
|
|
|
- (void)setRow:(MGLRow)row {
|
|
if (_row.distance == row.distance) {
|
|
return;
|
|
}
|
|
|
|
_row = row;
|
|
[_bars makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
|
_bars = nil;
|
|
|
|
[self updateLabels];
|
|
}
|
|
|
|
#pragma mark - Views
|
|
|
|
- (NSArray<UIView *> *)bars {
|
|
if (!_bars) {
|
|
NSMutableArray *bars = [NSMutableArray array];
|
|
for (NSUInteger i = 0; i < self.row.numberOfBars; i++) {
|
|
UIView *bar = [[UIView alloc] init];
|
|
[bars addObject:bar];
|
|
[self.containerView addSubview:bar];
|
|
}
|
|
_bars = bars;
|
|
}
|
|
return _bars;
|
|
}
|
|
|
|
#pragma mark - Labels
|
|
|
|
- (void)addZeroLabel {
|
|
NSDecimalNumber *zeroNumber = [NSDecimalNumber decimalNumberWithString:@"0"];
|
|
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
|
|
NSString *text = [formatter stringFromNumber:zeroNumber];
|
|
|
|
UIImage* image = [self imageForLabelText:text];
|
|
[self.labelImageCache setObject:image forKey:@(0)];
|
|
}
|
|
|
|
- (UIImage*)imageForLabelText:(NSString*)text {
|
|
self.prototypeLabel.text = text;
|
|
[self.prototypeLabel setNeedsDisplay];
|
|
[self.prototypeLabel sizeToFit];
|
|
|
|
// Now render
|
|
UIGraphicsBeginImageContextWithOptions(self.prototypeLabel.bounds.size, NO, 0.0);
|
|
[self.prototypeLabel.layer renderInContext: UIGraphicsGetCurrentContext()];
|
|
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
|
UIGraphicsEndImageContext();
|
|
return image;
|
|
}
|
|
|
|
- (UIImage*)cachedLabelImageForDistance:(CLLocationDistance)barDistance {
|
|
// Make a slightly nicer key, rather than something that's a double.
|
|
NSUInteger floorDist = (NSUInteger)(barDistance*100);
|
|
|
|
NSNumber *key = @(floorDist);
|
|
UIImage *cachedImage = [self.labelImageCache objectForKey:key];
|
|
|
|
if (cachedImage) {
|
|
return cachedImage;
|
|
}
|
|
|
|
// Calc it
|
|
NSString *text = [self.formatter stringFromDistance:barDistance];
|
|
UIImage *image = [self imageForLabelText:text];
|
|
|
|
[self.labelImageCache setObject:image forKey:key];
|
|
|
|
return image;
|
|
}
|
|
|
|
- (void)updateLabels {
|
|
NSEnumerator<UIView*> *viewEnumerator = [self.labelViews objectEnumerator];
|
|
NSInteger i = 0;
|
|
CLLocationDistance multiplier = (self.row.distance / self.row.numberOfBars);
|
|
|
|
if (![self usesMetricSystem]) {
|
|
multiplier /= MGLFeetPerMeter;
|
|
}
|
|
|
|
for (; i <= self.row.numberOfBars; i++) {
|
|
UIView *labelView = [viewEnumerator nextObject];
|
|
labelView.hidden = NO;
|
|
|
|
CLLocationDistance barDistance = multiplier * i;
|
|
UIImage *image = [self cachedLabelImageForDistance:barDistance];
|
|
|
|
labelView.layer.contents = (id)image.CGImage;
|
|
labelView.layer.contentsScale = image.scale;
|
|
}
|
|
|
|
// Hide the rest.
|
|
for (; i < self.labelViews.count; i++) {
|
|
UIView *labelView = [viewEnumerator nextObject];
|
|
labelView.hidden = YES;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Layout
|
|
|
|
- (void)layoutSubviews {
|
|
[super layoutSubviews];
|
|
|
|
if (!self.row.numberOfBars) {
|
|
// Current distance is not within allowed range
|
|
return;
|
|
}
|
|
|
|
[self layoutBars];
|
|
[self layoutLabels];
|
|
}
|
|
|
|
- (void)layoutBars {
|
|
CGFloat barWidth = round((CGRectGetWidth(self.bounds) - self.borderWidth * 2.0f) / self.bars.count);
|
|
|
|
NSUInteger i = 0;
|
|
for (UIView *bar in self.bars) {
|
|
CGFloat xPosition = barWidth * i + self.borderWidth;
|
|
bar.backgroundColor = (i % 2 == 0) ? self.primaryColor : self.secondaryColor;
|
|
bar.frame = CGRectMake(xPosition, self.borderWidth, barWidth, MGLBarHeight);
|
|
i++;
|
|
}
|
|
|
|
self.containerView.frame = CGRectMake(CGRectGetMinX(self.bars.firstObject.frame),
|
|
CGRectGetMaxY(self.bounds)-MGLBarHeight,
|
|
self.actualWidth,
|
|
MGLBarHeight+self.borderWidth*2);
|
|
|
|
[CATransaction begin];
|
|
[CATransaction setDisableActions:YES];
|
|
self.borderLayer.frame = CGRectInset(self.containerView.bounds, self.borderWidth, self.borderWidth);
|
|
self.borderLayer.zPosition = FLT_MAX;
|
|
[CATransaction commit];
|
|
}
|
|
|
|
- (void)layoutLabels {
|
|
CGFloat barWidth = round(self.bounds.size.width / self.bars.count);
|
|
BOOL RTL = [self usesRightToLeftLayout];
|
|
NSUInteger i = RTL ? self.bars.count : 0;
|
|
for (UIView *label in self.labelViews) {
|
|
CGFloat xPosition = round(barWidth * i - CGRectGetMidX(label.bounds) + self.borderWidth);
|
|
CGFloat yPosition = round(0.5 * (CGRectGetMaxY(self.bounds) - MGLBarHeight));
|
|
|
|
CGRect frame = label.frame;
|
|
frame.origin.x = xPosition;
|
|
frame.origin.y = yPosition;
|
|
label.frame = frame;
|
|
|
|
i = RTL ? i-1 : i+1;
|
|
}
|
|
}
|
|
|
|
@end
|