Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Source-driven attribution #5999

Merged
merged 8 commits into from
Dec 9, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions platform/darwin/src/MGLAttributionInfo.h
Original file line number Diff line number Diff line change
@@ -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
178 changes: 178 additions & 0 deletions platform/darwin/src/MGLAttributionInfo.mm
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions platform/darwin/src/MGLGeoJSONSource.mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

@interface MGLGeoJSONSource ()

- (instancetype)initWithRawSource:(mbgl::style::GeoJSONSource *)rawSource NS_DESIGNATED_INITIALIZER;

@property (nonatomic, readwrite) NSDictionary *options;
@property (nonatomic) mbgl::style::GeoJSONSource *rawSource;

Expand Down Expand Up @@ -60,6 +62,10 @@ - (instancetype)initWithIdentifier:(NSString *)identifier shape:(nullable MGLSha
return self;
}

- (instancetype)initWithRawSource:(mbgl::style::GeoJSONSource *)rawSource {
return [super initWithRawSource:rawSource];
}

- (void)addToMapView:(MGLMapView *)mapView
{
if (_pendingSource == nullptr) {
Expand Down
3 changes: 2 additions & 1 deletion platform/darwin/src/MGLGeoJSONSource_Private.h
Original file line number Diff line number Diff line change
@@ -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
14 changes: 13 additions & 1 deletion platform/darwin/src/MGLRasterSource.mm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#import "MGLRasterSource.h"
#import "MGLRasterSource_Private.h"

#import "MGLMapView_Private.h"
#import "MGLSource_Private.h"
Expand All @@ -9,6 +9,8 @@

@interface MGLRasterSource ()

- (instancetype)initWithRawSource:(mbgl::style::RasterSource *)rawSource NS_DESIGNATED_INITIALIZER;

@property (nonatomic) mbgl::style::RasterSource *rawSource;

@end
Expand Down Expand Up @@ -39,6 +41,16 @@ - (instancetype)initWithIdentifier:(NSString *)identifier tileSet:(MGLTileSet *)
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;
Expand Down
13 changes: 13 additions & 0 deletions platform/darwin/src/MGLRasterSource_Private.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#import "MGLRasterSource.h"

namespace mbgl {
namespace style {
class RasterSource;
}
}

@interface MGLRasterSource (Private)

- (instancetype)initWithRawSource:(mbgl::style::RasterSource *)rawSource;

@end
8 changes: 8 additions & 0 deletions platform/darwin/src/MGLSource.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ - (instancetype)initWithIdentifier:(NSString *)identifier
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. "
Expand Down
12 changes: 10 additions & 2 deletions platform/darwin/src/MGLSource_Private.h
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading