iPhone Mkmapview - Mkpolygon Issues

iPhone MKMapView - MKPolygon Issues

The polygonWithCoordinates method wants a C array of CLLocationCoordinate2D structs. You can use malloc to allocate memory for the array (and free to release the memory). Loop through your NSArray and set it each element in the struct array.

For example:

coordsLen = [coordinateData count];
CLLocationCoordinate2D *coords = malloc(sizeof(CLLocationCoordinate2D) * coordsLen);
for (int i=0; i < coordsLen; i++)
{
YourCustomObj *coordObj = (YourCustomObj *)[coordinateData objectAtIndex:i];
coords[i] = CLLocationCoordinate2DMake(coordObj.latitude, coordObj.longitude);
}
MKPolygon *polygon = [MKPolygon polygonWithCoordinates:coords count:coordsLen];
free(coords);
[mapView addOverlay:polygon];

The viewForOverlay method should look like this:

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
MKPolygonView *polygonView = [[[MKPolygonView alloc] initWithPolygon:overlay] autorelease];
polygonView.lineWidth = 1.0;
polygonView.strokeColor = [UIColor redColor];
polygonView.fillColor = [UIColor greenColor];
return polygonView;
}

Problems Adding MKPolygon as an Overlay to an MKMapView

The main issue is that the code is giving the map view latitude/longitude coordinates where it expects MKMapPoints. For an explanation of the difference, see "Understanding Map Geometry" in the Location Awareness Programming Guide. Use the MKMapPointForCoordinate function to convert from lat/long coordinates to an MKMapPoint.

The second issue is that in viewForOverlay, it is checking if overlay is of type MKPolygon. Your overlay class ParkingRegionOverlay contains an MKPolygon object inside it but is not itself of type MKPolygon.

To fix the main issue, you need to change the initialize and boundingMapRect methods:

-(id)init {
if (self = [super init]) {
MKMapPoint points[3];
CLLocationCoordinate2D c1 = {38.53607,-121.765793};
points[0] = MKMapPointForCoordinate(c1);
CLLocationCoordinate2D c2 = {38.537606,-121.768379};
points[1] = MKMapPointForCoordinate(c2);
CLLocationCoordinate2D c3 = {38.53487,-121.770578};
points[2] = MKMapPointForCoordinate(c3);

polygon = [MKPolygon polygonWithPoints:points count:3];
polygon.title = @"Some Polygon";
}
return self;
}

- (MKMapRect)boundingMapRect{
CLLocationCoordinate2D corner1 =
CLLocationCoordinate2DMake(38.537606, -121.770578);
MKMapPoint mp1 = MKMapPointForCoordinate(corner1);

CLLocationCoordinate2D corner2 =
CLLocationCoordinate2DMake(38.53487, -121.765793);
MKMapPoint mp2 = MKMapPointForCoordinate(corner2);

MKMapRect bounds =
MKMapRectMake(mp1.x, mp1.y, (mp2.x-mp1.x), (mp2.y-mp1.y));

return bounds;
}

Please notice by the way that I changed the method "initialize" to "init". Though it wasn't preventing the polygon from showing, the way you are overriding the initialization of ParkingRegionOverlay using a method called "initialize" and not calling [super init] does not follow convention. (Also remove "initialize" from the .h file.)

To fix the second issue, the viewForOverlay method should look like this:

- (MKOverlayView *)mapView:(MKMapView *)mapView 
viewForOverlay:(id <MKOverlay>)overlay
{
NSLog(@"in viewForOverlay!");

if ([overlay isKindOfClass:[ParkingRegionOverlay class]])
//^^^^^^^^^^^^^^^^^^^^
{
//get the MKPolygon inside the ParkingRegionOverlay...
MKPolygon *proPolygon = ((ParkingRegionOverlay*)overlay).polygon;

MKPolygonView *aView = [[[MKPolygonView alloc]
initWithPolygon:proPolygon] autorelease];
//^^^^^^^^^^

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

return aView;
}
return nil;
}

Finally, change the code in viewDidLoad:

ParkingRegionOverlay *polygon = [[ParkingRegionOverlay alloc] init];
[mapView addOverlay:polygon];
[polygon release]; //don't forget this

MKMapView with multiple overlays memory-issue

