Yesterday I blogged about a proof of concept 911 viewer I built using CFMap and jQuery. The first example simply retrieved all of the 911 reports and mapped them at once. The second demo was more complex. This demo actually showed you map data from the beginning of the collection to the most recent report. Watch the video if that doesn't make sense. Let's look at how I built that demo. (Warning: I'm going to jump around a bit code wise but at the end I'll paste the entire template.)

I began with a map centered on my home town:

<cfmap centeraddress="Lafayette, LA" width="500" height="500" zoomlevel="12" showcentermarker="false" name="mainMap"> </cfmap>

I then created a DIV that would store the current time:

<div id="clock"></div>

Next - I needed to tell ColdFusion to run a JavaScript function when everything (in this case, the map) was loaded. The last line of my script was:

<cfset ajaxOnLoad("init")>

This ran my init function. It's main purpose was to just set things up and run my main (and repeatable) function to display data.

function init() { map = ColdFusion.Map.getMapObject("mainMap") doMarkers() }

I'm using map there as a global variable. Remember that this is the "real" Google Map API object. When I have that I can do anything Google allows via their API. Let's look at doMarkers now.

function doMarkers() {

if(prevMarkers.length) { for(var i=0;i<prevMarkers.length;i++) { map.removeOverlay(prevMarkers[i]) } prevMarkers = [] }

current++ var thisDate = bucketArray[current]["date"] console.log(thisDate + ' has '+bucketArray[current].records.length +' items') $("#clock").text(thisDate) for(var i=0; i<bucketArray[current]["records"].length;i++) { var point = new GLatLng(bucketArray[current].records[i]["latitude"],bucketArray[current].records[i]["longitude"]) var icon = getIcon(bucketArray[current].records[i]["type"])

var iconOb = new GIcon(G_DEFAULT_ICON); if(icon != "") iconOb.image = icon; var marker = new GMarker(point, {icon:iconOb}) prevMarkers[prevMarkers.length]=marker map.addOverlay(marker) }

if(current+1 < bucketArray.length) window.setTimeout("doMarkers()", 1000)

}

Ok, a lot going on here. I begin by seeing if I have previous markers to delete. This will be true on the second iteration and will be true until the "viewer" is done. I'll end up storing my pointers in an array so I simply loop over that and run removeOverlay().

Next I work with a data structure called bucketArray. I'm going to explain how that is built later, but for now, just know it stores a date/time value and a list of 911 data. This is already ordered by time already, so as we go through the array we are going through time. You can see where I update the time div. Then I loop through my data. For each I figure out if I need a custom icon, and if so, I set it up. I then add the marker to the map being sure to store the result in my prevMarkers array. The last line simply calls out to itself to run the next block of data.

My getIcon function just translates the 911 "type" into a custom icon:

function getIcon(s) { s = s.toLowerCase() if(s.indexOf("vehicle accident") >= 0) return "icons/car.png"; if(s.indexOf("stalled vehicle") >= 0) return "icons/car.png"; if(s.indexOf("traffic control") >= 0) return "icons/stop.png"; if(s.indexOf("traffic signal") >= 0) return "icons/stop.png"; if(s.indexOf("fire") >= 0) return "icons/fire.png"; if(s.indexOf("hazard") >= 0) return "icons/hazard.png"; return ""; }

I could update this as more types of emergencies occur. I'm still waiting for "Giant Monster" or "Alien Invasion." For the most part, that's the gist of the JavaScript. So how did I create my data? I began with a query:

<cfquery name="getdata"> select longitude, latitude, type, incidenttime from data where (longitude !='' and latitude != '') <!--- fixes one bad row ---> and longitude < -88 and incidenttime is not null order by incidenttime asc </cfquery>

I then figure out the range of my data:

<!--- generate range of buckets ---> <cfset earliest = getdata.incidenttime[1]> <cfset latest = getdata.incidenttime[getdata.recordcount]> <cfset earliest = dateFormat(earliest, "m/d/yy") & " " & timeFormat(earliest, "H") & ":00"> <cfset latest = dateFormat(latest, "m/d/yy") & " " & timeFormat(latest, "H") & ":00">

Next I initialize my bucketArray:

<cfset bucketArray = []> <cfloop from="#earliest#" to="#latest#" index="x" step="#createTimeSpan(0,1,0,0)#"> <cfset arrayAppend(bucketArray, {date="#dateformat(x)# #timeformat(x)#"})> </cfloop> <!--- add one to top ---> <cfset toAppend = dateAdd("h", 1, bucketArray[arrayLen(bucketArray)].date)> <cfset arrayAppend(bucketArray, {date="#dateFormat(toAppend)# #timeFormat(toAppend)#"})> <!--- i'm pretty sure it is safe to do this ---> <cfset arrayDeleteAt(bucketArray, 1)>

So what's with the 'add one to top' and 'delete the bottom'? My thinking here was - if my last 911 report was for 5:14PM, I wanted to have this reported as 6PM. Every time value you see represents all the values for the last hour. By the same token, if my first bucket item is 1PM, it really means an item for 1:Something PM. Therefore I can drop that as well.

