/*!
*  angular-leaflet-directive  2015-11-06
*  angular-leaflet-directive - An AngularJS directive to easily interact with Leaflet maps
*  git: https://github.com/tombatossals/angular-leaflet-directive
*/
(function(angular){
'use strict';
angular.module('leaflet-directive', []).directive('leaflet', ["$q", "leafletData", "leafletMapDefaults", "leafletHelpers", "leafletMapEvents", function($q, leafletData, leafletMapDefaults, leafletHelpers, leafletMapEvents) {
  return {
    restrict: 'EA',
    replace: true,
    scope: {
      center: '=',
      lfCenter: '=',
      defaults: '=',
      maxbounds: '=',
      bounds: '=',
      markers: '=',
      legend: '=',
      geojson: '=',
      paths: '=',
      tiles: '=',
      layers: '=',
      controls: '=',
      decorations: '=',
      eventBroadcast: '=',
      markersWatchOptions: '=',
      geojsonWatchOptions: '=',
    },
    transclude: true,
    template: '<div class="angular-leaflet-map"><div ng-transclude></div></div>',
    controller: ["$scope", function($scope) {
      this._leafletMap = $q.defer();
      this.getMap = function() {
        return this._leafletMap.promise;
      };

      this.getLeafletScope = function() {
        return $scope;
      };
    }],

    link: function(scope, element, attrs, ctrl) {
      var isDefined = leafletHelpers.isDefined;
      var defaults  = leafletMapDefaults.setDefaults(scope.defaults, attrs.id);
      var mapEvents = leafletMapEvents.getAvailableMapEvents();
      var addEvents = leafletMapEvents.addEvents;

      scope.mapId =  attrs.id;
      leafletData.setDirectiveControls({}, attrs.id);

      // Set width and height utility functions
      function updateWidth() {
        if (isNaN(attrs.width)) {
          element.css('width', attrs.width);
        } else {
          element.css('width', attrs.width + 'px');
        }
      }

      function updateHeight() {
        if (isNaN(attrs.height)) {
          element.css('height', attrs.height);
        } else {
          element.css('height', attrs.height + 'px');
        }
      }

      // If the width attribute defined update css
      // Then watch if bound property changes and update css
      if (isDefined(attrs.width)) {
        updateWidth();

        scope.$watch(
          function() {
            return element[0].getAttribute('width');
          },

          function() {
            updateWidth();
            map.invalidateSize();
          });
      }

      // If the height attribute defined update css
      // Then watch if bound property changes and update css
      if (isDefined(attrs.height)) {
        updateHeight();

        scope.$watch(
          function() {
            return element[0].getAttribute('height');
          },

          function() {
            updateHeight();
            map.invalidateSize();
          });
      }

      // Create the Leaflet Map Object with the options
      var map = new L.Map(element[0], leafletMapDefaults.getMapCreationDefaults(attrs.id));
      ctrl._leafletMap.resolve(map);

      if (!isDefined(attrs.center) && !isDefined(attrs.lfCenter)) {
        map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom);
      }

      // If no layers nor tiles defined, set the default tileLayer
      if (!isDefined(attrs.tiles) && (!isDefined(attrs.layers))) {
        var tileLayerObj = L.tileLayer(defaults.tileLayer, defaults.tileLayerOptions);
        tileLayerObj.addTo(map);
        leafletData.setTiles(tileLayerObj, attrs.id);
      }

      // Set zoom control configuration
      if (isDefined(map.zoomControl) &&
          isDefined(defaults.zoomControlPosition)) {
        map.zoomControl.setPosition(defaults.zoomControlPosition);
      }

      if (isDefined(map.zoomControl) && defaults.zoomControl === false) {
        map.zoomControl.removeFrom(map);
      }

      if (isDefined(map.zoomsliderControl) &&
          isDefined(defaults.zoomsliderControl) &&
          defaults.zoomsliderControl === false) {
        map.zoomsliderControl.removeFrom(map);
      }

      // if no event-broadcast attribute, all events are broadcasted
      if (!isDefined(attrs.eventBroadcast)) {
        var logic = 'broadcast';
        addEvents(map, mapEvents, 'eventName', scope, logic);
      }

      // Resolve the map object to the promises
      map.whenReady(function() {
        leafletData.setMap(map, attrs.id);
      });

      scope.$on('$destroy', function() {
        leafletMapDefaults.reset();
        map.remove();
        leafletData.unresolveMap(attrs.id);
      });

      //Handle request to invalidate the map size
      //Up scope using $scope.$emit('invalidateSize')
      //Down scope using $scope.$broadcast('invalidateSize')
      scope.$on('invalidateSize', function() {
        map.invalidateSize();
      });
    },
  };
}]);

angular.module('leaflet-directive').factory('leafletBoundsHelpers', ["$log", "leafletHelpers", function($log, leafletHelpers) {

  var isArray = leafletHelpers.isArray;
  var isNumber = leafletHelpers.isNumber;
  var isFunction = leafletHelpers.isFunction;
  var isDefined = leafletHelpers.isDefined;

  function _isValidBounds(bounds) {
    return angular.isDefined(bounds) && angular.isDefined(bounds.southWest) &&
           angular.isDefined(bounds.northEast) && angular.isNumber(bounds.southWest.lat) &&
           angular.isNumber(bounds.southWest.lng) && angular.isNumber(bounds.northEast.lat) &&
           angular.isNumber(bounds.northEast.lng);
  }

  return {
    createLeafletBounds: function(bounds) {
      if (_isValidBounds(bounds)) {
        return L.latLngBounds([bounds.southWest.lat, bounds.southWest.lng],
                              [bounds.northEast.lat, bounds.northEast.lng]);
      }
    },

    isValidBounds: _isValidBounds,

    createBoundsFromArray: function(boundsArray) {
      if (!(isArray(boundsArray) && boundsArray.length === 2 &&
            isArray(boundsArray[0]) && isArray(boundsArray[1]) &&
            boundsArray[0].length === 2 && boundsArray[1].length === 2 &&
            isNumber(boundsArray[0][0]) && isNumber(boundsArray[0][1]) &&
            isNumber(boundsArray[1][0]) && isNumber(boundsArray[1][1]))) {
        $log.error('[AngularJS - Leaflet] The bounds array is not valid.');
        return;
      }

      return {
        northEast: {
          lat: boundsArray[0][0],
          lng: boundsArray[0][1],
        },
        southWest: {
          lat: boundsArray[1][0],
          lng: boundsArray[1][1],
        },
      };
    },

    createBoundsFromLeaflet: function(lfBounds) {
      if (!(isDefined(lfBounds) && isFunction(lfBounds.getNorthEast) && isFunction(lfBounds.getSouthWest))) {
        $log.error('[AngularJS - Leaflet] The leaflet bounds is not valid object.');
        return;
      }

      var northEast = lfBounds.getNorthEast();
      var southWest = lfBounds.getSouthWest();

      return {
        northEast: {
          lat: northEast.lat,
          lng: northEast.lng,
        },
        southWest: {
          lat: southWest.lat,
          lng: southWest.lng,
        },
      };
    },
  };
}]);

angular.module('leaflet-directive').factory('leafletControlHelpers', ["$rootScope", "$log", "leafletHelpers", "leafletLayerHelpers", "leafletMapDefaults", function($rootScope, $log, leafletHelpers, leafletLayerHelpers, leafletMapDefaults) {
  var isDefined = leafletHelpers.isDefined;
  var isObject = leafletHelpers.isObject;
  var createLayer = leafletLayerHelpers.createLayer;
  var _controls = {};
  var errorHeader = leafletHelpers.errorHeader + ' [Controls] ';

  var _controlLayersMustBeVisible = function(baselayers, overlays, mapId) {
    var defaults = leafletMapDefaults.getDefaults(mapId);
    if (!defaults.controls.layers.visible) {
      return false;
    }

    var atLeastOneControlItemMustBeShown = false;

    if (isObject(baselayers)) {
      Object.keys(baselayers).forEach(function(key) {
        var layer = baselayers[key];
        if (!isDefined(layer.layerOptions) || layer.layerOptions.showOnSelector !== false) {
          atLeastOneControlItemMustBeShown = true;
        }
      });
    }

    if (isObject(overlays)) {
      Object.keys(overlays).forEach(function(key) {
        var layer = overlays[key];
        if (!isDefined(layer.layerParams) || layer.layerParams.showOnSelector !== false) {
          atLeastOneControlItemMustBeShown = true;
        }
      });
    }

    return atLeastOneControlItemMustBeShown;
  };

  var _createLayersControl = function(mapId) {
    var defaults = leafletMapDefaults.getDefaults(mapId);
    var controlOptions = {
      collapsed: defaults.controls.layers.collapsed,
      position: defaults.controls.layers.position,
      autoZIndex: false,
    };

    angular.extend(controlOptions, defaults.controls.layers.options);

    var control;
    if (defaults.controls.layers && isDefined(defaults.controls.layers.control)) {
      control = defaults.controls.layers.control.apply(this, [[], [], controlOptions]);
    } else {
      control = new L.control.layers([], [], controlOptions);
    }

    return control;
  };

  var controlTypes = {
    draw: {
      isPluginLoaded: function() {
        if (!angular.isDefined(L.Control.Draw)) {
          $log.error(errorHeader + ' Draw plugin is not loaded.');
          return false;
        }

        return true;
      },

      checkValidParams: function(/* params */) {
        return true;
      },

      createControl: function(params) {
        return new L.Control.Draw(params);
      },
    },
    scale: {
      isPluginLoaded: function() {
        return true;
      },

      checkValidParams: function(/* params */) {
        return true;
      },

      createControl: function(params) {
        return new L.control.scale(params);
      },
    },
    fullscreen: {
      isPluginLoaded: function() {
        if (!angular.isDefined(L.Control.Fullscreen)) {
          $log.error(errorHeader + ' Fullscreen plugin is not loaded.');
          return false;
        }

        return true;
      },

      checkValidParams: function(/* params */) {
        return true;
      },

      createControl: function(params) {
        return new L.Control.Fullscreen(params);
      },
    },
    search: {
      isPluginLoaded: function() {
        if (!angular.isDefined(L.Control.Search)) {
          $log.error(errorHeader + ' Search plugin is not loaded.');
          return false;
        }

        return true;
      },

      checkValidParams: function(/* params */) {
        return true;
      },

      createControl: function(params) {
        return new L.Control.Search(params);
      },
    },
    custom: {},
    minimap: {
      isPluginLoaded: function() {
        if (!angular.isDefined(L.Control.MiniMap)) {
          $log.error(errorHeader + ' Minimap plugin is not loaded.');
          return false;
        }

        return true;
      },

      checkValidParams: function(params) {
        if (!isDefined(params.layer)) {
          $log.warn(errorHeader + ' minimap "layer" option should be defined.');
          return false;
        }

        return true;
      },

      createControl: function(params) {
        var layer = createLayer(params.layer);

        if (!isDefined(layer)) {
          $log.warn(errorHeader + ' minimap control "layer" could not be created.');
          return;
        }

        return new L.Control.MiniMap(layer, params);
      },
    },
  };

  return {
    layersControlMustBeVisible: _controlLayersMustBeVisible,

    isValidControlType: function(type) {
      return Object.keys(controlTypes).indexOf(type) !== -1;
    },

    createControl: function(type, params) {
      if (!controlTypes[type].checkValidParams(params)) {
        return;
      }

      return controlTypes[type].createControl(params);
    },

    updateLayersControl: function(map, mapId, loaded, baselayers, overlays, leafletLayers) {
      var i;
      var _layersControl = _controls[mapId];
      var mustBeLoaded = _controlLayersMustBeVisible(baselayers, overlays, mapId);

      if (isDefined(_layersControl) && loaded) {
        for (i in leafletLayers.baselayers) {
          _layersControl.removeLayer(leafletLayers.baselayers[i]);
        }

        for (i in leafletLayers.overlays) {
          _layersControl.removeLayer(leafletLayers.overlays[i]);
        }

        map.removeControl(_layersControl);
        delete _controls[mapId];
      }

      if (mustBeLoaded) {
        _layersControl = _createLayersControl(mapId);
        _controls[mapId] = _layersControl;
        for (i in baselayers) {
          var hideOnSelector = isDefined(baselayers[i].layerOptions) &&
                               baselayers[i].layerOptions.showOnSelector === false;
          if (!hideOnSelector && isDefined(leafletLayers.baselayers[i])) {
            _layersControl.addBaseLayer(leafletLayers.baselayers[i], baselayers[i].name);
          }
        }

        for (i in overlays) {
          var hideOverlayOnSelector = isDefined(overlays[i].layerParams) &&
                      overlays[i].layerParams.showOnSelector === false;
          if (!hideOverlayOnSelector && isDefined(leafletLayers.overlays[i])) {
            _layersControl.addOverlay(leafletLayers.overlays[i], overlays[i].name);
          }
        }

        map.addControl(_layersControl);
      }

      return mustBeLoaded;
    },
  };
}]);

angular.module('leaflet-directive').service('leafletData', ["$log", "$q", "leafletHelpers", function($log, $q, leafletHelpers) {
  var getDefer = leafletHelpers.getDefer,
      getUnresolvedDefer = leafletHelpers.getUnresolvedDefer,
      setResolvedDefer = leafletHelpers.setResolvedDefer;

  var _private = {};
  var self = this;

  var upperFirst = function(string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
  };

  var _privateItems = [
      'map',
      'tiles',
      'layers',
      'paths',
      'markers',
      'geoJSON',
      'UTFGrid', //odd ball on naming convention keeping to not break
      'decorations',
      'directiveControls',];

  //init
  _privateItems.forEach(function(itemName) {
    _private[itemName] = {};
  });

  this.unresolveMap = function(scopeId) {
    var id = leafletHelpers.obtainEffectiveMapId(_private.map, scopeId);
    _privateItems.forEach(function(itemName) {
      _private[itemName][id] = undefined;
    });
  };

  //int repetitive stuff (get and sets)
  _privateItems.forEach(function(itemName) {
    var name = upperFirst(itemName);
    self['set' + name] = function(lObject, scopeId) {
      var defer = getUnresolvedDefer(_private[itemName], scopeId);
      defer.resolve(lObject);
      setResolvedDefer(_private[itemName], scopeId);
    };

    self['get' + name] = function(scopeId) {
      var defer = getDefer(_private[itemName], scopeId);
      return defer.promise;
    };
  });
}]);

angular.module('leaflet-directive')
.service('leafletDirectiveControlsHelpers', ["$log", "leafletData", "leafletHelpers", function($log, leafletData, leafletHelpers) {
  var _isDefined = leafletHelpers.isDefined;
  var _isString = leafletHelpers.isString;
  var _isObject = leafletHelpers.isObject;
  var _mainErrorHeader = leafletHelpers.errorHeader;

  var _errorHeader = _mainErrorHeader + '[leafletDirectiveControlsHelpers';

  var _extend = function(id, thingToAddName, createFn, cleanFn) {
    var _fnHeader = _errorHeader + '.extend] ';
    var extender = {};
    if (!_isDefined(thingToAddName)) {
      $log.error(_fnHeader + 'thingToAddName cannot be undefined');
      return;
    }

    if (_isString(thingToAddName) && _isDefined(createFn) && _isDefined(cleanFn)) {
      extender[thingToAddName] = {
        create: createFn,
        clean: cleanFn,
      };
    }    else if (_isObject(thingToAddName) && !_isDefined(createFn) && !_isDefined(cleanFn)) {
      extender = thingToAddName;
    }    else {
      $log.error(_fnHeader + 'incorrect arguments');
      return;
    }

    //add external control to create / destroy markers without a watch
    leafletData.getDirectiveControls().then(function(controls) {
      angular.extend(controls, extender);
      leafletData.setDirectiveControls(controls, id);
    });
  };

  return {
    extend: _extend,
  };
}]);

angular.module('leaflet-directive')
.service('leafletGeoJsonHelpers', ["leafletHelpers", "leafletIterators", function(leafletHelpers, leafletIterators) {
  var lHlp = leafletHelpers;
  var lIt = leafletIterators;
  var Point = function(lat, lng) {
    this.lat = lat;
    this.lng = lng;
    return this;
  };

  var _getLat = function(value) {
    if (Array.isArray(value) && value.length === 2) {
      return value[1];
    } else if (lHlp.isDefined(value.type) && value.type === 'Point') {
      return +value.coordinates[1];
    } else {
      return +value.lat;
    }
  };

  var _getLng = function(value) {
    if (Array.isArray(value) && value.length === 2) {
      return value[0];
    } else if (lHlp.isDefined(value.type) && value.type === 'Point') {
      return +value.coordinates[0];
    } else {
      return +value.lng;
    }
  };

  var _validateCoords = function(coords) {
    if (lHlp.isUndefined(coords)) {
      return false;
    }

    if (lHlp.isArray(coords)) {
      if (coords.length === 2 && lHlp.isNumber(coords[0]) && lHlp.isNumber(coords[1])) {
        return true;
      }
    } else if (lHlp.isDefined(coords.type)) {
      if (
          coords.type === 'Point' && lHlp.isArray(coords.coordinates) &&
          coords.coordinates.length === 2  &&
          lHlp.isNumber(coords.coordinates[0]) &&
          lHlp.isNumber(coords.coordinates[1])) {
        return true;
      }
    }

    var ret = lIt.all(['lat', 'lng'], function(pos) {
      return lHlp.isDefined(coords[pos]) && lHlp.isNumber(coords[pos]);
    });

    return ret;
  };

  var _getCoords = function(value) {
    if (!value || !_validateCoords(value)) {
      return;
    }

    var p =  null;
    if (Array.isArray(value) && value.length === 2) {
      p = new Point(value[1], value[0]);
    } else if (lHlp.isDefined(value.type) && value.type === 'Point') {
      p = new Point(value.coordinates[1], value.coordinates[0]);
    } else {
      return value;
    }

    //note angular.merge is avail in angular 1.4.X we might want to fill it here
    return angular.extend(value, p);//tap on lat, lng if it doesnt exist
  };

  return {
    getLat: _getLat,
    getLng: _getLng,
    validateCoords: _validateCoords,
    getCoords: _getCoords,
  };
}]);

angular.module('leaflet-directive').service('leafletHelpers', ["$q", "$log", function($q, $log) {
  var _errorHeader = '[AngularJS - Leaflet] ';
  var _copy = angular.copy;
  var _clone = _copy;
  /*
  For parsing paths to a field in an object

  Example:
  var obj = {
      bike:{
       1: 'hi'
       2: 'foo'
      }
  };
  _getObjectValue(obj,"bike.1") returns 'hi'
  this is getPath in ui-gmap
   */
  var _getObjectValue = function(object, pathStr) {
    var obj;
    if (!object || !angular.isObject(object))
        return;

    //if the key is not a sting then we already have the value
    if ((pathStr === null) || !angular.isString(pathStr)) {
      return pathStr;
    }

    obj = object;
    pathStr.split('.').forEach(function(value) {
      if (obj) {
        obj = obj[value];
      }
    });

    return obj;
  };

  /*
   Object Array Notation
   _getObjectArrayPath("bike.one.two")
   returns:
   'bike["one"]["two"]'
   */
  var _getObjectArrayPath = function(pathStr) {
    return pathStr.split('.').reduce(function(previous, current) {
      return previous + '["' + current + '"]';
    });
  };

  /* Object Dot Notation
   _getObjectPath(["bike","one","two"])
   returns:
   "bike.one.two"
   */
  var _getObjectDotPath = function(arrayOfStrings) {
    return arrayOfStrings.reduce(function(previous, current) {
      return previous + '.' + current;
    });
  };

  function _obtainEffectiveMapId(d, mapId) {
    var id;
    var i;
    if (!angular.isDefined(mapId)) {
      if (Object.keys(d).length === 0) {
        id = 'main';
      } else if (Object.keys(d).length >= 1) {
        for (i in d) {
          if (d.hasOwnProperty(i)) {
            id = i;
          }
        }
      } else {
        $log.error(_errorHeader + '- You have more than 1 map on the DOM, you must provide the map ID to the leafletData.getXXX call');
      }
    } else {
      id = mapId;
    }

    return id;
  }

  function _getUnresolvedDefer(d, mapId) {
    var id = _obtainEffectiveMapId(d, mapId);
    var defer;

    if (!angular.isDefined(d[id]) || d[id].resolvedDefer === true) {
      defer = $q.defer();
      d[id] = {
        defer: defer,
        resolvedDefer: false,
      };
    } else {
      defer = d[id].defer;
    }

    return defer;
  }

  var _isDefined = function(value) {
    return angular.isDefined(value) && value !== null;
  };

  var _isUndefined = function(value) {
    return !_isDefined(value);
  };

  // BEGIN DIRECT PORT FROM AngularJS code base

  var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;

  var MOZ_HACK_REGEXP = /^moz([A-Z])/;

  var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;

  /**
  Converts snake_case to camelCase.
  Also there is special case for Moz prefix starting with upper case letter.
  @param name Name to normalize
   */

  var camelCase = function(name) {
      return name.replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
        if (offset) {
          return letter.toUpperCase();
        } else {
          return letter;
        }
      }).replace(MOZ_HACK_REGEXP, 'Moz$1');
    };

  /**
  Converts all accepted directives format into proper directive name.
  @param name Name to normalize
   */

  var directiveNormalize = function(name) {
      return camelCase(name.replace(PREFIX_REGEXP, ''));
    };

  // END AngularJS port

  return {
    camelCase: camelCase,
    directiveNormalize: directiveNormalize,
    copy:_copy,
    clone:_clone,
    errorHeader: _errorHeader,
    getObjectValue: _getObjectValue,
    getObjectArrayPath:_getObjectArrayPath,
    getObjectDotPath: _getObjectDotPath,
    defaultTo: function(val, _default) {
      return _isDefined(val) ? val : _default;
    },

    //mainly for checking attributes of directives lets keep this minimal (on what we accept)
    isTruthy: function(val) {
      return val === 'true' || val === true;
    },

    //Determine if a reference is {}
    isEmpty: function(value) {
      return Object.keys(value).length === 0;
    },

    //Determine if a reference is undefined or {}
    isUndefinedOrEmpty: function(value) {
      return (angular.isUndefined(value) || value === null) || Object.keys(value).length === 0;
    },

    // Determine if a reference is defined
    isDefined: _isDefined,
    isUndefined:_isUndefined,
    isNumber: angular.isNumber,
    isString: angular.isString,
    isArray: angular.isArray,
    isObject: angular.isObject,
    isFunction: angular.isFunction,
    equals: angular.equals,

    isValidCenter: function(center) {
      return angular.isDefined(center) && angular.isNumber(center.lat) &&
             angular.isNumber(center.lng) && angular.isNumber(center.zoom);
    },

    isValidPoint: function(point) {
      if (!angular.isDefined(point)) {
        return false;
      }

      if (angular.isArray(point)) {
        return point.length === 2 && angular.isNumber(point[0]) && angular.isNumber(point[1]);
      }

      return angular.isNumber(point.lat) && angular.isNumber(point.lng);
    },

    isSameCenterOnMap: function(centerModel, map) {
      var mapCenter = map.getCenter();
      var zoom = map.getZoom();
      if (centerModel.lat && centerModel.lng &&
          mapCenter.lat.toFixed(4) === centerModel.lat.toFixed(4) &&
          mapCenter.lng.toFixed(4) === centerModel.lng.toFixed(4) &&
          zoom === centerModel.zoom) {
        return true;
      }

      return false;
    },

    safeApply: function($scope, fn) {
      var phase = $scope.$root.$$phase;
      if (phase === '$apply' || phase === '$digest') {
        $scope.$eval(fn);
      } else {
        $scope.$evalAsync(fn);
      }
    },

    obtainEffectiveMapId: _obtainEffectiveMapId,

    getDefer: function(d, mapId) {
      var id = _obtainEffectiveMapId(d, mapId);
      var defer;
      if (!angular.isDefined(d[id]) || d[id].resolvedDefer === false) {
        defer = _getUnresolvedDefer(d, mapId);
      } else {
        defer = d[id].defer;
      }

      return defer;
    },

    getUnresolvedDefer: _getUnresolvedDefer,

    setResolvedDefer: function(d, mapId) {
      var id = _obtainEffectiveMapId(d, mapId);
      d[id].resolvedDefer = true;
    },

    rangeIsSupported: function() {
      var testrange = document.createElement('input');
      testrange.setAttribute('type', 'range');
      return testrange.type === 'range';
    },

    FullScreenControlPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.Control.Fullscreen);
      },
    },

    MiniMapControlPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.Control.MiniMap);
      },
    },

    AwesomeMarkersPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.AwesomeMarkers) && angular.isDefined(L.AwesomeMarkers.Icon);
      },

      is: function(icon) {
        if (this.isLoaded()) {
          return icon instanceof L.AwesomeMarkers.Icon;
        } else {
          return false;
        }
      },

      equal: function(iconA, iconB) {
        if (!this.isLoaded()) {
          return false;
        }

        if (this.is(iconA)) {
          return angular.equals(iconA, iconB);
        } else {
          return false;
        }
      },
    },

    VectorMarkersPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.VectorMarkers) && angular.isDefined(L.VectorMarkers.Icon);
      },

      is: function(icon) {
        if (this.isLoaded()) {
          return icon instanceof L.VectorMarkers.Icon;
        } else {
          return false;
        }
      },

      equal: function(iconA, iconB) {
        if (!this.isLoaded()) {
          return false;
        }

        if (this.is(iconA)) {
          return angular.equals(iconA, iconB);
        } else {
          return false;
        }
      },
    },

    DomMarkersPlugin: {
      isLoaded: function() {
        if (angular.isDefined(L.DomMarkers) && angular.isDefined(L.DomMarkers.Icon)) {
          return true;
        } else {
          return false;
        }
      },

      is: function(icon) {
        if (this.isLoaded()) {
          return icon instanceof L.DomMarkers.Icon;
        } else {
          return false;
        }
      },

      equal: function(iconA, iconB) {
        if (!this.isLoaded()) {
          return false;
        }

        if (this.is(iconA)) {
          return angular.equals(iconA, iconB);
        } else {
          return false;
        }
      },
    },

    PolylineDecoratorPlugin: {
      isLoaded: function() {
        if (angular.isDefined(L.PolylineDecorator)) {
          return true;
        } else {
          return false;
        }
      },

      is: function(decoration) {
        if (this.isLoaded()) {
          return decoration instanceof L.PolylineDecorator;
        } else {
          return false;
        }
      },

      equal: function(decorationA, decorationB) {
        if (!this.isLoaded()) {
          return false;
        }

        if (this.is(decorationA)) {
          return angular.equals(decorationA, decorationB);
        } else {
          return false;
        }
      },
    },

    MakiMarkersPlugin: {
      isLoaded: function() {
        if (angular.isDefined(L.MakiMarkers) && angular.isDefined(L.MakiMarkers.Icon)) {
          return true;
        } else {
          return false;
        }
      },

      is: function(icon) {
        if (this.isLoaded()) {
          return icon instanceof L.MakiMarkers.Icon;
        } else {
          return false;
        }
      },

      equal: function(iconA, iconB) {
        if (!this.isLoaded()) {
          return false;
        }

        if (this.is(iconA)) {
          return angular.equals(iconA, iconB);
        } else {
          return false;
        }
      },
    },
    ExtraMarkersPlugin: {
      isLoaded: function() {
        if (angular.isDefined(L.ExtraMarkers) && angular.isDefined(L.ExtraMarkers.Icon)) {
          return true;
        } else {
          return false;
        }
      },

      is: function(icon) {
        if (this.isLoaded()) {
          return icon instanceof L.ExtraMarkers.Icon;
        } else {
          return false;
        }
      },

      equal: function(iconA, iconB) {
        if (!this.isLoaded()) {
          return false;
        }

        if (this.is(iconA)) {
          return angular.equals(iconA, iconB);
        } else {
          return false;
        }
      },
    },
    LabelPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.Label);
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.MarkerClusterGroup;
        } else {
          return false;
        }
      },
    },
    MarkerClusterPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.MarkerClusterGroup);
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.MarkerClusterGroup;
        } else {
          return false;
        }
      },
    },
    GoogleLayerPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.Google);
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.Google;
        } else {
          return false;
        }
      },
    },
    LeafletProviderPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.TileLayer.Provider);
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.TileLayer.Provider;
        } else {
          return false;
        }
      },
    },
    ChinaLayerPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.tileLayer.chinaProvider);
      },
    },
    HeatLayerPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.heatLayer);
      },
    },
    WebGLHeatMapLayerPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.TileLayer.WebGLHeatMap);
      },
    },
    BingLayerPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.BingLayer);
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.BingLayer;
        } else {
          return false;
        }
      },
    },
    WFSLayerPlugin: {
      isLoaded: function() {
        return L.GeoJSON.WFS !== undefined;
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.GeoJSON.WFS;
        } else {
          return false;
        }
      },
    },
    AGSBaseLayerPlugin: {
      isLoaded: function() {
        return L.esri !== undefined && L.esri.basemapLayer !== undefined;
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.esri.basemapLayer;
        } else {
          return false;
        }
      },
    },
    AGSLayerPlugin: {
      isLoaded: function() {
        return lvector !== undefined && lvector.AGS !== undefined;
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof lvector.AGS;
        } else {
          return false;
        }
      },
    },
    AGSFeatureLayerPlugin: {
      isLoaded: function() {
        return L.esri !== undefined && L.esri.featureLayer !== undefined;
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.esri.featureLayer;
        } else {
          return false;
        }
      },
    },
    AGSTiledMapLayerPlugin: {
      isLoaded: function() {
        return L.esri !== undefined && L.esri.tiledMapLayer !== undefined;
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.esri.tiledMapLayer;
        } else {
          return false;
        }
      },
    },
    AGSDynamicMapLayerPlugin: {
      isLoaded: function() {
        return L.esri !== undefined && L.esri.dynamicMapLayer !== undefined;
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.esri.dynamicMapLayer;
        } else {
          return false;
        }
      },
    },
    AGSImageMapLayerPlugin: {
      isLoaded: function() {
        return L.esri !== undefined && L.esri.imageMapLayer !== undefined;
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.esri.imageMapLayer;
        } else {
          return false;
        }
      },
    },
    AGSClusteredLayerPlugin: {
      isLoaded: function() {
        return L.esri !== undefined && L.esri.clusteredFeatureLayer !== undefined;
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.esri.clusteredFeatureLayer;
        } else {
          return false;
        }
      },
    },
    AGSHeatmapLayerPlugin: {
      isLoaded: function() {
        return L.esri !== undefined && L.esri.heatmapFeatureLayer !== undefined;
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.esri.heatmapFeatureLayer;
        } else {
          return false;
        }
      },
    },
    YandexLayerPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.Yandex);
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.Yandex;
        } else {
          return false;
        }
      },
    },
    GeoJSONPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.TileLayer.GeoJSON);
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.TileLayer.GeoJSON;
        } else {
          return false;
        }
      },
    },
    UTFGridPlugin: {
      isLoaded: function() {
        return angular.isDefined(L.UtfGrid);
      },

      is: function(layer) {
        if (this.isLoaded()) {
          return layer instanceof L.UtfGrid;
        } else {
          $log.error('[AngularJS - Leaflet] No UtfGrid plugin found.');
          return false;
        }
      },
    },
    CartoDB: {
      isLoaded: function() {
        return cartodb;
      },

      is: function(/*layer*/) {
        return true;
        /*
        if (this.isLoaded()) {
            return layer instanceof L.TileLayer.GeoJSON;
        } else {
            return false;
        }*/
      },
    },
    Leaflet: {
      DivIcon: {
        is: function(icon) {
          return icon instanceof L.DivIcon;
        },

        equal: function(iconA, iconB) {
          if (this.is(iconA)) {
            return angular.equals(iconA, iconB);
          } else {
            return false;
          }
        },
      },
      Icon: {
        is: function(icon) {
          return icon instanceof L.Icon;
        },

        equal: function(iconA, iconB) {
          if (this.is(iconA)) {
            return angular.equals(iconA, iconB);
          } else {
            return false;
          }
        },
      },
    },
    /*
     watchOptions - object to set deep nested watches and turn off watches all together
     (rely on control / functional updates)
     watchOptions - Object
         doWatch:boolean
         isDeep:boolean (sets $watch(function,isDeep))
         individual
             doWatch:boolean
             isDeep:boolean
     */

    //legacy defaults
    watchOptions: {
      doWatch:true,
      isDeep: true,
      individual:{
        doWatch:true,
        isDeep: true,
      },
    },
  };
}]);