The Answer to this is not "reusing" but to draw them all in to one MKOverlayView and then draw that on the map.

Multiple MKPolygons, MKOverlays etc. cause heavy memory-usage when drawing on the map. This is due the NOT reusing of overlays by MapKit. As annotations have the reuseWithIdentifier, overlays however don't. Each overlay creates a new layer as a MKOverlayView on the map with the overlay in it. In that way memory-usage will rise quite fast and map-usage becomes... let's say sluggish to almost impossible.

Therefore there is a work-around: Instead of plotting each overlay individually, you can add all of the MKOverlays to one MKOverlayView. This way you're in fact creating only one MKOverlayView and thus there's no need to reuse.

This is the work-around, in this case for MKPolygons but should be not very different for others like MKCircles etc.

create a class: MultiPolygon (subclass of NSObject)

in MultiPolygon.h:

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

@interface MultiPolygon : NSObject <MKOverlay> {
NSArray *_polygons;
MKMapRect _boundingMapRect;
}

- (id)initWithPolygons:(NSArray *)polygons;
@property (nonatomic, readonly) NSArray *polygons;

@end

in MultiPolygon.m:

@implementation MultiPolygon

@synthesize polygons = _polygons;

- (id)initWithPolygons:(NSArray *)polygons
{
if (self = [super init]) {
_polygons = [polygons copy];

NSUInteger polyCount = [_polygons count];
if (polyCount) {
_boundingMapRect = [[_polygons objectAtIndex:0] boundingMapRect];
NSUInteger i;
for (i = 1; i < polyCount; i++) {
_boundingMapRect = MKMapRectUnion(_boundingMapRect, [[_polygons objectAtIndex:i] boundingMapRect]);
}
}
}
return self;
}

- (MKMapRect)boundingMapRect
{
return _boundingMapRect;
}

- (CLLocationCoordinate2D)coordinate
{
return MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMidX(_boundingMapRect), MKMapRectGetMidY(_boundingMapRect)));
}

@end

Now create a class: MultiPolygonView (subclass of MKOverlayPathView)

in MultiPolygonView.h:

#import <MapKit/MapKit.h>

@interface MultiPolygonView : MKOverlayPathView

@end

In MultiPolygonView.m:

#import "MultiPolygon.h"  //Add import "MultiPolygon.h"

@implementation MultiPolygonView

- (CGPathRef)polyPath:(MKPolygon *)polygon

{
MKMapPoint *points = [polygon points];
NSUInteger pointCount = [polygon pointCount];
NSUInteger i;

if (pointCount < 3)
return NULL;

CGMutablePathRef path = CGPathCreateMutable();

for (MKPolygon *interiorPolygon in polygon.interiorPolygons) {
CGPathRef interiorPath = [self polyPath:interiorPolygon];
CGPathAddPath(path, NULL, interiorPath);
CGPathRelease(interiorPath);
}

CGPoint relativePoint = [self pointForMapPoint:points[0]];
CGPathMoveToPoint(path, NULL, relativePoint.x, relativePoint.y);
for (i = 1; i < pointCount; i++) {
relativePoint = [self pointForMapPoint:points[i]];
CGPathAddLineToPoint(path, NULL, relativePoint.x, relativePoint.y);
}

return path;
}

- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context
{
MultiPolygon *multiPolygon = (MultiPolygon *)self.overlay;
for (MKPolygon *polygon in multiPolygon.polygons) {
CGPathRef path = [self polyPath:polygon];
if (path) {
[self applyFillPropertiesToContext:context atZoomScale:zoomScale];
CGContextBeginPath(context);
CGContextAddPath(context, path);
CGContextDrawPath(context, kCGPathEOFill);
[self applyStrokePropertiesToContext:context atZoomScale:zoomScale];
CGContextBeginPath(context);
CGContextAddPath(context, path);
CGContextStrokePath(context);
CGPathRelease(path);
}
}
}

@end

To us it import MultiPolygon.h and MultiPolygonView.h in your ViewController

Create one polygon from all:
As an example I've got an array with polygons: polygonsInArray.

MultiPolygon *allPolygonsInOne = [[MultiPolygon alloc] initWithPolygons:polygonsInArray];

