Search This Blog

Wednesday, April 6, 2011

Tutorial: RouteWeather Using Google weather and Driving Directions APIsin your iOS apps

First off, let me say this: Google's terms of service do not allow you to use their APIs for any commercial apps. So it is likely that if you use these APIs, they may never see the light of day in the app store, unless you negotiate a commercial deal with Google.

Here is what we will be developing: Let's assume you are planning a trip from Bethesda, MD to 90210, CA and you plan to drive (Yeah a road warrior!).  You can get the driving directions from many places, including Google. What you also want is, what "what is the weather along the route". Not only that, you want it for the day you plan to drive (so maybe tomorrow, the day after, or the day after the day after...)

I often need this information. I don't drive cross country, but I do drive and knowing the weather along the route and for the day I want to drive is very useful to me. I wish we had an app for that. Well, let's not wish, let's write one.

In the process, you will learn how to:

a) Use TBXML for XML parsing

b) Use Google Weather and Driving directions XML data

c) Use segmented controls

d) Use TableViews and custom cells

e) Write an app that brings all of this together

*** Credits: The fine folks at icodeblog started me off with this article. ***

First the customary screen shots:

First Screen: You enter your from and to and select which day you want the forecast for:





Second screen: We use a HUD to tell the user we are doing all the backend magic to get the route weather



Third screen: We display the results:



Now lets break up the design and code:

The following files will be explained:

  • DrivingWeatherViewController.h/m: This is the main program. It is responsible for bringing up the first screen, accept user inputs and then invoke Google's secret APIs to get driving directions and weather along the route. It subsequently invokes code in my next bullet point that displays all the retrieved data in a nice, friendly table.

  • WeatherTable.h/m: This is responsible for displaying the parsed data in a TableView (The tableview controller is implemented in DrivingWeatherViewController)

  • CustomCell.h/m: This is used by WeatherTable to display a custom table cell for each row (if you see the third screen image, you will notice we have 3 sub-rows of data in each TableView Cell along with an image on the right.

  • WeatherDataClass.h/m: Just a data structure (wait, they are called Classes) that holds weather information for each location along the driving path. Used by DrivingWeatherViewController

I will not be explaining how to use the HUD. For that take a look at my other tutorial here.

Now let's break up the code. Let's target the main code first:

DrivingWeatherViewController.h


//
// DrivingWeatherViewController.h
// DrivingWeather
//
// Created by Arjun on 10/29/10.
// Copyright 2010 Hughes Systique Corp. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "MBProgressHUD.h"
#import <MapKit/MapKit.h>


We are going to use a specific data-structure that is provided by MapKit.h called CLLocationCordinate2D. It is just a convenient structure to store a lat/long value.


@interface DrivingWeatherViewController : UIViewController <MBProgressHUDDelegate>
{

IBOutlet UITextField *textFrom;
IBOutlet UITextField *textTo;
MBProgressHUD *HUD;
NSString *escapedFrom;
NSString *escapedTo;
IBOutlet UIButton *goButton;
IBOutlet UIButton *tableButton;
IBOutlet UISegmentedControl *daySegment;
int day;

}

@property (nonatomic, retain) UITextField *textFrom;
@property (nonatomic, retain) UITextField *textTo;
@property (nonatomic, retain) UIButton *goButton;
@property (nonatomic, retain) UIButton *tableButton;
@property (nonatomic, retain) UISegmentedControl *daySegment;



First, since we plan to use MBProgressHUD, we need to implement its Delegate methods. As I indicated earlier, see my tutorial on "WhoCalledMe" for more details on MBProgressHUD. Looking at the @interface part, textFrom and textTo are input textfields where the use will enter his source and destination locations. You can be pretty flexible, including adding approximations here. I noticed that the google API is very flexible to abstract input. It accepts all kinds of inputs and also guesses if it can't figure out for sure. Fiddle around and test its limits. If the app crashes, then you know that's an input Google barfed on :-)

The variable HUD contains the HUD activity indicator. escapedFrom and escapedTo are basically 'sanitized' versions of the input with properly escaped sequences for special characters. For example, "Bethesda MD" needs to become "Bethesda%20MD". This is because we will be passing this as a URI parameter to google and it needs to be properly escaped to be able to do that.

goButton, is, well, when you hit the go Button :-p (sort of like explaining "i" in "for (i=0)")

tableButton - well, as a matter of convenience, I've added a small button at the lower right that displays the last resolved weather condition along a given route. The logic there is that you may want to see it again, and re-computing it everytime is costly. So all you need to do is hit that button, we be show the previous table of weather along the route that was displayed. How thoughtful of me, wouldn't you say?

