Parse Weather Forecast Data (from the NDFD) in PHP
Published: February 19th, 2009 by: Andrew
In a previous article, I covered 5 Sources of Free Weather Data for your Site, but did not provide any actual code to use the data. Starting with this article, I will post instructions on how to handle this data as well as sample code. For this article, we will start with the National Digital Forecast Database (NDFD) Simple Object Access Protocol (SOAP) Web Service.
Where to Find the Data
See weather.gov/xml for full details and available data. We will be using the 24 Hourly format, which gives us temperatures, cloud cover %, weather type (rain, snow, etc), hazardous conditions, and even weather icons.
Code to Fetch the Data
Below is some sample code that will fetch the data. The lat/lon coordinates need to be changed for your desired location on lines 10-11. Currently, it is set for Chicago, IL.
<?php
/* http://sourceforge.net/projects/nusoap/ */
require('../includes/nusoap/nusoap.php');
$parameters = array('product' => 'glance',
'numDays' => 5,
'format' => '24 hourly',
'latitude' => 41.879535,
'longitude' => -87.624333);
try
{
/* create the nuSOAP object */
$c = new nusoap_client('http://www.weather.gov/forecasts/xml/DWMLgen/wsdl/ndfdXML.wsdl', 'wsdl');
/* make the request */
$result = $c->call('NDFDgen', $parameters);
}
catch (Exception $ex)
{
/* nuSOAP throws an exception is there was a problem fetching the data */
echo 'failed';
}
header('Content-type: text/xml');
echo $result;
/* ?> */
This is by no means a production sample. You are going to want to cache the results somehow so you aren’t querying the NDFD every time you need this information. It is only updated every hour at most, so there is no need to query it more frequent than that.
Understanding the XML Output
It took me a great deal of staring and reading to figure out how this XML output is organized. Have a look at this sample output, as I will refer to this specific example to explain the sections. I recommend loading the sample XML file in Firefox or any other web browser that allows you to collapse and expand the XML tree.
First, collapse the “head” area by clicking on the minus next to the “head” tag. Everything in there is pretty self-explanatory. Our focus is going to be in the “data” tag. Next, collapse all tags inside the “data” tag, except for the “parameters” tag, but collapse everything inside the “parameters” tag so we can see everything in there w/o scrolling. Here is what your view should look like:
Now we are at a point where we can see how this is all grouped together. As we can see, the “parameters” section shows the products that are available, which are temperature (minimum), temperature (maximum), cloud-amount, weather, conditions-icon, and hazards. Each of them is associated with a time layout because most products are on a different time scale. For example, max temps are during the afternoon & once a day, min temps are at night & once a day, the conditions icons are at several points throughout the next several days, etc. It is separated out this way because not all products have a different time scale – some are the same, like “weather” and “conditions-icon” in our example.
Time to match each product to their time layout. We see that the maximum temps have the time layout of “k-p24h-n7-1″, so we go up to the time layouts and find one with the layout-key of “k-p24h-n7-1″.
We have to code a way to merge all data sets with their corresponding time layout. This may look complicated, but I will show you a somewhat easy way to parse this in PHP.
<?php
/************************************/
/* some functions we will use later */
/************************************/
/**
* get the string values of the object items
* @param mixed $object the XML object to extract the string values from
* @return array
*/
function get_values($object)
{
$arr = array();
foreach ($object as $field => $value)
{
$arr[] = (string)$value;
}
return $arr;
}
/*****************************/
/* actual script starts here */
/*****************************/
/* load our example from http://phpstarter.net/samples/348/ndfd_forecast.xml */
$xml_data = file_get_contents('ndfd_forecast.xml');
/* parse the XML data into a giant data object */
try
{
$xml = new SimpleXMLElement($xml_data);
}
catch (Exception $ex)
{
/* the XML was probably invalid */
die('Failed to parse the XML');
}
/* all time layouts go in here with the layout-key as the array keys */
$times = array();
/* loop through the time-layouts in the XML */
foreach ($xml->xpath('//time-layout') as $field => $value)
{
/* get the layout-key to make it the array key value */
$layout = (string)$value->{'layout-key'};
$times[$layout] = array('start' => get_values($value->xpath('start-valid-time')),
'end' => get_values($value->xpath('end-valid-time')));
}
header('Content-type: text/plain');
var_dump($times);
/**
* Why do I comment out the PHP closing tag?
* See: http://phpstarter.net/2009/01/omit-the-php-closing-tag/
*/
/* ?> */
Now we have all the time layouts in a way we can manage it. The next task is to match it up to the data to their respective time layouts. This next example includes the code to organize the time layouts, and then organize the data to correspond with the times. Running the example below will show that the data is now neatly organized in a way that it can be easily used.
<?php
/************************************/
/* some functions we will use later */
/************************************/
/**
* get the string values of the object items
* @param mixed $object the XML object to extract the string values from
* @return array
*/
function get_values($object)
{
$arr = array();
foreach ($object as $field => $value)
{
$arr[] = (string)$value;
}
return $arr;
}
/**
* Combine a time layout with a product
*
* @param string $data_xpath the xpath to the data set
* @param string $time_xpath the xpath to the time layout
* @param array $times the big assoc array of all the time layouts
*/
function merge_times_data($data_xpath, $time_xpath, $times, $xml)
{
$data = get_values($xml->xpath($data_xpath));
$time_layout = $xml->xpath($time_xpath);
if (!$time_layout) return false;
$time_layout = (string)$time_layout[0]['time-layout'];
$data = array_combine($times[$time_layout]['start'], $data);
return $data;
}
/**
* get the time layouts in an array form
*
* @param string $xml
* @return mixed
*/
function get_time_labels($xml)
{
$data = $xml->xpath("//start-valid-time");
$times = array();
foreach ($data as $field => $value)
{
if ((string)$value['period-name'])
{
$index = (string)$value;
$times[$index] = (string)$value['period-name'];
}
}
ksort($times);
return $times;
}
/*****************************/
/* actual script starts here */
/*****************************/
/* load our example from http://phpstarter.net/samples/348/ndfd_forecast.xml */
$xml_data = file_get_contents('ndfd_forecast.xml');
/* parse the XML data into a giant data object */
try
{
$xml = new SimpleXMLElement($xml_data);
}
catch (Exception $ex)
{
/* the XML was probably invalid */
die('Failed to parse the XML');
}
/* all time layouts go in here with the layout-key as the array keys */
$times = array();
/* loop through the time-layouts */
foreach ($xml->xpath('//time-layout') as $field => $value)
{
/* get the layout-key to make it the array key value */
$layout = (string)$value->{'layout-key'};
$times[$layout] = array('start' => get_values($value->xpath('start-valid-time')),
'end' => get_values($value->xpath('end-valid-time')));
}
$forecast['max_temps'] = merge_times_data("//parameters/temperature[@type='maximum']/value",
"//parameters/temperature[@type='maximum']", $times, $xml);
$forecast['min_temps'] = merge_times_data("//parameters/temperature[@type='minimum']/value",
"//parameters/temperature[@type='minimum']", $times, $xml);
$forecast['temps'] = array_merge($forecast['min_temps'], $forecast['max_temps']);
$forecast['icons'] = merge_times_data("//parameters/conditions-icon/icon-link",
"//parameters/conditions-icon", $times, $xml);
$forecast['time_labels'] = get_time_labels($xml);
header('Content-type: text/plain');
var_dump($forecast);
/**
* Why do I comment out the PHP closing tag?
* See: http://phpstarter.net/2009/01/omit-the-php-closing-tag/
*/
/* ?> */
There is another function in that example that associates the times to the time labels that you see in the XML. Some of them has a parameter called “period-name”, which is the day of the week or holiday name, if applicable.
How to Use the Formatted Data
For this last example, I will show you in the simplest terms how to use this data. Remember, we are not pulling data from the NDFD live – this is using the XML from our earlier example, so the temps are for mid-winter in NW Indiana.
<?php
/**
* pick up where we left off from the last example
* no need to generate the data array again here
*/
$forecast = file_get_contents('data.txt');
$forecast = unserialize($forecast);
/**
* Show the temperatures
*/
foreach ($forecast['max_temps'] as $field => $value)
{
echo 'High temp for ' . $forecast['time_labels'][$field] . ': ' . $value . "<br />\n";
}
foreach ($forecast['min_temps'] as $field => $value)
{
echo 'Low temp for ' . $forecast['time_labels'][$field] . ': ' . $value . "<br />\n";
}
/**
* Why do I comment out the PHP closing tag?
* See: http://phpstarter.net/2009/01/omit-the-php-closing-tag/
*/
/* ?> */
So there you have it – an XML parsing nightmare made easy. This method is in my opinion the best way to receive and parse this data from the National Weather Service. It may not be the best way for everyone, so if you have something better to add, please post a comment!
(Added 2009/02/20) Fetching Time-Series Data from the NDFD
Requesting the time-series data returns a whole bunch more information. As requested, here is an example on how to fetch it. Change the desired parameters to ‘true’. The other examples on how to parse the time layouts and formatting data works on this XML, too.
<?php
/* http://sourceforge.net/projects/nusoap/ */
require('../includes/nusoap/nusoap.php');
$parameters = array('product' => 'time-series',
'latitude' => 41.879535,
'longitude' => -87.624333,
'weatherParameters' => array(
'maxt' => true, 'mint' => false, 'temp' => false, 'dew' => false,
'appt' => true, 'pop12' => false, 'qpf' => false, 'snow' => false,
'sky' => false, 'rh' => false, 'wspd' => false, 'wdir' => false,
'wx' => false, 'icons' => false, 'waveh' => false, 'incw34' => false,
'incw50' => false, 'incw64' => false, 'cumw34' => false, 'cumw50' => false,
'cumw64' => false, 'wgust' => false, 'conhazo' => false, 'ptornado' => false,
'phail' => false, 'ptstmwinds' => false, 'pxtornado' => false, 'pxhail' => false,
'pxtstmwinds' => false, 'ptotsvrtstm' => false, 'pxtotsvrtstm' => false, 'tmpabv14d' => false,
'tmpblw14d' => false, 'tmpabv30d' => false, 'tmpblw30d' => false, 'tmpabv90d' => false,
'tmpblw90d' => false, 'prcpabv14d' => false, 'prcpblw14d' => false, 'prcpabv30d' => false,
'prcpblw30d' => false, 'prcpabv90d' => false, 'prcpblw90d' => false, 'precipa_r' => false,
'sky_r' => false, 'td_r' => false, 'temp_r' => false, 'wdir_r' => false,
'wwa' => false, 'wspd_r' => false)
);
try
{
/* create the nuSOAP object */
$c = new nusoap_client('http://www.weather.gov/forecasts/xml/DWMLgen/wsdl/ndfdXML.wsdl', 'wsdl');
/* make the request */
$result = $c->call('NDFDgen', $parameters);
}
catch (Exception $ex)
{
/* nuSOAP throws an exception is there was a problem fetching the data */
echo 'failed';
}
header('Content-type: text/xml');
echo $result;
/* ?> */
More tips can now be found here: More Examples with Parsing NDFD Data in PHP
No related posts.





