Ok, so yesterday I joked a bit about Gustav (which, by the way, is now tracking closer to Lafayette) and about writing some ColdFusion code to track the hurricane. Being the OCD-natured kind of boy I am, I whipped up something in ColdFusion that seems to work well. Here is what I did...

I began by grabbing the feed data for Gustav:

<cfset gustavXML = "http://www.nhc.noaa.gov/nhc_at2.xml"> <cffeed source="#gustavXML#" query="results">

Next (and I should be clear, the full code is at the bottom and has more error checking in it) I used query of query to find the first entry with Tropical Storm GUSTAV Public Advisory Number in the title:

<!--- find "Public Advisory" ---> <cfquery name="pa" dbtype="query" maxrows="1"> select rsslink, content, title from results where title like 'Tropical Storm GUSTAV Public Advisory Number%' </cfquery>

NOAA's RSS feed entries don't have much content in them - they mainly just link to the full text, so I retrieve the content next:

<cfhttp url="#pa.rsslink#" result="result"> <cfset text = result.fileContent>

Ok, now comes the pain in the rear part. If the feed looks like I assume it will (and that's not a good assumption) then it will have this text:

CENTER OF TROPICAL STORM GUSTAV WAS LOCATED NEAR LATITUDE

So I wrote up a regex that attempts to find the longitude and latitude from the text:

<!--- strip extra white space ---> <cfset text = reReplace(text, "[\r\n]+", " ", "all")>

<cfset regex = "CENTER OF TROPICAL STORM GUSTAV WAS LOCATED NEAR LATITUDE ([[:digit:].]+)[[:space:]]([NORTH|SOUTH|EAST|WEST]+)...LONGITUDE ([[:digit:].]+)[[:space:]]([NORTH|SOUTH|EAST|WEST]+)">

<!--- now look for: THE CENTER OF TROPICAL STORM GUSTAV WAS LOCATED NEAR LATITUDE 19.1 NORTH...LONGITUDE 74.4 WEST ---> <cfset match = reFind(regex, text, 1, true)> <cfif arrayLen(match.pos) is 5> <cfset long = mid(text, match.pos[2], match.len[2])> <cfset longdir = mid(text, match.pos[3], match.len[3])> <cfset lat = mid(text, match.pos[4], match.len[4])> <cfset latdir = mid(text, match.pos[5], match.len[5])>

<cfoutput> Gustav Lat: #lat# #latdir#<br /> Gustav Long: #long# #longdir#<br /> </cfoutput>

So there isn't anything too complex in here. You can see I'm using subexpressions to grab the values I want. I get the directions too but they aren't really necessary since the numeric values tell you all you need. Again, ignore the fact that I have a missing <cfelse> there - I'm just skipping over the error handling for now.

Ok, so that was the hard part. Now I need the longitude and latitude for Lafayette. I could use the Yahoo Weather API for this. The weather results include longitude and latitude information. Since they use a simple REST interface (hey Google, Yahoo called, they want to talk about writing APIs that developers actually enjoy using) I could have just opened up the feed in my browser and copied the values. Instead though I made it dynamic:

<cfset zip = 70508> <cfoutput><p/>Getting #zip#<br /></cfoutput> <cfflush> <cfhttp url="http://weather.yahooapis.com/forecastrss?p=#zip#" result="result"> <cfset content = xmlParse(result.fileContent)> <cfset geo = xmlSearch(content, "//geo:*")> <cfloop index="g" array="#geo#"> <cfif g.xmlName is "geo:lat"> <cfset myLat = g.xmlText> <cfelseif g.xmlName is "geo:long"> <cfset myLong = g.xmlText> </cfif> </cfloop>

This is fairly typical XML parsing using xmlSearch. I could have used my CFYahoo package but I wanted a quick and dirty script.

Ok, now all the hard work is done. Serious! All I need to do is get a function to generate the distance between two zips. Once again, CFLib comes to the rescue: LatLonDist. So the last portion is rather simple:

<cfset distance = latLonDist(lat,long,myLat,myLong,"sm")>

<cfoutput><p/><b>Distance:</b> #numberFormat(distance,"9.99")# miles</cfoutput>

Surprisingly, the code actually works, and worked fine from last night till tonight. My main concern is the string parsing, but so far, so good. Now for the real fun part. I added some simple logging to the script. I'm going to use my localhost scheduler to run the script every 4 hours. I'll then write a script to parse the log and see how the distance changes between now and Tuesday, or as I call it, Get the Hell Out of Dodge Day.

Anyway, here is the final script with error handling, logging, etc. Enjoy.

<cfscript> /** * Calculates the distance between two latitudes and longitudes. * This funciton uses forumlae from Ed Williams Aviation Foundry website at http://williams.best.vwh.net/avform.htm. * * @param lat1 Latitude of the first point in degrees. (Required) * @param lon1 Longitude of the first point in degrees. (Required) * @param lat2 Latitude of the second point in degrees. (Required) * @param lon2 Longitude of the second point in degrees. (Required) * @param units Unit to return distance in. Options are: km (kilometers), sm (statute miles), nm (nautical miles), or radians. (Required) * @return Returns a number or an error string. * @author Tom Nunamaker (tom@toshop.com) * @version 1, May 14, 2002 */ function LatLonDist(lat1,lon1,lat2,lon2,units) { // Check to make sure latitutdes and longitudes are valid if(lat1 GT 90 OR lat1 LT -90 OR lon1 GT 180 OR lon1 LT -180 OR lat2 GT 90 OR lat2 LT -90 OR lon2 GT 280 OR lon2 LT -280) { Return ("Incorrect parameters"); }

lat1 = lat1 * pi()/180; lon1 = lon1 * pi()/180; lat2 = lat2 * pi()/180; lon2 = lon2 * pi()/180; UnitConverter = 1.150779448; //standard is statute miles if(units eq 'nm') { UnitConverter = 1.0; }

if(units eq 'km') { UnitConverter = 1.852; }

distance = 2*asin(sqr((sin((lat1-lat2)/2))^2 + cos(lat1)cos(lat2)(sin((lon1-lon2)/2))^2)); //radians

if(units neq 'radians'){ distance = UnitConverter * 60 * distance * 180/pi(); }

Return (distance) ; }

</cfscript> <!--- quickie log func ---> <cffunction name="logit" output="false" returnType="void"> <cfargument name="str" type="string" required="true"> <cflog file="gustav" text="#arguments.str#"> </cffunction>

<!--- Lafayette, LA ---> <cfset zip = 70508>

<cfset gustavXML = "http://www.nhc.noaa.gov/nhc_at2.xml"> <cffeed source="#gustavXML#" query="results">

<cfif not results.recordCount> <cfset logit("Error - no feed entries")> <cfoutput>No feed entries.</cfoutput> <cfabort> </cfif>

<!--- find "Public Advisory" ---> <cfquery name="pa" dbtype="query" maxrows="1"> select rsslink, content, title from results where title like 'Tropical Storm GUSTAV Public Advisory Number%' </cfquery>

<cfif not pa.recordCount> <cfset logit("Error - cound't find a matching entry")> <cfoutput>Couldn't find an entry that matched my criteria.</cfoutput> <cfabort> </cfif>

<cfoutput> Parsing data from: <b>#pa.title#</b><br /> </cfoutput> <cfflush /> <cfhttp url="#pa.rsslink#" result="result"> <cfset text = result.fileContent>

<!--- strip extra white space ---> <cfset text = reReplace(text, "[\r\n]+", " ", "all")>

<cfset regex = "CENTER OF TROPICAL STORM GUSTAV WAS LOCATED NEAR LATITUDE ([[:digit:].]+)[[:space:]]([NORTH|SOUTH|EAST|WEST]+)...LONGITUDE ([[:digit:].]+)[[:space:]]([NORTH|SOUTH|EAST|WEST]+)">

<!--- now look for: THE CENTER OF TROPICAL STORM GUSTAV WAS LOCATED NEAR LATITUDE 19.1 NORTH...LONGITUDE 74.4 WEST ---> <cfset match = reFind(regex, text, 1, true)> <cfif arrayLen(match.pos) is 5> <cfset long = mid(text, match.pos[2], match.len[2])> <cfset longdir = mid(text, match.pos[3], match.len[3])> <cfset lat = mid(text, match.pos[4], match.len[4])> <cfset latdir = mid(text, match.pos[5], match.len[5])>

<cfoutput> Gustav Lat: #lat# #latdir#<br /> Gustav Long: #long# #longdir#<br /> </cfoutput> <cfelse> <cfset logit("Error - couldn't find my matches in the string")> <cfoutput>Couldn't find my matches in the string.</cfoutput> <cfabort> </cfif>

<cfoutput><p/>Getting #zip#<br /></cfoutput> <cfflush> <cfhttp url="http://weather.yahooapis.com/forecastrss?p=#zip#" result="result"> <cfset content = xmlParse(result.fileContent)> <cfset geo = xmlSearch(content, "//geo:*")> <cfloop index="g" array="#geo#"> <cfif g.xmlName is "geo:lat"> <cfset myLat = g.xmlText> <cfelseif g.xmlName is "geo:long"> <cfset myLong = g.xmlText> </cfif> </cfloop>

<!--- only continue if we have our own stuff ---> <cfif not isDefined("myLat") or not isDefined("myLong")> <cfset logit("Error - no geo data for #zip#")> <cfoutput>No data for #zip#</cfoutput> <cfabort /> </cfif>

<cfoutput> #zip# lat: #myLat#<br /> #zip# long: #myLong#<br /> </cfoutput>

<cfset distance = latLonDist(lat,long,myLat,myLong,"sm")>

<cfset logit("distance:#distance#")> <cfoutput><p/><b>Distance:</b> #numberFormat(distance,"9.99")# miles</cfoutput>