angular.module('leaflet-directive').service('leafletIterators', ["$log", "leafletHelpers", function($log, leafletHelpers) {

  var lHlp = leafletHelpers;
  var errorHeader = leafletHelpers.errorHeader + 'leafletIterators: ';

  //BEGIN COPY from underscore
  var _keys = Object.keys;
  var _isFunction = lHlp.isFunction;
  var _isObject = lHlp.isObject;

  // Helper for collection methods to determine whether a collection
  // should be iterated as an array or as an object
  // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
  var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

  var _isArrayLike = function(collection) {
    var length = collection !== null && collection.length;
    return lHlp.isNumber(length) && length >= 0 && length <= MAX_ARRAY_INDEX;
  };

  // Keep the identity function around for default iteratees.
  var _identity = function(value) {
    return value;
  };

  var _property = function(key) {
    return function(obj) {
      return obj === null ? void 0 : obj[key];
    };
  };

  // Internal function that returns an efficient (for current engines) version
  // of the passed-in callback, to be repeatedly applied in other Underscore
  // functions.
  var optimizeCb = function(func, context, argCount) {
    if (context === void 0) return func;
    switch (argCount === null ? 3 : argCount) {
      case 1: return function(value) {
        return func.call(context, value);
      };

      case 2: return function(value, other) {
        return func.call(context, value, other);
      };

      case 3: return function(value, index, collection) {
        return func.call(context, value, index, collection);
      };

      case 4: return function(accumulator, value, index, collection) {
        return func.call(context, accumulator, value, index, collection);
      };
    }
    return function() {
      return func.apply(context, arguments);
    };
  };

  // An internal function for creating assigner functions.
  var createAssigner = function(keysFunc, undefinedOnly) {
    return function(obj) {
      var length = arguments.length;
      if (length < 2 || obj === null) return obj;
      for (var index = 1; index < length; index++) {
        var source = arguments[index];
        var keys = keysFunc(source);
        var l = keys.length;

        for (var i = 0; i < l; i++) {
          var key = keys[i];
          if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
        }
      }

      return obj;
    };
  };

  // Assigns a given object with all the own properties in the passed-in object(s)
  // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
  var _extendOwn;
  var _assign = null;
  _extendOwn = _assign = createAssigner(_keys);

  // Returns whether an object has a given set of `key:value` pairs.
  var _isMatch = function(object, attrs) {
    var keys = _keys(attrs);
    var length = keys.length;
    if (object === null) return !length;
    var obj = Object(object);
    for (var i = 0; i < length; i++) {
      var key = keys[i];
      if (attrs[key] !== obj[key] || !(key in obj)) return false;
    }

    return true;
  };

  // Returns a predicate for checking whether an object has a given set of
  // `key:value` pairs.
  var _matcher;
  var _matches = null;
  _matcher = _matches = function(attrs) {
    attrs = _extendOwn({}, attrs);
    return function(obj) {
      return _isMatch(obj, attrs);
    };
  };

  // A mostly-internal function to generate callbacks that can be applied
  // to each element in a collection, returning the desired result — either
  // identity, an arbitrary callback, a property matcher, or a property accessor.
  var cb = function(value, context, argCount) {
    if (value === null) return _identity;
    if (_isFunction(value)) return optimizeCb(value, context, argCount);
    if (_isObject(value)) return _matcher(value);
    return _property(value);
  };

  var _every;
  var _all = null;
  _every = _all = function(obj, predicate, context) {
    predicate = cb(predicate, context);
    var keys = !_isArrayLike(obj) && _keys(obj);
    var length = (keys || obj).length;
    for (var index = 0; index < length; index++) {
      var currentKey = keys ? keys[index] : index;
      if (!predicate(obj[currentKey], currentKey, obj)) return false;
    }

    return true;
  };

  //END COPY fron underscore

  var _hasErrors = function(collection, cb, ignoreCollection, cbName) {
    if (!ignoreCollection) {
      if (!lHlp.isDefined(collection) || !lHlp.isDefined(cb)) {
        return true;
      }
    }

    if (!lHlp.isFunction(cb)) {
      cbName = lHlp.defaultTo(cb, 'cb');
      $log.error(errorHeader + cbName + ' is not a function');
      return true;
    }

    return false;
  };

  var _iterate = function(collection, externalCb, internalCb) {
    if (_hasErrors(undefined, internalCb, true, 'internalCb')) {
      return;
    }

    if (!_hasErrors(collection, externalCb)) {
      for (var key in collection) {
        if (collection.hasOwnProperty(key)) {
          internalCb(collection[key], key);
        }
      }
    }
  };

  //see http://jsperf.com/iterators/3
  //utilizing for in is way faster
  var _each = function(collection, cb) {
    _iterate(collection, cb, function(val, key) {
      cb(val, key);
    });
  };

  return {
    each:_each,
    forEach: _each,
    every: _every,
    all: _all,
  };
}]);

angular.module('leaflet-directive')
.factory('leafletLayerHelpers', ["$rootScope", "$log", "$q", "leafletHelpers", "leafletIterators", function($rootScope, $log, $q, leafletHelpers, leafletIterators) {
  var Helpers = leafletHelpers;
  var isString = leafletHelpers.isString;
  var isObject = leafletHelpers.isObject;
  var isArray = leafletHelpers.isArray;
  var isDefined = leafletHelpers.isDefined;
  var errorHeader = leafletHelpers.errorHeader;
  var $it = leafletIterators;

  var utfGridCreateLayer = function(params) {
    if (!Helpers.UTFGridPlugin.isLoaded()) {
      $log.error('[AngularJS - Leaflet] The UTFGrid plugin is not loaded.');
      return;
    }

    var utfgrid = new L.UtfGrid(params.url, params.pluginOptions);

    utfgrid.on('mouseover', function(e) {
      $rootScope.$broadcast('leafletDirectiveMap.utfgridMouseover', e);
    });

    utfgrid.on('mouseout', function(e) {
      $rootScope.$broadcast('leafletDirectiveMap.utfgridMouseout', e);
    });

    utfgrid.on('click', function(e) {
      $rootScope.$broadcast('leafletDirectiveMap.utfgridClick', e);
    });

    utfgrid.on('mousemove', function(e) {
      $rootScope.$broadcast('leafletDirectiveMap.utfgridMousemove', e);
    });

    return utfgrid;
  };

  var layerTypes = {
    xyz: {
      mustHaveUrl: true,
      createLayer: function(params) {
        return L.tileLayer(params.url, params.options);
      },
    },
    mapbox: {
      mustHaveKey: true,
      createLayer: function(params) {
        var version = 3;
        if (isDefined(params.options.version) && params.options.version === 4) {
          version = params.options.version;
        }

        var url = version === 3 ?
            '//{s}.tiles.mapbox.com/v3/' + params.key + '/{z}/{x}/{y}.png' :
            '//api.tiles.mapbox.com/v4/' + params.key + '/{z}/{x}/{y}.png?access_token=' + params.apiKey;
        return L.tileLayer(url, params.options);
      },
    },
    geoJSON: {
      mustHaveUrl: true,
      createLayer: function(params) {
        if (!Helpers.GeoJSONPlugin.isLoaded()) {
          return;
        }

        return new L.TileLayer.GeoJSON(params.url, params.pluginOptions, params.options);
      },
    },
    geoJSONShape: {
      mustHaveUrl: false,
      createLayer: function(params) {
        return new L.GeoJSON(params.data,
            params.options);
      },
    },
    geoJSONAwesomeMarker: {
      mustHaveUrl: false,
      createLayer: function(params) {
        return new L.geoJson(params.data, {
          pointToLayer: function(feature, latlng) {
            return L.marker(latlng, {icon: L.AwesomeMarkers.icon(params.icon)});
          },
        });
      },
    },
    geoJSONVectorMarker: {
      mustHaveUrl: false,
      createLayer: function(params) {
        return new L.geoJson(params.data, {
          pointToLayer: function(feature, latlng) {
            return L.marker(latlng, {icon: L.VectorMarkers.icon(params.icon)});
          },
        });
      },
    },
    utfGrid: {
      mustHaveUrl: true,
      createLayer: utfGridCreateLayer,
    },
    cartodbTiles: {
      mustHaveKey: true,
      createLayer: function(params) {
        var url = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/{z}/{x}/{y}.png';
        return L.tileLayer(url, params.options);
      },
    },
    cartodbUTFGrid: {
      mustHaveKey: true,
      mustHaveLayer: true,
      createLayer: function(params) {
        params.url = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/' + params.layer + '/{z}/{x}/{y}.grid.json';
        return utfGridCreateLayer(params);
      },
    },
    cartodbInteractive: {
      mustHaveKey: true,
      mustHaveLayer: true,
      createLayer: function(params) {
        var tilesURL = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/{z}/{x}/{y}.png';
        var tileLayer = L.tileLayer(tilesURL, params.options);
        params.url = '//' + params.user + '.cartodb.com/api/v1/map/' + params.key + '/' + params.layer + '/{z}/{x}/{y}.grid.json';
        var utfLayer = utfGridCreateLayer(params);
        return L.layerGroup([tileLayer, utfLayer]);
      },
    },
    wms: {
      mustHaveUrl: true,
      createLayer: function(params) {
        return L.tileLayer.wms(params.url, params.options);
      },
    },
    wmts: {
      mustHaveUrl: true,
      createLayer: function(params) {
        return L.tileLayer.wmts(params.url, params.options);
      },
    },
    wfs: {
      mustHaveUrl: true,
      mustHaveLayer: true,
      createLayer: function(params) {
        if (!Helpers.WFSLayerPlugin.isLoaded()) {
          return;
        }

        var options = angular.copy(params.options);
        if (options.crs && typeof options.crs === 'string') {
          /*jshint -W061 */
          options.crs = eval(options.crs);
        }

        return new L.GeoJSON.WFS(params.url, params.layer, options);
      },
    },
    group: {
      mustHaveUrl: false,
      createLayer: function(params) {
        var lyrs = [];
        $it.each(params.options.layers, function(l) {
                  lyrs.push(createLayer(l));
                });

        params.options.loadedDefer = function() {
          var defers = [];
          if (isDefined(params.options.layers)) {
            for (var i = 0; i < params.options.layers.length; i++) {
              var d = params.options.layers[i].layerOptions.loadedDefer;
              if (isDefined(d)) {
                defers.push(d);
              }
            }
          }

          return defers;
        };

        return L.layerGroup(lyrs);
      },
    },
    featureGroup: {
      mustHaveUrl: false,
      createLayer: function() {
        return L.featureGroup();
      },
    },
    google: {
      mustHaveUrl: false,
      createLayer: function(params) {
        var type = params.type || 'SATELLITE';
        if (!Helpers.GoogleLayerPlugin.isLoaded()) {
          return;
        }

        return new L.Google(type, params.options);
      },
    },
    here: {
      mustHaveUrl: false,
      createLayer: function(params) {
        var provider = params.provider || 'HERE.terrainDay';
        if (!Helpers.LeafletProviderPlugin.isLoaded()) {
          return;
        }

        return new L.TileLayer.Provider(provider, params.options);
      },
    },
    china:{
      mustHaveUrl:false,
      createLayer:function(params) {
        var type = params.type || '';
        if (!Helpers.ChinaLayerPlugin.isLoaded()) {
          return;
        }

        return L.tileLayer.chinaProvider(type, params.options);
      },
    },
    agsBase: {
      mustHaveLayer: true,
      createLayer: function(params) {
        if (!Helpers.AGSBaseLayerPlugin.isLoaded()) {
          return;
        }

        return L.esri.basemapLayer(params.layer, params.options);
      },
    },
    ags: {
      mustHaveUrl: true,
      createLayer: function(params) {
        if (!Helpers.AGSLayerPlugin.isLoaded()) {
          return;
        }

        var options = angular.copy(params.options);
        angular.extend(options, {
          url: params.url,
        });
        var layer = new lvector.AGS(options);
        layer.onAdd = function(map) {
          this.setMap(map);
        };

        layer.onRemove = function() {
          this.setMap(null);
        };

        return layer;
      },
    },
    agsFeature: {
      mustHaveUrl: true,
      createLayer: function(params) {
        if (!Helpers.AGSFeatureLayerPlugin.isLoaded()) {
          $log.warn(errorHeader + ' The esri plugin is not loaded.');
          return;
        }

        params.options.url = params.url;

        var layer = L.esri.featureLayer(params.options);
        var load = function() {
          if (isDefined(params.options.loadedDefer)) {
            params.options.loadedDefer.resolve();
          }
        };

        layer.on('loading', function() {
          params.options.loadedDefer = $q.defer();
          layer.off('load', load);
          layer.on('load', load);
        });

        return layer;
      },
    },
    agsTiled: {
      mustHaveUrl: true,
      createLayer: function(params) {
        if (!Helpers.AGSTiledMapLayerPlugin.isLoaded()) {
          $log.warn(errorHeader + ' The esri plugin is not loaded.');
          return;
        }

        params.options.url = params.url;

        return L.esri.tiledMapLayer(params.options);
      },
    },
    agsDynamic: {
      mustHaveUrl: true,
      createLayer: function(params) {
        if (!Helpers.AGSDynamicMapLayerPlugin.isLoaded()) {
          $log.warn(errorHeader + ' The esri plugin is not loaded.');
          return;
        }

        params.options.url = params.url;

        return L.esri.dynamicMapLayer(params.options);
      },
    },
    agsImage: {
      mustHaveUrl: true,
      createLayer: function(params) {
        if (!Helpers.AGSImageMapLayerPlugin.isLoaded()) {
          $log.warn(errorHeader + ' The esri plugin is not loaded.');
          return;
        }

        params.options.url = params.url;

        return L.esri.imageMapLayer(params.options);
      },
    },
    agsClustered: {
      mustHaveUrl: true,
      createLayer: function(params) {
        if (!Helpers.AGSClusteredLayerPlugin.isLoaded()) {
          $log.warn(errorHeader + ' The esri clustered layer plugin is not loaded.');
          return;
        }

        if (!Helpers.MarkerClusterPlugin.isLoaded()) {
          $log.warn(errorHeader + ' The markercluster plugin is not loaded.');
          return;
        }

        return L.esri.clusteredFeatureLayer(params.url, params.options);
      },
    },
    agsHeatmap: {
      mustHaveUrl: true,
      createLayer: function(params) {
        if (!Helpers.AGSHeatmapLayerPlugin.isLoaded()) {
          $log.warn(errorHeader + ' The esri heatmap layer plugin is not loaded.');
          return;
        }

        if (!Helpers.HeatLayerPlugin.isLoaded()) {
          $log.warn(errorHeader + ' The heatlayer plugin is not loaded.');
          return;
        }

        return L.esri.heatmapFeatureLayer(params.url, params.options);
      },
    },
    markercluster: {
      mustHaveUrl: false,
      createLayer: function(params) {
        if (!Helpers.MarkerClusterPlugin.isLoaded()) {
          $log.warn(errorHeader + ' The markercluster plugin is not loaded.');
          return;
        }

        return new L.MarkerClusterGroup(params.options);
      },
    },
    bing: {
      mustHaveUrl: false,
      createLayer: function(params) {
        if (!Helpers.BingLayerPlugin.isLoaded()) {
          return;
        }

        return new L.BingLayer(params.key, params.options);
      },
    },
    webGLHeatmap: {
      mustHaveUrl: false,
      mustHaveData: true,
      createLayer: function(params) {
        if (!Helpers.WebGLHeatMapLayerPlugin.isLoaded()) {
          return;
        }

        var layer = new L.TileLayer.WebGLHeatMap(params.options);
        if (isDefined(params.data)) {
          layer.setData(params.data);
        }

        return layer;
      },
    },
    heat: {
      mustHaveUrl: false,
      mustHaveData: true,
      createLayer: function(params) {
        if (!Helpers.HeatLayerPlugin.isLoaded()) {
          return;
        }

        var layer = new L.heatLayer();

        if (isArray(params.data)) {
          layer.setLatLngs(params.data);
        }

        if (isObject(params.options)) {
          layer.setOptions(params.options);
        }

        return layer;
      },
    },
    yandex: {
      mustHaveUrl: false,
      createLayer: function(params) {
        var type = params.type || 'map';
        if (!Helpers.YandexLayerPlugin.isLoaded()) {
          return;
        }

        return new L.Yandex(type, params.options);
      },
    },
    imageOverlay: {
      mustHaveUrl: true,
      mustHaveBounds: true,
      createLayer: function(params) {
        return L.imageOverlay(params.url, params.bounds, params.options);
      },
    },
    iip: {
      mustHaveUrl: true,
      createLayer: function(params) {
        return L.tileLayer.iip(params.url, params.options);
      },
    },

    // This "custom" type is used to accept every layer that user want to define himself.
    // We can wrap these custom layers like heatmap or yandex, but it means a lot of work/code to wrap the world,
    // so we let user to define their own layer outside the directive,
    // and pass it on "createLayer" result for next processes
    custom: {
      createLayer: function(params) {
        if (params.layer instanceof L.Class) {
          return angular.copy(params.layer);
        }        else {
          $log.error('[AngularJS - Leaflet] A custom layer must be a leaflet Class');
        }
      },
    },
    cartodb: {
      mustHaveUrl: true,
      createLayer: function(params) {
        return cartodb.createLayer(params.map, params.url);
      },
    },
  };

  function isValidLayerType(layerDefinition) {
    // Check if the baselayer has a valid type
    if (!isString(layerDefinition.type)) {
      $log.error('[AngularJS - Leaflet] A layer must have a valid type defined.');
      return false;
    }

    if (Object.keys(layerTypes).indexOf(layerDefinition.type) === -1) {
      $log.error('[AngularJS - Leaflet] A layer must have a valid type: ' + Object.keys(layerTypes));
      return false;
    }

    // Check if the layer must have an URL
    if (layerTypes[layerDefinition.type].mustHaveUrl && !isString(layerDefinition.url)) {
      $log.error('[AngularJS - Leaflet] A base layer must have an url');
      return false;
    }

    if (layerTypes[layerDefinition.type].mustHaveData && !isDefined(layerDefinition.data)) {
      $log.error('[AngularJS - Leaflet] The base layer must have a "data" array attribute');
      return false;
    }

    if (layerTypes[layerDefinition.type].mustHaveLayer && !isDefined(layerDefinition.layer)) {
      $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have an layer defined');
      return false;
    }

    if (layerTypes[layerDefinition.type].mustHaveBounds && !isDefined(layerDefinition.bounds)) {
      $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have bounds defined');
      return false;
    }

    if (layerTypes[layerDefinition.type].mustHaveKey && !isDefined(layerDefinition.key)) {
      $log.error('[AngularJS - Leaflet] The type of layer ' + layerDefinition.type + ' must have key defined');
      return false;
    }

    return true;
  }

  function createLayer(layerDefinition) {
    if (!isValidLayerType(layerDefinition)) {
      return;
    }

    if (!isString(layerDefinition.name)) {
      $log.error('[AngularJS - Leaflet] A base layer must have a name');
      return;
    }

    if (!isObject(layerDefinition.layerParams)) {
      layerDefinition.layerParams = {};
    }

    if (!isObject(layerDefinition.layerOptions)) {
      layerDefinition.layerOptions = {};
    }

    // Mix the layer specific parameters with the general Leaflet options. Although this is an overhead
    // the definition of a base layers is more 'clean' if the two types of parameters are differentiated
    for (var attrname in layerDefinition.layerParams) {
      layerDefinition.layerOptions[attrname] = layerDefinition.layerParams[attrname];
    }

    var params = {
      url: layerDefinition.url,
      data: layerDefinition.data,
      options: layerDefinition.layerOptions,
      layer: layerDefinition.layer,
      icon: layerDefinition.icon,
      type: layerDefinition.layerType,
      bounds: layerDefinition.bounds,
      key: layerDefinition.key,
      apiKey: layerDefinition.apiKey,
      pluginOptions: layerDefinition.pluginOptions,
      user: layerDefinition.user,
    };

    //TODO Add $watch to the layer properties
    return layerTypes[layerDefinition.type].createLayer(params);
  }

  function safeAddLayer(map, layer) {
    if (layer && typeof layer.addTo === 'function') {
      layer.addTo(map);
    } else {
      map.addLayer(layer);
    }
  }

  function safeRemoveLayer(map, layer, layerOptions) {
    if (isDefined(layerOptions) && isDefined(layerOptions.loadedDefer)) {
      if (angular.isFunction(layerOptions.loadedDefer)) {
        var defers = layerOptions.loadedDefer();
        $log.debug('Loaded Deferred', defers);
        var count = defers.length;
        if (count > 0) {
          var resolve = function() {
            count--;
            if (count === 0) {
              map.removeLayer(layer);
            }
          };

          for (var i = 0; i < defers.length; i++) {
            defers[i].promise.then(resolve);
          }
        } else {
          map.removeLayer(layer);
        }
      } else {
        layerOptions.loadedDefer.promise.then(function() {
          map.removeLayer(layer);
        });
      }
    } else {
      map.removeLayer(layer);
    }
  }

  return {
    createLayer: createLayer,
    safeAddLayer: safeAddLayer,
    safeRemoveLayer: safeRemoveLayer,
  };
}]);

