SOAP XML Transform – NWS Forecast

S

The National Digital Forecast Database is a SOAP web service distributing U.S. forecast data via XML. I recently examined the output with an eye toward incorporating a basic forecast component into a webpage, but found it rather difficult to use directly. The purpose of this post is to share the process I used to transform the XML into an easier to use format.

An example of the format returned by my transformation is reproduced below.  To see an example of the original XML format returned by the service use the tool at http://graphical.weather.gov/xml/SOAP_server/ndfdSOAPByDay.htm.

<?xml version="1.0" standalone="yes"?>
<my-forecast>
	<my-period start="2015-05-25T06:00:00-05:00" end="2015-05-25T18:00:00-05:00" name="Memorial Day">
		<my-conditions>Cloudy</my-conditions>
		<my-conditions-icon>http://www.nws.noaa.gov/weather/images/fcicons/ovc.jpg</my-conditions-icon>
		<my-temp type="max">64</my-temp>
	</my-period>
	<my-period start="2015-05-25T18:00:00-05:00" end="2015-05-26T06:00:00-05:00" name="Monday Night">
		<my-conditions>Patchy Fog</my-conditions>
		<my-conditions-icon>http://www.nws.noaa.gov/weather/images/fcicons/nfg.jpg</my-conditions-icon>
		<my-temp type="min">52</my-temp>
	</my-period>
	<my-period start="2015-05-26T06:00:00-05:00" end="2015-05-26T18:00:00-05:00" name="Tuesday">
		<my-conditions>Cloudy</my-conditions>
		<my-conditions-icon>http://www.nws.noaa.gov/weather/images/fcicons/ovc.jpg</my-conditions-icon>
		<my-temp type="max">72</my-temp>
	</my-period>
	<my-period start="2015-05-26T18:00:00-05:00" end="2015-05-27T06:00:00-05:00" name="Tuesday Night">
		<my-conditions>Mostly Cloudy</my-conditions>
		<my-conditions-icon>http://www.nws.noaa.gov/weather/images/fcicons/nbkn.jpg</my-conditions-icon>
		<my-temp type="min">55</my-temp>
	</my-period>
	<my-period start="2015-05-27T06:00:00-05:00" end="2015-05-27T18:00:00-05:00" name="Wednesday">
		<my-conditions>Partly Sunny</my-conditions>
		<my-conditions-icon>http://www.nws.noaa.gov/weather/images/fcicons/sct.jpg</my-conditions-icon>
		<my-temp type="max">81</my-temp>
	</my-period>
	<my-period start="2015-05-27T18:00:00-05:00" end="2015-05-28T06:00:00-05:00" name="Wednesday Night">
		<my-conditions>Partly Cloudy</my-conditions>
		<my-conditions-icon>http://www.nws.noaa.gov/weather/images/fcicons/nsct.jpg</my-conditions-icon>
		<my-temp type="min">58</my-temp>
	</my-period>
</my-forecast>

 

The PHP code retrieving the original XML and returning the transformed version is reproduced below. A few notes are in order. First, you may note that E_NOTICE is suppressed. This is because it is possible that some elements may be empty and not return any data. Second, the xpath() method of the SimpleXML object returns an array of SimpleXML objects, of which we will generally want the 0th element. As a matter of convenience it is being retrieved via the list($someVar) construct. Finally, there is a lot of implicit casting going on as the xpath() method is cast to a string containing the element’s text.

<?php
ini_set('display_errors', true);
error_reporting(E_ALL ^ E_NOTICE);

$client = new SoapClient("http://graphical.weather.gov/xml/DWMLgen/wsdl/ndfdXML.wsdl");
$response = $client->NDFDgenByDay(new SoapParam(45.3417, 'latitude'),
				new SoapParam(-93.6974, 'longitude'),
				new SoapParam('', 'start_date'),
				new SoapParam(3, 'num_days'),
				new SoapParam('e', 'Unit'),
				new SoapParam('12 hourly', 'format'));

$xml = new SimpleXMLElement($response);

$myForecast = new SimpleXMLElement("<?xml version='1.0' standalone='yes'?><my-forecast></my-forecast>");
list($weatherNode) = $xml->xpath('/dwml/data/parameters/weather');
list($weatherNodeTimeLayoutKey) = $weatherNode->xpath('@time-layout');
list(,$periodCount,) = sscanf($weatherNodeTimeLayoutKey, 'k-p%dh-n%d-%d');
list($timeLayoutWeather) = $xml->xpath("/dwml/data/time-layout/layout-key[text()='$weatherNodeTimeLayoutKey']/..");

