text: if (!(typeof window.google === 'object' && window.google.maps)) {
    throw 'Google Maps API is required. Please register the following JavaScript library maps.google.com/maps/api/js?sensor=true.'
  }
  
  var extend_object = function(obj, new_obj) {
    var name;
  
    if (obj === new_obj) {
      return obj;
    }
  
    for (name in new_obj) {
      obj[name] = new_obj[name];
    }
  
    return obj;
  };
  
  var replace_object = function(obj, replace) {
    var name;
  
    if (obj === replace) {
      return obj;
    }
  
    for (name in replace) {
      if (obj[name] != undefined) {
        obj[name] = replace[name];
      }
    }
  
    return obj;
  };
  
  var array_map = function(array, callback) {
    var original_callback_params = Array.prototype.slice.call(arguments, 2),
        array_return = [],
        array_length = array.length,
        i;
  
    if (Array.prototype.map && array.map === Array.prototype.map) {
      array_return = Array.prototype.map.call(array, function(item) {
        var callback_params = original_callback_params.slice(0);
        callback_params.splice(0, 0, item);
  
        return callback.apply(this, callback_params);
      });
    }
    else {
      for (i = 0; i < array_length; i++) {
        callback_params = original_callback_params;
        callback_params.splice(0, 0, array[i]);
        array_return.push(callback.apply(this, callback_params));
      }
    }
  
    return array_return;
  };
  
  var array_flat = function(array) {
    var new_array = [],
        i;
  
    for (i = 0; i < array.length; i++) {
      new_array = new_array.concat(array[i]);
    }
  
    return new_array;
  };
  
  var coordsToLatLngs = function(coords, useGeoJSON) {
    var first_coord = coords[0],
        second_coord = coords[1];
  
    if (useGeoJSON) {
      first_coord = coords[1];
      second_coord = coords[0];
    }
  
    return new google.maps.LatLng(first_coord, second_coord);
  };
  
  var arrayToLatLng = function(coords, useGeoJSON) {
    var i;
  
    for (i = 0; i < coords.length; i++) {
      if (!(coords[i] instanceof google.maps.LatLng)) {
        if (coords[i].length > 0 && typeof(coords[i][0]) === "object") {
          coords[i] = arrayToLatLng(coords[i], useGeoJSON);
        }
        else {
          coords[i] = coordsToLatLngs(coords[i], useGeoJSON);
        }
      }
    }
  
    return coords;
  };
  
  
  var getElementsByClassName = function (class_name, context) {
  
      var element,
          _class = class_name.replace('.', '');
  
      if ('jQuery' in this && context) {
          element = $("." + _class, context)[0];
      } else {
          element = document.getElementsByClassName(_class)[0];
      }
      return element;
  
  };
  
  var getElementById = function(id, context) {
    var element,
    id = id.replace('#', '');
  
    if ('jQuery' in window && context) {
      element = $('#' + id, context)[0];
    } else {
      element = document.getElementById(id);
    };
  
    return element;
  };
  
  var findAbsolutePosition = function(obj)  {
    var curleft = 0,
        curtop = 0;
  
    if (obj.offsetParent) {
      do {
        curleft += obj.offsetLeft;
        curtop += obj.offsetTop;
      } while (obj = obj.offsetParent);
    }
  
    return [curleft, curtop];
  };
  
  var GMaps = (function(global) {
    "use strict";
  
    var doc = document;
  
    var GMaps = function(options) {
      if (!this) return new GMaps(options);
  
      options.zoom = options.zoom || 15;
      options.mapType = options.mapType || 'roadmap';
  
      var self = this,
          i,
          events_that_hide_context_menu = [
            'bounds_changed', 'center_changed', 'click', 'dblclick', 'drag',
            'dragend', 'dragstart', 'idle', 'maptypeid_changed', 'projection_changed',
            'resize', 'tilesloaded', 'zoom_changed'
          ],
          events_that_doesnt_hide_context_menu = ['mousemove', 'mouseout', 'mouseover'],
          options_to_be_deleted = ['el', 'lat', 'lng', 'mapType', 'width', 'height', 'markerClusterer', 'enableNewStyle'],
          identifier = options.el || options.div,
          markerClustererFunction = options.markerClusterer,
          mapType = google.maps.MapTypeId[options.mapType.toUpperCase()],
          map_center = new google.maps.LatLng(options.lat, options.lng),
          zoomControl = options.zoomControl || true,
          zoomControlOpt = options.zoomControlOpt || {
            style: 'DEFAULT',
            position: 'TOP_LEFT'
          },
          zoomControlStyle = zoomControlOpt.style || 'DEFAULT',
          zoomControlPosition = zoomControlOpt.position || 'TOP_LEFT',
          panControl = options.panControl || true,
          mapTypeControl = options.mapTypeControl || true,
          scaleControl = options.scaleControl || true,
          streetViewControl = options.streetViewControl || true,
          overviewMapControl = overviewMapControl || true,
          map_options = {},
          map_base_options = {
            zoom: this.zoom,
            center: map_center,
            mapTypeId: mapType
          },
          map_controls_options = {
            panControl: panControl,
            zoomControl: zoomControl,
            zoomControlOptions: {
              style: google.maps.ZoomControlStyle[zoomControlStyle],
              position: google.maps.ControlPosition[zoomControlPosition]
            },
            mapTypeControl: mapTypeControl,
            scaleControl: scaleControl,
            streetViewControl: streetViewControl,
            overviewMapControl: overviewMapControl
          };
  
        if (typeof(options.el) === 'string' || typeof(options.div) === 'string') {
  
            if (identifier.indexOf("#") > -1) {
                this.el = getElementById(identifier, options.context);
            } else {
                this.el = getElementsByClassName.apply(this, [identifier, options.context]);
            }
  
        } else {
            this.el = identifier;
        }
  
      if (typeof(this.el) === 'undefined' || this.el === null) {
        throw 'No element defined.';
      }
  
      window.context_menu = window.context_menu || {};
      window.context_menu[self.el.id] = {};
  
      this.controls = [];
      this.overlays = [];
      this.layers = []; // array with kml/georss and fusiontables layers, can be as many
      this.singleLayers = {}; // object with the other layers, only one per layer
      this.markers = [];
      this.polylines = [];
      this.routes = [];
      this.polygons = [];
      this.infoWindow = null;
      this.overlay_el = null;
      this.zoom = options.zoom;
      this.registered_events = {};
  
      this.el.style.width = options.width || this.el.scrollWidth || this.el.offsetWidth;
      this.el.style.height = options.height || this.el.scrollHeight || this.el.offsetHeight;
  
      google.maps.visualRefresh = options.enableNewStyle;
  
      for (i = 0; i < options_to_be_deleted.length; i++) {
        delete options[options_to_be_deleted[i]];
      }
  
      if(options.disableDefaultUI != true) {
        map_base_options = extend_object(map_base_options, map_controls_options);
      }
  
      map_options = extend_object(map_base_options, options);
  
      for (i = 0; i < events_that_hide_context_menu.length; i++) {
        delete map_options[events_that_hide_context_menu[i]];
      }
  
      for (i = 0; i < events_that_doesnt_hide_context_menu.length; i++) {
        delete map_options[events_that_doesnt_hide_context_menu[i]];
      }
  
      this.map = new google.maps.Map(this.el, map_options);
  
      if (markerClustererFunction) {
        this.markerClusterer = markerClustererFunction.apply(this, [this.map]);
      }
  
      var buildContextMenuHTML = function(control, e) {
        var html = '',
            options = window.context_menu[self.el.id][control];
  
        for (var i in options){
          if (options.hasOwnProperty(i)) {
            var option = options[i];
  
            html += '<li><a id="' + control + '_' + i + '" href="#">' + option.title + '</a></li>';
          }
        }
  
        if (!getElementById('gmaps_context_menu')) return;
  
        var context_menu_element = getElementById('gmaps_context_menu');
        
        context_menu_element.innerHTML = html;
  
        var context_menu_items = context_menu_element.getElementsByTagName('a'),
            context_menu_items_count = context_menu_items.length,
            i;
  
        for (i = 0; i < context_menu_items_count; i++) {
          var context_menu_item = context_menu_items[i];
  
          var assign_menu_item_action = function(ev){
            ev.preventDefault();
  
            options[this.id.replace(control + '_', '')].action.apply(self, [e]);
            self.hideContextMenu();
          };
  
          google.maps.event.clearListeners(context_menu_item, 'click');
          google.maps.event.addDomListenerOnce(context_menu_item, 'click', assign_menu_item_action, false);
        }
  
        var position = findAbsolutePosition.apply(this, [self.el]),
            left = position[0] + e.pixel.x - 15,
            top = position[1] + e.pixel.y- 15;
  
        context_menu_element.style.left = left + "px";
        context_menu_element.style.top = top + "px";
  
        context_menu_element.style.display = 'block';
      };
  
      this.buildContextMenu = function(control, e) {
        if (control === 'marker') {
          e.pixel = {};
  
          var overlay = new google.maps.OverlayView();
          overlay.setMap(self.map);
          
          overlay.draw = function() {
            var projection = overlay.getProjection(),
                position = e.marker.getPosition();
            
            e.pixel = projection.fromLatLngToContainerPixel(position);
  
            buildContextMenuHTML(control, e);
          };
        }
        else {
          buildContextMenuHTML(control, e);
        }
      };
  
      this.setContextMenu = function(options) {
        window.context_menu[self.el.id][options.control] = {};
  
        var i,
            ul = doc.createElement('ul');
  
        for (i in options.options) {
          if (options.options.hasOwnProperty(i)) {
            var option = options.options[i];
  
            window.context_menu[self.el.id][options.control][option.name] = {
              title: option.title,
              action: option.action
            };
          }
        }
  
        ul.id = 'gmaps_context_menu';
        ul.style.display = 'none';
        ul.style.position = 'absolute';
        ul.style.minWidth = '100px';
        ul.style.background = 'white';
        ul.style.listStyle = 'none';
        ul.style.padding = '8px';
        ul.style.boxShadow = '2px 2px 6px #ccc';
  
        doc.body.appendChild(ul);
  
        var context_menu_element = getElementById('gmaps_context_menu')
  
        google.maps.event.addDomListener(context_menu_element, 'mouseout', function(ev) {
          if (!ev.relatedTarget || !this.contains(ev.relatedTarget)) {
            window.setTimeout(function(){
              context_menu_element.style.display = 'none';
            }, 400);
          }
        }, false);
      };
  
      this.hideContextMenu = function() {
        var context_menu_element = getElementById('gmaps_context_menu');
  
        if (context_menu_element) {
          context_menu_element.style.display = 'none';
        }
      };
  
      var setupListener = function(object, name) {
        google.maps.event.addListener(object, name, function(e){
          if (e == undefined) {
            e = this;
          }
  
          options[name].apply(this, [e]);
  
          self.hideContextMenu();
        });
      };
  
      //google.maps.event.addListener(this.map, 'idle', this.hideContextMenu);
      google.maps.event.addListener(this.map, 'zoom_changed', this.hideContextMenu);
  
      for (var ev = 0; ev < events_that_hide_context_menu.length; ev++) {
        var name = events_that_hide_context_menu[ev];
  
        if (name in options) {
          setupListener(this.map, name);
        }
      }
  
      for (var ev = 0; ev < events_that_doesnt_hide_context_menu.length; ev++) {
        var name = events_that_doesnt_hide_context_menu[ev];
  
        if (name in options) {
          setupListener(this.map, name);
        }
      }
  
      google.maps.event.addListener(this.map, 'rightclick', function(e) {
        if (options.rightclick) {
          options.rightclick.apply(this, [e]);
        }
  
        if(window.context_menu[self.el.id]['map'] != undefined) {
          self.buildContextMenu('map', e);
        }
      });
  
      this.refresh = function() {
        google.maps.event.trigger(this.map, 'resize');
      };
  
      this.fitZoom = function() {
        var latLngs = [],
            markers_length = this.markers.length,
            i;
  
        for (i = 0; i < markers_length; i++) {
          if(typeof(this.markers[i].visible) === 'boolean' && this.markers[i].visible) {
            latLngs.push(this.markers[i].getPosition());
          }
        }
  
        this.fitLatLngBounds(latLngs);
      };
  
      this.fitLatLngBounds = function(latLngs) {
        var total = latLngs.length,
            bounds = new google.maps.LatLngBounds(),
            i;
  
        for(i = 0; i < total; i++) {
          bounds.extend(latLngs[i]);
        }
  
        this.map.fitBounds(bounds);
      };
  
      this.setCenter = function(lat, lng, callback) {
        this.map.panTo(new google.maps.LatLng(lat, lng));
  
        if (callback) {
          callback();
        }
      };
  
      this.getElement = function() {
        return this.el;
      };
  
      this.zoomIn = function(value) {
        value = value || 1;
  
        this.zoom = this.map.getZoom() + value;
        this.map.setZoom(this.zoom);
      };
  
      this.zoomOut = function(value) {
        value = value || 1;
  
        this.zoom = this.map.getZoom() - value;
        this.map.setZoom(this.zoom);
      };
  
      var native_methods = [],
          method;
  
      for (method in this.map) {
        if (typeof(this.map[method]) == 'function' && !this[method]) {
          native_methods.push(method);
        }
      }
  
      for (i = 0; i < native_methods.length; i++) {
        (function(gmaps, scope, method_name) {
          gmaps[method_name] = function(){
            return scope[method_name].apply(scope, arguments);
          };
        })(this, this.map, native_methods[i]);
      }
    };
  
    return GMaps;
  })(this);
  