Tracking the storm (with ColdFusion!)

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])>

&lt;cfoutput&gt;
Gustav Lat: #lat# #latdir#&lt;br /&gt;
Gustav Long: #long# #longdir#&lt;br /&gt;
&lt;/cfoutput&gt; </code>

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> </code>

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 = 2asin(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])>

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

<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> </code>

Raymond Camden's Picture

About Raymond Camden

Raymond is a developer advocate. He focuses on JavaScript, serverless and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support.

Lafayette, LA https://www.raymondcamden.com

Comments