/*
 * MapFilter
 *
 * View data on a map and list, where the map filters the data,
 * only showing items that have coordinates/address on map's current display.
 *
 * Usage: $().mapfilter(options)
 * 
 * Note: mapFilter() is not meant to iterate over multiple elements. 
 *       Bad:  $("#mine').mapFilter(options);
 * 
 * Copyright (c) 2009 Chad Norwood
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * $Date:$
 * $Revision:$
 *
 * http://code.google.com/p/jquery-week-calendar/source/browse/trunk/jquery.weekcalendar/jquery.weekcalendar.js
 * http://code.google.com/p/jquery-googlemap/source/browse/trunk/googlemaps.js
 *
 * http://cjgiddings.wordpress.com/2009/05/11/mapstraction-with-jquery/
 *
 *
 * Creating JQuery Plugins:
 * http://www.learningjquery.com/2007/10/a-plugin-development-pattern
 * http://docs.jquery.com/Plugins/Authoring
 * 
 * OO Javascript 
 * http://jquery-howto.blogspot.com/2009/01/object-oriented-javascript-how-to_21.html
 */


;(function($) {

  /*
   *  LOGGING function separate from mapFilter
   *  TODO: look at log4javascript for logging errors/warns/info/debug
   */
  // I comment this out, since i set $.enableConsole in html/php
  // false: disable logging completely, true: enable if firebug is enabled
  //$.enableConsole = false
  var startMs2 = new Date().getTime();
  // http://snipplr.com/view/10358/jquery-to-firebug-logging/
  $.fn.console = $.console = function(method) {
      if ($.enableConsole && window.console && console[method]) {
          if (method != 'debug') {
		  	console[method].apply(this, [].splice.call(arguments, 1));
			return this;init
		  }
		  args = [].splice.call(arguments, 1);
		  ems = new Date().getTime() - startMs2;
          ems +=  "ms ";
		  args.unshift(ems);
		  console[method].apply(this, args);
	  }
      return this;
  }
  /*    
      // You can call any firebug functions through either the direct jQuery object, or in a chain:
      $.console('debug', 'This is some %s text', 'debugging');

      // The best part is the ability to use the firebug console chain easily:
      var color = '#FFF';
      $('#someElement').css('backgroundColor', color).console('log', 'Changing the background color to "%s"', color);
  
      // And the access to the other functions of the firebug console, like time/timeEnd
      $('#someElement').console('time', 'timing foo').animate({...}).console('timeEnd', 'timing foo'); 
   */


  // 
  // Variables used by both $.mapFilter() and mapFilter helper functions
  // 

  var opts;  // shortcut to $.mapFilter.opts
  var jumptxt;

  // google variables
  var map; 
  var geocoder;

  var
  debugFirebugTimers = 1,
  debugAlert = 0,
  errorAlert = 1; // pop up alert msgs if error state
  var myGeo; // geocoder instance 
  
  /*
   * mapMarkers is main data structure holding map marker data, see addMarkers() removeMarkers()
   * Note: markers.list contains calendar event info, see addEventObj()
   * Note: markers.types contains filters
   */ 
  // TODO: rename markers mapMarkers
  var markers = {
    gMarkers: {},
    gMarkerExists: function (nn) {
      if (typeof nn == 'number') coords = markers.list[nn].coords();
      if (typeof nn == 'object') coords = nn.coords();
      return (markers.gMarkers[coords] && markers.gMarkers[coords].gMarker);
    },
    numUnknownAddresses: function () {
      var xx = 0;
      for (var ii in markers.list) {
        if (markers.list[ii].lt == 0) xx++;
      }
      return xx;
    },
    types: [],
    list: []
  };


  /*
   * $().mapFilter() main function
   */
  $.fn.mapFilter = function(options) {

	/*
	 * set 
	 */
	opts = $.mapFilter.opts = $.extend({}, $.mapFilter.defaults, options);

	/* 
	 * Variables used by only $().mapFilter()
	 */ 


    gcTitle = 'A Calendar',
    gcLink = '',

    ndays = opts.gCalDays;
    jumptxt = opts.jumpTxt;

	/*
	 *   static variables
	 */
    var 
    urlIconDefault = "http://www.google.com/mapfiles/marker.png",
    urlIconOrange = "http://gmaps-samples.googlecode.com/svn/trunk/markers/orange/blank.png",
    urlIconBlue  = "http://gmaps-samples.googlecode.com/svn/trunk/markers/blue/blank.png",
	mapVersion = "Map Version 2009-6-9";

	// end variables

/*
    this.each(function() {

      //
      // Instance variables
      //
      var date = options.year ? // holds the year & month of current month
        new Date(options.year, options.month || 0, 1) :
        new Date();
      var start, end; // first & last VISIBLE dates
      var today;
      var numWeeks;
      var ignoreResizes = false;

      var events = [];
      var eventSources = options.eventSources || [];
      if (options.events) eventSources.push(options.events);

      function refreshMonth() {
        addYears(date, 1);
        //render();
      }
*/

/*
      //
      // Publicly accessible methods
      //
      $.data(this, 'fullCalendar', {
        refresh: refreshMonth,

        getEventsById: function(eventId) {
          var res = [];
          for (var i=0; i<events.length; i++) {
            if (events[i].id === eventId) {
              res.push(events[i]);
            }
          }
          return res;
        }

      });

*/


      function init() {

        if (!(window.console && window.console.firebug)) {
          if (debugAlert) alert("cnOnLoad() no console, no log");
          debugFirebugTimers = 0;
        }
//		      $('#someElement').console('time', 'timing foo').animate({...}).console('timeEnd', 'timing foo'); 

        $.console('debug', 'mapfilter().init()');
		updateStatus('Loading Google Map');
		
        if (!GBrowserIsCompatible()) { 
           document.getElementById(opts.mapId).innerHTML = unSupportedHtml;
           return;
        }
        map = new GMap2(document.getElementById(opts.mapId));
        map.setUIToDefault();
        map.setCenter(new GLatLng(opts.mapCenterLt, opts.mapCenterLg), opts.mapZoom, map.getMapTypes()[opts.mapType]);
        geocoder = new GClientGeocoder();
        GEvent.addListener(map, 'moveend', function(){mapMovedListener();});
        mapLogo(map);
        mapJumpBox(map);
        iconDefault = new GIcon(G_DEFAULT_ICON, urlIconDefault);

        // check for data sources and add them
        //
        if (opts.gCalUrl && opts.gCalUrl != 'u') {
			updateStatus('Map Loaded, Loading Calendar ..');
			getGCalData(opts.gCalUrl);
		} else {
			updateStatus('Map Loaded.');

		}
        $.console('debug', "mapFilter().init() Completed.");
      };
	  

      function mapRedraw(skipUpdateStatus) {

        if ((typeof redrawing == 'boolean') && redrawing) {
            $.console("debug", ems+"mapRedraw() redrawing already in process, skipping.");
            return;
        }
        redrawing=true;

        $.console('debug', "markers: ", markers);
        var t1='total redraw';
        if (debugFirebugTimers) $.console('time', t1);

        if (updateMarkers()) {
          $.console('debug', "mapRedraw() updated markers, changes found");
          if (!skipUpdateStatus) updateStatus("... Redrawing Map ...");
          updateResults();
        } else {
          $.console('debug', "mapRedraw() updated markers - no changes, no updateResults()");
        }
        if (debugFirebugTimers) $.console('timeEnd', t1);

        // todo: don't update status every map redraw. then we can call mapRedraw during geocoding, but 
		// will need to change maplink
		if (!skipUpdateStatus) updateStatus("<a title='Click to view Full Calendar' href='"+ gcLink +"'>"+ gcTitle +"</a>");
        
		//sleep(2000);
        mapChanged();
        redrawing=false;
      }

	  function mapAllEvents() {

		  // first create the box that holds all event locations
		  box = null;
		  for (var i in markers.list) {
            var kk = markers.list[i];
            if (kk.lt == 0) continue; // skip unrecognized addresses
			
			if (box == null) {
				corner = new GLatLng(kk.lt, kk.lg, true);
				box = new GLatLngBounds(corner, corner);
			} else {
				box.extend(new GLatLng(kk.lt, kk.lg, true)); 
			}
            //$.console('debug', "mapAllEvents(): adding lt,lg: ", kk.lt, kk.lg);

			/* non-google way, works for USA but not for world
			if (kk.lt < latS) latS = kk.lt;
            if (kk.lt > latN) latN = kk.lt;
            if (kk.lg < lngW) lngW = kk.lg;
            if (kk.lg > lngE) lngE = kk.lg;
            */
		  }
		  
		  if (!box) {
		  	$.console('debug', "mapAllEvents(): no events");
		  	return false; 
		  }
		  	
		  $.console('debug', "mapAllEvents(): setting new map ");
	
	      zoom = map.getBoundsZoomLevel(box);
          map.setCenter( box.getCenter(), (zoom < 2) ? zoom : zoom - 1  );

		  	
	  }

      //
      // returns true if changes were made to map, false otherwise
      //
      function updateMarkers() {

        $.console('debug', "updateMarkers() called ..");
/*
        ltSW = map.getBounds().getSouthWest().lat();
        lgSW = map.getBounds().getSouthWest().lng();
        ltNE = map.getBounds().getNorthEast().lat();
        lgNE = map.getBounds().getNorthEast().lng();
*/
		mapbox = map.getBounds();

        if (debugFirebugTimers) {
            tjson = "checking all markers";
            $.console('time', tjson);
        }
        added = 0;
        removed = 0;
        unchanged = 0;
     
        for (var i in markers.list) {
          var kk = markers.list[i];

		  // TODO: use this instead: 
		  insideCurMap = mapbox.containsLatLng(new GLatLng(kk.lt,kk.lg,true) );

          //insideCurMap = insideMap(kk.lt, kk.lg, ltSW,lgSW, ltNE,lgNE);
          //$.console('debug', "marker "+ (insideCurMap ? 'in':'out') +"side map %o", kk);

          // TODO : fix filters (search, category, etc)
          filteredOut = false;
          if (markers.types[kk.type].filtersActive) {
            //isFiltered = markers.type[kk.type].filter(kk);
            filteredOut = false;
          } 

          if (kk.isDisplayed && insideCurMap && !filteredOut) {
            unchanged++;
          } else
          // First remove all markers outside of map
          if (kk.isDisplayed && !insideCurMap) {
            kk.isDisplayed = false;
            removeMarkers(kk);
            removed++;
          } else
          // Then add markers that are new to map
          if (!kk.isDisplayed && insideCurMap && !filteredOut) {
            buildInfoHtml(kk);
            addMarkers(kk);
            kk.isDisplayed = true;
            added++;
          }
        }
        if (debugFirebugTimers) $.console('timeEnd', tjson);
        markers.numDisplayed = unchanged + added;
        $.console('debug', "updateMarkers() "+removed+" removed, "+added+" added, "
				 +unchanged+" unchanged, "+markers.numDisplayed+" total ", markers);

        return (removed || added);
      }
            



      function updateResults() {

        idTable = opts.listId + 'Table';
        //summaryHtml = "<a href='"+gcLink+"'>"+ this.mapTitle +"</a><br>";
        summaryHtml = " ";
        filterHTML = "<p><div id='showall'>[Show all]</div> "
		           + "Showing "+ markers.numDisplayed +" of "+ markers.list.length;
        if (markers.numUnknownAddresses() > 0) {
          filterHTML += " ("+ markers.numUnknownAddresses() +")";
        }
        filterHTML += " Events " + (ndays==1 ? 'for today' : 'over the next '+ days2weeks(ndays));
        filterHTML += ", starting Now, "+ $.mapFilter.formatDate(new Date(), 'ga d M D');
        //filterHTML += "<br>Found "+ markers.numUnknownAddresses() +" events without proper addresses <div id='err1'>[Show/Hide Events]</div>";

        for (var nd in $.mapFilter.days2weeks) {
            continue;
            if (ndays != nd) filterHTML += "<a href='"+ '' +"'>";
            filterHTML += $.mapFilter.days2weeks[nd] + ((ndays != nd) ? "</a>" : '') + ", ";
        }

        tableHtml = "<table id='"+idTable+"' class='tablesorter'><thead><tr><th>Date</th><th title='Start Time'>Time</th>";
        tableHtml += "<th>Name</th><th>Description</th><th>Address</th></tr></thead><tbody>\n";
        $("#"+ opts.listId ).html(summaryHtml + filterHTML + tableHtml);
        tablesorterOptions = {
          sortInitialOrder: 'desc', 
          headers: { 0: { sorter:'text' } , 
                     1: { sorter:'digit' } 
        }};
        tablesorterOptions = {
			// sort on the first column and second column, order asc 
			sortList: [[0, 0]]
		}
		
        for (var i in markers.list) {
          var kk = markers.list[i];
          if (!kk.isDisplayed) continue;

		  // TODO: if event is an all day event, represent that instead of currently saying 12a-12am
		  // TODO: also note recurring (weekly/monthly/etc)
          rowHTML = "<tr>";
          //rowHTML += "<td>"+ $.mapFilter.formatDate(kk['start'], (ndays < 8 ?'d':'Y-m-L d')) +"</td>";
          rowHTML += "<td>"+ $.mapFilter.formatDate(kk['start'], 'Y-m-L H:i d') +"</td>"; 
          rowHTML += "<td>"+ $.mapFilter.formatDate(kk['start'], 'gx') +"-";
          rowHTML +=    $.mapFilter.formatDate(kk['end'], 'gx') +"</td>";
          rowHTML += '<td><a href="javascript:void(0)" onclick="$.mapFilter.hItem('+ kk.id +')">'+ kk.name +"</a></td>";
          rowHTML += "<td>[<a href='"+kk.url+"'>EVENT LINK</a>] "+ $.mapFilter.maxStr(kk.desc, 300, 0, '') +"</td>";
          rowHTML += '<td><a href="javascript:void(0)" title="'+ kk.origAddress +'">'+ kk.g2Address +"</a></td>";
          rowHTML += "</tr>\n";

          $("#"+ opts.listId +" table").append(rowHTML);
        }
        $("#"+ opts.listId +" table").append("</table>");
        $("#"+idTable).tablesorter(tablesorterOptions);

        errorsHtml = "Found "+ markers.numUnknownAddresses() +" events without proper addresses ";
        errorsHtml += "<div id='err2'>"+ (markers.numUnknownAddresses() > 0 ? '[Show/Hide Events]' : '' ) +"</div>";
        errorsHtml += "<div id='eventErrors' style='display:none'><table id='errorsTable' class='tablesorter'><thead><tr><th>Date</th>";
        errorsHtml += "<th>Name</th><th>Description</th><th>Address</th><th>Error</th></tr></thead><tbody>\n";
        for (var i in markers.list) {
          var kk = markers.list[i];
          if (kk.lt != 0) continue;
          rowHTML = "<tr>";
          rowHTML += "<td>"+ $.mapFilter.formatDate(kk['start'], (ndays < 8 ?'d gx-':'n/D d gx-'));
          rowHTML +=  $.mapFilter.formatDate(kk['end'], 'gx') +"</td>";
          rowHTML += '<td><a href="'+kk.url+'">'+ kk.name +"</a></td>";
          rowHTML += "<td>[<a href='"+kk.url+"'>EVENT LINK</a>] "+ $.mapFilter.maxStr(kk.desc, 300, 0, '') +"</td>";
          rowHTML += '<td><a href="'+kk.url+'">'+ kk.origAddress +'</a></td>';
          rowHTML += '<td>'+ kk.error +"</td>";
          rowHTML += "</tr>\n";

          errorsHtml += rowHTML;
        }
        errorsHtml += "</table></div>";

        $("#"+ opts.listId ).append(errorsHtml);
        $("#showall").click(function() { mapAllEvents(); });
        $("#errorsTable").tablesorter();
        $("#err1").click(function() { $("#eventErrors").toggle(); });
        $("#err2").click(function() { 
          xy = function (key,obj) {
            if (typeof obj === 'string') return obj.replace(/"/,"\\\"");
            return obj;
          }
          $("#eventErrors").append("<pre>markers.list = \n"+ JSON.stringify(markers.list, xy, 2) +"\n\n</pre>\n");
          $("#eventErrors").toggle(); 
        });

        // highlight columns or rows:
        // http://p.sohei.org/stuff/jquery/tablehover/demo/demo.html#
      }


      function getMapType(oMap) {
        mm = oMap.getMapTypes().length;
        for (nn = 0; nn < mm; nn++){
          if (oMap.getMapTypes()[nn] == oMap.getCurrentMapType())
            return nn;
        }
        return -1;
      }


      // TODO - chad - move to index.html as callback function
      function mapChanged() {
        if (opts.mapChangeCallback) opts.mapChangeCallback({
          mapZoom: map.getZoom(),
          mapType: getMapType(map),
          mapCenterLt: map.getCenter().lat().toString().replace(/(\.\d\d\d\d\d\d)\d+/,"$1"),
          mapCenterLg: map.getCenter().lng().toString().replace(/(\.\d\d\d\d\d\d)\d+/,"$1"),
		  // TODO: use opts.ndays
          gCalDays: ndays
        });
      }


      function mapMovedListener() {

          if (!map.getInfoWindow().isHidden()) {
              $.console('debug', "mapMovedListener(): infowindow is open, not redrawing.");
              return;
          }
          $.console('debug', "mapMovedListener(): redrawing ...");
          mapRedraw();
            //console.log("sleeping for 2secs");
            //sleep(2000);
            //console.log("woke up !!");
      }


      function insideMap(lat,lng, latS,lngW, latN,lngE ) {
	  	
		  
		  // below doesn't work well at north pole
          //alert("checking ("+lat+", "+lng+"), inside\n NE("+latN+", "+lngE+")\n SW("+latS+", "+lngW+")");
          if (lat < latS) return false;
          if (lat > latN) return false;
          if (lng < lngW) return false;
          if (lng > lngE) return false;
          return true;
      }


      function addLinks (txt) {
          return txt.replace(/(http:\/\/[^<>\s]+)/gi,'<a href="$1">$1</a>');
      }


      function buildInfoHtml (kk) {
          if (kk.infoHtml) return;
          //str = i+" id("+kk['id']+"): "+kk['n']+"\n"+ kk['lt'] +", "+ kk['lg'] ; 
          //alert(str);
          //desc = "Star(s): "+ kk['r'] +" &nbsp; Category: "+ rawJson[0][kk['c']] + "<p>\n" + kk['d'];
          //infoHTML = "<div class=\"IW\"><h1>"+ kk['n'] +"<\/h1><div id=\"IWContent\">"+ desc +"</div>"+footer+"</div>";

          kk.infoHtml = "<div class='IW'><h1>"+ kk.name +"<\/h1>";
          kk.infoHtml += "<div id='IWContent' class='preWrapped'><pre>"+ $.mapFilter.maxStr( addLinks(kk.desc), 900, 26, kk.url) +"</pre></div>";
          kk.infoHtml += '<div id="IWZoom">';
          kk.infoHtml += $.mapFilter.formatDate(kk['start'], 'F D, l gx') +"-"+ $.mapFilter.formatDate(kk['end'], 'gx') +"<br>";
          kk.infoHtml += '<a href="javascript:void(0)" onclick="$.mapFilter.zoomTo('+ kk.id +')">Zoom To</a> - ';
          kk.infoHtml += kk.origAddress +" - " + kk.directionsHtml();
          kk.infoHtml += '</div></div>'; 
      }


      //
      // addMarkers() creates google markers and event listener
      //
      function addMarkers (kk) {

          if (markers.gMarkerExists(kk)) {
            markers.gMarkers[kk.coords()].list.push(kk.id);
            //console.log("addMarkers() added to existing marker, kk.id="+kk.id);
            return;
          } 
          //console.log("addMarkers() creating marker, kk.id=%s, %o", kk.id, kk.coords());

          gMrkr = new GMarker(new GLatLng( kk.lt, kk.lg ), {icon:iconDefault});
          GEvent.addListener(gMrkr, "click", function() {
            $.mapFilter.hItem(kk.coords());
          });
          map.addOverlay(gMrkr);

          markers.gMarkers[kk.coords()] = { 
            gMarker: gMrkr,
            list: [kk.id]
          }
          return;
      }


      function removeMarkers (kk) {
          coords = kk.coords();
          jj = -1;
          for (var ii in markers.gMarkers[coords].list) {
            if (markers.gMarkers[coords].list[ii] == kk.id) {
              //console.log("removeMarkers() found "+kk.id);
              jj = ii;
              break;
            }
          }
          if (jj == -1) {
              $.console('error', "removeMarkers() logic error, not found "+kk.id);
              return;
          }
          markers.gMarkers[coords].list.splice(jj, 1);

          if (markers.gMarkers[coords].list.length == 0) {
            map.removeOverlay(markers.gMarkers[coords].gMarker);
            markers.gMarkers[coords] = null;
          }
      }


      // Insert Jump Box (aka goto address)
      function mapJumpBox(oMap) {

        var info=document.createElement('div');
        //info.id='SearchBox';
        info.style.position='absolute';
        info.style.right='7px';
        info.style.top='30px';
        //info.style.backgroundColor='transparent';
        info.style.zIndex=25500;
        //info.innerHTML='<input id="gotoBox" type="text" size=35 onkeypress="handleKeyPress(event,\'#goToAddress\')" value ="'+jumptxt+'" onfocus="if (this.value == \''+jumptxt+'\') {this.value = \'\';}" onblur="if (this.value == \'\') {this.value = \''+jumptxt+'\';}" />';
        info.innerHTML = '<form action="#" onsubmit="$.mapFilter.jumpToAddress(this.address.value); return false">'
            + '<input id="jumpBox" type="text" size=35 name="address" value ="' + jumptxt 
            + '" onfocus="if (this.value == \''+ jumptxt +'\') {this.value = \'\';}" onblur="if ' 
            + '(this.value == \'\') {this.value = \''+ jumptxt +'\';}" /></form>';

        oMap.getContainer().appendChild(info);
      }


      function mapLogo(oMap) {
        /* Insert Logo on Map */
        var info=document.createElement('div');
        info.id='LogoInfo';
        info.style.position='absolute';
        info.style.right='4px';
        info.style.bottom='20px';
        info.style.backgroundColor='transparent';
        info.style.zIndex=25500;
        info.innerHTML='<a href="http://chadnorwood.com/" title="Powered by Craft Beer"><img src="http://chadnorwood.com/beermug.ico" style="border:0; margin: 2px;"/></a>';

        oMap.getContainer().appendChild(info);
      }


      function updateStatus(msg) {
        if (opts.statusId && opts.statusId.length > 0) {
          $("#"+opts.statusId).html(msg);
        }
        $.console('debug', "updateStatus(): "+msg);
      }
	  // note that updateStatus2 is used when decoding addreses, overwritten when we write results table. TODO: fix
      function updateStatus2(msg) {
        if (opts.listId && opts.listId.length > 0) {
          $("#"+opts.listId).html(msg);
        }
        $.console('debug', "updateStatus2(): "+msg);
      }

      function eventType(params) {

        // http://stackoverflow.com/questions/383402/is-javascript-s-new-keyword-considered-harmful
        if ( !(this instanceof eventType) ) return new eventType(params);

        this.filtersActive = false;
        for (var ii in params) this[ii] = params[ii];
        this.id = markers.types.length;
        markers.types.push(this);
      }


      function addEventObj(params) {

        // http://stackoverflow.com/questions/383402/is-javascript-s-new-keyword-considered-harmful
        if ( !(this instanceof addEventObj) ) return new addEventObj(params);

        //
        // defaults for event object
        //
        this.lt = 0;
        this.lg = 0;
        this.isDisplayed = false;
        this.coords = function() { return this.lt +","+ this.lg; }
        this.directionsUrl = function () { 
          return 'http://maps.google.com/maps?f=d&q='+ this.g1Address.replace(/ /g, '+').replace(/"/g,'%22');
        }
        this.directionsHtml = function () {
          return '<a href="'+ this.directionsUrl() +'" title="Get Directions using maps.google.com">Directions</a>';
        }

        //
        // overwrite defaults with params, except for a few cases
        //
        for (var ii in params) this[ii] = params[ii];
        this.start = $.mapFilter.parseDate(this.start);
        this.end   = $.mapFilter.parseDate(this.end);
		
        this.id    = markers.list.length;
        markers.list.push(this);
      }

	  // TODO: remove this?
      function getXmlData () {
        // jsoncallback=? 
        xmlUrl = 'http://feeds2.feedburner.com/torontoevents?format=xml&jsoncallback=?';
        if (xmlUrl.search(/^(http|feed)/i) < 0) {
          $.console('debug', "getXmlData(): bad url: "+ gCalUrl);
          return;
        }
        $.ajax({
            type: "GET",
            url: xmlUrl,
            dataType: "xml",
            dataType: "json",
            global: false,
            processData: false,
            dataFilter: function(xml) {
                $.console('debug', "xml2 data: %o", xml);
            },
            success: function(xml) {
                $.console('debug', "xml3 data: %o", xml);
/*
                $(xml).find('label').each(function(){
                     var id_text = $(this).attr('id')
                     var name_text = $(this).find('name').text()

                     $('<li></li>')
                         .html(name_text + ' (' + id_text + ')')
                         .appendTo('#update-target ol');
                 }); //close each(
*/
             }
        }); //close $.ajax

        return;
      }


      function fakeGCalData () {
          $.console('debug', "fakeGCalData() init");
          new eventType({
            title: 'Test1',
            titleLink: ''
          });

          for (var ii in fakeMarkersList) {
              kk = new addEventObj(fakeMarkersList[ii]);
          }

          $.console('debug', "fakeGCalData() using fake calendar data: ", markers);
          mapRedraw();
      }

      function getGCalData (gCalUrl) {
        if (gCalUrl == 'test1') return fakeGCalData();
        if (gCalUrl == 'test2') return getXmlData();

        if (gCalUrl.search(/^http/i) < 0) {
          $.console('warn', "getGCalData(): bad url: "+ gCalUrl);
          return;
        }
        gCalUrl = gCalUrl.replace(/\/basic$/, '/full');
        //startmax = '2009-07-09T10:57:00-08:00';
        today = new Date();
        startmin = $.mapFilter.rfc3339(today,false);
        $.console('debug', "getGCalData(): start-min: "+startmin);
        today.setTime(today.getTime() + ndays*24*3600*1000); //expires in ndays days (milliseconds) 
        startmax = $.mapFilter.rfc3339(today,true);
        $.console('debug', "getGCalData(): start-max: "+startmax);
        updateStatus('Loading <a href="'+gCalUrl+'">Calendar</a> ..');

        // http://code.google.com/apis/calendar/docs/2.0/reference.html
        $.getJSON(gCalUrl + "?alt=json-in-script&callback=?",
          {
            'start-min': startmin,
            'start-max': startmax,
            'max-results': 200,
            'orderby'  : 'starttime',
            'sortorder': 'ascending',
            'ctz'      : 'America/Chicago',
            'singleevents': false
          },
          function(cdata) {
      		updateStatus('Map and Calendar Loaded, Decoding Addresses ..');
			parseGCalData(cdata);
          });
      }


      function parseGCalData (cdata) {

          $("#"+ opts.listId ).append(' ..');
          $.console('debug', "parseGCalData() calendar data: ",cdata);

          if (cdata.feed.title) gcTitle = cdata.feed.title['$t'];
          if (cdata.feed.link) gcLink = cdata.feed.link[0]['href'];
          document.title = document.title +" - "+ gcTitle;

          updateStatus("<a title='Click to view Full Calendar' href='"+ gcLink +"'>"+ gcTitle +"</a><br>Mapping Events ... ");

          eType = new eventType({
            tableHeadHtml: "<td>one</td><td>two</td>",
            tableCols: [3,5],
            title: gcTitle,
            titleLink: gcLink
          });

          uniqAddr={};
		  totalEntries=0;
          if (cdata.feed.entry)
            $.each(cdata.feed.entry, function(i, entry) {
              var url;
              $.each(entry['link'], function(j, link) {
                if (link.type == 'text/html') url = link.href;
              });
              if (!entry['gd$when']) {
                $.console('debug', "skipping cal entry (no gd$when) %s (%o)", entry['title']['$t'], entry);
				return true; // continue to next one
              };
              kk = new addEventObj({
                type: eType.id,
                name: entry['title']['$t'],
                desc: entry['content']['$t'],
                origAddress: entry['gd$where'][0]['valueString'],
                g1Address: entry['gd$where'][0]['valueString'],
                g2Address: '',
                gCalId: entry['gCal$uid']['value'],
                url: url,
                start: $.mapFilter.parseDate(entry['gd$when'][0]['startTime']),
                end: $.mapFilter.parseDate(entry['gd$when'][0]['endTime'])
              });
              kk.g1Address = kk.g1Address.replace(/\([^\)]+\)\s*$/, ''); // make ready for geocode
              uniqAddr[kk.g1Address] = 1;
			  totalEntries++;
              $.console('debug', "parsed entry "+totalEntries+": ", entry['title']['$t'], entry, kk);             
            });          
		  uniqAddrCount = 0;
		  $.each(uniqAddr, function(key, val) { uniqAddrCount++; });
          //updateStatus2('Found '+ totalEntries +' events, '+ uniqAddrCount +' unique addresses, decoding .. ');

		  $.console('debug', "calling mapfilter.geocode(): ", uniqAddr );		  
		  myGeo = $.mapFilter.geocoder({
			addresses: uniqAddr,
			geocodedAddrCallback: function (gObj) {
				eventGeocoded(gObj);
			},
			geocodeCompleteCallback: function() {
				gCalComplete();
			}
		  });
          updateStatus2('Found '+ myGeo.numAddresses +' events, '+ myGeo.numUniqAddresses +' unique addresses, decoding .. ');

      }

	  /*
	   * gCalComplete() called once all addresses are decoded
	   */
	  function gCalComplete() {
		$.console('debug', "gCalComplete() begins");             
		if (opts.mapCenterLt == $.mapFilter.defaults.mapCenterLt) {
			mapAllEvents();
		} else {
			mapRedraw();
		}
	  }
	  
	  /*
	   * eventGeocoded() called from mapFilter.geocoder() after response from lookup
	   */
	  function eventGeocoded(gObj) {
	  	redrawMap = false;
	  	for (var i in markers.list) {
          kk = markers.list[i];
          if (kk.lt > 1) continue;

		  if (kk.g1Address != gObj.origAddr) continue;

          if (gObj.lt) {
            kk.lt = gObj.lt;
            kk.lg = gObj.lg;
            kk.g2Address = gObj.gAddr;
			redrawMap = true;
			
          }	else if (gObj.error) {
            kk.g2Address = 'address unrecognizable';
            kk.error = gObj.error;
          }	
	    }
        updateStatus2(myGeo.numUniqAddrDecoded() +' of '+ myGeo.numUniqAddresses +' decoded, '
		              + myGeo.numUniqAddrErrors() +' errors' );

		// update with new markers: map and results list
		//if (redrawMap) mapAllEvents();
		
	  }
	 

      function days2weeks(ndays) {
        if ($.mapFilter.days2weeks[ndays]) return $.mapFilter.days2weeks[ndays];
        return ndays + ' days';
      }

	  init();
	  return this;	
	}		


  //
  // private functions
  //

  // generated from stringify 
  var fakeMarkersList = [ {
        "lt": 41.9761944,
        "lg": -87.6683946,
        "isDisplayed": false,
        "type": 0,
        "name": "Midsommarfest ",
        "desc": "http://blog.andersonville.org/2009/05/19/midsommarfest-lineup-is-set/\n\nNow that is seems like it may stay on the warm side weather-wise, it is time for Andersonville lovers everywhere to turn their attention to the official kickoff of the summer festival season - Midsommarfest!  This year’s festival takes place on Saturday, June 13 and Sunday June 14 from 11:00 am - 10:00 pm both days and promises to once again be (in our humble opinion) the best party in the city.  We will have delicious food, great vendors, cold beer and 4 stages of music for your carefree dancing pleasure.  The line up on all 4 stages is now final - see it after the jump!\n",
        "origAddress": "5200 N Clark St, chicago IL",
        "g1Address": "5200 N Clark St, chicago IL",
        "g2Address": "5200 N Clark St, Chicago, IL 60640, USA",
        "gCalId": "feeb95353vsqr0fo6v5sr796a0@google.com",
        "url": "http://www.google.com/calendar/event?eid=ZmVlYjk1MzUzdnNxcjBmbzZ2NXNyNzk2YTAgdmYzdTdzNm9kajByNzRxNGxybmI3MzBwaGtAZw",
        "start": "2009-06-13T16:00:00Z",
        "end": "2009-06-15T03:00:00Z",
        "id": 0
      },
      {
        "lt": 41.8843101,
        "lg": -87.6199999,
        "isDisplayed": false,
        "type": 0,
        "name": "Chicago Blues Festival @ Grant Park",
        "desc": "http://blog.andersonville.org/2009/05/19/midsommarfest-lineup-is-set/\n\nNow that is seems like it may stay on the warm side weather-wise, it is time for Andersonville lovers everywhere to turn their attention to the official kickoff of the summer festival season - Midsommarfest!  This year’s festival takes place on Saturday, June 13 and Sunday June 14 from 11:00 am - 10:00 pm both days and promises to once again be (in our humble opinion) the best party in the city.  We will have delicious food, great vendors, cold beer and 4 stages of music for your carefree dancing pleasure.  The line up on all 4 stages is now final - see it after the jump!\n",
        "origAddress": "337 E Randolph Dr, Chicago, IL",
        "g1Address": "337 E Randolph Dr, Chicago, IL",
        "g2Address": "337 E Randolph Dr, Chicago, IL 60601, USA",
        "gCalId": "1fnj382f9ne17v02a0msigjup4@google.com",
        "url": "http://www.google.com/calendar/event?eid=MWZuajM4MmY5bmUxN3YwMmEwbXNpZ2p1cDQgdmYzdTdzNm9kajByNzRxNGxybmI3MzBwaGtAZw",
        "start": "2009-06-13T17:00:00Z",
        "end": "2009-06-14T03:00:00Z",
        "id": 1
      },
      {
        "lt": 41.922587,
        "lg": -87.706474,
        "isDisplayed": false,
        "type": 0,
        "name": "Karolina's Chili Bday",
        "desc": "http://blog.andersonville.org/2009/05/19/midsommarfest-lineup-is-set/\n\nNow that is seems like it may stay on the warm side weather-wise, it is time for Andersonville lovers everywhere to turn their attention to the official kickoff of the summer festival season - Midsommarfest!  This year’s festival takes place on Saturday, June 13 and Sunday June 14 from 11:00 am - 10:00 pm both days and promises to once again be (in our humble opinion) the best party in the city.  We will have delicious food, great vendors, cold beer and 4 stages of music for your carefree dancing pleasure.  The line up on all 4 stages is now final - see it after the jump!\n",
        "origAddress": "2245 N. Kedzie blvd Apt. 1, chicago",
        "g1Address": "2245 N. Kedzie blvd Apt. 1, chicago",
        "g2Address": "2245 N Kedzie Blvd, Chicago, IL 60647, USA",
        "gCalId": "7be2naloepse2urkp1dt3vbjpc@google.com",
        "url": "http://www.google.com/calendar/event?eid=N2JlMm5hbG9lcHNlMnVya3AxZHQzdmJqcGMgdmYzdTdzNm9kajByNzRxNGxybmI3MzBwaGtAZw",
        "start": "2009-06-13T22:00:00Z",
        "end": "2009-06-14T03:00:00Z",
        "id": 2
      },
      {
        "lt": 41.879535,
        "lg": -87.624333,
        "isDisplayed": false,
        "type": 0,
        "name": "Chicago Naked Ride",
        "desc": "http://chicagonakedride.org/\n\n6pm Gathering\n9pm Ride",
        "origAddress": "chicago",
        "g1Address": "chicago",
        "g2Address": "Chicago, IL, USA",
        "gCalId": "bfldar88a32o6pk0v7h2riavts@google.com",
        "url": "http://www.google.com/calendar/event?eid=YmZsZGFyODhhMzJvNnBrMHY3aDJyaWF2dHMgdmYzdTdzNm9kajByNzRxNGxybmI3MzBwaGtAZw",
        "start": "2009-06-13T23:00:00Z",
        "end": "2009-06-14T03:00:00Z",
        "id": 3
      },
      {
        "lt": 41.904149,
        "lg": -87.689781,
        "isDisplayed": false,
        "type": 0,
        "name": "Jeff Schlager Bday",
        "desc": "8pm - The 2nd Bi-annual Chicago-style Pizza Challenge! I will be ordering deep dish pizza from 4 famous Chicago pizza places. We'll vote on who truly has the best pie. \nGeno's East, Art of Pizza, Lou Malnatti's, and Pequods. (Assuming they all deliver here) Address: 1236 N Campbell Ave #3\n\n10:30pm - Wicker Park Dive Bar Tour - Going to these bars in this order:\n\n1. Rainbo Club - 1150 N Damen\n2. Happy Village - 1059 N Wolcott\n3. Goldstar - 1755 W Division\n4. Phyllis Inn - 1800 W Division\n\nMy number is 612-801-8679. Please text me to find out where we are at or headed when you're on your way. Looking forward to seeing y'all!\n\nhttp://www.facebook.com/inbox/readmessage.php?t=1047301593885#/event.php?eid=83126074267",
        "origAddress": "1236 N Campbell Ave #3, Chicago IL",
        "g1Address": "1236 N Campbell Ave #3, Chicago IL",
        "g2Address": "1236 N Campbell Ave, Chicago, IL 60622, USA",
        "gCalId": "f5vlkak8njemehkm1qccu0inko@google.com",
        "url": "http://www.google.com/calendar/event?eid=ZjV2bGthazhuamVtZWhrbTFxY2N1MGlua28gdmYzdTdzNm9kajByNzRxNGxybmI3MzBwaGtAZw",
        "start": "2009-06-14T01:00:00Z",
        "end": "2009-06-14T08:00:00Z",
        "id": 4
      },
      {
        "lt": 41.8843101,
        "lg": -87.6199999,
        "isDisplayed": false,
        "type": 0,
        "name": "Blues Festival @ Grant Park",
        "desc": "http://blog.andersonville.org/2009/05/19/midsommarfest-lineup-is-set/\n\nNow that is seems like it may stay on the warm side weather-wise, it is time for Andersonville lovers everywhere to turn their attention to the official kickoff of the summer festival season - Midsommarfest!  This year’s festival takes place on Saturday, June 13 and Sunday June 14 from 11:00 am - 10:00 pm both days and promises to once again be (in our humble opinion) the best party in the city.  We will have delicious food, great vendors, cold beer and 4 stages of music for your carefree dancing pleasure.  The line up on all 4 stages is now final - see it after the jump!\n",
        "origAddress": "337 E Randolph Dr, Chicago, IL (Grant Park)",
        "g1Address": "337 E Randolph Dr, Chicago, IL ",
        "g2Address": "337 E Randolph Dr, Chicago, IL 60601, USA",
        "gCalId": "a2p6976vtpdb8vmtv43avp8gdk@google.com",
        "url": "http://www.google.com/calendar/event?eid=YTJwNjk3NnZ0cGRiOHZtdHY0M2F2cDhnZGsgdmYzdTdzNm9kajByNzRxNGxybmI3MzBwaGtAZw",
        "start": "2009-06-14T17:00:00Z",
        "end": "2009-06-15T03:00:00Z",
        "id": 5
      } 
	];

  // 
  // mapFilter obj: helper data and functions, globally accessible
  // 
  $.mapFilter = {

    // 
    // default options, can be overwritten when calling mapFilter() or like this:
    // $.mapFilter.defaults.gCalDays = 3;
    // 
    defaults:  {
      gCalDays: 7,
      jumpTxt: " Jump to a City, Address, or Zip",

      // div id's used by mapFilter
      mapId: "MapID",  
      listId: "resultsTab",
      statusId: "MapStatus",

      mapH: 800,
      mapw: 600,
      // Chicago = 41.885405,-87.626072
      mapCenterLt: '41.885405',
      mapCenterLg: '-87.626072',
      mapZoom: 14,
      mapType: 0,
	  
	  // should map zoom to first calendar entry as it loads data?
	  zoomToCal: true,

      unSupportedHtml: "Unfortunately your browser doesn't support Google Maps.<br /> To check browser compatibility visit the following <a href=\"http://local.google.com/support/bin/answer.py?answer=16532&topic=1499\">link</a>.",

      googleApiKey: 'ABQIAAAAQ8l06ldZX6JSGI8gETtVhhTrRIj9DJoJiLGtM4J1SrTlGmVDcxQDT5BVw88R8j75IQxYlwFcEw6w9w' // chadnorwood.com
      
    },

	// opts is populated by function $().mapfilter()
	opts: {},  
	
    days2weeks: {
      1: 'today',
      7: 'week',
      31: 'month',
      365: 'year'
    },

    // jquery.mapFilter.hItem() should be called whenever user clicks on 
    // map marker or clicks on name in results tab,
    // Opens info window for marker and highights name in results tab
    hItem: function(n, cur) {

          if (typeof n == 'number') {
            // clicked on list, only one to be displayed in map info window
            kks = [n]; 
            gMrkr = markers.gMarkers[markers.list[n].coords()].gMarker;
          } else 
          if (typeof n == 'string') {
            // clicked on map, could be more than one event
            kks = markers.gMarkers[n].list;
            gMrkr = markers.gMarkers[n].gMarker;
          }

          if (!cur) cur = 0;
          infoHtml = markers.list[kks[cur]].infoHtml;
          if (kks.length > 1) {
            href = '<a href="javascript:void(0)" onclick="$.mapFilter.hItem(\''+ n +'\',';
            prev =  (cur == 0) ? '' : href + (cur-1) +')">&lt;&lt; prev</a> - '; 
            next =  (cur == (kks.length-1)) ? '' : ' - '+ href + (cur+1) + ')">next &gt;&gt;</a> '; 
            infoHtml += "<p><div style='text-align:center'>"+ prev +"Showing "+ (cur+1) +" of "+ kks.length;
            infoHtml += " Events at this location"+ next +"</div>";
          }

          $.console('debug', "hItem(%s) list %o", n, kks);

          // remove any previous highlights and highlight new ones
          $("#"+ opts.listId +" a").removeClass("highlight2");

          $("#"+ opts.listId +" a[onclick*=hItem("+ kks[cur] +")]").addClass("highlight2");
          for (var ii in kks) {
            //$("#"+ opts.listId +" a[onclick*=hItem("+ kks[ii] +")]").addClass("highlight2");
          }

          // open info window for item and center it
          map.closeInfoWindow();
          //map.panTo(gMrkr.getLatLng());
          gMrkr.openInfoWindowHtml(infoHtml);

          //mapChanged();
          return false;
    },

    zoomTo: function(id) {
        newZoom = (map.getZoom() >= 19) ? 19 : (map.getZoom() + 1);
        coords = markers.list[id].coords();
        map.setCenter(markers.gMarkers[coords].gMarker.getLatLng(), newZoom);
        //mapChanged();
    },

    jumpToAddress: function(address) {

          // if its a street address, we want to zoom in more than if city or country
          // assuming street address if address contains a comma
          if (address.search(/,/) == -1) {cZoom=11;} else {cZoom=16;}

          map.closeInfoWindow();
          geocoder.getLatLng( address, function(point) {
              if (!point) {
                // focus on something else so when user clicks on jumpBox again, it will clear
                $("#LogoInfo").focus;
                jumptxt = "NOT FOUND: "+ address;
                $("#jumpBox").val(jumptxt);
              } else {
                jumptxt = '';
                map.setCenter(point, cZoom);
              }
          });
    },

	/*
	 * geocoder object handles the geocoding of addresses 
	 * 
	 * TODO: use more than just google maps api
	 * 
     * http://tinygeocoder.com/blog/how-to-use/
     * http://github.com/straup/js-geocoder
     * http://www.geowebguru.com/articles/185-arcgis-javascript-api-part-1-getting-started
     * 
	 * TODO: use GeoAPI's reverse geocoder to get neighborhood (SOMA/Mission/marina, SF CA)
	 * 
     * TODO: create a local geocode cache server and mysql db?
     * http://code.google.com/apis/maps/articles/phpsqlajax.html
	 */
    geocoder: function (gOpts) {
		 		
	  /* count addreses and unique addreses.
	   * don't want duplicates - wasting calls to google
       */
      var uniqAddresses = {};
	  var numAddresses = numUniqAddresses = numUniqAddrDecoded = numUniqAddrErrors = 0;

	  var gQueue = new Array();
      var retried = {};
	  var numReqs = 0;
	  var startTime = new Date().getTime();
	  
      for (var ii in gOpts.addresses) {
	  	numAddresses++;
        if (!isNaN(ii)) uniqAddresses[gOpts.addresses[ii]] = 1; // array
        else  uniqAddresses[ii] = 1; // object
      } 
      for (var addr in uniqAddresses) {
	  	gQueue.push(addr);
	  	numUniqAddresses++;
	  }
	  
	  /*
	   * gGeocodeQueue() 
	   *  
       * Note: Google allows 15k lookups per day per IP.  However, too many requests
       * at the same time triggers a 620 code from google.  Therefore we want about 100ms
       * delay between each request using gGeocodeQueue.  Likewise, when we get a 620 code,
       * we wait a bit and resubmit.
       * http://code.google.com/apis/maps/faq.html#geocoder_limit
       * 
       * NOTE: yahoo allows 5k lookups per day per IP
       * http://developer.yahoo.com/maps/rest/V1/geocode.html
       */
	  var desiredRate = 100; // average long-term should be one query every 100ms
	  var maxBurstReq = 5;   // if timeout gets delayed, say 500ms, we can send 'maxBurstReq' at a time till we catch up
      var maxRetry = 4;
	  function gGeocodeQueue () {
                    
		if (numUniqAddrDecoded + numUniqAddrErrors >= numUniqAddresses) {
			if (gQueue.length > 0) {
				$.console('error', "gGeocodeQueue() bad addition");				
			} else {
				$.console("debug", "gGeocodeQueue() all queries complete, geocoder done");
		        gOpts.geocodeCompleteCallback();
				return;
			}
		}
		ems = new Date().getTime() - startTime;
		bursts = maxBurstReq;
		while (bursts-- && gQueue.length && (ems > numReqs*desiredRate )) {
			numReqs++;
			addr = gQueue.shift(); 
            $.console('debug', "    gGeocodeQueue() sending req "+numReqs+" at "+ems+"ms, addr: ", addr);
        	$.mapFilter.gGeocode( addr, function (gObj) {
			  	parseGObj(gObj);
			});
			ems = new Date().getTime() - startTime;
		}	
		setTimeout(function() { gGeocodeQueue() }, 
					desiredRate);
      }
	  
	  function parseGObj(gObj) {
        if (typeof(gObj) != 'object') {
          $.console('error', "parseGObj() shouldn't be here " + typeof(gObj), gObj);
          return;
        }
        if (gObj.tmpError && retried[gObj.origAddr] && (retried[gObj.origAddr] > maxRetry) ){
          gObj.error += " Max Retry "+ maxRetry;
          $.console('debug', "parseGObj() resubmit limit reached. ", gObj);
          numUniqAddrErrors++;
          gOpts.geocodedAddrCallback(gObj);

        } else if (gObj.tmpError) {
          retried[gObj.origAddr] = (retried[gObj.origAddr]) ? retried[gObj.origAddr]++ : 1;
		  if (gObj.errorCode == 620) {
            $.console('debug', "parseGObj() resubmit (too fast)", gObj.origAddr);
		  	desiredRate = 1.1 * desiredRate; // slow down requests
		  } else {
		  	$.console('debug', "parseGObj() resubmit (tiemout) ", gObj.origAddr);
		  }
          gQueue.push(gObj.origAddr);
          //gGeocodeQueue();  // no need to call, only quits when decode+errors > numUniqAddr

        } else if (gObj.error) {
          $.console('debug', "parseGObj() error ", gObj);
          numUniqAddrErrors++;
          gOpts.geocodedAddrCallback(gObj);

        } else if (gObj.lt) {
          $.console('debug', "parseGObj() got coords ", gObj);
		  if (!uniqAddresses[gObj.origAddr]) $.console('error', "parseGObj() debug me", gObj);
		  uniqAddresses[gObj.origAddr] = gObj;
          numUniqAddrDecoded++;
          gOpts.geocodedAddrCallback(gObj);

        } else {
          $.console('error', "parseGObj() should not be here ", gObj);
        }
      }
	  gGeocodeQueue();
	  
      return {
			numAddresses: numAddresses,
			numUniqAddresses: numUniqAddresses,
			numUniqAddrDecoded: function(){
				return numUniqAddrDecoded;
			},
			numUniqAddrErrors:  function(){
				return numUniqAddrErrors;
			},
		}
	},

	/*  description of gObj
	geocodeObj: {
      lg: null, // number, -180 +180
      lt: null, // number, -90 +90
      gAddr: null, // string, google's rewording of origAddr
      origAddr: null, // string, address passed to geocoder
      errorCode: null, // number
      tmpError: false, // boolean
      error: null // string error msg
	},
	*/
	
    //
    // gGeocode translates addresses into array of lat/lng coords using Google
    // 
    gGeocode: function(addr, callback) {
        //$.console('debug', "gGeocode() submitting addr to google: " + addr); 
        $("#"+ opts.listId ).append('.');
		
		/*
		 *  switched from getJson to ajax to handle errors.  However, looks like error function is not called 
		 *  when google responds with http 400 and text/html (versus http 200 with text/javascript) 
		 */
		// http://groups.google.com/group/google-maps-api/browse_thread/thread/e347b370e8586767/ddf95bdb0fc6a9f7?lnk=raot
        geoUrl = 'http://maps.google.com/maps/geo?'
				+ '&key='+ opts.googleApiKey 
				+ '&q='+ escape(addr)
				+ '&sensor=false&output=json'
				+ '&callback=?';
        //geoUrl = 'http://maps.google.com/maps/geo?callback=?';
		$.ajax({
            type: "GET",
            url: geoUrl,
            dataType: "json",
            //global: false,
			error: function (XMLHttpRequest, textStatus, errorThrown) {
                $.console('debug', "gGeocode() error for "+ geoUrl);
			},
			// complete is only called after success, not on error, therefore useless
			//complete: function (XMLHttpRequest, textStatus) {
  			//	$.console('debug', "gGeocode() complete ", textStatus, XMLHttpRequest);
			//},
            success: function(data, textStatus) {
                $.console('debug', "gGeocode() success() status,data: ", textStatus, data);
	            $("#"+ opts.listId ).append('.');
	            if (data.Placemark) {
	              callback( { 
	                lg: data.Placemark[0].Point.coordinates[0],
	                lt: data.Placemark[0].Point.coordinates[1],
	                gAddr: data.Placemark[0].address,
	                origAddr: data.name
	              });
	            } else { 
	              callback( {
	                // http://code.google.com/apis/maps/documentation/geocoding/index.html#StatusCodes
	                origAddr: data.name,
	                errorCode: data.Status.code,
	                tmpError: (data.Status.code == 620) || (data.Status.code == 500) || (data.Status.code == 610),
	                error: (data.Status.code) ? 
	                  ((602==data.Status.code) ? "Unknown Address" : 
	                  ((620==data.Status.code) ? "620: Too Many Lookups" : "Google code: "+data.Status.code)) :
	                  "Google geocode api changed"
	              });
	            }
             }
        }); //close $.ajax
    },

    maxStr: function(str, maxChars, maxLines, link) {
        shorten = false;
        if ((maxChars > 1) && (str.length > maxChars)) {
          shorten = true;
          str = str.substring(0,maxChars);
        }
        if (maxLines > 0) {
          for (ii=0; ii >= 0;  ii=str.indexOf("\n",ii+1) ) {
            if ((ii >= 0) && (maxLines-- < 1)) {
              shorten = true;
              str = str.substring(0,ii);
              break;
            }
          }
        }
        if (!shorten) return str;

        if (link && link.length > 1) {
          if (link.match(/^http/i)) link = "<a href='"+ link +"'>more</a>";
          return str + "...\n ("+ link +")";
          //return str.substring(0,str.length-5) + "...\n ("+ link +")";
        }
        return str + "...";
    },


    rfc3339: function(d, clearhours) {
      s = d.getUTCFullYear() +
        "-" + $.mapFilter.zeroPad(d.getUTCMonth() + 1) +
        "-" + $.mapFilter.zeroPad(d.getUTCDate());
      if (clearhours) {
        s += "T00:00:00"; 
      } else {
        s += 
        "T" + $.mapFilter.zeroPad(d.getUTCHours()) +
        ":" + $.mapFilter.zeroPad(d.getUTCMinutes()) +
        ":" + $.mapFilter.zeroPad(d.getUTCSeconds());
      } 
      return s + "-08:00"; // chicago offset
    },

    //
    // The following date and time routines from fullCalendar
    //
    zeroPad: function(n) {
      return (n < 10 ? '0' : '') + n;
    },

    monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
    monthAbbrevs: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
    dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
    dayAbbrevs: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],

    formatDate: function(d, format) {
      var f = $.mapFilter.dateFormatters;
      var s = '';
      for (var i=0; i<format.length; i++) {
        var c = format.charAt(i);
        if (f[c]) {
          s += f[c](d);
        }else{
          s += c;
        }
      }
      return s;
    },

    dateFormatters: {
      'a': function(d) { return d.getHours() < 12 ? 'am' : 'pm' },
      'A': function(d) { return d.getHours() < 12 ? 'AM' : 'PM' },
      'x': function(d) { return d.getHours() < 12 ? 'a' : 'p' },
      'X': function(d) { return d.getHours() < 12 ? 'A' : 'P' },
      'g': function(d) { return d.getHours() % 12 || 12 },
      'G': function(d) { return d.getHours() },
      'h': function(d) { return $.mapFilter.zeroPad(d.getHours() %12 || 12) },
      'H': function(d) { return $.mapFilter.zeroPad(d.getHours()) },
      'i': function(d) { return $.mapFilter.zeroPad(d.getMinutes()) },
      'L': function(d) { return $.mapFilter.zeroPad(d.getDate()) },
      'D': function(d) { return d.getDate() }, // day of month
      'd': function(d) { return $.mapFilter.dayAbbrevs[d.getDay()] },
      'l': function(d) { return $.mapFilter.dayNames[d.getDay()] },
      'F': function(d) { return $.mapFilter.monthNames[d.getMonth()] },
      'm': function(d) { return $.mapFilter.zeroPad(d.getMonth() + 1) },
      'M': function(d) { return $.mapFilter.monthAbbrevs[d.getMonth()] },
      'n': function(d) { return d.getMonth() + 1 },
      'Y': function(d) { return d.getFullYear() },
      'y': function(d) { return (d.getFullYear()+'').substring(2) },
      'c': function(d) {
        // ISO8601. derived from http://delete.me.uk/2005/03/iso8601.html
        return d.getUTCFullYear() +
          "-" + $.mapFilter.zeroPad(d.getUTCMonth() + 1) +
          "-" + $.mapFilter.zeroPad(d.getUTCDate()) +
          "T" + $.mapFilter.zeroPad(d.getUTCHours()) +
          ":" + $.mapFilter.zeroPad(d.getUTCMinutes()) +
          ":" + $.mapFilter.zeroPad(d.getUTCSeconds()) +
          "Z";
      }
    },

    parseDate: function(s) {
      if (typeof s == 'object')
        return s; // already a Date object
      if (typeof s == 'undefined')
        return null;
      if (typeof s == 'number')
        return new Date(s * 1000);
      return $.mapFilter.parseISO8601(s, true) ||
             Date.parse(s) ||
             new Date(parseInt(s) * 1000);
    },

    parseISO8601: function(s, ignoreTimezone) {
      // derived from http://delete.me.uk/2005/03/iso8601.html
      var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" +
        "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?" +
        "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?";
      var d = s.match(new RegExp(regexp));
      if (!d) return null;
      var offset = 0;
      var date = new Date(d[1], 0, 1);
      if (d[3]) { date.setMonth(d[3] - 1); }
      if (d[5]) { date.setDate(d[5]); }
      if (d[7]) { date.setHours(d[7]); }
      if (d[8]) { date.setMinutes(d[8]); }
      if (d[10]) { date.setSeconds(d[10]); }
      if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
      if (!ignoreTimezone) {
        if (d[14]) {
          offset = (Number(d[16]) * 60) + Number(d[17]);
          offset *= ((d[15] == '-') ? 1 : -1);
        }
        offset -= date.getTimezoneOffset();
      }
      return new Date(Number(date) + (offset * 60 * 1000));
    }

    //
    // end of fullCalendar
    //
 
  };


})(jQuery);

