Overview:
I have a database of KML files that represent bus routes. I plan to plot a line (sometimes multiple lines if the route is complex) onto a MKMapView in iOS using Objective-C. However, I've been having a terribly difficult time with this, and need your help. You can view a few of the sample KML files I am using here:
Example A: A Full KML file that would be used in production
Example B: A Single Path from the above full file
Example C: A reformatted KML file as JSON
I have two methods of going at this. I have the JSON way and the XML way. Both don't work, with different results problems when using the different example KML files.
In both the JSON and the KML method, I use this code to return the overlay for the mapview
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>)overlay{
// Checking if the called overlay is a polyline
if([overlay class] == MKPolyline.class){
// Make the polyline
MKPolyline* polyline = (MKPolyline *)overlay;
#try {
// Extract the title and make sure it isnt nil
NSString *title = polyline.title;
if(title){
// Grab the PolyLine from the container object using the title
MKPolylineRenderer * rtn = [self.polylineContainer objectForKey:title];
if(rtn){
return rtn;
} else {
return nil;
}
}
}
#catch (NSException *exception) {
NSLog(#"%#",exception);
}
} else {
return nil;
}
}
The JSON Way
Personally I'd prefer to use the JSON way because Objective C does a better job with JSON serialization (in my poinion at least) than XML. I have a PHP script set up to just extract the <MultiGeometry> nodes as well as the <LineString> nodes. There doesn't appear to be an issue with this script, so I'll omit it from this question, but if you would like please ask and I'll add it.
The KML way would use example C above and always fails at the [self.mapView addOverlay:polyline]; line with an Unrecognized Selector sent to Instance. It also triggers an EXEC_BAD_ACCESS exception, but I can't trace to where it occurs (even with an exception breakpoint)
// A Synchrnous URLRequest is performed, and the JSON is serialised into response_root
// Metadata. Used for iteration later.
NSDictionary * meta = [response_root valueForKey:#"meta"];
NSInteger mg_count = [[meta valueForKey:#"MultiGeometryCount"] integerValue];
NSInteger ls_count = [[meta valueForKey:#"LineStringCount"] integerValue];
// The Data dictionary holds the data. Obviously
NSDictionary * data = [response_root valueForKey:#"data"];
// mgi is just short for MultiGeometry. It contains LineStrings (lsi)
int mgi = 0;
// Loop through the MultiGeometry nodes
while (mgi < mg_count) {
// Grab the Root Node
NSDictionary * root_node = [data valueForKey:[NSString stringWithFormat:#"root_%d",mgi]];
// lsi is just short for LineString. It contains the coordinates in a JSON object
int lsi = 0;
while (lsi < ls_count) {
// Grab the sub node containing all of the coordinate pairs
NSDictionary * sub_node = [root_node valueForKey:[NSString stringWithFormat:#"node_%d",lsi]];
NSInteger pair_count = [[sub_node valueForKey:#"CoordPairCount"] integerValue];
int pc = 0;
// Set up the C Array for the Coordinates
CLLocationCoordinate2D coordinates[pair_count];
// Loop through the pairs
while (pc < pair_count) {
// Grab the Pair Node
NSDictionary * pair_node = [sub_node valueForKey:[NSString stringWithFormat:#"set_%d",pc]];
// Set X and Y and add them to the coordinate array
double longtitude = [[pair_node valueForKey:#"x"] doubleValue];
double latitude = [[pair_node valueForKey:#"y"] doubleValue];
coordinates[pc].latitude = latitude;
coordinates[pc].longitude = longtitude;
pc ++;
}
// When we've finished with all of the pairs, we create the polyline
MKPolyline * polyline = [[MKPolyline alloc] init];
polyline = [MKPolyline polylineWithCoordinates:coordinates count:pair_count];
if(polyline){
#try {
// This always triggers a "Unrecognised selector sent to instance" exception. Although polyline is correctly set
[self.mapView addOverlay:polyline];
}
#catch (NSException *exception) {
NSLog(#"%#",exception);
}
// Create the rendered line and set its properties
MKPolylineRenderer * line = [[MKPolylineRenderer alloc] initWithPolyline:polyline];
line.strokeColor = [UIColor blueColor];
line.lineWidth = 2;
line.polyline.title = [NSString stringWithFormat:#"ls_%d", lsi];
// Add it to the polyline container, which is just a NSMutableDictionary
[self.polylineContainer setObject:line forKey:[NSString stringWithFormat:#"ls_%d", lsi]];
}
lsi ++;
}
mgi ++;
}
The KML Way
The KML way would use example A and B above and also always fails at the [self.mapView addOverlay:polyline]; line
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
if ([elementName isEqualToString:#"coordinates"]) {
// The coordinate pairs are provided as one big string, I remove the garbage charters from it
coords = [coords stringByReplacingOccurrencesOfString:#"0 -" withString:#"-"];
coords = [coords stringByReplacingOccurrencesOfString:#"\n" withString:#""];
coords = [coords stringByReplacingOccurrencesOfString:#" " withString:#""];
coords = [coords stringByReplacingOccurrencesOfString:#"(null)" withString:#""];
// Split the string into a big array
NSArray * t = [[NSArray alloc] initWithArray:[coords componentsSeparatedByString:#","]];
// Create the C Array for the coordinates
CLLocationCoordinate2D coordinates[t.count];
// Because the X coordinate comes first, I use this toggle to go back between setting the X then the Y and loop
int isXorY = 0;
int i = 0;
double x = 0;
double y = 0;
for (NSString * c in t) {
// X always comes first
if(isXorY == 0){
isXorY = 1;
x = [c doubleValue];
// Then comes the Y
} else if(isXorY == 1){
isXorY = 0;
y = [c doubleValue];
}
// If both the X and the Y coordinate are set, add the pair to the Coordinates array and start over again
if(x != 0 && y != 0){
coordinates[i].latitude = y;
coordinates[i].longitude = x;
x = 0;
y = 0;
i ++;
}
}
// Create the Polyline using the coordinates
MKPolyline *polyline = [MKPolyline polylineWithCoordinates:coordinates count:i];
// This always triggers a "Unrecognised selector sent to instance" exception. Although polyline is correctly set
[self.mapView addOverlay:polyline];
// Create the polyline and set its properties
MKPolylineRenderer * line = [[MKPolylineRenderer alloc] initWithPolyline:polyline];
line.strokeColor = [UIColor blueColor];
line.lineWidth = 2;
line.polyline.title = [NSString stringWithFormat:#"ls_%d", totalCordPairs];
// Add it to the container object with its name. Polylinecontainer is just a NSMutableDictionary
[self.polylineContainer setObject:line forKey:[NSString stringWithFormat:#"ls_%d", totalCordPairs]];
// This is global integer that is used with the above name
totalCordPairs ++;
}
}
The results
When this actually does work (really rare) I get completely messed up results. It's easiest just to show you with pictures:
As you can see, the polylines loop back, and sometimes they just go across the map all the way to this village in France! That's where that stray red line is going in the first picture.
There are at least two separate problems causing the behavior you're seeing:
The rendererForOverlay delegate method is getting called before the polylineContainer has been updated with the required MKPolylineRenderer and the delegate method ends up returning nothing (not even nil).
You can't really assume when the map view will call its delegate methods but the rendererForOverlay is called when an overlay is in the visible region. This would also explain why it works "sometimes" which would be when you add the overlay when it's not in the visible region and the delegate method gets called after you've created and added the renderer.
In this case, the polyline's title will still be nil (because you're setting the polyline's title after calling addOverlay).
Since the current code in rendererForOverlay doesn't handle the case where title is nil and the method returns nothing in this scenario.
The method returning nothing (not even nil) is what causes the exception when addOverlay is called. Basically the map view ends up accessing garbage values for the renderer which in your case cause a "unrecognized selector" exception.
It's good practice to always return at least nil at the very end to handle unforeseen cases like this.
The real fix, however, is to move the creation of the renderer to the rendererForOverlay delegate method. You can still keep your polylineContainer approach but if the object is not found in there, then create and add the renderer right then and there in the delegate method.
Here's an example of the fix...
In the section where you create the MKPolyline:
//MKPolyline * polyline = [[MKPolyline alloc] init];
//The above alloc+init is unnecessary since the polylineWithCoordinates
//method effectively does that for you.
MKPolyline * polyline = [MKPolyline polylineWithCoordinates:coordinates
count:pair_count];
//set the polyline's title BEFORE adding it to the map view...
polyline.title = [NSString stringWithFormat:#"ls_%d", lsi];
//Call addOverlay but then do NOT create the renderer
//and add to polylineContainer HERE. Comment that code out.
Then in rendererForOverlay:
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>)overlay{
// Checking if the called overlay is a polyline
if([overlay class] == MKPolyline.class){
// Make the polyline
MKPolyline* polyline = (MKPolyline *)overlay;
#try {
// Extract the title and make sure it isnt nil
NSString *title = polyline.title;
if(title){
// Grab the PolyLine from the container object using the title
MKPolylineRenderer * rtn = [self.polylineContainer objectForKey:title];
//HERE, if we did not get already-created renderer,
//create it now and add to polylineContainer...
if (rtn == nil)
{
// Create the rendered line and set its properties
rtn = [[MKPolylineRenderer alloc] initWithPolyline:polyline];
rtn.strokeColor = [UIColor blueColor];
rtn.lineWidth = 2;
// Add it to the polyline container, which is just a NSMutableDictionary
[self.polylineContainer setObject:rtn forKey:title];
}
if(rtn){
return rtn;
} else {
return nil;
}
}
}
#catch (NSException *exception) {
NSLog(#"%#",exception);
}
} else {
return nil;
}
//Always return a default from a method
//that is supposed to return something...
return nil;
}
By the way, it's not clear why you are storing the renderers in polylineContainer. If you're thinking that creating the renderers is expensive and you want to optimize performance, ok but that might be premature at this point (unless you've already found this to be required in your case).
The cause of the lines "looping back" or appearing in unexpected locations like a village in France might be due to just bad data. Look at or log the coordinates you are adding to the polyline and confirm that they are correct. For example, in the JSON file you linked to in the question, there are lots of these:
"set_19":{"y":"0"}
This would end up adding a coordinate at 0,0 which is in the Atlantic Ocean off the west coast of Africa. It seems that either the data or how the code is interpreting the data is wrong.
Related
I'm adding an annotation at the user position in my viewDidLoad and try store that annotation using state restoration.
I want the annotation to stay there even if the app gets terminated in the background.
-(void)viewDidLoad
{
...
self.annotationregion = MKCoordinateRegionMake(CLLocationCoordinate2DMake(self.mapview.userLocation.location.coordinate.latitude, self.mapview.userLocation.location.coordinate.longitude), MKCoordinateSpanMake(0.5, 0.5));
self.parkinglocation = [[SWAnnotation alloc] init];
self.parkinglocation.title = #"Here it is!";
self.parkinglocation.coordinate = self.annotationregion.center;
[self.mapview addAnnotation:self.parkinglocation];
NSLog(#"userlocation: %#", self.mapview.userLocation);
NSNumber *longitudeNumber = [NSNumber numberWithDouble:self.annotationregion.center.longitude];
NSNumber *latitudeNumber = [NSNumber numberWithDouble:self.annotationregion.center.latitude];
self.coordinateArray = #[latitudeNumber,longitudeNumber];
}
-(void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
...
[coder encodeObject:self.coordinateArray forKey:#"CoordinateArray"];
}
-(void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
...
NSArray *coordinateArray = [coder decodeObjectForKey:#"CoordinateArray"];
self.annotationregion = MKCoordinateRegionMake(CLLocationCoordinate2DMake([coordinateArray[0] doubleValue], [coordinateArray[1] doubleValue]), MKCoordinateSpanMake(1.5,1.5));
self.parkinglocation = [[SWAnnotation alloc] init];
self.parkinglocation.title = #"tata!";
self.parkinglocation.coordinate = self.annotationregion.center;
[self.mapview addAnnotation:self.parkinglocation];
}
Note, in the initial viewDidLoad annotation I use the title "Here it is!" and in the decodeRestorableStateWithCoder annotation I use the title "tata!" just to see which one it's showing me at the moment.
Now, this seems to work when the app gets terminated for the first time in the background. The app starts back up and has an annotation with the new title "tata!" at the right position.
The second time it gets terminated in the background and then gets started back up it shows me an annotation with the initial title of "Here it is!" off the coast of Africa, which of course is not my position...
Sooo I'm getting frustrated, what am I missing?
It seems to me that when you encode the state you actually encode self.coordinateArray, but when you restore you use a local variable NSArray *coordinateArray to restore.
So when your app will encode again later, your self.coordinateArray would be nil.
I think you should set [self setCoordinateArray: coordinateArray]; just after NSArray *coordinateArray = [coder decodeObjectForKey:#"CoordinateArray"];
I am working on CFTree. I need to get all siblings on same depth for example in following figure I need an array of all 8 CFTreeRef on 3rd depth. How can I get that?
Following your depth numbering - i.e. starting with root at 1, not 0:
-(void)allNodesFromTree:(CFTreeRef)node currentDepth:(int)currDepth atDepth:(int)depth nodesFound:(NSMutableArray*)nodes
{
bool atTargetDepth = depth -1 == currDepth;
CFTreeRef curChild = CFTreeGetFirstChild(node);
for (; curChild; curChild = CFTreeGetNextSibling(curChild)) {
if(atTargetDepth) //stop recursion if target depth reached
{
[nodes addObject:(__bridge id)(curChild)];
}
else [self allNodesFromTree:curChild currentDepth:currDepth+1 atDepth:depth nodesFound:nodes];
}
}
To make things a bit cleaner, you might wrap it in a helper function:
-(NSMutableArray*)allNodesFromTree:(CFTreeRef)node atDepth:(int)depth
{
NSMutableArray *nodesArray = [[NSMutableArray alloc]init];
[self allNodesFromTree:node currentDepth:0 atDepth:depth nodesFound:nodesArray];
return nodesArray;
}
Then you can just call
NSMutableArray *nodes = [self allNodesFromTree:root atDepth:3];
I have a array of custom UIView objects with 2 or more objects having same center and I have to construct another array from it with distinct centers. What is the best way to do it?
I tried with below piece of code but it does not work.
self.distinctObjects = [NSMutableArray arrayWithCapacity:iAllObjects.count];
for (MyCustomView *customView in iAllObjects)
{
BOOL hasDuplicate = [[self.distinctObjects filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"SELF.center == %#", customView.center]] count] > 0;
if (!hasDuplicate) {
[self.distinctObjects addObject:customView];
}
}
You can't use struct's (in your case center is a CGPoint) in NSPredicate. Also comparing floating-point values directly isn't a good idea.
You should follow this answer. Just replace [annotation coordinate].latitude with myView.center.x, [annotation coordinate].longitude with myView.center.y, and so on. It should be easy.
BTW your code has O(n^2) complexity, but maybe that's not a problem if the array is small.
I fixed this with below piece of code:
NSMutableArray *uniqueCenters = [NSMutableArray arrayWithCapacity:iAllObjects.count];
self.distinctObjects = [NSMutableArray arrayWithCapacity:iAllObjects.count];
for (MyCustomView *customView in iAllObjects)
{
if (![uniqueCenters containsObject:[NSValue valueWithCGPoint:customView.center]]) {
[uniqueCenters addObject:[NSValue valueWithCGPoint:customView.center]];
[self.beacons addObject:customView];
}
}
I have a property of my class called position, which I'm trying to access with self.position.
I get:
(gdb) po self.position
There is no member named position.
But I set my breakpoint at the for loop in the following block. It definitely has a value:
- (IBAction)backFiftyWords:(UIButton *)sender {
self.position = #([self.position intValue] - 50);
NSString *wordsToBeShown = #"";
for (int i = 0; i < [self.numberOfWordsShown intValue]; i++) {
It's an NSNumber.
You should use
po [self position]
Dot syntax doesn't work well with GDB. Upgrading to LLDB/LLVM is a good idea as well.
You can also:
NSLog(#"%#", self.position);
In your code. Or create a breakpoint and edit it, add this is an expression:
expr (void)NSLog(#"%#", self.position)
See this link for more info: http://www.raywenderlich.com/28289/debugging-ios-apps-in-xcode-4-5
I'm building a simple 2D game in Cocos2d which involves having enemies cross the screen across different, predefined paths. The nextFrame method has the following code:
int indexCount = 0;
for (Enemy *e in enemies) {
if ([cocosGuy doesCollideWithRect: [e boundingBox]])
{
for (Enemy *e in enemies)
{
[self removeChild: e cleanup: YES];
}
[self startGame];
}
// ^ This code not relevant to the question
if ([e numberOfRunningActions] == 0)
{
[e setPosition: [[enemy_positions objectAtIndex:indexCount] CGPointValue]];
[e runAction: [beziers objectAtIndex: indexCount]];
}
++indexCount;
}
The code in the second if statement above is intended to take a CGPoint from the 'enemy_positions' array and a CCActionInterval from the 'beziers' array. It works - when an enemy completes its path, it is repositioned and the action reruns. But why doesn't this break after the action runs for the first time? Aren't CCActions supposed to be one time only?
I ask because I want to refactor the position and action into a single struct, and I want to make sure I know what's going on first. Am I misunderstanding the CCAction class?
Also, here is the current factory method for generating the 'beziers' array:
-(NSArray*) makeBeziers {
ccBezierConfig bezierconf1;
bezierconf1.controlPoint_1 = ccp(-200, 5);
bezierconf1.controlPoint_2 = ccp(300, 100);
bezierconf1.endPosition = ccp(1000,5);
ccBezierConfig bezierconf2;
bezierconf2.controlPoint_1 = ccp(-200, 5);
bezierconf2.controlPoint_2 = ccp(300, 100);
bezierconf2.endPosition = ccp(1000,5);
ccBezierConfig bezierconf3;
bezierconf3.controlPoint_1 = ccp(-200, 5);
bezierconf3.controlPoint_2 = ccp(300, 100);
bezierconf3.endPosition = ccp(1000,5);
NSArray *myarray;
myarray = [[NSArray arrayWithObjects: [CCBezierBy actionWithDuration:3 bezier: bezierconf1],
[CCBezierBy actionWithDuration:3 bezier: bezierconf2],
[CCBezierBy actionWithDuration:3 bezier: bezierconf3],
nil] retain];
return myarray;
}
This works "by accident". Actions are supposed to be one time only. After they've run, they will be released.
However since you store the actions in a separate array, those actions are retained. Therefore you can re-run them. This might work for some actions, other actions may show subtle issues, and some actions may not do anything, leak memory or crash immediately if you do so.
Re-using actions is generally considered bad practice, unless you know the code of each action and you have verified that reusing it doesn't do anything "bad".