[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.
This commit is contained in:
Minh Nguyễn
2016-12-08 17:43:17 -08:00
committed by GitHub
parent 89d4c40d92
commit fe53af7de0
33 changed files with 824 additions and 212 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;

View File

@@ -0,0 +1,13 @@
#import "MGLRasterSource.h"
namespace mbgl {
namespace style {
class RasterSource;
}
}
@interface MGLRasterSource (Private)
- (instancetype)initWithRawSource:(mbgl::style::RasterSource *)rawSource;
@end

View File

@@ -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. "

View File

@@ -1,12 +1,20 @@
#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,

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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
@end
NS_ASSUME_NONNULL_END

View File

@@ -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;

View File

@@ -0,0 +1,13 @@
#import "MGLVectorSource.h"
namespace mbgl {
namespace style {
class VectorSource;
}
}
@interface MGLVectorSource (Private)
- (instancetype)initWithRawSource:(mbgl::style::VectorSource *)rawSource;
@end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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 */,

View File

@@ -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";

View File

@@ -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;
}
}
}

View File

@@ -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))

View File

@@ -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:

View File

@@ -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 {

View File

@@ -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 */,
);

View File

@@ -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";

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>&lt;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
/**

View File

@@ -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 {

View File

@@ -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