diff options
author | Minh Nguyễn <mxn@1ec5.org> | 2016-12-08 17:43:17 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-12-08 17:43:17 -0800 |
commit | fe53af7de031e296cdb9b0a7e88688fd3f54e0d8 (patch) | |
tree | d51f217f5e90db30fb4447c17cf899c678688ed9 | |
parent | 89d4c40d927388f44e00e004e8a22db2d1b2eeab (diff) | |
download | qtlocation-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.
33 files changed, 824 insertions, 212 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\">© Mapbox</a> " + @"<a href=\"http://www.openstreetmap.org/about/\" target=\"_blank\">©️ OpenStreetMap</a> " + @"CC 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 diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 894c33510a..5c63bdd2f5 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -22,6 +22,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * `-[MGLMapView resetPosition]` now resets to the current style’s default center coordinates, zoom level, direction, and pitch, if specified. ([#6127](https://github.com/mapbox/mapbox-gl-native/pull/6127)) * Fixed an issue where feature querying sometimes failed to return the expected features when the map was tilted. ([#6773](https://github.com/mapbox/mapbox-gl-native/pull/6773)) * MGLFeature’s `attributes` and `identifier` properties are now writable. ([#6728](https://github.com/mapbox/mapbox-gl-native/pull/6728)) +* The action sheet that appears when tapping the information button in the bottom-right corner now lists the correct attribution for the current style. ([#5999](https://github.com/mapbox/mapbox-gl-native/pull/5999)) * The `text-pitch-alignment` property is now supported in stylesheets for improved street label legibility on a tilted map. ([#5288](https://github.com/mapbox/mapbox-gl-native/pull/5288)) * The `icon-text-fit` and `icon-text-fit-padding` properties are now supported in stylesheets, allowing the background of a shield to automatically resize to fit the shield’s text. ([#5334](https://github.com/mapbox/mapbox-gl-native/pull/5334)) * The `circle-pitch-scale` property is now supported in stylesheets, allowing circle features in a tilted base map to scale or remain the same size as the viewing distance changes. ([#5576](https://github.com/mapbox/mapbox-gl-native/pull/5576)) diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index e469c82ba5..30ed5556b2 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -172,6 +172,10 @@ 7E016D851D9E890300A29A21 /* MGLPolygon+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 7E016D821D9E890300A29A21 /* MGLPolygon+MGLAdditions.h */; }; 7E016D861D9E890300A29A21 /* MGLPolygon+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E016D831D9E890300A29A21 /* MGLPolygon+MGLAdditions.m */; }; 7E016D871D9E890300A29A21 /* MGLPolygon+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E016D831D9E890300A29A21 /* MGLPolygon+MGLAdditions.m */; }; + DA00FC8E1D5EEB0D009AABC8 /* MGLAttributionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = DA00FC8C1D5EEB0D009AABC8 /* MGLAttributionInfo.h */; }; + DA00FC8F1D5EEB0D009AABC8 /* MGLAttributionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = DA00FC8C1D5EEB0D009AABC8 /* MGLAttributionInfo.h */; }; + DA00FC901D5EEB0D009AABC8 /* MGLAttributionInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA00FC8D1D5EEB0D009AABC8 /* MGLAttributionInfo.mm */; }; + DA00FC911D5EEB0D009AABC8 /* MGLAttributionInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA00FC8D1D5EEB0D009AABC8 /* MGLAttributionInfo.mm */; }; DA0CD5901CF56F6A00A5F5A5 /* MGLFeatureTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA0CD58F1CF56F6A00A5F5A5 /* MGLFeatureTests.mm */; }; DA17BE301CC4BAC300402C41 /* MGLMapView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */; }; DA17BE311CC4BDAA00402C41 /* MGLMapView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */; }; @@ -407,6 +411,7 @@ DAED38641D62D0FC00D7640F /* NSURL+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = DAED38611D62D0FC00D7640F /* NSURL+MGLAdditions.h */; }; DAED38651D62D0FC00D7640F /* NSURL+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DAED38621D62D0FC00D7640F /* NSURL+MGLAdditions.m */; }; DAED38661D62D0FC00D7640F /* NSURL+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DAED38621D62D0FC00D7640F /* NSURL+MGLAdditions.m */; }; + DAEDC4341D603417000224FF /* MGLAttributionInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DAEDC4331D603417000224FF /* MGLAttributionInfoTests.m */; }; DD0902A91DB1929D00C5BDCE /* MGLNetworkConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = DD0902A21DB18DE700C5BDCE /* MGLNetworkConfiguration.m */; }; DD0902AA1DB1929D00C5BDCE /* MGLNetworkConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = DD0902A21DB18DE700C5BDCE /* MGLNetworkConfiguration.m */; }; DD0902AB1DB192A800C5BDCE /* MGLNetworkConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = DD0902A41DB18F1B00C5BDCE /* MGLNetworkConfiguration.h */; }; @@ -601,6 +606,8 @@ 7E016D7D1D9E86BE00A29A21 /* MGLPolyline+MGLAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MGLPolyline+MGLAdditions.m"; sourceTree = "<group>"; }; 7E016D821D9E890300A29A21 /* MGLPolygon+MGLAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MGLPolygon+MGLAdditions.h"; sourceTree = "<group>"; }; 7E016D831D9E890300A29A21 /* MGLPolygon+MGLAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MGLPolygon+MGLAdditions.m"; sourceTree = "<group>"; }; + DA00FC8C1D5EEB0D009AABC8 /* MGLAttributionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAttributionInfo.h; sourceTree = "<group>"; }; + DA00FC8D1D5EEB0D009AABC8 /* MGLAttributionInfo.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLAttributionInfo.mm; sourceTree = "<group>"; }; DA0CD58F1CF56F6A00A5F5A5 /* MGLFeatureTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLFeatureTests.mm; path = ../../darwin/test/MGLFeatureTests.mm; sourceTree = "<group>"; }; DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapView_Private.h; sourceTree = "<group>"; }; DA1DC94A1CB6C1C2006E619F /* Mapbox GL.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mapbox GL.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -775,6 +782,7 @@ DAD165771CF4CDFF001FF4B9 /* MGLShapeCollection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLShapeCollection.mm; sourceTree = "<group>"; }; DAED38611D62D0FC00D7640F /* NSURL+MGLAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURL+MGLAdditions.h"; sourceTree = "<group>"; }; DAED38621D62D0FC00D7640F /* NSURL+MGLAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURL+MGLAdditions.m"; sourceTree = "<group>"; }; + DAEDC4331D603417000224FF /* MGLAttributionInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLAttributionInfoTests.m; path = ../../darwin/test/MGLAttributionInfoTests.m; sourceTree = "<group>"; }; DD0902A21DB18DE700C5BDCE /* MGLNetworkConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLNetworkConfiguration.m; sourceTree = "<group>"; }; DD0902A41DB18F1B00C5BDCE /* MGLNetworkConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLNetworkConfiguration.h; sourceTree = "<group>"; }; DD4823721D94AE6C00EB71B7 /* fill_filter_style.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fill_filter_style.json; sourceTree = "<group>"; }; @@ -1060,6 +1068,7 @@ isa = PBXGroup; children = ( 357579811D502AD4000B822E /* Styling */, + DAEDC4331D603417000224FF /* MGLAttributionInfoTests.m */, 353D23951D0B0DFE002BE09D /* MGLAnnotationViewTests.m */, DA35A2C31CCA9F8300E826B2 /* MGLClockDirectionFormatterTests.m */, DA35A2C41CCA9F8300E826B2 /* MGLCompassDirectionFormatterTests.m */, @@ -1111,6 +1120,8 @@ DA8848001CBAFA6200AB86E3 /* MGLAccountManager.m */, DD0902A41DB18F1B00C5BDCE /* MGLNetworkConfiguration.h */, DD0902A21DB18DE700C5BDCE /* MGLNetworkConfiguration.m */, + DA00FC8C1D5EEB0D009AABC8 /* MGLAttributionInfo.h */, + DA00FC8D1D5EEB0D009AABC8 /* MGLAttributionInfo.mm */, DA8847E21CBAFA5100AB86E3 /* MGLMapCamera.h */, DA8848031CBAFA6200AB86E3 /* MGLMapCamera.mm */, DA8847EC1CBAFA5100AB86E3 /* MGLStyle.h */, @@ -1422,6 +1433,7 @@ 353933FE1D3FB7DD003F57D7 /* MGLSymbolStyleLayer.h in Headers */, DA8848861CBB033F00AB86E3 /* Fabric+FABKits.h in Headers */, DA8848201CBAFA6200AB86E3 /* MGLOfflinePack_Private.h in Headers */, + DA00FC8E1D5EEB0D009AABC8 /* MGLAttributionInfo.h in Headers */, DA8847FA1CBAFA5100AB86E3 /* MGLPolyline.h in Headers */, 3566C7711D4A9198008152BC /* MGLSource_Private.h in Headers */, 4018B1C91CDC288A00F666AF /* MGLAnnotationView_Private.h in Headers */, @@ -1593,6 +1605,7 @@ DABFB8601CBE99E500D62B32 /* MGLMapCamera.h in Headers */, DA737EE21D056A4E005BDA16 /* MGLMapViewDelegate.h in Headers */, DABFB86A1CBE99E500D62B32 /* MGLStyle.h in Headers */, + DA00FC8F1D5EEB0D009AABC8 /* MGLAttributionInfo.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1911,6 +1924,7 @@ DA35A2C61CCA9F8300E826B2 /* MGLCompassDirectionFormatterTests.m in Sources */, 3575798E1D502EC7000B822E /* MGLRuntimeStylingHelper.m in Sources */, 4085AF091D933DEA00F11B22 /* MGLTileSetTests.mm in Sources */, + DAEDC4341D603417000224FF /* MGLAttributionInfoTests.m in Sources */, 357579851D502AF5000B822E /* MGLSymbolStyleLayerTests.m in Sources */, 357579871D502AFE000B822E /* MGLLineStyleLayerTests.m in Sources */, 357579891D502B06000B822E /* MGLCircleStyleLayerTests.m in Sources */, @@ -1951,6 +1965,7 @@ 400533021DB0862B0069F638 /* NSArray+MGLAdditions.mm in Sources */, 35136D421D42274500C20EFD /* MGLRasterStyleLayer.mm in Sources */, 3538AA1F1D542239008EC33D /* MGLForegroundStyleLayer.m in Sources */, + DA00FC901D5EEB0D009AABC8 /* MGLAttributionInfo.mm in Sources */, DA88482D1CBAFA6200AB86E3 /* NSBundle+MGLAdditions.m in Sources */, DA88485B1CBAFB9800AB86E3 /* MGLUserLocation.m in Sources */, 350098BD1D480108004B2AF0 /* MGLVectorSource.mm in Sources */, @@ -2025,6 +2040,7 @@ 400533031DB086490069F638 /* NSArray+MGLAdditions.mm in Sources */, 35136D431D42274500C20EFD /* MGLRasterStyleLayer.mm in Sources */, 3538AA201D542239008EC33D /* MGLForegroundStyleLayer.m in Sources */, + DA00FC911D5EEB0D009AABC8 /* MGLAttributionInfo.mm in Sources */, DAA4E4201CBB730400178DFB /* MGLOfflinePack.mm in Sources */, DAA4E4331CBB730400178DFB /* MGLUserLocation.m in Sources */, 350098BE1D480108004B2AF0 /* MGLVectorSource.mm in Sources */, diff --git a/platform/ios/resources/Base.lproj/Localizable.strings b/platform/ios/resources/Base.lproj/Localizable.strings index c4569fe239..63bed7e326 100644 --- a/platform/ios/resources/Base.lproj/Localizable.strings +++ b/platform/ios/resources/Base.lproj/Localizable.strings @@ -19,12 +19,6 @@ /* Compass abbreviation for north */ "COMPASS_NORTH" = "N"; -/* Copyright notice in attribution sheet */ -"COPY_MAPBOX" = "© Mapbox"; - -/* Copyright notice in attribution sheet */ -"COPY_OSM" = "© OpenStreetMap"; - /* Instructions in Interface Builder designable; {key}, {plist file name} */ "DESIGNABLE" = "To display a Mapbox-hosted map here, set %1$@ to your access token in %2$@\n\nFor detailed instructions, see:"; @@ -46,9 +40,6 @@ /* Map accessibility value */ "MAP_A11Y_VALUE" = "Zoom %1$dx\n%2$ld annotation(s) visible"; -/* Action in attribution sheet */ -"MAP_FEEDBACK" = "Improve This Map"; - /* Action sheet title */ "SDK_NAME" = "Mapbox iOS SDK"; diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 3ee03182a8..c67105b9e7 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -55,6 +55,7 @@ #import "MGLCompactCalloutView.h" #import "MGLAnnotationContainerView.h" #import "MGLAnnotationContainerView_Private.h" +#import "MGLAttributionInfo.h" #include <algorithm> #include <cstdlib> @@ -310,6 +311,8 @@ public: BOOL _delegateHasLineWidthsForShapeAnnotations; MGLCompassDirectionFormatter *_accessibilityCompassFormatter; + + NS_ARRAY_OF(MGLAttributionInfo *) *_attributionInfos; } #pragma mark - Setup & Teardown - @@ -1714,44 +1717,27 @@ public: - (void)showAttribution { - if ( ! self.attributionSheet) + self.attributionSheet = [[UIActionSheet alloc] initWithTitle:NSLocalizedStringWithDefaultValue(@"SDK_NAME", nil, nil, @"Mapbox iOS SDK", @"Action sheet title") + delegate:self + cancelButtonTitle:NSLocalizedStringWithDefaultValue(@"CANCEL", nil, nil, @"Cancel", @"") + destructiveButtonTitle:nil + otherButtonTitles:nil]; + + _attributionInfos = [self.style attributionInfosWithFontSize:[UIFont buttonFontSize] linkColor:nil]; + for (MGLAttributionInfo *info in _attributionInfos) { - self.attributionSheet = [[UIActionSheet alloc] initWithTitle:NSLocalizedStringWithDefaultValue(@"SDK_NAME", nil, nil, @"Mapbox iOS SDK", @"Action sheet title") - delegate:self - cancelButtonTitle:NSLocalizedStringWithDefaultValue(@"CANCEL", nil, nil, @"Cancel", @"") - destructiveButtonTitle:nil - otherButtonTitles: - NSLocalizedStringWithDefaultValue(@"COPY_MAPBOX", nil, nil, @"© Mapbox", @"Copyright notice in attribution sheet"), - NSLocalizedStringWithDefaultValue(@"COPY_OSM", nil, nil, @"© OpenStreetMap", @"Copyright notice in attribution sheet"), - NSLocalizedStringWithDefaultValue(@"MAP_FEEDBACK", nil, nil, @"Improve This Map", @"Action in attribution sheet"), - NSLocalizedStringWithDefaultValue(@"TELEMETRY_NAME", nil, nil, @"Mapbox Telemetry", @"Action in attribution sheet"), - nil]; - + NSString *title = [info.title.string capitalizedStringWithLocale:[NSLocale currentLocale]]; + [self.attributionSheet addButtonWithTitle:title]; } - + + [self.attributionSheet addButtonWithTitle:NSLocalizedStringWithDefaultValue(@"TELEMETRY_NAME", nil, nil, @"Mapbox Telemetry", @"Action in attribution sheet")]; + [self.attributionSheet showFromRect:self.attributionButton.frame inView:self animated:YES]; } - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex { - if (buttonIndex == actionSheet.firstOtherButtonIndex) - { - [[UIApplication sharedApplication] openURL: - [NSURL URLWithString:@"https://www.mapbox.com/about/maps/"]]; - } - else if (buttonIndex == actionSheet.firstOtherButtonIndex + 1) - { - [[UIApplication sharedApplication] openURL: - [NSURL URLWithString:@"http://www.openstreetmap.org/about/"]]; - } - else if (buttonIndex == actionSheet.firstOtherButtonIndex + 2) - { - NSString *feedbackURL = [NSString stringWithFormat:@"https://www.mapbox.com/map-feedback/#/%.5f/%.5f/%i", - self.longitude, self.latitude, (int)round(self.zoomLevel + 1)]; - [[UIApplication sharedApplication] openURL: - [NSURL URLWithString:feedbackURL]]; - } - else if (buttonIndex == actionSheet.firstOtherButtonIndex + 3) + if (buttonIndex == actionSheet.numberOfButtons - 1) { NSString *message; NSString *participate; @@ -1777,6 +1763,19 @@ public: otherButtonTitles:NSLocalizedStringWithDefaultValue(@"TELEMETRY_MORE", nil, nil, @"Tell Me More", @"Telemetry prompt button"), optOut, nil]; [alert show]; } + else if (buttonIndex > 0) + { + MGLAttributionInfo *info = _attributionInfos[buttonIndex + actionSheet.firstOtherButtonIndex]; + NSURL *url = info.URL; + if (url) + { + if (info.feedbackLink) + { + url = [info feedbackURLAtCenterCoordinate:self.centerCoordinate zoomLevel:self.zoomLevel]; + } + [[UIApplication sharedApplication] openURL:url]; + } + } } - (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex @@ -4694,6 +4693,10 @@ public: } break; } + case mbgl::MapChangeSourceDidChange: + { + break; + } } } diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md index 1d5aa93638..ab85890edb 100644 --- a/platform/macos/CHANGELOG.md +++ b/platform/macos/CHANGELOG.md @@ -20,6 +20,7 @@ * Fixed an issue where the style zoom levels were not respected when deciding when to render a layer. ([#5811](https://github.com/mapbox/mapbox-gl-native/issues/5811)) * Fixed an issue where feature querying sometimes failed to return the expected features when the map was tilted. ([#6773](https://github.com/mapbox/mapbox-gl-native/pull/6773)) * MGLFeature’s `attributes` and `identifier` properties are now writable. ([#6728](https://github.com/mapbox/mapbox-gl-native/pull/6728)) +* Attribution views now display the correct attribution for the current style. ([#5999](https://github.com/mapbox/mapbox-gl-native/pull/5999)) * If MGLMapView is unable to obtain or parse a style, it now calls its delegate’s `-mapViewDidFailLoadingMap:withError:` method. ([#6145](https://github.com/mapbox/mapbox-gl-native/pull/6145)) * Added the `-[MGLMapViewDelegate mapView:didFinishLoadingStyle:]` delegate method, which offers the earliest opportunity to modify the layout or appearance of the current style before the map view is displayed to the user. ([#6636](https://github.com/mapbox/mapbox-gl-native/pull/6636)) * Fixed an issue causing stepwise zoom functions to be misinterpreted. ([#6328](https://github.com/mapbox/mapbox-gl-native/pull/6328)) diff --git a/platform/macos/INSTALL.md b/platform/macos/INSTALL.md index 665af128c5..a4b944611d 100644 --- a/platform/macos/INSTALL.md +++ b/platform/macos/INSTALL.md @@ -28,6 +28,7 @@ In a storyboard or XIB: 3. MGLMapView needs to be layer-backed: * You can make the window layer-backed by selecting the window and checking Full Size Content View in the Attributes inspector. This allows the map view to underlap the title bar and toolbar. * Alternatively, if you don’t want the entire window to be layer-backed, you can make just the map view layer-backed by selecting it and checking its entry under the View Effects inspector’s Core Animation Layer section. +4. Add a map feedback item to your Help menu. (Drag Menu Item from the Object library into Main Menu ‣ Help ‣ Menu.) Title it “Improve This Map” or similar, and connect it to the `giveFeedback:` action of First Responder. If you need to manipulate the map view programmatically: diff --git a/platform/macos/app/MapDocument.m b/platform/macos/app/MapDocument.m index 6c36d00d73..c742440e84 100644 --- a/platform/macos/app/MapDocument.m +++ b/platform/macos/app/MapDocument.m @@ -666,15 +666,6 @@ NS_ARRAY_OF(id <MGLAnnotation>) *MBXFlattenedShapes(NS_ARRAY_OF(id <MGLAnnotatio }]; } -#pragma mark Help methods - -- (IBAction)giveFeedback:(id)sender { - CLLocationCoordinate2D centerCoordinate = self.mapView.centerCoordinate; - NSURL *feedbackURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://www.mapbox.com/map-feedback/#/%.5f/%.5f/%.0f", - centerCoordinate.longitude, centerCoordinate.latitude, round(self.mapView.zoomLevel + 1)]]; - [[NSWorkspace sharedWorkspace] openURL:feedbackURL]; -} - #pragma mark Mouse events - (void)handlePressGesture:(NSPressGestureRecognizer *)gestureRecognizer { diff --git a/platform/macos/macos.xcodeproj/project.pbxproj b/platform/macos/macos.xcodeproj/project.pbxproj index 9a264f0bb7..2f7ef1c2df 100644 --- a/platform/macos/macos.xcodeproj/project.pbxproj +++ b/platform/macos/macos.xcodeproj/project.pbxproj @@ -60,6 +60,8 @@ 5548BE781D09E718005DDE81 /* libmbgl-core.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAE6C3451CC31D1200DB3429 /* libmbgl-core.a */; }; 558F18221D0B13B100123F46 /* libmbgl-loop.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 558F18211D0B13B000123F46 /* libmbgl-loop.a */; }; 55D9B4B11D005D3900C1CCE2 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 55D9B4B01D005D3900C1CCE2 /* libz.tbd */; }; + DA00FC8A1D5EEAC3009AABC8 /* MGLAttributionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = DA00FC881D5EEAC3009AABC8 /* MGLAttributionInfo.h */; }; + DA00FC8B1D5EEAC3009AABC8 /* MGLAttributionInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA00FC891D5EEAC3009AABC8 /* MGLAttributionInfo.mm */; }; DA0CD58E1CF56F5800A5F5A5 /* MGLFeatureTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA0CD58D1CF56F5800A5F5A5 /* MGLFeatureTests.mm */; }; DA2207BC1DC076940002F84D /* MGLStyleValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2207BB1DC076940002F84D /* MGLStyleValueTests.swift */; }; DA2784FE1DF03060001D5B8D /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA2784FD1DF03060001D5B8D /* Media.xcassets */; }; @@ -83,6 +85,8 @@ DA6408D81DA4E5DA00908C90 /* MGLVectorStyleLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = DA6408D61DA4E5DA00908C90 /* MGLVectorStyleLayer.m */; }; DA7262071DEEDD460043BB89 /* MGLOpenGLStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = DA7262051DEEDD460043BB89 /* MGLOpenGLStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; DA7262081DEEDD460043BB89 /* MGLOpenGLStyleLayer.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA7262061DEEDD460043BB89 /* MGLOpenGLStyleLayer.mm */; }; + DA7DC9811DED5F5C0027472F /* MGLVectorSource_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DA7DC9801DED5F5C0027472F /* MGLVectorSource_Private.h */; }; + DA7DC9831DED647F0027472F /* MGLRasterSource_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DA7DC9821DED647F0027472F /* MGLRasterSource_Private.h */; }; DA839E971CC2E3400062CAFB /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DA839E961CC2E3400062CAFB /* AppDelegate.m */; }; DA839E9A1CC2E3400062CAFB /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DA839E991CC2E3400062CAFB /* main.m */; }; DA839E9D1CC2E3400062CAFB /* MapDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = DA839E9C1CC2E3400062CAFB /* MapDocument.m */; }; @@ -184,7 +188,7 @@ DAE6C3A61CC31E9400DB3429 /* MGLMapViewDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = DAE6C3A21CC31E9400DB3429 /* MGLMapViewDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; DAE6C3B11CC31EF300DB3429 /* MGLAnnotationImage.m in Sources */ = {isa = PBXBuildFile; fileRef = DAE6C3A71CC31EF300DB3429 /* MGLAnnotationImage.m */; }; DAE6C3B21CC31EF300DB3429 /* MGLAttributionButton.h in Headers */ = {isa = PBXBuildFile; fileRef = DAE6C3A81CC31EF300DB3429 /* MGLAttributionButton.h */; }; - DAE6C3B31CC31EF300DB3429 /* MGLAttributionButton.m in Sources */ = {isa = PBXBuildFile; fileRef = DAE6C3A91CC31EF300DB3429 /* MGLAttributionButton.m */; }; + DAE6C3B31CC31EF300DB3429 /* MGLAttributionButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = DAE6C3A91CC31EF300DB3429 /* MGLAttributionButton.mm */; }; DAE6C3B41CC31EF300DB3429 /* MGLCompassCell.h in Headers */ = {isa = PBXBuildFile; fileRef = DAE6C3AA1CC31EF300DB3429 /* MGLCompassCell.h */; }; DAE6C3B51CC31EF300DB3429 /* MGLCompassCell.m in Sources */ = {isa = PBXBuildFile; fileRef = DAE6C3AB1CC31EF300DB3429 /* MGLCompassCell.m */; }; DAE6C3B61CC31EF300DB3429 /* MGLMapView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DAE6C3AC1CC31EF300DB3429 /* MGLMapView_Private.h */; }; @@ -203,6 +207,8 @@ DAE6C3D61CC34C9900DB3429 /* MGLStyleTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DAE6C3CC1CC34BD800DB3429 /* MGLStyleTests.mm */; }; DAED385F1D62CED700D7640F /* NSURL+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = DAED385D1D62CED700D7640F /* NSURL+MGLAdditions.h */; }; DAED38601D62CED700D7640F /* NSURL+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DAED385E1D62CED700D7640F /* NSURL+MGLAdditions.m */; }; + DAEDC4321D6033F1000224FF /* MGLAttributionInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DAEDC4311D6033F1000224FF /* MGLAttributionInfoTests.m */; }; + DAEDC4371D606291000224FF /* MGLAttributionButtonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DAEDC4361D606291000224FF /* MGLAttributionButtonTests.m */; }; DD0902B21DB1AC6400C5BDCE /* MGLNetworkConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = DD0902AF1DB1AC6400C5BDCE /* MGLNetworkConfiguration.m */; }; DD0902B31DB1AC6400C5BDCE /* MGLNetworkConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = DD0902B01DB1AC6400C5BDCE /* MGLNetworkConfiguration.h */; }; DD58A4C91D822C6700E1F038 /* MGLExpressionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD58A4C71D822C6200E1F038 /* MGLExpressionTests.mm */; }; @@ -300,6 +306,8 @@ 558F18211D0B13B000123F46 /* libmbgl-loop.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libmbgl-loop.a"; path = "../../build/osx/Debug/libmbgl-loop.a"; sourceTree = "<group>"; }; 55D9B4B01D005D3900C1CCE2 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 55FE0E8D1D100A0900FD240B /* config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = config.xcconfig; path = ../../build/macos/config.xcconfig; sourceTree = "<group>"; }; + DA00FC881D5EEAC3009AABC8 /* MGLAttributionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAttributionInfo.h; sourceTree = "<group>"; }; + DA00FC891D5EEAC3009AABC8 /* MGLAttributionInfo.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLAttributionInfo.mm; sourceTree = "<group>"; }; DA0CD58D1CF56F5800A5F5A5 /* MGLFeatureTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLFeatureTests.mm; path = ../../darwin/test/MGLFeatureTests.mm; sourceTree = "<group>"; }; DA2207BA1DC076930002F84D /* test-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "test-Bridging-Header.h"; sourceTree = "<group>"; }; DA2207BB1DC076940002F84D /* MGLStyleValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MGLStyleValueTests.swift; sourceTree = "<group>"; }; @@ -324,6 +332,8 @@ DA6408D61DA4E5DA00908C90 /* MGLVectorStyleLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLVectorStyleLayer.m; sourceTree = "<group>"; }; DA7262051DEEDD460043BB89 /* MGLOpenGLStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLOpenGLStyleLayer.h; sourceTree = "<group>"; }; DA7262061DEEDD460043BB89 /* MGLOpenGLStyleLayer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLOpenGLStyleLayer.mm; sourceTree = "<group>"; }; + DA7DC9801DED5F5C0027472F /* MGLVectorSource_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLVectorSource_Private.h; sourceTree = "<group>"; }; + DA7DC9821DED647F0027472F /* MGLRasterSource_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLRasterSource_Private.h; sourceTree = "<group>"; }; DA839E921CC2E3400062CAFB /* Mapbox GL.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mapbox GL.app"; sourceTree = BUILT_PRODUCTS_DIR; }; DA839E951CC2E3400062CAFB /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; }; DA839E961CC2E3400062CAFB /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; }; @@ -440,7 +450,7 @@ DAE6C3A21CC31E9400DB3429 /* MGLMapViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapViewDelegate.h; sourceTree = "<group>"; }; DAE6C3A71CC31EF300DB3429 /* MGLAnnotationImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLAnnotationImage.m; sourceTree = "<group>"; }; DAE6C3A81CC31EF300DB3429 /* MGLAttributionButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAttributionButton.h; sourceTree = "<group>"; }; - DAE6C3A91CC31EF300DB3429 /* MGLAttributionButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLAttributionButton.m; sourceTree = "<group>"; }; + DAE6C3A91CC31EF300DB3429 /* MGLAttributionButton.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLAttributionButton.mm; sourceTree = "<group>"; }; DAE6C3AA1CC31EF300DB3429 /* MGLCompassCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLCompassCell.h; sourceTree = "<group>"; }; DAE6C3AB1CC31EF300DB3429 /* MGLCompassCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLCompassCell.m; sourceTree = "<group>"; }; DAE6C3AC1CC31EF300DB3429 /* MGLMapView_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapView_Private.h; sourceTree = "<group>"; }; @@ -459,6 +469,8 @@ DAE6C3CC1CC34BD800DB3429 /* MGLStyleTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLStyleTests.mm; path = ../../darwin/test/MGLStyleTests.mm; sourceTree = "<group>"; }; DAED385D1D62CED700D7640F /* NSURL+MGLAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURL+MGLAdditions.h"; sourceTree = "<group>"; }; DAED385E1D62CED700D7640F /* NSURL+MGLAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURL+MGLAdditions.m"; sourceTree = "<group>"; }; + DAEDC4311D6033F1000224FF /* MGLAttributionInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLAttributionInfoTests.m; path = ../../darwin/test/MGLAttributionInfoTests.m; sourceTree = "<group>"; }; + DAEDC4361D606291000224FF /* MGLAttributionButtonTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLAttributionButtonTests.m; sourceTree = "<group>"; }; DD0902AF1DB1AC6400C5BDCE /* MGLNetworkConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLNetworkConfiguration.m; sourceTree = "<group>"; }; DD0902B01DB1AC6400C5BDCE /* MGLNetworkConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLNetworkConfiguration.h; sourceTree = "<group>"; }; DD58A4C71D822C6200E1F038 /* MGLExpressionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLExpressionTests.mm; path = ../../darwin/test/MGLExpressionTests.mm; sourceTree = "<group>"; }; @@ -543,11 +555,13 @@ DA8F25991D51CAD00010E6B5 /* MGLSource_Private.h */, 352742801D4C243B00A1ECE6 /* MGLSource.mm */, DA8F25951D51CAC70010E6B5 /* MGLVectorSource.h */, + DA7DC9801DED5F5C0027472F /* MGLVectorSource_Private.h */, DA8F25961D51CAC70010E6B5 /* MGLVectorSource.mm */, 352742871D4C245800A1ECE6 /* MGLGeoJSONSource.h */, DA87A99B1DC9D8DD00810D09 /* MGLGeoJSONSource_Private.h */, 352742881D4C245800A1ECE6 /* MGLGeoJSONSource.mm */, 352742831D4C244700A1ECE6 /* MGLRasterSource.h */, + DA7DC9821DED647F0027472F /* MGLRasterSource_Private.h */, 352742841D4C244700A1ECE6 /* MGLRasterSource.mm */, DA551B7F1DB496AC0009AFAF /* MGLTileSet.h */, DA551B801DB496AC0009AFAF /* MGLTileSet_Private.h */, @@ -849,6 +863,8 @@ isa = PBXGroup; children = ( DA8F257D1D51C5F40010E6B5 /* Styling */, + DAEDC4311D6033F1000224FF /* MGLAttributionInfoTests.m */, + DAEDC4361D606291000224FF /* MGLAttributionButtonTests.m */, DA35A2C11CCA9F4A00E826B2 /* MGLClockDirectionFormatterTests.m */, DA35A2B51CCA14D700E826B2 /* MGLCompassDirectionFormatterTests.m */, DA35A2A71CC9F41600E826B2 /* MGLCoordinateFormatterTests.m */, @@ -879,6 +895,8 @@ DAE6C36B1CC31E2A00DB3429 /* MGLAccountManager.m */, DD0902B01DB1AC6400C5BDCE /* MGLNetworkConfiguration.h */, DD0902AF1DB1AC6400C5BDCE /* MGLNetworkConfiguration.m */, + DA00FC881D5EEAC3009AABC8 /* MGLAttributionInfo.h */, + DA00FC891D5EEAC3009AABC8 /* MGLAttributionInfo.mm */, DAE6C34D1CC31E0400DB3429 /* MGLMapCamera.h */, DAE6C36E1CC31E2A00DB3429 /* MGLMapCamera.mm */, DAE6C3571CC31E0400DB3429 /* MGLStyle.h */, @@ -900,7 +918,7 @@ DAC2ABC41CC6D343006D18C4 /* MGLAnnotationImage_Private.h */, DAE6C3A71CC31EF300DB3429 /* MGLAnnotationImage.m */, DAE6C3A81CC31EF300DB3429 /* MGLAttributionButton.h */, - DAE6C3A91CC31EF300DB3429 /* MGLAttributionButton.m */, + DAE6C3A91CC31EF300DB3429 /* MGLAttributionButton.mm */, DAE6C3AA1CC31EF300DB3429 /* MGLCompassCell.h */, DAE6C3AB1CC31EF300DB3429 /* MGLCompassCell.m */, DAE6C3A01CC31E9400DB3429 /* MGLMapView.h */, @@ -934,11 +952,13 @@ DA8F258F1D51CA600010E6B5 /* MGLRasterStyleLayer.h in Headers */, 3508EC641D749D39009B0EE4 /* NSExpression+MGLAdditions.h in Headers */, DAE6C38D1CC31E2A00DB3429 /* MGLOfflineRegion_Private.h in Headers */, + DA7DC9831DED647F0027472F /* MGLRasterSource_Private.h in Headers */, 408AA8651DAEEE3400022900 /* MGLPolygon+MGLAdditions.h in Headers */, DA8F259C1D51CB000010E6B5 /* MGLStyleValue_Private.h in Headers */, DAE6C35B1CC31E0400DB3429 /* MGLAnnotation.h in Headers */, DAE6C3B61CC31EF300DB3429 /* MGLMapView_Private.h in Headers */, 3527428D1D4C24AB00A1ECE6 /* MGLCircleStyleLayer.h in Headers */, + DA00FC8A1D5EEAC3009AABC8 /* MGLAttributionInfo.h in Headers */, DAE6C3B21CC31EF300DB3429 /* MGLAttributionButton.h in Headers */, 40B77E451DB11BC9003DA2FE /* NSArray+MGLAdditions.h in Headers */, 35C5D8471D6DD66D00E95907 /* NSComparisonPredicate+MGLAdditions.h in Headers */, @@ -959,6 +979,7 @@ DAE6C39C1CC31E2A00DB3429 /* NSString+MGLAdditions.h in Headers */, 3529039B1D6C63B80002C7DF /* NSPredicate+MGLAdditions.h in Headers */, DA8F25971D51CAC70010E6B5 /* MGLVectorSource.h in Headers */, + DA7DC9811DED5F5C0027472F /* MGLVectorSource_Private.h in Headers */, DAE6C3861CC31E2A00DB3429 /* MGLGeometry_Private.h in Headers */, DAE6C3841CC31E2A00DB3429 /* MGLAccountManager_Private.h in Headers */, DAE6C3691CC31E0400DB3429 /* MGLTypes.h in Headers */, @@ -1211,7 +1232,7 @@ DACC22151CF3D3E200D220D9 /* MGLFeature.mm in Sources */, DA7262081DEEDD460043BB89 /* MGLOpenGLStyleLayer.mm in Sources */, 355BA4EE1D41633E00CCC6D5 /* NSColor+MGLAdditions.mm in Sources */, - DAE6C3B31CC31EF300DB3429 /* MGLAttributionButton.m in Sources */, + DAE6C3B31CC31EF300DB3429 /* MGLAttributionButton.mm in Sources */, 35602BFB1D3EA99F0050646F /* MGLFillStyleLayer.mm in Sources */, DAE6C3931CC31E2A00DB3429 /* MGLShape.mm in Sources */, 352742861D4C244700A1ECE6 /* MGLRasterSource.mm in Sources */, @@ -1245,6 +1266,7 @@ 408AA8681DAEEE5200022900 /* MGLPolygon+MGLAdditions.m in Sources */, DAE6C3951CC31E2A00DB3429 /* MGLTilePyramidOfflineRegion.mm in Sources */, DAE6C3851CC31E2A00DB3429 /* MGLAccountManager.m in Sources */, + DA00FC8B1D5EEAC3009AABC8 /* MGLAttributionInfo.mm in Sources */, DAE6C3921CC31E2A00DB3429 /* MGLPolyline.mm in Sources */, 3527428A1D4C245800A1ECE6 /* MGLGeoJSONSource.mm in Sources */, DAE6C3B51CC31EF300DB3429 /* MGLCompassCell.m in Sources */, @@ -1269,6 +1291,7 @@ DAE6C3D41CC34C9900DB3429 /* MGLOfflineRegionTests.m in Sources */, DA87A9A11DC9DCB400810D09 /* MGLRuntimeStylingHelper.m in Sources */, DAE6C3D61CC34C9900DB3429 /* MGLStyleTests.mm in Sources */, + DAEDC4371D606291000224FF /* MGLAttributionButtonTests.m in Sources */, DA35A2B61CCA14D700E826B2 /* MGLCompassDirectionFormatterTests.m in Sources */, DAE6C3D21CC34C9900DB3429 /* MGLGeometryTests.mm in Sources */, DA87A9A41DCACC5000810D09 /* MGLSymbolStyleLayerTests.m in Sources */, @@ -1286,6 +1309,7 @@ DA87A9981DC9D88400810D09 /* MGLGeoJSONSourceTests.mm in Sources */, DA87A9A21DC9DCF100810D09 /* MGLFillStyleLayerTests.m in Sources */, 3599A3E81DF70E2000E77FB2 /* MGLStyleValueTests.m in Sources */, + DAEDC4321D6033F1000224FF /* MGLAttributionInfoTests.m in Sources */, DA0CD58E1CF56F5800A5F5A5 /* MGLFeatureTests.mm in Sources */, DA2207BC1DC076940002F84D /* MGLStyleValueTests.swift in Sources */, ); diff --git a/platform/macos/sdk/Base.lproj/Localizable.strings b/platform/macos/sdk/Base.lproj/Localizable.strings index 818c82b2ec..b7a4a21173 100644 --- a/platform/macos/sdk/Base.lproj/Localizable.strings +++ b/platform/macos/sdk/Base.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* Linked part of copyright notice */ -"COPYRIGHT_MAPBOX" = "Mapbox"; - -/* Copyright notice link */ -"COPYRIGHT_MAPBOX_LINK" = "https://www.mapbox.com/about/maps/"; - -/* Linked part of copyright notice */ -"COPYRIGHT_OSM" = "OpenStreetMap"; - -/* Copyright notice link */ -"COPYRIGHT_OSM_LINK" = "http://www.openstreetmap.org/about/"; - -/* Copyright notice prefix */ -"COPYRIGHT_PREFIX" = "© "; - /* Accessibility title */ "MAP_A11Y_TITLE" = "Mapbox"; diff --git a/platform/macos/src/MGLAttributionButton.h b/platform/macos/src/MGLAttributionButton.h index 9ff3137849..88fcdadf78 100644 --- a/platform/macos/src/MGLAttributionButton.h +++ b/platform/macos/src/MGLAttributionButton.h @@ -1,15 +1,23 @@ #import <Cocoa/Cocoa.h> +#import "MGLTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MGLAttributionInfo; + /// Button that looks like a hyperlink and opens a URL. @interface MGLAttributionButton : NSButton -/// Returns an `MGLAttributionButton` instance with the given title and URL. -- (instancetype)initWithTitle:(NSString *)title URL:(NSURL *)url; +/// Returns an `MGLAttributionButton` instance with the given info. +- (instancetype)initWithAttributionInfo:(MGLAttributionInfo *)info; /// The URL to open and display as a tooltip. -@property (nonatomic) NSURL *URL; +@property (nonatomic, readonly, nullable) NSURL *URL; /// Opens the URL. -- (IBAction)openURL:(id)sender; +- (IBAction)openURL:(nullable id)sender; @end + +NS_ASSUME_NONNULL_END diff --git a/platform/macos/src/MGLAttributionButton.m b/platform/macos/src/MGLAttributionButton.m deleted file mode 100644 index e21b860794..0000000000 --- a/platform/macos/src/MGLAttributionButton.m +++ /dev/null @@ -1,50 +0,0 @@ -#import "MGLAttributionButton.h" - -#import "NSBundle+MGLAdditions.h" - -@implementation MGLAttributionButton { - NSTrackingRectTag _trackingAreaTag; -} - -- (instancetype)initWithTitle:(NSString *)title URL:(NSURL *)url { - if (self = [super initWithFrame:NSZeroRect]) { - self.bordered = NO; - self.bezelStyle = NSRegularSquareBezelStyle; - - // Start with a copyright symbol. The whole string will be mini. - NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc] initWithString:NSLocalizedStringWithDefaultValue(@"COPYRIGHT_PREFIX", nil, nil, @"© ", @"Copyright notice prefix") attributes:@{ - NSFontAttributeName: [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]], - }]; - // Append the specified title, underlining it like a hyperlink. - [attributedTitle appendAttributedString: - [[NSAttributedString alloc] initWithString:title - attributes:@{ - NSFontAttributeName: [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]], - NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), - }]]; - self.attributedTitle = attributedTitle; - [self sizeToFit]; - - _URL = url; - self.toolTip = _URL.absoluteString; - - self.target = self; - self.action = @selector(openURL:); - } - return self; -} - -- (BOOL)wantsLayer { - return YES; -} - -- (void)resetCursorRects { - // The whole button gets a pointing hand cursor, just like a hyperlink. - [self addCursorRect:self.bounds cursor:[NSCursor pointingHandCursor]]; -} - -- (IBAction)openURL:(__unused id)sender { - [[NSWorkspace sharedWorkspace] openURL:self.URL]; -} - -@end diff --git a/platform/macos/src/MGLAttributionButton.mm b/platform/macos/src/MGLAttributionButton.mm new file mode 100644 index 0000000000..ed8bb18a66 --- /dev/null +++ b/platform/macos/src/MGLAttributionButton.mm @@ -0,0 +1,55 @@ +#import "MGLAttributionButton.h" +#import "MGLAttributionInfo.h" + +#import "NSBundle+MGLAdditions.h" +#import "NSString+MGLAdditions.h" + +@implementation MGLAttributionButton + +- (instancetype)initWithAttributionInfo:(MGLAttributionInfo *)info { + if (self = [super initWithFrame:NSZeroRect]) { + self.bordered = NO; + self.bezelStyle = NSRegularSquareBezelStyle; + + // Extract any prefix consisting of intellectual property symbols. + NSScanner *scanner = [NSScanner scannerWithString:info.title.string]; + NSCharacterSet *symbolSet = [NSCharacterSet characterSetWithCharactersInString:@"©℗®℠™ &"]; + NSString *symbol; + [scanner scanCharactersFromSet:symbolSet intoString:&symbol]; + + // Remove the underline from the symbol for aesthetic reasons. + NSMutableAttributedString *title = info.title.mutableCopy; + [title removeAttribute:NSUnderlineStyleAttributeName range:NSMakeRange(0, symbol.length)]; + + self.attributedTitle = title; + [self sizeToFit]; + + _URL = info.URL; + if (_URL) { + self.toolTip = _URL.absoluteString; + } + + self.target = self; + self.action = @selector(openURL:); + } + return self; +} + +- (BOOL)wantsLayer { + return YES; +} + +- (void)resetCursorRects { + if (self.URL) { + // The whole button gets a pointing hand cursor, just like a hyperlink. + [self addCursorRect:self.bounds cursor:[NSCursor pointingHandCursor]]; + } +} + +- (IBAction)openURL:(__unused id)sender { + if (self.URL) { + [[NSWorkspace sharedWorkspace] openURL:self.URL]; + } +} + +@end diff --git a/platform/macos/src/MGLMapView.h b/platform/macos/src/MGLMapView.h index 88745db212..ea87f3b338 100644 --- a/platform/macos/src/MGLMapView.h +++ b/platform/macos/src/MGLMapView.h @@ -950,6 +950,25 @@ IB_DESIGNABLE */ - (CLLocationDistance)metersPerPointAtLatitude:(CLLocationDegrees)latitude; +#pragma mark Giving Feedback to Improve the Map + +/** + Opens one or more webpages in the default Web browser in which the user can + provide feedback about the map data. + + You should add a menu item to the Help menu of your application that invokes + this method. Title it “Improve This Map” or similar. Set its target to the + first responder and its action to `giveFeedback:`. + + This map view searches the current style’s sources for webpages to open. + Specifically, each source’s tile set has an `attribution` property containing + HTML code; if an <code><a></code> tag (link) within that code has an + <code>class</code> attribute set to <code>mapbox-improve-map</code>, its + <code>href</code> attribute defines the URL to open. Such links are omitted + from the attribution view. + */ +- (IBAction)giveFeedback:(id)sender; + #pragma mark Debugging the Map /** diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm index a2fe7acbda..bf31daffad 100644 --- a/platform/macos/src/MGLMapView.mm +++ b/platform/macos/src/MGLMapView.mm @@ -1,6 +1,7 @@ #import "MGLMapView_Private.h" #import "MGLAnnotationImage_Private.h" #import "MGLAttributionButton.h" +#import "MGLAttributionInfo.h" #import "MGLCompassCell.h" #import "MGLOpenGLLayer.h" #import "MGLStyle.h" @@ -84,23 +85,6 @@ const CGFloat MGLAnnotationImagePaddingForHitTest = 4; /// Distance from the callout’s anchor point to the annotation it points to. const CGFloat MGLAnnotationImagePaddingForCallout = 4; -/// Copyright notices displayed in the attribution view. -struct MGLAttribution { - /// Attribution button label text. A copyright symbol is prepended to this string. - NSString *title; - /// URL to open when the attribution button is clicked. - NSString *urlString; -} MGLAttributions[] = { - { - .title = NSLocalizedStringWithDefaultValue(@"COPYRIGHT_MAPBOX", nil, nil, @"Mapbox", @"Linked part of copyright notice"), - .urlString = NSLocalizedStringWithDefaultValue(@"COPYRIGHT_MAPBOX_LINK", nil, nil, @"https://www.mapbox.com/about/maps/", @"Copyright notice link"), - }, - { - .title = NSLocalizedStringWithDefaultValue(@"COPYRIGHT_OSM", nil, nil, @"OpenStreetMap", @"Linked part of copyright notice"), - .urlString = NSLocalizedStringWithDefaultValue(@"COPYRIGHT_OSM_LINK", nil, nil, @"http://www.openstreetmap.org/about/", @"Copyright notice link"), - }, -}; - /// Unique identifier representing a single annotation in mbgl. typedef uint32_t MGLAnnotationTag; @@ -371,6 +355,7 @@ public: /// Adds legally required map attribution to the lower-left corner. - (void)installAttributionView { + [_attributionView removeFromSuperview]; _attributionView = [[NSView alloc] initWithFrame:NSZeroRect]; _attributionView.wantsLayer = YES; @@ -433,37 +418,69 @@ public: /// Updates the attribution view to reflect the sources used. For now, this is /// hard-coded to the standard Mapbox and OpenStreetMap attribution. - (void)updateAttributionView { - self.attributionView.subviews = @[]; - - for (NSUInteger i = 0; i < sizeof(MGLAttributions) / sizeof(MGLAttributions[0]); i++) { + NSView *attributionView = self.attributionView; + for (NSView *button in attributionView.subviews) { + [button removeConstraints:button.constraints]; + } + attributionView.subviews = @[]; + [attributionView removeConstraints:attributionView.constraints]; + + // Make the whole string mini by default. + // Force links to be black, because the default blue is distracting. + CGFloat miniSize = [NSFont systemFontSizeForControlSize:NSMiniControlSize]; + NSArray *attributionInfos = [self.style attributionInfosWithFontSize:miniSize linkColor:[NSColor blackColor]]; + for (MGLAttributionInfo *info in attributionInfos) { + // Feedback links are added to the Help menu. + if (info.feedbackLink) { + continue; + } + // For each attribution, add a borderless button that responds to clicks // and feels like a hyperlink. - NSURL *url = [NSURL URLWithString:MGLAttributions[i].urlString]; - NSButton *button = [[MGLAttributionButton alloc] initWithTitle:MGLAttributions[i].title URL:url]; + NSButton *button = [[MGLAttributionButton alloc] initWithAttributionInfo:info]; button.controlSize = NSMiniControlSize; button.translatesAutoresizingMaskIntoConstraints = NO; // Set the new button flush with the buttom of the container and to the // right of the previous button, with standard spacing. If there is no // previous button, align to the container instead. - NSView *previousView = self.attributionView.subviews.lastObject; - [self.attributionView addSubview:button]; - [_attributionView addConstraint: + NSView *previousView = attributionView.subviews.lastObject; + [attributionView addSubview:button]; + [attributionView addConstraint: [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual - toItem:_attributionView + toItem:attributionView attribute:NSLayoutAttributeBottom multiplier:1 constant:0]]; - [_attributionView addConstraint: + [attributionView addConstraint: [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual - toItem:previousView ? previousView : _attributionView + toItem:previousView ? previousView : attributionView attribute:previousView ? NSLayoutAttributeTrailing : NSLayoutAttributeLeading multiplier:1 constant:8]]; + [attributionView addConstraint: + [NSLayoutConstraint constraintWithItem:button + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:attributionView + attribute:NSLayoutAttributeTop + multiplier:1 + constant:0]]; + } + + if (attributionInfos.count) { + [attributionView addConstraint: + [NSLayoutConstraint constraintWithItem:attributionView + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:attributionView.subviews.lastObject + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:8]]; } } @@ -732,20 +749,6 @@ public: attribute:NSLayoutAttributeTrailing multiplier:1 constant:8]]; - [self addConstraint:[NSLayoutConstraint constraintWithItem:_attributionView.subviews.firstObject - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:_attributionView - attribute:NSLayoutAttributeTop - multiplier:1 - constant:0]]; - [self addConstraint:[NSLayoutConstraint constraintWithItem:_attributionView - attribute:NSLayoutAttributeTrailing - relatedBy:NSLayoutRelationEqual - toItem:_attributionView.subviews.lastObject - attribute:NSLayoutAttributeTrailing - multiplier:1 - constant:8]]; [super updateConstraints]; } @@ -924,6 +927,12 @@ public: } break; } + case mbgl::MapChangeSourceDidChange: + { + [self installAttributionView]; + self.needsUpdateConstraints = YES; + break; + } } } @@ -1640,6 +1649,23 @@ public: [self setDirection:-sender.doubleValue animated:YES]; } +- (IBAction)giveFeedback:(id)sender { + CLLocationCoordinate2D centerCoordinate = self.centerCoordinate; + double zoomLevel = self.zoomLevel; + NSMutableArray *urls = [NSMutableArray array]; + for (MGLAttributionInfo *info in [self.style attributionInfosWithFontSize:0 linkColor:nil]) { + NSURL *url = [info feedbackURLAtCenterCoordinate:centerCoordinate zoomLevel:zoomLevel]; + if (url) { + [urls addObject:url]; + } + } + [[NSWorkspace sharedWorkspace] openURLs:urls + withAppBundleIdentifier:nil + options:0 + additionalEventParamDescriptor:nil + launchIdentifiers:nil]; +} + #pragma mark Annotations - (nullable NS_ARRAY_OF(id <MGLAnnotation>) *)annotations { @@ -2454,6 +2480,15 @@ public: return MGLFeaturesFromMBGLFeatures(features); } +#pragma mark User interface validation + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem { + if (menuItem.action == @selector(giveFeedback:)) { + return YES; + } + return [super validateMenuItem:menuItem]; +} + #pragma mark Interface Builder methods - (void)prepareForInterfaceBuilder { diff --git a/platform/macos/test/MGLAttributionButtonTests.m b/platform/macos/test/MGLAttributionButtonTests.m new file mode 100644 index 0000000000..f5c0aac856 --- /dev/null +++ b/platform/macos/test/MGLAttributionButtonTests.m @@ -0,0 +1,31 @@ +#import <Mapbox/Mapbox.h> +#import <XCTest/XCTest.h> + +#import "MGLAttributionButton.h" +#import "MGLAttributionInfo.h" + +@interface MGLAttributionButtonTests : XCTestCase + +@end + +@implementation MGLAttributionButtonTests + +- (void)testPlainSymbol { + NSAttributedString *title = [[NSAttributedString alloc] initWithString:@"® & ™ Mapbox" attributes:@{ + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), + }]; + MGLAttributionInfo *info = [[MGLAttributionInfo alloc] initWithTitle:title URL:nil]; + MGLAttributionButton *button = [[MGLAttributionButton alloc] initWithAttributionInfo:info]; + + NSRange symbolUnderlineRange; + NSNumber *symbolUnderline = [button.attributedTitle attribute:NSUnderlineStyleAttributeName atIndex:0 effectiveRange:&symbolUnderlineRange]; + XCTAssertNil(symbolUnderline); + XCTAssertEqual(symbolUnderlineRange.length, 6); + + NSRange wordUnderlineRange; + NSNumber *wordUnderline = [button.attributedTitle attribute:NSUnderlineStyleAttributeName atIndex:6 effectiveRange:&wordUnderlineRange]; + XCTAssertEqualObjects(wordUnderline, @(NSUnderlineStyleSingle)); + XCTAssertEqual(wordUnderlineRange.length, 6); +} + +@end |