angular.module('leaflet-directive').factory('leafletLegendHelpers', function() {
  var _updateLegend = function(div, legendData, type, url) {
    div.innerHTML = '';
    if (legendData.error) {
      div.innerHTML += '<div class="info-title alert alert-danger">' + legendData.error.message + '</div>';
    } else {
      if (type === 'arcgis') {
        for (var i = 0; i < legendData.layers.length; i++) {
          var layer = legendData.layers[i];
          div.innerHTML += '<div class="info-title" data-layerid="' + layer.layerId + '">' + layer.layerName + '</div>';
          for (var j = 0; j < layer.legend.length; j++) {
            var leg = layer.legend[j];
            div.innerHTML +=
            '<div class="inline" data-layerid="' + layer.layerId + '"><img src="data:' + leg.contentType + ';base64,' + leg.imageData + '" /></div>' +
            '<div class="info-label" data-layerid="' + layer.layerId + '">' + leg.label + '</div>';
          }
        }
      }      else if (type === 'image') {
        div.innerHTML = '<img src="' + url + '"/>';
      }
    }
  };

  var _getOnAddLegend = function(legendData, legendClass, type, url) {
    return function(/*map*/) {
      var div = L.DomUtil.create('div', legendClass);

      if (!L.Browser.touch) {
        L.DomEvent.disableClickPropagation(div);
        L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation);
      } else {
        L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation);
      }

      _updateLegend(div, legendData, type, url);
      return div;
    };
  };

  var _getOnAddArrayLegend = function(legend, legendClass) {
    return function(/*map*/) {
      var div = L.DomUtil.create('div', legendClass);
      for (var i = 0; i < legend.colors.length; i++) {
        div.innerHTML +=
            '<div class="outline"><i style="background:' + legend.colors[i] + '"></i></div>' +
            '<div class="info-label">' + legend.labels[i] + '</div>';
      }

      if (!L.Browser.touch) {
        L.DomEvent.disableClickPropagation(div);
        L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation);
      } else {
        L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation);
      }

      return div;
    };
  };

  return {
    getOnAddLegend: _getOnAddLegend,
    getOnAddArrayLegend: _getOnAddArrayLegend,
    updateLegend: _updateLegend,
  };
});

angular.module('leaflet-directive').factory('leafletMapDefaults', ["$q", "leafletHelpers", function($q, leafletHelpers) {
  function _getDefaults() {
    return {
      keyboard: true,
      dragging: true,
      worldCopyJump: false,
      doubleClickZoom: true,
      scrollWheelZoom: true,
      tap: true,
      touchZoom: true,
      zoomControl: true,
      zoomsliderControl: false,
      zoomControlPosition: 'topleft',
      attributionControl: true,
      controls: {
        layers: {
          visible: true,
          position: 'topright',
          collapsed: true,
        },
      },
      nominatim: {
        server: ' http://nominatim.openstreetmap.org/search',
      },
      crs: L.CRS.EPSG3857,
      tileLayer: '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      tileLayerOptions: {
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      },
      path: {
        weight: 10,
        opacity: 1,
        color: '#0000ff',
      },
      center: {
        lat: 0,
        lng: 0,
        zoom: 1,
      },
    };
  }

  var isDefined = leafletHelpers.isDefined;
  var isObject = leafletHelpers.isObject;
  var obtainEffectiveMapId = leafletHelpers.obtainEffectiveMapId;
  var defaults = {};

  // Get the _defaults dictionary, and override the properties defined by the user
  return {
    reset: function() {
      defaults = {};
    },

    getDefaults: function(scopeId) {
      var mapId = obtainEffectiveMapId(defaults, scopeId);
      return defaults[mapId];
    },

    getMapCreationDefaults: function(scopeId) {
      var mapId = obtainEffectiveMapId(defaults, scopeId);
      var d = defaults[mapId];

      var mapDefaults = {
        maxZoom: d.maxZoom,
        keyboard: d.keyboard,
        dragging: d.dragging,
        zoomControl: d.zoomControl,
        doubleClickZoom: d.doubleClickZoom,
        scrollWheelZoom: d.scrollWheelZoom,
        tap: d.tap,
        touchZoom: d.touchZoom,
        attributionControl: d.attributionControl,
        worldCopyJump: d.worldCopyJump,
        crs: d.crs,
      };

      if (isDefined(d.minZoom)) {
        mapDefaults.minZoom = d.minZoom;
      }

      if (isDefined(d.zoomAnimation)) {
        mapDefaults.zoomAnimation = d.zoomAnimation;
      }

      if (isDefined(d.fadeAnimation)) {
        mapDefaults.fadeAnimation = d.fadeAnimation;
      }

      if (isDefined(d.markerZoomAnimation)) {
        mapDefaults.markerZoomAnimation = d.markerZoomAnimation;
      }

      if (d.map) {
        for (var option in d.map) {
          mapDefaults[option] = d.map[option];
        }
      }

      return mapDefaults;
    },

    setDefaults: function(userDefaults, scopeId) {
      var newDefaults = _getDefaults();

      if (isDefined(userDefaults)) {
        newDefaults.doubleClickZoom = isDefined(userDefaults.doubleClickZoom) ? userDefaults.doubleClickZoom : newDefaults.doubleClickZoom;
        newDefaults.scrollWheelZoom = isDefined(userDefaults.scrollWheelZoom) ? userDefaults.scrollWheelZoom : newDefaults.doubleClickZoom;
        newDefaults.tap = isDefined(userDefaults.tap) ? userDefaults.tap : newDefaults.tap;
        newDefaults.touchZoom = isDefined(userDefaults.touchZoom) ? userDefaults.touchZoom : newDefaults.doubleClickZoom;
        newDefaults.zoomControl = isDefined(userDefaults.zoomControl) ? userDefaults.zoomControl : newDefaults.zoomControl;
        newDefaults.zoomsliderControl = isDefined(userDefaults.zoomsliderControl) ? userDefaults.zoomsliderControl : newDefaults.zoomsliderControl;
        newDefaults.attributionControl = isDefined(userDefaults.attributionControl) ? userDefaults.attributionControl : newDefaults.attributionControl;
        newDefaults.tileLayer = isDefined(userDefaults.tileLayer) ? userDefaults.tileLayer : newDefaults.tileLayer;
        newDefaults.zoomControlPosition = isDefined(userDefaults.zoomControlPosition) ? userDefaults.zoomControlPosition : newDefaults.zoomControlPosition;
        newDefaults.keyboard = isDefined(userDefaults.keyboard) ? userDefaults.keyboard : newDefaults.keyboard;
        newDefaults.dragging = isDefined(userDefaults.dragging) ? userDefaults.dragging : newDefaults.dragging;

        if (isDefined(userDefaults.controls)) {
          angular.extend(newDefaults.controls, userDefaults.controls);
        }

        if (isObject(userDefaults.crs)) {
          newDefaults.crs = userDefaults.crs;
        } else if (isDefined(L.CRS[userDefaults.crs])) {
          newDefaults.crs = L.CRS[userDefaults.crs];
        }

        if (isDefined(userDefaults.center)) {
          angular.copy(userDefaults.center, newDefaults.center);
        }

        if (isDefined(userDefaults.tileLayerOptions)) {
          angular.copy(userDefaults.tileLayerOptions, newDefaults.tileLayerOptions);
        }

        if (isDefined(userDefaults.maxZoom)) {
          newDefaults.maxZoom = userDefaults.maxZoom;
        }

        if (isDefined(userDefaults.minZoom)) {
          newDefaults.minZoom = userDefaults.minZoom;
        }

        if (isDefined(userDefaults.zoomAnimation)) {
          newDefaults.zoomAnimation = userDefaults.zoomAnimation;
        }

        if (isDefined(userDefaults.fadeAnimation)) {
          newDefaults.fadeAnimation = userDefaults.fadeAnimation;
        }

        if (isDefined(userDefaults.markerZoomAnimation)) {
          newDefaults.markerZoomAnimation = userDefaults.markerZoomAnimation;
        }

        if (isDefined(userDefaults.worldCopyJump)) {
          newDefaults.worldCopyJump = userDefaults.worldCopyJump;
        }

        if (isDefined(userDefaults.map)) {
          newDefaults.map = userDefaults.map;
        }

        if (isDefined(userDefaults.path)) {
          newDefaults.path = userDefaults.path;
        }
      }

      var mapId = obtainEffectiveMapId(defaults, scopeId);
      defaults[mapId] = newDefaults;
      return newDefaults;
    },
  };
}]);

