// Copyright 2009 Google Inc.
// All Rights Reserved.
// Owner: webgroup-emea@google.com

/**
 * @fileoverview Javascript for the Transit Coverage Map.
 * @author amcgrath@google.com (Adam Mcgrath)
 */

/**
 * Namespace tmap (Transit Map).
 */
var tmap = window.tmap || {};

/**
 * Config object, containing urls, html templates and misc. strings
 * @enum {string}
 */
tmap.Config = {
  URL_GEODATA: 'js/transit.data.txt',
  URL_MM: 'http://gmaps-utility-library.googlecode.com/svn/trunk/' +
          'markermanager/release/src/markermanager_packed.js',
  URL_GREEN_ICON: 'img/marker_green.png',
  URL_YELLOW_ICON: 'img/marker_yellow.png',
  MAP_ID: 'map',
  HL_ID: 'recent-cities',
  COUNT_ID: 'city-count',
  COLOR: '#30842C',
  TMPL_AGENCY: '<p><a href="{link}">{title}</a> {desc}</p>',
  TMPL_AREA: '<div class="tMapInfoWindow">{agency}</div>',
  TMPL_REGION: '<div class="tMapInfoWindow"><h5>{title}</h5> {agencies}</div>',
  TMPL_AGENCY_LINK: '/maps?ie=UTF8&ll={latitude},{longitude}&spn=' +
                    '{spanlatitude},{spanlongitude}{tlparam}'
};

/**
 * Loader class for managing all the dependencies then initiating the app
 * when complete.
 * @constructor
 */
tmap.Loader = function() {
  /**
   * Check for Google Maps API loaded.
   * @type {Boolean}
   */
  this.mapsLoaded = false;

  /**
   * Check for DOM loaded.
   * @type {Boolean}
   */
  this.domLoaded = false;

  /**
   * Check for Marker Manager lib loaded.
   * @type {Boolean}
   */
  this.markerManagerLoaded = false;

  /**
   * Check for geo data loaded.
   * @type {Boolean}
   */
  this.geoDataLoaded = false;

  /**
   * Check for coverage data loaded.
   * @type {Boolean}
   */
  this.coverageLoaded = false;

  /**
   * Store callbacks for loader completing.
   * @type {Array.<Function>}
   */
  this.callbacks = [];

  /**
   * Data from coverage loading
   * @type {Object}
   */
  this.coverageData = {};
};

/**
 * Starts loading two of the dependenices: The Maps API and the Geo data.
 */
tmap.Loader.prototype.init = function() {
  // Load the maps API.
  google.load('maps', '2.148',
             {'callback': tmap.utils.bind(this.mapsLoadHandler, this),
              'language': tmap.LANG});

  // Load the geo data.
  tmap.utils.LazyLoader.getInstance().load(tmap.Config.URL_GEODATA,
      tmap.utils.bind(this.geoDataLoadHandler, this));
};

/**
 * Registers a callback to be fired once tmap.Loader has loaded.
 * @param {function(function)} callback Adds function to be called once loaded.
 */
tmap.Loader.prototype.registerCallback = function(callback) {
  this.callbacks.push(callback);
};

/**
 * @return {google.gdata.atom.Feed} The GData feed coverage object.
 */
tmap.Loader.prototype.getCoverageData = function() {
  return this.coverageData;
};

/**
 * Handles the Maps loaded event, loads some libraries that depends on the
 * existence of the Maps API.
 */
tmap.Loader.prototype.mapsLoadHandler = function() {
  window.onunload = google.maps.Unload;
  // tmap.setUpPolyMarker();
  this.mapsLoaded = true;
  tmap.utils.LazyLoader.getInstance().load(tmap.Config.URL_MM,
      tmap.utils.bind(this.markerManagerLoadHandler, this));
  this.complete();
};

/**
 * Handles the geo data loaded event.
 */
tmap.Loader.prototype.geoDataLoadHandler = function() {
  this.geoDataLoaded = true;
  this.complete();
};

/**
 * Handles the Marker Manager library loaded event.
 */
tmap.Loader.prototype.markerManagerLoadHandler = function() {
  this.markerManagerLoaded = true;
  this.complete();
};

