/* circles.js
 * For the circles on the map representing tweeters.
 *--------------------------------------------------------------------------*/

/* Date object to display current date */
var CurrentDate = {

  /* months:        Array of months to convert number format into alphabet
     date:          Date of the month       
     lmonth:        Current month in alphabet
     year:          Current year
  */
  months : new Array(13),
  date : null,
  lmonth : null,
  year : null,

  /* initialize months variable */
     init : function(){
    CurrentDate.months[1]="January";
    CurrentDate.months[2]="February";
    CurrentDate.months[3]="March";
    CurrentDate.months[4]="April";
    CurrentDate.months[5]="May";
    CurrentDate.months[6]="June";
    CurrentDate.months[7]="July";
    CurrentDate.months[8]="August";
    CurrentDate.months[9]="September";
    CurrentDate.months[10]="October";
    CurrentDate.months[11]="November";
    CurrentDate.months[12]="December";
  },

  /* Return the current date in <Month Date, Year> format
       ex) December 25, 2009 */
  get_current_date : function(){
    var time = new Date();

    CurrentDate.lmonth = CurrentDate.months[time.getMonth() + 1];
    CurrentDate.date = time.getDate();
    CurrentDate.year = time.getYear();

    if (CurrentDate.year < 2000){
      CurrentDate.year = CurrentDate.year + 1900;
    }

    return CurrentDate.lmonth + " " + CurrentDate.date + ", " + CurrentDate.year; 
  },

  /* Subtract date_ from current date and return in <Month Date, Year> format */
  sub_date : function(date_){
    var time = new Date();
    time.setDate(time.getDate() - date_);

    CurrentDate.lmonth = CurrentDate.months[time.getMonth() + 1];
    CurrentDate.date = time.getDate();
    CurrentDate.year = time.getYear();      

    if (CurrentDate.year < 2000){
      CurrentDate.year = CurrentDate.year + 1900;
    }

    return CurrentDate.lmonth + " " + CurrentDate.date + ", " + CurrentDate.year;
  },

  /* Add date_ from current date and return in <Month Date, Year> format */
  add_date : function(date_){
    var time = new Date();
    time.setDate(time.getDate() + date_);

    CurrentDate.lmonth = CurrentDate.months[time.getMonth() + 1];
    CurrentDate.date = time.getDate();
    CurrentDate.year = time.getYear();

    if (CurrentDate.year < 2000){
      CurrentDate.year = CurrentDate.year + 1900;
    }

    return CurrentDate.lmonth + " " + CurrentDate.date + ", " + CurrentDate.year;
  }
}; /* CurrentDate */   


