How to store CLLocationCoordinate2D? - ios

I'm attempting to build an application that builds and saves routes similar to map my run. I'm using the Breadcrumb sample code, specifically the CrumbPath and CrumbPathView as the base of my routes, from Apple. Two questions:
If I try to access the MKMapPoint *points object of the CrumbPath like so:
[_route lockForReading];
NSLog(#"%#", _route.points);
NSLog(#"%d", _route.pointCount);
[_route unlockForReading];
my app crashes, saying:
Thread 1: EXC_BAD_ACCESS (code: 1, address: 0x9450342d)
Which I have a hard time understanding, because within the CrumbPath.m file, the folks at apple write to the "array" by explicitly acquiring the write lock, and then unlocking it, but if I acquire the read lock and attempt to read from it, it crashes.
The reason I attempt to access the points is in an attempt to get the MKMapPoints, convert them to CLLocationCoordinate2D objects, and save them so I can redraw the polyline at the user's request. Since I cannot get access to the points, I attempt to save the CLLocationCoordinate2D objects from my locationManager that I send to the _route in an array to upload to my Parse backend, but I always get an error saying:
Sending 'CLLocationCoordinate2D' to parameter of incompatible type 'id'
Which isn't making this any easier. Does anybody have any insight to why I'm getting these errors?
Location Manager Delegate
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
if (_userLocation.longitude != manager.location.coordinate.longitude
&& _userLocation.latitude != manager.location.coordinate.latitude) {
_userLocation = manager.location.coordinate;
}
if (_isRecording) {
if (!_route) {
NSLog(#"lat: %f, lng: %f", _userLocation.latitude, _userLocation.longitude);
_route = [[CrumbPath alloc] initWithCenterCoordinate:_userLocation];
[_mapView addOverlay:_route];
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(_userLocation, 2000, 2000);
[_mapView setRegion:region animated:YES];
}else {
MKMapRect updateRect = [_route addCoordinate:_userLocation];
if (!MKMapRectIsNull(updateRect)) {
MKZoomScale currentZoomScale = (CGFloat)(_mapView.bounds.size.width / _mapView.visibleMapRect.size.width);
CGFloat lineWidth = MKRoadWidthAtZoomScale(currentZoomScale);
updateRect = MKMapRectInset(updateRect, -lineWidth, -lineWidth);
[_routeView setNeedsDisplayInMapRect:updateRect];
}
}
[_routePoints addObject:_userLocation];
[_route lockForReading];
NSLog(#"%d", _route.pointCount);
NSLog(#"%#", _route.points);
[_route unlockForReading];
}
}
Stop Recording Logic
//stop recording
NSLog(#"STOP");
if (_route) {
NSLog(#"There is a route");
//Show route options toolbar
[_route lockForReading];
NSLog(#"%#", _route);
NSLog(#"%d", _route.pointCount);
NSLog(#"%#", _route.points);
PFObject *routeToSave = [PFObject objectWithClassName:#"Routes"];
//[routeToSave setObject:_route forKey:#"routePoints"];
[_route unlockForReading];
[routeToSave saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (!error) {
NSLog(#"%c", succeeded);
}else {
NSLog(#"%#", error);
}
}];
}

Regarding your first issue, the crash was because of this:
NSLog(#"%#", _route.pointCount);
It should be:
NSLog(#"%d", _route.pointCount);
As mentioned in my comments, %d should be used for count and %# will cause a crash.
Regarding your second issue, you cannot add a c struct to an NSArray. You should wrap it in NSValue before adding it to an array. CLLocationCoordinate2D is a c-struct. Check the documentation here.
Change this:
[_routePoints addObject:_userLocation];
to:
NSValue *aValue = [NSValue valueWithMKCoordinate:_userLocation];
[_routePoints addObject:aValue];
To get the coordinate back from NSValue, you can use,
[aValue MKCoordinateValue];
As mentioned in your error message, you were trying to add CLLocationCoordinate2D to an array which expects an object.

Whatever api you're using to talk to parse is expecting an id which is a pointer to any object. A cllocationcoordinate2d is a c-struct of two doubles and not an object if I'm not mistaken. You should probably create a little wrapper object to save those two doubles and convert them to/from CLLocationCoordinate2d items.

1:
Line: NSLog(#"%#", _route.points); is wrong
_route.points is not a String, and you are using the NSStrig formating symbol "%#".
Further:
Since CLLocationCoordinate2D is a C-Sruct and not an Objective-C Object, you probaly want to create an own GeoPoint class.

Related

How to get GMSAddress from PlaceID or GMSPlace

I am using Autocomplete of Google Places API for iOS, and it returns placeID. How can I get GMSAddress from the placeID? I currently use lookupPlaceID to get GMSPlace, and then use reverseGeocodeCoordinate to get GMSAddress from coordinate in GMSPlace. But it request Google API 3 times: AutoComplete, lookupPlaceID, reverseGeocodeCoordinate.
Is there any way to get GMSAddress from PlaceID or GMSPlace directly?
This is a bug with 1.1.0 of the Google Maps SDK for iOS. See here.
As the documentation states here, the address property should expose a GMSAddress on GMSPlace instances.
I've just switched to using the REST API. It's not too bad to write a quick wrapper for the API and ditch the Google SDK completely. If you are using Alamofire ping me and I may be able to share what I've written.
I could get the address from placedID in one API call, here is the code:
GMSPlacesClient *placesClient = [[GMSPlacesClient alloc] init];
[placesClient lookUpPlaceID:result.placeID callback:^(GMSPlace *place, NSError *error) {
if (error != nil) {
NSLog(#"Place Details error %#", [error localizedDescription]);
return;
}
if (place != nil) {
NSString *locality = #"";
for (GMSAddressComponent *add in place.addressComponents) {
//locality
NSLog(#"%#",add.type);
NSLog(#"%#",add.name);
//type will be street_name, locality, admin_area, country,
//name will hold the corresponding value
}
} else {
NSLog(#"No place details for %#", result.placeID);
}
}];
Now for the time being, is the code below safe (enough) for this version of the SDK (1.10.5)? What is the probability that this will break in the next 6 months?
NSArray* dic = [place valueForKey:#"addressComponents"];
for (int i=0;i<[dic count];i++) {
if ([[[dic objectAtIndex:i] valueForKey:#"type"] isEqualToString:#"street_number"]) {
NSLog(#"street_number: %#",[[dic objectAtIndex:i] valueForKey:#"name"]);
}

How to get current location at the instant UIImagePickerController captures an image?

I have researched on how to get location data from images returned from UIImagePickerController camera. However, I think that the easiest way is to get the current location from CLLocationManager at the instant UIImagePickerController captures an image.
Is there a way of doing this? Is there a way of listening for the "capturePhoto" event, for example?
Just to clarify, the users using my app will likely be moving pretty fast.
Here's what I'd recommend so you don't track the user's location any more than you have to and so you get the user's location closest to the time the image was actually snapped.
Instantiate the CLLocationManager class variable in your viewDidLoad, ex:
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
And make sure it's authorized:
if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedWhenInUse) {
[self.locationManager requestWhenInUseAuthorization];
}
(Also include the "NSLocationWhenInUseUsageDescription" key in the .plist)
Then you could wait until the UIImagePickerController is actually presented before (1) initializing the dictionary to hold the locations and (2) starting to update the location, ex:
[self presentViewController:self.imagePicker animated:YES completion:nil];
self.locationDictionary = [[NSMutableDictionary alloc] init];
[self.locationManager startUpdatingLocation];
At that point, you can start storing the user's updated locations in an NSMutableDictionary self.locationDictionary class instance variable when CLLocation values are returned from the didUpdateToLocation delegate method, ex:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
// Format the current date time to match the format of
// the photo's metadata timestamp string
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"YYYY:MM:dd HH:mm:ss"];
NSString *stringFromDate = [formatter stringFromDate:[NSDate date]];
// Add the location as a value in the location NSMutableDictionary
// while using the formatted current datetime as its key
[self.locationDictionary setValue:newLocation forKey:stringFromDate];
}
And then once the image is selected, find its timestamp in the metadata and find the value in the location dictionary with a timestamp key closest to the image timestamp, ex:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self.locationManager stopUpdatingLocation];
// When a photo is selected save it as a UIImage
self.selectedPhoto = info[UIImagePickerControllerOriginalImage];
// Get the timestamp from the metadata and store it as an NSString
self.selectedPhotoDateTime = [[[info valueForKey:UIImagePickerControllerMediaMetadata] objectForKey:#"{Exif}"] objectForKey:#"DateTimeOriginal"];
// If the CLLocationManager is in fact authorized
// and locations have been found...
if (self.locationDictionary.allKeys.count > 0) {
// Sort the location dictionary timestamps in ascending order
NSArray *sortedKeys = [[self.locationDictionary allKeys] sortedArrayUsingSelector: #selector(compare:)];
// As a default, set the selected photo's CLLocation class
// variable to contain the first value in the sorted dictionary
self.selectedPhotoLocation = [self.locationDictionary objectForKey:[sortedKeys objectAtIndex:0]];
// Then go through the location dictionary and set the
// photo location to whatever value in the dictionary
// has a key containing a time most closely before
// the image timestamp. Note that the keys can be compared
// as strings since they're formatted in descending order --
// year to month to day to hour to minute to second.
for (NSString *key in sortedKeys) {
// If the photo's metadata timestamp is less than or equal to
// the current key, set the selected photo's location class
// variable to contain the CLLocation value associated with the key
if ([self.selectedPhotoDateTime compare:key] != NSOrderedAscending) {
self.selectedPhotoLocation = [self.locationDictionary objectForKey:key];
}
// Else if the time in the location dictionary is past
// the photo's timestamp, break from the loop
else {
break;
}
}
}
[self dismissViewControllerAnimated:YES completion:nil];
}
In your .h file you need to add the following to your code:
#interface TakePhotoViewController : UIViewController <CLLocationManagerDelegate>
In your .m file you need the following.
#interface TakePhotoViewController (){
//location stuff
CLLocationManager * manager;
CLGeocoder *geocoder;
CLPlacemark * placemark;
}
The above code set's up relevant references to find your location.
Next add this to your viewDidLoad method:
manager = [[CLLocationManager alloc] init];
geocoder = [[CLGeocoder alloc] init];
This initialises the Location Manager and Geocoder.
Then in your code that either initiates taking a picture or returns the picture to a view use this:
manager.delegate = self;
manager.desiredAccuracy = kCLLocationAccuracyBest;
[manager startUpdatingLocation];
To stop the continuous updating of the location add this to your imagePickerReturn method:
[manager stopUpdatingLocation];
As soon as you stop updating the location you will be saving the very last location that was updated. The location updates once every .5 - 1 second, so even if you are moving or have the camera open for a long time it will only store the location of whatever image you pick. To save the date (which includes time down to milliseconds) use:
NSDate * yourCoreDataDateName = [NSDate date];
For good coding practice to handle any errors you will need this:
//handles the error if location unavailable
- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
NSLog(#"Error: %#", error);
NSLog(#"Failed to get location... :-(");
}
- (void) locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation{
NSLog(#"Location: %#", newLocation);
CLLocation * currentLocation = newLocation;
self.locationTF.text = #"Finding Location...";
if (currentLocation != nil){
[geocoder reverseGeocodeLocation:currentLocation completionHandler:^(NSArray *placemarks, NSError *error) {
if (error == nil && [placemarks count] > 0){
placemark = [placemarks lastObject];
//the code below translates the coordinates into readable text of the location
self.locationLabel.text = [NSString stringWithFormat:#"%# %# , %#, %#, %# %#", placemark.subThoroughfare, placemark.thoroughfare, placemark.locality, placemark.administrativeArea, placemark.country, placemark.postalCode];
}
else{
NSLog(#"%#", error.debugDescription);
self.locationLabel.text = #"Failed to find location";
}
}];
}
}
I must warn you that iOS8 will throw NO error when it can't find a location because it needs you as the programmer to add an alert view to authorise getting locations. See this tutorial on how to overcome this issue:
http://nevan.net/2014/09/core-location-manager-changes-in-ios-8/
I got this tutorial from the most popular asked question about the new error.
The most accurate way to do this would be through the exif metadata. Have a look at this post by Ole Begemann on how to do this.
UPDATE
It seems like Apple doesn't include the location to the metadata to images taken with the Camera from the UIImagePicker.
Another option to get the location in which the image was taken would be to use a custom overlay view for the UIImagePicker and get the location when the takePicture method is called. This would probably achieve the best result.

reverseGeocodeLocation Only Executes Completion Block Once

I have an array of coordinates that I step through with a for loop. I would like to place annotations on a map for each location and have the subtitle for the callout be the address of the coordinate, found by using reverseGeocodeLocation In the for loop, I call the reverseGeocodeLocation method, and inside the completion block I create the annotation and display it on the map. However, when I run the app, only one annotation shows up. I went in the debugger, and the completion block is only getting called once (for two calls to the reverseGeocodeLocation method). Any suggestions to fix this?
My for loop:
for(int i = 0; i < [locations count]; i++)
{
CLLocation *location = [locations objectAtIndex:i];
__block NSString *info;
NSLog(#"Resolving the Address");
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error)
{
NSLog(#"Found placemarks: %#, error: %#", placemarks, error);
if (error == nil && [placemarks count] > 0)
{
placemark = [placemarks lastObject];
info = [NSString stringWithFormat:#"%# %# %# %#, %#",
placemark.subThoroughfare, placemark.thoroughfare,
placemark.postalCode, placemark.locality,
placemark.administrativeArea];
[self remainderOfMethod:location withAddress:info atIndex:i];
}
else
{
NSLog(#"%#", error.debugDescription);
}
} ];
}
And the method called at the completion block:
- (void) remainderOfMethod: (CLLocation *)location withAddress:(NSString *)info atIndex: (int)i
{
MKPointAnnotation* annotation = [[MKPointAnnotation alloc] init];
if (location != nil)
{
[annotation setSubtitle:[NSString stringWithFormat:#"%#", info]];
annotation.coordinate = location.coordinate;
[self.mapView addAnnotation:annotation];
}
}
Thanks!
From the official Apple documentation:
After initiating a reverse-geocoding request, do not attempt to
initiate another reverse- or forward-geocoding request
You can find the docs here: https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLGeocoder_class/Reference/Reference.html#//apple_ref/occ/instm/CLGeocoder/reverseGeocodeLocation:completionHandler:
One way to solve it is to do only one request at a time in a recursive method that pops a location from a stack (or array) on each iteration.
Even in that case, consider what Apple has to say about it:
Geocoding requests are rate-limited for each app, so making too many
requests in a short period of time may cause some of the requests to
fail
So you may want to request geocoding on demand, for example when a user taps on an annotation.

Reverse geocoding from given coordinates (not current location, but manually provided)

I am fetching GPS information of all my images and they are stored in a Dictionary. I would pass on the lat & long values from this dictionary to the reverseGeocodeLocation function call & store the results in my database.
The problem here is that this function is an asynchronous call & I need to synchronize the whole process for my record to get inserted into the table.
Eg: My array read following coordinates: 32.77003,96.87532. It now calls the reverseGeocodeLocation function, passing on these coordinates as CLLocation object. Now before this async function returns me back the geo-coded location name, next request with new set of coordinates is sent to reverseGeocodeLocation function. This causes inconsistency to insert the record into database.
Is there any way to have this whole task turn synchronous?
i.e Make my for-loop wait until reverseGeocodeLocation returns a value and then move on to next record to be geo-coded?
A bit of my code is here:
for (int imgIdx=0; imgIdx<[imageMetaMutArray count]; imgIdx++)
{
NSDictionary *localGpsDict = [[NSDictionary alloc]initWithDictionary: [imageMetaMutArray objectAtIndex:imgIdx]];
imgLatLoc=[localGpsDict valueForKey:#"Latitude"];
imgLongLoc=[localGpsDict valueForKey:#"Longitude"];
dateStamp=[localGpsDict valueForKey:#"DateStamp"];
timeStamp=[localGpsDict valueForKey:#"TimeStamp"];
if(imgLatLoc && imgLongLoc && dateStamp && timeStamp)
{
CLGeocoder *geoCoder=[[CLGeocoder alloc]init];
CLLocation *currentLocation=[[CLLocation alloc]initWithLatitude:[imgLatLoc doubleValue] longitude:[imgLongLoc doubleValue]];
[geoCoder reverseGeocodeLocation:currentLocation completionHandler:^(NSArray *placeMarks, NSError *err){
if(err)
{
NSLog(#"Reverse geo-coding failed because: %#",err);
return;
}
if(placeMarks && placeMarks.count>0)
{
CLPlacemark *placeMarkers=placeMarks[0];
NSDictionary *locationDictionary=placeMarkers.addressDictionary;
NSString *country=[locationDictionary objectForKey:(NSString *)kABPersonAddressCountryKey];
NSString *city=[locationDictionary objectForKey:(NSString *)kABPersonAddressCityKey];
NSString *state=[locationDictionary objectForKey:(NSString *)kABPersonAddressStateKey];
NSLog(#"logged in from:");
if(city)
{
NSLog(#"city: %#",city);
locName = [[NSString alloc]initWithString:city];
if(state)
{
NSLog(#"state: %#",state);
locName=[locName stringByAppendingString:#","];
locName=[locName stringByAppendingString:state];
}
if(country)
{
NSLog(#"country: %#",country);
locName=[locName stringByAppendingString:#","];
locName=[locName stringByAppendingString:country];
}
}
else
{
if(state)
{
NSLog(#"state: %#",state);
locName = [[NSString alloc]initWithString:state];
if(country)
{
NSLog(#"country: %#",country);
locName=[locName stringByAppendingString:#","];
locName=[locName stringByAppendingString:country];
}
}
else
{
NSLog(#"country: %#",country);
locName = [[NSString alloc]initWithString:country];
}
}
}
else
{
NSLog(#"Placemark Error code:: %lu\n%#",(unsigned long)placeMarks.count,placeMarks);
}
[locName retain];
NSLog(#"location decoded is: %#",locName);
/*Call for Insert into Images table*/
[self insertDataImgTbl:locName];
});
}
}
}
The short answer is that you can't make it synchronous.
What you want to do is move the code that goes on to the next object into the completion block of the reverseGeocodeLocation because that is the soonest that you can submit another reverseGeocodeLocation request. Let me see if I can make some pseudocode here... (that is, I haven't compiled this so it might not be exactly right...)
// in place of the original for loop:
[self reverseGeocodeForIndex:0];
// Doing the reverse geocode is in a method so you can easily call it from within the completion block.
// Maybe your parameter is not the imgIdx value but is instead some object -- I'm just hacking your posted code
// The point is that your completion block has to be able to tell when
// it is done and how to go on to the next object when it is not done.
(void) reverseGeocodeForIndex:(int) imgIdx {
NSDictionary *localGpsDict = [[NSDictionary alloc]initWithDictionary: [imageMetaMutArray objectAtIndex:imgIdx]];
imgLatLoc=[localGpsDict valueForKey:#"Latitude"];
imgLongLoc=[localGpsDict valueForKey:#"Longitude"];
dateStamp=[localGpsDict valueForKey:#"DateStamp"];
timeStamp=[localGpsDict valueForKey:#"TimeStamp"];
if(imgLatLoc && imgLongLoc && dateStamp && timeStamp)
{
CLGeocoder *geoCoder=[[CLGeocoder alloc]init];
CLLocation *currentLocation=[[CLLocation alloc]initWithLatitude:[imgLatLoc doubleValue] longitude:[imgLongLoc doubleValue]];
[geoCoder reverseGeocodeLocation:currentLocation completionHandler:^(NSArray *placeMarks, NSError *err){
// completion block
if(err)
{
// error stuff
}
if(placeMarks && placeMarks.count>0)
{
// what happens when you get some data
}
// now see if we are done and if not do the next object
if (imgIdx<[imageMetaMutArray count])
{
[self reverseGeocodeForIndex:imgIdx+1];
} else {
// Everything is done maybe you want to start something else
}
}];
} else {
// Since you might not reverseGeocode an object you need an else case
// here to move on to the next object.
// Maybe you could be more clever and not duplicate this code.
if (imgIdx<[imageMetaMutArray count])
{
[self reverseGeocodeForIndex:imgIdx+1];
} else {
// Everything is done maybe you want to start something else
}
}
}
And, of course, you can't depend on this being done to do anything else except that you might kick something off when you have reverseGeocoded the last object.
This asynchronous programming can drive you nuts.
An alternative approach could be to place a synchronous request to the following URL, which returns reverse geo-coded results in XML format. You can later parse it, convert to JSON or whatever. The best part: 1) You're force synchronizing the whole process of reverse-geo coding
2) There's no restriction in terms of max requests you can make(I think its 50/min in case of calls to reverseGeocodeLocation handler). If exceeded, you get kCLErrorDomain code 2 error. So we avoid all that by the following approach below. Some sample code that works for me:
-(NSString *)giveMeLocName: (double)gpsLat :(double)gpsLong
{
NSString *finalLocation=[[NSString alloc]init];
//Below is URL we need to send request
NSString *reverseGeoCodeLoc = [NSString
stringWithFormat:#"http://nominatim.openstreetmap.org/reverse?format=xml&zoom=18&addressdetails=1&accept-language=en&lat=%lg&lon=%lg",gpsLat,gpsLong];
NSURL *myLocUrl = [NSURL URLWithString:reverseGeoCodeLoc];
ASIHTTPRequest *myLocRequest = [ASIHTTPRequest requestWithURL:myLocUrl];
[myLocRequest setDidFinishSelector:#selector(reverseGeoCodeImg:)];
[myLocRequest setDelegate:self];
[myLocRequest startSynchronous];
NSLog(#"waiting for location info..");
//Do stuff after receiving results here
}
//This block receives HTTP response as XML(containing reverse geo-coded info. I parse this to JSON using XMLReader class(downloadable)
-(void)reverseGeoCodeImg:(ASIHTTPRequest *)request
{
/*Allocations*/
locDict=[[NSDictionary alloc]init];
revGeoCodePart=[[NSDictionary alloc]init];
addressParts=[[NSDictionary alloc]init];
cityName=[[NSString alloc]init];
stateName=[[NSString alloc]init];
countryName=[[NSString alloc]init];
NSLog(#"starting reverse geo-code!!");
NSString *responseString = [request responseString];
NSError *parseError = nil;
locDict=[XMLReader dictionaryForXMLString:responseString error:&parseError];
[locDict retain];
revGeoCodePart=[locDict objectForKey:#"reversegeocode"];
[revGeoCodePart retain];
addressParts=[revGeoCodePart objectForKey:#"addressparts"];
[addressParts retain];
cityName=[[addressParts objectForKey:#"city"] objectForKey:#"text"];
[cityName retain];
stateName=[[addressParts objectForKey:#"state"]objectForKey:#"text"];
[stateName retain];
countryName=[[addressParts objectForKey:#"country"]objectForKey:#"text"];
[countryName retain];
NSLog(#"city: %#\nstate: %#\ncountry: %#",cityName,stateName,countryName);
}

Refreshing a MKMapView when the user location is updated or using default values

I'm trying to refresh my map view and load new data from the server when the device acquires the user location.
Here's what I'm doing:
- (void)viewDidLoad {
CGRect bounds = self.view.bounds;
mapView = [[MKMapView alloc] initWithFrame:bounds];
mapView.showsUserLocation=YES;
mapView.delegate=self;
[self.view insertSubview:mapView atIndex:0];
[self refreshMap];
}
- (void)mapView:(MKMapView *)theMapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
//this is to avoid frequent updates, I just need one position and don't need to continuously track the user's location
if (userLocation.location.horizontalAccuracy > self.myUserLocation.location.horizontalAccuracy) {
self.myUserLocation = userLocation;
CLLocationCoordinate2D centerCoord = { userLocation.location.coordinate.latitude, userLocation.location.coordinate.longitude };
[theMapView setCenterCoordinate:centerCoord zoomLevel:10 animated:YES];
[self refreshMap];
}
}
- (void)refreshMap {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
[self.mapView removeAnnotations:self.mapView.annotations];
//default position
NSString *lat = #"45.464161";
NSString *lon = #"9.190336";
if (self.myUserLocation != nil) {
lat = [[NSNumber numberWithDouble:myUserLocation.coordinate.latitude] stringValue];
lon = [[NSNumber numberWithDouble:myUserLocation.coordinate.longitude] stringValue];
}
NSString *url = [[NSString alloc] initWithFormat:#"http://myserver/geo_search.json?lat=%#&lon=%#", lat, lon];
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:url]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
[url release];
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}
I also have a - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data, to create and add the annotations.
Here's what happens:
sometimes I get the location after a while, so I already loaded data close to the default location.
Then I get the location and it should remove the old annotations and add the new ones, instead I keep seeing the old annotations and the new ones are added anyway (let's say I receive 10 annotations each time, so it means that I have 20 on my map when I should have 10).
What am I doing wrong?
Ok this might be a little long but it worked for me and I really do hope it will help you.
This is from an app that is used to share different traffic status reports between drivers.
I had this problem also, I tried to load annotations from a server, then delete them and reload the annotations array to the map every time the user sends an annotation to the server himself/presses the "refresh" button/every 1.5 minutes - so that he will always have the current set.
So I thought it might have something to do with the time interval that it takes to load the new annotations from the server, or with the array itself, later I realized it might also be connected to the way the entire code was organized and that maybe some things just got in the way/timing of others.
What I did is basically moving the [self.map addAnnotations:AN ARRAY OF ANNOTATIONS] command to the main thread and the whole loading process to the back, I also used a temp array instead of "self.map.annotations" to delete the current annotations, it worked ok afterwards so I just left it like that :), though I'm not a world-class expert (not even close) and I'm sure others might have a more professional and efficient solution, code sample:
//this is in viewDidLoad, it kicks off the whole process
[self performSelectorInBackground:#selector(retrieveAnnotationsDataFromServer) withObject:self];
//this is the method for loading data
-(void)retrieveAnnotationsDataFromServer
{
NSAutoreleasePool *pool=[[NSAutoreleasePool alloc]init];
//code for loading..
NSURL *URL=[NSURL URLWithString:#"http://YOUR SERVER NAME HERE"];
NSString *result=[NSString stringWithContentsOfURL:URL encoding:4 error:nil];
NSLog(#"%#",result);
NSData *data = [NSData dataWithContentsOfURL:URL];
NSXMLParser *parser = [[NSXMLParser alloc]initWithData:data];
[parser setDelegate:self];
[parser parse];
//Inside the parser delegate i take each element of the report and store it in a mutable array so that i could create the annotations later (report title, report description/subtitle, report location longitude and report location latitude)
[pool drain];
NSLog(#"retrieveannotatinsdatafromserver pool drained");
}
//Now, in this case the last thing that will happen before the pool is drained is that the parser`s "didEndDocument" method gonna be called, so this is where I delayed the loading like this:
- (void)parserDidEndDocument:(NSXMLParser *)parser
{
NSLog(#"parser - document ended, creating annotationsArray");
NSLog(#"this is the size of RA Array:%d",[self.recievedTitles count]);
for (int i=0;i<[self.recievedTitles count];i++)
{
//RC=recieved coordinate
//RA=recieved annotation
CLLocationCoordinate2D RC;
RC.latitude=[[self.recievedLatitude objectAtIndex:i] doubleValue];
RC.longitude=[[self.recievedLongitude objectAtIndex:i] doubleValue];
if([self.recievedTitles objectAtIndex:i]==nil)
continue;
Annotation *RA=[[Annotation alloc]initWithCoordinate:RC andTitle:[self.recievedTitles objectAtIndex:i] andSubTitle:[self.recievedSubTitles objectAtIndex:i]];
//the "self.AnnotationsArray" is an array i use to always hold the most current set retrieved from the server
[self.AnnotationsArray addObject:RA];
[RA autorelease];
NSLog(#"RA %d inserted",i);
}
NSLog(#"Data retrieving ended, loading annotations to map");
[self performSelectorOnMainThread:#selector(addAnnotations) withObject:self waitUntilDone:NO];
}
//this method was only defined so that i could apply it in the main thread instead of in the background like the whole loading process.. I'm sure it can be done better.
-(void)addAnnotations
{
[self.Map addAnnotations:self.AnnotationsArray];
NSLog(#"annotations loaded to Map");
}
//now we have retrieved them from the server for the first time, the reloading and refreshing part comes next, this method is called any time the user press refresh, sends a report to the sever, or automatically every 1.5 minutes:
-(void)refreshMap
{
NSArray *annotationsToDelete=[[NSArray alloc] initWithArray:self.AnnotationsArray];
[self.Map removeAnnotations:annotationsToDelete];
[annotationsToDelete release];
[self performSelectorInBackground:#selector(retrieveAnnotationsDataFromServer) withObject:self];
}
Please do comment on my answer since I will also be very happy to learn a less complicated way of solving this matter.
I think most of these type of problems would have been solved if there was a way to "wait until done" when using background threads, because now what happens is that the code continues to run forward before the whole process of downloading-parsing-creating annotation-loading to map process is actually finished.

Resources