Add the allPolygonsInOne to the mapView:

[mapView addOverlay:allPolygonsInOne];

Also change your viewForOverlay method:

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

if ([overlay isKindOfClass:[MultiPolygon class]]) {
MultiPolygonView *polygonsView = [[MultiPolygonView alloc] initWithOverlay:(MultiPolygon*)overlay];
polygonsView.fillColor = [[UIColor magentaColor] colorWithAlphaComponent:0.8];
polygonsView.strokeColor = [[UIColor blueColor] colorWithAlphaComponent:0.8];
polygonsView.lineWidth = 1;
return polygonsView;
}
else {
return nil;
}

}

And this greatly reduced memory usage for multiple overlays on the mapView. You're not reusing now because only one OverlayView is drawn. So no need to reuse.

Validate if a latlong is inside a MKPolygon in iOS

The picture shown appears to demonstrate the even-odd fill rule. By specifying FALSE as the final parameter to CGPathContainsPoint you've asked it to apply the winding number rule. Try passing TRUE.

For information on the teo rules see Apple's Quartz documentation, particularly 'Filling a Path' (slightly less than halfway down).

MKPolygon ontouch detailview

Unfortunately, for overlays, there's no built-in touch-detection and callout view like there is for annotations.

You'll have to do the touch-detection manually like you're already doing (and it looks like it should work).

(Even more unfortunate here is that adding a gesture recognizer directly to the overlay view doesn't work -- you have to add it to the whole map and then check whether the touch point is in any overlay.)

For an overlay callout view, once you've detected a touch on an overlay, you can create a custom UIView and do addSubview. I suggest adding it to the map instead of the overlay view and you might be able to use the CGPoint point you are already calculating to determine the frame of the custom callout view.

You might also want to keep a ivar/property reference to the overlay callout view so it can be easily removed and re-added if the user taps on another overlay while the callout for another overlay is already displayed.

Another option which is probably easier is to create a custom UIViewController and present or push it. The specifics of showing it depend on whether you're using a navigation controller and/or storyboard.

If your app is also built for iPad, you could also show the "callout" using a UIPopoverController.

See How do I display a UIPopoverView as a annotation to the map view? (iPad) for a code example (it's with an annotation but you should be able to adapt it for the overlay).


Once you've identified which overlay was tapped, you need to display its associated data which is in your original data source (the outages array). Right now, overlays are created and added but have no reference back to the original data object (outage dictionary in outages array).

(Subclassing MKPolygon to add a custom property has issues and workarounds and creating a completely custom MKOverlay class introduces a lot of other additional work.)

For your current data source structure, a simple, quick (and somewhat crude) option is to set the overlay's title property to the index in the outages array of the outage object associated with the overlay. Since the title property is an NSString and the array index is an integer, we'll convert it to a string:

NSUInteger outageIndex = [outages indexOfObject:coloredAreas];
poly2.title = [NSString stringWithFormat:@"%d", outageIndex];
[self.mapView addOverlay:poly2];

In viewForOverlay, it looks like you're using test (which comes from an outage object) to determine the polygon's color. The value of the externally declared/set test variable will not necessarily be in sync with the overlay the delegate method is currently being called for (the map could call viewForOverlay multiple times for the same overlay and not necessarily in the order you add them). You have to retrieve the outage object based on some property of the overlay parameter. Since we are setting the overlay's title property to the outage's index:

//int numbers = [test intValue];  <-- remove this line

int outageIndex = [overlay.title intValue];
NSDictionary *outageDict = [outages objectAtIndex:outageIndex];
id outageNumbersObject = outageDict[@"outages"];
//replace id above with actual type
//can't tell from code in question whether it's NSString or NSNumber
int numbers = [outageNumbersObject intValue];

//use "numbers" to set polygon color...

Finally, when an overlay is tapped, you use the same method as in viewForOverlay to get the outage object:

if (mapCoordinateIsInPolygon) {
int outageIndex = [overlay.title intValue];
NSDictionary *outageDict = [outages objectAtIndex:outageIndex];
NSLog(@"hit, outageDict = %@", outageDict);
//show view with info from outageDict...
}

How to ensure display of overlays in iOS MapKit

It looks like the latitude and longitude parameters of the coordinates for the polygons that don't display are backwards.

For example, this:

kansasPoints[0] = CLLocationCoordinate2DMake(-102.0595440241806, 39.99774930940907);

should be

kansasPoints[0] = CLLocationCoordinate2DMake(39.99774930940907, -102.0595440241806);


Also, you should not be calling release on the MKPolygon objects you are creating using polygonWithCoordinates since they will be autoreleased.

Detecting touches on MKOverlay in iOS7 (MKOverlayRenderer)

I've done it.

Thanks to incanus and Anna!

Basically I add a TapGestureRecognizer to the MapView, convert the point tapped to map coordinates, go through my overlays and check with CGPathContainsPoint.

Adding TapGestureRecognizer. I did that trick of adding a second double tap gesture, so that the single tap gesture isn't fired when doing a double tap to zoom on map. If anyone knows a better way, I'm glad to hear!

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapTap:)];
tap.cancelsTouchesInView = NO;
tap.numberOfTapsRequired = 1;

