Tracking the storm (with ColdFusion!)

This post is more than 2 years old.

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;

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

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

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

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

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

Archived Comments

Comment 1 by todd sharp posted on 8/28/2008 at 5:53 PM

So next step is to auto post to Twitter the distance, right? :)

Comment 2 by Brian Swartzfager posted on 8/28/2008 at 6:40 PM

Or put the data in a module on your blog at the very least. :)

But in all seriousness, that's pretty cool Ray. Nice work!

Comment 3 by Jonas Eriksson posted on 8/28/2008 at 6:42 PM

Awesome stuff Ray!
The CFlib to measure distance is very cool, gonna look into that myself one of these days.

Comment 4 by phill.nacelli posted on 8/28/2008 at 7:07 PM

Ok, so now we strap an gps device on Ray, take his coordinates and compare to the storm and create a "Watch Ray Run From the Storm" ColdFusion game! Who wants to write the "Ray's Odds of Survival" module?

Man I love my job!

On a more serious note, Ray I hope you and your family are not struck by this, stay safe buddy.

Cheers..

Comment 5 by Raymond Camden posted on 8/28/2008 at 7:15 PM

@phill: After Rita, trust me, I'm not staying around again.

Comment 6 by Mat Evans posted on 8/28/2008 at 7:19 PM

this is so asking for a mashup with google maps/live maps etc.. get an sms gateway running, it'll txt ya when you have to start running, and I reckon wouldn't be too hard to work out the direction you have to run in!

in all seriousness, I hope you guys stay safe.. in the UK we get hurricanes once in a blue moon and even then the weather men try to make us believe there isn't one coming :)

Comment 7 by Raymond Camden posted on 8/28/2008 at 9:53 PM

Holy crap, it's actually working.

8:32AM (I manually ran it): distance 4522.8
12:00PM : distance 4508.8

20 miles closer.

Comment 8 by phill.nacelli posted on 8/28/2008 at 9:55 PM

Nice.. now run!!!!!

Comment 9 by Dave DuPlantis posted on 8/28/2008 at 9:55 PM

Very nice!

I wonder, might you be able to simplify your regular expression by removing EAST and WEST from the LATITUDE portion and NORTH and SOUTH from the LONGITUDE portion?

Comment 10 by Raymond Camden posted on 8/28/2008 at 9:57 PM

I thought about that - but I like it there as it kinda makes it easier for me to read. Ie, I know that dir is there in the regex, so even though I don't use it, to me it makes the regex a bit tighter.

Comment 11 by Ken Auenson posted on 8/28/2008 at 10:11 PM

Ray,
I took your code and have been modifying it and playing with it a bit.
One of the changes I did make is to remove "CENTER OF TROPICAL STORM GUSTAV WAS LOCATED " from the RegEx, this still works.
I plan to spend some time tonight making a cool little app where you can search for a given zip and see the results with Google Maps. I will post that URL (with updated source) when I have it working.

Comment 12 by Joe Finan posted on 8/28/2008 at 10:30 PM

I also took the code, added a little text box/button to search for a given zip code... Now it's failing at "<cfif arrayLen(match.pos) is 5>", even in the original code, anyone else run into this problem?

Comment 13 by Raymond Camden posted on 8/28/2008 at 10:32 PM

If you cfdump match, what do you get?

Comment 14 by Joe Finan posted on 8/28/2008 at 10:37 PM

struct
LEN array
1 0

POS array
1 0

Comment 15 by Raymond Camden posted on 8/28/2008 at 10:46 PM

That doesn't make sense then. It's an array. The len isn't 5, which means it couldn't match, but the code should handle that.

Comment 16 by Joe Finan posted on 8/28/2008 at 10:55 PM

Sorry, maybe i worded that wrong... The code isn't failing, it's displaying the message "Couldn't find my matches in the string.". I was just wondering if it was something i changed, but it looks like the result from the rsslink is where the problem is.

Comment 17 by Raymond Camden posted on 8/28/2008 at 10:57 PM

Ah, same here. Let me see why my regex is failing.

Comment 18 by Raymond Camden posted on 8/28/2008 at 10:58 PM

Ah yes, the string changed. It's now:

THE CENTER OF TROPICAL STORM GUSTAV WAS LOCATED NEAR THE EASTERN TIP OF JAMAICA OR NEAR LATITUDE 17.9 NORTH...LONGITUDE 76.2 WEST

I'm thinking now to assume the first instance of LATITUDE X...LONGITUDE Y will be my search string.

Comment 19 by Raymond Camden posted on 8/28/2008 at 11:00 PM

Ok, regex line is below and still works. Again though, it assumes first instance of LONG... etc, so it's not "secure" I guess.

<cfset regex = "LATITUDE ([[:digit:]\.]+)[[:space:]]*([NORTH|SOUTH|EAST|WEST]+)...LONGITUDE ([[:digit:]\.]+)[[:space:]]*([NORTH|SOUTH|EAST|WEST]+)">

In case folks are curious: Distance is now 4505.8. 3 miles closer. RUN RAY RUN!

Comment 20 by Joe Finan posted on 8/28/2008 at 11:34 PM

I'm prolly doing something wrong again, but i added a zip code text box to change the location... I put in 60448 and it shows that i'm closer to GUSTAV, but I'm in Illinois!? Maybe i should start running, i'm only 3691.74 miles from Gustav...

Comment 21 by Raymond Camden posted on 8/29/2008 at 1:21 AM

I promise - no more comments till tomorrow morning:

4pm cst: distance:4496.3

Comment 22 by Raymond Camden posted on 8/30/2008 at 1:17 AM

FYI, you have to change the query of query now that GUSTAV is a hurricane. My QofQ is now:

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

Current miles: 4414.96

Comment 23 by Raymond Camden posted on 8/30/2008 at 1:33 AM

WOw. Two more bugs. The first one I can't believe I - and all you guys - missed. ;) Notice in my regex I match LAT then LONG, but my code was setting LONG than LAT. I had them reversed. You need to change regex IF block to:

<cfset lat = mid(text, match.pos[2], match.len[2])>
<cfset latdir = mid(text, match.pos[3], match.len[3])>
<cfset long = mid(text, match.pos[4], match.len[4])>
<cfset longdir = mid(text, match.pos[5], match.len[5])>

Ok, now the next problem is that NOAA uses direction in their results. Remember how I said I didn't need that? I was wrong. Turns out their longitude, when I use it for the math part and distance, needs to be negative.

I figure Gustav won't jump back across to the East so I just did this:

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

The good news is that now the miles is a lot more sensible to me. I thought 4k seemed rather high. Gustav is now only 1082.92 miles away. Sweet!

p.s. If anyone wants a clean copy of the script, let me know.

Comment 24 by Adam Tuttle posted on 9/6/2008 at 10:37 PM

I posted about some modifications I made to this code (including the corrections) here:

http://fusiongrokker.com/po...

My code also properly accounts for N/S/E/W to make the coordinates negative, and adds some caching (and cache refreshing) ability.

Comment 25 by Misty posted on 5/26/2014 at 11:06 AM

Hi Ray,

Offtopic; How we track the GPS on a Mobile Device with ColdFusion, I am looking for auch an API or Service or Something in CF

Any Idea, Clue

Regards

Comment 26 by Raymond Camden posted on 5/26/2014 at 6:10 PM

You can't. You can only track it client side. But once you have the GPS (using navigator.geolocation - https://developer.mozilla.o..., it is trivial to send that data to CF using an XHR hit.