iPhone Note #14: Drawing a Point, Line, Polygon on top of MKMapview
UPDATE: Aug 9, 2010
DrawMap2.zip
Note: This does not contain the new MapKit functions for overlaying lines and polygons. This zip was created to compile against 4.0.0 but still have the same codebase.
============================
This is an update to iPhone DevNote #13. This post has solved my zooming/panning problem with a CustomView on top of my MKMapView courtesy of http://spitzkoff.com/craig/?p=108 (Craig’s blog).
The trick here is instead of doing the drawing on the drawRect method of the CustomView, we will use Craig’s methodology to use the drawRect method of a custom MKAnnotationView. Note, that he also used an internal view and made clipsToBounds = NO, this way we can draw the whole geometry on top of MKMapView not just a portion of it. The end result is the shape (polygon in this example) is below the added pins.
@interface LinePolygonAnnotationInternalView : UIView { // line view which added this as a subview. LinePolygonAnnotationView* _mainView; } @property (nonatomic, retain) LinePolygonAnnotationView* mainView; @end @implementation LinePolygonAnnotationInternalView @synthesize mainView = _mainView; -(id) init { self = [super init]; self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = NO; return self; } -(void) drawRect:(CGRect) rect { GeometryAnnotation* myAnnotation = (GeometryAnnotation*)self.mainView.annotation; // only draw our lines if we're not int he moddie of a transition and we // acutally have some points to draw. if(!self.hidden && nil != myAnnotation.points && myAnnotation.points.count > ) { CGContextRef context = UIGraphicsGetCurrentContext(); // Drawing lines with a white stroke color CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0); // Draw them with a 2.0 stroke width so they are a bit more visible. CGContextSetLineWidth(context, 2.0); if(myAnnotation.geometryType == kGeometryTypePolygon){ CGContextSetRGBFillColor(context, 0.0, 0.0, 1.0, 1.0); } // Draw them with a 2.0 stroke width so they are a bit more visible. CGContextSetLineWidth(context, 2.0); for(int idx = ; idx < myAnnotation.points.count; idx++) { CLLocation* location = [myAnnotation.points objectAtIndex:idx]; CGPoint point = [self.mainView.mapView convertCoordinate:location.coordinate toPointToView:self]; NSLog(@"Point: %lf, %lf", point.x, point.y); if(idx == ) { // move to the first point CGContextMoveToPoint(context, point.x, point.y); } else { CGContextAddLineToPoint(context, point.x, point.y); } } if(myAnnotation.geometryType == kGeometryTypeLine){ CGContextStrokePath(context); } else if(myAnnotation.geometryType == kGeometryTypePolygon){ CGContextClosePath(context); CGContextDrawPath(context, kCGPathFillStroke); } } } -(void) dealloc { self.mainView = nil; [super dealloc]; } @end @implementation LinePolygonAnnotationView @synthesize mapView = _mapView; - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor clearColor]; // do not clip the bounds. We need the LinePolygonAnnotationInternalView to be able to render the whole line/polygon, regardless of where the // actual annotation view is displayed. self.clipsToBounds = NO; // create the internal line view that does the rendering of the line. _internalView = [[LinePolygonAnnotationInternalView alloc] init]; _internalView.mainView = self; [self addSubview:_internalView]; } return self; } -(void) setMapView:(MKMapView*) mapView { [_mapView release]; _mapView = [mapView retain]; [self regionChanged]; } -(void) regionChanged { NSLog(@"Region Changed"); // move the internal line view. CGPoint origin = CGPointMake(, ); origin = [_mapView convertPoint:origin toView:self]; _internalView.frame = CGRectMake(origin.x, origin.y, _mapView.frame.size.width, _mapView.frame.size.height); [_internalView setNeedsDisplay]; } - (void)dealloc { [_mapView release]; [_internalView release]; [super dealloc]; } @end |
I extended the class above to be able to draw both lines and polygons by checking a property (geometryType) of the GeometryAnnotation. If the geometryType is a line, then just stroke the path. However, if the geometryType is a polygon, then close the path and fill it.
if(myAnnotation.geometryType == kGeometryTypeLine){ CGContextStrokePath(context); } else if(myAnnotation.geometryType == kGeometryTypePolygon){ CGContextClosePath(context); CGContextDrawPath(context, kCGPathFillStroke); } |
And here is the GeometryAnnotation class. Most of the code is from Craig, i just added the geometryType property:
// Created by Craig on 8/18/09. // Copyright Craig Spitzkoff 2009. All rights reserved. // #import "GeometryAnnotation.h" @implementation GeometryAnnotation @synthesize coordinate = _center; @synthesize points = _points; @synthesize annotationID; @synthesize geometryType; -(id) initWithPoints:(NSArray*) points withGeometry:(GeometryType)geomType { self = [super init]; geometryType = geomType; _points = [[NSMutableArray alloc] initWithArray:points]; // create a unique ID for this line so it can be added to dictionaries by this key. self.annotationID = [NSString stringWithFormat:@"%p", self]; // determine a logical center point for this line based on the middle of the lat/lon extents. double maxLat = -91; double minLat = 91; double maxLon = -181; double minLon = 181; for(CLLocation* currentLocation in _points) { CLLocationCoordinate2D coordinate = currentLocation.coordinate; if(coordinate.latitude > maxLat) maxLat = coordinate.latitude; if(coordinate.latitude < minLat) minLat = coordinate.latitude; if(coordinate.longitude > maxLon) maxLon = coordinate.longitude; if(coordinate.longitude < minLon) minLon = coordinate.longitude; } _span.latitudeDelta = (maxLat + 90) - (minLat + 90); _span.longitudeDelta = (maxLon + 180) - (minLon + 180); // the center point is the average of the max and mins _center.latitude = minLat + _span.latitudeDelta / 2; _center.longitude = minLon + _span.longitudeDelta / 2; NSLog(@"Found center of new Annotation at %lf, %ld", _center.latitude, _center.longitude); return self; } -(MKCoordinateRegion) region { MKCoordinateRegion region; region.center = _center; region.span = _span; return region; } -(void) dealloc { [_points release]; [super dealloc]; } @end |
Now that we have a way to draw a line/polygon as a custom MKAnnotationView, we need a custom TouchView (GeometryTouchView) which could accept the touch events.
For example, if the user wants to draw a line geometry, the GeometryTouchView would accept touch events from the user and add a point as a PointAnnotation in the Map. Succeeding points would be added to an array. For every point added, the MKAnnotationView drawRects method connects the points to produce a line. The MKAnnotationView is now added to the map.
Once the geometry is added as an annotation, the custom TouchView is hidden. This way we have access (panning/zooming) to the mapview. If we make a pan or a zoom, the region changes, thus we need to redraw the shape of the annotation again.
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated { if(currentAnnotationView != nil){ NSLog(@"regionWillChangeAnimated"); currentAnnotationView.hidden = YES; } } - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated { if(currentAnnotationView != nil){ NSLog(@"regionDidChangeAnimated"); currentAnnotationView.hidden = NO; [currentAnnotationView regionChanged]; } } |
-(void) regionChanged { NSLog(@"Region Changed"); // move the internal line view. CGPoint origin = CGPointMake(, ); origin = [_mapView convertPoint:origin toView:self]; _internalView.frame = CGRectMake(origin.x, origin.y, _mapView.frame.size.width, _mapView.frame.size.height); [_internalView setNeedsDisplay]; } |
The resulting image is now:
(Download the DrawMap.zip code.) – old. This is for iOS < 4