diff options
author | Fredrik Karlsson <bjorn.fredrik.karlsson@gmail.com> | 2017-04-16 17:24:17 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-16 17:24:17 +0200 |
commit | 8eb23cbac8a6c5821dc5935e27689700216c3f1a (patch) | |
tree | d02fd3667c8e19daf3724384f4956f16ec0bf01c /platform/ios/src | |
parent | fcf5d1b8af5ca01f8634107f00fbc843c892dd48 (diff) | |
download | qtlocation-mapboxgl-8eb23cbac8a6c5821dc5935e27689700216c3f1a.tar.gz |
[ios] Scale bar (#7631)
* [ios] added a scale bar
* [ios] update design
* [ios] show/hide scale bar
* [ios] Remove the need to localize 0
* [ios] Support for imperial units
* [ios] Round to nearest foot
* [ios] Make scale bar private
* [ios] Update design and clean up
* [ios] Rename and various optimizations
* [ios] RTL support
* [ios] added max scale and removed animations
* [ios] animate scale bar
Diffstat (limited to 'platform/ios/src')
-rw-r--r-- | platform/ios/src/MGLMapView.h | 6 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 45 | ||||
-rw-r--r-- | platform/ios/src/MGLScaleBar.h | 9 | ||||
-rw-r--r-- | platform/ios/src/MGLScaleBar.mm | 365 |
4 files changed, 423 insertions, 2 deletions
diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index 4872ff2448..31320ac977 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -226,6 +226,12 @@ IB_DESIGNABLE - (IBAction)reloadStyle:(id)sender; /** + A control indicating the scale of the map. The scale bar is positioned in the + upper-left corner. The scale bar is hidden by default. + */ +@property (nonatomic, readonly) UIView *scaleBar; + +/** A control indicating the map’s direction and allowing the user to manipulate the direction, positioned in the upper-right corner. */ diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 6badb1d857..fc4d39a143 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -54,6 +54,7 @@ #import "MGLUserLocation_Private.h" #import "MGLAnnotationImage_Private.h" #import "MGLAnnotationView_Private.h" +#import "MGLScaleBar.h" #import "MGLStyle_Private.h" #import "MGLStyleLayer_Private.h" #import "MGLMapboxEvents.h" @@ -229,6 +230,8 @@ public: @property (nonatomic) EAGLContext *context; @property (nonatomic) GLKView *glView; @property (nonatomic) UIImageView *glSnapshotView; +@property (nonatomic, readwrite) MGLScaleBar *scaleBar; +@property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *scaleBarConstraints; @property (nonatomic, readwrite) UIImageView *compassView; @property (nonatomic) NS_MUTABLE_ARRAY_OF(NSLayoutConstraint *) *compassViewConstraints; @property (nonatomic, readwrite) UIImageView *logoView; @@ -493,7 +496,14 @@ public: _compassView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_compassView]; _compassViewConstraints = [NSMutableArray array]; - + + // setup scale control + // + _scaleBar = [[MGLScaleBar alloc] init]; + _scaleBar.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_scaleBar]; + _scaleBarConstraints = [NSMutableArray array]; + // setup interaction // _pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)]; @@ -774,6 +784,31 @@ public: - (void)updateConstraints { + // scale control + // + [self removeConstraints:self.scaleBarConstraints]; + [self.scaleBarConstraints removeAllObjects]; + + [self.scaleBarConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.scaleBar + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeTop + multiplier:1 + constant:5+self.contentInset.top]]; + + [self.scaleBarConstraints addObject: + [NSLayoutConstraint constraintWithItem:self.scaleBar + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:8 + self.contentInset.left]]; + + [self addConstraints:self.scaleBarConstraints]; + // compass // [self removeConstraints:self.compassViewConstraints]; @@ -1564,6 +1599,8 @@ public: { [weakSelf unrotateIfNeededForGesture]; }]; + } else { + [self unrotateIfNeededForGesture]; } } } @@ -4827,7 +4864,11 @@ public: } [self updateCompass]; - + + if (!self.scaleBar.hidden) { + [(MGLScaleBar *)self.scaleBar setMetersPerPoint:[self metersPerPointAtLatitude:self.centerCoordinate.latitude]]; + } + if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)]) { [self.delegate mapViewRegionIsChanging:self]; diff --git a/platform/ios/src/MGLScaleBar.h b/platform/ios/src/MGLScaleBar.h new file mode 100644 index 0000000000..77fd6736b5 --- /dev/null +++ b/platform/ios/src/MGLScaleBar.h @@ -0,0 +1,9 @@ +#import <UIKit/UIKit.h> +#import <CoreLocation/CoreLocation.h> + +@interface MGLScaleBar : UIView + +// Sets the scale and redraws the scale bar +@property (nonatomic, assign) CLLocationDistance metersPerPoint; + +@end diff --git a/platform/ios/src/MGLScaleBar.mm b/platform/ios/src/MGLScaleBar.mm new file mode 100644 index 0000000000..383710ea37 --- /dev/null +++ b/platform/ios/src/MGLScaleBar.mm @@ -0,0 +1,365 @@ +#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<MGLScaleBarLabel *> *labels; +@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; +@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]; +} + +#pragma mark - Dimensions + +- (CGSize)intrinsicContentSize { + return CGSizeMake(self.actualWidth, 16); +} + +- (CGFloat)actualWidth { + return self.row.distance / [self unitsPerPoint]; +} + +- (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 - Convenient 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]; + MGLRow row; + + BOOL useMetric = [self usesMetricSystem]; + NSUInteger count = useMetric + ? sizeof(MGLMetricTable) / sizeof(MGLMetricTable[0]) + : sizeof(MGLImperialTable) / sizeof(MGLImperialTable[0]); + + for (NSUInteger i = 0; i < count; i++) { + CLLocationDistance distance = useMetric ? MGLMetricTable[i].distance : MGLImperialTable[i].distance; + if (distance <= maximumDistance) { + row = useMetric ? MGLMetricTable[i] : MGLImperialTable[i]; + } else { + break; + } + } + + return row; +} + +#pragma mark - Setters + +- (void)setMetersPerPoint:(CLLocationDistance)metersPerPoint { + if (_metersPerPoint == metersPerPoint) { + return; + } + + _metersPerPoint = metersPerPoint; + + [self updateVisibility]; + + self.row = [self preferredRow]; + + [self invalidateIntrinsicContentSize]; + [self setNeedsLayout]; +} + +- (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)]; + [_labels makeObjectsPerformSelector:@selector(removeFromSuperview)]; + _bars = nil; + _labels = nil; +} + +#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; +} + +- (NSArray<UILabel *> *)labels { + if (!_labels) { + NSDecimalNumber *zeroNumber = [NSDecimalNumber decimalNumberWithString:@"0"]; + NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; + NSMutableArray *labels = [NSMutableArray array]; + + for (NSUInteger i = 0; i <= self.row.numberOfBars; i++) { + UILabel *label = [[MGLScaleBarLabel alloc] init]; + label.font = [UIFont systemFontOfSize:8 weight:UIFontWeightMedium]; + label.text = [formatter stringFromNumber:zeroNumber]; + label.clipsToBounds = NO; + [label setNeedsDisplay]; + [label sizeToFit]; + [labels addObject:label]; + [self addSubview:label]; + } + _labels = labels; + } + return _labels; +} + +#pragma mark - Layout + +- (void)layoutSubviews { + [super layoutSubviews]; + + if (!self.row.numberOfBars) { + // Current distance is not within allowed range + return; + } + + [self updateLabels]; + [self layoutBars]; + [self layoutLabels]; +} + +- (void)updateLabels { + NSArray *labels = [self.labels subarrayWithRange:NSMakeRange(1, self.labels.count-1)]; + BOOL useMetric = [self usesMetricSystem]; + NSUInteger i = 0; + + for (MGLScaleBarLabel *label in labels) { + CLLocationDistance barDistance = (self.row.distance / self.row.numberOfBars) * (i + 1); + + if (!useMetric) { + barDistance /= MGLFeetPerMeter; + } + + label.text = [self.formatter stringFromDistance:barDistance]; + [label setNeedsDisplay]; + [label sizeToFit]; + i++; + } +} + +- (void)layoutBars { + CGFloat barWidth = (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 = self.bounds.size.width / self.bars.count; + BOOL RTL = [self usesRightToLeftLayout]; + NSUInteger i = RTL ? self.bars.count : 0; + for (MGLScaleBarLabel *label in self.labels) { + CGFloat xPosition = barWidth * i - CGRectGetMidX(label.bounds) + self.borderWidth; + label.frame = CGRectMake(xPosition, 0, + CGRectGetWidth(label.bounds), + CGRectGetHeight(label.bounds)); + i = RTL ? i-1 : i+1; + } +} + +@end |