Finally, daySegment (actually day) will contain the day for which you need the weather prediction for. If you notice, I've only allowed for 4 days look ahead. I don't remember too well (its been a few months since I wrote this code, and I am positive my brain cells are dying due to which I have memory loss), but I think there may be a limit of how far ahead you can go with the hidden google HTTP API. Maybe I'll remember this once I start explaining the .m file. Or maybe, I just really wanted to use a segmented control because it looked cute and doing more than more made no sense...



- (IBAction) goPressed: (id)sender;
- (IBAction) tableButtonPressed: (id) sender;
-(IBAction) textFieldDoneEditing:(id)sender;
-(IBAction) backgroundTapped: (id)sender;
- (void) getDrivingWeather;
- (void) displayError: (NSString *)ex;
-(IBAction) segmentedControlIndexChanged;
@end


The only explanation I need to put in here are:
a) getDrivingWeather - the main working function
b) displayError - just a convenience API to show a Alert message when a parsing error occurs

DrivingWeatherViewController.m


//
// DrivingWeatherViewController.m
// DrivingWeather
//
// Created by Arjun on 10/29/10.
// Copyright 2010 Hughes Systique Corp. All rights reserved.
//

#import "DrivingWeatherViewController.h"

#import "WeatherTable.h"
#import "DrivingWeatherAppDelegate.h"
#import "WeatherDataClass.h"
#import "MBProgressHUD.h"
#import "TBXML.h"

@implementation DrivingWeatherViewController

@synthesize textFrom, textTo, goButton, tableButton, daySegment;

// MBProgressHUD calls this when the progress bar is over
- (void)hudWasHidden {
// Remove HUD from screen when the HUD was hidded
[HUD removeFromSuperview];
[HUD release];
}


// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
// Set up better looking buttons that the plain white ones that we get by default
UIImage *buttonImageNormal = [UIImage imageNamed:@"whiteButton.png"];
UIImage *stretchableButtonImageNormal = [buttonImageNormal stretchableImageWithLeftCapWidth:12 topCapHeight:0];
[goButton setBackgroundImage:stretchableButtonImageNormal forState:UIControlStateNormal];

UIImage *buttonImagePressed = [UIImage imageNamed:@"blueButton.png"];
UIImage *stretchableButtonImagePressed = [buttonImagePressed stretchableImageWithLeftCapWidth:12 topCapHeight:0];
[goButton setBackgroundImage:stretchableButtonImagePressed forState:UIControlStateHighlighted];

UIImage *tableButtonNormal = [UIImage imageNamed: @"notepad.png"];
[tableButton setBackgroundImage:tableButtonNormal forState:UIControlStateNormal];

[super viewDidLoad];

}



-(IBAction) segmentedControlIndexChanged
{
day = self.daySegment.selectedSegmentIndex;
}

// Just a convenient function to display an alert box

- (void) displayError : (NSString *)ex
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:[NSString stringWithFormat:@"%@",ex]
delegate:self cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
[alert release];
return;
}

// This is invoked when the user presses the notepad button on lower right
// Its just a convenient way to re-show the same table that you just saw (incase you want to go back to it)
- (IBAction) tableButtonPressed: (id) sender
{
WeatherTable *wt = [[WeatherTable alloc] initWithNibName:@"WeatherTable" bundle:[NSBundle mainBundle]];
[self presentModalViewController:wt animated:YES];
[wt release];
}

// This is invoked when user presses the Go button, which means we need to take the inputs in the text fields
// and process the driving weather parsing
- (IBAction) goPressed: (id)sender
{

escapedFrom = [[textFrom.text stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] retain];
escapedTo = [[textTo.text stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] retain];


NSLog (@"From:%@ To:%@",escapedFrom, escapedTo);

// Now lets show a progress dialog, since parsing all lat/long and geocoding takes time
HUD = [[MBProgressHUD alloc] initWithWindow:[UIApplication sharedApplication].keyWindow];
HUD.mode = MBProgressHUDModeDeterminate;

// Add HUD to screen
[self.view.window addSubview:HUD];

// Regisete for HUD callbacks so we can remove it from the window at the right time
HUD.delegate = self;

HUD.labelText = @"Working...";

// Show the HUD while the provided method executes in a new thread
NSLog (@"Just before showWhileExecuting");
[HUD showWhileExecuting:@selector(getDrivingWeather) onTarget:self withObject:nil animated:YES];

}