/**
 * Handles the DOM loaded event, once the DOM structure of the HTML
 * page is complete.
 */
tmap.Loader.prototype.domLoadHandler = function() {
  this.domLoaded = true;
  this.complete();
};

/**
 * Handles the coverage data loaded event.
 * @param {google.gdata.atom.Feed} data The GData feed coverage object.
 */
tmap.Loader.prototype.coverageLoadHandler = function(data) {
  this.coverageLoaded = true;
  this.coverageData = data;
};

/**
 * Tests that all elements are loaded, then initiates the application.
 */
tmap.Loader.prototype.complete = function() {
  if (this.mapsLoaded && this.domLoaded && this.markerManagerLoaded &&
      this.coverageLoaded && this.geoDataLoaded) {
    for (var i = 0, len = this.callbacks.length; i < len; i++) {
      this.callbacks[i]();
    }
  }
};

/**
 * Main class for the application, formats the coverge data
 * and creates the map.
 * @param {google.gdata.atom.Feed} data The GData feed coverage object.
 * @constructor
 */
tmap.App = function(data) {
  this.regionsObj = new tmap.TransitRegions(data);
  this.regions = this.regionsObj.getRegions();
  this.regionsObj.showCount();
  this.regionsObj.showHighlights();
  
  var mapDiv = document.getElementById(tmap.Config.MAP_ID);
  if (!mapDiv) {
    return;
  }
  this.map = new google.maps.Map2(mapDiv);
  this.map.addControl(new google.maps.SmallMapControl());
  this.setMapCenterByClientLocation(new google.maps.LatLng(31, 0), 1);
  this.map.enableScrollWheelZoom();
};

/**
 * Populates the map with markers and manages them using Marker Manager lib.
 */
tmap.App.prototype.init = function() {
  if (!this.map) {
    return;
  }

  // Create an array of markers for agencies and regions.
  var regionMarkers = [];
  var agencyMarkers = [];

  var icon = new tmap.MapIcon(tmap.Config.URL_YELLOW_ICON).getIcon();

  for (var prop in this.regions) {
    var obj = this.regions[prop];

    // Create the infoWindow HTML for the marker.
    var agencies = [];
    for (var i = 0, len = obj.agencies.length; i < len; i++) {
      agencyItem = obj.agencies[i];
      var agencySnippet = tmap.utils.templatize(
          agencyItem, tmap.Config.TMPL_AGENCY);
      agencies.push(agencySnippet);
      var agencyHtml = tmap.utils.templatize({'agency': agencySnippet},
                                             tmap.Config.TMPL_AREA);
      var latlng = new google.maps.LatLng(agencyItem.lat, agencyItem.lng);
      var marker = new google.maps.Marker(latlng, icon);
      marker.bindInfoWindowHtml(agencyHtml);
      agencyMarkers.push(marker);
    }
    var html = tmap.utils.templatize({'title': obj.name,
        'agencies': agencies.join('')}, tmap.Config.TMPL_REGION);

    google.maps.Event.addListener(obj.polygon, 'click',
        tmap.utils.bind(function(latlng, html) {
          this.map.openInfoWindowHtml(latlng, html, {'maxWidth': 400});
        }, this, obj.marker.getPoint(), html));

    obj.marker.bindInfoWindowHtml(html);

    // Display the polygons on the map.
    this.map.addOverlay(obj.polygon);
    
    // Add the marker to regionMarkers for Marker Manager.
    regionMarkers.push(obj.marker);
  }

  var markerManager = new MarkerManager(this.map);
  markerManager.addMarkers(regionMarkers, 0, 3);
  markerManager.addMarkers(agencyMarkers, 4);
  markerManager.refresh();
};

/**
 * Adds functionality onto map to center by user location, or fall back
 * to default.
 * @param {google.maps.LatLng} defaultLatLng default if location isn't found.
 * @param {number} defaultZoom default if location isn't found.
 */