The final step was to populate the data:

<cfloop index="b" array="#bucketArray#"> <cfset prev = dateAdd("h", -1, b.date)> <cfquery name="getInBucket" dbtype="query"> select * from getdata where incidenttime >= <cfqueryparam cfsqltype="cf_sql_timestamp" value="#prev#"> and incidenttime < <cfqueryparam cfsqltype="cf_sql_timestamp" value="#b.date#"> </cfquery> <cfset b.records = getInBucket> </cfloop>

To be honest, I bet I could do this all in one cfquery, probably by simply reformatting the date. Either way - it worked. So how did I get this bucketArray into JavaScript? It was incredibly difficult:

<cfoutput> var #toScript(bucketArray,"bucketArray",false)#; </cfoutput>

Yeah, cry over that PHP boys. Anyway, here is the complete CFM file. I'll remind folks that I wrote this very quickly, so please forgive any obvious dumb mistakes.

<cfquery name="getdata"> select longitude, latitude, type, incidenttime from data where (longitude !='' and latitude != '') <!--- fixes one bad row ---> and longitude < -88 and incidenttime is not null order by incidenttime asc </cfquery>

<!--- generate range of buckets ---> <cfset earliest = getdata.incidenttime[1]> <cfset latest = getdata.incidenttime[getdata.recordcount]> <cfset earliest = dateFormat(earliest, "m/d/yy") & " " & timeFormat(earliest, "H") & ":00"> <cfset latest = dateFormat(latest, "m/d/yy") & " " & timeFormat(latest, "H") & ":00">

<cfset bucketArray = []> <cfloop from="#earliest#" to="#latest#" index="x" step="#createTimeSpan(0,1,0,0)#"> <cfset arrayAppend(bucketArray, {date="#dateformat(x)# #timeformat(x)#"})> </cfloop> <!--- add one to top ---> <cfset toAppend = dateAdd("h", 1, bucketArray[arrayLen(bucketArray)].date)> <cfset arrayAppend(bucketArray, {date="#dateFormat(toAppend)# #timeFormat(toAppend)#"})> <!--- i'm pretty sure it is safe to do this ---> <cfset arrayDeleteAt(bucketArray, 1)>

<!--- <cfloop index="index" from="1" to="#arrayLen(bucketArray)#"> ---> <cfloop index="b" array="#bucketArray#"> <cfset prev = dateAdd("h", -1, b.date)> <cfquery name="getInBucket" dbtype="query"> select * from getdata where incidenttime >= <cfqueryparam cfsqltype="cf_sql_timestamp" value="#prev#"> and incidenttime < <cfqueryparam cfsqltype="cf_sql_timestamp" value="#b.date#"> </cfquery> <cfset b.records = getInBucket> </cfloop> <!--- <cfdump var="#bucketarray#"> --->

<html>

<head> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script> <script> <cfoutput> var #toScript(bucketArray,"bucketArray",false)#; </cfoutput>

var current = -1; var mainHB; var prevMarkers = [] var map;

function getIcon(s) { s = s.toLowerCase() if(s.indexOf("vehicle accident") >= 0) return "icons/car.png"; if(s.indexOf("stalled vehicle") >= 0) return "icons/car.png"; if(s.indexOf("traffic control") >= 0) return "icons/stop.png"; if(s.indexOf("traffic signal") >= 0) return "icons/stop.png"; if(s.indexOf("fire") >= 0) return "icons/fire.png"; if(s.indexOf("hazard") >= 0) return "icons/hazard.png"; return ""; }

function doMarkers() {

if(prevMarkers.length) { for(var i=0;i<prevMarkers.length;i++) { map.removeOverlay(prevMarkers[i]) } prevMarkers = [] }

current++ var thisDate = bucketArray[current]["date"] console.log(thisDate + ' has '+bucketArray[current].records.length +' items') $("#clock").text(thisDate) for(var i=0; i<bucketArray[current]["records"].length;i++) { var point = new GLatLng(bucketArray[current].records[i]["latitude"],bucketArray[current].records[i]["longitude"]) var icon = getIcon(bucketArray[current].records[i]["type"])

var iconOb = new GIcon(G_DEFAULT_ICON); if(icon != "") iconOb.image = icon; var marker = new GMarker(point, {icon:iconOb}) prevMarkers[prevMarkers.length]=marker map.addOverlay(marker) }

if(current+1 < bucketArray.length) window.setTimeout("doMarkers()", 1000)

}

function init() { map = ColdFusion.Map.getMapObject("mainMap") doMarkers() } </script> <style> #clock { font-weight:bold; font-size: 40px; font-family: "Courier New", Courier, monospace; } </style> </head>

<body>

<cfmap centeraddress="Lafayette, LA" width="500" height="500" zoomlevel="12" showcentermarker="false" name="mainMap"> </cfmap>

<div id="clock"></div> </body> </html>

<cfset ajaxOnLoad("init")>