/**
* @name HiveMaps
* @version 0.9.2
* @author Joe Johnston <joe@socialhive.org>
* @copyright (c) 2009 Joe Johnston
* http://socialhive.org/hivemaps
* http://code.google.com/p/hivemaps
*
* Creates SOCIALHIVE.map
*
* Requires hiveutils, hivegeocoder
*
**/

/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

(function($) {
  function map(mapElem, options) {
    this.options = $.extend(HiveMap.defaultOptions, options);
    this.mapDiv = (typeof mapElem == 'string') ? $('#'+mapElem)[0] : mapElem;
    this.map = new GMap2(this.mapDiv);
    this.data = null;
    this.markers = [];
    this.markersLoaded = false;
    this.markersInView = [];
    this.markerClusterer = null;
		this.timers = {};
		 
		this.map.addControl(new Logo);
       
    if (this.options.listDiv && typeof this.options.listDiv == 'string')
      this.options.listDiv = $('#'+this.options.listDiv)[0];
                
    this.loadData = function(feed, type, schema){
      return loadData(this, feed, type, schema);
    };
    this.initMarkers = function(data){
      return initMarkers(this, data);
    };
    this.search = function(query, successCallback, errorCallback){
      return searchMap(this, query, successCallback, errorCallback);
    };
    this.zoom = function(zoom, lat, lon){
			return updateMap(this, lat, lon, zoom);
    };
    this.pan = function(lat, lon, zoom){
			return updateMap(this, lat, lon, zoom);
    };
    this.getLinkQuery = function(){
      return $.address.value();
    };
    this.getLink = function(){
      return $.address.baseURL()+'#'+$.address.value();
    };
		this.fitToMarkers = function(markers, maxZoom){
			return fitToMarkers(this, markers, maxZoom);
		};
    
    if (this.options.onMapInit)
      this.options.onMapInit(this);
    if (this.options.enableHistory)
      this.options.initHistory(this);
    var map = this;
    GEvent.addListener(this.map, 'moveend', function(){map.options.onMapMoved(map);});
    if (this.options.dataFeed)
      this.loadData(this.options.dataFeed, this.options.dataSchema.type);  	
  }

  var HiveMap = {
	  createMap: function(mapID, options) {
      return new map(mapID, options);  
    },

		// error codes
		ERROR_LOAD_DATA: 'ERROR_LOAD_DATA',
		ERROR_NO_GEOCODER: 'ERROR_NO_GEOCODER',
		ERROR_SEARCH_FAILED: 'ERROR_SEARCH_FAILED',
		
		// default map options
    defaultOptions: {
			// max number of loop iterations per batch; setting this too large might cause script timeouts
			loopBatchSize: 200,
			// geocoder automatically defaults to SOCIALHIVE.geo.geocoder
	    geocoder: null,
      enableHistory: true,
      initHistory: initHistory,
      historyName: 'hivemap',
      defaultZoom: 1,
      defaultCenter: {lat:25.8, lon:13.36},
      maxMarkers: 5000,
      maxListSize: 500,
      checkMarkerOverlap: checkMarkerOverlap,
      loadingBar: LoadingBar,
			dataFeed: null,
			dataSchema: {
				// root: node of markers list
				root: 'xml',
				// type: xml or json
				type: 'xml',
				// marker: schema object of node names
				marker: {
					title: 'title',
					lat: 'lat',
					lon: 'lon'
				}
			},
      listDiv: null,
	  markerFunc: 'HiveMapsMarker',
      markerClusterer: 'HiveMapsClusterer',
      imagesBase: 'images/',
      markerClustererOptions: {
        gridSize: 55,
        maxZoom: 11,
        styles: [
          {url:'cluster3.png', width:53, height:53, opt_textColor:'#FF6319'},
          {url:'cluster3.png', width:53, height:53, opt_textColor:'#FF6319'},
          {url:'cluster3.png', width:53, height:53, opt_textColor:'#FF6319'},
          {url:'cluster3.png', width:53, height:53, opt_textColor:'#FF6319'},
          {url:'cluster3.png', width:53, height:53, opt_textColor:'#FF6319'}
        ]
      },
      /**
			* Dictionary of icon options objects or GIcon instances
			*
			* If the icon property is present in the loaded data, the cooresponding icon will be used if found in 
			* markerIcons.  Otherwise, the 'default' entry will be used.
			**/
      markerIcons: {
				// default: new GIcon(G_DEFAULT_ICON)
				'default': {
					icon: {url:'marker1.png', width:18, height:18},
					shadow: null,
					iconAnchor: {x:9, y:9},
					infoWindowAnchor: {x:9, y:9},
					transparent: 'marker_trans.png'
				},
				'dec11_action': {
					icon: {url:'candle-icon.png', width:18, height:18},
					shadow: null,
					iconAnchor: {x:9, y:9},
					infoWindowAnchor: {x:9, y:9},
					transparent: 'marker_trans.png'
				},
				'dec13_action': {
					icon: {url:'bell-icon.png', width:18, height:18},
					shadow: null,
					iconAnchor: {x:9, y:9},
					infoWindowAnchor: {x:9, y:9},
					transparent: 'marker_trans.png'
				}
			},
			messages: {
				errors: {
					ERROR_LOAD_DATA: 'Unable to load map marker data.',
					ERROR_NO_GEOCODER: 'Geocoder not specified.',
					ERROR_SEARCH_FAILED: 'Unable to find location.'
				}
			},
      gettext: function(str) {
        return str;
      },
      listItemHTML: function(map, marker){
        return '<div><a href="'+marker.data.id+'">'+marker.data[map.options.dataSchema.marker.title]+'</a></div>';
      },
      moreListItemsHTML: function(map, count, list){
        return '<div>'+(list.length-count)+' '+map.options.gettext('points not shown')+'.  '+map.options.gettext('Zoom in on the map')+'.</div>';
      },
      showLoading: function(map){
        if (map.options.loadingBar) {
          if (typeof map.options.loadingBar != 'object')
            map.options.loadingBar = new map.options.loadingBar;
          map.map.addControl(map.options.loadingBar);
        }
      },
      hideLoading: function(map){
        if (typeof map.options.loadingBar == 'object')
          map.map.removeControl(map.options.loadingBar);
				if (typeof map.options.onReady == 'function')
					map.options.onReady(map);
      },
      raiseError: function(error) {
        alert(error.map.options.gettext('Error')+': '+error.map.options.gettext(error.message));
      },
			// called when map is ready for interaction
			onReady: function(map){
			},
			// called when data feed is done loading and ready for processing
      onDataLoad: function(map, data){
        if (map.options.dataSchema.type == 'xml')
          data = SOCIALHIVE.utils.xml2js(data, map.options.dataSchema.root);
        map.data = data;
        map.initMarkers(data);
      },
			// called when map is initialized but before any markers are loaded
      onMapInit: function(map){
        map.options.showLoading(map);
        map.map.setCenter(new GLatLng(map.options.defaultCenter.lat, map.options.defaultCenter.lon), map.options.defaultZoom);
		map.map.setUIToDefault(); // was added 15 oct 09
       // map.map.addControl(new GMapTypeControl());
       // map.map.removeMapType(G_SATELLITE_MAP);
       // map.map.addMapType(G_PHYSICAL_MAP);
       // map.map.addControl(new GLargeMapControl());
        map.map.disableScrollWheelZoom();
       // map.map.setMapType(G_HYBRID_MAP); 
      },
			onMarkersInit: function(map){
				(map.options.markerClusterer) ? setTimeout(function(){initMarkerClusterer(map);}, 100) : addMarkers(map, map.markers);
		    (map.options.listDiv) ? setTimeout(function(){initList(map);}, 100) : setTimeout(function(){map.options.hideLoading(map);}, 100); 
			},
      onListInit: function(map, list, max){
      },
      onMapMoved: function(map) {
        map.options.showLoading(map);
        if (map.options.listDiv && map.markersLoaded)
          setTimeout(function(){initList(map);}, 100);
      },
      onDoneDrawing: function(map) {
        map.options.hideLoading(map);
      },
			// called when a non-overlapping marker is clicked
      onMarkerClick: function(map, marker) {
        map.map.openInfoWindowHtml(marker.getLatLng(), marker.data[map.options.dataSchema.marker.title]);
      },
			// called when a marker is clicked that is overlapping other markers at the current zoom level
      onMarkerClickOverlap: function(map, marker, list) {
        var node = $(document.createElement('ol')).attr('id', 'infoMarkerList');
        $(list).each(function(i, m){
          $('<li><a href="#">'+m.data[map.options.dataSchema.marker.title]+'</a></li>').click(function(){map.options.onMarkerClick(map, m); return false;}).appendTo(node);
        });
        map.map.openInfoWindow(marker.getLatLng(), node[0]);        
      }
    }
  };

  
  function LoadingBar() {
    this.container = null;
  }
  LoadingBar.prototype = new GControl();
  LoadingBar.prototype.initialize = function(map) {
    this.container = $(document.createElement('div')).attr('class', 'loading').appendTo(map.getContainer());
    return this.container[0];
  };
  LoadingBar.prototype.getDefaultPosition = function() {
    return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(90, 10));
  };

  function Logo() {
    this.container = null;
  }
  Logo.prototype = new GControl();
  Logo.prototype.initialize = function(map) {
    
    this.container = $(document.createElement('div')).css({
      'background': 'url(http://hivedata.appspot.com/static/img/1/socialhive_logo.png) 0 0 no-repeat',
      'width': '62px',
      'height': '29px',
      'cursor': 'pointer'
    }).bind('mouseover', function(e){
      $(this).css('background-position', '-66px 0');
    }).bind('mouseout', function(e){
      $(this).css('background-position', '0 0');
    }).bind('click', function(e){
      window.location='http://socialhive.org';
    }).appendTo(map.getContainer());
    return this.container[0];
  };
  Logo.prototype.getDefaultPosition = function() {
    return new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(66, 6));
  };


  function initMarkerClusterer(map) {
    if (!map.options.markerClustererOptions._imagesBaseAdded) {
      var styles = map.options.markerClustererOptions.styles;
      for (var i=0; i<styles.length; i++)
        styles[i].url = map.options.imagesBase+styles[i].url;
      map.options.markerClustererOptions._imagesBaseAdded = true;
    }
		// todo: cleanup markerClusterer if it already exists before creating a new one
    map.markerClusterer = new window[map.options.markerClusterer](map.map, map.markers, map.options.markerClustererOptions);
    map.markerClusterer.onDoneDrawing = function(){map.options.onDoneDrawing(map)};
  }
  
  function initList(map) {
    list = getMarkersInView(map);
    var max = (map.options.maxListSize < list.length) ? map.options.maxListSize : list.length;
    map.options.onListInit(map, list, max);
    ma = [];
    var htmlfunc = map.options.listItemHTML;
    var title = map.options.dataSchema.marker.title;
    for (var i=0; i<max; i++) {
      if (!list[i].data[title])
        continue;
      ma.push(htmlfunc(map, list[i]));
    }
    if (i < list.length)
      ma.push(map.options.moreListItemsHTML(map, i, list));
    setTimeout(function(){map.options.listDiv.innerHTML = ma.join('');}, 50);
    setTimeout(function(){map.options.hideLoading(map);}, 500);
  }
  
  function initHistory(map) {
    GEvent.addListener(map.map, 'moveend', function(){updateHistory(map);});
    $.address.change(function(e){
      var history = e.pathNames;
      if (history[0] != map.options.historyName || history.length < 4)
        return false;
      var center = map.map.getCenter();
      if (center.lat()!=history[1] || center.lng()!=history[2] || map.map.getZoom()!=history[3]) {
        updateMap(map, history[1], history[2], history[3]);
      }
    });
  }

  function initMarkers(map, data) {
		// if initMarkers loop is already in progress, stop it, wait a bit for the current 
		// loop to exit and try again 
		if (map.timers.initMarkers) {
			clearTimeout(map.timers.initMarkers);
			delete map.timers.initMarkers;
			setTimeout(function(){initMarkers(map, data);}, 200);
			return;
		}
    map.map.clearOverlays();
    map.markers = [];
		if (!map.options.markerIcons)
		 	map.options.markerIcons = {};				
    if (map.markerClusterer)
      map.markerClusterer.clearMarkers();
		var opt = {
			markerfunc: (window[map.options.markerFunc] || GMarker),
			clickfunc: function(){(map.options.checkMarkerOverlap) ? map.options.checkMarkerOverlap(map, this) : map.options.onMarkerClick(map, this);},
    	max: (map.options.maxMarkers < data.length) ? map.options.maxMarkers : data.length,
			lat: map.options.dataSchema.marker.lat,
			lon: map.options.dataSchema.marker.lon
		};
		map.timers.initMarkers = setTimeout(function(){_initMarkers(map, data, opt, 0, map.options.loopBatchSize);}, 10);
  }

	function _initMarkers(map, data, opt, batchNum, batchSize){

		var cnt = batchNum*batchSize;
		var n = (cnt+batchSize > opt.max) ? opt.max-cnt : batchSize;
		if (n > 0) {
			do {
				// exit loop if another one has started
				if (!map.timers.initMarkers) return;
	      var marker = new opt.markerfunc(new GLatLng(data[cnt][opt.lat], data[cnt][opt.lon]), {icon: getMarkerIcon(map, data[cnt].event_type)});
				marker.data = data[cnt];
	      GEvent.addListener(marker, 'click', opt.clickfunc);
	      map.markers[cnt] = marker;
				cnt++;
			} while(--n);
		}
		if (cnt < opt.max) {
			map.timers.initMarkers = setTimeout(function(){_initMarkers(map, data, opt, ++batchNum, batchSize);}, 0);
		} else {
			delete map.timers.initMarkers;
			setTimeout(function(){map.options.onMarkersInit(map);}, 50);
		}
	}

	function fitToMarkers(map, markers, maxZoom) {
		if (!markers)
			markers = map.markers;
		if (!maxZoom)
			maxZoom = 16;
		var bounds = new GLatLngBounds();
		var len = markers.length;
		for (var i=0; i<len; i++)
			bounds.extend(markers[i].getPoint());
		var zoom = map.map.getBoundsZoomLevel(bounds);
		map.map.setZoom((zoom<maxZoom)?zoom:maxZoom);
		map.map.setCenter(bounds.getCenter());
	}
  
  function updateHistory(map) {
    var center = map.map.getCenter();
    $.address.value(map.options.historyName+'/'+center.lat()+'/'+center.lng()+'/'+map.map.getZoom());
  }

  function loadData(map, feed, type, schema) {
		if (schema)
			map.options.dataSchema = schema;
    $.ajax({url: feed, type: 'GET', dataType: type,
      success: function(data) {
        map.markersLoaded = true;
        map.options.onDataLoad(map, data, type);
      },
      error: function(xhr, status, err) {
        	map.options.raiseError(getError(map, HiveMap.ERROR_LOAD_DATA));
      }
    });
  }

	function getError(map, code) {
		return {
			code: code,
			message: map.options.messages.errors[code],
			map: map
		};
	}
  
  function addMarkers(map, markers) {
    for (var i=markers.length; i--;) {
      map.map.addOverlay(markers[i]);
    }
  }

  function getMarkerIcon(map, type) {
	  //alert(map.options.markerIcons[type]);
		if (!type || !map.options.markerIcons[type])
			type = 'default';
    var opt = map.options.markerIcons[type];
		if (!opt) {
			var icon = new GIcon(G_DEFAULT_ICON);
		} else if (typeof opt == 'object' && opt.constructor == GIcon.prototype.constructor) {
				return opt;
		} else {
	    var  icon = new GIcon(G_DEFAULT_ICON, map.options.imagesBase+opt.icon.url);
	    icon.iconSize = new GSize(opt.icon.width, opt.icon.height);
	    if (opt.shadow) {
	      icon.shadow = map.options.imagesBase+opt.shadow.url;
	      icon.shadowSize = new GSize(opt.shadow.width, opt.shadow.height);
	    } else {
	      icon.shadow = null;
	    }
	    icon.iconAnchor = new GPoint(opt.iconAnchor.x, opt.iconAnchor.y);
	    icon.infoWindowAnchor = new GPoint(opt.infoWindowAnchor.x, opt.infoWindowAnchor.y);
	    icon.transparent = opt.transparent;
		}
		map.options.markerIcons[type] = icon;
    return icon;
  }

  function checkMarkerOverlap(map, marker) {
    var ll = marker.getLatLng();
    var sw = map.map.fromDivPixelToLatLng(new GPoint(0, 0));
		var icon = marker.getIcon();
    var ne = map.map.fromDivPixelToLatLng(new GPoint(icon.iconSize.width, icon.iconSize.height));
    var width = Math.abs(sw.lat()-ne.lat());
    var height = Math.abs(sw.lng()-ne.lng());
    var factor = 1.5;
    var bounds = new GLatLngBounds( // bounds of marker icon plus a little extra
      new GLatLng(ll.lat()-width/factor, ll.lng()-height/factor),
      new GLatLng(ll.lat()+width/factor, ll.lng()+height/factor)
    );
    var list = [];
    for (var i=map.markers.length; i--;) {
      if (bounds.containsLatLng(map.markers[i].getLatLng()))
        list.push(map.markers[i]);
    }
    for (var i=list.length; i--;) {
      if (list[i] == marker) {
        list.splice(i, 1);
        break;
      }
    }
    list.reverse();
    list.unshift(marker);
    (list.length>1) ? map.options.onMarkerClickOverlap(map, marker, list) : map.options.onMarkerClick(map, marker);
  }
  
  function getMarkersInView(map) {
  		if (map.markersLoaded) {
  			map.markersInView = getMarkersInBounds(map.markers, map.map.getBounds()); 
  		}
  		return map.markersInView;
  }
  
  function getMarkersInBounds(markers, bounds) {
		var list = [];
		var len = markers.length;
		for (var i=0; i<len; i++) {
			if (bounds.containsLatLng(markers[i].getPoint()))
				list.push(markers[i]);
		}
		return list;
	}
    
  function updateMap(map, lat, lon, zoom) {
		lat = parseFloat(lat);
		lon = parseFloat(lon);
		zoom = parseInt(zoom);
  	map.map.closeInfoWindow();
  	if (!isNaN(lat) && !isNaN(lon)) {
  		map.map.setCenter(new GLatLng(lat, lon), (zoom)?zoom:null);
    } else if (!isNaN(zoom) && zoom!=null) {
      map.map.setZoom(zoom);
    }
  }
  
  function searchMap(map, query, successCallback, errorCallback) {
		if (!map.options.geocoder)
			map.options.raiseError(getError(map, HiveMap.ERROR_NO_GEOCODER));
  	map.options.geocoder.getLocations(query, SOCIALHIVE.utils.ajaxUniqueCallback('hivemapSearchMap',
			function(result) {
	  		if (result.Status.code == G_GEO_SUCCESS) {
	  			var p = result.Placemark[0].Point.coordinates;
	  			updateMap(map, p[1], p[0], map.options.geocoder.getZoomFromResult(result));
					if (typeof successCallback == 'function')
						successCallback(map, result);
	  		} else {
					(typeof errorCallback == 'function') ? 
						errorCallback(map, result) : map.options.raiseError(getError(map, HiveMap.ERROR_SEARCH_FAILED));
	  		}
  		}
		));
  }
  
  
  // init
  jQuery(function($) {
		if (window.SOCIALHIVE.geo && window.SOCIALHIVE.geo.geocoder)
			HiveMap.defaultOptions.geocoder = window.SOCIALHIVE.geo.geocoder;
    $(window).unload(function(){
      GUnload();
    });
  });
  
  if (!window.SOCIALHIVE)
    window.SOCIALHIVE = {};
  window.SOCIALHIVE.map = HiveMap;
})(jQuery);