tmap.App.prototype.setMapCenterByClientLocation = function(defaultLatLng,
                                                           defaultZoom) {
  if (google.loader.ClientLocation) {
    var cc = google.loader.ClientLocation.address.country_code.toUpperCase();
    for (var continent in tmap.GEO_CONTINENTS) {
      if (tmap.arrayContains(tmap.GEO_CONTINENTS[continent]['countries'], cc)) {
        var cont = tmap.GEO_CONTINENTS[continent];
        if (continent != 'AF') {
          this.map.setCenter(new google.maps.LatLng(cont['lat'], cont['lng']),
              cont['zoom'] || 3);
          return;
        }
      }
    }
  }
  this.map.setCenter(defaultLatLng, defaultZoom);
};

/**
  * A transit region represents the cities and agencis in a given world region
  * @param {Object} obj The coverage data object.
  * @constructor
  */
tmap.TransitRegions = function(obj) {
  this.data = obj;
  this.regions = {};
  this.highlights = [];
  this.icon = new tmap.MapIcon(tmap.Config.URL_GREEN_ICON).getIcon();
};

/**
 * @return {Array.<Object>} The transit regions with associated coverage data.
 */
tmap.TransitRegions.prototype.getRegions = function() {
  for (var i = 0, len = this.data.feed.entry.length; i < len; i++) {
    var cItem = new tmap.CoverageItem(this.data.feed.entry[i]);
    var id = cItem.getId();
    if (cItem.highlight) {
      this.highlights.push(cItem);
    }
    if (tmap.GEO_DATA[id]) {
      var gdItem = tmap.GEO_DATA[id];
      var agencyObj = {
        'title': cItem.area,
        'desc': cItem.description,
        'link': cItem.getLink(),
        'lat': cItem.latitude,
        'lng': cItem.longitude
      };
      // If a region property already exists, append agency descrition to it.
      if (this.regions[id]) {
        this.regions[id].agencies.push(agencyObj);
      } else {
        // get the latlngs for polygon
        var latlngs = [];
        for (var j = 0, len2 = gdItem.area.length; j < len2; j++) {
          var ll = new google.maps.LatLng(gdItem.area[j][0],
                                          gdItem.area[j][1]);
          latlngs.push(ll);
        }

        var polygon = new google.maps.Polygon(latlngs, tmap.Config.COLOR, 0,
                                              0.5, tmap.Config.COLOR, 0.5, {});
        // The latlng for the marker can be set by geo data or calculated from
        // the polygon.
        var latlng = gdItem.center ? new google.maps.LatLng(gdItem.center[1],
                     gdItem.center[0]) : polygon.getBounds().getCenter();
        var marker = new google.maps.Marker(latlng, this.icon);

        // Create the region property.
        this.regions[id] = {
          'name': tmap.REGION_NAMES[id] || '',
          'marker': marker,
          'polygon': polygon,
          'agencies': [agencyObj]
        };
      }
    }
  };
  return this.regions;
};

/**
 * Shows the city count
 */
tmap.TransitRegions.prototype.showCount = function() {
  var countContainer = document.getElementById(tmap.Config.COUNT_ID);
  var tmpl = countContainer.innerHTML;
  var html = tmap.utils.templatize({num: this.data.feed.entry.length}, tmpl);
  countContainer.innerHTML = html;
  countContainer.style.display = 'inline';
};

/**
 * Shows the 'Recently highlighted' cities
 */
tmap.TransitRegions.prototype.showHighlights = function() {
  var hlContainer = document.getElementById(tmap.Config.HL_ID);
  var html = [];
  for (var i = 0, len = this.highlights.length; i < len; i++) {
    var lnk = '<a href="' + this.highlights[i].getLink() + '">' +
              this.highlights[i].getHighlightText() + '</a>';
    html.push(lnk);
  }
  hlContainer.innerHTML = html.join('\n');
};

/**
 * A coverage item represents a unique agency.
 * @param {google.gdata.atom.Feed.Entry} data An individual entry in the
 *    coverage feed list.
 * @constructor
 */
