如何拦截MKMapView或UIWebView对象上的触摸事件?


96

我不确定自己在做什么错,但是我试图抓住MKMapView物体。我通过创建以下类将其子类化:

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@interface MapViewWithTouches : MKMapView {

}

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *) event;   

@end

并执行:

#import "MapViewWithTouches.h"
@implementation MapViewWithTouches

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *) event {

    NSLog(@"hello");
    //[super touchesBegan:touches   withEvent:event];

}
@end

但是当我使用此类时,在控制台上什么也看不到:

MapViewWithTouches *mapView = [[MapViewWithTouches alloc] initWithFrame:self.view.frame];
[self.view insertSubview:mapView atIndex:0];

知道我在做什么错吗?

Answers:


147

我发现实现此目标的最佳方法是使用手势识别器。事实证明,其他方法还涉及许多骇人听闻的编程,这些编程不能完美地复制Apple的代码,尤其是在多点触控的情况下。

我的工作是:实现一个无法阻止的手势识别器,并且不能阻止其他手势识别器。将其添加到地图视图中,然后根据需要使用手手势识别器的touchesBegan,touchesMoved等。

如何检测MKMapView内部的任何轻敲(无提示)

WildcardGestureRecognizer * tapInterceptor = [[WildcardGestureRecognizer alloc] init];
tapInterceptor.touchesBeganCallback = ^(NSSet * touches, UIEvent * event) {
        self.lockedOnUserLocation = NO;
};
[mapView addGestureRecognizer:tapInterceptor];

WildcardGestureRecognizer.h

//
//  WildcardGestureRecognizer.h
//  Copyright 2010 Floatopian LLC. All rights reserved.
//

#import <Foundation/Foundation.h>

typedef void (^TouchesEventBlock)(NSSet * touches, UIEvent * event);

@interface WildcardGestureRecognizer : UIGestureRecognizer {
    TouchesEventBlock touchesBeganCallback;
}
@property(copy) TouchesEventBlock touchesBeganCallback;


@end

WildcardGestureRecognizer.m

//
//  WildcardGestureRecognizer.m
//  Created by Raymond Daly on 10/31/10.
//  Copyright 2010 Floatopian LLC. All rights reserved.
//

#import "WildcardGestureRecognizer.h"


@implementation WildcardGestureRecognizer
@synthesize touchesBeganCallback;

-(id) init{
    if (self = [super init])
    {
        self.cancelsTouchesInView = NO;
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (touchesBeganCallback)
        touchesBeganCallback(touches, event);
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}

- (void)reset
{
}

- (void)ignoreTouch:(UITouch *)touch forEvent:(UIEvent *)event
{
}

- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
{
    return NO;
}

- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
{
    return NO;
}

@end

SWIFT 3

let tapInterceptor = WildCardGestureRecognizer(target: nil, action: nil)
tapInterceptor.touchesBeganCallback = {
    _, _ in
    self.lockedOnUserLocation = false
}
mapView.addGestureRecognizer(tapInterceptor)

WildCardGestureRecognizer.swift

import UIKit.UIGestureRecognizerSubclass

class WildCardGestureRecognizer: UIGestureRecognizer {

    var touchesBeganCallback: ((Set<UITouch>, UIEvent) -> Void)?

    override init(target: Any?, action: Selector?) {
        super.init(target: target, action: action)
        self.cancelsTouchesInView = false
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)
        touchesBeganCallback?(touches, event)
    }

    override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }

    override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }
}

3
什么是“ lockedOnUserLocation”?
jowie 2011年

那是我的应用程序特有的无关紧要的变量。它会跟踪系统是否应自动将地图放在当前位置的中心位置
gonzojive 2011年

这是完美的解决方案。我需要澄清一下:在方法“-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event”中,使用代码的目的是:if(touchesBeganCallback)touchesBeganCallback(touches,event);
萨蒂扬

1
这在大多数情况下都很好用,但是我发现了一个问题。如果Web视图中的HTML包含video带有控件的HTML5 标签,则手势识别器将阻止用户使用控件。我一直在寻找解决方法,但尚未找到解决方法。
Bryan Irace

感谢你的分享。为什么没有合适的委托方法来跟踪用户与地图视图的交互,这超出了我的范围,但这很好。
贾斯汀·德里斯科

