[ios] Updated MGLScaleBar to use rendered UIImages instead of MGLScaleBarLabel (#11921)

This commit is contained in:
Julian Rex
2018-05-24 10:29:56 -04:00
committed by GitHub
parent 4067a61c80
commit 9e4a16d7ee
3 changed files with 274 additions and 64 deletions

View File

@@ -19,6 +19,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT
* Improved application launch performance.
* Fixed an issue preventing nested key path expressions get parsed accordingly to the spec. ([#11959](https://github.com/mapbox/mapbox-gl-native/pull/11959))
* Added custom `-hitTest:withEvent:` to `MGLSMCalloutView` to avoid registering taps in transparent areas of the standard annotation callout. ([#11939](https://github.com/mapbox/mapbox-gl-native/pull/11939))
* Improved performance and memory impact of `MGLScaleBar`. ([#11921](https://github.com/mapbox/mapbox-gl-native/pull/11921))
## 4.0.1 - May 14, 2018

View File

@@ -91,6 +91,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsRuntimeStylingRows) {
typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
MBXSettingsMiscellaneousShowReuseQueueStats = 0,
MBXSettingsMiscellaneousWorldTour,
MBXSettingsMiscellaneousRandomTour,
MBXSettingsMiscellaneousShowZoomLevel,
MBXSettingsMiscellaneousScrollView,
MBXSettingsMiscellaneousToggleTwoMaps,
@@ -101,6 +102,65 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
MBXSettingsMiscellaneousDeleteLogFile,
};
// Utility methods
CLLocationCoordinate2D coordinateCentered(CLLocationCoordinate2D origin, CLLocationDegrees bearing, CLLocationDistance distance) {
// Convert to radians
double multiplier = M_PI / 180.0;
double sourceLatitude = multiplier * origin.latitude;
double sourceLongitude = multiplier * origin.longitude;
bearing *= multiplier;
distance /= 6378137.0;
// Pulled from MGLRadianCoordinateAtDistanceFacingDirection:
double latitude = asin((sin(sourceLatitude) * cos(distance)) +
(cos(sourceLatitude) * sin(distance) * cos(bearing)));
double longitude = sourceLongitude + atan2((sin(bearing) * sin(distance) * cos(sourceLatitude)),
cos(distance) - (sin(sourceLatitude) * sin(latitude)));
CLLocationCoordinate2D result;
result.latitude = fmin(85.0, fmax(-85.0, (latitude / multiplier)));
result.longitude = longitude / multiplier;
return result;
}
CLLocationCoordinate2D randomWorldCoordinate() {
static const struct {
CLLocationCoordinate2D coordinate;
CLLocationDistance radius;
} landmasses[] = {
// Rough land masses
{{ 38.328531, 94.778736 }, 4100000 }, // Asia
{{ 1.477244, 18.138111 }, 4100000 }, // Africa
{{ 52.310059, 22.295425 }, 2000000 }, // Europe
{{ 42.344216, -96.532700 }, 3000000 }, // N America
{{ -11.537273, -57.035181 }, 2220000 }, // S America
{{ -20.997030, 134.660541 }, 2220000 }, // Australia
// A few cities
{{ 51.504787, -0.106977 }, 33000 }, // London
{{ 37.740186, -122.437086 }, 8500 }, // SF
{{ 52.509978, 13.406510 }, 12000 }, // Berlin
{{ 12.966246, 77.586505 }, 19000 } // Bengaluru
};
NSInteger index = drand48() * (sizeof(landmasses)/sizeof(landmasses[0]));
CLLocationCoordinate2D coordinate = landmasses[index].coordinate;
CLLocationDistance radius = landmasses[index].radius;
// Now create a world coord
CLLocationDegrees heading = drand48()*360.0;
CLLocationDistance distance = drand48()*radius;
CLLocationCoordinate2D newLocation = coordinateCentered(coordinate, heading, distance);
return newLocation;
}
@interface MBXDroppedPinAnnotation : MGLPointAnnotation
@end
@@ -136,13 +196,11 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
@property (nonatomic) BOOL reuseQueueStatsEnabled;
@property (nonatomic) BOOL showZoomLevelEnabled;
@property (nonatomic) BOOL shouldLimitCameraChanges;
@property (nonatomic) BOOL randomWalk;
@end
@interface MGLMapView (MBXViewController)
@property (nonatomic) NSDictionary *annotationViewReuseQueueByIdentifier;
@end
@implementation MBXViewController
@@ -277,6 +335,8 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
- (IBAction)showSettings:(__unused id)sender
{
self.randomWalk = NO;
UITableViewController *settingsViewController = [[UITableViewController alloc] initWithStyle:UITableViewStyleGrouped];
settingsViewController.tableView.delegate = self;
settingsViewController.tableView.dataSource = self;
@@ -377,6 +437,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
[settingsTitles addObjectsFromArray:@[
[NSString stringWithFormat:@"%@ Reuse Queue Stats", (_reuseQueueStatsEnabled ? @"Hide" :@"Show")],
@"Start World Tour",
@"Random Tour",
[NSString stringWithFormat:@"%@ Zoom/Pitch/Direction Label", (_showZoomLevelEnabled ? @"Hide" :@"Show")],
@"Embedded Map View",
[NSString stringWithFormat:@"%@ Second Map", ([self.view viewWithTag:2] == nil ? @"Show" : @"Hide")],
@@ -580,6 +641,10 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
case MBXSettingsMiscellaneousWorldTour:
[self startWorldTour];
break;
case MBXSettingsMiscellaneousRandomTour:
[self randomWorldTour];
break;
case MBXSettingsMiscellaneousPrintLogFile:
[self printTelemetryLogFile];
break;
@@ -1681,6 +1746,75 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
return filePath;
}
#pragma mark - Random World Tour
- (void)addAnnotations:(NSInteger)numAnnotations aroundCoordinate:(CLLocationCoordinate2D)coordinate radius:(CLLocationDistance)radius {
NSMutableArray *annotations = [[NSMutableArray alloc] initWithCapacity:numAnnotations];
for (NSInteger i = 0; i<numAnnotations; i++) {
CLLocationDegrees heading = drand48()*360.0;
CLLocationDistance distance = drand48()*radius;
CLLocationCoordinate2D newLocation = coordinateCentered(coordinate, heading, distance);
MBXDroppedPinAnnotation *annotation = [[MBXDroppedPinAnnotation alloc] init];
annotation.coordinate = newLocation;
[annotations addObject:annotation];
}
[self.mapView addAnnotations:annotations];
}
- (void)randomWorldTour {
// Consistent initial conditions (consider setting these by test params)
srand48(0);
[self.mapView removeAnnotations:self.mapView.annotations];
[self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(31, -100) zoomLevel:3 animated:NO];
[self randomWorldTourInternal];
}
- (void)randomWorldTourInternal {
self.randomWalk = YES;
// Remove all annotations
NSTimeInterval duration = 16.0;
__weak MBXViewController *weakSelf = self;
// Remove old annotations, half-way through the flight.
NSArray *annotationsToRemove = [self.mapView.annotations copy];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * 0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf.mapView removeAnnotations:annotationsToRemove];
});
MBXDroppedPinAnnotation *annotation = [[MBXDroppedPinAnnotation alloc] init];
annotation.coordinate = randomWorldCoordinate();
[self.mapView addAnnotation:annotation];
// Add annotations around that coord
[self addAnnotations:50 aroundCoordinate:annotation.coordinate radius:100000.0]; // 100km
MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:annotation.coordinate
fromDistance:10000.0
pitch:drand48()*60.0
heading:drand48()*360];
[self.mapView flyToCamera:camera
withDuration:duration
peakAltitude:2000000.0
completionHandler:^{
// This completion handler is currently called BEFORE the
// region did change delegate method, and we don't have a "reason"
// so we can't tell if the motion was cancelled. We use the delegate
// for that, and set self.randomWalk. But since we want a delay
// anyway, we can just check later. Not ideal though..
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
MBXViewController *strongSelf = weakSelf;
if (strongSelf.randomWalk) {
[strongSelf randomWorldTourInternal];
}
});
}];
}
#pragma mark - User Actions
- (IBAction)handleLongPress:(UILongPressGestureRecognizer *)longPress
@@ -2033,6 +2167,10 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
- (void)mapView:(MGLMapView *)mapView regionDidChangeWithReason:(MGLCameraChangeReason)reason animated:(BOOL)animated
{
if (reason != MGLCameraChangeReasonProgrammatic) {
self.randomWalk = NO;
}
[self updateHUD];
}

View File

@@ -75,7 +75,7 @@ static const MGLRow MGLImperialTable[] ={
@class MGLScaleBarLabel;
@interface MGLScaleBar()
@property (nonatomic) NSArray<MGLScaleBarLabel *> *labels;
@property (nonatomic) NSArray<UIView *> *labelViews;
@property (nonatomic) NSArray<UIView *> *bars;
@property (nonatomic) UIView *containerView;
@property (nonatomic) MGLDistanceFormatter *formatter;
@@ -84,6 +84,10 @@ static const MGLRow MGLImperialTable[] ={
@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;
@@ -152,6 +156,29 @@ static const CGFloat MGLFeetPerMeter = 3.28084;
[_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
@@ -190,21 +217,31 @@ static const CGFloat MGLFeetPerMeter = 3.28084;
CLLocationDistance maximumDistance = [self maximumWidth] * [self unitsPerPoint];
BOOL useMetric = [self usesMetricSystem];
MGLRow row = useMetric ? MGLMetricTable[0] : MGLImperialTable[0];
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;
}
const MGLRow *row;
const MGLRow *table;
NSUInteger count;
if (useMetric) {
row = table = MGLMetricTable;
count = sizeof(MGLMetricTable) / sizeof(MGLMetricTable[0]);
}
return row;
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
@@ -255,9 +292,9 @@ static const CGFloat MGLFeetPerMeter = 3.28084;
_row = row;
[_bars makeObjectsPerformSelector:@selector(removeFromSuperview)];
[_labels makeObjectsPerformSelector:@selector(removeFromSuperview)];
_bars = nil;
_labels = nil;
[self updateLabels];
}
#pragma mark - Views
@@ -275,61 +312,91 @@ static const CGFloat MGLFeetPerMeter = 3.28084;
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;
#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;
}
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 = round((CGRectGetWidth(self.bounds) - self.borderWidth * 2.0f) / self.bars.count);
@@ -357,11 +424,15 @@ static const CGFloat MGLFeetPerMeter = 3.28084;
CGFloat barWidth = round(self.bounds.size.width / self.bars.count);
BOOL RTL = [self usesRightToLeftLayout];
NSUInteger i = RTL ? self.bars.count : 0;
for (MGLScaleBarLabel *label in self.labels) {
for (UIView *label in self.labelViews) {
CGFloat xPosition = round(barWidth * i - CGRectGetMidX(label.bounds) + self.borderWidth);
label.frame = CGRectMake(xPosition, 0,
CGRectGetWidth(label.bounds),
CGRectGetHeight(label.bounds));
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;
}
}