// This is the main function. goPressed calls this to actually start the entire XML GET/parse etc.
- (void) getDrivingWeather
{

CLLocationCoordinate2D coord[200]; // This should really be an NSArray...
NSLog (@"Inside getDrivingWeather");

NSString *drivingURI= [[NSString alloc] initWithFormat:@"http://maps.googleapis.com/maps/api/directions/xml?origin=%@&destination=%@&sensor=false",escapedFrom,escapedTo] ;

[escapedTo release];
[escapedFrom release];

float progress=0.0f;
float progressincr=0.0f;

HUD.labelText = @"Getting Directions";
NSLog(@"Google Driving URI is %@",drivingURI);



drivingURI is the base URL string that we need to invoke for Google to return the XML response (explained below). So we are setting up here - we display a HUD saying we are working on it, and then plan to pass the from and to values to this URL and fetch the responses (explained later).

A Diversion: Let's first understand the XML format returned by Google's driving directions APIs

Let's analyze the output of the following REST URL invocation:


http://maps.googleapis.com/maps/api/directions/xml?origin=20876&destination=21704@&sensor=false


Here is a fragment of the output:


The output is organized as so:
a) The entire directions is encapsulated in a<route>  tag
b) Each 'via point' is encapsulated in a  <leg> tag (in our app, there will only be one 'leg' of the journey - from start to finish. If you instead did A to B to C, then A-B would be one leg and B-C the other.
c) Next, each leg has many 'steps'. Each step is one instruction (example, step 1: Take left on 'Foo'. Step 2: Take right on 'Moo')
d) Inside each step, you have a lat/long for the starting and ending point.
e) There other tags, you can explore - easy to understand

What is our strategy:

  1. Iterate through this list and get to the lat/long values

  2. Next, pass the lat/longs to another google API to return weather conditions

  3. Next, convert the lat/longs to city names  so it can be displayed (If you know someone who sees a dump of weather and lat/long and finds it useful, you know a massive nerd)

  4. Next, eliminate repeated cities (for example, there may be 5 steps within a single city or zip code)

  5. Display it all in a nice way, including the weather icon pictorially showing the prediction (part of the weather API output from google)

Okay, now that you understand the structure and our strategy, lets get into execution



DrivingWeatherAppDelegate *appDelegate = (DrivingWeatherAppDelegate *)[[UIApplication sharedApplication] delegate];

// Each time we call this API, we need to clear the past table. Easy way is to free it and re-alloc it
// I suppose I really should be just deleting all rows..
[appDelegate.drivingWeatherArray release];
appDelegate.drivingWeatherArray = [[NSMutableArray alloc] init];

// Get the XML document that returns full driving directions
TBXML *directionsParser = [[TBXML alloc] initWithURL:[NSURL URLWithString:drivingURI]];

[drivingURI release];

if (directionsParser == nil)
{
[self displayError:@"Error getting driving directions. Try again, or check your input."];
return;
}



The appDelegate variable is just a convenient method for us to exchange data between different classes. It may not be a good design practice, but heck, good design requires intelligent thinking and I'm just slapping code together here. I don't claim to be an intelligent thinker. Basically, drivingWeatherArray will contain the parsed weather conditions for the route and it will be shared between this class and the WeatherTable class.

So here, we initialize the array, and then use an excellent open source, fast XML parser for iOS called TBXML. We pass it the driving directions URL that was composed using the base URL and the user input. the TBXML initWithURL instantiates a TBXML object, fetches the data in that URL and returns it to the directionParser variable. We now need to walk the XML chain from here.

TBXML is super easy to use and its instructions are really really simple. If you can't figure out how to use it from my code, go to their website and read about it.


HUD.labelText = @"Parsing locations";

TBXMLElement *routeLegStep = directionsParser.rootXMLElement;
routeLegStep = [TBXML childElementNamed:@"route" parentElement:routeLegStep];
routeLegStep = [TBXML childElementNamed:@"leg" parentElement:routeLegStep];
routeLegStep = [TBXML childElementNamed:@"step" parentElement:routeLegStep];
int cnt=0;

