summaryrefslogtreecommitdiff
path: root/platform/darwin/src/MGLAttributionInfo.mm
diff options
context:
space:
mode:
authorMinh Nguyễn <mxn@1ec5.org>2016-12-08 17:43:17 -0800
committerGitHub <noreply@github.com>2016-12-08 17:43:17 -0800
commitfe53af7de031e296cdb9b0a7e88688fd3f54e0d8 (patch)
treed51f217f5e90db30fb4447c17cf899c678688ed9 /platform/darwin/src/MGLAttributionInfo.mm
parent89d4c40d927388f44e00e004e8a22db2d1b2eeab (diff)
downloadqtlocation-mapboxgl-fe53af7de031e296cdb9b0a7e88688fd3f54e0d8.tar.gz
[ios, macos] Source-driven attribution (#5999)
* [ios, macos] Source-driven attribution Refactored MGLSource initialization. Implemented a new private class, MGLAttributionInfo, that parses an HTML attribution string from TileJSON and stores the resulting structured data. Added methods to MGLTileSet and MGLStyle to aggregate MGLAttributionInfos. On macOS, update the attribution view as soon as the source attribution changes. On iOS, fetch the current attribution information when displaying the action sheet. Removed hard-coded attribution strings. * [macos] Respect inline formatting in attribution HTML Apply a default font and color to attribution HTML as it is imported into an attributed string. Pass the attributed string into MGLAttributionButton as is, without stripping formatting. Avoid overriding the font and color after importing the HTML, in case these attributes are explicitly specified rather than intrinsic to a hyperlink. Constrain the top of the attribution view to all the attribution buttons, in case one of them needs additional headspace. * [ios, macos] Display unlinked attribution strings Unlinked attribution strings are represented on macOS as buttons that have the default cursor and do nothing when clicked. On iOS, they are action sheet buttons that do nothing but dismiss the action sheet. * [macos] Fixed random Auto Layout exception Auto Layout randomly finds itself unable to satisfy constraints when updating attribution, due to some spurious constraints between attribution buttons. Regenerate the entire attribution view every time the source attribution changes. * [ios, macos] Thoroughly dedupe attribution infos Also added a test to verify parity with the GL JS implementation. This implementation avoids sorting. * [ios, macos] Trim whitespace from attribution strings Also added parsing tests. * [ios, macos] Added attribution parsing tests for styles Included an emoji test to ensure that attribution strings are interpreted as UTF-8, to avoid mojibake. Included a test of removing the underline from a leading copyright symbol. * [ios, macos] Derive feedback link from source MGLAttributionInfo now detects feedback links in the attribution HTML code, and it is responsible for tailoring the feedback URL to the current viewport. Removed the hard-coded feedback action from the attribution sheet on iOS in favor of a source-derived feedback title and URL. Moved the feedback action from macosapp to MGLMapView; applications are now expected to hook an Improve This Map menu item to an MGLMapView action.
Diffstat (limited to 'platform/darwin/src/MGLAttributionInfo.mm')
-rw-r--r--platform/darwin/src/MGLAttributionInfo.mm178
1 files changed, 178 insertions, 0 deletions
diff --git a/platform/darwin/src/MGLAttributionInfo.mm b/platform/darwin/src/MGLAttributionInfo.mm
new file mode 100644
index 0000000000..2719aef7ca
--- /dev/null
+++ b/platform/darwin/src/MGLAttributionInfo.mm
@@ -0,0 +1,178 @@
+#import "MGLAttributionInfo.h"
+
+#if TARGET_OS_IPHONE
+ #import <UIKit/UIKit.h>
+#else
+ #import <Cocoa/Cocoa.h>
+#endif
+
+#import "MGLMapCamera.h"
+#import "NSString+MGLAdditions.h"
+
+#include <string>
+
+@implementation MGLAttributionInfo
+
++ (NS_ARRAY_OF(MGLAttributionInfo *) *)attributionInfosFromHTMLString:(NSString *)htmlString fontSize:(CGFloat)fontSize linkColor:(nullable MGLColor *)linkColor {
+ NSDictionary *options = @{
+ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
+ NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding),
+ };
+ // Apply a bogus, easily detectable style rule to any feedback link, since
+ // NSAttributedString doesn’t preserve the class attribute.
+ NSMutableString *css = [NSMutableString stringWithString:
+ @".mapbox-improve-map { -webkit-text-stroke-width: 1000px; }"];
+ if (fontSize) {
+ [css appendFormat:@"html { font-size: %.1fpx; }", fontSize];
+ }
+ if (linkColor) {
+ CGFloat red;
+ CGFloat green;
+ CGFloat blue;
+ CGFloat alpha;
+#if !TARGET_OS_IPHONE
+ linkColor = [linkColor colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
+#endif
+ [linkColor getRed:&red green:&green blue:&blue alpha:&alpha];
+ [css appendFormat:
+ @"a:link { color: rgba(%f%%, %f%%, %f%%, %f); }",
+ red * 100, green * 100, blue * 100, alpha];
+ }
+ NSString *styledHTML = [NSString stringWithFormat:@"<style type='text/css'>%@</style>%@", css, htmlString];
+ NSData *htmlData = [styledHTML dataUsingEncoding:NSUTF8StringEncoding];
+
+#if TARGET_OS_IPHONE
+ NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithData:htmlData
+ options:options
+ documentAttributes:nil
+ error:NULL];
+#else
+ NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithHTML:htmlData
+ options:options
+ documentAttributes:nil];
+#endif
+
+ NSMutableArray *infos = [NSMutableArray array];
+ [attributedString enumerateAttribute:NSLinkAttributeName
+ inRange:attributedString.mgl_wholeRange
+ options:0
+ usingBlock:
+ ^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
+ NSCAssert(!value || [value isKindOfClass:[NSURL class]], @"If present, URL attribute must be an NSURL.");
+
+ // Detect feedback links by the bogus style rule applied above.
+ NSNumber *strokeWidth = [attributedString attribute:NSStrokeWidthAttributeName
+ atIndex:range.location
+ effectiveRange:NULL];
+ BOOL isFeedbackLink = NO;
+ if ([strokeWidth floatValue] > 100) {
+ isFeedbackLink = YES;
+ [attributedString removeAttribute:NSStrokeWidthAttributeName range:range];
+ }
+
+ // Omit whitespace-only strings.
+ NSAttributedString *title = [[attributedString attributedSubstringFromRange:range]
+ mgl_attributedStringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ if (!title.length) {
+ return;
+ }
+
+ MGLAttributionInfo *info = [[MGLAttributionInfo alloc] initWithTitle:title URL:value];
+ info.feedbackLink = isFeedbackLink;
+ [infos addObject:info];
+ }];
+ return infos;
+}
+
+- (instancetype)initWithTitle:(NSAttributedString *)title URL:(NSURL *)URL {
+ if (self = [super init]) {
+ _title = title;
+ _URL = URL;
+ }
+ return self;
+}
+
+- (nullable NSURL *)feedbackURLAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel {
+ if (!self.feedbackLink) {
+ return nil;
+ }
+
+ NSURLComponents *components = [NSURLComponents componentsWithURL:self.URL resolvingAgainstBaseURL:NO];
+ components.fragment = [NSString stringWithFormat:@"/%.5f/%.5f/%i",
+ centerCoordinate.longitude, centerCoordinate.latitude, (int)round(zoomLevel + 1)];
+ return components.URL;
+}
+
+- (BOOL)isEqual:(id)object {
+ return [object isKindOfClass:[self class]] && [[object title] isEqual:self.title] && [[object URL] isEqual:self.URL];
+}
+
+- (NSUInteger)hash {
+ return self.title.hash + self.URL.hash;
+}
+
+/**
+ Returns whether the given attribution info object overlaps with the receiver by
+ its plain text title.
+
+ @return `NSOrderedAscending` if the given object is a superset of the receiver,
+ `NSOrderedDescending` if it is a subset of the receiver, or `NSOrderedSame`
+ if there is no overlap.
+ */
+- (NSComparisonResult)subsetCompare:(MGLAttributionInfo *)otherInfo {
+ NSString *title = self.title.string;
+ NSString *otherTitle = otherInfo.title.string;
+ if ([title containsString:otherTitle]) {
+ return NSOrderedDescending;
+ }
+ if ([otherTitle containsString:title]) {
+ return NSOrderedAscending;
+ }
+ return NSOrderedSame;
+}
+
+@end
+
+@implementation NSMutableArray (MGLAttributionInfoAdditions)
+
+- (void)growArrayByAddingAttributionInfo:(MGLAttributionInfo *)info {
+ __block BOOL didInsertInfo = NO;
+ __block BOOL shouldAddInfo = YES;
+ [self enumerateObjectsUsingBlock:^(MGLAttributionInfo * _Nonnull existingInfo, NSUInteger idx, BOOL * _Nonnull stop) {
+ switch ([info subsetCompare:existingInfo]) {
+ case NSOrderedDescending:
+ // The existing info object is a subset of the one we’re adding.
+ // Replace the existing object the first time we find a subset;
+ // remove the existing object every time after that.
+ if (didInsertInfo) {
+ [self removeObjectAtIndex:idx];
+ } else {
+ [self replaceObjectAtIndex:idx withObject:info];
+ didInsertInfo = YES;
+ }
+ break;
+
+ case NSOrderedAscending:
+ // The info object we’re adding is a subset of the existing one.
+ // Don’t add the object and stop looking.
+ shouldAddInfo = NO;
+ *stop = YES;
+ break;
+
+ default:
+ break;
+ }
+ }];
+ if (shouldAddInfo && !didInsertInfo) {
+ // No overlapping infos were found, so append the info object.
+ [self addObject:info];
+ }
+}
+
+- (void)growArrayByAddingAttributionInfosFromArray:(NS_ARRAY_OF(MGLAttributionInfo *) *)infos {
+ for (MGLAttributionInfo *info in infos) {
+ [self growArrayByAddingAttributionInfo:info];
+ }
+}
+
+@end