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>
Archived Comments
So next step is to auto post to Twitter the distance, right? :)
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!
Awesome stuff Ray!
The CFlib to measure distance is very cool, gonna look into that myself one of these days.
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..
@phill: After Rita, trust me, I'm not staying around again.
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 :)
Holy crap, it's actually working.
8:32AM (I manually ran it): distance 4522.8
12:00PM : distance 4508.8
20 miles closer.
Nice.. now run!!!!!
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?
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.
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.
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?
If you cfdump match, what do you get?
struct
LEN array
1 0
POS array
1 0
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.
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.
Ah, same here. Let me see why my regex is failing.
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.
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!
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...
I promise - no more comments till tomorrow morning:
4pm cst: distance:4496.3
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
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.
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.
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
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.