tmap.CoverageItem = function(data) {
  /**
   * 2 letter ISO Region id continent, US or CA.
   * @type {string}
   */
  this.region = data.gsx$region.$t;

  /**
   * 2 letter ISO Sub Region id Country or US/CA state.
   * @type {string}
   */
  this.subregion = data.gsx$subregion.$t;

  /**
   * Usually the name of a city.
   * @type {string}
   */
  this.area = data.gsx$area.$t;

  /**
   * Latitude of the coverage item.
   * @type {number}
   */
  this.latitude = data.gsx$latitude.$t;

  /**
   * Longitude of coverage item.
   * @type {string}
   */
  this.longitude = data.gsx$longitude.$t;

  /**
   * Span latitiude for coverage link.
   * @type {string}
   */
  this.spanlatitude = data.gsx$spanlatitude.$t;

  /**
   * Span longitude for coverage link.
   * @type {string}
   */
  this.spanlongitude = data.gsx$spanlongitude.$t;

  /**
   * Transit layer only param
   * @type {string}
   */
  this.tlparam = data.gsx$layeronly.$t == 'yes' ? '&lci=transit' : '&dirflg=r';

  /**
   * Description of coverage, usually names of transit agencies.
   * @type {string}
   */
  this.description = data.gsx$layeronly.$t == 'yes' ? tmap.MSG_LAYER :
                                                      data.gsx$description.$t;

  /**
   * If the coverage item should be highlighted on page, and the string
   * @type {bool}
   */
   this.highlight = tmap.CoverageItem.isEmptyCell(data.gsx$highlight.$t);

   /**
    * The coverage items highlight text
    * to display.
    * @type {'yes'|string}
    */
   this.highlightText = data.gsx$highlight.$t;

  /**
  * If the coverage item should be marked as 'New'.
  * @type {boolean}
  */
   this.isNew = data.gsx$new.$t == 'yes';

};

/**
 * Returns true if cell is empy or just contains spaces.
 * @param {string} str The cells contents.
 * @return {boolean} is cell empty.
 */
tmap.CoverageItem.isEmptyCell = function(str) {
  return !(/^ *$/.test(str));
};

/**
 * @return {string} ISO region code - ISO subregion code.
 */
tmap.CoverageItem.prototype.getId = function() {
  return this.region + '-' + this.subregion;
};

/**
 * @return {string} HTML string for the agency anchor tag.
 */
tmap.CoverageItem.prototype.getLink = function() {
  return tmap.utils.templatize(this, tmap.Config.TMPL_AGENCY_LINK);
};

/**
 * @return {string} Name of highlighted transit agency.
 */
tmap.CoverageItem.prototype.getHighlightText = function() {
  return this.highlightText == 'yes' ? this.area : this.highlightText;
};



/**
 * namespace tmap.utils to group all the helper functions
 */
tmap.utils = tmap.utils || {}

/**
 * Partially applies this function to a particular 'this object' and zero or
 * more arguments. The result is a new function with some arguments of the first
 * function pre-filled and the value of |this| 'pre-specified'.
 * @param {Function} fn A function to partially apply.
 * @param {Object} self Specifies the object which |this| should point to
 *     when the function is run. If the value is null or undefined, it will
 *     default to the global object.
 * @param {Object} var_args Additional arguments that are partially
 *     applied to the function.
 * @return {Function} A partially-applied form of the function bind() was
 *     invoked as a method of.
 */
tmap.utils.bind = function(fn, self, var_args) {
  var boundArgs = fn.boundArgs_;
  if (arguments.length > 2) {
    var args = Array.prototype.slice.call(arguments, 2);
    if (boundArgs) {
      args.unshift.apply(args, boundArgs);
    }
    boundArgs = args;
  }
  self = fn.boundSelf_ || self;
  fn = fn.boundFn_ || fn;
  var newfn;
  var context = self || goog.global;
  if (boundArgs) {
    newfn = function() {
      // Combine the static args and the new args into one big array.
      var args = Array.prototype.slice.call(arguments);
      args.unshift.apply(args, boundArgs);
      return fn.apply(context, args);
    }
  } else {
    newfn = function() {
      return fn.apply(context, arguments);
    }
  }
  newfn.boundArgs_ = boundArgs;
  newfn.boundSelf_ = self;
  newfn.boundFn_ = fn;
  return newfn;
};

/**
  * Adds a {@code getInstance} static method that always return the same
  * instance object.
  * @param {function} ctor The constructor for the class to add the
  *     static method to.
  */