29

经过一天的尖叫,比萨饼,我终于找到了解决方案!井井有条!

彼得,我使用了上面的技巧,并对其进行了一些微调,最终得到了一个可以与MKMapView完美搭配并且也应该与UIWebView搭配使用的解决方案

MKTouchAppDelegate.h

#import <UIKit/UIKit.h>
@class UIViewTouch;
@class MKMapView;

@interface MKTouchAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    UIViewTouch *viewTouch;
    MKMapView *mapView;
}
@property (nonatomic, retain) UIViewTouch *viewTouch;
@property (nonatomic, retain) MKMapView *mapView;
@property (nonatomic, retain) IBOutlet UIWindow *window;

@end

MKTouchAppDelegate.m

#import "MKTouchAppDelegate.h"
#import "UIViewTouch.h"
#import <MapKit/MapKit.h>

@implementation MKTouchAppDelegate

@synthesize window;
@synthesize viewTouch;
@synthesize mapView;


- (void)applicationDidFinishLaunching:(UIApplication *)application {

    //We create a view wich will catch Events as they occured and Log them in the Console
    viewTouch = [[UIViewTouch alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];

    //Next we create the MKMapView object, which will be added as a subview of viewTouch
    mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
    [viewTouch addSubview:mapView];

    //And we display everything!
    [window addSubview:viewTouch];
    [window makeKeyAndVisible];


}


- (void)dealloc {
    [window release];
    [super dealloc];
}


@end

UIViewTouch.h

#import <UIKit/UIKit.h>
@class UIView;

@interface UIViewTouch : UIView {
    UIView *viewTouched;
}
@property (nonatomic, retain) UIView * viewTouched;

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

@end

UIViewTouch.m

#import "UIViewTouch.h"
#import <MapKit/MapKit.h>

@implementation UIViewTouch
@synthesize viewTouched;

//The basic idea here is to intercept the view which is sent back as the firstresponder in hitTest.
//We keep it preciously in the property viewTouched and we return our view as the firstresponder.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"Hit Test");
    viewTouched = [super hitTest:point withEvent:event];
    return self;
}

//Then, when an event is fired, we log this one and then send it back to the viewTouched we kept, and voilà!!! :)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Began");
    [viewTouched touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Moved");
    [viewTouched touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Ended");
    [viewTouched touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Cancelled");
}

@end

希望对您有所帮助!

干杯


14
真好 小建议:您应该避免使用UI前缀命名自己的类。Apple保留/不鼓励使用NS或UI作为类前缀,因为它们可能最终与Apple类冲突(即使它是私有类)。
Daniel Dickison 09年

嘿,丹尼尔,您说得很对,我也这么认为!为了完成上面的回答,让我添加一点警告:我的示例假定只有一个viewTouched对象正在使用所有事件。但这不是事实。您可能在地图上有一些注释,然后我的代码不再起作用。要100%工作,您需要为每个hitTest记住与特定事件关联的视图(并在触发touchesEnded或touchesCancelled时最终释放它,因此您无需跟踪已完成的事件...)。
马丁

1
非常有用的代码,谢谢马丁!我想知道您是否在实现此功能后尝试缩小地图?对我来说,当我使用与上面基本相同的代码来工作时,除了捏缩放地图外,其他所有功能似乎都可以工作。有人有主意吗?
亚当·亚历山大

嗨,亚当,我也有这个限制,我真的不明白为什么!真烦人。如果您找到解决方案,请告诉我!Thx
Martin

好的,我投了赞成票,因为它最初似乎可以解决我的问题。然而...!我似乎无法实现多点触控。也就是说,即使我直接将touchesBegan和touchesMoved传递给viewTouched(我在touchesEnded上进行拦截),也无法使用捏合手势缩放地图。(续...)
Olie

24
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleGesture:)];   
tgr.numberOfTapsRequired = 2;
tgr.numberOfTouchesRequired = 1;
[mapView addGestureRecognizer:tgr];
[tgr release];


- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer.state != UIGestureRecognizerStateEnded)
        return;

    CGPoint touchPoint = [gestureRecognizer locationInView:mapView];
    CLLocationCoordinate2D touchMapCoordinate = [mapView convertPoint:touchPoint toCoordinateFromView:mapView];

    //.............
}