while (routeLegStep)
{
TBXMLElement *start_location = [TBXML childElementNamed:@"start_location" parentElement:routeLegStep];
TBXMLElement *lat_node = [TBXML childElementNamed:@"lat" parentElement:start_location];
TBXMLElement *long_node = [TBXML childElementNamed:@"lng" parentElement:start_location];

NSLog (@"TBXML GOT Lat:%@ Long:%@", [TBXML textForElement:lat_node], [TBXML textForElement:long_node]);
coord[cnt].latitude=[[TBXML textForElement:lat_node] doubleValue];
coord[cnt].longitude=[[TBXML textForElement:long_node] doubleValue];
cnt++;

TBXMLElement *end_location = [TBXML childElementNamed:@"end_location" parentElement:routeLegStep];
lat_node = [TBXML childElementNamed:@"lat" parentElement:end_location];
long_node = [TBXML childElementNamed:@"lng" parentElement:end_location];
NSLog (@"TBXML GOT Lat:%@ Long:%@", [TBXML textForElement:lat_node], [TBXML textForElement:long_node]);

coord[cnt].latitude=[[TBXML textForElement:lat_node] doubleValue];
coord[cnt].longitude=[[TBXML textForElement:long_node] doubleValue];
cnt++;

routeLegStep =[TBXML nextSiblingNamed:@"step" searchFromElement:routeLegStep];

}
[directionsParser release];


Next up, now that we have a XML document with the entire driving directions, we need to navigate down to the "route" tag, then get to the "leg" tag within in and then iterate though each "step" tag within it. Note that I've taken shortcuts - I know for sure there will only be one "leg" tag, so I don't bother looking for others. I just focus on the series of "step" tags inside the one "leg" tag. Once I have the starting pointer to the first "step", I iterate till I reach the end. For each iteration, I get the 'lat/long' for the start location as well as the 'lat/long' for the end location. Obviously, there will be many lat/longs for one 'city' or 'zip' and we need to filter them out. We will do all that later.
For now, we use TBXML to quickly iterate through all the start/stop lat longs and stuff them into a "coord" array. That's all we need from the directions XML response. A list of lat-longs along the driving route. Now let's get to the google weather APIs to work the next part of the magic.


NSLog (@"TOTAL LATLONG %d", cnt);
NSLog (@"Now getting weather for each LATLONG");

// This is used to update the progress pie. Basically, for each "cnt" (i.e. total list of lat/long)
// we are doing two network operations (weather, geocode). So we increment progress accordigly so it reaches
// 1 after 2*cnt operations are completed. MBProgressHUD class considers 1 to be complete.
progressincr = 1.0f/(2.0f*cnt);