angular.module('leaflet-directive').service('leafletMarkersHelpers', ["$rootScope", "$timeout", "leafletHelpers", "$log", "$compile", "leafletGeoJsonHelpers", function($rootScope, $timeout, leafletHelpers, $log, $compile, leafletGeoJsonHelpers) {
  var isDefined = leafletHelpers.isDefined;
  var defaultTo = leafletHelpers.defaultTo;
  var MarkerClusterPlugin = leafletHelpers.MarkerClusterPlugin;
  var AwesomeMarkersPlugin = leafletHelpers.AwesomeMarkersPlugin;
  var VectorMarkersPlugin = leafletHelpers.VectorMarkersPlugin;
  var MakiMarkersPlugin = leafletHelpers.MakiMarkersPlugin;
  var ExtraMarkersPlugin = leafletHelpers.ExtraMarkersPlugin;
  var DomMarkersPlugin = leafletHelpers.DomMarkersPlugin;
  var safeApply = leafletHelpers.safeApply;
  var Helpers = leafletHelpers;
  var isString = leafletHelpers.isString;
  var isNumber = leafletHelpers.isNumber;
  var isObject = leafletHelpers.isObject;
  var groups = {};
  var geoHlp = leafletGeoJsonHelpers;
  var errorHeader = leafletHelpers.errorHeader;

  var _string = function(marker) {
    //this exists since JSON.stringify barfs on cyclic
    var retStr = '';
    ['_icon', '_latlng', '_leaflet_id', '_map', '_shadow'].forEach(function(prop) {
      retStr += prop + ': ' + defaultTo(marker[prop], 'undefined') + ' \n';
    });

    return '[leafletMarker] : \n' + retStr;
  };

  var _log = function(marker, useConsole) {
    var logger = useConsole ? console : $log;
    logger.debug(_string(marker));
  };

  var createLeafletIcon = function(iconData) {
    if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'awesomeMarker') {
      if (!AwesomeMarkersPlugin.isLoaded()) {
        $log.error(errorHeader + ' The AwesomeMarkers Plugin is not loaded.');
      }

      return new L.AwesomeMarkers.icon(iconData);
    }

    if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'vectorMarker') {
      if (!VectorMarkersPlugin.isLoaded()) {
        $log.error(errorHeader + ' The VectorMarkers Plugin is not loaded.');
      }

      return new L.VectorMarkers.icon(iconData);
    }

    if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'makiMarker') {
      if (!MakiMarkersPlugin.isLoaded()) {
        $log.error(errorHeader + 'The MakiMarkers Plugin is not loaded.');
      }

      return new L.MakiMarkers.icon(iconData);
    }

    if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'extraMarker') {
      if (!ExtraMarkersPlugin.isLoaded()) {
        $log.error(errorHeader + 'The ExtraMarkers Plugin is not loaded.');
      }

      return new L.ExtraMarkers.icon(iconData);
    }

    if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'div') {
      return new L.divIcon(iconData);
    }

    if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'dom') {
      if (!DomMarkersPlugin.isLoaded()) {
        $log.error(errorHeader + 'The DomMarkers Plugin is not loaded.');
      }

      var markerScope = angular.isFunction(iconData.getMarkerScope) ? iconData.getMarkerScope() : $rootScope;
      var template = $compile(iconData.template)(markerScope);
      var iconDataCopy = angular.copy(iconData);
      iconDataCopy.element = template[0];
      return new L.DomMarkers.icon(iconDataCopy);
    }

    // allow for any custom icon to be used... assumes the icon has already been initialized
    if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'icon') {
      return iconData.icon;
    }

    var base64icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAGmklEQVRYw7VXeUyTZxjvNnfELFuyIzOabermMZEeQC/OclkO49CpOHXOLJl/CAURuYbQi3KLgEhbrhZ1aDwmaoGqKII6odATmH/scDFbdC7LvFqOCc+e95s2VG50X/LLm/f4/Z7neY/ne18aANCmAr5E/xZf1uDOkTcGcWR6hl9247tT5U7Y6SNvWsKT63P58qbfeLJG8M5qcgTknrvvrdDbsT7Ml+tv82X6vVxJE33aRmgSyYtcWVMqX97Yv2JvW39UhRE2HuyBL+t+gK1116ly06EeWFNlAmHxlQE0OMiV6mQCScusKRlhS3QLeVJdl1+23h5dY4FNB3thrbYboqptEFlphTC1hSpJnbRvxP4NWgsE5Jyz86QNNi/5qSUTGuFk1gu54tN9wuK2wc3o+Wc13RCmsoBwEqzGcZsxsvCSy/9wJKf7UWf1mEY8JWfewc67UUoDbDjQC+FqK4QqLVMGGR9d2wurKzqBk3nqIT/9zLxRRjgZ9bqQgub+DdoeCC03Q8j+0QhFhBHR/eP3U/zCln7Uu+hihJ1+bBNffLIvmkyP0gpBZWYXhKussK6mBz5HT6M1Nqpcp+mBCPXosYQfrekGvrjewd59/GvKCE7TbK/04/ZV5QZYVWmDwH1mF3xa2Q3ra3DBC5vBT1oP7PTj4C0+CcL8c7C2CtejqhuCnuIQHaKHzvcRfZpnylFfXsYJx3pNLwhKzRAwAhEqG0SpusBHfAKkxw3w4627MPhoCH798z7s0ZnBJ/MEJbZSbXPhER2ih7p2ok/zSj2cEJDd4CAe+5WYnBCgR2uruyEw6zRoW6/DWJ/OeAP8pd/BGtzOZKpG8oke0SX6GMmRk6GFlyAc59K32OTEinILRJRchah8HQwND8N435Z9Z0FY1EqtxUg+0SO6RJ/mmXz4VuS+DpxXC3gXmZwIL7dBSH4zKE50wESf8qwVgrP1EIlTO5JP9Igu0aexdh28F1lmAEGJGfh7jE6ElyM5Rw/FDcYJjWhbeiBYoYNIpc2FT/SILivp0F1ipDWk4BIEo2VuodEJUifhbiltnNBIXPUFCMpthtAyqws/BPlEF/VbaIxErdxPphsU7rcCp8DohC+GvBIPJS/tW2jtvTmmAeuNO8BNOYQeG8G/2OzCJ3q+soYB5i6NhMaKr17FSal7GIHheuV3uSCY8qYVuEm1cOzqdWr7ku/R0BDoTT+DT+ohCM6/CCvKLKO4RI+dXPeAuaMqksaKrZ7L3FE5FIFbkIceeOZ2OcHO6wIhTkNo0ffgjRGxEqogXHYUPHfWAC/lADpwGcLRY3aeK4/oRGCKYcZXPVoeX/kelVYY8dUGf8V5EBRbgJXT5QIPhP9ePJi428JKOiEYhYXFBqou2Guh+p/mEB1/RfMw6rY7cxcjTrneI1FrDyuzUSRm9miwEJx8E/gUmqlyvHGkneiwErR21F3tNOK5Tf0yXaT+O7DgCvALTUBXdM4YhC/IawPU+2PduqMvuaR6eoxSwUk75ggqsYJ7VicsnwGIkZBSXKOUww73WGXyqP+J2/b9c+gi1YAg/xpwck3gJuucNrh5JvDPvQr0WFXf0piyt8f8/WI0hV4pRxxkQZdJDfDJNOAmM0Ag8jyT6hz0WGXWuP94Yh2jcfjmXAGvHCMslRimDHYuHuDsy2QtHuIavznhbYURq5R57KpzBBRZKPJi8eQg48h4j8SDdowifdIrEVdU+gbO6QNvRRt4ZBthUaZhUnjlYObNagV3keoeru3rU7rcuceqU1mJBxy+BWZYlNEBH+0eH4vRiB+OYybU2hnblYlTvkHinM4m54YnxSyaZYSF6R3jwgP7udKLGIX6r/lbNa9N6y5MFynjWDtrHd75ZvTYAPO/6RgF0k76mQla3FGq7dO+cH8sKn0Vo7nDllwAhqwLPkxrHwWmHJOo+AKJ4rab5OgrM7rVu8eWb2Pu0Dh4eDgXoOfvp7Y7QeqknRmvcTBEyq9m/HQQSCSz6LHq3z0yzsNySRfMS253wl2KyRDbcZPcfJKjZmSEOjcxyi+Y8dUOtsIEH6R2wNykdqrkYJ0RV92H0W58pkfQk7cKevsLK10Py8SdMGfXNXATY+pPbyJR/ET6n9nIfztNtZYRV9XniQu9IA2vOVgy4ir7GCLVmmd+zjkH0eAF9Po6K61pmCXHxU5rHMYd1ftc3owjwRSVRzLjKvqZEty6cRUD7jGqiOdu5HG6MdHjNcNYGqfDm5YRzLBBCCDl/2bk8a8gdbqcfwECu62Fg/HrggAAAABJRU5ErkJggg==';
    var base64shadow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAApCAYAAACoYAD2AAAC5ElEQVRYw+2YW4/TMBCF45S0S1luXZCABy5CgLQgwf//S4BYBLTdJLax0fFqmB07nnQfEGqkIydpVH85M+NLjPe++dcPc4Q8Qh4hj5D/AaQJx6H/4TMwB0PeBNwU7EGQAmAtsNfAzoZkgIa0ZgLMa4Aj6CxIAsjhjOCoL5z7Glg1JAOkaicgvQBXuncwJAWjksLtBTWZe04CnYRktUGdilALppZBOgHGZcBzL6OClABvMSVIzyBjazOgrvACf1ydC5mguqAVg6RhdkSWQFj2uxfaq/BrIZOLEWgZdALIDvcMcZLD8ZbLC9de4yR1sYMi4G20S4Q/PWeJYxTOZn5zJXANZHIxAd4JWhPIloTJZhzMQduM89WQ3MUVAE/RnhAXpTycqys3NZALOBbB7kFrgLesQl2h45Fcj8L1tTSohUwuxhy8H/Qg6K7gIs+3kkaigQCOcyEXCHN07wyQazhrmIulvKMQAwMcmLNqyCVyMAI+BuxSMeTk3OPikLY2J1uE+VHQk6ANrhds+tNARqBeaGc72cK550FP4WhXmFmcMGhTwAR1ifOe3EvPqIegFmF+C8gVy0OfAaWQPMR7gF1OQKqGoBjq90HPMP01BUjPOqGFksC4emE48tWQAH0YmvOgF3DST6xieJgHAWxPAHMuNhrImIdvoNOKNWIOcE+UXE0pYAnkX6uhWsgVXDxHdTfCmrEEmMB2zMFimLVOtiiajxiGWrbU52EeCdyOwPEQD8LqyPH9Ti2kgYMf4OhSKB7qYILbBv3CuVTJ11Y80oaseiMWOONc/Y7kJYe0xL2f0BaiFTxknHO5HaMGMublKwxFGzYdWsBF174H/QDknhTHmHHN39iWFnkZx8lPyM8WHfYELmlLKtgWNmFNzQcC1b47gJ4hL19i7o65dhH0Negbca8vONZoP7doIeOC9zXm8RjuL0Gf4d4OYaU5ljo3GYiqzrWQHfJxA6ALhDpVKv9qYeZA8eM3EhfPSCmpuD0AAAAASUVORK5CYII=';

    if (!isDefined(iconData) || !isDefined(iconData.iconUrl)) {
      return new L.Icon.Default({
        iconUrl: base64icon,
        shadowUrl: base64shadow,
        iconSize: [25, 41],
        iconAnchor: [12, 41],
        popupAnchor: [1, -34],
        shadowSize: [41, 41],
      });
    }

    return new L.Icon(iconData);
  };

  var _resetMarkerGroup = function(groupName) {
    if (isDefined(groups[groupName])) {
      groups.splice(groupName, 1);
    }
  };

  var _resetMarkerGroups = function() {
    groups = {};
  };

  var _deleteMarker = function(marker, map, layers) {
    marker.closePopup();

    // There is no easy way to know if a marker is added to a layer, so we search for it
    // if there are overlays
    if (isDefined(layers) && isDefined(layers.overlays)) {
      for (var key in layers.overlays) {
        if (layers.overlays[key] instanceof L.LayerGroup || layers.overlays[key] instanceof L.FeatureGroup) {
          if (layers.overlays[key].hasLayer(marker)) {
            layers.overlays[key].removeLayer(marker);
            return;
          }
        }
      }
    }

    if (isDefined(groups)) {
      for (var groupKey in groups) {
        if (groups[groupKey].hasLayer(marker)) {
          groups[groupKey].removeLayer(marker);
        }
      }
    }

    if (map.hasLayer(marker)) {
      map.removeLayer(marker);
    }
  };

  var adjustPopupPan = function(marker, map) {
    var containerHeight = marker._popup._container.offsetHeight;
    var layerPos = new L.Point(marker._popup._containerLeft, -containerHeight - marker._popup._containerBottom);
    var containerPos = map.layerPointToContainerPoint(layerPos);
    if (containerPos !== null) {
      marker._popup._adjustPan();
    }
  };

  var compilePopup = function(marker, markerScope) {
    $compile(marker._popup._contentNode)(markerScope);
  };

  var updatePopup = function(marker, markerScope, map) {
    //The innerText should be more than 1 once angular has compiled.
    //We need to keep trying until angular has compiled before we _updateLayout and _updatePosition
    //This should take care of any scenario , eg ngincludes, whatever.
    //Is there a better way to check for this?
    var innerText = marker._popup._contentNode.innerText || marker._popup._contentNode.textContent;
    if (innerText.length < 1) {
      $timeout(function() {
        updatePopup(marker, markerScope, map);
      });
    }

    //cause a reflow - this is also very important - if we don't do this then the widths are from before $compile
    var reflow = marker._popup._contentNode.offsetWidth;

    marker._popup._updateLayout();
    marker._popup._updatePosition();

    if (marker._popup.options.autoPan) {
      adjustPopupPan(marker, map);
    }

    //using / returning reflow so jshint doesn't moan
    return reflow;
  };

  var _manageOpenPopup = function(marker, markerData, map) {
    // The marker may provide a scope returning function used to compile the message
    // default to $rootScope otherwise
    var markerScope = angular.isFunction(markerData.getMessageScope) ? markerData.getMessageScope() : $rootScope;
    var compileMessage = isDefined(markerData.compileMessage) ? markerData.compileMessage : true;

    if (compileMessage) {
      if (!isDefined(marker._popup) || !isDefined(marker._popup._contentNode)) {
        $log.error(errorHeader + 'Popup is invalid or does not have any content.');
        return false;
      }

      compilePopup(marker, markerScope);
      updatePopup(marker, markerData, map);
    }
  };

  var _manageOpenLabel = function(marker, markerData) {
    var markerScope = angular.isFunction(markerData.getMessageScope) ? markerData.getMessageScope() : $rootScope;
    var labelScope = angular.isFunction(markerData.getLabelScope) ? markerData.getLabelScope() : markerScope;
    var compileMessage = isDefined(markerData.compileMessage) ? markerData.compileMessage : true;

    if (Helpers.LabelPlugin.isLoaded() && isDefined(markerData.label)) {
      if (isDefined(markerData.label.options) && markerData.label.options.noHide === true) {
        marker.showLabel();
      }

      if (compileMessage && isDefined(marker.label)) {
        $compile(marker.label._container)(labelScope);
      }
    }
  };

  var _updateMarker = function(markerData, oldMarkerData, marker, name, leafletScope, layers, map) {
    if (!isDefined(oldMarkerData)) {
      return;
    }

    // Update the lat-lng property (always present in marker properties)
    if (!geoHlp.validateCoords(markerData)) {
      $log.warn('There are problems with lat-lng data, please verify your marker model');
      _deleteMarker(marker, map, layers);
      return;
    }

    // watch is being initialized if old and new object is the same
    var isInitializing = markerData === oldMarkerData;

    // Update marker rotation
    if (isDefined(markerData.iconAngle) && oldMarkerData.iconAngle !== markerData.iconAngle) {
      marker.setIconAngle(markerData.iconAngle);
    }

    // It is possible that the layer has been removed or the layer marker does not exist
    // Update the layer group if present or move it to the map if not
    if (!isString(markerData.layer)) {
      // There is no layer information, we move the marker to the map if it was in a layer group
      if (isString(oldMarkerData.layer)) {
        // Remove from the layer group that is supposed to be
        if (isDefined(layers.overlays[oldMarkerData.layer]) && layers.overlays[oldMarkerData.layer].hasLayer(marker)) {
          layers.overlays[oldMarkerData.layer].removeLayer(marker);
          marker.closePopup();
        }

        // Test if it is not on the map and add it
        if (!map.hasLayer(marker)) {
          map.addLayer(marker);
        }
      }
    }

    if ((isNumber(markerData.opacity) || isNumber(parseFloat(markerData.opacity))) && markerData.opacity !== oldMarkerData.opacity) {
      // There was a different opacity so we update it
      marker.setOpacity(markerData.opacity);
    }

    if (isString(markerData.layer) && oldMarkerData.layer !== markerData.layer) {
      // If it was on a layer group we have to remove it
      if (isString(oldMarkerData.layer) && isDefined(layers.overlays[oldMarkerData.layer]) && layers.overlays[oldMarkerData.layer].hasLayer(marker)) {
        layers.overlays[oldMarkerData.layer].removeLayer(marker);
      }

      marker.closePopup();

      // Remove it from the map in case the new layer is hidden or there is an error in the new layer
      if (map.hasLayer(marker)) {
        map.removeLayer(marker);
      }

      // The markerData.layer is defined so we add the marker to the layer if it is different from the old data
      if (!isDefined(layers.overlays[markerData.layer])) {
        $log.error(errorHeader + 'You must use a name of an existing layer');
        return;
      }

      // Is a group layer?
      var layerGroup = layers.overlays[markerData.layer];
      if (!(layerGroup instanceof L.LayerGroup || layerGroup instanceof L.FeatureGroup)) {
        $log.error(errorHeader + 'A marker can only be added to a layer of type "group" or "featureGroup"');
        return;
      }

      // The marker goes to a correct layer group, so first of all we add it
      layerGroup.addLayer(marker);

      // The marker is automatically added to the map depending on the visibility
      // of the layer, so we only have to open the popup if the marker is in the map
      if (map.hasLayer(marker) && markerData.focus === true) {
        marker.openPopup();
      }
    }

    // Update the draggable property
    if (markerData.draggable !== true && oldMarkerData.draggable === true && (isDefined(marker.dragging))) {
      marker.dragging.disable();
    }

    if (markerData.draggable === true && oldMarkerData.draggable !== true) {
      // The markerData.draggable property must be true so we update if there wasn't a previous value or it wasn't true
      if (marker.dragging) {
        marker.dragging.enable();
      } else {
        if (L.Handler.MarkerDrag) {
          marker.dragging = new L.Handler.MarkerDrag(marker);
          marker.options.draggable = true;
          marker.dragging.enable();
        }
      }
    }

    // Update the icon property
    if (!isObject(markerData.icon)) {
      // If there is no icon property or it's not an object
      if (isObject(oldMarkerData.icon)) {
        // If there was an icon before restore to the default
        marker.setIcon(createLeafletIcon());
        marker.closePopup();
        marker.unbindPopup();
        if (isString(markerData.message)) {
          marker.bindPopup(markerData.message, markerData.popupOptions);
        }
      }
    }

    if (isObject(markerData.icon) && isObject(oldMarkerData.icon) && !angular.equals(markerData.icon, oldMarkerData.icon)) {
      var dragG = false;
      if (marker.dragging) {
        dragG = marker.dragging.enabled();
      }

      marker.setIcon(createLeafletIcon(markerData.icon));
      if (dragG) {
        marker.dragging.enable();
      }

      marker.closePopup();
      marker.unbindPopup();
      if (isString(markerData.message)) {
        marker.bindPopup(markerData.message, markerData.popupOptions);

        // if marker has been already focused, reopen popup
        if (map.hasLayer(marker) && markerData.focus === true) {
          marker.openPopup();
        }
      }
    }

    // Update the Popup message property
    if (!isString(markerData.message) && isString(oldMarkerData.message)) {
      marker.closePopup();
      marker.unbindPopup();
    }

    // Update the label content or bind a new label if the old one has been removed.
    if (Helpers.LabelPlugin.isLoaded()) {
      if (isDefined(markerData.label) && isDefined(markerData.label.message)) {
        if ('label' in oldMarkerData && 'message' in oldMarkerData.label && !angular.equals(markerData.label.message, oldMarkerData.label.message)) {
          marker.updateLabelContent(markerData.label.message);
        } else if (!angular.isFunction(marker.getLabel) || angular.isFunction(marker.getLabel) && !isDefined(marker.getLabel())) {
          marker.bindLabel(markerData.label.message, markerData.label.options);
          _manageOpenLabel(marker, markerData);
        } else {
          _manageOpenLabel(marker, markerData);
        }
      } else if (!('label' in markerData && !('message' in markerData.label))) {
        if (angular.isFunction(marker.unbindLabel)) {
          marker.unbindLabel();
        }
      }
    }

    // There is some text in the popup, so we must show the text or update existing
    if (isString(markerData.message) && !isString(oldMarkerData.message)) {
      // There was no message before so we create it
      marker.bindPopup(markerData.message, markerData.popupOptions);
    }

    if (isString(markerData.message) && isString(oldMarkerData.message) && markerData.message !== oldMarkerData.message) {
      // There was a different previous message so we update it
      marker.setPopupContent(markerData.message);
    }

    // Update the focus property
    var updatedFocus = false;
    if (markerData.focus !== true && oldMarkerData.focus === true) {
      // If there was a focus property and was true we turn it off
      marker.closePopup();
      updatedFocus = true;
    }

    // The markerData.focus property must be true so we update if there wasn't a previous value or it wasn't true
    if (markerData.focus === true && (!isDefined(oldMarkerData.focus) || oldMarkerData.focus === false) || (isInitializing && markerData.focus === true)) {
      // Reopen the popup when focus is still true
      marker.openPopup();
      updatedFocus = true;
    }

    // zIndexOffset adjustment
    if (oldMarkerData.zIndexOffset !== markerData.zIndexOffset) {
      marker.setZIndexOffset(markerData.zIndexOffset);
    }

    var markerLatLng = marker.getLatLng();
    var isCluster = (isString(markerData.layer) && Helpers.MarkerClusterPlugin.is(layers.overlays[markerData.layer]));

    // If the marker is in a cluster it has to be removed and added to the layer when the location is changed
    if (isCluster) {
      // The focus has changed even by a user click or programatically
      if (updatedFocus) {
        // We only have to update the location if it was changed programatically, because it was
        // changed by a user drag the marker data has already been updated by the internal event
        // listened by the directive
        if ((markerData.lat !== oldMarkerData.lat) || (markerData.lng !== oldMarkerData.lng)) {
          layers.overlays[markerData.layer].removeLayer(marker);
          marker.setLatLng([markerData.lat, markerData.lng]);
          layers.overlays[markerData.layer].addLayer(marker);
        }
      } else {
        // The marker has possibly moved. It can be moved by a user drag (marker location and data are equal but old
        // data is diferent) or programatically (marker location and data are diferent)
        if ((markerLatLng.lat !== markerData.lat) || (markerLatLng.lng !== markerData.lng)) {
          // The marker was moved by a user drag
          layers.overlays[markerData.layer].removeLayer(marker);
          marker.setLatLng([markerData.lat, markerData.lng]);
          layers.overlays[markerData.layer].addLayer(marker);
        } else if ((markerData.lat !== oldMarkerData.lat) || (markerData.lng !== oldMarkerData.lng)) {
          // The marker was moved programatically
          layers.overlays[markerData.layer].removeLayer(marker);
          marker.setLatLng([markerData.lat, markerData.lng]);
          layers.overlays[markerData.layer].addLayer(marker);
        } else if (isObject(markerData.icon) && isObject(oldMarkerData.icon) && !angular.equals(markerData.icon, oldMarkerData.icon)) {
          layers.overlays[markerData.layer].removeLayer(marker);
          layers.overlays[markerData.layer].addLayer(marker);
        }
      }
    } else if (markerLatLng.lat !== markerData.lat || markerLatLng.lng !== markerData.lng) {
      marker.setLatLng([markerData.lat, markerData.lng]);
    }
  };

  return {
    resetMarkerGroup: _resetMarkerGroup,

    resetMarkerGroups: _resetMarkerGroups,

    deleteMarker: _deleteMarker,

    manageOpenPopup: _manageOpenPopup,

    manageOpenLabel: _manageOpenLabel,

    createMarker: function(markerData) {
      if (!isDefined(markerData) || !geoHlp.validateCoords(markerData)) {
        $log.error(errorHeader + 'The marker definition is not valid.');
        return;
      }

      var coords = geoHlp.getCoords(markerData);

      if (!isDefined(coords)) {
        $log.error(errorHeader + 'Unable to get coordinates from markerData.');
        return;
      }

      var markerOptions = {
        icon: createLeafletIcon(markerData.icon),
        title: isDefined(markerData.title) ? markerData.title : '',
        draggable: isDefined(markerData.draggable) ? markerData.draggable : false,
        clickable: isDefined(markerData.clickable) ? markerData.clickable : true,
        riseOnHover: isDefined(markerData.riseOnHover) ? markerData.riseOnHover : false,
        zIndexOffset: isDefined(markerData.zIndexOffset) ? markerData.zIndexOffset : 0,
        iconAngle: isDefined(markerData.iconAngle) ? markerData.iconAngle : 0,
      };

      // Add any other options not added above to markerOptions
      for (var markerDatum in markerData) {
        if (markerData.hasOwnProperty(markerDatum) && !markerOptions.hasOwnProperty(markerDatum)) {
          markerOptions[markerDatum] = markerData[markerDatum];
        }
      }

      var marker = new L.marker(coords, markerOptions);

      if (!isString(markerData.message)) {
        marker.unbindPopup();
      }

      return marker;
    },

    addMarkerToGroup: function(marker, groupName, groupOptions, map) {
      if (!isString(groupName)) {
        $log.error(errorHeader + 'The marker group you have specified is invalid.');
        return;
      }

      if (!MarkerClusterPlugin.isLoaded()) {
        $log.error(errorHeader + 'The MarkerCluster plugin is not loaded.');
        return;
      }

      if (!isDefined(groups[groupName])) {
        groups[groupName] = new L.MarkerClusterGroup(groupOptions);
        map.addLayer(groups[groupName]);
      }

      groups[groupName].addLayer(marker);
    },

    listenMarkerEvents: function(marker, markerData, leafletScope, doWatch, map) {
      marker.on('popupopen', function(/* event */) {
        safeApply(leafletScope, function() {
          if (isDefined(marker._popup) || isDefined(marker._popup._contentNode)) {
            markerData.focus = true;
            _manageOpenPopup(marker, markerData, map);//needed since markerData is now a copy
          }
        });
      });

      marker.on('popupclose', function(/* event */) {
        safeApply(leafletScope, function() {
          markerData.focus = false;
        });
      });

      marker.on('add', function(/* event */) {
        safeApply(leafletScope, function() {
          if ('label' in markerData)
              _manageOpenLabel(marker, markerData);
        });
      });
    },

    updateMarker: _updateMarker,

    addMarkerWatcher: function(marker, name, leafletScope, layers, map, isDeepWatch) {
      var markerWatchPath = Helpers.getObjectArrayPath('markers.' + name);
      isDeepWatch = defaultTo(isDeepWatch, true);

      var clearWatch = leafletScope.$watch(markerWatchPath, function(markerData, oldMarkerData) {
        if (!isDefined(markerData)) {
          _deleteMarker(marker, map, layers);
          clearWatch();
          return;
        }

        _updateMarker(markerData, oldMarkerData, marker, name, leafletScope, layers, map);
      }, isDeepWatch);
    },

    string: _string,
    log: _log,
  };
}]);