for($i=1; $i<=$periodCount; $i++){
	list($timePeriodStart) = $timeLayoutWeather->xpath("start-valid-time[$i]");
	list($timePeriodName) = $timeLayoutWeather->xpath("start-valid-time[$i]/@period-name");
	list($timePeriodEnd) = $timeLayoutWeather->xpath("end-valid-time[$i]");
	list($conditions) = $weatherNode->xpath("weather-conditions[$i]/@weather-summary");
	list($conditionsIcon) = $xml->xpath("/dwml/data/parameters/conditions-icon/icon-link[$i]");
	$myPeriod = $myForecast->addChild('my-period');
	$myPeriod->addAttribute('start', $timePeriodStart);
	$myPeriod->addAttribute('end', $timePeriodEnd);
	$myPeriod->addAttribute('name', $timePeriodName);
	$myPeriod->addChild('my-conditions', $conditions);
	$myPeriod->addChild('my-conditions-icon', $conditionsIcon);
}

//Add Max Temperatures...
list($maxTempNode) = $xml->xpath("/dwml/data/parameters/temperature[@type='maximum']");
list($maxTempNodeTimeLayoutKey) = $maxTempNode->xpath('@time-layout');
list(,$periodCount,) = sscanf($maxTempNodeTimeLayoutKey, 'k-p%dh-n%d-%d');
list($timeLayoutMaxTemp) = $xml->xpath("/dwml/data/time-layout/layout-key[text()='$maxTempNodeTimeLayoutKey']/..");

for($i=1; $i<=$periodCount; $i++){
	list($timeLayoutStartValidTime) = $timeLayoutMaxTemp->xpath("start-valid-time[$i]");
	list($maxTemp) = $maxTempNode->xpath("value[$i]");
	list($periodNode) = $myForecast->xpath("my-period[@start='$timeLayoutStartValidTime']");
	$tempNode = $periodNode->addChild('my-temp', $maxTemp);
	$tempNode->addAttribute('type', 'max');
}

//Add Min Temperatures...
list($minTempNode) = $xml->xpath("/dwml/data/parameters/temperature[@type='minimum']");
list($minTempNodeTimeLayoutKey) = $minTempNode->xpath('@time-layout');
list(,$periodCount,) = sscanf($minTempNodeTimeLayoutKey, 'k-p%dh-n%d-%d');
list($timeLayoutMinTemp) = $xml->xpath("/dwml/data/time-layout/layout-key[text()='$minTempNodeTimeLayoutKey']/..");

for($i=1; $i<=$periodCount; $i++){
	list($timeLayoutStartValidTime) = $timeLayoutMinTemp->xpath("start-valid-time[$i]");
	list($minTemp) = $minTempNode->xpath("value[$i]");
	list($periodNode) = $myForecast->xpath("my-period[@start='$timeLayoutStartValidTime']");
	$tempNode = $periodNode->addChild('my-temp', $minTemp);
	$tempNode->addAttribute('type', 'min');
}

//Save to a file
$myForecast->asXML('my_forecast.xml');
?>

 

At a high level, the code begins by matching the time-layout key from the <weather> element to the time-layout key of the corresponding <time-layout> element.  The key itself is parsed by the sscanf() function in line 18 to retrieve the number of elements to be processed.  Both elements are then iterated in lock-step by the for loop, generating the transformed XML.  Note that the same time-layout key matches the <conditions-icon> element and is also processed therein.

The <temperature> elements are processed in a similar manner, with the added feature of being mapped to the appropriate period in the new XML.  Note that the maximum and minimum types processed separately as they both have independent <time-layout> elements.

Hopefully, this code will be useful to others in transforming this rather unfriendly XML into a format more amenable to simple applications.

About the author

Chris Peterson

As a Web Application Developer & Elephant Trainer I have been putting the PHP mascot to work for more than a decade. I specialize in back-end development and use the LAMP stack to craft software that frees human beings to spend their time on more productive and rewarding things.

By Chris Peterson

Chris Peterson

As a Web Application Developer & Elephant Trainer I have been putting the PHP mascot to work for more than a decade. I specialize in back-end development and use the LAMP stack to craft software that frees human beings to spend their time on more productive and rewarding things.

Recent Posts

Recent Comments

Archives