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

Commit

Permalink
[ios, macos] Derive feedback link from source
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
1ec5 committed Dec 8, 2016
1 parent a92a3f6 commit 23acd17
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 45 deletions.
4 changes: 4 additions & 0 deletions platform/darwin/src/MGLAttributionInfo.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <CoreLocation/CoreLocation.h>

#import "MGLTypes.h"

Expand All @@ -25,6 +26,9 @@ NS_ASSUME_NONNULL_BEGIN

@property (nonatomic) NSAttributedString *title;
@property (nonatomic, nullable) NSURL *URL;
@property (nonatomic, getter=isFeedbackLink) BOOL feedbackLink;

- (nullable NSURL *)feedbackURLAtCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel;

@end

Expand Down
59 changes: 39 additions & 20 deletions platform/darwin/src/MGLAttributionInfo.mm
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
#import "MGLAttributionInfo.h"
#import "NSString+MGLAdditions.h"

#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#else
#import <Cocoa/Cocoa.h>
#endif

#include <string>
#import "MGLMapCamera.h"
#import "NSString+MGLAdditions.h"

/**
Absolute string of the URL to the Map Feedback tool.
*/
static NSString * const MGLAttributionFeedbackURLString = @"https://www.mapbox.com/map-feedback/";
#include <string>

@implementation MGLAttributionInfo

Expand All @@ -21,7 +18,10 @@ @implementation MGLAttributionInfo
NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding),
};
NSMutableString *css = [NSMutableString string];
// 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];
}
Expand All @@ -34,33 +34,40 @@ @implementation MGLAttributionInfo
linkColor = [linkColor colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
#endif
[linkColor getRed:&red green:&green blue:&blue alpha:&alpha];
[css appendFormat:@"a:link { color: rgba(%f%%, %f%%, %f%%, %f); }",
[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
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithData:htmlData
options:options
documentAttributes:nil
error:NULL];
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithData:htmlData
options:options
documentAttributes:nil
error:NULL];
#else
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithHTML:htmlData
options:options
documentAttributes:nil];
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) {
// Omit the Map Feedback link because the SDK already provides the appropriate UI for giving feedback.
// Ideally we’d look for class="mapbox-improve-map", but NSAttributedString loses that information.
usingBlock:
^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
NSCAssert(!value || [value isKindOfClass:[NSURL class]], @"If present, URL attribute must be an NSURL.");
if ([value isEqual:[NSURL URLWithString:MGLAttributionFeedbackURLString]]) {
return;

// 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.
Expand All @@ -71,6 +78,7 @@ @implementation MGLAttributionInfo
}

MGLAttributionInfo *info = [[MGLAttributionInfo alloc] initWithTitle:title URL:value];
info.feedbackLink = isFeedbackLink;
[infos addObject:info];
}];
return infos;
Expand All @@ -84,6 +92,17 @@ - (instancetype)initWithTitle:(NSAttributedString *)title URL:(NSURL *)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];
}
Expand Down
15 changes: 14 additions & 1 deletion platform/darwin/test/MGLAttributionInfoTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,29 @@ - (void)testParsing {
[infos growArrayByAddingAttributionInfosFromArray:subinfos];
}

XCTAssertEqual(infos.count, 3);
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 {
Expand Down
3 changes: 0 additions & 3 deletions platform/ios/resources/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -40,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";

Expand Down
22 changes: 10 additions & 12 deletions platform/ios/src/MGLMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1726,25 +1726,18 @@ - (void)showAttribution
_attributionInfos = [self.style attributionInfosWithFontSize:[UIFont buttonFontSize] linkColor:nil];
for (MGLAttributionInfo *info in _attributionInfos)
{
[self.attributionSheet addButtonWithTitle:info.title.string];
NSString *title = [info.title.string capitalizedStringWithLocale:[NSLocale currentLocale]];
[self.attributionSheet addButtonWithTitle:title];
}

[self.attributionSheet addButtonWithTitle:NSLocalizedStringWithDefaultValue(@"MAP_FEEDBACK", nil, nil, @"Improve This Map", @"Action in attribution sheet")];
[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.numberOfButtons - 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.numberOfButtons - 1)
if (buttonIndex == actionSheet.numberOfButtons - 1)
{
NSString *message;
NSString *participate;
Expand Down Expand Up @@ -1773,9 +1766,14 @@ - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSIn
else if (buttonIndex > 0)
{
MGLAttributionInfo *info = _attributionInfos[buttonIndex + actionSheet.firstOtherButtonIndex];
if (info.URL)
NSURL *url = info.URL;
if (url)
{
[[UIApplication sharedApplication] openURL:info.URL];
if (info.feedbackLink)
{
url = [info feedbackURLAtCenterCoordinate:self.centerCoordinate zoomLevel:self.zoomLevel];
}
[[UIApplication sharedApplication] openURL:url];
}
}
}
Expand Down
1 change: 1 addition & 0 deletions platform/macos/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
9 changes: 0 additions & 9 deletions platform/macos/app/MapDocument.m
Original file line number Diff line number Diff line change
Expand Up @@ -666,15 +666,6 @@ - (IBAction)addOfflinePack:(id)sender {
}];
}

#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 {
Expand Down
19 changes: 19 additions & 0 deletions platform/macos/src/MGLMapView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down
31 changes: 31 additions & 0 deletions platform/macos/src/MGLMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,11 @@ - (void)updateAttributionView {
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.
NSButton *button = [[MGLAttributionButton alloc] initWithAttributionInfo:info];
Expand Down Expand Up @@ -1644,6 +1649,23 @@ - (IBAction)rotate:(NSSlider *)sender {
[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 {
Expand Down Expand Up @@ -2458,6 +2480,15 @@ - (void)resetCursorRects {
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 {
Expand Down

0 comments on commit 23acd17

Please sign in to comment.