angular.module('leaflet-directive').factory('leafletPathsHelpers', ["$rootScope", "$log", "leafletHelpers", function($rootScope, $log, leafletHelpers) {
  var isDefined = leafletHelpers.isDefined;
  var isArray = leafletHelpers.isArray;
  var isNumber = leafletHelpers.isNumber;
  var isValidPoint = leafletHelpers.isValidPoint;

  var availableOptions = [

      // Path options
      'stroke', 'weight', 'color', 'opacity',
      'fill', 'fillColor', 'fillOpacity',
      'dashArray', 'lineCap', 'lineJoin', 'clickable',
      'pointerEvents', 'className',

      // Polyline options
      'smoothFactor', 'noClip',
  ];
  function _convertToLeafletLatLngs(latlngs) {
    return latlngs.filter(function(latlng) {
      return isValidPoint(latlng);
    }).map(function(latlng) {
      return _convertToLeafletLatLng(latlng);
    });
  }

  function _convertToLeafletLatLng(latlng) {
    if (isArray(latlng)) {
      return new L.LatLng(latlng[0], latlng[1]);
    } else {
      return new L.LatLng(latlng.lat, latlng.lng);
    }
  }

  function _convertToLeafletMultiLatLngs(paths) {
    return paths.map(function(latlngs) {
      return _convertToLeafletLatLngs(latlngs);
    });
  }

  function _getOptions(path, defaults) {
    var options = {};
    for (var i = 0; i < availableOptions.length; i++) {
      var optionName = availableOptions[i];

      if (isDefined(path[optionName])) {
        options[optionName] = path[optionName];
      } else if (isDefined(defaults.path[optionName])) {
        options[optionName] = defaults.path[optionName];
      }
    }

    return options;
  }

  var _updatePathOptions = function(path, data) {
    var updatedStyle = {};
    for (var i = 0; i < availableOptions.length; i++) {
      var optionName = availableOptions[i];
      if (isDefined(data[optionName])) {
        updatedStyle[optionName] = data[optionName];
      }
    }

    path.setStyle(data);
  };

  var _isValidPolyline = function(latlngs) {
    if (!isArray(latlngs)) {
      return false;
    }

    for (var i = 0; i < latlngs.length; i++) {
      var point = latlngs[i];
      if (!isValidPoint(point)) {
        return false;
      }
    }

    return true;
  };

  var pathTypes = {
    polyline: {
      isValid: function(pathData) {
        var latlngs = pathData.latlngs;
        return _isValidPolyline(latlngs);
      },

      createPath: function(options) {
        return new L.Polyline([], options);
      },

      setPath: function(path, data) {
        path.setLatLngs(_convertToLeafletLatLngs(data.latlngs));
        _updatePathOptions(path, data);
        return;
      },
    },
    multiPolyline: {
      isValid: function(pathData) {
        var latlngs = pathData.latlngs;
        if (!isArray(latlngs)) {
          return false;
        }

        for (var i in latlngs) {
          var polyline = latlngs[i];
          if (!_isValidPolyline(polyline)) {
            return false;
          }
        }

        return true;
      },

      createPath: function(options) {
        return new L.multiPolyline([[[0, 0], [1, 1]]], options);
      },

      setPath: function(path, data) {
        path.setLatLngs(_convertToLeafletMultiLatLngs(data.latlngs));
        _updatePathOptions(path, data);
        return;
      },
    },
    polygon: {
      isValid: function(pathData) {
        var latlngs = pathData.latlngs;
        return _isValidPolyline(latlngs);
      },

      createPath: function(options) {
        return new L.Polygon([], options);
      },

      setPath: function(path, data) {
        path.setLatLngs(_convertToLeafletLatLngs(data.latlngs));
        _updatePathOptions(path, data);
        return;
      },
    },
    multiPolygon: {
      isValid: function(pathData) {
        var latlngs = pathData.latlngs;

        if (!isArray(latlngs)) {
          return false;
        }

        for (var i in latlngs) {
          var polyline = latlngs[i];
          if (!_isValidPolyline(polyline)) {
            return false;
          }
        }

        return true;
      },

      createPath: function(options) {
        return new L.MultiPolygon([[[0, 0], [1, 1], [0, 1]]], options);
      },

      setPath: function(path, data) {
        path.setLatLngs(_convertToLeafletMultiLatLngs(data.latlngs));
        _updatePathOptions(path, data);
        return;
      },
    },
    rectangle: {
      isValid: function(pathData) {
        var latlngs = pathData.latlngs;

        if (!isArray(latlngs) || latlngs.length !== 2) {
          return false;
        }

        for (var i in latlngs) {
          var point = latlngs[i];
          if (!isValidPoint(point)) {
            return false;
          }
        }

        return true;
      },

      createPath: function(options) {
        return new L.Rectangle([[0, 0], [1, 1]], options);
      },

      setPath: function(path, data) {
        path.setBounds(new L.LatLngBounds(_convertToLeafletLatLngs(data.latlngs)));
        _updatePathOptions(path, data);
      },
    },
    circle: {
      isValid: function(pathData) {
        var point = pathData.latlngs;
        return isValidPoint(point) && isNumber(pathData.radius);
      },

      createPath: function(options) {
        return new L.Circle([0, 0], 1, options);
      },

      setPath: function(path, data) {
        path.setLatLng(_convertToLeafletLatLng(data.latlngs));
        if (isDefined(data.radius)) {
          path.setRadius(data.radius);
        }

        _updatePathOptions(path, data);
      },
    },
    circleMarker: {
      isValid: function(pathData) {
        var point = pathData.latlngs;
        return isValidPoint(point) && isNumber(pathData.radius);
      },

      createPath: function(options) {
        return new L.CircleMarker([0, 0], options);
      },

      setPath: function(path, data) {
        path.setLatLng(_convertToLeafletLatLng(data.latlngs));
        if (isDefined(data.radius)) {
          path.setRadius(data.radius);
        }

        _updatePathOptions(path, data);
      },
    },
  };

  var _getPathData = function(path) {
    var pathData = {};
    if (path.latlngs) {
      pathData.latlngs = path.latlngs;
    }

    if (path.radius) {
      pathData.radius = path.radius;
    }

    return pathData;
  };

  return {
    setPathOptions: function(leafletPath, pathType, data) {
      if (!isDefined(pathType)) {
        pathType = 'polyline';
      }

      pathTypes[pathType].setPath(leafletPath, data);
    },

    createPath: function(name, path, defaults) {
      if (!isDefined(path.type)) {
        path.type = 'polyline';
      }

      var options = _getOptions(path, defaults);
      var pathData = _getPathData(path);

      if (!pathTypes[path.type].isValid(pathData)) {
        $log.error('[AngularJS - Leaflet] Invalid data passed to the ' + path.type + ' path');
        return;
      }

      return pathTypes[path.type].createPath(options);
    },
  };
}]);

angular.module('leaflet-directive')
.service('leafletWatchHelpers', function() {

  var _maybe = function(scope, watchFunctionName, thingToWatchStr, watchOptions, initCb) {
    //watchOptions.isDeep is/should be ignored in $watchCollection
    var unWatch = scope[watchFunctionName](thingToWatchStr, function(newValue, oldValue) {
      initCb(newValue, oldValue);
      if (!watchOptions.doWatch)
          unWatch();
    }, watchOptions.isDeep);

    return unWatch;
  };

  /*
  @name: maybeWatch
  @description: Utility to watch something once or forever.
  @returns unWatch function
  @param watchOptions - see markersWatchOptions and or derrivatives. This object is used
  to set watching to once and its watch depth.
  */
  var _maybeWatch = function(scope, thingToWatchStr, watchOptions, initCb) {
    return _maybe(scope, '$watch', thingToWatchStr, watchOptions, initCb);
  };

  /*
  @name: _maybeWatchCollection
  @description: Utility to watch something once or forever.
  @returns unWatch function
  @param watchOptions - see markersWatchOptions and or derrivatives. This object is used
  to set watching to once and its watch depth.
  */
  var _maybeWatchCollection = function(scope, thingToWatchStr, watchOptions, initCb) {
    return _maybe(scope, '$watchCollection', thingToWatchStr, watchOptions, initCb);
  };

  return {
    maybeWatch: _maybeWatch,
    maybeWatchCollection: _maybeWatchCollection,
  };
});

angular.module('leaflet-directive').factory('nominatimService', ["$q", "$http", "leafletHelpers", "leafletMapDefaults", function($q, $http, leafletHelpers, leafletMapDefaults) {
  var isDefined = leafletHelpers.isDefined;

  return {
    query: function(address, mapId) {
      var defaults = leafletMapDefaults.getDefaults(mapId);
      var url = defaults.nominatim.server;
      var df = $q.defer();

      $http.get(url, { params: { format: 'json', limit: 1, q: address } }).success(function(data) {
        if (data.length > 0 && isDefined(data[0].boundingbox)) {
          df.resolve(data[0]);
        } else {
          df.reject('[Nominatim] Invalid address');
        }
      });

      return df.promise;
    },
  };
}]);

angular.module('leaflet-directive').directive('bounds', ["$log", "$timeout", "$http", "leafletHelpers", "nominatimService", "leafletBoundsHelpers", function($log, $timeout, $http, leafletHelpers, nominatimService, leafletBoundsHelpers) {

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: ['leaflet'],

    link: function(scope, element, attrs, controller) {
      var isDefined = leafletHelpers.isDefined;
      var createLeafletBounds = leafletBoundsHelpers.createLeafletBounds;
      var leafletScope = controller[0].getLeafletScope();
      var mapController = controller[0];
      var errorHeader = leafletHelpers.errorHeader + ' [Bounds] ';

      var emptyBounds = function(bounds) {
        return (bounds._southWest.lat === 0 && bounds._southWest.lng === 0 &&
                bounds._northEast.lat === 0 && bounds._northEast.lng === 0);
      };

      mapController.getMap().then(function(map) {
        leafletScope.$on('boundsChanged', function(event) {
          var scope = event.currentScope;
          var bounds = map.getBounds();

          if (emptyBounds(bounds) || scope.settingBoundsFromScope) {
            return;
          }

          scope.settingBoundsFromLeaflet = true;
          var newScopeBounds = {
            northEast: {
              lat: bounds._northEast.lat,
              lng: bounds._northEast.lng,
            },
            southWest: {
              lat: bounds._southWest.lat,
              lng: bounds._southWest.lng,
            },
            options: bounds.options,
          };
          if (!angular.equals(scope.bounds, newScopeBounds)) {
            scope.bounds = newScopeBounds;
          }

          $timeout(function() {
            scope.settingBoundsFromLeaflet = false;
          });
        });

        var lastNominatimQuery;
        leafletScope.$watch('bounds', function(bounds) {
          if (scope.settingBoundsFromLeaflet)
              return;
          if (isDefined(bounds.address) && bounds.address !== lastNominatimQuery) {
            scope.settingBoundsFromScope = true;
            nominatimService.query(bounds.address, attrs.id).then(function(data) {
              var b = data.boundingbox;
              var newBounds = [[b[0], b[2]], [b[1], b[3]]];
              map.fitBounds(newBounds);
            }, function(errMsg) {

              $log.error(errorHeader + ' ' + errMsg + '.');
            });

            lastNominatimQuery = bounds.address;
            $timeout(function() {
              scope.settingBoundsFromScope = false;
            });

            return;
          }

          var leafletBounds = createLeafletBounds(bounds);
          if (leafletBounds && !map.getBounds().equals(leafletBounds)) {
            scope.settingBoundsFromScope = true;
            map.fitBounds(leafletBounds, bounds.options);
            $timeout(function() {
              scope.settingBoundsFromScope = false;
            });
          }
        }, true);
      });
    },
  };
}]);

var centerDirectiveTypes = ['center', 'lfCenter'];
var centerDirectives = {};

centerDirectiveTypes.forEach(function(directiveName) {
  centerDirectives[directiveName] = ['$log', '$q', '$location', '$timeout', 'leafletMapDefaults', 'leafletHelpers',
      'leafletBoundsHelpers', 'leafletMapEvents',
        function($log, $q, $location, $timeout, leafletMapDefaults, leafletHelpers,
      leafletBoundsHelpers, leafletMapEvents) {

      var isDefined = leafletHelpers.isDefined;
      var isNumber = leafletHelpers.isNumber;
      var isSameCenterOnMap = leafletHelpers.isSameCenterOnMap;
      var safeApply = leafletHelpers.safeApply;
      var isValidCenter = leafletHelpers.isValidCenter;
      var isValidBounds = leafletBoundsHelpers.isValidBounds;
      var isUndefinedOrEmpty = leafletHelpers.isUndefinedOrEmpty;
      var errorHeader = leafletHelpers.errorHeader;

      var shouldInitializeMapWithBounds = function(bounds, center) {
        return isDefined(bounds) && isValidBounds(bounds) && isUndefinedOrEmpty(center);
      };

      var _leafletCenter;
      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: 'leaflet',
        controller: function() {
          _leafletCenter = $q.defer();
          this.getCenter = function() {
            return _leafletCenter.promise;
          };
        },

        link: function(scope, element, attrs, controller) {
          var leafletScope = controller.getLeafletScope();
          var centerModel = leafletScope[directiveName];

          controller.getMap().then(function(map) {
            var defaults = leafletMapDefaults.getDefaults(attrs.id);

            if (attrs[directiveName].search('-') !== -1) {
              $log.error(errorHeader + ' The "center" variable can\'t use a "-" on its key name: "' + attrs[directiveName] + '".');
              map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom);
              return;
            } else if (shouldInitializeMapWithBounds(leafletScope.bounds, centerModel)) {
              map.fitBounds(leafletBoundsHelpers.createLeafletBounds(leafletScope.bounds), leafletScope.bounds.options);
              centerModel = map.getCenter();
              safeApply(leafletScope, function(scope) {
                angular.extend(scope[directiveName], {
                  lat: map.getCenter().lat,
                  lng: map.getCenter().lng,
                  zoom: map.getZoom(),
                  autoDiscover: false,
                });
              });

              safeApply(leafletScope, function(scope) {
                var mapBounds = map.getBounds();
                scope.bounds = {
                  northEast: {
                    lat: mapBounds._northEast.lat,
                    lng: mapBounds._northEast.lng,
                  },
                  southWest: {
                    lat: mapBounds._southWest.lat,
                    lng: mapBounds._southWest.lng,
                  },
                };
              });
            } else if (!isDefined(centerModel)) {
              $log.error(errorHeader + ' The "center" property is not defined in the main scope');
              map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom);
              return;
            } else if (!(isDefined(centerModel.lat) && isDefined(centerModel.lng)) && !isDefined(centerModel.autoDiscover)) {
              angular.copy(defaults.center, centerModel);
            }

            var urlCenterHash;
            var mapReady;
            if (attrs.urlHashCenter === 'yes') {
              var extractCenterFromUrl = function() {
                var search = $location.search();
                var centerParam;
                if (isDefined(search.c)) {
                  var cParam = search.c.split(':');
                  if (cParam.length === 3) {
                    centerParam = {
                      lat: parseFloat(cParam[0]),
                      lng: parseFloat(cParam[1]),
                      zoom: parseInt(cParam[2], 10),
                    };
                  }
                }

                return centerParam;
              };

              urlCenterHash = extractCenterFromUrl();

              leafletScope.$on('$locationChangeSuccess', function(event) {
                var scope = event.currentScope;

                //$log.debug("updated location...");
                var urlCenter = extractCenterFromUrl();
                if (isDefined(urlCenter) && !isSameCenterOnMap(urlCenter, map)) {
                  //$log.debug("updating center model...", urlCenter);
                  angular.extend(scope[directiveName], {
                    lat: urlCenter.lat,
                    lng: urlCenter.lng,
                    zoom: urlCenter.zoom,
                  });
                }
              });
            }

            leafletScope.$watch(directiveName, function(center) {
              if (leafletScope.settingCenterFromLeaflet)
                  return;

              //$log.debug("updated center model...");
              // The center from the URL has priority
              if (isDefined(urlCenterHash)) {
                angular.copy(urlCenterHash, center);
                urlCenterHash = undefined;
              }

              if (!isValidCenter(center) && center.autoDiscover !== true) {
                $log.warn(errorHeader + ' invalid \'center\'');

                //map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom);
                return;
              }

              if (center.autoDiscover === true) {
                if (!isNumber(center.zoom)) {
                  map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom);
                }

                if (isNumber(center.zoom) && center.zoom > defaults.center.zoom) {
                  map.locate({
                    setView: true,
                    maxZoom: center.zoom,
                  });
                } else if (isDefined(defaults.maxZoom)) {
                  map.locate({
                    setView: true,
                    maxZoom: defaults.maxZoom,
                  });
                } else {
                  map.locate({
                    setView: true,
                  });
                }

                return;
              }

              if (mapReady && isSameCenterOnMap(center, map)) {
                //$log.debug("no need to update map again.");
                return;
              }

              //$log.debug("updating map center...", center);
              leafletScope.settingCenterFromScope = true;
              map.setView([center.lat, center.lng], center.zoom);
              leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map);
              $timeout(function() {
                leafletScope.settingCenterFromScope = false;

                //$log.debug("allow center scope updates");
              });
            }, true);

            map.whenReady(function() {
              mapReady = true;
            });

            map.on('moveend', function(/* event */) {
              // Resolve the center after the first map position
              _leafletCenter.resolve();
              leafletMapEvents.notifyCenterUrlHashChanged(leafletScope, map, attrs, $location.search());

              //$log.debug("updated center on map...");
              if (isSameCenterOnMap(centerModel, map) || leafletScope.settingCenterFromScope) {
                //$log.debug("same center in model, no need to update again.");
                return;
              }

              leafletScope.settingCenterFromLeaflet = true;
              safeApply(leafletScope, function(scope) {
                if (!leafletScope.settingCenterFromScope) {
                  //$log.debug("updating center model...", map.getCenter(), map.getZoom());
                  angular.extend(scope[directiveName], {
                    lat: map.getCenter().lat,
                    lng: map.getCenter().lng,
                    zoom: map.getZoom(),
                    autoDiscover: false,
                  });
                }

                leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map);
                $timeout(function() {
                  leafletScope.settingCenterFromLeaflet = false;
                });
              });
            });

            if (centerModel.autoDiscover === true) {
              map.on('locationerror', function() {
                $log.warn(errorHeader + ' The Geolocation API is unauthorized on this page.');
                if (isValidCenter(centerModel)) {
                  map.setView([centerModel.lat, centerModel.lng], centerModel.zoom);
                  leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map);
                } else {
                  map.setView([defaults.center.lat, defaults.center.lng], defaults.center.zoom);
                  leafletMapEvents.notifyCenterChangedToBounds(leafletScope, map);
                }
              });
            }
          });
        },
      };
    },
    ];
});

centerDirectiveTypes.forEach(function(dirType) {
  angular.module('leaflet-directive').directive(dirType, centerDirectives[dirType]);
});

angular.module('leaflet-directive').directive('controls', ["$log", "leafletHelpers", "leafletControlHelpers", function($log, leafletHelpers, leafletControlHelpers) {

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: '?^leaflet',

    link: function(scope, element, attrs, controller) {
      if (!controller) {
        return;
      }

      var createControl = leafletControlHelpers.createControl;
      var isValidControlType = leafletControlHelpers.isValidControlType;
      var leafletScope  = controller.getLeafletScope();
      var isDefined = leafletHelpers.isDefined;
      var isArray = leafletHelpers.isArray;
      var leafletControls = {};
      var errorHeader = leafletHelpers.errorHeader + ' [Controls] ';

      controller.getMap().then(function(map) {

        leafletScope.$watchCollection('controls', function(newControls) {

          // Delete controls from the array
          for (var name in leafletControls) {
            if (!isDefined(newControls[name])) {
              if (map.hasControl(leafletControls[name])) {
                map.removeControl(leafletControls[name]);
              }

              delete leafletControls[name];
            }
          }

          for (var newName in newControls) {
            var control;

            var controlType = isDefined(newControls[newName].type) ? newControls[newName].type : newName;

            if (!isValidControlType(controlType)) {
              $log.error(errorHeader + ' Invalid control type: ' + controlType + '.');
              return;
            }

            if (controlType !== 'custom') {
              control = createControl(controlType, newControls[newName]);
              map.addControl(control);
              leafletControls[newName] = control;
            } else {
              var customControlValue = newControls[newName];
              if (isArray(customControlValue)) {
                for (var i in customControlValue) {
                  var customControl = customControlValue[i];
                  map.addControl(customControl);
                  leafletControls[newName] = !isDefined(leafletControls[newName]) ? [customControl] : leafletControls[newName].concat([customControl]);
                }
              } else {
                map.addControl(customControlValue);
                leafletControls[newName] = customControlValue;
              }
            }
          }

        });

      });
    },
  };
}]);

angular.module('leaflet-directive').directive('decorations', ["$log", "leafletHelpers", function($log, leafletHelpers) {

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: 'leaflet',

    link: function(scope, element, attrs, controller) {
      var leafletScope = controller.getLeafletScope();
      var PolylineDecoratorPlugin = leafletHelpers.PolylineDecoratorPlugin;
      var isDefined = leafletHelpers.isDefined;
      var leafletDecorations = {};

      /* Creates an "empty" decoration with a set of coordinates, but no pattern. */
      function createDecoration(options) {
        if (isDefined(options) && isDefined(options.coordinates)) {
          if (!PolylineDecoratorPlugin.isLoaded()) {
            $log.error('[AngularJS - Leaflet] The PolylineDecorator Plugin is not loaded.');
          }
        }

        return L.polylineDecorator(options.coordinates);
      }

      /* Updates the path and the patterns for the provided decoration, and returns the decoration. */
      function setDecorationOptions(decoration, options) {
        if (isDefined(decoration) && isDefined(options)) {
          if (isDefined(options.coordinates) && isDefined(options.patterns)) {
            decoration.setPaths(options.coordinates);
            decoration.setPatterns(options.patterns);
            return decoration;
          }
        }
      }

      controller.getMap().then(function(map) {
        leafletScope.$watch('decorations', function(newDecorations) {
          for (var name in leafletDecorations) {
            if (!isDefined(newDecorations[name]) || !angular.equals(newDecorations[name], leafletDecorations)) {
              map.removeLayer(leafletDecorations[name]);
              delete leafletDecorations[name];
            }
          }

          for (var newName in newDecorations) {
            var decorationData = newDecorations[newName];
            var newDecoration = createDecoration(decorationData);

            if (isDefined(newDecoration)) {
              leafletDecorations[newName] = newDecoration;
              map.addLayer(newDecoration);
              setDecorationOptions(newDecoration, decorationData);
            }
          }
        }, true);
      });
    },
  };
}]);

angular.module('leaflet-directive').directive('eventBroadcast', ["$log", "$rootScope", "leafletHelpers", "leafletMapEvents", "leafletIterators", function($log, $rootScope, leafletHelpers, leafletMapEvents, leafletIterators) {

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: 'leaflet',

    link: function(scope, element, attrs, controller) {
      var isObject = leafletHelpers.isObject;
      var isDefined = leafletHelpers.isDefined;
      var leafletScope  = controller.getLeafletScope();
      var eventBroadcast = leafletScope.eventBroadcast;
      var availableMapEvents = leafletMapEvents.getAvailableMapEvents();
      var addEvents = leafletMapEvents.addEvents;

      controller.getMap().then(function(map) {

        var mapEvents = [];
        var logic = 'broadcast';

        // We have a possible valid object
        if (!isDefined(eventBroadcast.map)) {
          // We do not have events enable/disable do we do nothing (all enabled by default)
          mapEvents = availableMapEvents;
        } else if (!isObject(eventBroadcast.map)) {
          // Not a valid object
          $log.warn('[AngularJS - Leaflet] event-broadcast.map must be an object check your model.');
        } else {
          // We have a possible valid map object
          // Event propadation logic
          if (eventBroadcast.map.logic !== 'emit' && eventBroadcast.map.logic !== 'broadcast') {
            // This is an error
            $log.warn('[AngularJS - Leaflet] Available event propagation logic are: \'emit\' or \'broadcast\'.');
          } else {
            logic = eventBroadcast.map.logic;
          }

          if (!(isObject(eventBroadcast.map.enable) && eventBroadcast.map.enable.length >= 0)) {
            $log.warn('[AngularJS - Leaflet] event-broadcast.map.enable must be an object check your model.');
          } else {
            // Enable events
            leafletIterators.each(eventBroadcast.map.enable, function(eventName) {
              // Do we have already the event enabled?
              if (mapEvents.indexOf(eventName) === -1 && availableMapEvents.indexOf(eventName) !== -1) {
                mapEvents.push(eventName);
              }
            });
          }

        }

        // as long as the map is removed in the root leaflet directive we
        // do not need ot clean up the events as leaflet does it itself
        addEvents(map, mapEvents, 'eventName', leafletScope, logic);
      });
    },
  };
}]);

