# gmaps.core.js

    var extend_object = function(obj, new_obj) {
      var name;

      if (obj === new_obj) {
        return obj;
      }

      for (name in new_obj) {
        if (new_obj[name] !== undefined) {
          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;
      /**
       * Creates a new GMaps instance, including a Google Maps map.
       * @class GMaps
       * @constructs
       * @param {object} options - `options` accepts all the [MapOptions](https://developers.google.com/maps/documentation/javascript/reference#MapOptions) and [events](https://developers.google.com/maps/documentation/javascript/reference#Map) listed in the Google Maps API. Also accepts:
       * * `lat` (number): Latitude of the map's center
       * * `lng` (number): Longitude of the map's center
       * * `el` (string or HTMLElement): container where the map will be rendered
       * * `markerClusterer` (function): A function to create a marker cluster. You can use MarkerClusterer or MarkerClustererPlus.
       */
      var GMaps = function(options) {

        if (!(typeof window.google === 'object' && window.google.maps)) {
          if (typeof window.console === 'object' && window.console.error) {
            console.error('Google Maps API is required. Please register the following JavaScript library https://maps.googleapis.com/maps/api/js.');
          }

          return function() {};
        }

        if (!this) return new GMaps(options);

        options.zoom = options.zoom || 15;
        options.mapType = options.mapType || 'roadmap';

        var valueOrDefault = function(value, defaultValue) {
          return value === undefined ? defaultValue : value;
        };

        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 = valueOrDefault(options.zoomControl, true),
            zoomControlOpt = options.zoomControlOpt || {
              style: 'DEFAULT',
              position: 'TOP_LEFT'
            },
            zoomControlStyle = zoomControlOpt.style || 'DEFAULT',
            zoomControlPosition = zoomControlOpt.position || 'TOP_LEFT',
            panControl = valueOrDefault(options.panControl, true),
            mapTypeControl = valueOrDefault(options.mapTypeControl, true),
            scaleControl = valueOrDefault(options.scaleControl, true),
            streetViewControl = valueOrDefault(options.streetViewControl, true),
            overviewMapControl = valueOrDefault(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) {
                /**
                 * Container element
                 *
                 * @type {HTMLElement}
                 */
                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] = {};

        /**
         * Collection of custom controls in the map UI
         *
         * @type {array}
         */
        this.controls = [];
        /**
         * Collection of map's overlays
         *
         * @type {array}
         */
        this.overlays = [];
        /**
         * Collection of KML/GeoRSS and FusionTable layers
         *
         * @type {array}
         */
        this.layers = [];
        /**
         * Collection of data layers (See {@link GMaps#addLayer})
         *
         * @type {object}
         */
        this.singleLayers = {};
        /**
         * Collection of map's markers
         *
         * @type {array}
         */
        this.markers = [];
        /**
         * Collection of map's lines
         *
         * @type {array}
         */
        this.polylines = [];
        /**
         * Collection of map's routes requested by {@link GMaps#getRoutes}, {@link GMaps#renderRoute}, {@link GMaps#drawRoute}, {@link GMaps#travelRoute} or {@link GMaps#drawSteppedRoute}
         *
         * @type {array}
         */
        this.routes = [];
        /**
         * Collection of map's polygons
         *
         * @type {array}
         */
        this.polygons = [];
        this.infoWindow = null;
        this.overlay_el = null;
        /**
         * Current map's zoom
         *
         * @type {number}
         */
        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]];
        }

        /**
         * Google Maps map instance
         *
         * @type {google.maps.Map}
         */
        this.map = new google.maps.Map(this.el, map_options);

        if (markerClustererFunction) {
          /**
           * Marker Clusterer instance
           *
           * @type {object}
           */
          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);
          }

          var context_menu_element = getElementById('gmaps_context_menu');

          setTimeout(function() {
            context_menu_element.style.display = 'block';
          }, 0);
        };

        /**
         * Add a context menu for a map or a marker.
         *
         * @param {object} options - The `options` object should contain:
         * * `control` (string): Kind of control the context menu will be attached. Can be "map" or "marker".
         * * `options` (array): A collection of context menu items:
         *   * `title` (string): Item's title shown in the context menu.
         *   * `name` (string): Item's identifier.
         *   * `action` (function): Function triggered after selecting the context menu item.
         */
        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';

          if (!getElementById('gmaps_context_menu')) {
            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);
        };

        /**
         * Hide the current context menu
         */
        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);
          }
        });

        /**
         * Trigger a `resize` event, useful if you need to repaint the current map (for changes in the viewport or display / hide actions).
         */
        this.refresh = function() {
          google.maps.event.trigger(this.map, 'resize');
        };

        /**
         * Adjust the map zoom to include all the markers added in the map.
         */
        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);
        };

        /**
         * Adjust the map zoom to include all the coordinates in the `latLngs` array.
         *
         * @param {array} latLngs - Collection of `google.maps.LatLng` objects.
         */
        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);
        };

        /**
         * Center the map using the `lat` and `lng` coordinates.
         *
         * @param {number} lat - Latitude of the coordinate.
         * @param {number} lng - Longitude of the coordinate.
         * @param {function} [callback] - Callback that will be executed after the map is centered.
         */
        this.setCenter = function(lat, lng, callback) {
          this.map.panTo(new google.maps.LatLng(lat, lng));

          if (callback) {
            callback();
          }
        };

        /**
         * Return the HTML element container of the map.
         *
         * @returns {HTMLElement} the element container.
         */
        this.getElement = function() {
          return this.el;
        };

        /**
         * Increase the map's zoom.
         *
         * @param {number} [magnitude] - The number of times the map will be zoomed in.
         */
        this.zoomIn = function(value) {
          value = value || 1;

          this.zoom = this.map.getZoom() + value;
          this.map.setZoom(this.zoom);
        };

        /**
         * Decrease the map's zoom.
         *
         * @param {number} [magnitude] - The number of times the map will be zoomed out.
         */
        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);