3
我不确定为什么这不是最佳答案。似乎可以完美工作,并且简单得多。
elsurudo 2013年

12

对于MKMapView,真正的工作解决方案是手势识别!

我当我拖动地图或捏缩放时,想停止更新地图中心的位置。

因此,创建手势识别器并将其添加到mapView:

- (void)viewDidLoad {

    ...

    // Add gesture recognizer for map hoding
    UILongPressGestureRecognizer *longPressGesture = [[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressAndPinchGesture:)] autorelease];
    longPressGesture.delegate = self;
    longPressGesture.minimumPressDuration = 0;  // In order to detect the map touching directly (Default was 0.5)
    [self.mapView addGestureRecognizer:longPressGesture];

    // Add gesture recognizer for map pinching
    UIPinchGestureRecognizer *pinchGesture = [[[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressAndPinchGesture:)] autorelease];
    pinchGesture.delegate = self;
    [self.mapView addGestureRecognizer:pinchGesture];

    // Add gesture recognizer for map dragging
    UIPanGestureRecognizer *panGesture = [[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)] autorelease];
    panGesture.delegate = self;
    panGesture.maximumNumberOfTouches = 1;  // In order to discard dragging when pinching
    [self.mapView addGestureRecognizer:panGesture];
}

查看UIGestureRecognizer类参考以查看所有可用的手势识别器。

因为我们已经将委托定义为self,所以我们必须实现协议UIGestureRecognizerDelegate:

typedef enum {
    MapModeStateFree,                    // Map is free
    MapModeStateGeolocalised,            // Map centred on our location
    MapModeStateGeolocalisedWithHeading  // Map centred on our location and oriented with the compass
} MapModeState;

@interface MapViewController : UIViewController <CLLocationManagerDelegate, UIGestureRecognizerDelegate> {
    MapModeState mapMode;
}

@property (nonatomic, retain) IBOutlet MKMapView *mapView;
...

并重写方法methodRecognizer:gestureRecognizer应该同时使用GestureRecognizerRecognizeWithGestureRecognizer:以允许同时识别多个手势(如果我理解正确的话):

// Allow to recognize multiple gestures simultaneously (Implementation of the protocole UIGestureRecognizerDelegate)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

现在编写将由我们的手势识别器调用的方法:

// On map holding or pinching pause localise and heading
- (void)handleLongPressAndPinchGesture:(UIGestureRecognizer *)sender {
    // Stop to localise and/or heading
    if (sender.state == UIGestureRecognizerStateBegan && mapMode != MapModeStateFree) {
        [locationManager stopUpdatingLocation];
        if (mapMode == MapModeStateGeolocalisedWithHeading) [locationManager stopUpdatingHeading];
    }
    // Restart to localise and/or heading
    if (sender.state == UIGestureRecognizerStateEnded && mapMode != MapModeStateFree) {
        [locationManager startUpdatingLocation];
        if (mapMode == MapModeStateGeolocalisedWithHeading) [locationManager startUpdatingHeading];
    }
}

// On dragging gesture put map in free mode
- (void)handlePanGesture:(UIGestureRecognizer *)sender {
    if (sender.state == UIGestureRecognizerStateBegan && mapMode != MapModeStateFree) [self setMapInFreeModePushedBy:sender];
}

这个解决方案是完美的!这里有一些快捷方法:如果您想在用户结束使用此操作执行操作时进行干预-(void)handleLongPressAndPinchGesture:(UIGestureRecognizer *)sender {if(sender.state == UIGestureRecognizerStateEnded){NSLog(@“ handleLongPressAndPinchGesture Ended”) ; }
Alejandro Luengo

同样不要忘记添加代理<UIGestureRecognizerDelegate>
Alejandro Luengo 2013年

6

万一有人试图像我一样做同样的事情:我想在用户点击的位置创建一个注释。为此,我使用了UITapGestureRecognizer解决方案:

UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapOnMap:)];
[self.mapView addGestureRecognizer:tapGestureRecognizer];
[tapGestureRecognizer setDelegate:self];

- (void)didTapOnMap:(UITapGestureRecognizer *)gestureRecognizer
{
    CGPoint point = [gestureRecognizer locationInView:self.mapView];
    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:point toCoordinateFromView:self.mapView];
    .......
}