angular.module('leaflet-directive')
.directive('geojson', ["$log", "$rootScope", "leafletData", "leafletHelpers", "leafletWatchHelpers", "leafletDirectiveControlsHelpers", "leafletIterators", "leafletGeoJsonEvents", function($log, $rootScope, leafletData, leafletHelpers,
    leafletWatchHelpers, leafletDirectiveControlsHelpers, leafletIterators, leafletGeoJsonEvents) {
  var _maybeWatch = leafletWatchHelpers.maybeWatch;
  var _watchOptions = leafletHelpers.watchOptions;
  var _extendDirectiveControls = leafletDirectiveControlsHelpers.extend;
  var hlp = leafletHelpers;
  var $it = leafletIterators;

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: 'leaflet',

    link: function(scope, element, attrs, controller) {
      var isDefined = leafletHelpers.isDefined;
      var leafletScope  = controller.getLeafletScope();
      var leafletGeoJSON = {};
      var _hasSetLeafletData = false;

      controller.getMap().then(function(map) {
        var watchOptions = leafletScope.geojsonWatchOptions || _watchOptions;

        var _hookUpEvents = function(geojson, maybeName) {
          var onEachFeature;

          if (angular.isFunction(geojson.onEachFeature)) {
            onEachFeature = geojson.onEachFeature;
          } else {
            onEachFeature = function(feature, layer) {
              if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(feature.properties.description)) {
                layer.bindLabel(feature.properties.description);
              }

              leafletGeoJsonEvents.bindEvents(attrs.id, layer, null, feature,
                  leafletScope, maybeName,
                  {resetStyleOnMouseout: geojson.resetStyleOnMouseout,
                  mapId: attrs.id, });
            };
          }

          return onEachFeature;
        };

        var isNested = (hlp.isDefined(attrs.geojsonNested) &&
            hlp.isTruthy(attrs.geojsonNested));

        var _clean = function() {
          if (!leafletGeoJSON)
              return;
          var _remove = function(lObject) {
            if (isDefined(lObject) && map.hasLayer(lObject)) {
              map.removeLayer(lObject);
            }
          };

          if (isNested) {
            $it.each(leafletGeoJSON, function(lObject) {
              _remove(lObject);
            });

            return;
          }

          _remove(leafletGeoJSON);
        };

        var _addGeojson = function(model, maybeName) {
          var geojson = angular.copy(model);
          if (!(isDefined(geojson) && isDefined(geojson.data))) {
            return;
          }

          var onEachFeature = _hookUpEvents(geojson, maybeName);

          if (!isDefined(geojson.options)) {
            //right here is why we use a clone / copy (we modify and thus)
            //would kick of a watcher.. we need to be more careful everywhere
            //for stuff like this
            geojson.options = {
              style: geojson.style,
              filter: geojson.filter,
              onEachFeature: onEachFeature,
              pointToLayer: geojson.pointToLayer,
            };
          }

          var lObject = L.geoJson(geojson.data, geojson.options);

          if (maybeName && hlp.isString(maybeName)) {
            leafletGeoJSON[maybeName] = lObject;
          }          else {
            leafletGeoJSON = lObject;
          }

          lObject.addTo(map);

          if (!_hasSetLeafletData) {//only do this once and play with the same ref forever
            _hasSetLeafletData = true;
            leafletData.setGeoJSON(leafletGeoJSON, attrs.id);
          }
        };

        var _create = function(model) {
          _clean();
          if (isNested) {
            if (!model || !Object.keys(model).length)
                return;
            $it.each(model, function(m, name) {
              //name could be layerName and or groupName
              //for now it is not tied to a layer
              _addGeojson(m, name);
            });

            return;
          }

          _addGeojson(model);
        };

        _extendDirectiveControls(attrs.id, 'geojson', _create, _clean);

        _maybeWatch(leafletScope, 'geojson', watchOptions, function(geojson) {
          _create(geojson);
        });
      });
    },
  };
}]);

angular.module('leaflet-directive').directive('layercontrol', ["$filter", "$log", "leafletData", "leafletHelpers", function($filter, $log, leafletData, leafletHelpers) {

  return {
    restrict: 'E',
    scope: {
      icons: '=?',
      autoHideOpacity: '=?', // Hide other opacity controls when one is activated.
      showGroups: '=?', // Hide other opacity controls when one is activated.
      title: '@',
      baseTitle: '@',
      overlaysTitle: '@',
    },
    replace: true,
    transclude: false,
    require: '^leaflet',
    controller: ["$scope", "$element", "$sce", function($scope, $element, $sce) {
      $log.debug('[Angular Directive - Layers] layers', $scope, $element);
      var safeApply = leafletHelpers.safeApply;
      var isDefined = leafletHelpers.isDefined;
      angular.extend($scope, {
        baselayer: '',
        oldGroup: '',
        layerProperties: {},
        groupProperties: {},
        rangeIsSupported: leafletHelpers.rangeIsSupported(),
        changeBaseLayer: function(key, e) {
          leafletHelpers.safeApply($scope, function(scp) {
            scp.baselayer = key;
            leafletData.getMap().then(function(map) {
              leafletData.getLayers().then(function(leafletLayers) {
                if (map.hasLayer(leafletLayers.baselayers[key])) {
                  return;
                }

                for (var i in scp.layers.baselayers) {
                  scp.layers.baselayers[i].icon = scp.icons.unradio;
                  if (map.hasLayer(leafletLayers.baselayers[i])) {
                    map.removeLayer(leafletLayers.baselayers[i]);
                  }
                }

                map.addLayer(leafletLayers.baselayers[key]);
                scp.layers.baselayers[key].icon = $scope.icons.radio;
              });
            });
          });

          e.preventDefault();
        },

        moveLayer: function(ly, newIndex, e) {
          var delta = Object.keys($scope.layers.baselayers).length;
          if (newIndex >= (1 + delta) && newIndex <= ($scope.overlaysArray.length + delta)) {
            var oldLy;
            for (var key in $scope.layers.overlays) {
              if ($scope.layers.overlays[key].index === newIndex) {
                oldLy = $scope.layers.overlays[key];
                break;
              }
            }

            if (oldLy) {
              safeApply($scope, function() {
                oldLy.index = ly.index;
                ly.index = newIndex;
              });
            }
          }

          e.stopPropagation();
          e.preventDefault();
        },

        initIndex: function(layer, idx) {
          var delta = Object.keys($scope.layers.baselayers).length;
          layer.index = isDefined(layer.index) ? layer.index : idx + delta + 1;
        },

        initGroup: function(groupName) {
          $scope.groupProperties[groupName] = $scope.groupProperties[groupName] ? $scope.groupProperties[groupName] : {};
        },

        toggleOpacity: function(e, layer) {
          if (layer.visible) {
            if ($scope.autoHideOpacity && !$scope.layerProperties[layer.name].opacityControl) {
              for (var k in $scope.layerProperties) {
                $scope.layerProperties[k].opacityControl = false;
              }
            }

            $scope.layerProperties[layer.name].opacityControl = !$scope.layerProperties[layer.name].opacityControl;
          }

          e.stopPropagation();
          e.preventDefault();
        },

        toggleLegend: function(layer) {
          $scope.layerProperties[layer.name].showLegend = !$scope.layerProperties[layer.name].showLegend;
        },

        showLegend: function(layer) {
          return layer.legend && $scope.layerProperties[layer.name].showLegend;
        },

        unsafeHTML: function(html) {
          return $sce.trustAsHtml(html);
        },

        getOpacityIcon: function(layer) {
          return layer.visible && $scope.layerProperties[layer.name].opacityControl ? $scope.icons.close : $scope.icons.open;
        },

        getGroupIcon: function(group) {
          return group.visible ? $scope.icons.check : $scope.icons.uncheck;
        },

        changeOpacity: function(layer) {
          var op = $scope.layerProperties[layer.name].opacity;
          leafletData.getMap().then(function(map) {
            leafletData.getLayers().then(function(leafletLayers) {
              var ly;
              for (var k in $scope.layers.overlays) {
                if ($scope.layers.overlays[k] === layer) {
                  ly = leafletLayers.overlays[k];
                  break;
                }
              }

              if (map.hasLayer(ly)) {
                if (ly.setOpacity) {
                  ly.setOpacity(op / 100);
                }

                if (ly.getLayers && ly.eachLayer) {
                  ly.eachLayer(function(lay) {
                    if (lay.setOpacity) {
                      lay.setOpacity(op / 100);
                    }
                  });
                }
              }
            });
          });
        },

        changeGroupVisibility: function(groupName) {
          if (!isDefined($scope.groupProperties[groupName])) {
            return;
          }

          var visible = $scope.groupProperties[groupName].visible;
          for (var k in $scope.layers.overlays) {
            var layer = $scope.layers.overlays[k];
            if (layer.group === groupName) {
              layer.visible = visible;
            }
          }
        },
      });

      var div = $element.get(0);
      if (!L.Browser.touch) {
        L.DomEvent.disableClickPropagation(div);
        L.DomEvent.on(div, 'mousewheel', L.DomEvent.stopPropagation);
      } else {
        L.DomEvent.on(div, 'click', L.DomEvent.stopPropagation);
      }
    }],

    template:
    '<div class="angular-leaflet-control-layers" ng-show="overlaysArray.length">' +
        '<h4 ng-if="title">{{ title }}</h4>' +
        '<div class="lf-baselayers">' +
            '<h5 class="lf-title" ng-if="baseTitle">{{ baseTitle }}</h5>' +
            '<div class="lf-row" ng-repeat="(key, layer) in baselayersArray">' +
                '<label class="lf-icon-bl" ng-click="changeBaseLayer(key, $event)">' +
                    '<input class="leaflet-control-layers-selector" type="radio" name="lf-radio" ' +
                        'ng-show="false" ng-checked="baselayer === key" ng-value="key" /> ' +
                    '<i class="lf-icon lf-icon-radio" ng-class="layer.icon"></i>' +
                    '<div class="lf-text">{{layer.name}}</div>' +
                '</label>' +
            '</div>' +
        '</div>' +
        '<div class="lf-overlays">' +
            '<h5 class="lf-title" ng-if="overlaysTitle">{{ overlaysTitle }}</h5>' +
            '<div class="lf-container">' +
                '<div class="lf-row" ng-repeat="layer in (o = (overlaysArray | orderBy:\'index\':order))" ng-init="initIndex(layer, $index)">' +
                    '<label class="lf-icon-ol-group" ng-if="showGroups &amp;&amp; layer.group &amp;&amp; layer.group != o[$index-1].group">' +
                        '<input class="lf-control-layers-selector" type="checkbox" ng-show="false" ' +
                            'ng-change="changeGroupVisibility(layer.group)" ng-model="groupProperties[layer.group].visible"/> ' +
                        '<i class="lf-icon lf-icon-check" ng-class="getGroupIcon(groupProperties[layer.group])"></i>' +
                        '<div class="lf-text">{{ layer.group }}</div>' +
                    '</label>' +
                    '<label class="lf-icon-ol">' +
                        '<input class="lf-control-layers-selector" type="checkbox" ng-show="false" ng-model="layer.visible"/> ' +
                        '<i class="lf-icon lf-icon-check" ng-class="layer.icon"></i>' +
                        '<div class="lf-text">{{layer.name}}</div>' +
                    '</label>' +
                    '<div class="lf-icons">' +
                        '<i class="lf-icon lf-up" ng-class="icons.up" ng-click="moveLayer(layer, layer.index - orderNumber, $event)"></i> ' +
                        '<i class="lf-icon lf-down" ng-class="icons.down" ng-click="moveLayer(layer, layer.index + orderNumber, $event)"></i> ' +
                        '<i class="lf-icon lf-toggle-legend" ng-class="icons.toggleLegend" ng-if="layer.legend" ng-click="toggleLegend(layer)"></i> ' +
                        '<i class="lf-icon lf-open" ng-class="getOpacityIcon(layer)" ng-click="toggleOpacity($event, layer)"></i>' +
                    '</div>' +
                    '<div class="lf-legend" ng-if="showLegend(layer)" ng-bind-html="unsafeHTML(layer.legend)"></div>' +
                    '<div class="lf-opacity clearfix" ng-if="layer.visible &amp;&amp; layerProperties[layer.name].opacityControl">' +
                        '<label ng-if="rangeIsSupported" class="pull-left" style="width: 50%">0</label>' +
                        '<label ng-if="rangeIsSupported" class="pull-left text-right" style="width: 50%">100</label>' +
                        '<input ng-if="rangeIsSupported" class="clearfix" type="range" min="0" max="100" class="lf-opacity-control" ' +
                            'ng-model="layerProperties[layer.name].opacity" ng-change="changeOpacity(layer)"/>' +
                        '<h6 ng-if="!rangeIsSupported">Range is not supported in this browser</h6>' +
                    '</div>' +
                '</div>' +
            '</div>' +
        '</div>' +
    '</div>',
    link: function(scope, element, attrs, controller) {
      var isDefined = leafletHelpers.isDefined;
      var leafletScope = controller.getLeafletScope();
      var layers = leafletScope.layers;

      scope.$watch('icons', function() {
        var defaultIcons = {
          uncheck: 'fa fa-square-o',
          check: 'fa fa-check-square-o',
          radio: 'fa fa-dot-circle-o',
          unradio: 'fa fa-circle-o',
          up: 'fa fa-angle-up',
          down: 'fa fa-angle-down',
          open: 'fa fa-angle-double-down',
          close: 'fa fa-angle-double-up',
          toggleLegend: 'fa fa-pencil-square-o',
        };
        if (isDefined(scope.icons)) {
          angular.extend(defaultIcons, scope.icons);
          angular.extend(scope.icons, defaultIcons);
        } else {
          scope.icons = defaultIcons;
        }
      });

      // Setting layer stack order.
      attrs.order = (isDefined(attrs.order) && (attrs.order === 'normal' || attrs.order === 'reverse')) ? attrs.order : 'normal';
      scope.order = attrs.order === 'normal';
      scope.orderNumber = attrs.order === 'normal' ? -1 : 1;

      scope.layers = layers;
      controller.getMap().then(function(map) {
        leafletScope.$watch('layers.baselayers', function(newBaseLayers) {
          var baselayersArray = {};
          leafletData.getLayers().then(function(leafletLayers) {
            var key;
            for (key in newBaseLayers) {
              var layer = newBaseLayers[key];
              layer.icon = scope.icons[map.hasLayer(leafletLayers.baselayers[key]) ? 'radio' : 'unradio'];
              baselayersArray[key] = layer;
            }

            scope.baselayersArray = baselayersArray;
          });
        });

        leafletScope.$watch('layers.overlays', function(newOverlayLayers) {
          var overlaysArray = [];
          var groupVisibleCount = {};
          leafletData.getLayers().then(function(leafletLayers) {
            var key;
            for (key in newOverlayLayers) {
              var layer = newOverlayLayers[key];
              layer.icon = scope.icons[(layer.visible ? 'check' : 'uncheck')];
              overlaysArray.push(layer);
              if (!isDefined(scope.layerProperties[layer.name])) {
                scope.layerProperties[layer.name] = {
                  opacity: isDefined(layer.layerOptions.opacity) ? layer.layerOptions.opacity * 100 : 100,
                  opacityControl: false,
                  showLegend: true,
                };
              }

              if (isDefined(layer.group)) {
                if (!isDefined(scope.groupProperties[layer.group])) {
                  scope.groupProperties[layer.group] = {
                    visible: false,
                  };
                }

                groupVisibleCount[layer.group] = isDefined(groupVisibleCount[layer.group]) ? groupVisibleCount[layer.group] : {
                  count: 0,
                  visibles: 0,
                };
                groupVisibleCount[layer.group].count++;
                if (layer.visible) {
                  groupVisibleCount[layer.group].visibles++;
                }
              }

              if (isDefined(layer.index) && leafletLayers.overlays[key].setZIndex) {
                leafletLayers.overlays[key].setZIndex(newOverlayLayers[key].index);
              }
            }

            for (key in groupVisibleCount) {
              scope.groupProperties[key].visible = groupVisibleCount[key].visibles === groupVisibleCount[key].count;
            }

            scope.overlaysArray = overlaysArray;
          });
        }, true);
      });
    },
  };
}]);

angular.module('leaflet-directive').directive('layers', ["$log", "$q", "leafletData", "leafletHelpers", "leafletLayerHelpers", "leafletControlHelpers", function($log, $q, leafletData, leafletHelpers, leafletLayerHelpers, leafletControlHelpers) {

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: 'leaflet',
    controller: ["$scope", function($scope) {
      $scope._leafletLayers = $q.defer();
      this.getLayers = function() {
        return $scope._leafletLayers.promise;
      };
    }],

    link: function(scope, element, attrs, controller) {
      var isDefined = leafletHelpers.isDefined;
      var leafletLayers = {};
      var leafletScope  = controller.getLeafletScope();
      var layers = leafletScope.layers;
      var createLayer = leafletLayerHelpers.createLayer;
      var safeAddLayer = leafletLayerHelpers.safeAddLayer;
      var safeRemoveLayer = leafletLayerHelpers.safeRemoveLayer;
      var updateLayersControl = leafletControlHelpers.updateLayersControl;
      var isLayersControlVisible = false;

      controller.getMap().then(function(map) {

        // We have baselayers to add to the map
        scope._leafletLayers.resolve(leafletLayers);
        leafletData.setLayers(leafletLayers, attrs.id);

        leafletLayers.baselayers = {};
        leafletLayers.overlays = {};

        var mapId = attrs.id;

        // Setup all baselayers definitions
        var oneVisibleLayer = false;
        for (var layerName in layers.baselayers) {
          var newBaseLayer = createLayer(layers.baselayers[layerName]);
          if (!isDefined(newBaseLayer)) {
            delete layers.baselayers[layerName];
            continue;
          }

          leafletLayers.baselayers[layerName] = newBaseLayer;

          // Only add the visible layer to the map, layer control manages the addition to the map
          // of layers in its control
          if (layers.baselayers[layerName].top === true) {
            safeAddLayer(map, leafletLayers.baselayers[layerName]);
            oneVisibleLayer = true;
          }
        }

        // If there is no visible layer add first to the map
        if (!oneVisibleLayer && Object.keys(leafletLayers.baselayers).length > 0) {
          safeAddLayer(map, leafletLayers.baselayers[Object.keys(layers.baselayers)[0]]);
        }

        // Setup the Overlays
        for (layerName in layers.overlays) {
          //if (layers.overlays[layerName].type === 'cartodb') {
          //
          //}

          var newOverlayLayer = createLayer(layers.overlays[layerName]);
          if (!isDefined(newOverlayLayer)) {
            delete layers.overlays[layerName];
            continue;
          }

          leafletLayers.overlays[layerName] = newOverlayLayer;

          // Only add the visible overlays to the map
          if (layers.overlays[layerName].visible === true) {
            safeAddLayer(map, leafletLayers.overlays[layerName]);
          }
        }

        // Watch for the base layers
        leafletScope.$watch('layers.baselayers', function(newBaseLayers, oldBaseLayers) {
          if (angular.equals(newBaseLayers, oldBaseLayers)) {
            isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, newBaseLayers, layers.overlays, leafletLayers);
            return true;
          }

          // Delete layers from the array
          for (var name in leafletLayers.baselayers) {
            if (!isDefined(newBaseLayers[name]) || newBaseLayers[name].doRefresh) {
              // Remove from the map if it's on it
              if (map.hasLayer(leafletLayers.baselayers[name])) {
                map.removeLayer(leafletLayers.baselayers[name]);
              }

              delete leafletLayers.baselayers[name];

              if (newBaseLayers[name] && newBaseLayers[name].doRefresh) {
                newBaseLayers[name].doRefresh = false;
              }
            }
          }

          // add new layers
          for (var newName in newBaseLayers) {
            if (!isDefined(leafletLayers.baselayers[newName])) {
              var testBaseLayer = createLayer(newBaseLayers[newName]);
              if (isDefined(testBaseLayer)) {
                leafletLayers.baselayers[newName] = testBaseLayer;

                // Only add the visible layer to the map
                if (newBaseLayers[newName].top === true) {
                  safeAddLayer(map, leafletLayers.baselayers[newName]);
                }
              }
            } else {
              if (newBaseLayers[newName].top === true && !map.hasLayer(leafletLayers.baselayers[newName])) {
                safeAddLayer(map, leafletLayers.baselayers[newName]);
              } else if (newBaseLayers[newName].top === false && map.hasLayer(leafletLayers.baselayers[newName])) {
                map.removeLayer(leafletLayers.baselayers[newName]);
              }
            }
          }

          //we have layers, so we need to make, at least, one active
          var found = false;

          // search for an active layer
          for (var key in leafletLayers.baselayers) {
            if (map.hasLayer(leafletLayers.baselayers[key])) {
              found = true;
              break;
            }
          }

          // If there is no active layer make one active
          if (!found && Object.keys(leafletLayers.baselayers).length > 0) {
            safeAddLayer(map, leafletLayers.baselayers[Object.keys(leafletLayers.baselayers)[0]]);
          }

          // Only show the layers switch selector control if we have more than one baselayer + overlay
          isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, newBaseLayers, layers.overlays, leafletLayers);
        }, true);

        // Watch for the overlay layers
        leafletScope.$watch('layers.overlays', function(newOverlayLayers, oldOverlayLayers) {
          if (angular.equals(newOverlayLayers, oldOverlayLayers)) {
            isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, layers.baselayers, newOverlayLayers, leafletLayers);
            return true;
          }

          // Delete layers from the array
          for (var name in leafletLayers.overlays) {
            if (!isDefined(newOverlayLayers[name]) || newOverlayLayers[name].doRefresh) {
              // Remove from the map if it's on it
              if (map.hasLayer(leafletLayers.overlays[name])) {
                // Safe remove when ArcGIS layers is loading.
                var options = isDefined(newOverlayLayers[name]) ?
                    newOverlayLayers[name].layerOptions : null;
                safeRemoveLayer(map, leafletLayers.overlays[name], options);
              }

              // TODO: Depending on the layer type we will have to delete what's included on it
              delete leafletLayers.overlays[name];

              if (newOverlayLayers[name] && newOverlayLayers[name].doRefresh) {
                newOverlayLayers[name].doRefresh = false;
              }
            }
          }

          // add new overlays
          for (var newName in newOverlayLayers) {
            if (!isDefined(leafletLayers.overlays[newName])) {
              var testOverlayLayer = createLayer(newOverlayLayers[newName]);
              if (!isDefined(testOverlayLayer)) {
                // If the layer creation fails, continue to the next overlay
                continue;
              }

              leafletLayers.overlays[newName] = testOverlayLayer;
              if (newOverlayLayers[newName].visible === true) {
                safeAddLayer(map, leafletLayers.overlays[newName]);
              }
            } else {
              // check for the .visible property to hide/show overLayers
              if (newOverlayLayers[newName].visible && !map.hasLayer(leafletLayers.overlays[newName])) {
                safeAddLayer(map, leafletLayers.overlays[newName]);
              } else if (newOverlayLayers[newName].visible === false && map.hasLayer(leafletLayers.overlays[newName])) {
                // Safe remove when ArcGIS layers is loading.
                safeRemoveLayer(map, leafletLayers.overlays[newName], newOverlayLayers[newName].layerOptions);
              }
            }

            //refresh heatmap data if present
            if (newOverlayLayers[newName].visible && map._loaded && newOverlayLayers[newName].data && newOverlayLayers[newName].type === 'heatmap') {
              leafletLayers.overlays[newName].setData(newOverlayLayers[newName].data);
              leafletLayers.overlays[newName].update();
            }
          }

          // Only add the layers switch selector control if we have more than one baselayer + overlay
          isLayersControlVisible = updateLayersControl(map, mapId, isLayersControlVisible, layers.baselayers, newOverlayLayers, leafletLayers);
        }, true);
      });
    },
  };
}]);

