/**
 * jQuery gMap v3
 *
 * @url         http://www.smashinglabs.pl/gmap
 * @author      Sebastian Poreba <sebastian.poreba@gmail.com>
 * @version     3.3.3
 * @date        27.12.2012
 */
/*jslint white: false, undef: true, regexp: true, plusplus: true, bitwise: true, newcap: true, strict: true, devel: true, maxerr: 50, indent: 4 */
/*global window, jQuery, $, google, $googlemaps */
(function($) {
  'use strict';

  /**
     * Internals and experimental section
     */
  var Cluster = function() {
    this.markers = [];
    this.mainMarker = false;
    this.icon = 'http://www.google.com/mapfiles/marker.png';
  };

  /**
     * For iterating over all clusters to find if any is close enough to be merged with marker
     *
     * @param marker
     * @param currentSize - calculated as viewport percentage (opts.clusterSize).
     * @return bool.
     */
  Cluster.prototype.dist = function(marker) {
    return Math.sqrt(Math.pow(this.markers[0].latitude - marker.latitude, 2) +
            Math.pow(this.markers[0].longitude - marker.longitude, 2));
  };

  Cluster.prototype.setIcon = function(icon) {
    this.icon = icon;
  };

  Cluster.prototype.addMarker = function(marker) {
    this.markers[this.markers.length] = marker;
  };

  /**
     * returns one marker if there is only one or
     * returns special cloister marker if there are more
     */
  Cluster.prototype.getMarker = function() {
    if (this.mainmarker) {return this.mainmarker; }
    var gicon, title;
    if (this.markers.length > 1) {
      gicon = new $googlemaps.MarkerImage('http://chart.googleapis.com/chart?chst=d_map_pin_letter&chld=' + this.markers.length + '%7cff776b%7c000000');
      title = 'cluster of ' + this.markers.length + ' markers';
    } else {
      gicon = new $googlemaps.MarkerImage(this.icon);
      title = this.markers[0].title;
    }
    this.mainmarker = new $googlemaps.Marker({
      position: new $googlemaps.LatLng(this.markers[0].latitude, this.markers[0].longitude),
      icon: gicon,
      title: title,
      map: null
    });
    return this.mainmarker;
  };

  // global google maps objects
  var $googlemaps = google.maps,
      $geocoder = new $googlemaps.Geocoder(),
      $markersToLoad = 0,
      overQueryLimit = 0,
      methods = {};
  methods = {
    /**
         * initialisation/internals
         */

    init: function(options) {
      var k,
          // Build main options before element iteration
          opts = $.extend({}, $.fn.gMap.defaults, options);

                // recover icon array
                for (k in $.fn.gMap.defaults.icon) {
        if (!opts.icon[k]) {
          opts.icon[k] = $.fn.gMap.defaults.icon[k];
        }
                }

      // Iterate through each element
      return this.each(function() {
        var $this = $(this),
                    center = methods._getMapCenter.apply($this, [opts]),
                    i, $data;

        if (opts.zoom == 'fit') {
                    opts.zoomFit = true;
                    opts.zoom = methods._autoZoom.apply($this, [opts]);
        }

        var mapOptions = {
          zoom: opts.zoom,
          center: center,
          mapTypeControl: opts.mapTypeControl,
          mapTypeControlOptions: {},
          zoomControl: opts.zoomControl,
          zoomControlOptions: {},
          panControl: opts.panControl,
          panControlOptions: {},
          scaleControl: opts.scaleControl,
          scaleControlOptions: {},
          streetViewControl: opts.streetViewControl,
          streetViewControlOptions: {},
          mapTypeId: opts.maptype,
          scrollwheel: opts.scrollwheel,
          maxZoom: opts.maxZoom,
          minZoom: opts.minZoom
        };
        if (opts.controlsPositions.mapType) {mapOptions.mapTypeControlOptions.position = opts.controlsPositions.mapType}
        if (opts.controlsPositions.zoom) {mapOptions.zoomControlOptions.position = opts.controlsPositions.zoom}
        if (opts.controlsPositions.pan) {mapOptions.panControlOptions.position = opts.controlsPositions.pan}
        if (opts.controlsPositions.scale) {mapOptions.scaleControlOptions.position = opts.controlsPositions.scale}
        if (opts.controlsPositions.streetView) {mapOptions.streetViewControlOptions.position = opts.controlsPositions.streetView}

        if (opts.styles) {mapOptions.styles = opts.styles}

        mapOptions.mapTypeControlOptions.style = opts.controlsStyle.mapType;
        mapOptions.zoomControlOptions.style = opts.controlsStyle.zoom;

        mapOptions = $.extend(mapOptions, opts.extra);

        // Create map and set initial options
        var $gmap = new $googlemaps.Map(this, mapOptions);

        if (opts.log) {console.log('map center is:'); }
        if (opts.log) {console.log(center); }

        // Create map and set initial options

        $this.data('$gmap', $gmap);

        $this.data('gmap', {
                    'opts': opts,
                    'gmap': $gmap,
                    'markers': [],
                    'markerKeys' : {},
                    'infoWindow': null,
                    'clusters': []
        });

        // Check for map controls
        if (opts.controls.length !== 0) {
                    // Add custom map controls
                    for (i = 0; i < opts.controls.length; i += 1) {
            $gmap.controls[opts.controls[i].pos].push(opts.controls[i].div);
                    }
        }

        if (opts.clustering.enabled) {
                    $data = $this.data('gmap');
                    (function(markers) {$data.markers = markers;}(opts.markers));
                    methods._renderCluster.apply($this, []);

                    $googlemaps.event.addListener($gmap, 'bounds_changed', function() {
            methods._renderCluster.apply($this, []);
                    });
        } else {
                    if (opts.markers.length !== 0) {
            methods.addMarkers.apply($this, [opts.markers]);
                    }
        }

        methods._onComplete.apply($this, []);
      });
    },


    _delayedMode: false,

    /**
         * Check every 100ms if all markers were loaded, then call onComplete
         */
    _onComplete: function() {
      var $data = this.data('gmap'),
                that = this;
      if ($markersToLoad !== 0) {
        window.setTimeout(function() {methods._onComplete.apply(that, []); }, 100);
        return;
      }
      if (methods._delayedMode) {
        var center = methods._getMapCenter.apply(this, [$data.opts, true]);
        methods._setMapCenter.apply(this, [center]);
        if ($data.opts.zoomFit) {
          var zoom = methods._autoZoom.apply(this, [$data.opts, true]);
          $data.gmap.setZoom(zoom);
        }
      }
      $data.opts.onComplete();
    },

    /**
         * set map center when map is loaded (check every 100ms)
         */
    _setMapCenter: function(center) {
      var $data = this.data('gmap');
      if ($data && $data.opts.log) {console.log('delayed setMapCenter called'); }
      if ($data && $data.gmap !== undefined && $markersToLoad == 0) {
        $data.gmap.setCenter(center);
      } else {
        var that = this;
        window.setTimeout(function() {methods._setMapCenter.apply(that, [center]); }, 100);
      }
    },

    /**
         * calculate boundaries, optimised and independent from Google Maps
         */
    _boundaries: null,
    _getBoundaries: function(opts) {
      // if(methods._boundaries) {return methods._boundaries; }
      var markers = opts.markers, i;
      var mostN = 1000,
                mostE = -1000,
                mostW = 1000,
                mostS = -1000;
      if (markers) {
        for (i = 0; i < markers.length; i += 1) {
                    if (!markers[i].latitude || !markers[i].longitude) continue;

                    if (mostN > markers[i].latitude) {mostN = markers[i].latitude; }
                    if (mostE < markers[i].longitude) {mostE = markers[i].longitude; }
                    if (mostW > markers[i].longitude) {mostW = markers[i].longitude; }
                    if (mostS < markers[i].latitude) {mostS = markers[i].latitude; }
                    if (opts.log) {
                      console.log(markers[i].latitude, markers[i].longitude, mostN, mostE, mostW, mostS);
                    }
        }
        methods._boundaries = {N: mostN, E: mostE, W: mostW, S: mostS};
      }

      if (mostN == -1000) methods._boundaries = {N: 0, E: 0, W: 0, S: 0};

      return methods._boundaries;
    },

    /**
         * Priorities order:
         * - latitude & longitude in options
         * - address in options
         * - latitude & longitude of first marker having it
         * - address of first marker having it
         * - failsafe (0,0)
         *
         * Note: with geocoding returned value is (0,0) and callback sets map center. It's not very nice nor efficient.
         *       It is quite good idea to use only first option
         */
    _getMapCenter: function(opts, fromMarkers) {
      // Create new object to geocode addresses

      var center,
                that = this, // 'that' scope fix in geocoding
                i,
                selectedToCenter,
                most; //hoisting

      if (opts.markers.length && (opts.latitude == 'fit' || opts.longitude == 'fit')) {
        if (fromMarkers) {
          opts.markers = methods._convertMarkers(data.markers);
        }

        most = methods._getBoundaries(opts);
        center = new $googlemaps.LatLng((most.N + most.S) / 2, (most.E + most.W) / 2);
        if (opts.log) {
          console.log(fromMarkers, most, center);
        }
        return center;
      }

      if (opts.latitude && opts.longitude) {
        // lat & lng available, return
        center = new $googlemaps.LatLng(opts.latitude, opts.longitude);
        return center;
      } else {
        center = new $googlemaps.LatLng(0, 0);
      }

      // Check for address to center on
      if (opts.address) {
        // Get coordinates for given address and center the map
        $geocoder.geocode(
            {address: opts.address},
            function(result, status) {
                        if (status === google.maps.GeocoderStatus.OK) {
                methods._setMapCenter.apply(that, [result[0].geometry.location]);
                        } else {
                if (opts.log) {console.log('Geocode was not successful for the following reason: ' + status); }
                        }
            }
        );
        return center;
      }

      // Check for a marker to center on (if no coordinates given)
      if (opts.markers.length > 0) {
        selectedToCenter = null;

        for (i = 0; i < opts.markers.length; i += 1) {
                    if (opts.markers[i].setCenter) {
            selectedToCenter = opts.markers[i];
            break;
                    }
        }

        if (selectedToCenter === null) {
                    for (i = 0; i < opts.markers.length; i += 1) {
            if (opts.markers[i].latitude && opts.markers[i].longitude) {
              selectedToCenter = opts.markers[i];
              break;
            }
            if (opts.markers[i].address) {
              selectedToCenter = opts.markers[i];
            }
                    }
        }
        // failed to find any reasonable marker (it's quite impossible BTW)
        if (selectedToCenter === null) {
                    return center;
        }

        if (selectedToCenter.latitude && selectedToCenter.longitude) {
                    return new $googlemaps.LatLng(selectedToCenter.latitude, selectedToCenter.longitude);
        }

        // Check if the marker has an address
        if (selectedToCenter.address) {
                    // Get the coordinates for given marker address and center
                    $geocoder.geocode(
                        {address: selectedToCenter.address},
                        function(result, status) {
                if (status === google.maps.GeocoderStatus.OK) {
                  methods._setMapCenter.apply(that, [result[0].geometry.location]);
                } else {
                  if (opts.log) {console.log('Geocode was not successful for the following reason: ' + status); }
                }
                        }
                    );
        }
      }
      return center;
    },


    /**
         * clustering
         */
    _renderCluster: function() {
      var $data = this.data('gmap'),
                markers = $data.markers,
                clusters = $data.clusters,
                opts = $data.opts,
                i,
                j,
                viewport;

      for (i = 0; i < clusters.length; i += 1) {
        clusters[i].getMarker().setMap(null);
      }
      clusters.length = 0;

      viewport = $data.gmap.getBounds();

      if (!viewport) {
        var that = this;
        window.setTimeout(function() {methods._renderCluster.apply(that); }, 1000);
        return;
      }

      var ne = viewport.getNorthEast(),
                sw = viewport.getSouthWest(),
                width = ne.lat() - sw.lat(),
                // height = ne.lng() - sw.lng(), // unused
                clusterable = [],
                best,
                bestDist,
                maxSize = width * opts.clustering.clusterSize / 100,
                dist,
                newCluster;

      for (i = 0; i < markers.length; i += 1) {
        if (markers[i].latitude < ne.lat() &&
            markers[i].latitude > sw.lat() &&
                    markers[i].longitude < ne.lng() &&
                    markers[i].longitude > sw.lng()) {
                    clusterable[clusterable.length] = markers[i];
        }
      }

      if (opts.log) {console.log('number of markers ' + clusterable.length + '/' + markers.length); }
      if (opts.log) {console.log('cluster radius: ' + maxSize); }

      for (i = 0; i < clusterable.length; i += 1) {
        bestDist = 10000;
        best = -1;
        for (j = 0; j < clusters.length; j += 1) {
                    dist = clusters[j].dist(clusterable[i]);
                    if (dist < maxSize) {
            bestDist = dist;
            best = j;
            if (opts.clustering.fastClustering) {break; }
                    }
        }
        if (best === -1) {
                    newCluster = new Cluster();
                    newCluster.addMarker(clusterable[i]);
                    clusters[clusters.length] = newCluster;
        } else {
                    clusters[best].addMarker(clusterable[i]);
        }
      }

      if (opts.log) {console.log('Total clusters in viewport: ' + clusters.length); }

      for (j = 0; j < clusters.length; j += 1) {
        clusters[j].getMarker().setMap($data.gmap);
      }
    },

    _processMarker: function(marker, gicon, gshadow, location) {
      var $data = this.data('gmap'),
                $gmap = $data.gmap,
                opts = $data.opts,
                gmarker,
                markeropts;

      if (location === undefined) {
        location = new $googlemaps.LatLng(marker.latitude, marker.longitude);
      }

      if (!gicon) {

        // Set icon properties from global options
        var _gicon = {
                    image: opts.icon.image,
                    iconSize: new $googlemaps.Size(opts.icon.iconsize[0], opts.icon.iconsize[1]),
                    iconAnchor: new $googlemaps.Point(opts.icon.iconanchor[0], opts.icon.iconanchor[1]),
                    infoWindowAnchor: new $googlemaps.Size(opts.icon.infowindowanchor[0], opts.icon.infowindowanchor[1])
        };
        gicon = new $googlemaps.MarkerImage(_gicon.image, _gicon.iconSize, null, _gicon.iconAnchor);
      }

      if (!gshadow) {
        var _gshadow = {
                    image: opts.icon.shadow,
                    iconSize: new $googlemaps.Size(opts.icon.shadowsize[0], opts.icon.shadowsize[1]),
                    anchor: (_gicon && _gicon.iconAnchor) ? _gicon.iconAnchor : new $googlemaps.Point(opts.icon.iconanchor[0], opts.icon.iconanchor[1])
        };
      }

      markeropts = {
        position: location,
        icon: gicon,
        title: marker.title,
        map: null,
        draggable: ((marker.draggable === true) ? true : false)
      };

      if (!opts.clustering.enabled) {markeropts.map = $gmap; }

      gmarker = new $googlemaps.Marker(markeropts);
      gmarker.setShadow(gshadow);
      $data.markers.push(gmarker);

      if (marker.key) {$data.markerKeys[marker.key] = gmarker; }

      // Set HTML and check if info window should be opened
      var infoWindow;
      if (marker.html) {
        var infoContent = typeof(marker.html) === 'string' ? opts.html_prepend + marker.html + opts.html_append : marker.html;
        var infoOpts = {
          content: infoContent,
          pixelOffset: marker.infoWindowAnchor,
          disableAutoPan: true
        };

        if (opts.log) {console.log('setup popup with data'); }
        if (opts.log) {console.log(infoOpts); }
        infoWindow = new $googlemaps.InfoWindow(infoOpts);

        $googlemaps.event.addListener(gmarker, 'click', function() {
          if (opts.log) {console.log('opening popup ' + marker.html); }
          if (opts.singleInfoWindow && $data.infoWindow) {$data.infoWindow.close(); }
          infoWindow.open($gmap, gmarker);
          $data.infoWindow = infoWindow;
        });
      }
      if (marker.html && marker.popup) {
        if (opts.log) {console.log('opening popup ' + marker.html); }
        infoWindow.open($gmap, gmarker);
        $data.infoWindow = infoWindow;
      }

      if (marker.onDragEnd) {
        $googlemaps.event.addListener(gmarker, 'dragend', function(event) {
          if (opts.log) {console.log('drag end');}
          marker.onDragEnd(event);
        });
      }

    },

    _convertMarkers: function(googleMarkers) {
      var markers = [], i;
      for (i = 0; i < googleMarkers.length; i += 1) {
        markers[i] = {
          latitude: googleMarkers[i].getPosition().lat(),
          longitude: googleMarkers[i].getPosition().lng()
        };
      }
      return markers;
    },

    _geocodeMarker: function(marker, gicon, gshadow) {
      var that = this;
      $geocoder.geocode({'address': marker.address}, function(results, status) {
        if (status === $googlemaps.GeocoderStatus.OK) {
                    $markersToLoad -= 1;
                    if (that.data('gmap').opts.log) {console.log('Geocode was successful with point: ', results[0].geometry.location); }
                    methods._processMarker.apply(that, [marker, gicon, gshadow, results[0].geometry.location]);
        } else {
                    if (status === $googlemaps.GeocoderStatus.OVER_QUERY_LIMIT) {
            if ((!that.data('gmap').opts.noAlerts) && (overQueryLimit === 0)) {alert('Error: too many geocoded addresses! Switching to 1 marker/s mode.'); }

            overQueryLimit += 1000;
            window.setTimeout(function() {
              methods._geocodeMarker.apply(that, [marker, gicon, gshadow]);
            }, overQueryLimit);
                    }
                    if (that.data('gmap').opts.log) {console.log('Geocode was not successful for the following reason: ' + status); }
        }
      });
    },

    _autoZoom: function(options, fromMarkers) {
      var data = $(this).data('gmap'),
                opts = $.extend({}, data ? data.opts : {}, options),
                i, boundaries, resX, resY, baseScale = 39135.758482;
      if (opts.log) {console.log('autozooming map');}

      if (fromMarkers) {
        opts.markers = methods._convertMarkers(data.markers);
      }

      boundaries = methods._getBoundaries(opts);

      resX = (boundaries.E - boundaries.W) * 111000 / this.width();
      resY = (boundaries.S - boundaries.N) * 111000 / this.height();

      for (i = 2; i < 20; i += 1) {
        if (resX > baseScale || resY > baseScale) {
                    break;
        }
        baseScale = baseScale / 2;
      }
      return i - 2;
    },

    /**
         * public methods section
         */

    /**
         * add array of markers
         * @param markers
         */
    addMarkers: function(markers) {
      var opts = this.data('gmap').opts;

      if (markers.length !== 0) {
        if (opts.log) {console.log('adding ' + markers.length + ' markers');}
        // Loop through marker array
        for (var i = 0; i < markers.length; i += 1) {
                    methods.addMarker.apply(this, [markers[i]]);
        }
      }
    },

    /**
         * add single marker
         * @param marker
         */
    addMarker: function(marker) {
      var opts = this.data('gmap').opts;

      if (opts.log) {console.log('putting marker at ' + marker.latitude + ', ' + marker.longitude + ' with address ' + marker.address + ' and html ' + marker.html); }

      // Create new icon
      // Set icon properties from global options
      var _gicon = {
        image: opts.icon.image,
        iconSize: new $googlemaps.Size(opts.icon.iconsize[0], opts.icon.iconsize[1]),
        iconAnchor: new $googlemaps.Point(opts.icon.iconanchor[0], opts.icon.iconanchor[1]),
        infoWindowAnchor: new $googlemaps.Size(opts.icon.infowindowanchor[0], opts.icon.infowindowanchor[1])
      },
          _gshadow = {
            image: opts.icon.shadow,
            iconSize: new $googlemaps.Size(opts.icon.shadowsize[0], opts.icon.shadowsize[1]),
            anchor: new $googlemaps.Point(opts.icon.shadowanchor[0], opts.icon.shadowanchor[1])
          };

      // not very nice, but useful
      marker.infoWindowAnchor = _gicon.infoWindowAnchor;

      if (marker.icon) {
        // Overwrite global options
        if (marker.icon.image) { _gicon.image = marker.icon.image; }
        if (marker.icon.iconsize) { _gicon.iconSize = new $googlemaps.Size(marker.icon.iconsize[0], marker.icon.iconsize[1]); }

        if (marker.icon.iconanchor) { _gicon.iconAnchor = new $googlemaps.Point(marker.icon.iconanchor[0], marker.icon.iconanchor[1]); }
        if (marker.icon.infowindowanchor) { _gicon.infoWindowAnchor = new $googlemaps.Size(marker.icon.infowindowanchor[0], marker.icon.infowindowanchor[1]); }

        if (marker.icon.shadow) { _gshadow.image = marker.icon.shadow; }
        if (marker.icon.shadowsize) { _gshadow.iconSize = new $googlemaps.Size(marker.icon.shadowsize[0], marker.icon.shadowsize[1]); }

        if (marker.icon.shadowanchor) { _gshadow.anchor = new $googlemaps.Point(marker.icon.shadowanchor[0], marker.icon.shadowanchor[1]); }
      }

      var gicon = new $googlemaps.MarkerImage(_gicon.image, _gicon.iconSize, null, _gicon.iconAnchor);
      var gshadow = new $googlemaps.MarkerImage(_gshadow.image, _gshadow.iconSize, null, _gshadow.anchor);

      // Check if address is available
      if (marker.address) {
        // Check for reference to the marker's address
        if (marker.html === '_address') {
                    marker.html = marker.address;
        }

        if (marker.title == '_address') {
                    marker.title = marker.address;
        }

        if (opts.log) {console.log('geocoding marker: ' + marker.address); }
        // Get the point for given address
        $markersToLoad += 1;
        methods._delayedMode = true;
        methods._geocodeMarker.apply(this, [marker, gicon, gshadow]);
      } else {
        // Check for reference to the marker's latitude/longitude
        if (marker.html === '_latlng') {
                    marker.html = marker.latitude + ', ' + marker.longitude;
        }

        if (marker.title == '_latlng') {
                    marker.title = marker.latitude + ', ' + marker.longitude;
        }

        // Create marker
        var gpoint = new $googlemaps.LatLng(marker.latitude, marker.longitude);
        methods._processMarker.apply(this, [marker, gicon, gshadow, gpoint]);
      }
    },

    /**
         *
         */
    removeAllMarkers: function() {
      var markers = this.data('gmap').markers, markerKeys = this.data('gmap').markerKeys, i;

      for (i = 0; i < markers.length; i += 1) {
        markers[i].setMap(null);
        delete markers[i];
      }
      markers.length = 0;

      for (i in markerKeys) {
        delete markerKeys[i];
      }
    },

    removeMarker: function(marker) {
      var markers = this.data('gmap').markers, markerKeys = this.data('gmap').markerKeys, i = markers.indexOf(marker);

      if (i !== -1) {
        markers[i].setMap(null);
        delete markers[i];
        markers.splice(i, 1);
      }

      for (i in markerKeys) {
        if (markerKeys[i] === marker) {
          delete markerKeys[i];
        }
      }
    },

    /**
         * get marker by key, if set previously
         * @param key
         */
    getMarker: function(key) {
      return this.data('gmap').markerKeys[key];
    },

    /**
         * should be called if DOM element was resized
         * @param nasty
         */
    fixAfterResize: function(nasty) {
      var data = this.data('gmap');
      $googlemaps.event.trigger(data.gmap, 'resize');

      if (nasty) {
        data.gmap.panTo(new google.maps.LatLng(0, 0));
      }
      data.gmap.panTo(this.gMap('_getMapCenter', data.opts));
    },

    /**
         * change zoom, works with 'fit' option as well
         * @param zoom
         */
    setZoom: function(zoom, opts, fromMarkers) {
      var $map = this.data('gmap').gmap;
      if (zoom === 'fit') {
        zoom = methods._autoZoom.apply(this, [opts, fromMarkers]);
      }
      $map.setZoom(parseInt(zoom));
    },

    changeSettings: function(options) {
      var data = this.data('gmap'),
                markers = [], i, originalMarkers;
      if (options.markers === undefined) {
        options.markers = methods._convertMarkers(data.markers);
      }
      else if (options.markers.length !== 0 && options.markers[0].latitude === undefined) {
        options.markers = methods._convertMarkers(options.markers);
      }

      if (options.zoom) methods.setZoom.apply(this, [options.zoom, options]);
      if (options.latitude || options.longitude) {
        data.gmap.panTo(methods._getMapCenter.apply(this, [options]));
      }

      // add controls and maptype
    },

    mapclick: function(callback) {
      google.maps.event.addListener(this.data('gmap').gmap, 'click', function(event) {
        callback(event.latLng);
      });
    },

    geocode: function(address, callback, errorCallback) {
      $geocoder.geocode({'address': address}, function(results, status) {
        if (status === $googlemaps.GeocoderStatus.OK) {
          callback(results[0].geometry.location, results[0].geometry.viewport);
        } else if (errorCallback) {
          errorCallback(results, status);
        }
      });
    },

    getRoute: function(options) {

      var $data = this.data('gmap'),
          $gmap = $data.gmap,
          $directionsDisplay = new $googlemaps.DirectionsRenderer(),
          $directionsService = new $googlemaps.DirectionsService(),
          $travelModes = { 'BYCAR': $googlemaps.DirectionsTravelMode.DRIVING, 'BYBICYCLE': $googlemaps.DirectionsTravelMode.BICYCLING, 'BYFOOT': $googlemaps.DirectionsTravelMode.WALKING },
          $travelUnits = { 'MILES': $googlemaps.DirectionsUnitSystem.IMPERIAL, 'KM': $googlemaps.DirectionsUnitSystem.METRIC },
          displayObj = null,
          travelMode = null,
          travelUnit = null,
          unitSystem = null;

      // look if there is an individual or otherwise a default object for this call to display route text informations
      if (options.routeDisplay !== undefined) {
        displayObj = (options.routeDisplay instanceof jQuery) ? options.routeDisplay[0] : ((typeof options.routeDisplay == 'string') ? $(options.routeDisplay)[0] : null);
      } else if ($data.opts.routeFinder.routeDisplay !== null) {
        displayObj = ($data.opts.routeFinder.routeDisplay instanceof jQuery) ? $data.opts.routeFinder.routeDisplay[0] : ((typeof $data.opts.routeFinder.routeDisplay == 'string') ? $($data.opts.routeFinder.routeDisplay)[0] : null);
      }

      // set route renderer to map
      $directionsDisplay.setMap($gmap);
      if (displayObj !== null) {
        $directionsDisplay.setPanel(displayObj);
      }

      // get travel mode and unit
      travelMode = ($travelModes[$data.opts.routeFinder.travelMode] !== undefined) ? $travelModes[$data.opts.routeFinder.travelMode] : $travelModes['BYCAR'];
      travelUnit = ($travelUnits[$data.opts.routeFinder.travelUnit] !== undefined) ? $travelUnits[$data.opts.routeFinder.travelUnit] : $travelUnits['KM'];

      // build request
      var request = {
        origin: options.from,
        destination: options.to,
        travelMode: travelMode,
        unitSystem: travelUnit
      };

      // send request
      $directionsService.route(request, function(result, status) {
        // show the rout or otherwise show an error message in a defined container for route text information
        if (status == $googlemaps.DirectionsStatus.OK) {
                    $directionsDisplay.setDirections(result);
        } else if (displayObj !== null) {
                    $(displayObj).html($data.opts.routeFinder.routeErrors[status]);
        }
      });
      return this;
    }
  };


  // Main plugin function
  $.fn.gMap = function(method) {
    // Method calling logic
    if (methods[method]) {
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
    } else if (typeof method === 'object' || !method) {
      return methods.init.apply(this, arguments);
    } else {
      $.error('Method ' + method + ' does not exist on jQuery.gmap');
    }
  };

  // Default settings
  $.fn.gMap.defaults = {
    log: false,
    address: '',
    latitude: null,
    longitude: null,
    zoom: 3,
    maxZoom: null,
    minZoom: null,
    markers: [],
    controls: {},
    scrollwheel: true,
    maptype: google.maps.MapTypeId.ROADMAP,

    mapTypeControl: true,
    zoomControl: true,
    panControl: false,
    scaleControl: false,
    streetViewControl: true,

    controlsPositions: {
      mapType: null,
      zoom: null,
      pan: null,
      scale: null,
      streetView: null
    },
    controlsStyle: {
      mapType: google.maps.MapTypeControlStyle.DEFAULT,
      zoom: google.maps.ZoomControlStyle.DEFAULT
    },

    singleInfoWindow: true,

    html_prepend: '<div class="gmap_marker">',
    html_append: '</div>',
    icon: {
      image: 'http://www.google.com/mapfiles/marker.png',
      iconsize: [20, 34],
      iconanchor: [9, 34],
      infowindowanchor: [0, 0],
      shadow: 'http://www.google.com/mapfiles/shadow50.png',
      shadowsize: [37, 34],
      shadowanchor: [9, 34]
    },

    onComplete: function() {},

    routeFinder: {
      travelMode: 'BYCAR',
      travelUnit: 'KM',
      routeDisplay: null,
      routeErrors:  {
        'INVALID_REQUEST': 'The provided request is invalid.',
        'NOT_FOUND': 'One or more of the given addresses could not be found.',
        'OVER_QUERY_LIMIT': 'A temporary error occured. Please try again in a few minutes.',
        'REQUEST_DENIED': 'An error occured. Please contact us.',
        'UNKNOWN_ERROR': 'An unknown error occured. Please try again.',
        'ZERO_RESULTS': 'No route could be found within the given addresses.'
      }
    },

    clustering: {
      enabled: false,
      fastClustering: false,
      clusterCount: 10,
      clusterSize: 40 //radius as % of viewport width
    },
    extra: {}
  };
}(jQuery));