var MapKit = Class.create({
  initialize: function(element, options) {
    // we'll define self in the beginning of every class method so
    // we can use "this" in JQuery and Prototype blocks and not get confused
    var self = this;

    self.mapElement = element || "map";
    self.movedWindow = false;
    self.currentMarker = null;
    self.showDebug = true;
    self.markers = $H({});
    self.managers = $H({});
    self.managerMarkers = $H({});
    self.feeds = $H({});
    self.feedRefreshFlags = $H({});
    self.refreshPeopleFlag = true;
    self.refreshSessionFlag = true;
    
    self.socialBrowsingFlag = false;
    self.myMarker = null;
    self.myLatLng = null;
    self.personIcon = 1;
    
    self.adsManager = null;

    self.mapOptions = MapKit.mergeOptions({}, self.mapOptions, options);
    MapKit.set(this);
  },
  
  initMapCenter: function(centerLat, centerLng, centerZoomLevel) {
    var self = this;
    if(self.mapOptions.mapCenter != undefined)
    {
      return;
    }
    
    // initialize map center
    if(centerLat != "" && centerLng != "" && centerZoomLevel != "")
    {
      self.mapOptions.mapCenter = [parseFloat(centerLat), parseFloat(centerLng)];      
      self.mapOptions.mapZoom = parseInt(centerZoomLevel);
    }
  },

  initViewCenter: function(centerLat, centerLng, centerZoomLevel) {
    var self = this;
    if(self.mapOptions.mapCenter != undefined)
    {
      return;
    }

    // initialize view center
    if(centerLat != "" && centerLng != "" && centerZoomLevel != "")
    {
      self.mapOptions.mapCenter = [parseFloat(centerLat), parseFloat(centerLng)];      
      self.mapOptions.mapZoom = parseInt(centerZoomLevel);
    }
  },
  
  enableSocialBrowsing: function(lat, lng) {
    var self = this;
    self.socialBrowsingFlag = true;
    if(lat != "" && lng != "")
    {
      self.myLatLng = [parseFloat(lat), parseFloat(lng)];
    }
  },
  
  setup: function() {
    var self = this;
    
    if(self.myLatLng)
    {
      self.mapOptions.mapCenter = self.myLatLng;
    }
    
    if(self.mapOptions.mapCenter == undefined || self.mapOptions.mapZoom == undefined)
    {
      self.mapOptions.mapCenter = [self.mapOptions.default_lat, self.mapOptions.default_lng];
      self.mapOptions.mapZoom = self.mapOptions.default_zoom;
    }
    
    // init map
    self.initGoogleMap();
    
    // configure adsense
    if(self.mapOptions.enable_ads && self.mapOptions.publisher_id != undefined && self.mapOptions.publisher_id != "")
    {
      self.adsManager = new GAdsManager(self.gmap, self.mapOptions.publisher_id, 
        {
          max_ads_on_map: self.mapOptions.max_ads_on_map,
          minZoomLevel: self.mapOptions.min_zoom_level_for_ads
        }
      );
      self.adsManager.enable();
    }
    
    // configure session tracker
    if(self.mapOptions.refresh_session_interval > 0)
    {
      GEvent.addListener(self.gmap, "moveend", function() {
        self.refreshSessionFlag = true;
      });
      new PeriodicalExecuter(function() {
        self.refreshSession();
      }, self.mapOptions.refresh_session_interval);
    }
    else
    {
      GEvent.addListener(self.gmap, "moveend", function() {
        self.refreshSessionFlag = true;
        self.refreshSession();
      });
    }
    
    // configure feed tracker
    if(self.mapOptions.refresh_feeds_interval > 0)
    {
      new PeriodicalExecuter(function() {
        self.refreshFeeds();
      }, self.mapOptions.refresh_feeds_interval);
    }
    
    // configure social browsing
    if(self.socialBrowsingFlag)
    {
      if(self.mapOptions.refresh_people_interval > 0)
      {
        GEvent.addListener(self.gmap, "moveend", function() {
          self.refreshPeopleFlag = true;
        });
        
        new PeriodicalExecuter(function() {
          self.refreshPeople();
        }, self.mapOptions.refresh_people_interval);
      }
      else
      {
        GEvent.addListener(self.gmap, "moveend", function() {
          self.refreshPeopleFlag = true;
          self.refreshPeople();
        });
      }
      self.createManager("people")
      self.refreshPeople();
      
      // default my latlng
      if(self.myLatLng == null)
      {
        mapCenter = self.gmap.getCenter();
        self.myLatLng = [mapCenter.lat(), mapCenter.lng()];
      }
      
      // set default title for my marker
      var myMarkerTitle = $j("#my_marker_content .marker").attr("title");
      if(myMarkerTitle == "" || myMarkerTitle == undefined)
      {
        myMarkerTitle = "My Marker";
      }
      
      // set up my marker
      self.myMarker = self.buildMarker("my_marker", {
        draggable: true,
        latLng: self.myLatLng,
        title: myMarkerTitle,
        bubbleStyle: self.mapOptions.my_bubble_style,
        getHtml: function() {
          return "<div id='my_marker_bubble'>"+$j("#my_marker_content").html()+"</div>";
        }
      });
      
      // when starting to drag, close my marker bubble
      GEvent.addListener(self.myMarker, "dragstart", function() {
        self.gmap.closeExtInfoWindow();
      });
      
      // when ending drag, open up my marker bubble
      GEvent.addListener(self.myMarker, "dragend", function() {
        newCenter = self.myMarker.getPoint();
        self.myLatLng = [newCenter.lat(), newCenter.lng()];
        self.myMarker.openBubble();
        self.refreshSessionFlag = true;  
        self.refreshSession();
      });
      
      
      self.gmap.addOverlay(self.myMarker);
    }
    
    // create the default marker manager
    self.createManager("default");
    self.bindUnload();

    return self.gmap;
  },
  
  // Add a Dynamic Loading Marker Feed
  addFeed: function(feedLink){
    var self = this;
    
    self.feeds.set(feedLink.id, feedLink.href);
    var manager = self.createManager(feedLink.id)
    self.refreshFeed(feedLink.id);
    
    if(self.mapOptions.refresh_feeds_interval > 0)
    {
      GEvent.addListener(self.gmap, "moveend", function() {
        self.feedRefreshFlags.set(feedLink.id, true);
      });
    }
    else
    {
      GEvent.addListener(self.gmap, "moveend", function() {
        self.refreshFeed(feedLink.id);
      });
    }
  },
  
  initGoogleMap: function() {
    var self = this;

    // Initialise the map with the passed settings
    self.gmap = new GMap2(document.getElementById(self.mapElement));
    self.gmap.setCenter(new GLatLng(self.mapOptions.mapCenter[0], self.mapOptions.mapCenter[1]), 
                            self.mapOptions.mapZoom);

    // set enable type position
    var typePosition = new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(75, 5));

    // Attach a controller to the map view
    // Will attach a large or small.  If any other value passed (i.e. "none") it is ignored
    switch(self.mapOptions.map_control_style)
    {
      case "default-small":
        self.gmap.addControl(new GSmallMapControl());
        self.gmap.addControl(new GMapTypeControl(), typePosition);
        if(self.mapOptions.map_enable_scale_control) { self.gmap.addControl(new GScaleControl()); }
        break;
      case "default-large":
        self.gmap.addControl(new GLargeMapControl());
        self.gmap.addControl(new GMapTypeControl(), typePosition);
        if(self.mapOptions.map_enable_scale_control) { self.gmap.addControl(new GScaleControl()); }
        break;
      default:
        self.gmap.addControl(new MapKitZoomControl({color: self.mapOptions.map_control_color}));
        self.gmap.addControl(new MapKitPanControl({color: self.mapOptions.map_control_color}));
        self.gmap.addControl(new MapKitMapTypeControl({color: self.mapOptions.map_control_color}));
    }
    
    // set map type
    var maptype = G_NORMAL_MAP;
    switch(self.mapOptions.mapType) {
      case "sat": // Satallite Imagery
        maptype = G_SATELLITE_MAP;
      break;
      case "hybrid":  //Hybrid Map
        maptype = G_HYBRID_MAP;
      break;
    }
    self.gmap.setMapType(maptype);
    
    // add map overview window if enabled
    if(self.mapOptions.map_enable_overview)
    {
      self.gmap.addControl(new GOverviewMapControl());
    }
    
    // add double click zooming capability if enabled
    if(self.mapOptions.map_enable_double_click_zoom)
    {
      self.gmap.enableDoubleClickZoom();
    }
    
    // add smooth zoom functionality if enabled
    if(self.mapOptions.map_enable_smooth_zoom)
    {
      self.gmap.enableContinuousZoom();
    }
    
    // add google search bar if enabled
    //if(self.mapOptions.map_enable_google_bar)
    //{
    //  self.gmap.enableGoogleBar();
    //}
    
    if(self.mapOptions.map_enable_scroll_zoom)
    {
      self.gmap.enableScrollWheelZoom();
    }
  },

  zoomTo: function(lat, lng, zoom_level) {
  	var self = this;
    self.gmap.setCenter(new GLatLng(lat, lng),zoom_level);
  },
  
  refreshFeeds: function() {
    var self = this;
    self.feedRefreshFlags.each(function(pair) {
      if(self.feedRefreshFlags.get(pair.key))
      {
        self.refreshFeed(pair.key);
      }
    });
  },
  
  // bind livequery for looking at _markers element
  addContainer: function(container) {
    var self = this;
    
    // create manager for container
    var container_id = container.id;    
    var manager = self.createManager(container_id);
    
    // find all the marker elements
    $j("#"+container_id+" .marker").livequery( 
      // when a new marker element is added to dom
      // this function is run automatically
      function() {
        $marker = $j(this);
        
        self.log("\nAdding Marker from: "+container_id);
        self.log(this);
        
        // add basic content
        markerOptions = {
          latLng: MapKit.parseLatLng($marker.attr("rel")),
          html: $marker.find(".bubbleHTML").html(),
          icon: self.buildIcon($marker.attr("icon")),
          managerName: container_id,
          title: ""
        };
        
        if($marker.find(".title").size() > 0)
        {
          markerOptions.title = $marker.find(".title").html();
        }
        
        if($marker.attr("min"))
        {
          markerOptions.minZoom = parseInt($marker.attr("min"));
        }

        if($marker.attr("max"))
        {
          markerOptions.maxZoom = parseInt($marker.attr("max"));
        }
        
        if($marker.attr("expand"))
        {
          markerOptions.expand = ($marker.attr("expand") == "true");
        }
        
        if($marker.attr("bubbleStyle"))
        {
          markerOptions.bubbleStyle = $marker.attr("bubbleStyle");
        }        
        
        self.addMarker($marker.attr("id"), markerOptions);
      },

      // when a marker element is deleted from the dom
      // this function is run automatically
      function() {
        $marker = $j(this);
        
        self.log("\nRemoving Marker from: "+container_id);
        self.log(this);
        
        self.removeMarker(self.markers.get($marker.attr("id")), container_id);
        manager.refresh();
      }
    );
    
    if($j(container).attr("autozoom") == "true")
    {
      //autozoom after 1 sec
      //setTimeout(function(){
      //  self.zoomFit();
      //}, 1000);
    }
  },
  
  addMarker: function(id, options)
  {
    var self = this;
    
    var options = MapKit.mergeOptions({}, self.markerDefaults, options);
    var marker = self.buildMarker(id, options);
    
    if(options.managerName != undefined)
    {
      var manager = self.managers.get(options.managerName);
      manager.addMarker(marker, options.minZoom, options.maxZoom);
      self.managerMarkers.get(options.managerName).push(marker);
    }
    else
    {
      self.gmap.addOverlay(marker);
    }
    
    self.markers.set(id, marker);
    
    return marker;
  },
  
  // create a marker object from options
  buildMarker: function(id, options) {
    var self = this;
    
    var options = MapKit.mergeOptions({}, self.markerDefaults, options);
    
    var marker = new GMarker(new GLatLng(options.latLng[0], options.latLng[1]), options);
    marker.id = id;
    marker.expand = options.expand;
    marker.title = options.title;
    marker.bubbleStyle = options.bubbleStyle;
    marker.ajaxUrl = options.ajaxUrl;

    // set current marker when opening / closing info window
    GEvent.addListener(marker, "infowindowopen", function(e) {
      self.currentMarker = e;
    });
    GEvent.addListener(marker, "infowindowclose", function(e) {
      self.currentMarker = undefined;
    });
    
    // If it has HTML to pass in, add an event listner for a click
    if(options.html)
    {
      marker.openBubble = function() {
        self.openBubble(marker, marker.bubbleStyle, options.html);
      };
    }
    else if(options.getHtml)
    {
      marker.openBubble = function(){
        var currentHtml = options.getHtml();
        self.openBubble(marker, marker.bubbleStyle, currentHtml);
      };
    }
    else
    {
      marker.openBubble = function() {
        
      }
    }
    GEvent.addListener(marker, options.htmlOpenEvent, marker.openBubble);

    return marker;
  },
  
  openBubble: function(marker, bubbleStyle, html) {
    var self = this;
    
    var bubbleHtml = "<div class='infoWindowView'><h3 class='title'>"+marker.title+"</h3><div class='content'>"+html+"</div></div>";
    
    if(bubbleStyle == null || bubbleStyle == "")
    {
      bubbleStyle = self.mapOptions.default_bubble_style;
    }
    
    marker.openExtInfoWindow(self.gmap, bubbleStyle, bubbleHtml, {
      beakOffset: 0,
      paddingX: 80,
      paddingY: 50,
      ajaxUrl: marker.ajaxUrl,
      maxContent: marker.expand ? "map/expand_content/"+marker.id : null,
      maxWidth: 500,
      maxHeight: 250,
      maximizeEnabled: marker.expand
    });
  },
  
  buildIcon: function(iconId){
    var self = this;
    try
    {      
      iconElement = $("icon_"+iconId);
      if(iconElement == undefined)
      {
        self.log("Unable to find icon element for: icon_"+iconId)
        return;
      }
    
      iconImage = iconElement.down("img.marker-icon");
      shadowImage = iconElement.down("img.marker-shadow");

      if(iconImage != undefined)
      {
        var icon = new GIcon();
        icon.image = iconImage.src;
        var imgWidth = parseInt($j(iconImage).attr("w"));
        var imgHeight = parseInt($j(iconImage).attr("h"));
        
        icon.iconAnchor = new GPoint(imgWidth/2, imgHeight/2);
        icon.infoWindowAnchor = new GPoint(imgWidth/2, 0)
        icon.iconSize = new GSize(imgWidth, imgHeight);
        if(shadowImage != undefined)
        {
          icon.shadow = shadowImage.src;
          var shadowWidth = parseInt($j(shadowImage).attr("w"));
          var shadowHeight = parseInt($j(shadowImage).attr("h"));
          
          icon.shadowSize = new GSize(shadowWidth, shadowHeight);
        }
        return icon;
      }
      else
      {
        return new GIcon(G_DEFAULT_ICON);
      }
    }
    catch(ex) {
      console.error(ex);
    }
  },
  
  zoomFit: function(zoomLevel) {
    var self = this;
    
    var bounds = new GLatLngBounds();
    
    self.markers.each(function(pair) {
      bounds.extend(pair.value.getPoint())
    });
    
    if(self.myLatLng == undefined)
    {
      self.gmap.setCenter(bounds.getCenter(), self.gmap.getBoundsZoomLevel(bounds));      
    }
  },
  
  // mark people around you
  refreshPeople: function()
  {
    var self = this;
    if(self.refreshPeopleFlag)
    {
      self.log("Refreshing Map People...");

      var bounds = self.gmap.getBounds();
      var northEast = bounds.getNorthEast();
      var southWest = bounds.getSouthWest();

      new Ajax.Request('/map_sessions', {
        method: "get",
        parameters: {
          max_lat: northEast.lat(),
          max_lng: northEast.lng(),
          min_lat: southWest.lat(),
          min_lng: southWest.lng()
        },
        onSuccess: function(transport){
          try
          {
            var jsonMarkers = transport.responseText.evalJSON();
            var mapMarkers = $A([]);

            // build mapMarkers from JSON Response
            jsonMarkers.each(function(jsonMapSession) {
              mapMarkers.push(self.buildMarker("marker_"+jsonMapSession.id, {
                latLng: [jsonMapSession.marker_lat, jsonMapSession.marker_lng],
                ajaxUrl: "/map_session/?id="+jsonMapSession.id,
                title: jsonMapSession.name,
                html: "Loading...",
                bubbleStyle: jsonMapSession.bubble_style,
                icon: self.buildIcon(self.personIcon)
              }));
            });
            
            // Replace Markers on Map Manager
            self.log("People Markers: "+mapMarkers.size());
            self.replaceManagerMarkers("people", mapMarkers, 0, 17);
          } 
          catch(ex) {
            console.error(ex);
          }
        }
      });
      self.refreshPeopleFlag = false;
    }
  },
  
  // set map session data, and look for new messages
  refreshSession: function() {
    var self = this;
    if(self.refreshSessionFlag)
    {    
      var sessionParams = {
        "map_session[lat]": self.gmap.getCenter().lat(),
        "map_session[lng]": self.gmap.getCenter().lng(),
        "map_session[zoom_level]": self.gmap.getZoom(),
        "_method": "put"
      }
      
      if(self.myLatLng)
      {
        sessionParams["map_session[marker_lat]"] = self.myLatLng[0];
        sessionParams["map_session[marker_lng]"] = self.myLatLng[1];
      }
    
      new Ajax.Request('/map_session', {
        method: "post",
        parameters: sessionParams,
        onSuccess: function(transport){
          var jsonMessages = transport.responseText.evalJSON();
          jsonMessages.each(function(jsonMessage) {
            mapkit_viewmessage_open(jsonMessage.id);
          });
        }
      });
      self.refreshSessionFlag = false;
    }
  },
  
  // create a new marker manager for the map
  createManager: function(name, options) {
    var self = this;
    
    var options = MapKit.mergeOptions({}, self.managerDefaults, options);
    
    var manager = new MarkerManager(self.gmap, options);
    self.managers.set(name, manager);
    self.managerMarkers.set(name, $A([]));
    
    return manager;
  },
  
  // refresh the feed markers for a specified feed
  refreshFeed: function(name) {
    var self = this;
    
    var manager = self.managers.get(name);
    var managerName = name;

    var bounds = self.gmap.getBounds();
    var northEast = bounds.getNorthEast();
    var southWest = bounds.getSouthWest();
    
    // look up matching markers
    var feedUrl = self.feeds.get(name);    
    new Ajax.Request(feedUrl, {
      method: "get",
      parameters: {
        "bounds[max_lat]": northEast.lat(),
        "bounds[max_lng]": northEast.lng(),
        "bounds[min_lat]": southWest.lat(),
        "bounds[min_lng]": southWest.lng(),
        "current_zoom": self.gmap.getZoom()
      },
      onSuccess: function(transport){
        var jsonMarkers = transport.responseText.evalJSON();
        var mapMarkers = $A([]);
        var minZoom = 0;
        var maxZoom = 17;
        
        // build mapMarkers from JSON Response
        jsonMarkers.each(function(jsonMarker) {
          var markerOptions = {
            latLng: [jsonMarker.lat, jsonMarker.lng],
            html: jsonMarker.bubble_html,
            icon: self.buildIcon(jsonMarker.icon_id),
            title: jsonMarker.title,
            bubbleStyle: jsonMarker.bubble_style
          };
          
          if(jsonMarker.expanded_html != null && jsonMarker.expanded_html != "")
          {
            markerOptions.maxContent = jsonMarker.expanded_html;
            markerOptions.expand = true;
          }
          
          // create marker
          var marker = self.buildMarker("marker_"+jsonMarker.id, markerOptions);
          if(self.mapOptions.ajax_bubble_content)
          {
            marker.ajaxUrl = "map/expand_content/"+marker.id;
          }          
          mapMarkers.push(marker);
          
          minZoom = jsonMarker.min_zoom;
          maxZoom = jsonMarker.max_zoom;
        });
        
        // Replace Markers on Map Manager
        self.log("New Markers: "+mapMarkers.size());
        self.replaceManagerMarkers(managerName, mapMarkers, minZoom, maxZoom);
      }
    }); 
    
    self.feedRefreshFlags.set(name, false);
  },
  
  // replace the visible markers for a particular marker manager
  replaceManagerMarkers: function(managerName, newMarkers, minZoom, maxZoom)
  {
    var self = this;
    
    var manager = self.managers.get(managerName);
    var currentMarkers = self.managerMarkers.get(managerName);
    // self.log_array("Current Markers", currentMarkers);
    
    // find all the markers that are in the new but not in the current
    var markersToAdd = newMarkers.reject(function(m) { 
      return currentMarkers.map(function(cm) { return cm.id; }).include(m.id);
    });
    // self.log_array("Adding Markers", markersToAdd);

    // find all the markers that are in the old but not in the new
    var markersToDelete = currentMarkers.reject(function(m) { 
      return newMarkers.map(function(cm) { return cm.id; }).include(m.id);
    });
    // self.log_array("Removing Markers", markersToDelete);
        
    // make the additions + subtractions
    // set marker hash
    markersToAdd.each(function(m) {
      manager.addMarker(m, minZoom, maxZoom);
      self.markers.set(m.id, m);
      currentMarkers.push(m);
    });
    
    // remove other markers
    markersToDelete.each(function(markerToDelete) {
      self.removeMarker(markerToDelete, managerName);
    });
  },
  
  // remove a marker from the map
  removeMarker: function(marker, managerName)
  {
    try{
      var self = this;
    
      // dont remove an opened marker
      if(self.currentMarker != undefined && marker.id == self.currentMarker.id)
      {
        return false;
      }
    
      if(managerName != undefined)
      {
        var manager = self.managers.get(managerName);
        manager.removeMarker(marker);
        self.managerMarkers.set(managerName, self.managerMarkers.get(managerName).without(marker));
      }
      else
      {
        self.gmap.removeOverlay(marker);
      }
    
      self.markers.unset(marker.id);
      return true;
    }
    catch(ex) { console.error(ex); }     
  },
  
  bindUnload: function() {
    document.body.onunload = function() {
      if (GBrowserIsCompatible()) {
        GUnload();
      }
    }
  },
  
  log: function(msg) {
    if(this.showDebug)
    {
      console.log(msg);
    }
  },
  
  log_array: function(msg, array) {
    this.log("\n"+message+": "+array.size());
    this.log(array.toArray());
  },
  
  // default map options
  mapOptions: {
    // map_control_style can be "default-large", "default-small", or "mapkit"
    map_control_style: "mapkit",
    
    // corresponds to the map_controls folder used in the image paths
    map_control_color: "black",
    
    // default map type can be "sat", "hybrid", or "normal"
    mapType: "normal",
    
    // bubble style can be "bubble_default", "bubble_blue", "bubble_green", "bubble_red"
    default_bubble_style: "bubble_default",
    my_bubble_style: "bubble_red",
    people_bubble_style: "bubble_blue",
    
    // standard map options
    map_enable_scroll_zoom: true,
    map_enable_smooth_zoom: true,
    map_enable_google_bar: true,
    map_enable_double_click_zoom: true,
    map_enable_type_position: true,
    map_enable_overview: true,
    map_enable_scale_control: false,

    // set to 0 for realtime
    refresh_people_interval: 0, // 60
    refresh_session_interval: 0, // 5
    refresh_feeds_interval: 0, // 0.5
    
    // map adsense
    enable_ads: false,
    publisher_id: "",
    max_ads_on_map: 5,
    min_zoom_level_for_ads: 6
  },

  // default options for creating markers
  markerDefaults: {
    // Point lat & lng
    latLng: [],
    // Point HTML for infoWindow
    html: null,
    // Event to open infoWindow (click, dblclick, mouseover, etc)
    htmlOpenEvent: "click",
    // Point is draggable?
    draggable: false,
    // These two are only required if adding to the marker manager
    minZoom: 0,
    maxZoom: 17,
    // Optional Icon to pass in
    icon: null,
    // For maximizing infoWindows
    maxContent: null
  },

  // default options for creating marker managers
  managerDefaults: {
    // Border Padding in pixels
    borderPadding: 100,
    // Max zoom level 
    maxZoom: 17,
    // Track markers
    trackMarkers: false
  }
});