angular.module('leaflet-directive').directive('legend', ["$log", "$http", "leafletHelpers", "leafletLegendHelpers", function($log, $http, leafletHelpers, leafletLegendHelpers) {

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: 'leaflet',

    link: function(scope, element, attrs, controller) {

      var isArray = leafletHelpers.isArray;
      var isDefined = leafletHelpers.isDefined;
      var isFunction = leafletHelpers.isFunction;
      var leafletScope = controller.getLeafletScope();
      var legend = leafletScope.legend;

      var legendClass;
      var position;
      var leafletLegend;
      var type;

      leafletScope.$watch('legend', function(newLegend) {

        if (isDefined(newLegend)) {

          legendClass = newLegend.legendClass ? newLegend.legendClass : 'legend';

          position = newLegend.position || 'bottomright';

          // default to arcgis
          type = newLegend.type || 'arcgis';
        }

      }, true);

      controller.getMap().then(function(map) {

        leafletScope.$watch('legend', function(newLegend) {

          if (!isDefined(newLegend)) {

            if (isDefined(leafletLegend)) {
              leafletLegend.removeFrom(map);
              leafletLegend = null;
            }

            return;
          }

          if (!isDefined(newLegend.url) && (type === 'arcgis') && (!isArray(newLegend.colors) || !isArray(newLegend.labels) || newLegend.colors.length !== newLegend.labels.length)) {

            $log.warn('[AngularJS - Leaflet] legend.colors and legend.labels must be set.');

            return;
          }

          if (isDefined(newLegend.url)) {

            $log.info('[AngularJS - Leaflet] loading legend service.');

            return;
          }

          if (isDefined(leafletLegend)) {
            leafletLegend.removeFrom(map);
            leafletLegend = null;
          }

          leafletLegend = L.control({
            position: position,
          });
          if (type === 'arcgis') {
            leafletLegend.onAdd = leafletLegendHelpers.getOnAddArrayLegend(newLegend, legendClass);
          }

          leafletLegend.addTo(map);

        });

        leafletScope.$watch('legend.url', function(newURL) {

          if (!isDefined(newURL)) {
            return;
          }

          $http.get(newURL)
                            .success(function(legendData) {

                              if (isDefined(leafletLegend)) {

                                leafletLegendHelpers.updateLegend(leafletLegend.getContainer(), legendData, type, newURL);

                              } else {

                                leafletLegend = L.control({
                                  position: position,
                                });
                                leafletLegend.onAdd = leafletLegendHelpers.getOnAddLegend(legendData, legendClass, type, newURL);
                                leafletLegend.addTo(map);
                              }

                              if (isDefined(legend.loadedData) && isFunction(legend.loadedData)) {
                                legend.loadedData();
                              }
                            })
                            .error(function() {
                              $log.warn('[AngularJS - Leaflet] legend.url not loaded.');
                            });
        });

      });
    },
  };
}]);

angular.module('leaflet-directive').directive('markers',
    ["$log", "$rootScope", "$q", "leafletData", "leafletHelpers", "leafletMapDefaults", "leafletMarkersHelpers", "leafletMarkerEvents", "leafletIterators", "leafletWatchHelpers", "leafletDirectiveControlsHelpers", function($log, $rootScope, $q, leafletData, leafletHelpers, leafletMapDefaults,
              leafletMarkersHelpers, leafletMarkerEvents, leafletIterators, leafletWatchHelpers,
              leafletDirectiveControlsHelpers) {
      //less terse vars to helpers
      var isDefined = leafletHelpers.isDefined;
      var errorHeader = leafletHelpers.errorHeader;
      var Helpers = leafletHelpers;
      var isString = leafletHelpers.isString;
      var addMarkerWatcher = leafletMarkersHelpers.addMarkerWatcher;
      var updateMarker = leafletMarkersHelpers.updateMarker;
      var listenMarkerEvents = leafletMarkersHelpers.listenMarkerEvents;
      var addMarkerToGroup = leafletMarkersHelpers.addMarkerToGroup;
      var createMarker = leafletMarkersHelpers.createMarker;
      var deleteMarker = leafletMarkersHelpers.deleteMarker;
      var $it = leafletIterators;
      var _markersWatchOptions = leafletHelpers.watchOptions;
      var maybeWatch = leafletWatchHelpers.maybeWatch;
      var extendDirectiveControls = leafletDirectiveControlsHelpers.extend;

      var _getLMarker = function(leafletMarkers, name, maybeLayerName) {
        if (!Object.keys(leafletMarkers).length) return;
        if (maybeLayerName && isString(maybeLayerName)) {
          if (!leafletMarkers[maybeLayerName] || !Object.keys(leafletMarkers[maybeLayerName]).length)
              return;
          return leafletMarkers[maybeLayerName][name];
        }

        return leafletMarkers[name];
      };

      var _setLMarker = function(lObject, leafletMarkers, name, maybeLayerName) {
        if (maybeLayerName && isString(maybeLayerName)) {
          if (!isDefined(leafletMarkers[maybeLayerName]))
              leafletMarkers[maybeLayerName] = {};
          leafletMarkers[maybeLayerName][name] = lObject;
        } else
            leafletMarkers[name] = lObject;
        return lObject;
      };

      var _maybeAddMarkerToLayer = function(layerName, layers, model, marker, doIndividualWatch, map) {

        if (!isString(layerName)) {
          $log.error(errorHeader + ' A layername must be a string');
          return false;
        }

        if (!isDefined(layers)) {
          $log.error(errorHeader + ' You must add layers to the directive if the markers are going to use this functionality.');
          return false;
        }

        if (!isDefined(layers.overlays) || !isDefined(layers.overlays[layerName])) {
          $log.error(errorHeader + ' A marker can only be added to a layer of type "group"');
          return false;
        }

        var layerGroup = layers.overlays[layerName];
        if (!(layerGroup instanceof L.LayerGroup || layerGroup instanceof L.FeatureGroup)) {
          $log.error(errorHeader + ' Adding a marker to an overlay needs a overlay of the type "group" or "featureGroup"');
          return false;
        }

        // The marker goes to a correct layer group, so first of all we add it
        layerGroup.addLayer(marker);

        // The marker is automatically added to the map depending on the visibility
        // of the layer, so we only have to open the popup if the marker is in the map
        if (!doIndividualWatch && map.hasLayer(marker) && model.focus === true) {
          marker.openPopup();
        }

        return true;
      };

      //TODO: move to leafletMarkersHelpers??? or make a new class/function file (leafletMarkersHelpers is large already)
      var _addMarkers = function(mapId, markersToRender, oldModels, map, layers, leafletMarkers, leafletScope,
                                 watchOptions, maybeLayerName, skips) {
        for (var newName in markersToRender) {
          if (skips[newName])
              continue;

          if (newName.search('-') !== -1) {
            $log.error('The marker can\'t use a "-" on his key name: "' + newName + '".');
            continue;
          }

          var model = Helpers.copy(markersToRender[newName]);
          var pathToMarker = Helpers.getObjectDotPath(maybeLayerName ? [maybeLayerName, newName] : [newName]);
          var maybeLMarker = _getLMarker(leafletMarkers, newName, maybeLayerName);
          if (!isDefined(maybeLMarker)) {
            //(nmccready) very important to not have model changes when lObject is changed
            //this might be desirable in some cases but it causes two-way binding to lObject which is not ideal
            //if it is left as the reference then all changes from oldModel vs newModel are ignored
            //see _destroy (where modelDiff becomes meaningless if we do not copy here)
            var marker = createMarker(model);
            var layerName = (model ? model.layer : undefined) || maybeLayerName; //original way takes pref
            if (!isDefined(marker)) {
              $log.error(errorHeader + ' Received invalid data on the marker ' + newName + '.');
              continue;
            }

            _setLMarker(marker, leafletMarkers, newName, maybeLayerName);

            // Bind message
            if (isDefined(model.message)) {
              marker.bindPopup(model.message, model.popupOptions);
            }

            // Add the marker to a cluster group if needed
            if (isDefined(model.group)) {
              var groupOptions = isDefined(model.groupOption) ? model.groupOption : null;
              addMarkerToGroup(marker, model.group, groupOptions, map);
            }

            // Show label if defined
            if (Helpers.LabelPlugin.isLoaded() && isDefined(model.label) && isDefined(model.label.message)) {
              marker.bindLabel(model.label.message, model.label.options);
            }

            // Check if the marker should be added to a layer
            if (isDefined(model) && (isDefined(model.layer) || isDefined(maybeLayerName))) {

              var pass = _maybeAddMarkerToLayer(layerName, layers, model, marker,
                  watchOptions.individual.doWatch, map);
              if (!pass)
                  continue; //something went wrong move on in the loop
            } else if (!isDefined(model.group)) {
              // We do not have a layer attr, so the marker goes to the map layer
              map.addLayer(marker);
              if (!watchOptions.individual.doWatch && model.focus === true) {
                marker.openPopup();
              }
            }

            if (watchOptions.individual.doWatch) {
              addMarkerWatcher(marker, pathToMarker, leafletScope, layers, map,
                  watchOptions.individual.isDeep);
            }

            listenMarkerEvents(marker, model, leafletScope, watchOptions.individual.doWatch, map);
            leafletMarkerEvents.bindEvents(mapId, marker, pathToMarker, model, leafletScope, layerName);
          }          else {
            var oldModel = isDefined(oldModel) ? oldModels[newName] : undefined;
            updateMarker(model, oldModel, maybeLMarker, pathToMarker, leafletScope, layers, map);
          }
        }
      };

      var _seeWhatWeAlreadyHave = function(markerModels, oldMarkerModels, lMarkers, isEqual, cb) {
        var hasLogged = false;
        var equals = false;
        var oldMarker;
        var newMarker;

        var doCheckOldModel =  isDefined(oldMarkerModels);
        for (var name in lMarkers) {
          if (!hasLogged) {
            $log.debug(errorHeader + '[markers] destroy: ');
            hasLogged = true;
          }

          if (doCheckOldModel) {
            //might want to make the option (in watch options) to disable deep checking
            //ie the options to only check !== (reference check) instead of angular.equals (slow)
            newMarker = markerModels[name];
            oldMarker = oldMarkerModels[name];
            equals = angular.equals(newMarker, oldMarker) && isEqual;
          }

          if (!isDefined(markerModels) ||
              !Object.keys(markerModels).length ||
              !isDefined(markerModels[name]) ||
              !Object.keys(markerModels[name]).length ||
              equals) {
            if (cb && Helpers.isFunction(cb))
                cb(newMarker, oldMarker, name);
          }
        }
      };

      var _destroy = function(markerModels, oldMarkerModels, lMarkers, map, layers) {
        _seeWhatWeAlreadyHave(markerModels, oldMarkerModels, lMarkers, false,
            function(newMarker, oldMarker, lMarkerName) {
              $log.debug(errorHeader + '[marker] is deleting marker: ' + lMarkerName);
              deleteMarker(lMarkers[lMarkerName], map, layers);
              delete lMarkers[lMarkerName];
            });
      };

      var _getNewModelsToSkipp =  function(newModels, oldModels, lMarkers) {
        var skips = {};
        _seeWhatWeAlreadyHave(newModels, oldModels, lMarkers, true,
            function(newMarker, oldMarker, lMarkerName) {
              $log.debug(errorHeader + '[marker] is already rendered, marker: ' + lMarkerName);
              skips[lMarkerName] = newMarker;
            });

        return skips;
      };

      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: ['leaflet', '?layers'],

        link: function(scope, element, attrs, controller) {
          var mapController = controller[0];
          var leafletScope  = mapController.getLeafletScope();

          mapController.getMap().then(function(map) {
            var leafletMarkers = {};
            var getLayers;

            // If the layers attribute is used, we must wait until the layers are created
            if (isDefined(controller[1])) {
              getLayers = controller[1].getLayers;
            } else {
              getLayers = function() {
                var deferred = $q.defer();
                deferred.resolve();
                return deferred.promise;
              };
            }

            var watchOptions = leafletScope.markersWatchOptions || _markersWatchOptions;

            // backwards compat
            if (isDefined(attrs.watchMarkers))
                watchOptions.doWatch = watchOptions.individual.doWatch =
                    (!isDefined(attrs.watchMarkers) || Helpers.isTruthy(attrs.watchMarkers));

            var isNested = (isDefined(attrs.markersNested) && Helpers.isTruthy(attrs.markersNested));

            getLayers().then(function(layers) {
              var _clean = function(models, oldModels) {
                if (isNested) {
                  $it.each(models, function(markerToMaybeDel, layerName) {
                    var oldModel = isDefined(oldModel) ? oldModels[layerName] : undefined;
                    _destroy(markerToMaybeDel, oldModel, leafletMarkers[layerName], map, layers);
                  });

                  return;
                }

                _destroy(models, oldModels, leafletMarkers, map, layers);
              };

              var _create = function(models, oldModels) {
                _clean(models, oldModels);
                var skips = null;
                if (isNested) {
                  $it.each(models, function(markersToAdd, layerName) {
                    var oldModel = isDefined(oldModel) ? oldModels[layerName] : undefined;
                    skips = _getNewModelsToSkipp(models[layerName], oldModel, leafletMarkers[layerName]);
                    _addMarkers(attrs.id, markersToAdd, oldModels, map, layers, leafletMarkers, leafletScope,
                        watchOptions, layerName, skips);
                  });

                  return;
                }

                skips = _getNewModelsToSkipp(models, oldModels, leafletMarkers);
                _addMarkers(attrs.id, models, oldModels, map, layers, leafletMarkers, leafletScope,
                    watchOptions, undefined, skips);
              };

              extendDirectiveControls(attrs.id, 'markers', _create, _clean);
              leafletData.setMarkers(leafletMarkers, attrs.id);

              maybeWatch(leafletScope, 'markers', watchOptions, function(newMarkers, oldMarkers) {
                _create(newMarkers, oldMarkers);
              });
            });
          });
        },
      };
    }]);

angular.module('leaflet-directive').directive('maxbounds', ["$log", "leafletMapDefaults", "leafletBoundsHelpers", "leafletHelpers", function($log, leafletMapDefaults, leafletBoundsHelpers, leafletHelpers) {

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: 'leaflet',

    link: function(scope, element, attrs, controller) {
      var leafletScope  = controller.getLeafletScope();
      var isValidBounds = leafletBoundsHelpers.isValidBounds;
      var isNumber = leafletHelpers.isNumber;

      controller.getMap().then(function(map) {
        leafletScope.$watch('maxbounds', function(maxbounds) {
          if (!isValidBounds(maxbounds)) {
            // Unset any previous maxbounds
            map.setMaxBounds();
            return;
          }

          var leafletBounds = leafletBoundsHelpers.createLeafletBounds(maxbounds);
          if (isNumber(maxbounds.pad)) {
            leafletBounds = leafletBounds.pad(maxbounds.pad);
          }

          map.setMaxBounds(leafletBounds);
          if (!attrs.center && !attrs.lfCenter) {
            map.fitBounds(leafletBounds);
          }
        });
      });
    },
  };
}]);

angular.module('leaflet-directive').directive('paths', ["$log", "$q", "leafletData", "leafletMapDefaults", "leafletHelpers", "leafletPathsHelpers", "leafletPathEvents", function($log, $q, leafletData, leafletMapDefaults, leafletHelpers, leafletPathsHelpers, leafletPathEvents) {

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: ['leaflet', '?layers'],

    link: function(scope, element, attrs, controller) {
      var mapController = controller[0];
      var isDefined = leafletHelpers.isDefined;
      var isString = leafletHelpers.isString;
      var leafletScope  = mapController.getLeafletScope();
      var paths     = leafletScope.paths;
      var createPath = leafletPathsHelpers.createPath;
      var bindPathEvents = leafletPathEvents.bindPathEvents;
      var setPathOptions = leafletPathsHelpers.setPathOptions;

      mapController.getMap().then(function(map) {
        var defaults = leafletMapDefaults.getDefaults(attrs.id);
        var getLayers;

        // If the layers attribute is used, we must wait until the layers are created
        if (isDefined(controller[1])) {
          getLayers = controller[1].getLayers;
        } else {
          getLayers = function() {
            var deferred = $q.defer();
            deferred.resolve();
            return deferred.promise;
          };
        }

        if (!isDefined(paths)) {
          return;
        }

        getLayers().then(function(layers) {

          var leafletPaths = {};
          leafletData.setPaths(leafletPaths, attrs.id);

          // Should we watch for every specific marker on the map?
          var shouldWatch = (!isDefined(attrs.watchPaths) || attrs.watchPaths === 'true');

          // Function for listening every single path once created
          var watchPathFn = function(leafletPath, name) {
            var clearWatch = leafletScope.$watch('paths["' + name + '"]', function(pathData, old) {
              if (!isDefined(pathData)) {
                if (isDefined(old.layer)) {
                  for (var i in layers.overlays) {
                    var overlay = layers.overlays[i];
                    overlay.removeLayer(leafletPath);
                  }
                }

                map.removeLayer(leafletPath);
                clearWatch();
                return;
              }

              setPathOptions(leafletPath, pathData.type, pathData);
            }, true);
          };

          leafletScope.$watchCollection('paths', function(newPaths) {

            // Delete paths (by name) from the array
            for (var name in leafletPaths) {
              if (!isDefined(newPaths[name])) {
                map.removeLayer(leafletPaths[name]);
                delete leafletPaths[name];
              }
            }

            // Create the new paths
            for (var newName in newPaths) {
              if (newName.search('\\$') === 0) {
                continue;
              }

              if (newName.search('-') !== -1) {
                $log.error('[AngularJS - Leaflet] The path name "' + newName + '" is not valid. It must not include "-" and a number.');
                continue;
              }

              if (!isDefined(leafletPaths[newName])) {
                var pathData = newPaths[newName];
                var newPath = createPath(newName, newPaths[newName], defaults);

                // bind popup if defined
                if (isDefined(newPath) && isDefined(pathData.message)) {
                  newPath.bindPopup(pathData.message, pathData.popupOptions);
                }

                // Show label if defined
                if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(pathData.label) && isDefined(pathData.label.message)) {
                  newPath.bindLabel(pathData.label.message, pathData.label.options);
                }

                // Check if the marker should be added to a layer
                if (isDefined(pathData) && isDefined(pathData.layer)) {

                  if (!isString(pathData.layer)) {
                    $log.error('[AngularJS - Leaflet] A layername must be a string');
                    continue;
                  }

                  if (!isDefined(layers)) {
                    $log.error('[AngularJS - Leaflet] You must add layers to the directive if the markers are going to use this functionality.');
                    continue;
                  }

                  if (!isDefined(layers.overlays) || !isDefined(layers.overlays[pathData.layer])) {
                    $log.error('[AngularJS - Leaflet] A path can only be added to a layer of type "group"');
                    continue;
                  }

                  var layerGroup = layers.overlays[pathData.layer];
                  if (!(layerGroup instanceof L.LayerGroup || layerGroup instanceof L.FeatureGroup)) {
                    $log.error('[AngularJS - Leaflet] Adding a path to an overlay needs a overlay of the type "group" or "featureGroup"');
                    continue;
                  }

                  // Listen for changes on the new path
                  leafletPaths[newName] = newPath;

                  // The path goes to a correct layer group, so first of all we add it
                  layerGroup.addLayer(newPath);

                  if (shouldWatch) {
                    watchPathFn(newPath, newName);
                  } else {
                    setPathOptions(newPath, pathData.type, pathData);
                  }
                } else if (isDefined(newPath)) {
                  // Listen for changes on the new path
                  leafletPaths[newName] = newPath;
                  map.addLayer(newPath);

                  if (shouldWatch) {
                    watchPathFn(newPath, newName);
                  } else {
                    setPathOptions(newPath, pathData.type, pathData);
                  }
                }

                bindPathEvents(attrs.id, newPath, newName, pathData, leafletScope);
              }
            }
          });
        });
      });
    },
  };
}]);

