MKMapViewDelegate: How to identifiy overlay in rendererForOverlay - ios

I'm adding two different MKGeodesicPolyline instances to an MKMapView like this
CLLocation *LAX = [[CLLocation alloc] ...];
CLLocation *JFK = [[CLLocation alloc] ...];
CLLocation *LHR = [[CLLocation alloc] ...];
CLLocationCoordinate2D laxToJfkCoords[2] = {LAX.coordinate, JFK.coordinate};
CLLocationCoordinate2D jfkToLhrCoords[2] = {JFK.coordinate, LHR.coordinate};
MKGeodesicPolyline *laxToJfk = [MKGeodesicPolyline polylineWithCoordinates:laxToJfkCoords count:2];
MKGeodesicPolyline *jfkToLhr = [MKGeodesicPolyline polylineWithCoordinates:jfkToLhrCoords count:2];
[mapView addOverlay:laxToJfk];
[mapView addOverlay:jfkToLhr];
I want to render both of these overlays with different styles which need to be configured in the rendererForOverlay delegate method.
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>)overlay {
if (![overlay isKindOfClass:[MKPolyline class]]) {
return nil;
}
MKPolylineRenderer *renderer = [[MKPolylineRenderer alloc] initWithPolyline:(MKPolyline *)overlay];
renderer.lineWidth = 3.0f;
// How to set different colors for LAX-JFK and JFK-LHR?
renderer.strokeColor = [UIColor blueColor];
return renderer;
}
My question is what options are there to identify the two different overlays in the above method?
Here's what I considered so far:
Subclassing: Not an option because MKGeodesicPolyline is initialized through a static factory method.
Keep references to the overlays in properties and then compare the delegate's overlay parameter against those. This does work but it feels a little clumsy. Also, for more than two overlays this approach would need to be extended by using an NSSet or an NSArray.
Is there anything else I could do to simplify this? It seems that MKGeodesicPolyline does not possess any properties that could be used for tagging.

One alternative to subclassing is to use associated objects. But its use is often discouraged.
A longer, but more stable solution, is to make a custom MKOverlay and a MKOverlayRenderer that forward most of their implementations to a private instance of MKGeodesicPolyline and MKPolylineRenderer respectively. Then you can add a custom property to set the color.

Related

MKPolyline only draw points instead of lines

I am trying to track user's route and drawing lines of the route, but the addOverlay only gives me correct points but no connection between each point.
-(void)viewWillAppear:(BOOL)animated{
self.trackPointArray = [[NSMutableArray alloc] init];
}
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(CLLocation *)userLocation
{
[self.trackPointArray addObject:userLocation];
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, 1000, 1000);
[self.myMapView setRegion:[self.myMapView regionThatFits:region] animated:YES];
NSInteger stepsNumber = self.trackPointArray.count;
CLLocationCoordinate2D coordinates[stepsNumber];
for (NSInteger index = 0; index < stepsNumber; index++) {
CLLocation *location = [self.trackPointArray objectAtIndex:index];
coordinates[index] = [location coordinate];
}
MKPolyline *polyLine = [MKPolyline polylineWithCoordinates:coordinates count:stepsNumber];
[self.myMapView addOverlay:polyLine];
}
- (MKOverlayRenderer *)mapView:(MKMapView *)myMapView rendererForOverlay:(id<MKOverlay>)overlay
{
MKPolylineRenderer *polylineRenderer = [[MKPolylineRenderer alloc] initWithOverlay:overlay];
polylineRenderer.lineWidth = 4.0f;
polylineRenderer.strokeColor = [UIColor redColor];
return polylineRenderer;
}
The userLocation object the map view passes to the didUpdateUserLocation delegate method is the same object every time.
The coordinate inside the object may be different at each moment but each call to the delegate method always points to the same container object.
Specifically, it always points to the same object that the map view's userLocation property points to (mapView.userLocation). You can see this if you NSLog userLocation and mapView.userLocation and notice their memory addresses are the same each time.
For this reason, when the code does this:
[self.trackPointArray addObject:userLocation];
it just adds the same object reference to the array multiple times.
Later, when the code loops through the trackPointArray array, each call to [location coordinate] returns the same coordinate every time because location always points to the same object (mapView.userLocation) and the coordinate does not change during the loop.
So each time the delegate method is called, a polyline is created with N coordinates (all the same) which ends up drawing a "dot".
The reason you see multiple dots is because the code is not removing previous overlays.
To fix all this, one easy way is to create a new CLLocation instance each time you want to add the updated coordinates:
CLLocation *tpLocation = [[CLLocation alloc]
initWithLatitude:userLocation.coordinate.latitude
longitude:userLocation.coordinate.longitude];
[self.trackPointArray addObject:tpLocation];
Additionally, you should remove the previous overlay before adding the updated line. You won't notice the previous lines if you don't do this but they'll be there using up memory and performance:
[self.myMapView removeOverlays:self.myMapView.overlays];
[self.myMapView addOverlay:polyLine];