// Wrap MapKit in an singleton
// MapKit.get returns the instance
Object.extend(MapKit, {
  set: function(mapkit) {
    MapKit.instance = mapkit;
  },
  get: function(){
    if(MapKit.instance == null)
    {
      MapKit.instance = new MapKit();
    }
    return MapKit.instance;
  },
  mergeOptions: function(){
    // copy reference to target object
    var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
    // Handle a deep copy situation
    if ( target.constructor == Boolean ) {
      deep = target;
      target = arguments[1] || {};
      // skip the boolean and the target
      i = 2;
    }
    // Handle case when target is a string or something (possible in deep copy)
    if ( typeof target != "object" && typeof target != "function" )
    {
      target = {};
    }
    // extend jQuery itself if only one argument is passed
    if ( length == i ) {
      target = this;
      --i;
    }
    for ( ; i < length; i++ )
    {
      // Only deal with non-null/undefined values
      if ( (options = arguments[ i ]) != null )
      {
        // Extend the base object
        for ( var name in options ) {
          var src = target[ name ], copy = options[ name ];
          // Prevent never-ending loop
          if ( target === copy )
          {
            continue;
          }
          // Recurse if we're merging object values
          if ( deep && copy && typeof copy == "object" && !copy.nodeType )
            target[ name ] = jQuery.extend( deep, 
              // Never move original objects, clone them
              src || ( copy.length != null ? [ ] : { } )
            , copy );

          // Don't bring in undefined values
          else if ( copy !== undefined )
          {
            target[ name ] = copy;
          }
        }
      }
    }

    // Return the modified object
    return target;
  },
  // Parse a Lat Lng String separated by 'x' 
  // eg "-34.53434x45.3483497394"
  // returns an array of floats
  parseLatLng: function(str) {
    parts = [];
    if(str)
    {
      parts = str.split("x");
      parts[0] = parseFloat(parts[0]);
      parts[1] = parseFloat(parts[1]);
    }
    return parts;
  }  
});