tmap.utils.addSingletonGetter = function(ctor) {
  ctor.getInstance = function() {
    return ctor.instance_ || (ctor.instance_ = new ctor());
  };
};

/**
 * Creates HTML from objects and simple template strings.
 * @param {Obj} obj The object containg values for the template.
 * @param {string} template HTML string with place deliminators to be replaced
 *    with values eg. {value}.
 * @return {string} HTML string.
 */
tmap.utils.templatize = function(obj, template) {
  return template.replace(/{(.*?)}/g, function(str, p1) {
    return obj[p1] || '';
  });
};

/**
 * Simple browser checker.
 * @constructor
 */
tmap.utils.Browser = function() {
  var userAgent = navigator.userAgent.toLowerCase();
  this.version = (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1];
  this.webkit = /webkit/.test((userAgent));
  this.opera = /opera/.test((userAgent));
  this.msie = /msie/.test((userAgent)) && !/opera/.test((userAgent));
  this.mozilla = /mozilla/.test((userAgent)) &&
                 !/(compatible|webkit)/.test(userAgent);
};

/**
 * Lazy Loader class for asychronously loading JavaSciript files, and firing
 * a call back when complete.
 * @constructor
 */
tmap.utils.LazyLoader = function() {
  this.timer = {};
  this.scripts = [];
  this.browser = new tmap.utils.Browser();
};

tmap.utils.addSingletonGetter(tmap.utils.LazyLoader);

/**
 * Loads a script file and registers a callback for when it is complete.
 * @param {string} url The url of the file you wish to load.
 * @param {function} callback The function you want to fire when the script
 *    has loaded.
 */
tmap.utils.LazyLoader.prototype.load = function(url, callback) {
  var classname = null;
  var properties = null;
  // Make the scripts each script is only loaded once.
  if (!tmap.arrayContains(this.scripts, url)) {
    // note that we loaded already
    this.scripts.push(url);
    var script = document.createElement('script');
    script.src = url;
    script.type = 'text/javascript';
    var head = document.getElementsByTagName('head')[0];
    try {
      head.appendChild(script);
    } catch (e) {}
    // Check if a callback was requested.
    if (callback) {
      // Test for onreadystatechange to trigger callback.
      script.onreadystatechange = function() {
        if (script.readyState == 'loaded' ||
           script.readyState == 'complete') {
          callback();
        }
      };
      // Test for onload to trigger callback.
      script.onload = function(e) {
        callback(e);
        return;
      };
      var self = this;
      // Neither Webkit nor Opera fully support the scripts onload or
      // onreadystate property. So we poll the document's readyState to
      // check when it is 'loaded' or 'complete'.
      if (this.browser.webkit || this.browser.opera) {
        self.timer[url] = setInterval(function() {
          if (/loaded|complete/.test(document.readyState)) {
            clearInterval(self.timer[url]);
            callback();
          }
        }, 10);
      }
    }
  } else {
    if (callback) {
      callback();
    }
  }
};

/**
 * Creates a new map icon, based on the url of an image.
 * @param {{string} url The url of the image.
 * @constructor
 */
tmap.MapIcon = function(url) {
  this.icon = new google.maps.Icon();
  this.icon.image = url;
  this.icon.iconSize = new google.maps.Size(24, 24);
  this.icon.iconAnchor = new google.maps.Point(12, 24);
  this.icon.infoWindowAnchor = new google.maps.Point(5, 1);
};

/**
 * @return {google.maps.Icon} The maps icon.
 */
tmap.MapIcon.prototype.getIcon = function() {
  return this.icon;
};

/**
 * Checks if an array contains a specified object.
 * @param {Array} arr The array to be searched.
 * @param {*} obj The object for which we are searching.
 * @return {boolean} True of the array contains the obj.
 */
tmap.arrayContains = function(arr, obj) {
  if (arr.contains) {
    return arr.contains(obj);
  }
  if (arr.indexOf) {
    return arr.indexOf(obj) > -1;
  }
  if (Array.indexOf) {
    return Array.indexOf(arr, obj) > -1;
  }
  for (var i = 0, len = arr.length; i < len; i++) {
    if (arr[i] === obj){
      return true;
    }
  }
  return false;
};