但是,didTapOnMap:当我点击注释并创建一个新注释时,也会被调用。解决方案是实施UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    if ([touch.view isKindOfClass:[MKAnnotationView class]])
    {
        return NO;
    }
    return YES;
}

这是一个很好的解决方案!但是,如果您将自定义视图用作,则无法使用MKAnnotation。在这种情况下,您可能需要另一个注释的子视图来触发手势识别器。我必须递归检查touch.view的超级视图,以找到潜在的MKAnnotationView
KIDdAe

3

就像使用基于UIWebView的控件一样,您可能需要覆盖一个透明的视图来捕捉触摸。“地图视图”已经通过触摸完成了许多特殊操作,以使地图可以移动,居中,缩放等...消息不会冒泡到您的应用程序。

我可以想到的另外两个(未测试)选项:

1)通过IB辞职第一响应者,并将其设置为“文件的所有者”,以允许文件的所有者响应触摸。我怀疑这是否可行,因为MKMapView扩展了NSObject,而不是UIView,结果触摸事件仍然可能不会传播给您。

2)如果您想在地图状态更改时(例如在缩放时)捕获,只需实现MKMapViewDelegate协议以侦听特定事件。我的直觉是,这是您轻松捕获一些交互的最佳方法(缺少在地图上实现透明View的功能)。不要忘记将包含MKMapView的View Controller设置为地图的委托(map.delegate = self)。

祝好运。


MKMapView绝对是UIView的子类。
Daniel Dickison 09年

2

我还没有尝试过,但是MapKit很有可能基于类集群,因此对其进行子类化既困难又无效。

我建议使MapKit视图成为自定义视图的子视图,这应该允许您在触摸事件到达之前对其进行拦截。


你好格雷厄姆!谢谢您的帮助!如果我按照您的建议进行超级自定义视图,那么如何将事件转发到MKMapView?任何想法?
马丁

2

因此,经过半天的时间搞乱之后,我发现了以下内容:

  1. 正如其他所有人所发现的,捏不起作用。我尝试了子类化MKMapView和上述方法(进行拦截)。结果是一样的。
  2. 在斯坦福iPhone视频中,苹果公司的一个家伙说,如果您“转移”触摸请求(又称为上述两种方法),许多UIKit事情都会导致很多错误,并且您可能无法使它正常工作。

  3. 解决方案:在此进行描述:拦截/劫持MKMapView的iPhone触摸事件。基本上,您是在任何响应者获取事件之前就“捕获”该事件,并在那里进行解释。


2

在Swift 3.0中

import UIKit
import MapKit

class CoordinatesPickerViewController: UIViewController {

    @IBOutlet var mapView: MKMapView!
    override func viewDidLoad() {
        super.viewDidLoad()

        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(clickOnMap))
        mapView.addGestureRecognizer(tapGestureRecognizer)
    }

    @objc func clickOnMap(_ sender: UITapGestureRecognizer) {

        if sender.state != UIGestureRecognizerState.ended { return }
        let touchLocation = sender.location(in: mapView)
        let locationCoordinate = mapView.convert(touchLocation, toCoordinateFrom: mapView)
        print("Tapped at lat: \(locationCoordinate.latitude) long: \(locationCoordinate.longitude)")

    }

}

0

使MKMapView为自定义视图的子视图并实现

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

在自定义视图中返回self而不是子视图。


您好彼得,谢谢您的回答!但是我认为通过这样做,MKMapView可能无法获取任何touche事件,不是吗?我正在寻找一种方法来捕获事件,然后将其转发到MKMapView。
马丁

0

感谢您的披萨和尖叫声-您节省了我很多时间。

启用多点触控会偶尔出现。

viewTouch.multipleTouchEnabled = TRUE;

最后,当我需要捕获触摸时(与需要pinchzooms不同的时间点),我切换了视图:

    [mapView removeFromSuperview];
    [viewTouch addSubview:mapView];
    [self.view insertSubview:viewTouch atIndex:0];

但不适用于实时变焦。它似乎也总是缩小。
罗格

0

