30

I have searched a lot for this question, but none of them seem to do exactly what I want. A lot of tutorials show me how to add lines and polygons in code, but not with freehand drawing.

The question is the following one:

I am building a real estate application. If the user is on the MKMapView it has the ability to draw a rectangle/circle/... around a certain area where he/she wants to buy/rent a house. Then I need to display the results that correspond within the area the user has selected.

Currently I have a UIView on top of my MKMapView where I do some custom drawing, is there a way to translate points to coordinates from that or ..? Or is this completely not the way this is done ? I have also heard about MKMapOverlayView, etc .. but am not exactly sure how to use this.

Can anybody point me in the right direction or does he have some sample code or a tutorial that can help me accomplish what I am in need for?

Thanks

Nate
  • 30,589
  • 12
  • 76
  • 201
krswtjns
  • 321
  • 4
  • 4
  • I am trying to do the same thing but no luck so far please let me know if you get the solution. My email is :ghostsmitm@gmail.com – Ashutosh Feb 01 '13 at 19:03
  • Hi Guys i am also trying to do same thing but not able to do exactly...if u succeeded then please help me also...my email id rahulmishra449@gmail.com – Rahul Sep 26 '13 at 07:04
  • @krswtjns have you implement this ? – Abhishek Gupta Oct 07 '13 at 08:07

4 Answers4

17

I have an app that basically does this. I have a map view, with a toolbar at the top of the screen. When you press a button on that toolbar, you are now in a mode where you can swipe your finger across the map. The start and end of the swipe will represent the corners of a rectangle. The app will draw a translucent blue rectangle overlay to show the area you've selected. When you lift your finger, the rectangular selection is complete, and the app begins a search for locations in my database.

I do not handle circles, but I think you could do something similar, where you have two selection modes (rectangular, or circular). In the circular selection mode, the swipe start and end points could represent circle center, and edge (radius). Or, the two ends of a diameter line. I'll leave that part to you.

Implementation

First, I define a transparent overlay layer, that handles selection (OverlaySelectionView.h):

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

@protocol OverlaySelectionViewDelegate
// callback when user finishes selecting map region
- (void) areaSelected: (CGRect)screenArea;
@end


@interface OverlaySelectionView : UIView {
@private    
    UIView* dragArea;
    CGRect dragAreaBounds;
    id<OverlaySelectionViewDelegate> delegate;
}

@property (nonatomic, assign) id<OverlaySelectionViewDelegate> delegate;

@end

and OverlaySelectionView.m:

#import "OverlaySelectionView.h"

@interface OverlaySelectionView()
@property (nonatomic, retain) UIView* dragArea;
@end

@implementation OverlaySelectionView

@synthesize dragArea;
@synthesize delegate;

- (void) initialize {
    dragAreaBounds = CGRectMake(0, 0, 0, 0);
    self.userInteractionEnabled = YES;
    self.multipleTouchEnabled = NO;
    self.backgroundColor = [UIColor clearColor];
    self.opaque = NO;
    self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
}

- (id) initWithCoder: (NSCoder*) coder {
    self = [super initWithCoder: coder];
    if (self != nil) {
        [self initialize];
    }
    return self;
}

- (id) initWithFrame: (CGRect) frame {
    self = [super initWithFrame: frame];
    if (self != nil) {
        [self initialize];
    }
    return self;
}

- (void)drawRect:(CGRect)rect {
    // do nothing
}

#pragma mark - Touch handling

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch* touch = [[event allTouches] anyObject];
    dragAreaBounds.origin = [touch locationInView:self];
}