Brian Shin
Feb 19th, 2009
9:03 pm
Andrew,
Thanks for the work on the NDFD stuff. I’m looking to do this exact project, but with a different part of the NDFD.
I’m working with this one;
http://www.weather.gov/forecasts/xml/DWMLgen/wsdl/ndfdXML.wsdl
and I can’t seem to build the parameters for the entire thing. It looks like it has an embedded array/complex type like this;
-40.7561
–111.901
-time-series
-2009-02-18T00:00:00
-2009-02-18T00:00:00
-
-true
-true
-false
-false
-
Anyhow, do you have any suggestions on how this could work using your example?
Thanks,
Brian
Andrew
Feb 20th, 2009
12:22 pm
Hi Brian,
I updated the post to show an example for the time-series data. I didn’t use it at first because I couldn’t get it to function correctly with the parameters, either. It turns out that the weatherParameters has to be in a sub-array inside the other parameters, as shown in the example. Hope this helps.
I cleaned up some the first example as well…there were some parameters in there that were not needed.
Brian Shin
Feb 20th, 2009
12:27 pm
Andrew,
Thanks for the quick reply and update. This complex type has caused me so much headache. You are right with the array of the weatherParameters, so I tried;
$weatherParameters = array(‘maxt’ => true,
‘mint’ => true,
‘temp’ => true);
$parameters = array(‘product’ => ‘time-series’,
’startTime’ => ‘2009-02-18T00:00:00′,
‘endTime’ => ‘2009-02-18T00:00:00′,
‘format’ => ‘24 hourly’,
‘latitude’ => 40.7561,
‘longitude’ => -111.901,
$weatherParameters);
but that doesn’t work. I don’t know if the boolean is “true”, true, 1, or 0.
Then I found a sample in the ndfdXMLclient.php file and they make a call like;
$weatherParameters = array(‘weatherParameters’ => array(‘maxt’ => 1 == 1,’mint’ => 1 == 1,’temp’ => 1 == 1));
$parameters = array(‘product’ => ‘time-series’,
’startTime’ => ‘2009-02-20T00:00:00′,
‘endTime’ => ‘2009-02-20T00:00:00′,
‘latitude’ => 40.7561,
‘longitude’ => -111.901,
‘weatherParameters’ => $weatherParameters);
but when I run this, I don’t think what I get back is right. I’m not all that good with PHP yet so I’m not sure what I’m doing wrong. You can see at test on my web site took, http://www.vikingcomputerconsulting.com/soap.html. It’s the first link. The items 1 through 3 are me trying to get your examples to work
(Which you can see don’t work either) lol!
Thanks for your help,
Brian
Andrew
Feb 24th, 2009
3:33 pm
What you saw in the ndfdXMLclient.php file was the testing for the presence of the $_GET variables. If you run the example hosted on their servers here, you see that all form values are sent in the URL, so the receiving PHP script is testing for those values in the URL.
For our purposes, your first example is close…here is what it should be:
$weatherParameters = array('maxt' => true, 'mint' => true, 'temp' => true); $parameters = array('product' => 'time-series', 'format' => '24 hourly', 'latitude' => 40.7561, 'longitude' => -111.901, 'weatherParameters' => $weatherParameters);All I did was add “‘weatherParameters’ => ” to the last line and removed the time limits.
Sam
Feb 25th, 2009
8:53 pm
Howdy Andrew,
I’ve been jumping around the web looking for hints and tips on parsing this monster of an xml file I get from noaa when I request (and cache) the NDFDgen() function. So far high and low temperatures have been easy to parse, but i can’t figure out how to get the correct icons, snow reports, etc to correspond with the correct date because there are at times at least 40 of each!
Here is what I’ve got parsing the xml thus far. I am a novice to php and am learning by fire at the moment. If there are any thoughts you could offer they would be greatly appreciated!
Cheers,
Sam
data->parameters; $count = count($xml_weather->temperature->value); for($i = 0; $i data->{'time-layout'}->{'start-valid-time'}[$i]); $weekdays[] = date('l', $timestamp); $high = $xml_weather->temperature[0]->value[$i]; $highs[] = $high; } for($i = 0; $i temperature[1]->value[$i]; $lows[] = $low; } $credit = $xml->head->source->credit; } else { exit('Failed to open forecast.xml.'); } echo $weekdays[1].""; echo 'High: '.$highs[1]. '°'; echo 'Low: '.$lows[1]. '°'; echo $condition; echo ''.$credit; ?>Andrew
Feb 25th, 2009
10:17 pm
Hi Sam,
Yes, with some of the products using different time intervals, it can make it annoying to put, let’s say the temperature and the icons together. For example, on this forecast page, I got the temps and images to line up with some extra parsing not described in this article. Is that what you are looking to do?
Sam
Feb 25th, 2009
10:42 pm
Andrew,
Thanks so much for the quick response. That is EXACTLY what I am trying to do! Any guidance you might be able to offer would be greatly appreciated!
Cheers,
Sam
Sam
Feb 25th, 2009
10:44 pm
In fact chasingweather.com is doing a lot of what I hope to create!
Andrew
Feb 26th, 2009
9:14 am
Sam: I will post an article on Tuesday that will cover the topic you talked about as well as more examples with working with NDFD data.
Sam
Feb 26th, 2009
9:36 am
Sounds good Andrew. I very much look forward to it. Any fodder you could throw at me to chew on in the meantime would be great, but if you would rather wait, I’ll be waiting anxiously
Sam
Robert
Aug 21st, 2009
12:21 am
Why is it I am always getting this error referring to:
header(‘Content-type: text/html’);
Warning: Cannot modify header information – headers already sent by (output started at
Andrew
Aug 30th, 2009
10:39 am
Robert – You are outputting something before the line that has header(‘Content-type: text/xml’);. I’m guessing that you added some echo statements in the code for debugging purposes, or you have some whitespace before the opening PHP tag.
Sean
Mar 5th, 2010
9:57 am
I have this working in ASP.net, but I feel like the icons must be wrong because it sometimes shows nighttime icons for the day highs…. Am I somehow not matching these up right?
Here’s the page I’m referring to: http://www.boatpittsburgh.com
You can use the down arrow to expand the weather for the whole week… apparently on Monday and Tuesday it’s forecast to be a solar eclipse all day long… O.o
Andrew
Mar 7th, 2010
11:45 am
Sean-
That’s interesting. I had a similar issue because the arrays weren’t aligning properly, causing the days to be off, and the forecasts were flipped – showing day for night and night for day. It’s hard to say why you are seeing night for both. How are you processing the XML? I assume you are doing it in ASP.NET, but can you post how you are getting the data from the XML to an object to use?
Sean
Mar 10th, 2010
1:11 pm
Here’s some of the code… there’s a whole mess of it, and I dont’ want to clog your board with randomness, so I’ve stripped out that which I think is relevant…
‘ Find roots nodes for temperature and cloud data
Dim wtClouds As New WeatherTable()
wtClouds.nodeData = xmlDoc.SelectSingleNode(“/dwml/data/parameters/conditions-icon”)
‘ Find out corresponding time layout table for each top data node
wtClouds.nodeTimeLayout = FindLayoutTable(xmlDoc, wtClouds.nodeData)
FillCloudData(wtClouds, arrDayWeatherData, arrNightWeather)
arrDayWeatherData = New DayWeatherData(cTimes – 1) {}
arrNightWeather = New DayWeatherData(cTimes – 1) {}
Dim DR As DataRow
For N As Integer = 0 To arrDayWeatherData.Length – 1
DR = DT.NewRow
DR.Item(“WeatherDate”) = arrDayWeatherData(N).DateTime
DR.Item(“DayIcon”) = arrDayWeatherData(N).CloudIconURL
DR.Item(“DayHigh”) = arrDayWeatherData(N).HighTempF
If N < arrDayWeatherData.Length – 1 Then
DR.Item(“NightIcon”) = arrNightWeather(N).CloudIconURL
DR.Item(“NightLow”) = arrDayWeatherData(N).LowTempF
Else
DR.Item(“NightIcon”) = Nothing
DR.Item(“NightLow”) = Nothing
End If
DT.Rows.Add(DR)
DR = Nothing
Next
Private Shared Sub FillCloudData(ByVal wt As WeatherTable, ByRef arrDayWeatherData As DayWeatherData(), ByRef arrNightWeather As DayWeatherData())
‘ Cloud data is typically much longer than day high/low data
‘ We need to find times that match ones in high and low temp tables.
Dim listTimes As XmlNodeList = wt.nodeTimeLayout.SelectNodes(“start-valid-time”)
Dim listIcons As XmlNodeList = wt.nodeData.SelectNodes(“icon-link”)
Dim cWeatherData As Integer = 0
Dim cNodes As Integer = 0
Dim hourDiff As Integer = Int32.MaxValue
For Each node As XmlNode In listTimes
cNodes += 1
Dim dt As DateTime = ParseDateTime(node.InnerText)
If dt.[Date] > arrDayWeatherData(cWeatherData).DateTime.[Date] Then
cWeatherData += 1
If cWeatherData >= arrDayWeatherData.Length Then
Exit For
End If
hourDiff = Int32.MaxValue
End If
If dt.[Date] = arrDayWeatherData(cWeatherData).DateTime.[Date] Then
Dim diff As Integer = Math.Abs(dt.Hour – arrDayWeatherData(cWeatherData).DateTime.Hour)
If diff < hourDiff Then
hourDiff = diff
If cWeatherData < arrDayWeatherData.Length Then
arrDayWeatherData(cWeatherData).CloudIconURL = listIcons(cNodes).InnerText
End If
End If
End If
Next
cNodes = 0
cWeatherData = 0
For Each node As XmlNode In listTimes
cNodes += 1
Dim dt As DateTime = ParseDateTime(node.InnerText)
If dt.[Date] > arrNightWeather(cWeatherData).DateTime.[Date] Then
cWeatherData += 1
If cWeatherData >= arrNightWeather.Length Then
Exit For
End If
hourDiff = Int32.MaxValue
End If
If dt.[Date] = arrNightWeather(cWeatherData).DateTime.[Date] Then
If dt.Hour = arrNightWeather(cWeatherData).DateTime.Hour Then
Dim diff As Integer = Math.Abs(dt.Hour – arrNightWeather(cWeatherData).DateTime.Hour)
If diff < hourDiff Then
hourDiff = diff
If cWeatherData < arrNightWeather.Length Then
arrNightWeather(cWeatherData).CloudIconURL = listIcons(cNodes).InnerText
End If
End If
End If
End If
Next
End Sub
Private Shared Function FindLayoutTable(ByVal xmlDoc As XmlDocument, ByVal topDataNode As XmlNode) As XmlNode
Dim nlTimeLayouts As XmlNodeList = xmlDoc.SelectNodes(“/dwml/data/time-layout”)
Dim timeLayout As String = topDataNode.Attributes(“time-layout”).Value
For Each node As XmlNode In nlTimeLayouts
If timeLayout = node.SelectSingleNode(“layout-key”).InnerText Then
Return node
End If
Next
Return Nothing
End Function