我注意到,您可以跟踪触摸的次数和位置,并在视图中获取每个触摸的位置:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Moved %d", [[event allTouches] count]);

 NSEnumerator *enumerator = [touches objectEnumerator];
 id value;

 while ((value = [enumerator nextObject])) {
  NSLog(@"touch description %f", [value locationInView:mapView].x);
 }
    [viewTouched touchesMoved:touches withEvent:event];
}

是否有其他人尝试使用这些值来更新地图的缩放级别?只需记录开始位置,然后记录结束位置,计算相对差并更新地图即可。

我正在玩Martin提供的基本代码,看起来好像可以使用...


0

这是我放在一起的内容,它确实可以在模拟器中进行捏缩放(尚未在真正的iPhone上尝试过),但我认为可以:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Began %d", [touches count]);
 reportTrackingPoints = NO;
 startTrackingPoints = YES;
    [viewTouched touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
 if ([[event allTouches] count] == 2) {
  reportTrackingPoints = YES;
  if (startTrackingPoints == YES) {
   BOOL setA = NO;
   NSEnumerator *enumerator = [[event allTouches] objectEnumerator];
   id value;
   while ((value = [enumerator nextObject])) {
    if (! setA) {
     startPointA = [value locationInView:mapView];
     setA = YES;
    } else {
     startPointB = [value locationInView:mapView];
    }
   }
   startTrackingPoints = NO;
  } else {
   BOOL setA = NO;
   NSEnumerator *enumerator = [[event allTouches] objectEnumerator];
   id value;
   while ((value = [enumerator nextObject])) {
    if (! setA) {
     endPointA = [value locationInView:mapView];
     setA = YES;
    } else {
     endPointB = [value locationInView:mapView];
    }
   }
  }
 }
 //NSLog(@"Touch Moved %d", [[event allTouches] count]);
    [viewTouched touchesMoved:touches withEvent:event];
}

- (void) updateMapFromTrackingPoints {
 float startLenA = (startPointA.x - startPointB.x);
 float startLenB = (startPointA.y - startPointB.y);
 float len1 = sqrt((startLenA * startLenA) + (startLenB * startLenB));
 float endLenA = (endPointA.x - endPointB.x);
 float endLenB = (endPointA.y - endPointB.y);
 float len2 = sqrt((endLenA * endLenA) + (endLenB * endLenB));
 MKCoordinateRegion region = mapView.region;
 region.span.latitudeDelta = region.span.latitudeDelta * len1/len2;
 region.span.longitudeDelta = region.span.longitudeDelta * len1/len2;
 [mapView setRegion:region animated:YES];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 if (reportTrackingPoints) {
  [self updateMapFromTrackingPoints];
  reportTrackingPoints = NO;
 }


    [viewTouched touchesEnded:touches withEvent:event];
}

主要思想是,如果用户使用两个手指,则可以跟踪值。我在startPoints A和B中记录了起点和终点。然后,记录了当前的跟踪点,完成后,在touchesEnded上,我可以调用一个例程来计算起点之间的线的相对长度,以及使用简单斜边calc结束的点之间的线。它们之间的比率是缩放量:我将区域跨度乘以该量。

希望对某人有用。


0

从MystikSpiral的回答中,我想到了“覆盖”透明视图的想法,它对于我要实现的目标非常有效。快速,干净的解决方案。

简而言之,我有一个自定义UITableViewCell(在IB中设计),左侧带有MKMapView,右侧带有一些UILabel。我想制作自定义单元,以便您可以在任何地方触摸它,这将推动一个新的视图控制器。但是,触摸地图并不会传递“向上”到UITableViewCell,直到我只是在其顶部(在IB中)添加了一个与地图视图大小相同的UIView,并将其背景设置为代码中的“清晰颜色”(不要以为您可以在IB中设置clearColor?):

dummyView.backgroundColor = [UIColor clearColor];

认为这可能会帮助别人;当然,如果您想为表格视图单元实现相同的行为。


“但是直到我只是简单地在其顶部添加与地图视图相同大小的UIView,UITableViewCell才会触摸地图”。该地图正在处理触摸,因为它具有其自身的用户交互(例如滚动等)。如果要检测单元格上的通行信息而不是与地图进行交互,只需设置map.isUserInteractionEnabled = false即可在表中使用didSelectRowAtIndexPath查看代表。
BROK3N S0UL
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.