setCoordinate updating after annotation added on my mapView - ios

Im adding a bunch of annotations, and sotirng them in an array so i can pull in their new locations and update the map accordingly WITHOUT removing all the annotations first and adding them which causes a flicker. Unfortunately, after their coordinates are initially set and added any setCoordinate call no longer works. Any ideas?
- (void)updateMap:(NSData *)responseData {
//parse out the JSON data
NSError* error;
vehicleData = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
//find which Dictionary is for our bus.
for (NSDictionary* route in vehicleData) {
//find our bus in our vehicles dictionary. Key is by routeID.
BusPositionDot *busLocationDot;
if ([vehicles objectForKey:[route objectForKey:#"RouteID"]] == nil) {
busLocationDot = [[BusPositionDot alloc] init];
[vehicles setObject:busLocationDot forKey:[route objectForKey:#"RouteID"]];
[mapView addAnnotation:[vehicles objectForKey:[route objectForKey:#"RouteID"]]];
}
else {
busLocationDot = [vehicles objectForKey:#"RouteID"];
}
float latitude = [[route objectForKey:#"Latitude"] floatValue];
float longitude = [[route objectForKey:#"Longitude"] floatValue];
float groundSpeed = [[route objectForKey:#"GroundSpeed"] floatValue];
float direction = [[route objectForKey:#"Heading"] floatValue];
float roundedDirection=45 * round(direction/45);
if(groundSpeed<=3)
//get view for annotation
[mapView viewForAnnotation:busLocationDot].image=[UIImage imageNamed:#"buspositiondot.png"];
else if((roundedDirection==0)||(roundedDirection==360))
[mapView viewForAnnotation:busLocationDot].image=[UIImage imageNamed:#"buspositiondot0.png"];
else if(roundedDirection==45)
[mapView viewForAnnotation:busLocationDot].image=[UIImage imageNamed:#"buspositiondot45.png"];
else if(roundedDirection==90)
[mapView viewForAnnotation:busLocationDot].image=[UIImage imageNamed:#"buspositiondot90.png"];
else if(roundedDirection==135)
[mapView viewForAnnotation:busLocationDot].image=[UIImage imageNamed:#"buspositiondot135.png"];
else if(roundedDirection==180)
[mapView viewForAnnotation:busLocationDot].image=[UIImage imageNamed:#"buspositiondot180.png"];
else if(roundedDirection==225)
[mapView viewForAnnotation:busLocationDot].image=[UIImage imageNamed:#"buspositiondot225.png"];
else if(roundedDirection==270)
[mapView viewForAnnotation:busLocationDot].image=[UIImage imageNamed:#"buspositiondot270.png"];
else if(roundedDirection==315)
[mapView viewForAnnotation:busLocationDot].image=[UIImage imageNamed:#"buspositiondot315.png"];
CLLocationCoordinate2D currentBusLocation = CLLocationCoordinate2DMake(latitude, longitude);
NSLog(#"setting coord %f & %f",currentBusLocation.latitude,currentBusLocation.longitude);
[UIView animateWithDuration:0.5 animations:^{
[busLocationDot setCoordinate:currentBusLocation];
}];
}
}

In reference to this part:
if ([vehicles objectForKey:[route objectForKey:#"RouteID"]] == nil) {
...
[mapView addAnnotation:
[vehicles objectForKey:[route objectForKey:#"RouteID"]]];
}
else {
busLocationDot = [vehicles objectForKey:#"RouteID"];
}
when you call addAnnotation, you pass:
[vehicles objectForKey:[route objectForKey:#"RouteID"]]
but if it already exists, you use:
[vehicles objectForKey:#"RouteID"]
This means when updating an annotation, a different (most likely wrong) reference is being used.
Either [vehicles objectForKey:#"RouteID"] is not actually a BusPositionDot or it's not the same instance that was originally added with that "route id".
Therefore, the setCoordinate wouldn't work.
Using the same reference when updating the annotation should fix it.
There are a couple of other unrelated, potential issues:
The code is doing a direct comparison using a float variable (eg. if(roundedDirection==45)). This is not recommended even if it "seems to work" -- there's the potential for floating-point precision errors. Either check if roundedDirection is within a very small range of the target value or, in your case, just declare roundedDirection as an int since it looks like the expression
45 * round(direction/45) will only return values with no fractions.
The code is setting the image of the annotation view directly. This is ok but make sure the viewForAnnnotation delegate method also has the same exact logic to set the image based on direction otherwise what may happen is the annotation view's image will get reset to some default when panning or zooming. You may need to add a direction property to BusPositionDot.

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];

MKMapView does not move and zoom to search results

I'm implementing search in my MKMapView and I've faced two problems:
When I perform search, location appears in the result and map moves to the found location only after I start to move to the destination. This happens, when the search results are out of the view bounds. If they are inside of the map view bounds or near them it's fine.
It "hops" all the time from one search result to another or to the user's location. I don't expect such behaviour from it.
I've tried several things and I suppose, that the problem is in: didAddAnnotationViews:
[self.locationManager stopUpdatingLocation];
MKAnnotationView *annotationView = [views objectAtIndex:0];
NSLog(#"_Here_ %#", [views description]);
id<MKAnnotation> mp = [annotationView annotation];
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance([mp coordinate], 250, 250);
[mv setRegion:region animated:YES];
[self.mapView selectAnnotation:mp animated:YES];
Though, I also thought that the problem is in didUpdateToLocation, so I disable updating after the first pin is drop (by search or by tap):
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation {
double miles = 0.3;
double scalingFactor =
ABS( cos(2 * M_PI * newLocation.coordinate.latitude /360.0) );
MKCoordinateSpan span;
span.latitudeDelta = miles/69.0;
span.longitudeDelta = miles/( scalingFactor*69.0 );
MKCoordinateRegion region;
region.span = span;
region.center = newLocation.coordinate;
[self.mapView setRegion:region animated:YES];
self.mapView.showsUserLocation = YES;
}
Finally, search method:
-(void)searchBarSearchButtonClicked:(UISearchBar *)theSearchBar
{
MKLocalSearchRequest *searchRequest = [[MKLocalSearchRequest alloc] init];
[searchRequest setNaturalLanguageQuery:theSearchBar.text];
searchRequest.region = MKCoordinateRegionMakeWithDistance(self.mapView.userLocation.coordinate, 1000, 1000);
MKLocalSearch *localSearch = [[MKLocalSearch alloc] initWithRequest:searchRequest];
[localSearch startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
if (!error) {
NSMutableArray *annotations = [NSMutableArray array];
[response.mapItems enumerateObjectsUsingBlock:^(MKMapItem *item, NSUInteger idx, BOOL *stop) {
for (id<MKAnnotation>annotation in self.mapView.annotations)
{
if (annotation.coordinate.latitude == item.placemark.coordinate.latitude &&
annotation.coordinate.longitude == item.placemark.coordinate.longitude)
{
return;
}
}
MKPointAnnotation *addAnnotation = [[MKPointAnnotation alloc] init];
addAnnotation.title = [item.placemark.addressDictionary objectForKey:#"Street"];
addAnnotation.coordinate = item.placemark.coordinate;
[annotations addObject:addAnnotation];
}];
for (id<MKAnnotation>annotation in self.mapView.annotations) {
[self.mapView removeAnnotation:annotation];
}
[self.mapView addAnnotations:annotations];
} else {
NSLog(#"Search Request Error: %#", [error localizedDescription]);
}
}];
//Hide the keyboard.
[self.searchBar resignFirstResponder];
}
My aim is to create a MapView, where user can pin the location by tap or via search and, obviously, see the search result.
For the first problem:
When I perform search, location appears in the result and map moves to
the found location only after I start to move to the destination. This
happens, when the search results are out of the view bounds. If they
are inside of the map view bounds or near them it's fine.
This happens because you are moving the map to the annotations found (at least the first one) in the didAddAnnotationViews delegate method.
But that delegate method is only called when an annotation is in the visible area. If an annotation is added to the map but it's not in the visible area (yet), viewForAnnotation won't get called and therefore didAddAnnotationViews won't get called.
Then, when you manually move the map, the annotations that were added start coming into the visible area and then the delegate method gets called and suddenly the map jumps to one of those annotations.
Don't call setRegion inside the didAddAnnotationViews delegate method.
Sometimes, doing so can also cause an endless cycle of viewForAnnotation and didAddAnnotationViews calls because when the region is changed, it causes other annotations to come into view that weren't previously, so viewForAnnotation gets called and then didAddAnnotationViews gets called, and so on.
Instead, set the region right after you call addAnnotations: (or better, just call showAnnotations:) in the searchBarSearchButtonClicked: method.
I would also remove the call to stopUpdatingLocation from didAddAnnotationViews. You probably don't even need the location manager at all if you set the map's showsUserLocation to YES.
For the second problem:
It "hops" all the time from one search result to another or to the
user's location. I don't expect such behaviour from it.
This is also partly due to calling setRegion in didAddAnnotationViews but also because setRegion is called in didUpdateToLocation.
So for the reason described for the first problem, the two delegate methods and the user's manual movements are fighting with each other and the map ends up hopping around.
Don't call setRegion in the didUpdateToLocation method (or, call it once by keeping track in a BOOL whether you've already zoomed to the user location or not).
Not affecting the behavior, but setting showsUserLocation to YES in the didUpdateToLocation doesn't make sense. Why not set this in viewDidLoad or turn it on in the storyboard/xib?
Also, there's no need to calculate the region span manually like that (it's better to let the MapKit do that work for you). Just convert the miles to meters and call MKCoordinateRegionMakeWithDistance.

Matching Annotations on MKMapView to those in the source array that created them

I have an NSArray of custom objects which are called Proximity. I add these to the map by creating a new ProximityAnnotation like the following:
// Add the annotations to the map
if (self.proximityItems) {
for (Proximity *proximity in self.proximityItems) {
// Create a pin
ProximityAnnotation *proximityAnnotation = [[ProximityAnnotation alloc] init];
proximityAnnotation.coordinate = CLLocationCoordinate2DMake([proximity.latitude doubleValue], [proximity.longitude doubleValue]);
proximityAnnotation.title = proximity.title;
proximityAnnotation.subtitle = NSLocalizedString(#"Drag to change location", nil);
[self.map addAnnotation:proximityAnnotation];
}//end
// Create the map rect
MapUtility *util = [[MapUtility alloc] init];
[util zoomMapViewToFitAnnotations:self.map animated:YES];
}//end
This works great.
Now, when I drag an annotation, I would like to update the corresponding ProximityAnnotation object that is contained in my proximityItems array. I am trying to do this by doing the following:
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)annotationView didChangeDragState:(MKAnnotationViewDragState)newState fromOldState:(MKAnnotationViewDragState)oldState {
// Get the coordiante
if ([annotationView.annotation isKindOfClass:[ProximityAnnotation class]] && newState == MKAnnotationViewDragStateEnding) {
ProximityAnnotation *annotation = (ProximityAnnotation *)annotationView.annotation;
CLLocationCoordinate2D coordinate = annotation.coordinate;
// Find the annotation that matches
for (Proximity *proximity in self.proximityItems) {
NSLog(#"%f == %f && %f == %f && %# == %#", [proximity.latitude doubleValue], coordinate.latitude, [proximity.longitude doubleValue], coordinate.longitude, annotation.title, proximity.title);
if ([proximity.latitude doubleValue] == coordinate.latitude && [proximity.longitude doubleValue] == coordinate.longitude && [annotation.title isEqualToString:proximity.title]) {
// Update the proximity item
proximity.longitude = [NSNumber numberWithDouble:coordinate.longitude];
proximity.latitude = [NSNumber numberWithDouble:coordinate.latitude];
break;
}
}//end
}//end
}//end
Unfortunately, this doesn't seem to get a match even though there is only 1 annotation on the map. Here is what was logged from my NSLog:
37.627946 == 37.622267 && -122.431599 == -122.435596 && Testlocation == Testlocation
Strangely, the double values seem to get a bit off, but I am not sure why.
Is there a better way to match the annotation to the object that I have in my array, so that I can updated that original object?
The coordinate values are most likely "off" because the annotation has been dragged to a new location.
Even if the values were equal, I don't recommend comparing floating-point numbers as a test for object equality.
Instead, I suggest these options:
Add a reference to the source Proximity object in the ProximityAnnotation class and set it when creating the annotation (eg. proximityAnnotation.sourceProximity = proximity;). Then to update the original Proximity object, you can get a reference to it directly from the annotation itself.
Eliminate the ProximityAnnotation class and make the Proximity class itself implement the MKAnnotation protocol in which case an update might not even be necessary.

How to remove just added, one annotation?

I have a lot of annotations on the mapView and user location dot. Then, if user tap for 2 sec. on the map, I add an extra annotation with options. I need to remove that last added annotation from map by pressing the button. How can I remove it without to remove any other annotation?
- (void)addPin:(UILongPressGestureRecognizer*)recognizer {
if(UIGestureRecognizerStateBegan == recognizer.state) {
CGPoint tappedPoint = [recognizer locationInView:mapView];
CLLocationCoordinate2D locCoord= [mapView convertPoint:tappedPoint toCoordinateFromView:mapView];
MKPointAnnotation *annot = [[MKPointAnnotation alloc] init];
annot.coordinate = locCoord;
[self.mapView addAnnotation:annot];
}
if(UIGestureRecognizerStateChanged == recognizer.state) {
// Do repeated work here (repeats continuously) while finger is down
}
if(UIGestureRecognizerStateEnded == recognizer.state) {
// Do end work here when finger is lifted
}
}
To remove all the annotations from map view:
[vwMap removeAnnotations:vwMap.annotations];
PS: vwMap is the MKMap view object
Do the following,
If you have the annotation object
[self.mapView removeAnnotation:annot];
If you have the index of the object
[self.mapView removeAnnotation:self.mapView.annotations.lastObject];
Do this to remove your last added annotation in your delete Action:
[self.mapView removeAnnotation:[self.mapView.annotations lastObject]];
Hope helpful
I managed to remove the annotation object that is touched by doing the following, I know this wasn't the question but it may help someone out
set the mapView as delegate
- (void)mapView:(MKMapView *)thisMapView didSelectAnnotationView:(MKAnnotationView *)view {
MKPointAnnotation *thisTouchedAnnotation = view.annotation;
uint8_t annotationCount = thisMapView.annotations.count;
for(int i =0; i<annotationCount; i++)
{
if ([thisMapView.annotations objectAtIndex:i]==thisTouchedAnnotation){
[thisMapView removeAnnotation:[mapView.annotations objectAtIndex:i]];
break;
}
}
}
not flawless code but it may guide you :-)
Use this code!
NSArray *array=self.mapview.annotations;
for (MKPointAnnotation *anno in array)
{
if(anno==[array lastObject])
{
[self.mapview removeAnnotation:anno];
}
}

Unrecognized Error in MapView (iOS)

I'm getting an error in MapView that I don't recognize and can't find documentation on. It looks like this:
CoreAnimation: ignoring exception:
Invalid Region <center:-180.00000000, -180.00000000
span:+2.81462803, +28.12500000>
Obviously the numbers are exclusive to my code right now but I can't figure out what's going on. The MapView runs just fine and all of my annotations show up (and it zooms on the user's location like I have it set). What specifically does this point to?
Thanks.
Here's the method I use to zoom to the user's location. It's a little unorthodox but it's what I've been helped with since I had problems with zooms for a variety of reasons (I can explain if need be, but it's probably not relevant):
- (void)zoomToUserLocation:(MKUserLocation *)userlocation
{
if (!userlocation)
return;
MKCoordinateRegion region;
region.center = userlocation.coordinate;
region.span = MKCoordinateSpanMake(2.0, 2.0);
region = [self.mapView regionThatFits:region];
[self.mapView setRegion:region animated:YES];
}
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self zoomToUserLocation:self.mapView.userLocation];
}
- (void)mapView:(MKMapView *)theMapView didUpdateUserLocation:(MKUserLocation *)location
{
[self zoomToUserLocation:location];
}
Can't tell where the invalid coordinates are coming from but I suggest adding the following checks to the zoomToUserLocation method.
Just checking if userlocation is nil is not enough. You have to also check if the location property inside userlocation is nil or not. Then, you can use the coordinate property (especially when you're using the coordinates outside the didUpdateUserLocation delegate method).
Also, just checking if coordinate is 0,0 (technically a valid coordinate) is not recommended as the struct will be "zero" if it's never been set or it could even be filled with random data. The Core Location framework's CLLocationCoordinate2DIsValid function is used as the last line of defense to prevent an invalid region.
You could also check the timestamp and horizontalAccuracy if you want.
- (void)zoomToUserLocation:(MKUserLocation *)userlocation
{
if (!userlocation)
return;
if (!userlocation.location)
{
NSLog(#"actual location has not been obtained yet");
return;
}
//optional: check age and/or horizontalAccuracy
//(technically should check if location.timestamp is nil first)
NSTimeInterval locationAgeInSeconds =
[[NSDate date] timeIntervalSinceDate:userlocation.location.timestamp];
if (locationAgeInSeconds > 300) //adjust max age as needed
{
NSLog(#"location data is too old");
return;
}
if (!CLLocationCoordinate2DIsValid(userlocation.coordinate))
{
NSLog(#"userlocation coordinate is invalid");
return;
}
MKCoordinateRegion region;
region.center = userlocation.coordinate;
region.span = MKCoordinateSpanMake(2.0, 2.0);
//region = [self.mapView regionThatFits:region];
//don't need to call regionThatFits explicitly, setRegion will do it
[self.mapView setRegion:region animated:YES];
}
Additionally (possibly unrelated and you may have already done this but), based on a couple of your previous questions related to this, you might want to clear and re-set the map view's delegate in the map view controller's viewWillDisappear and viewWillAppear methods to prevent certain errors:
-(void)viewWillAppear:(BOOL)animated
{
mapView.delegate = self;
}
-(void)viewWillDisappear:(BOOL)animated
{
mapView.delegate = nil;
}
I have found that if you have location services enabled and then display a map view that contains the current user location as an annotation, then disable location services and attempt to use the "location" property of the annotation, the result will be (-180, -180).

Resources