UITapGestureRecognizer *tap2 = [[UITapGestureRecognizer alloc] init];
tap2.cancelsTouchesInView = NO;
tap2.numberOfTapsRequired = 2;

[self.mapView addGestureRecognizer:tap2];
[self.mapView addGestureRecognizer:tap];
[tap requireGestureRecognizerToFail:tap2]; // Ignore single tap if the user actually double taps

Then, on the tap handler:

-(void)handleMapTap:(UIGestureRecognizer*)tap{
CGPoint tapPoint = [tap locationInView:self.mapView];

CLLocationCoordinate2D tapCoord = [self.mapView convertPoint:tapPoint toCoordinateFromView:self.mapView];
MKMapPoint mapPoint = MKMapPointForCoordinate(tapCoord);
CGPoint mapPointAsCGP = CGPointMake(mapPoint.x, mapPoint.y);

for (id<MKOverlay> overlay in self.mapView.overlays) {
if([overlay isKindOfClass:[MKPolygon class]]){
MKPolygon *polygon = (MKPolygon*) overlay;

CGMutablePathRef mpr = CGPathCreateMutable();

MKMapPoint *polygonPoints = polygon.points;

for (int p=0; p < polygon.pointCount; p++){
MKMapPoint mp = polygonPoints[p];
if (p == 0)
CGPathMoveToPoint(mpr, NULL, mp.x, mp.y);
else
CGPathAddLineToPoint(mpr, NULL, mp.x, mp.y);
}

if(CGPathContainsPoint(mpr , NULL, mapPointAsCGP, FALSE)){
// ... found it!
}

CGPathRelease(mpr);
}
}
}

I could ask for the MKPolygonRenderer which already has the "path" property and use it, but for some reason it is always nil. I did read someone saying that I could call invalidatePath on the renderer and it does fill the path property but it just seems wrong as the point is never found inside any of the polygons. That is why I rebuild the path from the points. This way I don't even need the renderer and just make use of the MKPolygon object.

MKPolygon performance problem

This is an issue that is known by Apple but unlikely to change. Basically anything more then a couple of MKOverlayViews you will have performance issues no matter what your hardware. What you have to basically do is to subclass MKPolygonView and merge all the MKPolygons into one MKPolygonView.

Code is available on Apple Forums but as I didn't write it I don't think I should post it here.

MKMapView overlays redraw(blink) when adding new overlays. MKPolyLines are jagged

For the blinking, see this post - it may be a bug in the overlay system.

With the jagged lines, they do appear to be antialiased in your screenshot, but may have been scaled up. From experimentation, I believe that overlays are rendered to a separate bitmap for discrete zoom levels and then rendered over the map view scaled up or down to suit the current zoom level. So it may be that your overlay looks a bit rough at that level but zooming in or out a bit improves it.

Regarding the delegate, you could check the class of the overlay that is passed in to cut down the complexity of your mapView:viewForOverlay function rather than checking for specific overlays. So:

if ([overlay isKindOfClass:[MKPolyline class]])
{
borderView = [[MKPolylineView alloc] initWithPolyline:overlay];
borderView.strokeColor = [UIColor blackColor];
borderView.lineWidth = 1.0;
return borderView;
}


Related Topics



Leave a reply



Submit