var CIRCLES = {

  /* daysToAnimate: How many days from current date to go back to animate
     curDate:       cursor
     sleepTime:     How many miliseconds to pause between each animation frame.
                    Try not to have this value shorter than 1000 miliseconds.
     flag:          Set to 1 if backward or forward button is pressed so that
                    it stops the animation.
  */            
  timer_ : null,  
  daysToAnimate_ : 7,
  curDate_ : null,
  sleepTime_ : 2000,
  flag_ : 0,
  date : null,  

  api_ : null,
  map_ : null,
  date_ : null,
  author_ : null,
  keyword_: null,
  startDR_ : null,
  endDR_ : null,
  datacache_ : null,
  evth_ : [],  /* event handlers */
  listeners_ : [],
  pt_ : null,
  metadata_ : null,

  //         0     1    2     3     4   
  zooms_ : [ 300  ,300 ,300  ,300  ,300,
             200  ,100 ,50   ,20   ,10 
             ,5   ,1   ,0.75 ,0.50 ,0.25,
             0.10,0.05,0.01 ,0.005,0.001
             ,0.0005 ],
           
            
            

  init : function(api, map) {
    CIRCLES.api_ = api;
    CIRCLES.map_ = map;
    CIRCLES.curDate_ = CIRCLES.daysToAnimate_;
    CIRCLES.date = CurrentDate;
    CIRCLES.date.init();
    GEvent.addListener(map, 'zoomend',
                       function(oz, nz) { 
                         if (CIRCLES.zooms_[oz] != CIRCLES.zooms_[nz]) {
                           CIRCLES.redraw();
                           /* redraw the marker on the map */
                           bubble.redraw_marker();
                         }
                       });
  },

  clear_filters : function() { 
    CIRCLES.date_ = null;
    CIRCLES.author_ = null;
    CIRCLES.keyword_ = null;
    CIRCLES.startDR_ = null;
    CIRCLES.endDR_ = null;
    CIRCLES.reload();
  },
  
  clear_filters_no_reload : function() { 
//    CIRCLES.date_ = null;
    CIRCLES.author_ = null;
    CIRCLES.keyword_ = null;
//    CIRCLES.startDR_ = null;
//    CIRCLES.endDR_ = null;
  },

  set_date : function(filter_date) {
//    CIRCLES.clear_filters_no_reload();
    CIRCLES.date_ = filter_date;
    CIRCLES.reload();
  },

  set_author : function(filter_author) {
    CIRCLES.clear_filters_no_reload();
    CIRCLES.author_ = filter_author;
    CIRCLES.reload();
  },

  set_keyword : function(filter_keyword) {
    CIRCLES.clear_filters_no_reload();
    CIRCLES.keyword_ = filter_keyword;
    CIRCLES.reload();
  },
  
  set_DR : function(start, end) {
    CIRCLES.clear_filters_no_reload();
    CIRCLES.startDR_ = start;
    CIRCLES.endDR_ = end;
    CIRCLES.reload();
  },

  /* Listen for events from this.  Arg should be an object with
     the following functions on it:

     filterby : function([args]){...}
       - Called when user wants to filter the tweets by a location
         name.
  */
  add_listener : function(cb) {
    CIRCLES.listeners_.push(cb);
  },


  /* Private method used to notify all listeners when an event 
     occurs. */
  notify : function(what, args) {
    for(var i=0; i < CIRCLES.listeners_.length; ++i) {
      CIRCLES.listeners_[i][what](args);
    }
  },    


  /* Show the map of the first frame */
  move_backward : function() {
    CIRCLES.erase();

    /* Stop the animation */ 
    CIRCLES.flag_ = 1;

    document.getElementById("play").innerHTML = 
        CIRCLES.date.sub_date(CIRCLES.daysToAnimate_) +  
         " - " + CIRCLES.date.sub_date(CIRCLES.daysToAnimate_);
    CIRCLES.api_.geodata(CIRCLES.load_complete, CIRCLES.daysToAnimate_, CIRCLES.date_, CIRCLES.author_, CIRCLES.keyword_, CIRCLES.startDR_, CIRCLES.endDR_);
  },

  /* Show the map of the last frame */
  move_forward : function() {  
    CIRCLES.erase();  

    /* Stop the animation */
    CIRCLES.flag_ = 1;

    document.getElementById("play").innerHTML = 
        CIRCLES.date.get_current_date() +                  
         " - " + CIRCLES.date.get_current_date(); 
    CIRCLES.api_.geodata(CIRCLES.load_complete, CIRCLES.curDate_, CIRCLES.date_, CIRCLES.author_, CIRCLES.keyword_, CIRCLES.startDR_, CIRCLES.endDR_);  
  },  

  /* Play button is pressed */
  start_animation : function(){    
    /* Set the flag back to 0 so that it will animate again */
    CIRCLES.flag_ = 0;

    CIRCLES.curDate_ = CIRCLES.daysToAnimate_;
    CIRCLES.draw_curDate();
    
  },

  /* Draw the google map of the curDate */
  draw_curDate : function() {
    /* Check to see if the forward or the backward button is pressed 
       before drawing the map */
    if (CIRCLES.flag_ === 0){
      CIRCLES.erase();
      document.getElementById("play").innerHTML = 
        CIRCLES.date.sub_date(CIRCLES.curDate_) +                  
         " - " + CIRCLES.date.get_current_date();
 
      CIRCLES.api_.geodata(CIRCLES.animate_next_day, CIRCLES.curDate_, CIRCLES.date_, CIRCLES.author_, CIRCLES.keyword_, CIRCLES.startDR_, CIRCLES.endDR_);
    }    
  },

  load : function() {
    CIRCLES.api_.geodata(CIRCLES.load_complete, null, CIRCLES.date_, CIRCLES.author_, CIRCLES.keyword_, CIRCLES.startDR_, CIRCLES.endDR_);
  },


  reload : function() {
    CIRCLES.api_.geodata(CIRCLES.reload_complete, null, CIRCLES.date_, CIRCLES.author_, CIRCLES.keyword_, CIRCLES.startDR_, CIRCLES.endDR_);
  },

  /* Call draw_curDate() on the next */ 
  animate_next_day : function(data) {
    /*If data is null, erase any existing circles on the map and do not call the 
      draw function */
    if(data === null){
      CIRCLES.erase();
    }
    else{
      CIRCLES.draw(data.data);

      if(CIRCLES.curDate_ > 0){         
        CIRCLES.curDate_ -= 1;
        clearTimeout(CIRCLES.timer_);         
        CIRCLES.timer_ = setTimeout('CIRCLES.draw_curDate()', CIRCLES.sleepTime_);
      }

      else{
        clearTimeout(CIRCLES.timer_);
      }
    }
  },  

  load_complete : function(data) {
    CIRCLES.draw(data.data);
  },


  reload_complete : function(data) {
    CIRCLES.erase()
    CIRCLES.draw(data.data);
    if (CIRCLES.pt_ || CIRCLES.metadata_) {
       CIRCLES.circle_clicked(CIRCLES.pt_, CIRCLES.metadata_);
       CIRCLES.pt_ = null;
       CIRCLES.metadata_ = null;
    }
    
  },


  redraw : function() {
    CIRCLES.erase();
    CIRCLES.draw(CIRCLES.datacache_);
  },


  erase : function() {
    /* XXX assumption: we are the only overlays on the map! */
    CIRCLES.map_.clearOverlays();
    for(var i=0; i < CIRCLES.evth_.length; ++i) {
      GEvent.removeListener(CIRCLES.evth_[i]);
    }
    CIRCLES.evth_ = [];
  },



  /* data is array of 5-tuples: [LAT, LNG, T_COUNT, U_COUNT, LOC_NAME ] 
   */
  draw : function(data) {
    CIRCLES.datacache_ = data;
    
    /* First problem: what are the ranges for tweet and user count? */
    var maxt = 0;
    var maxu = 0;
    for(var j=0; j < data.length; ++j) {
      if (data[j][2] > maxt) {
        maxt = data[j][2];
      }
      if (data[j][3] > maxu) {
        maxu = data[j][3];
      }
    }

    for(var i=0; i < data.length; ++i) { 

      /* COLOR = more red means more tweets */
      var color =
      CIRCLES.color2hex(CIRCLES.hsv2rgb(0, 35 + (65 * (data[i][2]/maxt)), 100));

      /* SIZE = larger means more users */
      var zoomf = CIRCLES.zooms_[CIRCLES.map_.getZoom()];
      var poly = genCircle(new GLatLng(data[i][0], data[i][1]), 
                           CIRCLES.getSizeFactor(maxu, data[i][3]) * zoomf,
                           40, color, 2, null, color, null);
      CIRCLES.map_.addOverlay(poly);
      CIRCLES.register_listener(i, data[i][2], data[i][3], data[i][4], poly);
    }
  },


  /* Register click listener on the poly.  We need a function in order
     to create a closure to hold the metadata. */
  register_listener : function(cid, tcount, ucount, loc, poly) {
    var metadata = { 
      cid : cid,
      tcount: tcount,
      ucount: ucount,
      loc:    loc
    };
    var h = GEvent.addListener(poly, 'click', 
                               function(pt) {CIRCLES.circle_clicked(pt, metadata);});
    CIRCLES.evth_.push(h);
  },


  /* Show beautiful geodata popup on the map. */
  circle_clicked : function(pt, metadata) {
    /* If we don't have a pt, we got here from the top locations graph
       so we need to geocode the location in metadata to find out where
       to put our point on the map
    */
    
    var needwords = false;
    var needmetrics = false;
    if (CIRCLES.author_ != null || CIRCLES.date_ != null || CIRCLES.keyword_ != null || CIRCLES.startDR_ != null || CIRCLES.endDR_ != null) {
       CIRCLES.clear_filters();
       CIRCLES.pt_ = pt;
       CIRCLES.metadata_ = metadata;
    } else {
    /* We cache the keyword and metric info in the metadata, if it is
       not there then we need to look it up... */
    if (! metadata.words) {
      metadata.words = 'keywords: loading...';
      needwords = true;
    }
    if (! metadata.metrics) {
      metadata.metrics = 'metrics: loading...';
      needmetrics = true;
    }
    /* Metadata has the number of tweets and users in it, but
       since it is hard to keep in sync with the deck, it is
       omitted in the bubble. */
    CIRCLES.map_.openInfoWindowHtml(pt, 
                                    '<div class="geopopup" '
                                    + 'id="circle_pop_' + metadata.cid + '" >'
                                    + '<div class="geotxt">'
                                    + 'around <span class="place">' + metadata.loc 
                                    + '</span></div>'
                                    + '<div class="words">' + metadata.words 
                                    + '</div>'
                                    + '<div class="metrics">' 
                                    + metadata.metrics + '</div>'
                                    + '</div>');

    /* And trigger filterby event... */
      CIRCLES.filter_by(metadata.loc);

    /* Do the lookups after rendering the bubble, and after a short delay
       to reduce change of the request returning before we have a 
       chance to render. */
    if (needwords) {
      window.setTimeout(function() {
                          CIRCLES.api_.keywords(function(data) { 
                              CIRCLES.keywords_complete(metadata, data.data); 
                            },
                            metadata.loc);    
                        }, 1000);
    }
    if (needmetrics) {
      window.setTimeout(function() {
                          CIRCLES.api_.search_metrics(function(data) {
                                    CIRCLES.metrics_complete(metadata, data);
                                  },
                                  metadata.loc);
                        }, 1000);
    }
    }
  },


  /* Private callback function -- called when the keywords API call
     completes. */
  keywords_complete : function(metadata, data) {
    var dhtml = '';
    for (var i=0; i < data.length; ++i) {
      dhtml += '<a onclick="cloud.set_key(\'' + data[i][0] + 
            '\');">'+data[i][0]+'</a>';
      if (i+1 < data.length) {
        dhtml += ' / ';
      }
    }
    if (data.length === 0) {
      dhtml = '(no keyword data)';
    }
    metadata.words = dhtml;
    $("#circle_pop_" + metadata.cid + " .words").html(metadata.words);
  },


  /* Private callback function -- called when the search_metrics API call
     completes. */
  metrics_complete : function(metadata, data) {
    var dhtml = '';
    dhtml = data.day + " tweets today / "
    + data.week + " this week / "
    + data.month + " this month";
    metadata.metrics = dhtml;
    $("#circle_pop_" + metadata.cid + " .metrics").html(metadata.metrics);
  },


  filter_by : function(loc) {
    CIRCLES.notify('filterby', [loc]);
  },


  /* Compression function that returns a factor by which a circle radius
     should be multiplied based on how large cval is relative to cmax. 
   */
  getSizeFactor : function(cmax, cval) {
    if (cval == cmax) {
      return 1.0;
    }
    var q = cval / cmax;
    if (q > 0.8) {
      return 1.0;
    }
    if (q > 0.6) {
      return 0.75;
    }
    if (q > 0.2) {
      return 0.50;
    }
    return 0.25;
  },


  pad : function(s) {
    if (s.length < 2) {
      return "0" + s;
    }
    return s;
  },


  color2hex : function(c) {
    return ('#' + CIRCLES.pad(c.r.toString(16)) 
            + CIRCLES.pad(c.g.toString(16)) 
            + CIRCLES.pad(c.b.toString(16)));
  },


  hsv2rgb : function(hue, sat, val) {
    var red, grn, blu, i, f, p, q, t;
    hue%=360;
    if(val===0) {return({r:0, g:0, v:0});}
    sat/=100;
    val/=100;
    hue/=60;
    i = Math.floor(hue);
    f = hue-i;
    p = val*(1-sat);
    q = val*(1-(sat*f));
    t = val*(1-(sat*(1-f)));
    if (i===0) {red=val; grn=t; blu=p;}
    else if (i==1) {red=q; grn=val; blu=p;}
    else if (i==2) {red=p; grn=val; blu=t;}
    else if (i==3) {red=p; grn=q; blu=val;}
    else if (i==4) {red=t; grn=p; blu=val;}
    else if (i==5) {red=val; grn=p; blu=q;}
    red = Math.floor(red*255);
    grn = Math.floor(grn*255);
    blu = Math.floor(blu*255);
    return ({r:red, g:grn, b:blu});
  },


  rgb2hsv : function(red, grn, blu) {
    var x, val, f, i, hue, sat;
    red/=255;
    grn/=255;
    blu/=255;
    x = Math.min(Math.min(red, grn), blu);
    val = Math.max(Math.max(red, grn), blu);
    if (x==val){
      return({h:undefined, s:0, v:val*100});
    }
    f = (red == x) ? grn-blu : ((grn == x) ? blu-red : red-grn);
    i = (red == x) ? 3 : ((grn == x) ? 5 : 1);
    hue = Math.floor((i-f/(val-x))*60)%360;
    sat = Math.floor(((val-x)/val)*100);
    val = Math.floor(val*100);
    return({h:hue, s:sat, v:val});
  }

}; /* CIRCLES */