- (void)handleTouch:(UIEvent *)event {
    UITouch* touch = [[event allTouches] anyObject];
    CGPoint location = [touch locationInView:self];

    dragAreaBounds.size.height = location.y - dragAreaBounds.origin.y;
    dragAreaBounds.size.width = location.x - dragAreaBounds.origin.x;

    if (self.dragArea == nil) {
        UIView* area = [[UIView alloc] initWithFrame: dragAreaBounds];
        area.backgroundColor = [UIColor blueColor];
        area.opaque = NO;
        area.alpha = 0.3f;
        area.userInteractionEnabled = NO;
        self.dragArea = area;
        [self addSubview: self.dragArea];
        [dragArea release];
    } else {
        self.dragArea.frame = dragAreaBounds;
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [self handleTouch: event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [self handleTouch: event];

    if (self.delegate != nil) {
        [delegate areaSelected: dragAreaBounds];
    }
    [self initialize];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [self initialize];
    [self.dragArea removeFromSuperview];
    self.dragArea = nil;
}

#pragma mark -

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

@end

Then I have a class that implements the protocol defined above (MapViewController.h):

#import "OverlaySelectionView.h"

typedef struct {
    CLLocationDegrees minLatitude;
    CLLocationDegrees maxLatitude;
    CLLocationDegrees minLongitude;
    CLLocationDegrees maxLongitude;
} LocationBounds;

@interface MapViewController : UIViewController<MKMapViewDelegate, OverlaySelectionViewDelegate> {
    LocationBounds searchBounds;
    UIBarButtonItem* areaButton;

And in my MapViewController.m, the areaSelected method is where I perform the conversion of touch coordinates to geographic coordinates with convertPoint:toCoordinateFromView: :

#pragma mark - OverlaySelectionViewDelegate

- (void) areaSelected: (CGRect)screenArea
{       
    self.areaButton.style = UIBarButtonItemStyleBordered;
    self.areaButton.title = @"Area";

    CGPoint point = screenArea.origin;
    // we must account for upper nav bar height!
    point.y -= 44;
    CLLocationCoordinate2D upperLeft = [mapView convertPoint: point toCoordinateFromView: mapView];
    point.x += screenArea.size.width;
    CLLocationCoordinate2D upperRight = [mapView convertPoint: point toCoordinateFromView: mapView];
    point.x -= screenArea.size.width;
    point.y += screenArea.size.height;
    CLLocationCoordinate2D lowerLeft = [mapView convertPoint: point toCoordinateFromView: mapView];
    point.x += screenArea.size.width;
    CLLocationCoordinate2D lowerRight = [mapView convertPoint: point toCoordinateFromView: mapView];

    searchBounds.minLatitude = MIN(lowerLeft.latitude, lowerRight.latitude);
    searchBounds.minLongitude = MIN(upperLeft.longitude, lowerLeft.longitude);
    searchBounds.maxLatitude = MAX(upperLeft.latitude, upperRight.latitude);
    searchBounds.maxLongitude = MAX(upperRight.longitude, lowerRight.longitude);

    // TODO: comment out to keep search rectangle on screen
    [[self.view.subviews lastObject] removeFromSuperview];

    [self performSelectorInBackground: @selector(lookupHistoryByArea) withObject: nil];
}

// this action is triggered when user selects the Area button to start selecting area
// TODO: connect this to areaButton yourself (I did it in Interface Builder)
- (IBAction) selectArea: (id) sender
{
    PoliteAlertView* message = [[PoliteAlertView alloc] initWithTitle: @"Information"
                                                              message: @"Select an area to search by dragging your finger across the map"
                                                             delegate: self
                                                              keyName: @"swipe_msg_read"
                                                    cancelButtonTitle: @"Ok"
                                                    otherButtonTitles: nil];
    [message show];
    [message release];

    OverlaySelectionView* overlay = [[OverlaySelectionView alloc] initWithFrame: self.view.frame];
    overlay.delegate = self;
    [self.view addSubview: overlay];
    [overlay release];

    self.areaButton.style = UIBarButtonItemStyleDone;
    self.areaButton.title = @"Swipe";
}

You'll notice that my MapViewController has a property, areaButton. That's a button on my toolbar, which normally says Area. After the user presses it, they are in area selection mode at which point, the button label changes to say Swipe to remind them to swipe (maybe not the best UI, but that's what I have).

Also notice that when the user presses Area to enter area selection mode, I show them an alert that tells them that they need to swipe. Since this is probably only a reminder they need to see once, I have used my own PoliteAlertView, which is a custom UIAlertView that users can suppress (don't show the alert again).

My lookupHistoryByArea is just a method that searches my database for locations, by the saved searchBounds (in the background), and then plots new overlays on the map at the found locations. This will obviously be different for your app.

Limitations

  • Since this is for letting the user select approximate areas, I did not consider geographic precision to be critical. It doesn't sound like it should be in your app, either. Thus, I just draw rectangles with 90 degree angles, not accounting for earth curvature, etc. For areas of just a few miles, this should be fine.

  • I had to make some assumptions about your phrase touch based drawing. I decided that both the easiest way to implement the app, and the easiest for a touchscreen user to use, was to simply define the area with one single swipe. Drawing a rectangle with touches would require 4 swipes instead of one, introduce the complexity of non-closed rectangles, yield sloppy shapes, and probably not get the user what they even wanted. So, I tried to keep the UI simple. If you really want the user drawing on the map, see this related answer which does that.

  • This app was written before ARC, and not changed for ARC.

  • In my app, I actually do use mutex locking for some variables accessed on the main (UI) thread, and in the background (search) thread. I took that code out for this example. Depending on how your database search works, and how you choose to run the search (GCD, etc.), you should make sure to audit your own thread-safety.

Community
  • 1
  • 1
Nate
  • 30,589
  • 12
  • 76
  • 201
  • hii nate i amm able to to draw the area on map but i am not able to get the coordinates within the selected area please help – Rahul Sep 26 '13 at 07:14
  • @Rahul, if you look at the code above, `areaSelected:` is the method where I convert a screen area to geographic coordinates (latitude and longitude). If you have other needs, please post a new question. Your comment above isn't enough information for me to know exactly what problem you're having. Thanks. – Nate Sep 26 '13 at 07:35
  • thanks for your reply..my requirement is exactly same as above asked question by Krswtjns for same in my current status i am able to draw line on to my mapview. But in my case as per your above instruction i and given example m able to draw the line..but not able to get coordinate...not able to find where to call your areaSelected method as i am not using any seperate overlay class – Rahul Sep 26 '13 at 07:50
  • @Rahul, in my example, using the `OverlaySelectionView` class is required. This class is where I detect user touches, and where I then call the `areaSelected:` method. If you really don't want to show the user any overlay that indicates the area that their finger has "drawn", that's ok. You can change the code in `OverlaySelectionView` so that you remove all code with the `dragArea` instance variable. You must, however, leave the code that uses `dragAreaBounds`, because that it what will get passed to `areaSelected:`. – Nate Sep 26 '13 at 08:42
3

ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@end

ViewController.m

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

@interface ViewController () <MKMapViewDelegate>
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@property (nonatomic, weak) MKPolyline *polyLine;
@property (nonatomic, strong) NSMutableArray *coordinates;
@property (weak, nonatomic) IBOutlet UIButton *drawPolygonButton;
@property (nonatomic) BOOL isDrawingPolygon;
@end

@implementation ViewController
@synthesize coordinates = _coordinates;

- (NSMutableArray*)coordinates
{
    if(_coordinates == nil) _coordinates = [[NSMutableArray alloc] init];
    return _coordinates;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

- (IBAction)didTouchUpInsideDrawButton:(UIButton*)sender
{
    if(self.isDrawingPolygon == NO) {

        self.isDrawingPolygon = YES;
        [self.drawPolygonButton setTitle:@"done" forState:UIControlStateNormal];
        [self.coordinates removeAllObjects];
        self.mapView.userInteractionEnabled = NO;

    } else {

        NSInteger numberOfPoints = [self.coordinates count];

        if (numberOfPoints > 2)
        {
            CLLocationCoordinate2D points[numberOfPoints];
            for (NSInteger i = 0; i < numberOfPoints; i++)
                points[i] = [self.coordinates[i] MKCoordinateValue];
            [self.mapView addOverlay:[MKPolygon polygonWithCoordinates:points count:numberOfPoints]];
        }

        if (self.polyLine)
            [self.mapView removeOverlay:self.polyLine];

        self.isDrawingPolygon = NO;
        [self.drawPolygonButton setTitle:@"draw" forState:UIControlStateNormal];
        self.mapView.userInteractionEnabled = YES;

    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (self.isDrawingPolygon == NO)
        return;

    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.mapView];
    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];

    [self addCoordinate:coordinate];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (self.isDrawingPolygon == NO)
        return;

    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.mapView];
    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];

    [self addCoordinate:coordinate];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (self.isDrawingPolygon == NO)
        return;

    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInView:self.mapView];
    CLLocationCoordinate2D coordinate = [self.mapView convertPoint:location toCoordinateFromView:self.mapView];

    [self addCoordinate:coordinate];
    [self didTouchUpInsideDrawButton:nil];

}

- (void)addCoordinate:(CLLocationCoordinate2D)coordinate
{
    [self.coordinates addObject:[NSValue valueWithMKCoordinate:coordinate]];

    NSInteger numberOfPoints = [self.coordinates count];
    if (numberOfPoints > 2) {
        MKPolyline *oldPolyLine = self.polyLine;
        CLLocationCoordinate2D points[numberOfPoints];
        for (NSInteger i = 0; i < numberOfPoints; i++) {
            points[i] = [self.coordinates[i] MKCoordinateValue];
        }
        MKPolyline *newPolyLine = [MKPolyline polylineWithCoordinates:points count:numberOfPoints];
        [self.mapView addOverlay:newPolyLine];

        self.polyLine = newPolyLine;
        if (oldPolyLine) {
            [self.mapView removeOverlay:oldPolyLine];
        }
    }
}

#pragma mark - MKMapViewDelegate

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
    MKOverlayPathView *overlayPathView;

    if ([overlay isKindOfClass:[MKPolygon class]])
    {
        overlayPathView = [[MKPolygonView alloc] initWithPolygon:(MKPolygon*)overlay];

        overlayPathView.fillColor = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
        overlayPathView.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.7];
        overlayPathView.lineWidth = 3;

        return overlayPathView;
    }

    else if ([overlay isKindOfClass:[MKPolyline class]])
    {
        overlayPathView = [[MKPolylineView alloc] initWithPolyline:(MKPolyline *)overlay];

        overlayPathView.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.7];
        overlayPathView.lineWidth = 3;

        return overlayPathView;
    }

    return nil;
}

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    if ([annotation isKindOfClass:[MKUserLocation class]])
        return nil;

    static NSString * const annotationIdentifier = @"CustomAnnotation";

    MKAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];

    if (annotationView)
    {
        annotationView.annotation = annotation;
    }
    else
    {
        annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationIdentifier];
        annotationView.image = [UIImage imageNamed:@"annotation.png"];
        annotationView.alpha = 0.5;
    }

    annotationView.canShowCallout = NO;

    return annotationView;
}

