summaryrefslogtreecommitdiff
path: root/platform/darwin
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
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')
-rw-r--r--platform/darwin/src/MGLAttributionInfo.h58
-rw-r--r--platform/darwin/src/MGLAttributionInfo.mm178
-rw-r--r--platform/darwin/src/MGLGeoJSONSource.mm6
-rw-r--r--platform/darwin/src/MGLGeoJSONSource_Private.h3
-rw-r--r--platform/darwin/src/MGLRasterSource.mm14
-rw-r--r--platform/darwin/src/MGLRasterSource_Private.h13
-rw-r--r--platform/darwin/src/MGLSource.mm8
-rw-r--r--platform/darwin/src/MGLSource_Private.h12
-rw-r--r--platform/darwin/src/MGLStyle.mm47
-rw-r--r--platform/darwin/src/MGLStyle_Private.h5
-rw-r--r--platform/darwin/src/MGLTileSet.h48
-rw-r--r--platform/darwin/src/MGLTileSet.mm8
-rw-r--r--platform/darwin/src/MGLTileSet_Private.h17
-rw-r--r--platform/darwin/src/MGLVectorSource.mm14
-rw-r--r--platform/darwin/src/MGLVectorSource_Private.h13
-rw-r--r--platform/darwin/src/NSString+MGLAdditions.h13
-rw-r--r--platform/darwin/src/NSString+MGLAdditions.m25
-rw-r--r--platform/darwin/test/MGLAttributionInfoTests.m115
18 files changed, 549 insertions, 48 deletions
diff --git a/platform/darwin/src/MGLAttributionInfo.h b/platform/darwin/src/MGLAttributionInfo.h
new file mode 100644
index 0000000000..c0cb1578b5
--- /dev/null
+++ b/platform/darwin/src/MGLAttributionInfo.h
@@ -0,0 +1,58 @@
+#import <Foundation/Foundation.h>
+#import <CoreGraphics/CoreGraphics.h>
+#import <CoreLocation/CoreLocation.h>
+
+#import "MGLTypes.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ Information about an attribution statement, usually a copyright or trademark
+ statement, associated with a map source.
+ */
+@interface MGLAttributionInfo : NSObject
+
+/**
+ Parses and returns the attribution infos contained in the given HTML source
+ code string.
+
+ @param htmlString The HTML source code to parse.
+ @param fontSize The default text size in points.
+ @param linkColor The default link color.
+ */
++ (NS_ARRAY_OF(MGLAttributionInfo *) *)attributionInfosFromHTMLString:(NSString *)htmlString fontSize:(CGFloat)fontSize linkColor:(nullable MGLColor *)linkColor;
+
+- (instancetype)initWithTitle:(NSAttributedString *)title URL:(nullable NSURL *)URL;
+
+@property (nonatomic) NSAttributedString *title;
+@property (nonatomic, nullable) NSURL *URL;
+@property (nonatomic, getter=isFeedbackLink) BOOL feedbackLink;
+
+- (nullable NSURL *)feedbackURLAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel;
+
+@end
+
+@interface NSMutableArray (MGLAttributionInfoAdditions)
+
+/**
+ Adds the given attribution info object to the receiver as long as it isn’t
+ redundant to any object already in the receiver. Any existing object that is
+ redundant to the given object is replaced by the given object.
+
+ @param info The info object to add to the receiver.
+ @return True if the given info object was added to the receiver.
+ */
+- (void)growArrayByAddingAttributionInfo:(MGLAttributionInfo *)info;
+
+/**
+ Adds each of the given attribution info objects to the receiver as long as it
+ isn’t redundant to any object already in the receiver. Any existing object that
+ is redundant to the given object is replaced by the given object.
+
+ @param infos An array of info objects to add to the receiver.
+ */
+- (void)growArrayByAddingAttributionInfosFromArray:(NS_ARRAY_OF(MGLAttributionInfo *) *)infos;
+
+@end
+
+NS_ASSUME_NONNULL_END
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
diff --git a/platform/darwin/src/MGLGeoJSONSource.mm b/platform/darwin/src/MGLGeoJSONSource.mm
index 7fd89ddc74..570b884149 100644
--- a/platform/darwin/src/MGLGeoJSONSource.mm
+++ b/platform/darwin/src/MGLGeoJSONSource.mm
@@ -18,6 +18,8 @@ const MGLGeoJSONSourceOption MGLGeoJSONSourceOptionSimplificationTolerance = @"M
@interface MGLGeoJSONSource ()
+- (instancetype)initWithRawSource:(mbgl::style::GeoJSONSource *)rawSource NS_DESIGNATED_INITIALIZER;
+
@property (nonatomic, readwrite) NSDictionary *options;
@property (nonatomic) mbgl::style::GeoJSONSource *rawSource;
@@ -60,6 +62,10 @@ const MGLGeoJSONSourceOption MGLGeoJSONSourceOptionSimplificationTolerance = @"M
return self;
}
+- (instancetype)initWithRawSource:(mbgl::style::GeoJSONSource *)rawSource {
+ return [super initWithRawSource:rawSource];
+}
+
- (void)addToMapView:(MGLMapView *)mapView
{
if (_pendingSource == nullptr) {
diff --git a/platform/darwin/src/MGLGeoJSONSource_Private.h b/platform/darwin/src/MGLGeoJSONSource_Private.h
index de5bb10fac..9d67deee34 100644
--- a/platform/darwin/src/MGLGeoJSONSource_Private.h
+++ b/platform/darwin/src/MGLGeoJSONSource_Private.h
@@ -1,10 +1,11 @@
#import "MGLGeoJSONSource.h"
-#import "MGLGeoJSONSource_Private.h"
#include <mbgl/style/sources/geojson_source.hpp>
@interface MGLGeoJSONSource (Private)
+- (instancetype)initWithRawSource:(mbgl::style::GeoJSONSource *)rawSource;
+
- (mbgl::style::GeoJSONOptions)geoJSONOptions;
@end
diff --git a/platform/darwin/src/MGLRasterSource.mm b/platform/darwin/src/MGLRasterSource.mm
index 62472050e3..1671e1decd 100644
--- a/platform/darwin/src/MGLRasterSource.mm
+++ b/platform/darwin/src/MGLRasterSource.mm
@@ -1,4 +1,4 @@
-#import "MGLRasterSource.h"
+#import "MGLRasterSource_Private.h"
#import "MGLMapView_Private.h"
#import "MGLSource_Private.h"
@@ -9,6 +9,8 @@
@interface MGLRasterSource ()
+- (instancetype)initWithRawSource:(mbgl::style::RasterSource *)rawSource NS_DESIGNATED_INITIALIZER;
+
@property (nonatomic) mbgl::style::RasterSource *rawSource;
@end
@@ -39,6 +41,16 @@
return self;
}
+- (instancetype)initWithRawSource:(mbgl::style::RasterSource *)rawSource {
+ if (self = [super initWithRawSource:rawSource]) {
+ if (auto attribution = rawSource->getAttribution()) {
+ _tileSet = [[MGLTileSet alloc] initWithTileURLTemplates:@[]];
+ _tileSet.attribution = @(attribution->c_str());
+ }
+ }
+ return self;
+}
+
- (void)commonInit
{
std::unique_ptr<mbgl::style::RasterSource> source;
diff --git a/platform/darwin/src/MGLRasterSource_Private.h b/platform/darwin/src/MGLRasterSource_Private.h
new file mode 100644
index 0000000000..4a367cf8f8
--- /dev/null
+++ b/platform/darwin/src/MGLRasterSource_Private.h
@@ -0,0 +1,13 @@
+#import "MGLRasterSource.h"
+
+namespace mbgl {
+ namespace style {
+ class RasterSource;
+ }
+}
+
+@interface MGLRasterSource (Private)
+
+- (instancetype)initWithRawSource:(mbgl::style::RasterSource *)rawSource;
+
+@end
diff --git a/platform/darwin/src/MGLSource.mm b/platform/darwin/src/MGLSource.mm
index 2fa580df89..c96b6c41c6 100644
--- a/platform/darwin/src/MGLSource.mm
+++ b/platform/darwin/src/MGLSource.mm
@@ -20,6 +20,14 @@
return self;
}
+- (instancetype)initWithRawSource:(mbgl::style::Source *)rawSource {
+ NSString *identifier = @(rawSource->getID().c_str());
+ if (self = [self initWithIdentifier:identifier]) {
+ _rawSource = rawSource;
+ }
+ return self;
+}
+
- (void)addToMapView:(MGLMapView *)mapView {
[NSException raise:NSInvalidArgumentException format:
@"The source %@ cannot be added to the style. "
diff --git a/platform/darwin/src/MGLSource_Private.h b/platform/darwin/src/MGLSource_Private.h
index d360e71f3c..3100e0ae6e 100644
--- a/platform/darwin/src/MGLSource_Private.h
+++ b/platform/darwin/src/MGLSource_Private.h
@@ -1,13 +1,21 @@
#import "MGLSource.h"
-#include <mbgl/mbgl.hpp>
-#include <mbgl/style/source.hpp>
+namespace mbgl {
+ namespace style {
+ class Source;
+ }
+}
@class MGLMapView;
@interface MGLSource (Private)
/**
+ Initializes and returns a source with a raw pointer to the backing store.
+ */
+- (instancetype)initWithRawSource:(mbgl::style::Source *)rawSource;
+
+/**
A raw pointer to the mbgl object, which is always initialized, either to the
value returned by `mbgl::Map getSource`, or for independently created objects,
to the pointer value held in `pendingSource`. In the latter case, this raw
diff --git a/platform/darwin/src/MGLStyle.mm b/platform/darwin/src/MGLStyle.mm
index a6de4e798d..ee0bb286ba 100644
--- a/platform/darwin/src/MGLStyle.mm
+++ b/platform/darwin/src/MGLStyle.mm
@@ -17,10 +17,13 @@
#import "NSDate+MGLAdditions.h"
#import "MGLSource.h"
-#import "MGLVectorSource.h"
+#import "MGLVectorSource_Private.h"
#import "MGLRasterSource.h"
#import "MGLGeoJSONSource.h"
+#import "MGLAttributionInfo.h"
+#import "MGLTileSet_Private.h"
+
#include <mbgl/util/default_styles.hpp>
#include <mbgl/sprite/sprite_image.hpp>
#include <mbgl/style/layers/fill_layer.hpp>
@@ -158,25 +161,18 @@ static NSURL *MGLStyleURL_emerald;
return rawSource ? [self sourceFromMBGLSource:rawSource] : nil;
}
-- (MGLSource *)sourceFromMBGLSource:(mbgl::style::Source *)mbglSource {
- NSString *identifier = @(mbglSource->getID().c_str());
-
+- (MGLSource *)sourceFromMBGLSource:(mbgl::style::Source *)source {
// TODO: Fill in options specific to the respective source classes
// https://github.com/mapbox/mapbox-gl-native/issues/6584
- MGLSource *source;
- if (mbglSource->is<mbgl::style::VectorSource>()) {
- source = [[MGLVectorSource alloc] initWithIdentifier:identifier];
- } else if (mbglSource->is<mbgl::style::GeoJSONSource>()) {
- source = [[MGLGeoJSONSource alloc] initWithIdentifier:identifier];
- } else if (mbglSource->is<mbgl::style::RasterSource>()) {
- source = [[MGLRasterSource alloc] initWithIdentifier:identifier];
+ if (auto vectorSource = source->as<mbgl::style::VectorSource>()) {
+ return [[MGLVectorSource alloc] initWithRawSource:vectorSource];
+ } else if (auto geoJSONSource = source->as<mbgl::style::GeoJSONSource>()) {
+ return [[MGLGeoJSONSource alloc] initWithRawSource:geoJSONSource];
+ } else if (auto rasterSource = source->as<mbgl::style::RasterSource>()) {
+ return [[MGLRasterSource alloc] initWithRawSource:rasterSource];
} else {
- source = [[MGLSource alloc] initWithIdentifier:identifier];
+ return [[MGLSource alloc] initWithRawSource:source];
}
-
- source.rawSource = mbglSource;
-
- return source;
}
- (void)addSource:(MGLSource *)source
@@ -206,6 +202,25 @@ static NSURL *MGLStyleURL_emerald;
[source removeFromMapView:self.mapView];
}
+- (nullable NS_ARRAY_OF(MGLAttributionInfo *) *)attributionInfosWithFontSize:(CGFloat)fontSize linkColor:(nullable MGLColor *)linkColor {
+ // It’d be incredibly convenient to use -sources here, but this operation
+ // depends on the sources being sorted in ascending order by creation, as
+ // with the std::vector used in mbgl.
+ auto rawSources = self.mapView.mbglMap->getSources();
+ NSMutableArray *infos = [NSMutableArray arrayWithCapacity:rawSources.size()];
+ for (auto rawSource = rawSources.begin(); rawSource != rawSources.end(); ++rawSource) {
+ MGLSource *source = [self sourceFromMBGLSource:*rawSource];
+ if (![source isKindOfClass:[MGLVectorSource class]]
+ && ![source isKindOfClass:[MGLRasterSource class]]) {
+ continue;
+ }
+
+ NSArray *tileSetInfos = [[(id)source tileSet] attributionInfosWithFontSize:fontSize linkColor:linkColor];
+ [infos growArrayByAddingAttributionInfosFromArray:tileSetInfos];
+ }
+ return infos;
+}
+
#pragma mark Style layers
- (NS_MUTABLE_ARRAY_OF(MGLStyleLayer *) *)layers
diff --git a/platform/darwin/src/MGLStyle_Private.h b/platform/darwin/src/MGLStyle_Private.h
index ee4a30c887..23ce8fbee0 100644
--- a/platform/darwin/src/MGLStyle_Private.h
+++ b/platform/darwin/src/MGLStyle_Private.h
@@ -2,11 +2,10 @@
#import "MGLStyleLayer.h"
#import "MGLFillStyleLayer.h"
-#import <mbgl/util/default_styles.hpp>
-#include <mbgl/mbgl.hpp>
NS_ASSUME_NONNULL_BEGIN
+@class MGLAttributionInfo;
@class MGLMapView;
@class MGLOpenGLStyleLayer;
@@ -16,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly, weak) MGLMapView *mapView;
+- (nullable NS_ARRAY_OF(MGLAttributionInfo *) *)attributionInfosWithFontSize:(CGFloat)fontSize linkColor:(nullable MGLColor *)linkColor;
+
@property (nonatomic, readonly, strong) NS_MUTABLE_DICTIONARY_OF(NSString *, MGLOpenGLStyleLayer *) *openGLLayers;
- (void)setStyleClasses:(NS_ARRAY_OF(NSString *) *)appliedClasses transitionDuration:(NSTimeInterval)transitionDuration;
diff --git a/platform/darwin/src/MGLTileSet.h b/platform/darwin/src/MGLTileSet.h
index 08a34338b1..88bc7e4ae0 100644
--- a/platform/darwin/src/MGLTileSet.h
+++ b/platform/darwin/src/MGLTileSet.h
@@ -17,6 +17,32 @@ typedef NS_ENUM(NSUInteger, MGLTileSetScheme) {
*/
@interface MGLTileSet : NSObject
+#pragma mark Creating a Tile Set
+
+/**
+ Initializes and returns a new tile set object.
+
+ @param tileURLTemplates An `NSArray` of `NSString` objects that represent the
+ tile templates.
+ @return The initialized tile set object.
+ */
+- (instancetype)initWithTileURLTemplates:(NS_ARRAY_OF(NSString *) *)tileURLTemplates;
+
+/**
+ Initializes and returns a new tile set object.
+
+ @param tileURLTemplates An `NSArray` of `NSString` objects that represent the
+ tile templates.
+ @param minimumZoomLevel An `NSUInteger`; specifies the minimum zoom level at
+ which to display tiles.
+ @param maximumZoomLevel An `NSUInteger`; specifies the maximum zoom level at
+ which to display tiles.
+ @return The initialized tile set object.
+ */
+- (instancetype)initWithTileURLTemplates:(NS_ARRAY_OF(NSString *) *)tileURLTemplates minimumZoomLevel:(NSUInteger)minimumZoomLevel maximumZoomLevel:(NSUInteger)maximumZoomLevel;
+
+#pragma mark Accessing Tile Set Metadata
+
/**
An `NSArray` of `NSString` objects that represent the tile templates.
*/
@@ -51,28 +77,6 @@ typedef NS_ENUM(NSUInteger, MGLTileSetScheme) {
*/
@property (nonatomic) MGLTileSetScheme scheme;
-/**
- Initializes and returns a new tile set object.
-
- @param tileURLTemplates An `NSArray` of `NSString` objects that represent the
- tile templates.
- @return The initialized tile set object.
- */
-- (instancetype)initWithTileURLTemplates:(NS_ARRAY_OF(NSString *) *)tileURLTemplates;
-
-/**
- Initializes and returns a new tile set object.
-
- @param tileURLTemplates An `NSArray` of `NSString` objects that represent the
- tile templates.
- @param minimumZoomLevel An `NSUInteger`; specifies the minimum zoom level at
- which to display tiles.
- @param maximumZoomLevel An `NSUInteger`; specifies the maximum zoom level at
- which to display tiles.
- @return The initialized tile set object.
- */
-- (instancetype)initWithTileURLTemplates:(NS_ARRAY_OF(NSString *) *)tileURLTemplates minimumZoomLevel:(NSUInteger)minimumZoomLevel maximumZoomLevel:(NSUInteger)maximumZoomLevel;
-
@end
NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/src/MGLTileSet.mm b/platform/darwin/src/MGLTileSet.mm
index f795545eed..6afc6c19af 100644
--- a/platform/darwin/src/MGLTileSet.mm
+++ b/platform/darwin/src/MGLTileSet.mm
@@ -1,5 +1,7 @@
#import "MGLTileSet.h"
+#import "MGLAttributionInfo.h"
+
#include <mbgl/util/tileset.hpp>
@implementation MGLTileSet
@@ -57,6 +59,12 @@
_maximumZoomLevel = maximumZoomLevel;
}
+- (nullable NS_ARRAY_OF(MGLAttributionInfo *) *)attributionInfosWithFontSize:(CGFloat)fontSize linkColor:(nullable MGLColor *)linkColor {
+ return [MGLAttributionInfo attributionInfosFromHTMLString:self.attribution
+ fontSize:fontSize
+ linkColor:linkColor];
+}
+
- (mbgl::Tileset)mbglTileset
{
mbgl::Tileset tileset;
diff --git a/platform/darwin/src/MGLTileSet_Private.h b/platform/darwin/src/MGLTileSet_Private.h
index 6a14d428db..038fe57fa2 100644
--- a/platform/darwin/src/MGLTileSet_Private.h
+++ b/platform/darwin/src/MGLTileSet_Private.h
@@ -2,8 +2,23 @@
#include <mbgl/util/tileset.hpp>
+NS_ASSUME_NONNULL_BEGIN
+
+@class MGLAttributionInfo;
+
@interface MGLTileSet (Private)
+/**
+ A structured representation of the `attribution` property. The default value is
+ `nil`.
+
+ @param fontSize The default text size in points.
+ @param linkColor The default link color.
+ */
+- (nullable NS_ARRAY_OF(MGLAttributionInfo *) *)attributionInfosWithFontSize:(CGFloat)fontSize linkColor:(nullable MGLColor *)linkColor;
+
- (mbgl::Tileset)mbglTileset;
-@end \ No newline at end of file
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/src/MGLVectorSource.mm b/platform/darwin/src/MGLVectorSource.mm
index ab68d45ba1..b5ec0b33be 100644
--- a/platform/darwin/src/MGLVectorSource.mm
+++ b/platform/darwin/src/MGLVectorSource.mm
@@ -1,4 +1,4 @@
-#import "MGLVectorSource.h"
+#import "MGLVectorSource_Private.h"
#import "MGLMapView_Private.h"
#import "MGLSource_Private.h"
@@ -9,6 +9,8 @@
@interface MGLVectorSource ()
+- (instancetype)initWithRawSource:(mbgl::style::VectorSource *)rawSource NS_DESIGNATED_INITIALIZER;
+
@property (nonatomic) mbgl::style::VectorSource *rawSource;
@end
@@ -38,6 +40,16 @@
return self;
}
+- (instancetype)initWithRawSource:(mbgl::style::VectorSource *)rawSource {
+ if (self = [super initWithRawSource:rawSource]) {
+ if (auto attribution = rawSource->getAttribution()) {
+ _tileSet = [[MGLTileSet alloc] initWithTileURLTemplates:@[]];
+ _tileSet.attribution = @(attribution->c_str());
+ }
+ }
+ return self;
+}
+
- (void)commonInit
{
std::unique_ptr<mbgl::style::VectorSource> source;
diff --git a/platform/darwin/src/MGLVectorSource_Private.h b/platform/darwin/src/MGLVectorSource_Private.h
new file mode 100644
index 0000000000..ce6bccdbae
--- /dev/null
+++ b/platform/darwin/src/MGLVectorSource_Private.h
@@ -0,0 +1,13 @@
+#import "MGLVectorSource.h"
+
+namespace mbgl {
+ namespace style {
+ class VectorSource;
+ }
+}
+
+@interface MGLVectorSource (Private)
+
+- (instancetype)initWithRawSource:(mbgl::style::VectorSource *)rawSource;
+
+@end
diff --git a/platform/darwin/src/NSString+MGLAdditions.h b/platform/darwin/src/NSString+MGLAdditions.h
index 5b549affd5..45fea25588 100644
--- a/platform/darwin/src/NSString+MGLAdditions.h
+++ b/platform/darwin/src/NSString+MGLAdditions.h
@@ -4,9 +4,22 @@ NS_ASSUME_NONNULL_BEGIN
@interface NSString (MGLAdditions)
+/** Returns the range spanning the entire receiver. */
+- (NSRange)mgl_wholeRange;
+
/** Returns the receiver if non-empty or nil if empty. */
- (nullable NSString *)mgl_stringOrNilIfEmpty;
@end
+@interface NSAttributedString (MGLAdditions)
+
+/** Returns the range spanning the entire receiver. */
+- (NSRange)mgl_wholeRange;
+
+/** Returns a copy of the receiver with leading and trailing members of the given set removed. */
+- (NSAttributedString *)mgl_attributedStringByTrimmingCharactersInSet:(NSCharacterSet *)set;
+
+@end
+
NS_ASSUME_NONNULL_END
diff --git a/platform/darwin/src/NSString+MGLAdditions.m b/platform/darwin/src/NSString+MGLAdditions.m
index 969886651b..04a65dc5e2 100644
--- a/platform/darwin/src/NSString+MGLAdditions.m
+++ b/platform/darwin/src/NSString+MGLAdditions.m
@@ -2,9 +2,30 @@
@implementation NSString (MGLAdditions)
-- (nullable NSString *)mgl_stringOrNilIfEmpty
-{
+- (NSRange)mgl_wholeRange {
+ return NSMakeRange(0, self.length);
+}
+
+- (nullable NSString *)mgl_stringOrNilIfEmpty {
return self.length ? self : nil;
}
@end
+
+@implementation NSAttributedString (MGLAdditions)
+
+- (NSRange)mgl_wholeRange {
+ return NSMakeRange(0, self.length);
+}
+
+- (NSAttributedString *)mgl_attributedStringByTrimmingCharactersInSet:(NSCharacterSet *)set {
+ NSScanner *scanner = [NSScanner scannerWithString:self.string];
+ scanner.charactersToBeSkipped = nil;
+ NSString *prefix;
+ [scanner scanCharactersFromSet:set intoString:&prefix];
+
+ NSString *trimmedString = [self.string stringByTrimmingCharactersInSet:set];
+ return [self attributedSubstringFromRange:NSMakeRange(prefix.length, trimmedString.length)];
+}
+
+@end
diff --git a/platform/darwin/test/MGLAttributionInfoTests.m b/platform/darwin/test/MGLAttributionInfoTests.m
new file mode 100644
index 0000000000..003637bf1b
--- /dev/null
+++ b/platform/darwin/test/MGLAttributionInfoTests.m
@@ -0,0 +1,115 @@
+#import <Mapbox/Mapbox.h>
+#import <XCTest/XCTest.h>
+
+#import "MGLAttributionInfo.h"
+
+@interface MGLAttributionInfoTests : XCTestCase
+
+@end
+
+@implementation MGLAttributionInfoTests
+
+- (void)testParsing {
+ static NSString * const htmlStrings[] = {
+ @"<a href=\"https://www.mapbox.com/about/maps/\" target=\"_blank\">&copy; Mapbox</a> "
+ @"<a href=\"http://www.openstreetmap.org/about/\" target=\"_blank\">©️ OpenStreetMap</a> "
+ @"CC&nbsp;BY-SA "
+ @"<a class=\"mapbox-improve-map\" href=\"https://www.mapbox.com/map-feedback/\" target=\"_blank\">Improve this map</a>",
+ };
+
+ NS_MUTABLE_ARRAY_OF(MGLAttributionInfo *) *infos = [NSMutableArray array];
+ for (NSUInteger i = 0; i < sizeof(htmlStrings) / sizeof(htmlStrings[0]); i++) {
+ NSArray *subinfos = [MGLAttributionInfo attributionInfosFromHTMLString:htmlStrings[i]
+ fontSize:0
+ linkColor:nil];
+ [infos growArrayByAddingAttributionInfosFromArray:subinfos];
+ }
+
+ XCTAssertEqual(infos.count, 4);
+
+ CLLocationCoordinate2D mapbox = CLLocationCoordinate2DMake(12.9810816, 77.6368034);
+ XCTAssertEqualObjects(infos[0].title.string, @"© Mapbox");
+ XCTAssertEqualObjects(infos[0].URL, [NSURL URLWithString:@"https://www.mapbox.com/about/maps/"]);
+ XCTAssertFalse(infos[0].feedbackLink);
+ XCTAssertNil([infos[0] feedbackURLAtCenterCoordinate:mapbox zoomLevel:14]);
+
+ XCTAssertEqualObjects(infos[1].title.string, @"©️ OpenStreetMap");
+ XCTAssertEqualObjects(infos[1].URL, [NSURL URLWithString:@"http://www.openstreetmap.org/about/"]);
+ XCTAssertFalse(infos[1].feedbackLink);
+ XCTAssertNil([infos[1] feedbackURLAtCenterCoordinate:mapbox zoomLevel:14]);
+
+ XCTAssertEqualObjects(infos[2].title.string, @"CC\u00a0BY-SA");
+ XCTAssertNil(infos[2].URL);
+ XCTAssertFalse(infos[2].feedbackLink);
+ XCTAssertNil([infos[2] feedbackURLAtCenterCoordinate:mapbox zoomLevel:14]);
+
+ XCTAssertEqualObjects(infos[3].title.string, @"Improve this map");
+ XCTAssertEqualObjects(infos[3].URL, [NSURL URLWithString:@"https://www.mapbox.com/map-feedback/"]);
+ XCTAssertTrue(infos[3].feedbackLink);
+ XCTAssertEqualObjects([infos[3] feedbackURLAtCenterCoordinate:mapbox zoomLevel:14],
+ [NSURL URLWithString:@"https://www.mapbox.com/map-feedback/#/77.63680/12.98108/15"]);
+}
+
+- (void)testStyle {
+ static NSString * const htmlStrings[] = {
+ @"<a href=\"https://www.mapbox.com/\">Mapbox</a>",
+ };
+
+ CGFloat fontSize = 72;
+ MGLColor *color = [MGLColor redColor];
+ NS_MUTABLE_ARRAY_OF(MGLAttributionInfo *) *infos = [NSMutableArray array];
+ for (NSUInteger i = 0; i < sizeof(htmlStrings) / sizeof(htmlStrings[0]); i++) {
+ NSArray *subinfos = [MGLAttributionInfo attributionInfosFromHTMLString:htmlStrings[i]
+ fontSize:72
+ linkColor:color];
+ [infos growArrayByAddingAttributionInfosFromArray:subinfos];
+ }
+
+ XCTAssertEqual(infos.count, 1);
+
+ XCTAssertEqualObjects(infos[0].title.string, @"Mapbox");
+ XCTAssertEqualObjects([infos[0].title attribute:NSLinkAttributeName atIndex:0 effectiveRange:nil], [NSURL URLWithString:@"https://www.mapbox.com/"]);
+ XCTAssertEqualObjects([infos[0].title attribute:NSUnderlineStyleAttributeName atIndex:0 effectiveRange:nil], @(NSUnderlineStyleSingle));
+
+#if TARGET_OS_IPHONE
+ UIFont *font;
+#else
+ NSFont *font;
+#endif
+ font = [infos[0].title attribute:NSFontAttributeName atIndex:0 effectiveRange:nil];
+ XCTAssertEqual(font.pointSize, fontSize);
+
+ CGFloat r, g, b, a;
+ [color getRed:&r green:&g blue:&b alpha:&a];
+ MGLColor *linkColor = [infos[0].title attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:nil];
+ CGFloat linkR, linkG, linkB, linkA;
+ [linkColor getRed:&linkR green:&linkG blue:&linkB alpha:&linkA];
+ XCTAssertEqual(r, linkR);
+ XCTAssertEqual(g, linkG);
+ XCTAssertEqual(b, linkB);
+ XCTAssertEqual(a, linkA);
+}
+
+- (void)testDedupe {
+ static NSString * const htmlStrings[] = {
+ @"World",
+ @"Hello World",
+ @"Another Source",
+ @"Hello",
+ @"Hello World",
+ };
+
+ NS_MUTABLE_ARRAY_OF(MGLAttributionInfo *) *infos = [NSMutableArray array];
+ for (NSUInteger i = 0; i < sizeof(htmlStrings) / sizeof(htmlStrings[0]); i++) {
+ NSArray *subinfos = [MGLAttributionInfo attributionInfosFromHTMLString:htmlStrings[i]
+ fontSize:0
+ linkColor:nil];
+ [infos growArrayByAddingAttributionInfosFromArray:subinfos];
+ }
+
+ XCTAssertEqual(infos.count, 2);
+ XCTAssertEqualObjects(infos[0].title.string, @"Hello World");
+ XCTAssertEqualObjects(infos[1].title.string, @"Another Source");
+}
+
+@end