mapView viewForOverlay never called

So I want to display where my app's user walked on a MKMapView, I collect datas with the following code :
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation {
// calc. distance walked
CLLocationDistance meters = [newLocation distanceFromLocation:oldLocation];
self.totalMetters += meters;
[[self labelDistance] setText:[self formatDistanceIntoString:self.totalMetters]];
// create another annotation
MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
annotation.coordinate = newLocation.coordinate;
// Also add to our map so we can remove old values later
[self.locations addObject:annotation];
// Remove values if the array is too big
while (self.locations.count > 100)
{
annotation = [self.locations objectAtIndex:0];
[self.locations removeObjectAtIndex:0];
// Also remove from the map
[self.map removeAnnotation:annotation];
}
Once it's finished, I call my draw method :
[self drawRoute];
Which contains the following :
- (void)drawRoute {
NSLog(#"drawRoute");
NSInteger pointsCount = self.locations.count;
CLLocationCoordinate2D pointsToUse[pointsCount];
for(int i = 0; i < pointsCount; i++) {
MKPointAnnotation *an = [self.locations objectAtIndex:i];
pointsToUse[i] = CLLocationCoordinate2DMake(an.coordinate.latitude,an.coordinate.latitude);
}
MKPolyline *myPolyline = [MKPolyline polylineWithCoordinates:pointsToUse count:pointsCount];
[self.map addOverlay:myPolyline];
}
Finally my mapView delegate :
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay {
NSLog(#"route");
if ([overlay isKindOfClass:MKPolyline.class]) {
MKPolylineView *lineView = [[MKPolylineView alloc] initWithOverlay:overlay];
lineView.strokeColor = [UIColor greenColor];
return lineView;
}
return nil;
}
Obviously my controller is MKMapView Delegate conform
#interface NewWalkViewController : UIViewController <CLLocationManagerDelegate, MKMapViewDelegate>
And the mapView in the Storyboard is linked to the controller (outlet and delegate)
I use the "bike road" debug tool and there is the output :
2014-01-25 20:27:30.132 The walking dog[2963:70b] new location : 37.330435
2014-01-25 20:27:30.133 The walking dog[2963:70b] drawRoute
As I can see the method for drawing the overlay is never called, and I don't have a single clue how to fix it.
The main problem is that in drawRoute, this line is passing latitude for both parameters to CLLocationCoordinate2DMake:
pointsToUse[i] = CLLocationCoordinate2DMake
(an.coordinate.latitude,an.coordinate.latitude);
This results in the line being drawn in a different part of the world than where the actual an.coordinate is. For example, if an.coordinate is 37,-122 (somewhere near San Francisco), the line is being drawn instead at 37,37 (somewhere in southern Turkey).
Since you are not actually positioning the map at the wrong location (you are looking for the line at the "right" location), viewForOverlay is never called because the map only calls it when it's possible that the overlay will be visible.
Change that line to:
pointsToUse[i] = CLLocationCoordinate2DMake
(an.coordinate.latitude,an.coordinate.longitude);
or simply:
pointsToUse[i] = an.coordinate;
As James Frost mentions in the comments, as of iOS 7, you should be using rendererForOverlay instead of viewForOverlay though the map view will still call viewForOverlay in iOS 7 if rendererForOverlay has not been implemented. Though this isn't preventing your overlay from displaying in the current case, you should implement the new delegate method as well as the old one (if the iOS 7 app will also be running on iOS 6 or earlier).
Another important but unrelated issue is that you are unnecessarily creating multiple, overlapping overlays. In drawRoute, since the overlay you are creating includes all the locations, you should remove any existing overlays before adding the new one. Otherwise, the map ends up with an overlay for location 0 to 1, an overlay for location 0 to location 2, an overlay for location 0 to location 3, etc. The previous overlays are not obviously visible since they have the same color and line width. Before the addOverlay line, put:
[self.map removeOverlays:self.map.overlays];

How to set location in map in IOS

I am trying to learn MAP for iPhone.
What I have right now is below.
Created new project
Added framework for MAP
Brought map object on storyboard (UIViewController)
Run the project.
What I see is, its not showing any location. When I change location in xcode, it shows me the dot at location.
What I wanted is, by default it should show me the PIN to the location that I will set by using latitude and longitude. Also the map should be zoomed. What I meant by zoom is, I should see the location with lets say 13 zoom effect. Right now, I see world map on screen.
Any idea how to get this done?
You can center your map around a location by doing something like this:
MKCoordinateRegion mapRegion;
mapRegion.center.latitude = aLatitude;
mapRegion.center.longitude = aLongitude;
mapRegion.span.latitudeDelta = 0.005;
mapRegion.span.longitudeDelta = 0.005;
self.mapView.region = mapRegion;
Use the span values to determine the zoom level you want.
In order to show a pin you need to create an annotation with the coordinates of your location and then add it to the map.
Also, check out this tutorial.. http://www.raywenderlich.com/2847/introduction-to-mapkit-on-ios-tutorial
Dot is showing your current location.
If you want to add a pin with coordinate you should call addAnnotation method with object which conforms to MKAnnotation protocol. Such object has a property coordinate (you should add it to your class):
#property (nonatomic, assign) CLLocationCoordinate2D coordinate;
Also you should add MKMapViewDelegate protocol to your controller and implement -mapView:viewForAnnotation: method. It works as -tableView:viewForRowAtIndexPath:.
- (MKAnnotationView *)mapView:(MKMapView *)_mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}
static NSString *annotationIdentifier = #"annotation";
MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[_mapView dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier]; // Reusing
if (!annotationView) {
MKPinAnnotationView *pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationIdentifier];
pinView.animatesDrop = YES;
annotationView = pinView;
}
else {
annotationView.annotation = annotation; // Reusing already created pin as UITableViewCell does
}
return annotationView;
}
Then when you call
MKMapView *mapView = ...;
id<MKAnnotation> obj = ...;
[mapView addAnnotation:obj];
The pin would be placed on map.
For zoom look there. There is a handy category for those purposes.
If you want to remove current location dot you should find an object with class MKUserLocation in mapView.annotations and then call [mapView removeAnnotation:userLocationDot].
For creating an application with Map you need to implement the MKAnnotation, MKMapViewDelegate delgates.
This is a good tutorial for you.