@end

or You can find here the entire project : https://github.com/tazihosniomar/MapKitDrawing

i hope it will help you.

poyo fever.
  • 682
  • 1
  • 5
  • 20
2

this is my way how I convert the touches to CLLocation on the MKMapView.

it works with the the Google Maps and the Apple Maps as well:

- (void)viewDidLoad {
    // ...

    // ... where the _customMapView is a MKMapView object;

    // find the gesture recogniser of the map
    UIGestureRecognizer *_factoryDoubleTapGesture = nil;
    NSArray *_gestureRecognizersArray = [_customMapView gestureRecognizers];
    for (UIGestureRecognizer *_tempRecogniser in _gestureRecognizersArray) {
        if ([_tempRecogniser isKindOfClass:[UITapGestureRecognizer class]]) {
            if ([(UITapGestureRecognizer *)_tempRecogniser numberOfTapsRequired] == 2) {
                _factoryDoubleTapGesture = _tempRecogniser;
                break;
            }
        }
    }

    // my tap gesture recogniser
    UITapGestureRecognizer *_locationTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(mapLocationTouchedUpInside:)];
    if (_factoryDoubleTapGesture) [_locationTapGesture requireGestureRecognizerToFail:_factoryDoubleTapGesture];
    [_customMapView addGestureRecognizer:_locationTapGesture];

    // ...
}

and...

- (void)mapLocationTouchedUpInside:(UITapGestureRecognizer *)sender {
    CGPoint _tapPoint = [sender locationInView:_customMapView];
    CLLocationCoordinate2D _coordinates = [_customMapView convertPoint:_tapPoint toCoordinateFromView:_customMapView];

    // ... do whatever you'd like with the coordinates
}
holex
  • 23,420
  • 7
  • 59
  • 73
0

Try MKOverlayPathView. The problem in denoting a region by drawing a path on an MKMapView is, unless you know the zoom scale you don't know much. So you have to track that.