// For each lat/long, we need to call the google weather API
// and then also call google geocode API
for (int i=0; i&lt; cnt; i++)
{

HUD.labelText=[NSString stringWithFormat:@"Getting Weather (%d of %d)",i+1,cnt];
NSString *myLatString=[NSString stringWithFormat:@"%f", coord[i].latitude];
NSString *myLongString=[NSString stringWithFormat:@"%f", coord[i].longitude];

myLatString = [myLatString stringByReplacingOccurrencesOfString:@"." withString:@""];
myLongString = [myLongString stringByReplacingOccurrencesOfString:@"." withString:@""];

NSString *weathercoord, *revgeocode;
weathercoord = [NSString stringWithFormat:@"http://www.google.com/ig/api?weather=,,,%@,%@", myLatString, myLongString];

NSLog(@"Google Weather URI:%@",weathercoord);


For each lat/long in our array, we need to pass that lat/long to the google weather API URL to get its weather. This is a time consuming operation, so we display a HUD. We also use a Pie-chart HUD display that MBProgressHUD offers so the user can visually see how much more of parsing is left (Just as one example, driving from MD to CA involves 84 different steps, which means we have to call the weather API 84 times, plus, reverseGeoCode API (coming up later) another 84 times to get city names.

So really, the pie chart doesn't just look cool. It serves a purpose.

The variable weathercoord contains the google weather API URL along with the lat/long we need data for.

Also, the XML URL does not accept "decimal points" in the lat/long. All you need to do is remove it and it works fine (discovered by some blogger - I don't have the reference now)

Diversion: Understanding the Weather XML data

Here is an example of the output for http://www.google.com/ig/api?weather=,,,39341250,-77366200



Lets understand first on how we need to parse it and then go to the code. It's very simple. First there is a "current_conditions" tag that tells you what is the current temperature at that lat/long. Next, there are "forecast_conditions" tags that tell you what is the expected low and high for the next 4 days (Aha! so that explains why I only let you select upto 4 days ahead. My ailing memory came back). Simple! One more thing - it also gives you a link to an image that pictorially shows the weather condition too. Now that's some jazz that would look good...

Okay back to the code:


TBXML *weatherParser = [[TBXML alloc] initWithURL:[NSURL URLWithString:weathercoord]];
if (weatherParser == nil)
{
NSLog(@"Error parsing Weather URL for lat:%f, long:%f",coord[i].latitude, coord[i].longitude);
continue;
}

progress += progressincr;
HUD.progress = progress;

WeatherDataClass *wdc;
wdc = [[WeatherDataClass alloc] init]; // just alloc wdc object. member functions will retain memory from NSString below






@try
{

wdc.longitude = [[NSString stringWithFormat:@"%f", coord[i].longitude] retain];
wdc.latitude = [[NSString stringWithFormat:@"%f", coord[i].latitude] retain];

TBXMLElement *myElement = weatherParser.rootXMLElement;
TBXMLElement *myChild;

myElement = [TBXML childElementNamed:@"weather" parentElement:myElement];

// first check if there is a tag called problem_cause_data. If it is, there was an error in the XML

TBXMLElement *problemData = [TBXML childElementNamed:@"problem_cause_data" parentElement:myElement];

if (problemData) // yes it found it
{
NSLog(@"(Problem_cause reported) Error parsing Weather URL for lat:%f, long:%f",coord[i].latitude, coord[i].longitude);
continue;

}


Like before, we use the trusty TBXML class to walk though the XML API we explained before. A minor nit - before you parse, look for a tag called "problem_cause_data" - if it is part of the XML, parsing failed. So before we burn and crash, check it first. If it was returned, something got foo bar'd.



// if it comes here, I think the XML should have everything

// now check what is the value of day. If it is 0, then look at current_conditions
// else iterate forecast_conditions

// Now get the date as well
NSString *forecast_day=@"Unknown";
if (day==0)
{
myElement = [TBXML childElementNamed:@"current_conditions" parentElement:myElement];
//[forecast_day release];
forecast_day=@"Today";
//forecast_day=[[NSString alloc] initWthString:@"Today"];
NSLog(@"Today");
}
else
{
myElement = [TBXML childElementNamed:@"forecast_conditions" parentElement:myElement];
NSLog(@"Next Day");
for (int i=0; i&lt;day-1; i++)

{
NSLog(@"Next Day");
if (myElement) {myElement =[TBXML nextSiblingNamed:@"forecast_conditions" searchFromElement:myElement];}

}
if (i==0)
{
TBXMLElement *mDayOfWeek = [TBXML childElementNamed:@"day_of_week" parentElement:myElement];
if (mDayOfWeek)
{
//[forecast_day release];
forecast_day = [[TBXML valueOfAttributeNamed:@"data" forElement:mDayOfWeek] retain];
}
} // i==0
} //else


Now we need to figure out which weather data is of interest to us. That depends on what day the user selected in the segmented control (day variable). If the user selected "Today" then we need to parse the "current_conditions" tag. If not, then we need to walk through the "forecast_conditions" tags, once for each day ahead chosen.


if (i==0)
{
[appDelegate.drivingWeatherArray addObject:[NSString stringWithFormat:@"Weather Forecast is for %@", forecast_day]];
}
//[forecast_day release];

myChild = [TBXML childElementNamed:@"condition" parentElement:myElement];
if (myChild) {wdc.condition = [[TBXML valueOfAttributeNamed:@"data" forElement:myChild] retain];}
NSLog(@"TBXML GOT condition:%@",wdc.condition);

myChild = [TBXML childElementNamed:@"temp_f" parentElement:myElement];
if (myChild) {wdc.curtemp = [[TBXML valueOfAttributeNamed:@"data" forElement:myChild] retain];}
NSLog(@"TBXML GOT curtemp:%@",wdc.curtemp);

myChild = [TBXML childElementNamed:@"low" parentElement:myElement];
if (myChild) {wdc.lowtemp = [[TBXML valueOfAttributeNamed:@"data" forElement:myChild] retain];}
NSLog(@"TBXML GOT lowtemp:%@",wdc.lowtemp);

myChild = [TBXML childElementNamed:@"high" parentElement:myElement];
if (myChild) {wdc.hitemp = [[TBXML valueOfAttributeNamed:@"data" forElement:myChild] retain];}
NSLog(@"TBXML GOT hitemp:%@",wdc.hitemp);

myChild = [TBXML childElementNamed:@"icon" parentElement:myElement];
if (myChild) {wdc.url = [[NSString stringWithFormat:@"http://www.google.com%@", [TBXML valueOfAttributeNamed:@"data" forElement:myChild]]retain] ;}
NSLog(@"TBXML GOT url:%@",wdc.url);
}
@catch (NSException *ex)
{
[self displayError:@"Error parsing Weather. Try again or check input"];
NSLog(@"Error:%@",ex);
return;
}



Right, so the code above walks through the XML and picks up the current temperature ( if current day is chosen), or, the forecast high and low for a forecast day. We also pick up the image URL (its in the "icon" tag in the XML output). All of this goes into a data structure (wdc) for use later.



revgeocode = [NSString stringWithFormat:@"http://maps.google.com/maps/api/geocode/xml?latlng=%f,%f&amp;amp;sensor=true", coord[i].latitude, coord[i].longitude];
NSLog (@"GOOGLE GEOCODE URI:%@",revgeocode);



Finished! Now lets populate the table! Oh wait. One more thing. Our last operation is to convert all the lat/longs to cities and zipcodes, and then filter our repeating zip codes (I could have chosen to remove repeating cities, but my assumption is if zip changes, it may be far enough for weather to change). So here, we use the google geocode API, pass it a lat long and hope to get some human understandable city/zip in return

For this one, I am not going to explain the XML output in detail. It is a little voluminous. Take a look at the output for


http://maps.google.com/maps/api/geocode/xml?latlng=39.341250,-77.366200&sensor=true


as an example. Suffice to say, what we are looking for is the "postal_code" tag with a "formatted_address" tag inside it. Thats the value we want to display. So the next piece of code walks this chain and looks for it.

TBXML *geocodeParser;
@try
{
geocodeParser = [[TBXML alloc] initWithURL:[NSURL URLWithString:revgeocode]];

if (!geocodeParser) { [self displayError:@"Failed to get Google GeoCode XML!"]; [wdc release]; continue; }

TBXMLElement * rootXMLElement = geocodeParser.rootXMLElement;
if (!rootXMLElement) { NSLog(@"Error:Failed to get Root of Google GeoCode XML!"); [geocodeParser release];[wdc release]; continue; }

TBXMLElement *resultElement = [TBXML childElementNamed:@"result" parentElement:rootXMLElement];
if (!resultElement) { NSLog(@"Error:Failed to get result tag. Continuing."); [geocodeParser release]; [wdc release];continue; }

TBXMLElement *typeElement = [TBXML childElementNamed:@"type" parentElement:resultElement];
if (!typeElement) { NSLog(@"Error:Failed to get type tag"); [geocodeParser release]; [wdc release]; continue; }

NSString *type_text = [TBXML textForElement:typeElement];
if (!type_text) { NSLog(@"Error:Failed to get text of type tag (odd)"); [geocodeParser release]; [wdc release]; continue; }

while ( (resultElement) &amp;&amp; ( !([type_text isEqualToString:@"postal_code"]) ) )
{
resultElement = [TBXML nextSiblingNamed:@"result" searchFromElement:resultElement];
if (resultElement)
{
typeElement = [TBXML childElementNamed:@"type" parentElement:resultElement];
type_text = [TBXML textForElement:typeElement];
}
else
{
typeElement=nil;
type_text=nil;
}

NSLog(@"TYPE IS:%@",type_text);
}

if (resultElement) // you got to the area which has address
{
TBXMLElement *address=[TBXML childElementNamed:@"formatted_address" parentElement:resultElement];
if (address) { wdc.city = [TBXML textForElement:address]; NSLog (@"GOT Geocode %@",wdc.city);}
}
progress += progressincr;
HUD.progress = progress;
[geocodeParser release];
}
@catch (NSException *ex)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"TBXML Error" message:[NSString stringWithFormat:@"%@",ex]
delegate:self cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
[alert release];
return;
}



Right, now we have the city and zip for that lat long.



/* Now, iterate through the weather array and and check if city was already added. We can't do it before because different lat/longs map
to the same cities. So we first need to get all the latlongs and then compare the resultant city returned by the google geocode api */

int match=0;

if (wdc.city)
{
NSLog(@"**********************************************************");
for (int ndx=1; ndx&lt;[appDelegate.drivingWeatherArray count]; ndx++) // start at 1, as 0 has header info
{
WeatherDataClass *w = [appDelegate.drivingWeatherArray objectAtIndex:ndx];

if ([w.city isEqualToString:wdc.city])
{
match=1;
}

} // for array match
if (!match)
{
[appDelegate.drivingWeatherArray addObject:wdc];
}
[wdc release];
}
} // for

HUD.progress=1;

WeatherTable *wt = [[WeatherTable alloc] initWithNibName:@"WeatherTable" bundle:[NSBundle mainBundle]];
[self presentModalViewController:wt animated:YES];
[wt release];

}