How to ensure display of overlays in iOS MapKit

I've looked at several StackOverflow posts and Apple documentation on how to implement overlays in MKMapView. For me, I'm interested specifically in displaying MKPolygon objects on my map. I've found that fundamentally, the process boils down to the following:
Link to MapKit and CoreLocation frameworks
Make an outlet to an MKMapKit object and declare view controller as delegate
Declare a CLLocationCoordinate2D array containing the points of a polygon and create an MKPolygon object with the class method polygonWithCoordinates:count:
Call addOverlay: of map and pass the newly created MKPolygon object as the parameter
Implement (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id)overlay
Later on, I'll likely be having to display 20-30 polygons at a given time on the map. However, in my exploration of how to display overlays (hardcoding test examples right now, rather than reading in data from a file), I've found that I can get some overlays to appear, but not others. Reading the Location Awareness Programming Guide by Apple, I came across an example of a polygon overlaid above the state of Colorado. That worked. But when I tried to make a polygon that covered Kansas, I couldn't get it to work. It seems that any polygon that I tried to make on my own (Embry-Riddle Aeronautical University polygon and Kansas polygon) won't display, but those that I got online work perfectly. I used Google Earth to create the polygons and then exported them as KML files to get the coordinates.
Code for the implementation of my ViewController is below. Just trying to find out what I may be unintentionally doing wrong to create this problem. Thanks in advance for help.
#import "ViewController.h"
#import <CoreLocation/CoreLocation.h>
#interface ViewController ()
#end
#implementation ViewController
#synthesize mapView;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// Array of coordinates for polygon covering state of Colorado ... DISPLAYS PERFECTLY
CLLocationCoordinate2D points[4];
points[0] = CLLocationCoordinate2DMake(41.000512, -109.050116);
points[1] = CLLocationCoordinate2DMake(36.99892, -109.045267);
points[2] = CLLocationCoordinate2DMake(36.993076, -102.041981);
points[3] = CLLocationCoordinate2DMake(41.002371, -102.052066);
MKPolygon *polygon = [MKPolygon polygonWithCoordinates:points count:4];
[mapView addOverlay:polygon];
[polygon release];
// Array of coordinates for polygon covering state of Kansas ... DOESN'T DISPLAY
CLLocationCoordinate2D kansasPoints[9];
kansasPoints[0] = CLLocationCoordinate2DMake(-102.0595440241806, 39.99774930940907);
kansasPoints[1] = CLLocationCoordinate2DMake(-102.0424467175215, 36.99846609483674);
kansasPoints[2] = CLLocationCoordinate2DMake(-94.62550551403953, 36.98936020770036);
kansasPoints[3] = CLLocationCoordinate2DMake(-94.58798745384412, 39.11683771419185);
kansasPoints[4] = CLLocationCoordinate2DMake(-94.79955391183, 39.21290793052091);
kansasPoints[5] = CLLocationCoordinate2DMake(-95.13489191971419, 39.51613476830012);
kansasPoints[6] = CLLocationCoordinate2DMake(-94.86553124171813, 39.78380472206268);
kansasPoints[7] = CLLocationCoordinate2DMake(-95.02618283417986, 39.89072859904893);
kansasPoints[8] = CLLocationCoordinate2DMake(-95.31904155494097, 39.99390420513669);
MKPolygon *kansasPolygon = [MKPolygon polygonWithCoordinates:kansasPoints count:9];
[mapView addOverlay:kansasPolygon];
[kansasPolygon release];
// Array of coordinates for polygon covering part of Daytona Beach, FL campus
// of Embry-Riddle Aeronautical University... DOESN'T DISPLAY
CLLocationCoordinate2D erauPoints[7];
erauPoints[0] = CLLocationCoordinate2DMake(-81.05176, 29.18492);
erauPoints[1] = CLLocationCoordinate2DMake(-81.04409, 29.18801);
erauPoints[2] = CLLocationCoordinate2DMake(-81.05166, 29.19293);
erauPoints[3] = CLLocationCoordinate2DMake(-81.05365, 29.19536);
erauPoints[4] = CLLocationCoordinate2DMake(-81.05465, 29.19493);
erauPoints[5] = CLLocationCoordinate2DMake(-81.05376, 29.19323);
erauPoints[6] = CLLocationCoordinate2DMake(-81.05506, 29.19188);
MKPolygon *erauPolygon = [MKPolygon polygonWithCoordinates:erauPoints count:7];
[mapView addOverlay:erauPolygon];
[erauPolygon release];
// Array of coordinates taken from http://www.shawngrimes.me/2011/04/adding-polygon-map-overlays/
// for commuter parking lot at Capitol College in Maryland ... DISPLAYS PERFECTLY
CLLocationCoordinate2D commuterLotCoords[5]={
CLLocationCoordinate2DMake(39.048019,-76.850535),
CLLocationCoordinate2DMake(39.048027,-76.850234),
CLLocationCoordinate2DMake(39.047407,-76.850181),
CLLocationCoordinate2DMake(39.047407,-76.8505),
CLLocationCoordinate2DMake(39.048019,-76.850535)
};
MKPolygon *commuterPoly1 = [MKPolygon polygonWithCoordinates:commuterLotCoords count:5];
[mapView addOverlay:commuterPoly1];
[commuterPoly1 release];
}
- (void)viewDidUnload
{
[super viewDidUnload];
// Release any retained subviews of the main view.
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay {
if ([overlay isKindOfClass:[MKPolygon class]]) {
MKPolygonView *polygonView = [[[MKPolygonView alloc] initWithOverlay:overlay] autorelease];
polygonView.fillColor = [[UIColor cyanColor] colorWithAlphaComponent:0.3f];
polygonView.strokeColor = [UIColor redColor];
polygonView.lineWidth = 1.0f;
return polygonView;
}
return nil;
}
#end
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.

Force MKMapView viewForAnnotation to update

So I have a MKMapView with all my pins added, and the colour of the pin is dependent on whether a value is set for that pin. When I first load the app, viewForAnnotation is called and the colours are set accordingly. However, when I update the pin's details (such as location, title, etc...) I also update the pinColour to find it doesn't update. It looks like viewForAnnotation isn't called again after the initial add.
I have read many questions similar to this and I can confirm that mapView.delegate = self;
Here is my viewForAnnotation code:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(MapAnnotation *)annotation
{
if([annotation class] == MKUserLocation.class)
return nil;
NSString *pinIdentifier = annotation.identifier; // This is a unique string for each pin and is getting populated every time!
MKPinAnnotationView *annotationView = (MKPinAnnotationView *) [mapView dequeueReusableAnnotationViewWithIdentifier:pinIdentifier];
if(annotationView == nil)
annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:pinIdentifier];
else
annotationView.annotation = annotation; // Never had this line fire...
annotationView.canShowCallout = YES;
annotationView.animatesDrop = NO;
annotationView.enabled = YES;
annotationView.tag = annotation.counter;
if(annotation.pinColour == Stopped) // from enum
annotationView.pinColor = MKPinAnnotationColorRed;
else
annotationView.pinColor = MKPinAnnotationColorGreen;
UIButton *infoButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[infoButton addTarget:self action:#selector(mapCalloutButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
infoButton.tag = annotation.counter;
annotationView.rightCalloutAccessoryView = infoButton;
return annotationView;
}
Here is the code where I add the pin:
CLLocationCoordinate2D annotationCoord;
annotationCoord.latitude = latestPosition.latitude;
annotationCoord.longitude = latestPosition.longitude;
MapAnnotation *annotation = [[MapAnnotation alloc] init];
annotation.coordinate = annotationCoord;
annotation.identifier = theIdentifier;
annotation.title = theTitle;
annotation.subtitle = theSubtitle
annotation.pinColour = [self getPinColour];
annotation.counter = theCounter;
[theMapView addAnnotation:annotation];
Here is the code where I update the pin (different method to add):
updatePin = true;
pinCounter = mapPin.counter;
CLLocationCoordinate2D annotationCoord;
annotationCoord.latitude = latestPosition.latitude;
annotationCoord.longitude = latestPosition.longitude;
[mapPin setCoordinate:annotationCoord];
mapPin.identifier = theIdentifier;
mapPin.subtitle = theSubtitle;
mapPin.pinColour = [self getPinColour];
I'm not sure what I'm missing. viewForAnnotation is obviously working, it's just not ever called after the initial add! If it were to call this function I'm 100% sure it would work as it does the colour change if I restart the app!
EDIT: Oh and I really don't want to start removing annotations and re-adding them. It's what I'm doing in the short term anyway!
Actually, I dont' know if this worked for you but this is how I did it.
I didn't need to delete the annotation from map. All I need to do is tell the map to give me the annotation view for a parameter annotation. The map will return the correct annotation. From there, I have a property for my custom annotation to identify whether it is an active item, if yes, show the normal pin image, else show full pin image.
-(void)updateAnnotationImage:(CustomAnnotation *)paramAnnotation
{
MKAnnotationView *av = [geoMap viewForAnnotation:paramAnnotation];
if (paramAnnotation.active)
{
av.image = [UIImage imageNamed:#"PinNormal.png"];
}
else
{
av.image = [UIImage imageNamed:#"PinFull.png"];
}
}
Bit late but hopefully it helps others who came across this problem.
Due to the way the map view caches its annotations, you NEED to remove and re-add the annotation if you need to make changes to its appearance. A simple remove & add is the way to go. There is no cache invalidating mechanism but this.
I also found this answer helpful: In which case that mapView:viewForAnnotation: will be called?
Whenever you call addAnnotation method
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id < MKAnnotation >)annotation gets called.
Swift 2.1:
I had the same issue, and found a quick solution, trigger this when needed, also sending it to the main thread would be wise:
var annotationsArray = mapView.annotations
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(arrayIncs)
arrayIncs.removeAll()
Just spent a couple of hours to get this to work on Xamarin; this is a warning for other Xamarin developers. Make sure you use the ViewForAnnotation method and not the GetViewForAnnotation delegate. I was using the wrong method which returned new annotation views instead of the existing ones... of course it wasn't working!

Resources