angular.module('leaflet-directive').directive('tiles', ["$log", "leafletData", "leafletMapDefaults", "leafletHelpers", function($log, leafletData, leafletMapDefaults, leafletHelpers) {

  return {
    restrict: 'A',
    scope: false,
    replace: false,
    require: 'leaflet',

    link: function(scope, element, attrs, controller) {
      var isDefined = leafletHelpers.isDefined;
      var leafletScope  = controller.getLeafletScope();
      var tiles = leafletScope.tiles;

      if (!isDefined(tiles) ||  !isDefined(tiles.url)) {
        $log.warn('[AngularJS - Leaflet] The \'tiles\' definition doesn\'t have the \'url\' property.');
        return;
      }

      controller.getMap().then(function(map) {
        var defaults = leafletMapDefaults.getDefaults(attrs.id);
        var tileLayerObj;
        leafletScope.$watch('tiles', function(tiles, oldtiles) {
          var tileLayerOptions = defaults.tileLayerOptions;
          var tileLayerUrl = defaults.tileLayer;

          // If no valid tiles are in the scope, remove the last layer
          if (!isDefined(tiles.url) && isDefined(tileLayerObj)) {
            map.removeLayer(tileLayerObj);
            return;
          }

          // No leafletTiles object defined yet
          if (!isDefined(tileLayerObj)) {
            if (isDefined(tiles.options)) {
              angular.copy(tiles.options, tileLayerOptions);
            }

            if (isDefined(tiles.url)) {
              tileLayerUrl = tiles.url;
            }

            if (tiles.type === 'wms') {
              tileLayerObj = L.tileLayer.wms(tileLayerUrl, tileLayerOptions);
            } else {
              tileLayerObj = L.tileLayer(tileLayerUrl, tileLayerOptions);
            }

            tileLayerObj.addTo(map);
            leafletData.setTiles(tileLayerObj, attrs.id);
            return;
          }

          // If the options of the tilelayer is changed, we need to redraw the layer
          if (isDefined(tiles.url) && isDefined(tiles.options) &&
              (tiles.type !== oldtiles.type || !angular.equals(tiles.options, tileLayerOptions))) {
            map.removeLayer(tileLayerObj);
            tileLayerOptions = defaults.tileLayerOptions;
            angular.copy(tiles.options, tileLayerOptions);
            tileLayerUrl = tiles.url;

            if (tiles.type === 'wms') {
              tileLayerObj = L.tileLayer.wms(tileLayerUrl, tileLayerOptions);
            } else {
              tileLayerObj = L.tileLayer(tileLayerUrl, tileLayerOptions);
            }

            tileLayerObj.addTo(map);
            leafletData.setTiles(tileLayerObj, attrs.id);
            return;
          }

          // Only the URL of the layer is changed, update the tiles object
          if (isDefined(tiles.url)) {
            tileLayerObj.setUrl(tiles.url);
          }
        }, true);
      });
    },
  };
}]);

/*
    Create multiple similar directives for watchOptions to support directiveControl
    instead. (when watches are disabled)
    NgAnnotate does not work here due to the functional creation
*/
['markers', 'geojson'].forEach(function(name) {
  angular.module('leaflet-directive').directive(name + 'WatchOptions', [
      '$log', '$rootScope', '$q', 'leafletData', 'leafletHelpers',
        function($log, $rootScope, $q, leafletData, leafletHelpers) {

          var isDefined = leafletHelpers.isDefined,
              errorHeader = leafletHelpers.errorHeader,
              isObject = leafletHelpers.isObject,
              _watchOptions = leafletHelpers.watchOptions;

          return {
            restrict: 'A',
            scope: false,
            replace: false,
            require: ['leaflet'],

            link: function(scope, element, attrs, controller) {
              var mapController = controller[0],
                  leafletScope = mapController.getLeafletScope();

              mapController.getMap().then(function() {
                if (isDefined(scope[name + 'WatchOptions'])) {
                  if (isObject(scope[name + 'WatchOptions']))
                      angular.extend(_watchOptions, scope[name + 'WatchOptions']);
                  else
                      $log.error(errorHeader + '[' + name + 'WatchOptions] is not an object');
                  leafletScope[name + 'WatchOptions'] = _watchOptions;
                }
              });
            },
          };
        },]);
});

angular.module('leaflet-directive')
.factory('LeafletEventsHelpersFactory', ["$rootScope", "$q", "$log", "leafletHelpers", function($rootScope, $q, $log, leafletHelpers) {
  var safeApply = leafletHelpers.safeApply;
  var isDefined = leafletHelpers.isDefined;
  var isObject = leafletHelpers.isObject;
  var isArray = leafletHelpers.isArray;
  var errorHeader = leafletHelpers.errorHeader;

  var EventsHelper = function(rootBroadcastName, lObjectType) {
    this.rootBroadcastName = rootBroadcastName;
    $log.debug('LeafletEventsHelpersFactory: lObjectType: ' + lObjectType + 'rootBroadcastName: ' + rootBroadcastName);

    //used to path/key out certain properties based on the type , "markers", "geojson"
    this.lObjectType = lObjectType;
  };

  EventsHelper.prototype.getAvailableEvents = function() {return [];};

  /*
   argument: name: Note this can be a single string or dot notation
   Example:
   markerModel : {
   m1: { lat:_, lon: _}
   }
   //would yield name of
   name = "m1"

   If nested:
   markerModel : {
   cars: {
   m1: { lat:_, lon: _}
   }
   }
   //would yield name of
   name = "cars.m1"
   */
  EventsHelper.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName, extra) {
    var _this = this;

    maybeMapId = maybeMapId || '';
    if (maybeMapId)
      maybeMapId = '.' + maybeMapId;

    return function(e) {
      var broadcastName = _this.rootBroadcastName + maybeMapId + '.' + eventName;
      $log.debug(broadcastName);
      _this.fire(leafletScope, broadcastName, logic, e, e.target || lObject, model, name, layerName, extra);
    };
  };

  EventsHelper.prototype.fire = function(scope, broadcastName, logic, event, lObject, model, modelName, layerName) {
    // Safely broadcast the event
    safeApply(scope, function() {
      var toSend = {
        leafletEvent: event,
        leafletObject: lObject,
        modelName: modelName,
        model: model,
      };
      if (isDefined(layerName))
          angular.extend(toSend, {layerName: layerName});

      if (logic === 'emit') {
        scope.$emit(broadcastName, toSend);
      } else {
        $rootScope.$broadcast(broadcastName, toSend);
      }
    });
  };

  EventsHelper.prototype.bindEvents = function(maybeMapId, lObject, name, model, leafletScope, layerName, extra) {
    var events = [];
    var logic = 'emit';
    var _this = this;

    if (!isDefined(leafletScope.eventBroadcast)) {
      // Backward compatibility, if no event-broadcast attribute, all events are broadcasted
      events = this.getAvailableEvents();
    } else if (!isObject(leafletScope.eventBroadcast)) {
      // Not a valid object
      $log.error(errorHeader + 'event-broadcast must be an object check your model.');
    } else {
      // We have a possible valid object
      if (!isDefined(leafletScope.eventBroadcast[_this.lObjectType])) {
        // We do not have events enable/disable do we do nothing (all enabled by default)
        events = this.getAvailableEvents();
      } else if (!isObject(leafletScope.eventBroadcast[_this.lObjectType])) {
        // Not a valid object
        $log.warn(errorHeader + 'event-broadcast.' + [_this.lObjectType]  + ' must be an object check your model.');
      } else {
        // We have a possible valid map object
        // Event propadation logic
        if (isDefined(leafletScope.eventBroadcast[this.lObjectType].logic)) {
          // We take care of possible propagation logic
          if (leafletScope.eventBroadcast[_this.lObjectType].logic !== 'emit' &&
              leafletScope.eventBroadcast[_this.lObjectType].logic !== 'broadcast')
                  $log.warn(errorHeader + 'Available event propagation logic are: \'emit\' or \'broadcast\'.');
        }

        // Enable / Disable
        var eventsEnable = false;
        var eventsDisable = false;
        if (isDefined(leafletScope.eventBroadcast[_this.lObjectType].enable) &&
            isArray(leafletScope.eventBroadcast[_this.lObjectType].enable))
                eventsEnable = true;
        if (isDefined(leafletScope.eventBroadcast[_this.lObjectType].disable) &&
            isArray(leafletScope.eventBroadcast[_this.lObjectType].disable))
                eventsDisable = true;

        if (eventsEnable && eventsDisable) {
          // Both are active, this is an error
          $log.warn(errorHeader + 'can not enable and disable events at the same time');
        } else if (!eventsEnable && !eventsDisable) {
          // Both are inactive, this is an error
          $log.warn(errorHeader + 'must enable or disable events');
        } else {
          // At this point the object is OK, lets enable or disable events
          if (eventsEnable) {
            // Enable events
            leafletScope.eventBroadcast[this.lObjectType].enable.forEach(function(eventName) {
              // Do we have already the event enabled?
              if (events.indexOf(eventName) !== -1) {
                // Repeated event, this is an error
                $log.warn(errorHeader + 'This event ' + eventName + ' is already enabled');
              } else {
                // Does the event exists?
                if (_this.getAvailableEvents().indexOf(eventName) === -1) {
                  // The event does not exists, this is an error
                  $log.warn(errorHeader + 'This event ' + eventName + ' does not exist');
                } else {
                  // All ok enable the event
                  events.push(eventName);
                }
              }
            });
          } else {
            // Disable events
            events = this.getAvailableEvents();
            leafletScope.eventBroadcast[_this.lObjectType].disable.forEach(function(eventName) {
              var index = events.indexOf(eventName);
              if (index === -1) {
                // The event does not exist
                $log.warn(errorHeader + 'This event ' + eventName + ' does not exist or has been already disabled');

              } else {
                events.splice(index, 1);
              }
            });
          }
        }
      }
    }

    events.forEach(function(eventName) {
      lObject.on(eventName, _this.genDispatchEvent(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName, extra));
    });

    return logic;
  };

  return EventsHelper;
}])
.service('leafletEventsHelpers', ["LeafletEventsHelpersFactory", function(LeafletEventsHelpersFactory) {
  return new LeafletEventsHelpersFactory();
}]);

angular.module('leaflet-directive')
.factory('leafletGeoJsonEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "LeafletEventsHelpersFactory", "leafletData", function($rootScope, $q, $log, leafletHelpers,
  LeafletEventsHelpersFactory, leafletData) {
  var safeApply = leafletHelpers.safeApply;
  var EventsHelper = LeafletEventsHelpersFactory;

  var GeoJsonEvents = function() {
      EventsHelper.call(this, 'leafletDirectiveGeoJson', 'geojson');
    };

  GeoJsonEvents.prototype =  new EventsHelper();

  GeoJsonEvents.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName, extra) {
    var base = EventsHelper.prototype.genDispatchEvent.call(this, maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName);
    var _this = this;

    return function(e) {
      if (eventName === 'mouseout') {
        if (extra.resetStyleOnMouseout) {
          leafletData.getGeoJSON(extra.mapId)
                    .then(function(leafletGeoJSON) {
                      //this is broken on nested needs to traverse or user layerName (nested)
                      var lobj = layerName ? leafletGeoJSON[layerName] : leafletGeoJSON;
                      lobj.resetStyle(e.target);
                    });

        }

        safeApply(leafletScope, function() {
          $rootScope.$broadcast(_this.rootBroadcastName + '.mouseout', e);
        });
      }

      base(e); //common
    };
  };

  GeoJsonEvents.prototype.getAvailableEvents = function() { return [
      'click',
      'dblclick',
      'mouseover',
      'mouseout',
      ];
  };

  return new GeoJsonEvents();
}]);

angular.module('leaflet-directive')
.factory('leafletLabelEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "LeafletEventsHelpersFactory", function($rootScope, $q, $log, leafletHelpers, LeafletEventsHelpersFactory) {
  var Helpers = leafletHelpers;
  var EventsHelper = LeafletEventsHelpersFactory;

  var LabelEvents = function() {
          EventsHelper.call(this, 'leafletDirectiveLabel', 'markers');
        };

  LabelEvents.prototype =  new EventsHelper();

  LabelEvents.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) {
    var markerName = name.replace('markers.', '');
    return EventsHelper.prototype
        .genDispatchEvent.call(this, maybeMapId, eventName, logic, leafletScope, lObject, markerName, model, layerName);
  };

  LabelEvents.prototype.getAvailableEvents = function() {
    return [
        'click',
        'dblclick',
        'mousedown',
        'mouseover',
        'mouseout',
        'contextmenu',
    ];
  };

  LabelEvents.prototype.genEvents = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) {
    var _this = this;
    var labelEvents = this.getAvailableEvents();
    var scopeWatchName = Helpers.getObjectArrayPath('markers.' + name);
    labelEvents.forEach(function(eventName) {
      lObject.label.on(eventName, _this.genDispatchEvent(
          maybeMapId, eventName, logic, leafletScope, lObject.label, scopeWatchName, model, layerName));
    });
  };

  LabelEvents.prototype.bindEvents = function() {};

  return new LabelEvents();
}]);

angular.module('leaflet-directive')
.factory('leafletMapEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "leafletEventsHelpers", "leafletIterators", function($rootScope, $q, $log, leafletHelpers, leafletEventsHelpers, leafletIterators) {
  var isDefined = leafletHelpers.isDefined;
  var fire = leafletEventsHelpers.fire;

  var _getAvailableMapEvents = function() {
    return [
        'click',
        'dblclick',
        'mousedown',
        'mouseup',
        'mouseover',
        'mouseout',
        'mousemove',
        'contextmenu',
        'focus',
        'blur',
        'preclick',
        'load',
        'unload',
        'viewreset',
        'movestart',
        'move',
        'moveend',
        'dragstart',
        'drag',
        'dragend',
        'zoomstart',
        'zoomanim',
        'zoomend',
        'zoomlevelschange',
        'resize',
        'autopanstart',
        'layeradd',
        'layerremove',
        'baselayerchange',
        'overlayadd',
        'overlayremove',
        'locationfound',
        'locationerror',
        'popupopen',
        'popupclose',
        'draw:created',
        'draw:edited',
        'draw:deleted',
        'draw:drawstart',
        'draw:drawstop',
        'draw:editstart',
        'draw:editstop',
        'draw:deletestart',
        'draw:deletestop',
    ];
  };

  var _genDispatchMapEvent = function(scope, eventName, logic, maybeMapId) {
    if (maybeMapId)
      maybeMapId = maybeMapId + '.';
    return function(e) {
      // Put together broadcast name
      var broadcastName = 'leafletDirectiveMap.' + maybeMapId + eventName;
      $log.debug(broadcastName);

      // Safely broadcast the event
      fire(scope, broadcastName, logic, e, e.target, scope);
    };
  };

  var _notifyCenterChangedToBounds = function(scope) {
    scope.$broadcast('boundsChanged');
  };

  var _notifyCenterUrlHashChanged = function(scope, map, attrs, search) {
    if (!isDefined(attrs.urlHashCenter)) {
      return;
    }

    var center = map.getCenter();
    var centerUrlHash = (center.lat).toFixed(4) + ':' + (center.lng).toFixed(4) + ':' + map.getZoom();
    if (!isDefined(search.c) || search.c !== centerUrlHash) {
      //$log.debug("notified new center...");
      scope.$emit('centerUrlHash', centerUrlHash);
    }
  };

  var _addEvents =  function(map, mapEvents, contextName, scope, logic) {
    leafletIterators.each(mapEvents, function(eventName) {
      var context = {};
      context[contextName] = eventName;
      map.on(eventName, _genDispatchMapEvent(scope, eventName, logic, map._container.id || ''), context);
    });
  };

  return {
    getAvailableMapEvents: _getAvailableMapEvents,
    genDispatchMapEvent: _genDispatchMapEvent,
    notifyCenterChangedToBounds: _notifyCenterChangedToBounds,
    notifyCenterUrlHashChanged: _notifyCenterUrlHashChanged,
    addEvents: _addEvents,
  };
}]);

angular.module('leaflet-directive')
.factory('leafletMarkerEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "LeafletEventsHelpersFactory", "leafletLabelEvents", function($rootScope, $q, $log, leafletHelpers, LeafletEventsHelpersFactory, leafletLabelEvents) {
  var safeApply = leafletHelpers.safeApply;
  var isDefined = leafletHelpers.isDefined;
  var Helpers = leafletHelpers;
  var lblHelp = leafletLabelEvents;
  var EventsHelper = LeafletEventsHelpersFactory;

  var MarkerEvents = function() {
      EventsHelper.call(this, 'leafletDirectiveMarker', 'markers');
    };

  MarkerEvents.prototype = new EventsHelper();

  MarkerEvents.prototype.genDispatchEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) {
    var handle = EventsHelper.prototype
        .genDispatchEvent.call(this, maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName);
    return function(e) {
      // Broadcast old marker click name for backwards compatibility
      if (eventName === 'click') {
        safeApply(leafletScope, function() {
          $rootScope.$broadcast('leafletDirectiveMarkersClick', name);
        });
      } else if (eventName === 'dragend') {
        safeApply(leafletScope, function() {
          model.lat = lObject.getLatLng().lat;
          model.lng = lObject.getLatLng().lng;
        });

        if (model.message && model.focus === true) {
          lObject.openPopup();
        }
      }

      handle(e); //common
    };
  };

  MarkerEvents.prototype.getAvailableEvents = function() { return [
      'click',
      'dblclick',
      'mousedown',
      'mouseover',
      'mouseout',
      'contextmenu',
      'dragstart',
      'drag',
      'dragend',
      'move',
      'remove',
      'popupopen',
      'popupclose',
      'touchend',
      'touchstart',
      'touchmove',
      'touchcancel',
      'touchleave',
      ];
  };

  MarkerEvents.prototype.bindEvents = function(maybeMapId, lObject, name, model, leafletScope, layerName) {
      var logic = EventsHelper.prototype.bindEvents.call(this, maybeMapId, lObject, name, model, leafletScope, layerName);

      if (Helpers.LabelPlugin.isLoaded() && isDefined(lObject.label)) {
        lblHelp.genEvents(maybeMapId, name, logic, leafletScope, lObject, model, layerName);
      }
    };

  return new MarkerEvents();
}]);

angular.module('leaflet-directive')
.factory('leafletPathEvents', ["$rootScope", "$q", "$log", "leafletHelpers", "leafletLabelEvents", "leafletEventsHelpers", function($rootScope, $q, $log, leafletHelpers, leafletLabelEvents, leafletEventsHelpers) {
  var isDefined = leafletHelpers.isDefined;
  var isObject = leafletHelpers.isObject;
  var Helpers = leafletHelpers;
  var errorHeader = leafletHelpers.errorHeader;
  var lblHelp = leafletLabelEvents;
  var fire = leafletEventsHelpers.fire;

  /*
  TODO (nmccready) This EventsHelper needs to be derrived from leafletEventsHelpers to elminate copy and paste code.
  */

  var _genDispatchPathEvent = function(maybeMapId, eventName, logic, leafletScope, lObject, name, model, layerName) {
    maybeMapId = maybeMapId || '';

    if (maybeMapId)
      maybeMapId = '.' + maybeMapId;

    return function(e) {
      var broadcastName = 'leafletDirectivePath' + maybeMapId + '.' + eventName;
      $log.debug(broadcastName);
      fire(leafletScope, broadcastName, logic, e, e.target || lObject, model, name, layerName);
    };
  };

  var _bindPathEvents = function(maybeMapId, lObject, name, model, leafletScope) {
    var pathEvents = [];
    var i;
    var eventName;
    var logic = 'broadcast';

    if (!isDefined(leafletScope.eventBroadcast)) {
      // Backward compatibility, if no event-broadcast attribute, all events are broadcasted
      pathEvents = _getAvailablePathEvents();
    } else if (!isObject(leafletScope.eventBroadcast)) {
      // Not a valid object
      $log.error(errorHeader + 'event-broadcast must be an object check your model.');
    } else {
      // We have a possible valid object
      if (!isDefined(leafletScope.eventBroadcast.path)) {
        // We do not have events enable/disable do we do nothing (all enabled by default)
        pathEvents = _getAvailablePathEvents();
      } else if (isObject(leafletScope.eventBroadcast.paths)) {
        // Not a valid object
        $log.warn(errorHeader + 'event-broadcast.path must be an object check your model.');
      } else {
        // We have a possible valid map object
        // Event propadation logic
        if (leafletScope.eventBroadcast.path.logic !== undefined && leafletScope.eventBroadcast.path.logic !== null) {
          // We take care of possible propagation logic
          if (leafletScope.eventBroadcast.path.logic !== 'emit' && leafletScope.eventBroadcast.path.logic !== 'broadcast') {
            // This is an error
            $log.warn(errorHeader + 'Available event propagation logic are: \'emit\' or \'broadcast\'.');
          } else if (leafletScope.eventBroadcast.path.logic === 'emit') {
            logic = 'emit';
          }
        }

        // Enable / Disable
        var pathEventsEnable = false;
        var pathEventsDisable = false;
        if (leafletScope.eventBroadcast.path.enable !== undefined && leafletScope.eventBroadcast.path.enable !== null) {
          if (typeof leafletScope.eventBroadcast.path.enable === 'object') {
            pathEventsEnable = true;
          }
        }

        if (leafletScope.eventBroadcast.path.disable !== undefined && leafletScope.eventBroadcast.path.disable !== null) {
          if (typeof leafletScope.eventBroadcast.path.disable === 'object') {
            pathEventsDisable = true;
          }
        }

        if (pathEventsEnable && pathEventsDisable) {
          // Both are active, this is an error
          $log.warn(errorHeader + 'can not enable and disable events at the same time');
        } else if (!pathEventsEnable && !pathEventsDisable) {
          // Both are inactive, this is an error
          $log.warn(errorHeader + 'must enable or disable events');
        } else {
          // At this point the path object is OK, lets enable or disable events
          if (pathEventsEnable) {
            // Enable events
            for (i = 0; i < leafletScope.eventBroadcast.path.enable.length; i++) {
              eventName = leafletScope.eventBroadcast.path.enable[i];

              // Do we have already the event enabled?
              if (pathEvents.indexOf(eventName) !== -1) {
                // Repeated event, this is an error
                $log.warn(errorHeader + 'This event ' + eventName + ' is already enabled');
              } else {
                // Does the event exists?
                if (_getAvailablePathEvents().indexOf(eventName) === -1) {
                  // The event does not exists, this is an error
                  $log.warn(errorHeader + 'This event ' + eventName + ' does not exist');
                } else {
                  // All ok enable the event
                  pathEvents.push(eventName);
                }
              }
            }
          } else {
            // Disable events
            pathEvents = _getAvailablePathEvents();
            for (i = 0; i < leafletScope.eventBroadcast.path.disable.length; i++) {
              eventName = leafletScope.eventBroadcast.path.disable[i];
              var index = pathEvents.indexOf(eventName);
              if (index === -1) {
                // The event does not exist
                $log.warn(errorHeader + 'This event ' + eventName + ' does not exist or has been already disabled');

              } else {
                pathEvents.splice(index, 1);
              }
            }
          }
        }
      }
    }

    for (i = 0; i < pathEvents.length; i++) {
      eventName = pathEvents[i];
      lObject.on(eventName, _genDispatchPathEvent(maybeMapId, eventName, logic, leafletScope, pathEvents, name));
    }

    if (Helpers.LabelPlugin.isLoaded() && isDefined(lObject.label)) {
      lblHelp.genEvents(maybeMapId, name, logic, leafletScope, lObject, model);
    }
  };

  var _getAvailablePathEvents = function() {
    return [
        'click',
        'dblclick',
        'mousedown',
        'mouseover',
        'mouseout',
        'contextmenu',
        'add',
        'remove',
        'popupopen',
        'popupclose',
    ];
  };

  return {
    getAvailablePathEvents: _getAvailablePathEvents,
    bindPathEvents: _bindPathEvents,
  };
}]);

}(angular));