/**
* Word on the Street
*
* description
*   - A widget to display all of the elected Congressional officials for the user's current location (within the US)
*
* Copyright (c) 2009 Klokie <klokie@klokie.com>
*
* Version 1.0.0
*
* Dual licensed under the MIT and GPL licenses:
*   http://www.opensource.org/licenses/mit-license.php
*   http://www.gnu.org/licenses/gpl.html
*
* Usage:
*   include the following lines to embed the widget in your html or blog code:
*
*   <script src="http://www.google.com/jsapi?key=__GOOGLE_KEY__" type="text/javascript"></script>
*   <style type="text/css">@import "http://klokie.com/widgets/wots/css/style.css";</style>
*   <script type="text/javascript" src="http://klokie.com/widgets/wots/js/wots.js"></script>
*
* Changes:
*   now sorting all posts with most recent at the top (thanks, tsort!)
*   geoCoding more defensively 
*   caching some of the results at the district level
*/
/**********************************************
* GLOBAL OBJECTS
***********************************************/
WOTS = {
	//$ :window.jQuery.noConflict ( ),
	currentLocation : { 'lat': null, 'lng': null, 'state': null, 'country': null, 'district': null },
	services : { 
		'sunlightlabs' : { 'base': 'http://services.sunlightlabs.com/api/', 'key' : '7cee4f7a4f97912fb4b6e3e03b569582',
			'methods': { 
				'getDistrict' : 'districts.getDistrictFromLatLong.json?latitude={LAT}&longitude={LONG}&apikey={KEY}'
				,'getLegislators': 'legislators.getList.json?state={state}&district={district}&apikey={KEY}'
			} }
		,'opensecrets' : { 'base': 'http://www.opensecrets.org/api/?', 'key' : 'fa1b3fd43e230da643cf944b7bdced1b' }
		,'images' : { 'base': 'http://www.opensecrets.org/politicians/img/pix/' }
		,'topwords' : { 'base': 'http://capitolwords.org/api/',
			'methods' : {
				'top5' : 'lawmaker/{bioguide_id}/latest/top5.json'
			} }
	},
	
	cache : {}, // cache container for RSS/JSON
	
	profileOpenHTML : '<div class="person hcard party-{party} status-{in_office} state-{state}"><div class="photo">'
		+'<a href="{website}" class="url website" rel="nofollow" title="{title} {firstname} {middlename} {lastname} {suffix}">{photo}</a>'
		+'</div>'
	,profileCloseHTML : '</div>'
	,profileHTML : {
 		'contact' : {
			'phone' : '<label for="tel-{crp_id}">tel:</label> '
				+'<a href="callto:{phone}" class="tel" id="tel-{crp_id}" title="Congressional office phone number">{phone}</a>'
			,'fax' : '<label for="fax-{crp_id}">fax:</label> '
				+'<a class="fax" id="fax-{crp_id}" title="Congressional office fax number">{fax}</a>'
			,'email' : '<label for="email-{crp_id}">net:</label> '
				+'<a href="mailto:{email}" class="email" id="email-{crp_id}" title="Legislator\'s email address">email</a>'
			,'webform' : '<span class="separator">&nbsp;✭&nbsp;</span><label class="online" for="www-{crp_id}">net:</label> '
				+'<a href="{webform}" rel="nofollow" class="webform" id="www-{crp_id}" title="URL of web contact form">form</a>'
		}
		,'links' : {
			'website' : '<a href="{website}" class="url website" rel="nofollow" title="URL of Congressional website">{website}</a>'
			,'twitter_id' : '<a href="http://twitter.com/{twitter_id}" rel="nofollow" class="twitter_id" title="Congressperson\'s official Twitter account">Twitter user &ldquo;{twitter_id}&rdquo;</a>'
			//,'youtube_url' : '<a href="{youtube_url}" class="youtube_url" title="Congressperson\'s official Youtube account">YouTube</a>'
			,'congresspedia_url' : '<a rel="nofollow" href="{congresspedia_url}" class="congresspedia_url" title="URL of Legislator\'s entry in Congresspedia">Congresspedia</a>'
			,'bioguide_id' : '<a rel="nofollow" href="http://bioguide.congress.gov/scripts/biodisplay.pl?index={bioguide_id}" class="bioguide_url" title="URL of Legislator\'s entry in the Congressional Biographical Directory">Congressional Biographical Directory</a>'
			,'votesmart_id' : '<a rel="nofollow" href="http://votesmart.org/bio.php?can_id={votesmart_id}" class="votesmart_url" title="URL of Legislator\'s entry in the Project Vote Smart">Project Vote Smart</a>'
			,'govtrack_id' : '<a rel="nofollow" href="http://www.govtrack.us/congress/person.xpd?id={govtrack_id}" class="govtrack_url" title="URL of Legislator\'s entry on Govtrack.us">Govtrack.us</a>'
			,'crp_id' : '<a rel="nofollow" href="http://www.opensecrets.org/politicians/summary.php?cid={crp_id}" class="crp_url" title="URL of Legislator\'s entry on the Center for Responsive Politics">Center for Responsive Politics</a>'
			//,'eventful_id' : '<a rel="nofollow" href="http://eventful.com/performers/{eventful_id}" class="eventful_url" title="URL of Legislator\'s entry on the Eventful.com">Eventful.com</a>'
		}
	},
	
	// Google Maps objects
	GMap : {},
	geocoder : {},

	/**
	* message logging function
	*/
	log : function() {
		if (typeof console != 'undefined') console.log(WOTS.log.arguments);
		var msg = WOTS.serialize($.makeArray(arguments));
		$('#WOTSabout').html(msg);
	},
		
	serialize : function (obj) {
		var out = '';
		if (typeof obj === 'string') {
			out += obj;
		} else {
			for (var i in obj) { 
				if (out.length) out += '\n';
				if (i == 0) out += WOTS.serialize(obj[i]);
				else out += i+': '+WOTS.serialize(obj[i]);
			}
		}
		return out;
	},

	/**
	* display the google map
	*/
	map : function(point) {
		if (!WOTS.GMap.isLoaded()) {
			if (!point) {
				// display the map and center to current location
				point = new google.maps.LatLng(google.loader.ClientLocation.latitude, google.loader.ClientLocation.longitude);
			}
			var customUI = WOTS.GMap.getDefaultUI();
			customUI.controls.menumaptypecontrol = false;
			WOTS.GMap.setUI(customUI);
			if (point) WOTS.GMap.setCenter(point, 10);
		}
		
		if (point) WOTS.showSelection( point );			

		$('#WOTSmap:hidden,#WOTScontrols:hidden').show('blind');
		//$('#WOTSaddressInput').focus();
	},
	
	/**
	* display the "about" section
	*/
	about : function() {
		if ( $('#WOTSabout .description').toggle('blind').length == 0) {
			$('#WOTSheader .info .description').clone().prependTo('#WOTSabout').show('blind');
		}
	},
	
	/**
	* loadFromHistory
	* get history entries from #hash links and query string
	* @param hash String
	*/
	loadFromHistory : function (hash) {
		var q = location.search.match( /q=([^&\?=]+)/ );
		var params = hash ? hash.replace(/^WOTS/, '').split(/:/) : [];

		if(params.length > 0 && typeof WOTS[params[0]] == 'function') {
			// restore ajax loaded state
			if (params.length > 2) WOTS[params[0]]( params[2], params[1] );
			else WOTS[params[0]]( params[1] );

			$('#WOTSheader .menu .map').show();
			
			return ;
		} else if (location.search && q) {
			WOTS.log('looking for: ' + unescape(q[1]));
			WOTS.findAddress( unescape(q[1]) );

		} else {
			WOTS.log('<strong class="progress">Determining current location&hellip;</strong>');
		
			// display the map and center to current location
			var point = new google.maps.LatLng(google.loader.ClientLocation.latitude, google.loader.ClientLocation.longitude);			
			WOTS.map( point );			
		}

		if (WOTS.currentLocation.state ) {
			WOTS.renderLegislatorsForLocation(WOTS.currentLocation);
		}
	},

	/**
	* main
	*/
	main : function() {
		/*
		* jQuery TinySort 1.0.2
		* Copyright (c) 2008 Ron Valstar
		* Dual licensed under the MIT and GPL licenses:
		*   http://www.opensource.org/licenses/mit-license.php
		*   http://www.gnu.org/licenses/gpl.html
		*/
		(function(B){B.tinysort={id:"TinySort",version:"1.0.2",defaults:{order:"asc",attr:"",place:"start",returns:false}};B.fn.extend({tinysort:function(H,I){if(H&&typeof (H)!="string"){I=H;H=null}var E=B.extend({},B.tinysort.defaults,I);var O={};this.each(function(S){var U=(!H||H=="")?B(this):B(this).find(H);var T=E.order=="rand"?""+Math.random():(E.attr==""?U.text():U.attr(E.attr));var R=B(this).parent();if(!O[R]){O[R]={s:[],n:[]}}if(U.length>0){O[R].s.push({s:T,e:B(this),n:S})}else{O[R].n.push({e:B(this),n:S})}});for(var G in O){var D=O[G];D.s.sort(function J(T,S){var R=T.s.toLowerCase?T.s.toLowerCase():T.s;var U=S.s.toLowerCase?S.s.toLowerCase():S.s;if(C(T.s)&&C(S.s)){R=parseFloat(T.s);U=parseFloat(S.s)}return(E.order=="asc"?1:-1)*(R<U?-1:(R>U?1:0))})}var L=[];for(var G in O){var D=O[G];var M=[];var F=B(this).length;switch(E.place){case"first":B.each(D.s,function(R,S){F=Math.min(F,S.n)});break;case"org":B.each(D.s,function(R,S){M.push(S.n)});break;case"end":F=D.n.length;break;default:F=0}var P=[0,0];for(var K=0;K<B(this).length;K++){var N=K>=F&&K<F+D.s.length;if(A(M,K)){N=true}var Q=(N?D.s:D.n)[P[N?0:1]].e;Q.parent().append(Q);if(N||!E.returns){L.push(Q.get(0))}P[N?0:1]++}}return this.setArray(L)}});function C(D){return/^[\+-]?\d*\.?\d*$/.exec(D)}function A(E,F){var D=false;B.each(E,function(H,G){if(!D){D=G==F}});return D}B.fn.TinySort=B.fn.Tinysort=B.fn.tsort=B.fn.tinysort})(jQuery);
		
		// set tsort global defaults:
		$.tinysort.defaults.order = "desc";
		$.tinysort.defaults.attr = "id";

		/*
		 * jQuery history plugin
		 *
		 * Copyright (c) 2006 Taku Sano (Mikage Sawatari)
		 * Licensed under the MIT License:
		 *   http://www.opensource.org/licenses/mit-license.php
		 *
		 * Modified by Lincoln Cooper to add Safari support and only call the callback once during initialization
		 * for msie when no initial hash supplied.
		 * API rewrite by Lauris Bukðis-Haberkorns
		 */
		(function(b){function a(){this._curHash="";this._callback=function(c){}}b.extend(a.prototype,{init:function(d){this._callback=d;this._curHash=location.hash;if(b.browser.msie){if(this._curHash==""){this._curHash="#"}b("body").prepend('<iframe id="jQuery_history" style="display: none;"></iframe>');var c=b("#jQuery_history")[0].contentWindow.document;c.open();c.close();c.location.hash=this._curHash}else{if(b.browser.safari){this._historyBackStack=[];this._historyBackStack.length=history.length;this._historyForwardStack=[];this._isFirst=true;this._dontCheck=false}}this._callback(this._curHash.replace(/^#/,""));setInterval(this._check,100)},add:function(c){this._historyBackStack.push(c);this._historyForwardStack.length=0;this._isFirst=true},_check:function(){if(b.browser.msie){var c=b("#jQuery_history")[0];var f=c.contentDocument||c.contentWindow.document;var h=f.location.hash;if(h!=b.history._curHash){location.hash=h;b.history._curHash=h;b.history._callback(h.replace(/^#/,""))}}else{if(b.browser.safari){if(!b.history._dontCheck){var d=history.length-b.history._historyBackStack.length;if(d){b.history._isFirst=false;if(d<0){for(var e=0;e<Math.abs(d);e++){b.history._historyForwardStack.unshift(b.history._historyBackStack.pop())}}else{for(var e=0;e<d;e++){b.history._historyBackStack.push(b.history._historyForwardStack.shift())}}var g=b.history._historyBackStack[b.history._historyBackStack.length-1];if(g!=undefined){b.history._curHash=location.hash;b.history._callback(g)}}else{if(b.history._historyBackStack[b.history._historyBackStack.length-1]==undefined&&!b.history._isFirst){if(document.URL.indexOf("#")>=0){b.history._callback(document.URL.split("#")[1])}else{b.history._callback("")}b.history._isFirst=true}}}}else{var h=location.hash;if(h!=b.history._curHash){b.history._curHash=h;b.history._callback(h.replace(/^#/,""))}}}},load:function(f){var g;if(b.browser.safari){g=f}else{g="#"+f;location.hash=g}this._curHash=g;if(b.browser.msie){var c=b("#jQuery_history")[0];var e=c.contentWindow.document;e.open();e.close();e.location.hash=g;this._callback(f)}else{if(b.browser.safari){this._dontCheck=true;this.add(f);var d=function(){b.history._dontCheck=false};window.setTimeout(d,200);this._callback(f);location.hash=g}else{this._callback(f)}}}});b(document).ready(function(){b.history=new a()})})(jQuery);

		/**
		* onload/onunload
		*/
		jQuery(document).ready(function(){

			WOTS.initGoogleMaps();

    		$.history.init(WOTS.loadFromHistory);
    
			$('form#WOTSfControls').submit(function(){
				if (!WOTS.findAddress(this.q.value)) {
					$('#WOTSaddressInput').focus();
				} else {
					location.href = '#WOTSfindAddress:'+encodeURIComponent(this.q.value);
				}
				return false;
			});
			$('#WOTSaddressInput').focus();

			$(window).unload(function(){
				GUnload();
			});
		});
	},
	
	/**
	* renderLegislatorsForDistrict
	* retrieves and displays a list of legislators for a given city/state
	*/
	renderLegislatorsForDistrict : function(place) {
		WOTS.log('<strong class="progress">Loading districts for '
			+ (place.city ? place.city+', '+place.state : place.state)
			+ "&hellip;</strong>");

		WOTS.getDistrict( place, function(json1){

			// district fetch complete
			if (json1 && json1.response && json1.response.districts && json1.response.districts.length) {
				place.state = json1.response.districts[0].district.state;
				place.district = json1.response.districts[0].district.number;
			} else {
				WOTS.log('The district you selected could not be found. Please try another.');
				return false;
			}

			WOTS.renderLegislatorsForLocation(place);
		});
	},
	
	/**
	*
	*/
	displayRenderedLegislators : function(place, classes) {
		var summary = { 'districts':[], 'inactive':[], 'titles':[] }, x = 0;
			
		// been there, done that - show previous results and return
		if ( $('#WOTSresults').children().hide().filter( '.' + classes.join('.') ).fadeIn().children().each(function(){
			// generate stats from prerendered content
			var title = $('.person .meta .title', this).text();
			++summary.districts;

			if ($('.person', this).hasClass('status-0')) ++summary.inactive; 
			else if (typeof summary.titles[ title ] == 'undefined')
				summary.titles[ title ] = 1;
			else ++summary.titles[ title ];
			
		}).length > 0 ) {
			html = '<span class="nowrap"><a href="#WOTSmap" onclick="$(\'#WOTSaddressInput\').focus();return false">';
			if (WOTS.currentLocation.city) {
				html += WOTS.currentLocation.city + '</a>, '
					+ '<a href="#WOTSfindAddress:state=' + WOTS.currentLocation.state + '">';
			}
			html += WOTS.currentLocation.state
				+ '</a> ('
				+ (place.district
					? 'district ' + place.district 
					: (summary.districts==1 ? '1 district' : summary.districts + ' districts')
					)
				+ ')</span> has <span class="nowrap">'
				;
						
			for (title in summary.titles) {
				if (x++ > 0) html += (summary.titles[title] == 2) ? ' and ' : ', ';
				html += summary.titles[title]
					+ ' '
					+ (title=='Sen' ? 'Senator' : title=='Rep' ? 'Representative' : title=='Del' ? 'Delegate' : title)
					;
				if (summary.titles[title] != 1) html += 's';
			}
			if (summary.inactive > 0) {
				if (summary.districts || summary.titles[title]) html += ' and ';
				html += summary.inactive + ' inactive legislators';
			}
			
			html += '.</span>';

			if (summary.districts==1) {
				html += ' <a class="nowrap" href="#WOTSfindAddress:state=' + WOTS.currentLocation.state + '">View all</a>';
			}

			WOTS.log(html);
			return true;
		}

		return false;
	},
	
	/**
	* renderLegislatorsForLocation
	* fetches & formats a list of legislators for a given location
	*/
	renderLegislatorsForLocation : function(place) {
		if (place.district) var classes = [ 'district-' + place.district ];// , 'state-' + place.state ];
		else var classes = [ 'state-' + place.state ];

		// check cache in page
		if (WOTS.displayRenderedLegislators(place, classes)) return;

		WOTS.log('<strong class="progress">Loading legislators for ' 
			+ (place.city ? place.city + ', ' + place.state : place.state)
			+ (place.district ? ", district " + place.district : '') + "&hellip;</strong>");

		WOTS.getLegislators( place, function(json2){
			// people fetch complete			
			if (json2 && json2.response && json2.response.legislators && json2.response.legislators.length) {
				var html = '', summary = { 'districts':[], 'inactive':[], 'titles':[] }, person = {}, index = 0;
				
				// loop thru all people
				for (index in json2.response.legislators) {
					person = json2.response.legislators[index].legislator;

					html += '<li id="p'+index+'">'+WOTS.formatLegislators(person)+'</li>';

					// load posts
					var sources = { 
						'RSS': { format: 'rss', url: person.official_rss }
						,'YouTube': { format: 'rss', url: person.youtube_url, pattern: /.+[\.\/]youtube.com\/(\w+)\/?.*/, 
						replacement: 'http://gdata.youtube.com/feeds/base/users/$1/uploads?alt=rss&amp;v=2&amp;orderby=published' }
						,'Twitter': { format: 'json', url: person.twitter_id, pattern: /(.+[\.\/]twitter.com\/)?(\w+)\/?.*/,
						replacement: 'http://search.twitter.com/search.json?q=from%3A$2' }
					};
					for (feed in sources ) {
						var url = sources[feed].url;
						if (typeof url != 'undefined' && url != '') {
							if (typeof sources[feed].pattern != 'undefined' && typeof sources[feed].replacement != 'undefined') {
								url = url.replace(sources[feed].pattern, sources[feed].replacement);
							}
							WOTS.getFeed(sources[feed].format, url, '#p' + index + ' .person .hfeed', feed );
						}
					}
					
					/*
					* top words
					* disabled until I get JSONP or a proxy...
					if (person.bioguide_id) {
						//console.log('Loading top5 words for ' , person);
						var renderFeed = function(id) {
							return function(response) {
								//console.log('Loaded top5 words for ' , id);
								var dummy = [];
								var html = '', i = 0;
								for (i in response.words) {
									dummy.push( response.words[i].word );
									if (i > 0) html += ', ';
									html += '<a href="#" class="wc'+response.words[i].word_count+'" rel="nofollow">'+response.words[i].word+'</a>';
								}
								//console.log('words:',dummy.join(','));
								$('#p' + id + ' .person .words').append(html);
							}
						}
						WOTS.getTopWords(person, renderFeed(index));
					}
					*/

					// compile stats
					++summary.districts;
					if (!person['in_office']) ++summary.inactive; 
					else if (typeof summary.titles[ person['title'] ] == 'undefined')
						summary.titles[ person['title'] ] = 1;
					else ++summary.titles[ person['title'] ];

				} // end for loop
				
				// display the full result set with summary
				$('#WOTSresults').append('<ul class="legislators ' + classes.join(' ') + '">' + html + '</ul>' )
					.show('blind', null, 500, function(){
						var x = 0, title = '', html = '';

						html += '<span class="nowrap"><a href="#WOTSmap" onclick="$(\'#WOTSaddressInput\').focus();return false">';
						if (WOTS.currentLocation.city) {
							html += WOTS.currentLocation.city + '</a>, '
								+ '<a href="#WOTSfindAddress:state=' + WOTS.currentLocation.state + '">';
						}
						html += WOTS.currentLocation.state
							+ '</a> (' + (summary.districts==1 ? 'district ' + person['district'] : summary.districts +' districts')
							+ ')</span>'
							+ ' has <span class="nowrap">';
						
						for (title in summary.titles) {
							if (x++ > 0) html += (summary.titles[title] == 2) ? ' and ' : ', ';
							html += summary.titles[title]
								+ ' '
								+ (title=='Sen' ? 'Senator' : title=='Rep' ? 'Representative' : title=='Del' ? 'Delegate' : title)
								;
							if (summary.titles[title] != 1) html += 's';
						}
						if (summary.inactive > 0) {
							if (summary.districts || summary.titles[title]) html += ' and ';
							html += summary.inactive + ' inactive legislators';
						}
						html += '.</span>';

						if (summary.districts==1) {
							html += ' <a class="nowrap" href="#WOTSfindAddress:state=' + WOTS.currentLocation.state + '">View all</a>';
						}

						WOTS.log(html);
					})
					.find("a[rel*='nofollow']").attr('target','_BLANK') // launch external links in separate windows
					;

			} else {
				$('#WOTSresults').append('<ul class="legislators ' + classes.join(' ') + '">'
					+ '<li>No legislators were found for this district. Please try another.</li>'
					+ '</ul>'
				);
					
				//return false;
			}
		});
	},
	
	/**
	* renderGoogleFeed
	* fetch the content via Google Feeds or $.ajax
	* create a callback function which depends on the variable "selector"
	* @param id jQuery selector
	*/
	renderGoogleFeed : function(id) {
		return function(result){
			if (!result.error) {
				var html = '';
				for (var i = 0, j = result.feed.entries.length; i < j; i++) {
					var entry = result.feed.entries[i],
						entryDate = new Date(entry['publishedDate']), 
						published = '';
					if (isNaN(entryDate)) published = '';
					else published = (entryDate.getMonth()+1) + '/' + entryDate.getDate() + '/' + entryDate.getFullYear();

					html += '<li class="hentry" id="' + entryDate.getTime() + '">'
						+ '<a class="entry-title bookmark" rel="nofollow" href="' + entry[ "link" ] + '" title="' 
						+ entry[ "title" ] + '">' 
						+ entry[ "title" ] 
						+ '</a>'
						+ ' <cite class="entry-summary">' + entry[ "contentSnippet" ] + '</cite>'
						+ (published ? ' (<abbr class="published" title="'+entry['publishedDate']+'">' + published + '</abbr>)' : '')
						+ '</li>'
						;
				}
			} else {
				html = '<div class="error">No posts available'
					// we don't display the full error message to the user
					//+': '+result.error.message
					 + '</div>';
			}

			// display the formatted posts or error message, only if there are new valid results
			if (!result.error || $('.hentry,.error', id).length == 0) {
				$(id).children('.progress,.error').fadeOut().end().append( html );
				$(id).children().tsort();
			}
		}
	},

	/**
	* renderTwitterFeed
	* fetch via XHR
	* Twitter-specific rendering implementation
	* create a callback function which depends on the variable "selector"
	* @param id jQuery selector
	*/
	renderTwitterFeed : function(id) {
		return function(result){
			if (result.results) {
				var html = '';
				for (var i = 0, j = result.results.length; i < j; i++) {
					var entry = result.results[i],
						entryDate = new Date(entry['created_at']),
						published = '';

					if (isNaN(entryDate)) published = '';
					else published = (entryDate.getMonth()+1) + '/' + entryDate.getDate() + '/' + entryDate.getFullYear();

					html += '<li class="hentry" id="' + entryDate.getTime() + '">'
						+ '<a class="entry-title bookmark" href="http://twitter.com/' + entry['from_user'] + '/status/'
						+entry['id']
						+'" rel="nofollow" title="Tweet from ' + entry[ "from_user" ] + '">' 
						+ entry[ "text" ] 
						+ '</a>'
						//+ ' <cite class="entry-summary">' + WOTS.decodeEntities(entry[ "text" ]) + '</cite>'
						+' ('
						+ (published ? '<abbr class="published" title="'+entry['publishedDate']+'">' + published + '</abbr>' : '')
						//+ ' via '+WOTS.decodeEntities(entry[ "source" ])
						+ ')'
						+ '</li>'
						;
				}
			} else {
				html = '<div class="error">No posts available'
					//+': '+result.error.message
					 + '</div>';
			}

			// display the formatted posts or error message, only if there are new valid results
			if (!result.error || $('.hentry,.error', id).length == 0) {
				$(id).children('.progress,.error').fadeOut().end().append(html);
				$(id).children().tsort();
			}
		}
	},
	
	/**
	* getFeed
	*/
	getFeed : function( format, url, selector, source ) {
		if (format == 'rss' || format == 'atom') {
			// here we do the actual feed fetch
			var feed = new google.feeds.Feed( url );
			feed.load(WOTS.renderGoogleFeed(selector));
		} else {
			$.ajax({url: url, success: WOTS.renderTwitterFeed(selector), dataType: 'jsonp', cache: true, ifModified: true});
		}
	},
	
	/**
	* convert HTML entities back to HTML
	* useful for e.g. Twitter posts
	*/
	decodeEntities : function(str) {
		return unescape(str).replace( /&lt;/g, '<' ).replace( /&gt;/g, '>' ).replace( /&amp;/g, '&' ).replace( /&quot;/g, '"' ).replace( /\"{2,}/g, '"');
	},
	
	/**
	* getTopWords
	*/
	getTopWords : function( person, callback ) {
		if (!callback) callback = {};
		var url = WOTS.services['topwords']['base']+WOTS.services['topwords']['methods']['top5'];
		url = url.replace( /\{bioguide_id\}/, person.bioguide_id );

		$.ajax({url: url, success: callback, dataType: 'jsonp', jsonp: 'callback', cache: true, ifModified: true});
	},

	/**
	* getDistrict
	* retrieves the district number from a lat/long pair
	* @param place Object
	* @param callback function
	*/
	getDistrict : function( place, callback ) {
		if (!callback) callback = {};
		var url = WOTS.services['sunlightlabs']['base']+WOTS.services['sunlightlabs']['methods']['getDistrict'];
		url = url.replace( /\{LAT\}/, place.lat )
			.replace( /\{LONG\}/, place.lng )
			.replace( /\{KEY\}/, WOTS.services['sunlightlabs']['key'] );

		$.ajax({url: url, success: callback, dataType: 'jsonp', jsonp: 'jsonp', cache: true, ifModified: true});
	},

	/**
	* getLegislators
	* retrieves a list of legislators for a given location
	* @param place Object
	* @param callback function
	*/
	getLegislators : function( place, callback ) {
		if (!callback) callback = {};
		var url = WOTS.services['sunlightlabs']['base']+WOTS.services['sunlightlabs']['methods']['getLegislators'];
		url = url.replace( /\{state\}/, place.state ).replace( /\{KEY\}/, WOTS.services['sunlightlabs']['key'] );
		if (place.district) url = url.replace( /\{district\}/, place.district )
		else url = url.replace( /&district=\{district\}/, '' )

		$.ajax({url: url, success: callback, dataType: 'jsonp', jsonp: 'jsonp', cache: true, ifModified: true});
	},

	/**
	* formatLegislators
	*/
	formatLegislators : function(record) {
		var html = '', line = '', heading = '', field = '';

		// include meta info about the legislator
		html += '<div class="meta">'
			+'<div class="state" title="2 letter abbreviation of legislator\'s state">'+record['state']+'</div>'
			+'<div class="district" title="If legislator is a representative, their district">'+(record['district'] >=0 ? 'District '+record['district'] : record['district'])+'</div>'
			+'<h5><a class="fn" href="{website}" rel="nofollow">'
			+'<span title="Legislator\'s first name" class="firstname">'+record['firstname']+'</span> '
			+'<span title="Legislator\'s middle name or initial" class="middlename">'+record['middlename']+'</span> '
			+'<span title="Legislator\'s last name" class="lastname">'+record['lastname']+'</span> '
			+'<div title="Legislator\'s suffix (Jr., III, etc.)" class="name_suffix">'+record['name_suffix']+'</div> '
			//,'nickname' : "Preferred nickname of legislator (if any)"
			+'</a></h5>' // end of .fn
			+'<div title="Title held by this legislator, either Senator or Representative" class="title">'
			+(record['title']=='Sen' ? 'Senator' : record['title']=='Rep' ? 'Representative' : record['title']=='Del' ? 'Delegate' : record['title'])+'</div>'
			+' ✭ '
			+'<div class="party" title="Party affiliation">'+(record['party']=='D' ? 'Democrat' : record['party']=='R' ? 'Republican' : record['party'])+'</div> '
			+'<div class="bioguide_id">'+record['bioguide_id']+'</div>'
			+'<div class="votesmart_id">'+record['votesmart_id']+'</div>'
			+'<div class="fec_id">'+record['fec_id']+'</div>'
			+'<div class="govtrack_id">'+record['govtrack_id']+'</div>'
			+'<div class="crp_id">'+record['crp_id']+'</div>'
			+'<div class="eventful_id">'+record['eventful_id']+'</div>'
			+'<div class="official_rss">'+record['official_rss']+'</div>'
			+'<div class="congress_office">'+record['congress_office']+'</div>'
			+'</div>' // end of .meta
			;
		
		// generate the HTML for this contact record
		for (heading in WOTS.profileHTML) {
			line = '';
			for (field in WOTS.profileHTML[heading]) {
				if (typeof record[field] != 'undefined' && record[field] != '') {
					line += '<div class="'+field+'">' + WOTS.profileHTML[heading][field] + '</div>';
				}
			}
			if (line != '') {
				if (heading == 'contact') html += '<div class="info">' ; // wrapper div
				html += '<fieldset class="'+heading+'"><legend>'+heading+'</legend>'+line+'</fieldset>';
				if (heading == 'contact') html += '</div>'; // close wrapper div
			}
		}

		var photo = 'no photo available';
		if (record['crp_id']) photo = '<img src="http://www.opensecrets.org/politicians/img/pix/' + record['crp_id'] + '.jpg"/>'
		else if (record['govtrack_id']) photo = '<img src="http://www.opencongress.org/images/photos/thumbs_125/' + record['govtrack_id'] + '.jpeg"/>';
		else if (record['votesmart_id']) photo = '<img src="http://watchdog.net/data/crawl/votesmart/photos/' + record['votesmart_id'] + '.JPG"/>'
	
		html = WOTS.template( WOTS.profileOpenHTML.replace(/\{photo\}/g, photo) + html, record );
			
		// include an empty hfeed container for posts
		if (record['official_rss'] || record['youtube_url'] || record['twitter_id']) {
			html += '<fieldset class="links"><legend>Recent Posts</legend>'
				+'<ul class="hfeed">'
				+'<strong class="progress">Loading&hellip;</strong>'
				+'</ul></fieldset>'
				;
		}

		html += WOTS.profileCloseHTML;
		
		return html;
	},

	/**
	* template
	* do key substitutions within the HTML using keys from the record
	* @param str String containing key references
	* @param record Object containing key/value pairs
	*/
	template : function(str, record) {
		return str.replace( /\{(\w+)\}/g, 
			function(t, key) { return typeof record[key] != 'undefined' ? record[key] : ''; }
		 );
	},
	
	/**
	* findAddress()
	* GeoCode an address and display on the map
	* @param address String
	*/
	findAddress : function(address) {
		if (WOTS.geocoder) {
			if (!address) {
				WOTS.log('<strong class="progress">Please enter a valid combination of city, state, and/or zip.</strong>');
				return false;
			}
			
			WOTS.log('<strong class="progress">Determining selected location&hellip;</strong>');
			WOTS.geocoder.getLatLng(address, function(point) {
				if (!point) {
					WOTS.log("Unable to find address: "+address);
				} else {
					WOTS.map(point);
				}
			});
			return true;
		}
	},

	/**
	* find and zoom in on a given address or point
	* @param point GLatLng
	*/
	showSelection: function(point) {
		WOTS.geocoder.getLocations(point, function(response) {
			if (!response || response.Status.code != 200) {
				WOTS.log("Google Maps unavailable, please try again later (code:" + response.Status.code + ")");
			} else {
				place = response.Placemark[0];
				
				if (place.AddressDetails.Country.CountryNameCode == 'US') {
					//var point = new GLatLng(place.Point.coordinates[1],place.Point.coordinates[0]);

					var address = '',
						accuracy = place.AddressDetails.Accuracy;

					// shrink the map
					//$('#WOTSmap:visible,#WOTScontrols:visible').hide('blind');

					//if (!WOTS.GMap.getBounds().contains(point)) 
					WOTS.GMap.panTo(point);
					
					//WOTS.GMap.setZoom(12 - accuracy);
					//WOTS.GMap.setZoom(10);
					
					WOTS.currentLocation = {
						'lat': point.lat() //'lat': place.Point.coordinates[1],
						,'lng': point.lng() //'lng': place.Point.coordinates[0]
					};				

					if (place.AddressDetails.Country.AdministrativeArea) {
						switch (place.AddressDetails.Accuracy) {
							case 9: //Premise
							case 8: //Address
							case 7: //Intersection
							case 6: //Street
							case 5: //Post code
								if (place.AddressDetails.Country.AdministrativeArea.Locality) {
									WOTS.currentLocation.zip = place.AddressDetails.Country.AdministrativeArea.Locality.PostalCode.PostalCodeNumber;
									WOTS.currentLocation.city = place.AddressDetails.Country.AdministrativeArea.Locality.LocalityName;
								}
							case 4: //Town
								if (!WOTS.currentLocation.city && place.AddressDetails.Country.AdministrativeArea.Locality) {
									if (place.AddressDetails.Country.AdministrativeArea.Locality.LocalityName)
										WOTS.currentLocation.city = place.AddressDetails.Country.AdministrativeArea.Locality.LocalityName;
									else 
										WOTS.currentLocation.city = place.AddressDetails.Country.AdministrativeArea.Locality;
								}
								if (WOTS.currentLocation.city) {
									address += WOTS.currentLocation.city + ', ';
								}
							case 3: //Sub-region
							case 2: //Region
								WOTS.currentLocation.state = place.AddressDetails.Country.AdministrativeArea.AdministrativeAreaName;
								address += WOTS.currentLocation.state;
							case 1: //Country
								WOTS.currentLocation.country = place.AddressDetails.Country.CountryNameCode;
						}
					} else {
						WOTS.currentLocation.state = '';
						WOTS.currentLocation.country = place.AddressDetails.Country.CountryNameCode;
					}
					
					if (WOTS.currentLocation.zip)	address += ' ' + WOTS.currentLocation.zip;
					//address += ' ' + WOTS.currentLocation.country;

					$('#WOTSheader .address').text( address );
					$('#WOTSaddressInput').val( address );

					if (WOTS.currentLocation.city) {
						WOTS.renderLegislatorsForDistrict(WOTS.currentLocation);
					} else {
						WOTS.renderLegislatorsForLocation(WOTS.currentLocation);
					}

					if (typeof marker != 'undefined') {
						WOTS.GMap.removeOverlay(marker);
					}
					//WOTS.GMap.savePosition();
					marker = new google.maps.Marker(point);
					WOTS.GMap.addOverlay(marker);
					//marker.openInfoWindowHtml( '<div>'+address+'</div>' );

					/*
					google.maps.Event.addListener(marker, 'click', function(){
						//marker.openInfoWindowHtml("long = " + point.x, "lat = " + point.y);
						WOTS.log("long=" + point.x, "lat=" + point.y);
					});
					*/

				} else {
					// handle click point outside the US
					WOTS.log('Please select a location within the USA.');
					
					$('#WOTSmap:hidden,#WOTScontrols:hidden').show('blind');

				}
			}
		});
	},

	/**
	* init the map object
	* attempts to get user's current location; default to GeoCode for US Congress
	*/
	initGoogleMaps: function() {
		if (GBrowserIsCompatible()) {
			// set up standard UI without the map type selector
			WOTS.GMap = new google.maps.Map2(document.getElementById("WOTSmap"));
	
			WOTS.geocoder = new google.maps.ClientGeocoder();
			WOTS.geocoder.setBaseCountryCode('US');
			
			// process clicks on new locations on the map
			lstner = google.maps.Event.addListener(WOTS.GMap, 'click', function(overlay, point){
				if (point) {
					WOTS.log('<strong class="progress">Determining selected location&hellip;</strong>');
					WOTS.map(new google.maps.LatLng(point.y, point.x));			
				}
			});
			
		} else {
			WOTS.log('<strong>Sorry, your browser is not supported at this time.</strong>');
		}		
	},
	
	/**
	* render the main app
	*/
	render : function(id) {
		if (document.getElementById(id)) return;
		
		document.write(
		'<div id="'+id+'">'
		+'<div id="WOTSheader"><div class="info"><h1>Word on the Street</h1>'
		+'<h5 class="address">wordonthestreet.klokie.com</h5>'
		+'<blockquote class="description"><p class="strong"><em>Word on the Street</em> is a software widget that will automagically recognize the user\'s current location and display all of the elected Congressional officials for that region (within the US).</p>'
		+'<p>Each Senator, Representative, or other delegate is displayed in an easy to read format including a recent photo, contact information, relevant links, and any recent posts they have made to their primary blogs or to Twitter, or videos they posted to YouTube. WOTS can be easily embedded into any website, blog, or social networking site (coming soon), and is specially suited for use on the iPhone, Google Android, BlackBerry, and other GPS-equipped devices as well as web browsers for Mac, Windows, and Linux, without requiring any additional software installation, customization, registration, or hosting. Since it is nearly effortless to use (at least I hope it is) and provides a lot of useful and timely information to the user, I am hoping that it will help to increase political transparency and public participation in US government.</p><p>Here\'s the basic flow:'
		+'<ol><li>You arrive here. WOTS should recognize the general area where you\'re located and pull up the list of all senators, representatives, and any other delegates to Congress for your area. If you are outside the US or Google Maps wasn\'t able to detect your location, you can select it by clicking on the map or typing it in to the "location" field.</li>'
		+'<li>You can change your location at any time by clicking the "<a href="#WOTSCL">change location</a>" link at the top. You can try just about anything - city, state, zip code, intersection should all work (thanks to Google Maps)! A more specific location should provide you with a specific person to contact for that area, while a more general area or state name will bring up a list of legislators for the region.</li>'
		+'<li>The contact info and links sections are pretty concise, while the posts area is a lot more interesting. There is a wealth of information about all these people in a variety of places, but I\'m trying to make it easier for citizens to quickly get up to date on exactly what their representatives are up to - and soon, what they are voting for and spending your tax money on - and to contact them if necessary.</li></ol></p>'
		+'<p>I want to help get Americans more interested and involved in politics, so they can not only follow what their elected leaders are doing on a daily basis, but more importantly feel like they can voice their opinions to people in power and feel like their voices are getting heard. Finding out exactly who those powerful people are, starting with the legislators in US Congress, is the first step toward making positive change.</p>'
		+'<p>If you have any comments or questions, please feel free to email me at <a href="mailto:%20wots@klokie.com">wots@klokie.com</a>.</p>'
		+'Thanks!<div class="created">3/28/2009</div><a href="mailto:%20wots@klokie.com" class="author">Klokie</a></blockquote>'
		+'</div>'//<!--/.info-->
		+'<div class="menu"><a href="#WOTSmap" id="WOTSCL" class="tab map">Change Location</a><a href="#WOTSabout" class="tab about">Learn More</a></div>'
		+'</div>'//<!--/#header-->
		+'<div id="WOTSmap"></div>'
		+'<div id="WOTScontrols">'
		+'<form method="GET" action="#" name="fControls" id="WOTSfControls">'
		+'<label for="location">Location:</label>'
		+'<input type="text" id="WOTSaddressInput" name="q" value="" size="32" maxlength="50"/>'
		+'<input type="submit" class="submit" value="Go"/>'
		+'</form>'
		+'</div>'
		+'<div id="WOTSabout"></div>'
		+'<div id="WOTSresults"></div>'
		+'<div id="WOTSfooter">'
		+'<div class="info hcard nowrap"><a href="http://klokie.com/" title="by Klokie">Klokie</a> | <a href="http://s2ai.net/main.php" class="url fn org">S2A Interactive</a> |'
		+'<span class="email"><a href="mailto:%20wots@klokie.com" class="value">wots@klokie.com</a></span>'
		+'</div>'//<!--/.info-->
		+'<div class="license"><div class="nowrap">Data provided by <a href="http://sunlightfoundation.com/" target="_BLANK">The Sunlight Foundation</a>.</div><div class="nowrap">Flag photo courtesy of <a rel="license" href="http://www.flickr.com/photos/branditressler/" target="_BLANK">Brandi Tressler</a></div></div>'
		+'</div>'//<!--/#footer-->
		+'</div>'//<!--/#content-->
		);
	}

}; // end:WOTS

/**
* init Google Loader
*/
google.load("jquery", "1.3");
google.load("jqueryui", "1.7.1");
google.load("maps", "2", {'other_params': 'sensor=true'} );
google.load("feeds", "1");

WOTS.render('WOTS');

google.setOnLoadCallback( WOTS.main );