What we now do here is check the drivingWeatherArray object to see if this city+zip is already part of our list. If it is, its a repeat, so just skip and don't add, else add to the object array.

And finally, display all of that data in a nice TableView. Done!


// used to make keyboard disappear

-(IBAction) textFieldDoneEditing:(id)sender
{
[sender resignFirstResponder];
}

-(IBAction) backgroundTapped:(id)sender;
{
[textTo resignFirstResponder];
[textFrom resignFirstResponder];
}

/*
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
*/

- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
NSLog (@"******MEMORY WARNING***********");
[super didReceiveMemoryWarning];

// Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}

- (void)dealloc {

[textFrom release];
[textTo release];
[escapedFrom release];
[escapedTo release];
[tableButton release];
[daySegment release];

[super dealloc];
}

@end



The "remaining tail" of the main code - usual iphone skeleton code.

WeatherTable.h


//
// WeatherTable.h
// DrivingWeather
//
// Created by Arjun on 10/31/10.
// Copyright 2010 Hughes Systique Corp. All rights reserved.
//

#import <UIKit/UIKit.h>
@interface WeatherTable : UIViewController <UITableViewDataSource, UITableViewDelegate>{
IBOutlet UITableView *weatherTableView;
IBOutlet UIButton *doneButton;

}

@property (nonatomic, retain) UITableView *weatherTableView;
@property (nonatomic, retain) UIButton *doneButton;

