Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add inverted circle overlay to map view

(Using iOS 5 and Xcode 4.2.)

I've followed the instructions here: http://developer.apple.com/library/ios/#documentation/UserExperience/Conceptual/LocationAwarenessPG/AnnotatingMaps/AnnotatingMaps.html#//apple_ref/doc/uid/TP40009497-CH6-SW15 and used the MKCircle and MKCircleView classes to add a circle overlay on my MKMapView.

However what I actually want is an inverted circle overlay, like the left map in the sketch below (currently I have a circle overlay like the one on the right):

Inverted and not-inverted circle overlay sketches

For the inverted circle, the overlay should cover the entire map - apart from the visible circle.

Is there an easy way to accomplish this using the MKCircle/MKCircleView classes? Or will I have to go deeper and define a custom overlay object/view?

Thank you for your help :)

like image 601
Jon Cox Avatar asked Jan 29 '12 00:01

Jon Cox


3 Answers

I had the same task and here is how I solve it:

NOTE: this code will only work starting from iOS7

Add an overlay to the map, somewhere in your view controller:

MyMapOverlay *overlay = [[MyMapOverlay alloc] initWithCoordinate:coordinate];
[self.mapView addOverlay:overlay level:MKOverlayLevelAboveLabels];

In the MKMapViewDelegate methods write next:

- (MKOverlayRenderer *)mapView:(MKMapView *)map rendererForOverlay:(id<MKOverlay>)overlay {
    /// we need to draw overlay on the map in a way when everything except the area in radius of 500 should be grayed
    /// to do that there is special renderer implemented - NearbyMapOverlay
    if ([overlay isKindOfClass:[NearbyMapOverlay class]]) {
        MyMapOverlayRenderer *renderer = [[MyMapOverlayRenderer alloc] initWithOverlay:overlay];
        renderer.fillColor = [UIColor whateverColor];/// specify color which you want to use for gray out everything out of radius
        renderer.diameterInMeters = 1000;/// choose whatever diameter you need

        return renderer;
    }
    return nil;
}

The MyMapOverlay itself should be something like followed:

@interface MyMapOverlay : NSObject<MKOverlay>
- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate;
@end

@implementation MyMapOverlay

@synthesize coordinate = _coordinate;

- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate {
    self = [super init];
    if (self) {
        _coordinate = coordinate;
    }
    return self;
}

- (MKMapRect)boundingMapRect {
    return MKMapRectWorld;
}

@end

And the MyMapOverlayRenderer:

@interface MyMapOverlayRenderer : MKOverlayRenderer
@property (nonatomic, assign) double diameterInMeters;
@property (nonatomic, copy) UIColor *fillColor;
@end

@implementation MyMapOverlayRenderer

/// this method is called as a part of rendering the map, and it draws the overlay polygon by polygon
/// which means that it renders overlay by square pieces
- (void)drawMapRect:(MKMapRect)mapRect
      zoomScale:(MKZoomScale)zoomScale
      inContext:(CGContextRef)context {

    /// main path - whole area
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(mapRect.origin.x, mapRect.origin.y, mapRect.size.width, mapRect.size.height)];

    /// converting to the 'world' coordinates
    double radiusInMapPoints = self.diameterInMeters * MKMapPointsPerMeterAtLatitude(self.overlay.coordinate.latitude);
    MKMapSize radiusSquared = {radiusInMapPoints, radiusInMapPoints};
    MKMapPoint regionOrigin = MKMapPointForCoordinate(self.overlay.coordinate);
    MKMapRect regionRect = (MKMapRect){regionOrigin, radiusSquared}; //origin is the top-left corner
    regionRect = MKMapRectOffset(regionRect, -radiusInMapPoints/2, -radiusInMapPoints/2);
    // clamp the rect to be within the world
    regionRect = MKMapRectIntersection(regionRect, MKMapRectWorld);

    /// next path is used for excluding the area within the specific radius from current user location, so it will not be filled by overlay fill color
    UIBezierPath *excludePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(regionRect.origin.x, regionRect.origin.y, regionRect.size.width, regionRect.size.height) cornerRadius:regionRect.size.width / 2];
    [path appendPath:excludePath];

    /// setting overlay fill color
    CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
    /// adding main path. NOTE that exclusionPath was appended to main path, so we should only add 'path'
    CGContextAddPath(context, path.CGPath);
    /// tells the context to fill the path but with regards to even odd rule
    CGContextEOFillPath(context);
}

As a result you will have exact same view like on the left image that was posted in the question.

like image 65
Valerii Lider Avatar answered Nov 15 '22 21:11

Valerii Lider


The best way to do it, would be to subclass MKMapView and override the drawRect method call super, then paint over the map with the color you want. Then each time the user moves, drawRect should respond by drawing appropriately.

like image 33
Amit Shah Avatar answered Nov 15 '22 22:11

Amit Shah


Here a Swift version. Thanks Valerii.

https://github.com/dariopellegrini/MKInvertedCircle

like image 40
Dario Pellegrini Avatar answered Nov 15 '22 21:11

Dario Pellegrini