-(IBAction) done: (id) sender;

@end


Nothing much - a standard TableView implementation...

WeatherTable.m

This class displays the parsed weather along your driving route in a nice table


//
// WeatherTable.m
// DrivingWeather
//
// Created by Arjun on 10/31/10.
// Copyright 2010 Hughes Systique Corp. All rights reserved.
//

#import "WeatherTable.h"
#import "CustomCell.h"
#import "DrivingWeatherAppDelegate.h"
#import "WeatherDataClass.h"

@implementation WeatherTable

@synthesize weatherTableView;
@synthesize doneButton;

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
DrivingWeatherAppDelegate *appDelegate = (DrivingWeatherAppDelegate *)[[UIApplication sharedApplication] delegate];
return [appDelegate.drivingWeatherArray count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DrivingWeatherAppDelegate *appDelegate = (DrivingWeatherAppDelegate *)[[UIApplication sharedApplication] delegate];
static NSString *CustomCellIdentifier=@"CustomCellIdentifier";
NSUInteger row = [indexPath row];
CustomCell *cell;


We are using a pointer to the appDelegate class to share the driving list Array between the two classes as explained earlier.
Also, each row of the Table will be filled with a custom cell which will accomodate an image and two rows of data.


/* First row is special - I display forecast day, copyrights etc, so handle differently */
if (row==0)
{
cell=(CustomCell *) [tableView dequeueReusableCellWithIdentifier:@"HeaderCell"];
if (cell==nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}

cell.cityLabel.text = [appDelegate.drivingWeatherArray objectAtIndex:row];
cell.forecastLabel.text=@"All data \u00A9 Google,inc.";
//[cell.forecastLabel setFont:[UIFont fontWithName:@"Arial-BoldMT" size:18]];
cell.imageView.image = [UIImage imageNamed:@"icon.png"];

}
else
{

cell=(CustomCell *) [tableView dequeueReusableCellWithIdentifier:CustomCellIdentifier];
if (cell==nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}


If you look at my sample screenshot for the results screen, the first row is "special" - it displays a copyright message attributing the data to google and my app icon. So we need to ensure that we use a different identifier in "dequeueReusableCellWithIdentifier". If we did not, then iphone will start sharing memory of that row with other rows. that will mess up formatting the moment you scroll around because it will overwrite formatting data of the header row with the other rows (since you told it that it can be reused if needed) - try it out. So we keep them separate.



WeatherDataClass *wdc= [appDelegate.drivingWeatherArray objectAtIndex:row];
NSString *weathertext;
NSString *nowtext, *lowtext, *hitext;
nowtext=@"";
lowtext=@"";
hitext=@"";

/* When you get weather from google, it shows current temperature for "today" but no high and low
Similarly, if you ask for temperature for other days (+1 to +4 days from today), it shows high and low
but current does not make sense, as it is future. So here, I just present stuff I get while keeping the null
values from being displayed. \xC2\xB0 is hex for the degree symbol
*/

if (wdc.curtemp) { nowtext = [NSString stringWithFormat:@"Now:%@\xC2\xB0 F",wdc.curtemp];}
if (wdc.lowtemp) { lowtext = [NSString stringWithFormat:@"Low:%@\xC2\xB0 F",wdc.lowtemp];}
if (wdc.hitemp) { hitext = [NSString stringWithFormat:@"High:%@\xC2\xB0 F",wdc.hitemp];}

weathertext= [NSString stringWithFormat:@"%@ %@ %@",nowtext,lowtext,hitext];
cell.cityLabel.text = wdc.city;
cell.temperatureLabel.text = weathertext;
cell.forecastLabel.text = wdc.condition;

NSString* imageURL = wdc.url;
NSLog(@"IMAGE URL:%@",wdc.url);
NSData* imageData = [[NSData alloc]initWithContentsOfURL:[NSURL URLWithString:imageURL]];

UIImage* image = [[UIImage alloc] initWithData:imageData];
[cell.imageView setImage:image];
[imageData release];
[image release];
}
return cell;

}



Next up, we iterate through the drivingWeather Array and set the table cell fields to the right values. Loose explanation, but I think its pretty simple what is going on up here.



// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
UIImage *buttonImageNormal = [UIImage imageNamed:@"whiteButton.png"];
UIImage *stretchableButtonImageNormal = [buttonImageNormal stretchableImageWithLeftCapWidth:12 topCapHeight:0];
[doneButton setBackgroundImage:stretchableButtonImageNormal forState:UIControlStateNormal];

UIImage *buttonImagePressed = [UIImage imageNamed:@"blueButton.png"];
UIImage *stretchableButtonImagePressed = [buttonImagePressed stretchableImageWithLeftCapWidth:12 topCapHeight:0];
[doneButton setBackgroundImage:stretchableButtonImagePressed forState:UIControlStateHighlighted];

[super viewDidLoad];

}

-(IBAction) done:(id) sender {
[self.parentViewController dismissModalViewControllerAnimated:YES];
}

- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];

// Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}

- (void)dealloc {
[super dealloc];
}

@end


No explanation needed above -standard stuff.

CustomCell.h and .m

.h:


//
// CustomCell.h
//
//
// Created by Arjun on 9/29/10.
// Copyright 2010 Hughes Systique Corp. All rights reserved.
//

#import <UIKit/UIKit.h>

// This defines how each cell of the table will look

@interface CustomCell : UITableViewCell {
IBOutlet UILabel *cityLabel;
IBOutlet UILabel *forecastLabel;
IBOutlet UILabel *temperatureLabel;
IBOutlet UIImageView *imageView;

}
@property (nonatomic, retain) UILabel *cityLabel;
@property (nonatomic, retain) UILabel *forecastLabel;
@property (nonatomic, retain) UILabel *temperatureLabel;
@property (nonatomic, retain) UIImageView *imageView;

@end


CustomCell is a XIB file I created in Interface Builder. It has a label to display the city, forecast, temperature and an imageview to show the weather icon.

.m:


//
// CustomCell.m
// ViewSwitcher
//
// Created by Arjun on 9/29/10.
// Copyright 2010 Hughes Systique Corp. All rights reserved.
//

#import "CustomCell.h"

@implementation CustomCell

@synthesize cityLabel;
@synthesize forecastLabel;
@synthesize temperatureLabel;
@synthesize imageView;

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if ((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) {
// Initialization code
}
return self;
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {

[super setSelected:selected animated:animated];

// Configure the view for the selected state
}

- (void)dealloc {
[cityLabel release];
[forecastLabel release];
[temperatureLabel release];
[super dealloc];
}

@end


Download Project Source

Grab it from HERE. Hope it helps you.

8 comments:

  1. Hello,

    I try your code but when I go to two cities, the application crashes and closes as soon as I press GO?

    is this normal? Can you help me or tell me if I forget something?

    thank you

    PS: excuse me in English but I'm french

    ReplyDelete
  2. Hi, the code I posted does not check for null parser returns in several places. I fixed that later, but did not upload the new code. Please check the input you are providing - if its an invalid input, likely it is crashing because the parser was not able to parse the returned response.

    ReplyDelete
  3. hi,
    Thank you for your answer but YES my destination is correct since I use the same as in the example you ... and it does not work

    ReplyDelete
  4. Hi, maybe you can run the debugger and see where its crashing? It seems to be working fine with me (note: I haven\'t tested in ios 5 yet)

    ReplyDelete
  5. I'm truly enjoying the design and layout of your site. It's a very easy on
    the eyes which makes it much more enjoyable for me to
    come here and visit more often. Did you hire out a
    designer to create your theme? Exceptional work!

    ReplyDelete
  6. Oh my goodness! Impressive article dude! Many thanks,
    However I am going through difficulties with your RSS.

    I don't know why I cannot join it. Is there anybody else getting similar RSS issues? Anyone who knows the answer will you kindly respond? Thanks!!

    ReplyDelete
  7. I see you share interesting things here, you can earn some additional money, your website has huge potential,
    for the monetizing method, just search in google - K2 advices how to monetize a website

    ReplyDelete