(function () {
  'use strict';
  angular.module('leaflet-directive', []).directive('leaflet', [
    '$q',
    'leafletData',
    'leafletMapDefaults',
    'leafletHelpers',
    'leafletEvents',
    function ($q, leafletData, leafletMapDefaults, leafletHelpers, leafletEvents) {
      var _leafletMap;
      return {
        restrict: 'EA',
        replace: true,
        scope: {
          center: '=center',
          defaults: '=defaults',
          maxbounds: '=maxbounds',
          bounds: '=bounds',
          markers: '=markers',
          legend: '=legend',
          geojson: '=geojson',
          paths: '=paths',
          tiles: '=tiles',
          layers: '=layers',
          controls: '=controls',
          eventBroadcast: '=eventBroadcast'
        },
        template: '<div class="angular-leaflet-map"></div>',
        controller: [
          '$scope',
          function ($scope) {
            _leafletMap = $q.defer();
            this.getMap = function () {
              return _leafletMap.promise;
            };
            this.getLeafletScope = function () {
              return $scope;
            };
          }
        ],
        link: function (scope, element, attrs) {
          var isDefined = leafletHelpers.isDefined, defaults = leafletMapDefaults.setDefaults(scope.defaults, attrs.id), genDispatchMapEvent = leafletEvents.genDispatchMapEvent, mapEvents = leafletEvents.getAvailableMapEvents();
          // Set width and height if they are defined
          if (isDefined(attrs.width)) {
            if (isNaN(attrs.width)) {
              element.css('width', attrs.width);
            } else {
              element.css('width', attrs.width + 'px');
            }
          }
          if (isDefined(attrs.height)) {
            if (isNaN(attrs.height)) {
              element.css('height', attrs.height);
            } else {
              element.css('height', attrs.height + 'px');
            }
          }
          // Create the Leaflet Map Object with the options
          var map = new L.Map(element[0], leafletMapDefaults.getMapCreationDefaults(attrs.id));
          _leafletMap.resolve(map);
          if (!isDefined(attrs.center)) {
            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';
            for (var i = 0; i < mapEvents.length; i++) {
              var eventName = mapEvents[i];
              map.on(eventName, genDispatchMapEvent(scope, eventName, logic), { eventName: eventName });
            }
          }
          // Resolve the map object to the promises
          map.whenReady(function () {
            leafletData.setMap(map, attrs.id);
          });
          scope.$on('$destroy', function () {
            leafletData.unresolveMap(attrs.id);
          });
        }
      };
    }
  ]);
  angular.module('leaflet-directive').directive('center', [
    '$log',
    '$q',
    '$location',
    'leafletMapDefaults',
    'leafletHelpers',
    'leafletBoundsHelpers',
    'leafletEvents',
    function ($log, $q, $location, leafletMapDefaults, leafletHelpers, leafletBoundsHelpers, leafletEvents) {
      var isDefined = leafletHelpers.isDefined, isNumber = leafletHelpers.isNumber, isSameCenterOnMap = leafletHelpers.isSameCenterOnMap, safeApply = leafletHelpers.safeApply, isValidCenter = leafletHelpers.isValidCenter, isEmpty = leafletHelpers.isEmpty, isUndefinedOrEmpty = leafletHelpers.isUndefinedOrEmpty;
      var shouldInitializeMapWithBounds = function (bounds, center) {
        return isDefined(bounds) && !isEmpty(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(), centerModel = leafletScope.center;
          controller.getMap().then(function (map) {
            var defaults = leafletMapDefaults.getDefaults(attrs.id);
            if (attrs.center.search('-') !== -1) {
              $log.error('The "center" variable can\'t use a "-" on his key name: "' + attrs.center + '".');
              map.setView([
                defaults.center.lat,
                defaults.center.lng
              ], defaults.center.zoom);
              return;
            } else if (shouldInitializeMapWithBounds(leafletScope.bounds, centerModel)) {
              map.fitBounds(leafletBoundsHelpers.createLeafletBounds(leafletScope.bounds));
              centerModel = map.getCenter();
              safeApply(leafletScope, function (scope) {
                scope.center = {
                  lat: map.getCenter().lat,
                  lng: map.getCenter().lng,
                  zoom: map.getZoom(),
                  autoDiscover: false
                };
              });
              safeApply(leafletScope, function (scope) {
                var mapBounds = map.getBounds();
                var newScopeBounds = {
                    northEast: {
                      lat: mapBounds._northEast.lat,
                      lng: mapBounds._northEast.lng
                    },
                    southWest: {
                      lat: mapBounds._southWest.lat,
                      lng: mapBounds._southWest.lng
                    }
                  };
                scope.bounds = newScopeBounds;
              });
            } else if (!isDefined(centerModel)) {
              $log.error('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, 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);
                  scope.center = {
                    lat: urlCenter.lat,
                    lng: urlCenter.lng,
                    zoom: urlCenter.zoom
                  };
                }
              });
            }
            leafletScope.$watch('center', function (center) {
              //$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('[AngularJS - Leaflet] 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);
              map.setView([
                center.lat,
                center.lng
              ], center.zoom);
              leafletEvents.notifyCenterChangedToBounds(leafletScope, map);
            }, true);
            map.whenReady(function () {
              mapReady = true;
            });
            map.on('moveend', function () {
              // Resolve the center after the first map position
              _leafletCenter.resolve();
              leafletEvents.notifyCenterUrlHashChanged(leafletScope, map, attrs, $location.search());
              //$log.debug("updated center on map...");
              if (isSameCenterOnMap(centerModel, map)) {
                //$log.debug("same center in model, no need to update again.");
                return;
              }
              safeApply(leafletScope, function (scope) {
                //$log.debug("updating center model...", map.getCenter(), map.getZoom());
                scope.center = {
                  lat: map.getCenter().lat,
                  lng: map.getCenter().lng,
                  zoom: map.getZoom(),
                  autoDiscover: false
                };
                leafletEvents.notifyCenterChangedToBounds(leafletScope, map);
              });
            });
            if (centerModel.autoDiscover === true) {
              map.on('locationerror', function () {
                $log.warn('[AngularJS - Leaflet] The Geolocation API is unauthorized on this page.');
                if (isValidCenter(centerModel)) {
                  map.setView([
                    centerModel.lat,
                    centerModel.lng
                  ], centerModel.zoom);
                  leafletEvents.notifyCenterChangedToBounds(leafletScope, map);
                } else {
                  map.setView([
                    defaults.center.lat,
                    defaults.center.lng
                  ], defaults.center.zoom);
                  leafletEvents.notifyCenterChangedToBounds(leafletScope, map);
                }
              });
            }
          });
        }
      };
    }
  ]);
  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, leafletScope = controller.getLeafletScope(), 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) {
              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;
                }
                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) && !angular.equals(tiles.options, tileLayerOptions)) {
                map.removeLayer(tileLayerObj);
                tileLayerOptions = defaults.tileLayerOptions;
                angular.copy(tiles.options, tileLayerOptions);
                tileLayerUrl = tiles.url;
                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);
          });
        }
      };
    }
  ]);
  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, isDefined = leafletHelpers.isDefined, isFunction = leafletHelpers.isFunction, leafletScope = controller.getLeafletScope(), legend = leafletScope.legend;
          var legendClass = legend.legendClass ? legend.legendClass : 'legend';
          var position = legend.position || 'bottomright';
          var leafletLegend;
          controller.getMap().then(function (map) {
            if (!isDefined(legend.url) && (!isArray(legend.colors) || !isArray(legend.labels) || legend.colors.length !== legend.labels.length)) {
              $log.warn('[AngularJS - Leaflet] legend.colors and legend.labels must be set.');
            } else if (isDefined(legend.url)) {
              $log.info('[AngularJS - Leaflet] loading arcgis legend service.');
            } else {
              // TODO: Watch array legend.
              leafletLegend = L.control({ position: position });
              leafletLegend.onAdd = leafletLegendHelpers.getOnAddArrayLegend(legend, legendClass);
              leafletLegend.addTo(map);
            }
            leafletScope.$watch('legend.url', function (newURL) {
              if (!isDefined(newURL)) {
                return;
              }
              $http.get(newURL).success(function (legendData) {
                if (isDefined(leafletLegend)) {
                  leafletLegendHelpers.updateArcGISLegend(leafletLegend.getContainer(), legendData);
                } else {
                  leafletLegend = L.control({ position: position });
                  leafletLegend.onAdd = leafletLegendHelpers.getOnAddArcGISLegend(legendData, legendClass);
                  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('geojson', [
    '$log',
    '$rootScope',
    'leafletData',
    'leafletHelpers',
    function ($log, $rootScope, leafletData, leafletHelpers) {
      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: 'leaflet',
        link: function (scope, element, attrs, controller) {
          var safeApply = leafletHelpers.safeApply, isDefined = leafletHelpers.isDefined, leafletScope = controller.getLeafletScope(), leafletGeoJSON = {};
          controller.getMap().then(function (map) {
            leafletScope.$watch('geojson', function (geojson) {
              if (isDefined(leafletGeoJSON) && map.hasLayer(leafletGeoJSON)) {
                map.removeLayer(leafletGeoJSON);
              }
              if (!(isDefined(geojson) && isDefined(geojson.data))) {
                return;
              }
              var resetStyleOnMouseout = geojson.resetStyleOnMouseout, onEachFeature = geojson.onEachFeature;
              if (!onEachFeature) {
                onEachFeature = function (feature, layer) {
                  if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(geojson.label)) {
                    layer.bindLabel(feature.properties.description);
                  }
                  layer.on({
                    mouseover: function (e) {
                      safeApply(leafletScope, function () {
                        geojson.selected = feature;
                        $rootScope.$broadcast('leafletDirectiveMap.geojsonMouseover', e);
                      });
                    },
                    mouseout: function (e) {
                      if (resetStyleOnMouseout) {
                        leafletGeoJSON.resetStyle(e.target);
                      }
                      safeApply(leafletScope, function () {
                        geojson.selected = undefined;
                        $rootScope.$broadcast('leafletDirectiveMap.geojsonMouseout', e);
                      });
                    },
                    click: function (e) {
                      safeApply(leafletScope, function () {
                        geojson.selected = feature;
                        $rootScope.$broadcast('leafletDirectiveMap.geojsonClick', geojson.selected, e);
                      });
                    }
                  });
                };
              }
              geojson.options = {
                style: geojson.style,
                filter: geojson.filter,
                onEachFeature: onEachFeature,
                pointToLayer: geojson.pointToLayer
              };
              leafletGeoJSON = L.geoJson(geojson.data, geojson.options);
              leafletData.setGeoJSON(leafletGeoJSON);
              leafletGeoJSON.addTo(map);
            });
          });
        }
      };
    }
  ]);
  angular.module('leaflet-directive').directive('layers', [
    '$log',
    '$q',
    'leafletData',
    'leafletHelpers',
    'leafletLayerHelpers',
    'leafletControlHelpers',
    function ($log, $q, leafletData, leafletHelpers, leafletLayerHelpers, leafletControlHelpers) {
      var _leafletLayers;
      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: 'leaflet',
        controller: function () {
          _leafletLayers = $q.defer();
          this.getLayers = function () {
            return _leafletLayers.promise;
          };
        },
        link: function (scope, element, attrs, controller) {
          var isDefined = leafletHelpers.isDefined, leafletLayers = {}, leafletScope = controller.getLeafletScope(), layers = leafletScope.layers, createLayer = leafletLayerHelpers.createLayer, updateLayersControl = leafletControlHelpers.updateLayersControl, isLayersControlVisible = false;
          controller.getMap().then(function (map) {
            // Do we have a baselayers property?
            if (!isDefined(layers) || !isDefined(layers.baselayers) || Object.keys(layers.baselayers).length === 0) {
              // No baselayers property
              $log.error('[AngularJS - Leaflet] At least one baselayer has to be defined');
              return;
            }
            // We have baselayers to add to the map
            _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) {
                map.addLayer(leafletLayers.baselayers[layerName]);
                oneVisibleLayer = true;
              }
            }
            // If there is no visible layer add first to the map
            if (!oneVisibleLayer && Object.keys(leafletLayers.baselayers).length > 0) {
              map.addLayer(leafletLayers.baselayers[Object.keys(layers.baselayers)[0]]);
            }
            // Setup the Overlays
            for (layerName in layers.overlays) {
              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) {
                map.addLayer(leafletLayers.overlays[layerName]);
              }
            }
            // Watch for the base layers
            leafletScope.$watch('layers.baselayers', function (newBaseLayers) {
              // Delete layers from the array
              for (var name in leafletLayers.baselayers) {
                if (!isDefined(newBaseLayers[name])) {
                  // Remove from the map if it's on it
                  if (map.hasLayer(leafletLayers.baselayers[name])) {
                    map.removeLayer(leafletLayers.baselayers[name]);
                  }
                  delete leafletLayers.baselayers[name];
                }
              }
              // 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) {
                      map.addLayer(leafletLayers.baselayers[newName]);
                    }
                  }
                }
              }
              if (Object.keys(leafletLayers.baselayers).length === 0) {
                $log.error('[AngularJS - Leaflet] At least one baselayer has to be defined');
                return;
              }
              //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) {
                map.addLayer(leafletLayers.baselayers[Object.keys(layers.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) {
              // Delete layers from the array
              for (var name in leafletLayers.overlays) {
                if (!isDefined(newOverlayLayers[name])) {
                  // Remove from the map if it's on it
                  if (map.hasLayer(leafletLayers.overlays[name])) {
                    map.removeLayer(leafletLayers.overlays[name]);
                  }
                  // TODO: Depending on the layer type we will have to delete what's included on it
                  delete leafletLayers.overlays[name];
                }
              }
              // add new overlays
              for (var newName in newOverlayLayers) {
                if (!isDefined(leafletLayers.overlays[newName])) {
                  var testOverlayLayer = createLayer(newOverlayLayers[newName]);
                  if (isDefined(testOverlayLayer)) {
                    leafletLayers.overlays[newName] = testOverlayLayer;
                    if (newOverlayLayers[newName].visible === true) {
                      map.addLayer(leafletLayers.overlays[newName]);
                    }
                  }
                }
                // check for the .visible property to hide/show overLayers
                if (newOverlayLayers[newName].visible && !map.hasLayer(leafletLayers.overlays[newName])) {
                  map.addLayer(leafletLayers.overlays[newName]);
                } else if (newOverlayLayers[newName].visible === false && map.hasLayer(leafletLayers.overlays[newName])) {
                  map.removeLayer(leafletLayers.overlays[newName]);
                }
              }
              // 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('bounds', [
    '$log',
    '$timeout',
    'leafletHelpers',
    'leafletBoundsHelpers',
    function ($log, $timeout, leafletHelpers, leafletBoundsHelpers) {
      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: [
          'leaflet',
          'center'
        ],
        link: function (scope, element, attrs, controller) {
          var isDefined = leafletHelpers.isDefined, createLeafletBounds = leafletBoundsHelpers.createLeafletBounds, leafletScope = controller[0].getLeafletScope(), mapController = controller[0];
          var emptyBounds = function (bounds) {
            if (bounds._southWest.lat === 0 && bounds._southWest.lng === 0 && bounds._northEast.lat === 0 && bounds._northEast.lng === 0) {
              return true;
            }
            return false;
          };
          mapController.getMap().then(function (map) {
            leafletScope.$on('boundsChanged', function (event) {
              var scope = event.currentScope;
              var bounds = map.getBounds();
              //$log.debug('updated map bounds...', bounds);
              if (emptyBounds(bounds)) {
                return;
              }
              var newScopeBounds = {
                  northEast: {
                    lat: bounds._northEast.lat,
                    lng: bounds._northEast.lng
                  },
                  southWest: {
                    lat: bounds._southWest.lat,
                    lng: bounds._southWest.lng
                  }
                };
              if (!angular.equals(scope.bounds, newScopeBounds)) {
                //$log.debug('Need to update scope bounds.');
                scope.bounds = newScopeBounds;
              }
            });
            leafletScope.$watch('bounds', function (bounds) {
              //$log.debug('updated bounds...', bounds);
              if (!isDefined(bounds)) {
                $log.error('[AngularJS - Leaflet] Invalid bounds');
                return;
              }
              var leafletBounds = createLeafletBounds(bounds);
              if (leafletBounds && !map.getBounds().equals(leafletBounds)) {
                //$log.debug('Need to update map bounds.');
                map.fitBounds(leafletBounds);
              }
            }, true);
          });
        }
      };
    }
  ]);
  angular.module('leaflet-directive').directive('markers', [
    '$log',
    '$rootScope',
    '$q',
    'leafletData',
    'leafletHelpers',
    'leafletMapDefaults',
    'leafletMarkersHelpers',
    'leafletEvents',
    function ($log, $rootScope, $q, leafletData, leafletHelpers, leafletMapDefaults, leafletMarkersHelpers, leafletEvents) {
      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: [
          'leaflet',
          '?layers'
        ],
        link: function (scope, element, attrs, controller) {
          var mapController = controller[0], Helpers = leafletHelpers, isDefined = leafletHelpers.isDefined, isString = leafletHelpers.isString, leafletScope = mapController.getLeafletScope(), markers = leafletScope.markers, deleteMarker = leafletMarkersHelpers.deleteMarker, addMarkerWatcher = leafletMarkersHelpers.addMarkerWatcher, listenMarkerEvents = leafletMarkersHelpers.listenMarkerEvents, addMarkerToGroup = leafletMarkersHelpers.addMarkerToGroup, bindMarkerEvents = leafletEvents.bindMarkerEvents, createMarker = leafletMarkersHelpers.createMarker;
          mapController.getMap().then(function (map) {
            var leafletMarkers = {}, 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(markers)) {
              return;
            }
            getLayers().then(function (layers) {
              leafletData.setMarkers(leafletMarkers, attrs.id);
              leafletScope.$watch('markers', function (newMarkers) {
                // Delete markers from the array
                for (var name in leafletMarkers) {
                  if (!isDefined(newMarkers) || !isDefined(newMarkers[name])) {
                    deleteMarker(leafletMarkers[name], map, layers);
                    delete leafletMarkers[name];
                  }
                }
                // add new markers
                for (var newName in newMarkers) {
                  if (newName.search('-') !== -1) {
                    $log.error('The marker can\'t use a "-" on his key name: "' + newName + '".');
                    continue;
                  }
                  if (!isDefined(leafletMarkers[newName])) {
                    var markerData = newMarkers[newName];
                    var marker = createMarker(markerData);
                    if (!isDefined(marker)) {
                      $log.error('[AngularJS - Leaflet] Received invalid data on the marker ' + newName + '.');
                      continue;
                    }
                    leafletMarkers[newName] = marker;
                    // Bind message
                    if (isDefined(markerData.message)) {
                      marker.bindPopup(markerData.message, markerData.popupOptions);
                    }
                    // Add the marker to a cluster group if needed
                    if (isDefined(markerData.group)) {
                      addMarkerToGroup(marker, markerData.group, map);
                    }
                    // Show label if defined
                    if (Helpers.LabelPlugin.isLoaded() && isDefined(markerData.label) && isDefined(markerData.label.message)) {
                      marker.bindLabel(markerData.label.message, markerData.label.options);
                    }
                    // Check if the marker should be added to a layer
                    if (isDefined(markerData) && isDefined(markerData.layer)) {
                      if (!isString(markerData.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[markerData.layer])) {
                        $log.error('[AngularJS - Leaflet] A marker can only be added to a layer of type "group"');
                        continue;
                      }
                      var layerGroup = layers.overlays[markerData.layer];
                      if (!(layerGroup instanceof L.LayerGroup)) {
                        $log.error('[AngularJS - Leaflet] Adding a marker to an overlay needs a overlay of the type "group"');
                        continue;
                      }
                      // 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();
                      }  // Add the marker to the map if it hasn't been added to a layer or to a group
                    } else if (!isDefined(markerData.group)) {
                      // We do not have a layer attr, so the marker goes to the map layer
                      map.addLayer(marker);
                      if (markerData.focus === true) {
                        marker.openPopup();
                      }
                      if (Helpers.LabelPlugin.isLoaded() && isDefined(markerData.label) && isDefined(markerData.label.options) && markerData.label.options.noHide === true) {
                        marker.showLabel();
                      }
                    }
                    // Should we watch for every specific marker on the map?
                    var shouldWatch = !isDefined(attrs.watchMarkers) || attrs.watchMarkers === 'true';
                    if (shouldWatch) {
                      addMarkerWatcher(marker, newName, leafletScope, layers, map);
                      listenMarkerEvents(marker, markerData, leafletScope);
                    }
                    bindMarkerEvents(marker, newName, markerData, leafletScope);
                  }
                }
              }, true);
            });
          });
        }
      };
    }
  ]);
  angular.module('leaflet-directive').directive('paths', [
    '$log',
    'leafletData',
    'leafletMapDefaults',
    'leafletHelpers',
    'leafletPathsHelpers',
    'leafletEvents',
    function ($log, leafletData, leafletMapDefaults, leafletHelpers, leafletPathsHelpers, leafletEvents) {
      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: 'leaflet',
        link: function (scope, element, attrs, controller) {
          var isDefined = leafletHelpers.isDefined, leafletScope = controller.getLeafletScope(), paths = leafletScope.paths, createPath = leafletPathsHelpers.createPath, bindPathEvents = leafletEvents.bindPathEvents, setPathOptions = leafletPathsHelpers.setPathOptions;
          controller.getMap().then(function (map) {
            var defaults = leafletMapDefaults.getDefaults(attrs.id);
            if (!isDefined(paths)) {
              return;
            }
            var leafletPaths = {};
            leafletData.setPaths(leafletPaths, attrs.id);
            // Function for listening every single path once created
            var watchPathFn = function (leafletPath, name) {
              var clearWatch = leafletScope.$watch('paths.' + name, function (pathData) {
                  if (!isDefined(pathData)) {
                    map.removeLayer(leafletPath);
                    clearWatch();
                    return;
                  }
                  setPathOptions(leafletPath, pathData.type, pathData);
                }, true);
            };
            leafletScope.$watch('paths', function (newPaths) {
              // Create the new paths
              for (var newName in newPaths) {
                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);
                  }
                  // Show label if defined
                  if (leafletHelpers.LabelPlugin.isLoaded() && isDefined(pathData.label) && isDefined(pathData.label.message)) {
                    newPath.bindLabel(pathData.label.message, pathData.label.options);
                  }
                  // Listen for changes on the new path
                  if (isDefined(newPath)) {
                    leafletPaths[newName] = newPath;
                    map.addLayer(newPath);
                    watchPathFn(newPath, newName);
                  }
                  bindPathEvents(newPath, newName, pathData, leafletScope);
                }
              }
              // Delete paths (by name) from the array
              for (var name in leafletPaths) {
                if (!isDefined(newPaths[name])) {
                  delete leafletPaths[name];
                }
              }
            }, true);
          });
        }
      };
    }
  ]);
  angular.module('leaflet-directive').directive('controls', [
    '$log',
    'leafletHelpers',
    function ($log, leafletHelpers) {
      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: '?^leaflet',
        link: function (scope, element, attrs, controller) {
          if (!controller) {
            return;
          }
          var isDefined = leafletHelpers.isDefined, leafletScope = controller.getLeafletScope(), controls = leafletScope.controls;
          controller.getMap().then(function (map) {
            if (isDefined(L.Control.Draw) && isDefined(controls.draw)) {
              var drawnItems = new L.FeatureGroup();
              map.addLayer(drawnItems);
              var options = { edit: { featureGroup: drawnItems } };
              angular.extend(options, controls.draw.options);
              var drawControl = new L.Control.Draw(options);
              map.addControl(drawControl);
            }
            if (isDefined(controls.custom)) {
              for (var i in controls.custom) {
                map.addControl(controls.custom[i]);
              }
            }
          });
        }
      };
    }
  ]);
  angular.module('leaflet-directive').directive('eventBroadcast', [
    '$log',
    '$rootScope',
    'leafletHelpers',
    'leafletEvents',
    function ($log, $rootScope, leafletHelpers, leafletEvents) {
      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: 'leaflet',
        link: function (scope, element, attrs, controller) {
          var isObject = leafletHelpers.isObject, leafletScope = controller.getLeafletScope(), eventBroadcast = leafletScope.eventBroadcast, availableMapEvents = leafletEvents.getAvailableMapEvents(), genDispatchMapEvent = leafletEvents.genDispatchMapEvent;
          controller.getMap().then(function (map) {
            var mapEvents = [];
            var i;
            var eventName;
            var logic = 'broadcast';
            if (isObject(eventBroadcast)) {
              // We have a possible valid object
              if (eventBroadcast.map === undefined || eventBroadcast.map === null) {
                // We do not have events enable/disable do we do nothing (all enabled by default)
                mapEvents = availableMapEvents;
              } else if (typeof eventBroadcast.map !== 'object') {
                // 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 !== undefined && eventBroadcast.map.logic !== null) {
                  // We take care of possible propagation 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 if (eventBroadcast.map.logic === 'emit') {
                    logic = 'emit';
                  }
                }
                // Enable / Disable
                var mapEventsEnable = false, mapEventsDisable = false;
                if (eventBroadcast.map.enable !== undefined && eventBroadcast.map.enable !== null) {
                  if (typeof eventBroadcast.map.enable === 'object') {
                    mapEventsEnable = true;
                  }
                }
                if (eventBroadcast.map.disable !== undefined && eventBroadcast.map.disable !== null) {
                  if (typeof eventBroadcast.map.disable === 'object') {
                    mapEventsDisable = true;
                  }
                }
                if (mapEventsEnable && mapEventsDisable) {
                  // Both are active, this is an error
                  $log.warn('[AngularJS - Leaflet] can not enable and disable events at the time');
                } else if (!mapEventsEnable && !mapEventsDisable) {
                  // Both are inactive, this is an error
                  $log.warn('[AngularJS - Leaflet] must enable or disable events');
                } else {
                  // At this point the map object is OK, lets enable or disable events
                  if (mapEventsEnable) {
                    // Enable events
                    for (i = 0; i < eventBroadcast.map.enable.length; i++) {
                      eventName = eventBroadcast.map.enable[i];
                      // Do we have already the event enabled?
                      if (mapEvents.indexOf(eventName) !== -1) {
                        // Repeated event, this is an error
                        $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' is already enabled');
                      } else {
                        // Does the event exists?
                        if (availableMapEvents.indexOf(eventName) === -1) {
                          // The event does not exists, this is an error
                          $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist');
                        } else {
                          // All ok enable the event
                          mapEvents.push(eventName);
                        }
                      }
                    }
                  } else {
                    // Disable events
                    mapEvents = availableMapEvents;
                    for (i = 0; i < eventBroadcast.map.disable.length; i++) {
                      eventName = eventBroadcast.map.disable[i];
                      var index = mapEvents.indexOf(eventName);
                      if (index === -1) {
                        // The event does not exist
                        $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist or has been already disabled');
                      } else {
                        mapEvents.splice(index, 1);
                      }
                    }
                  }
                }
              }
              for (i = 0; i < mapEvents.length; i++) {
                eventName = mapEvents[i];
                map.on(eventName, genDispatchMapEvent(leafletScope, eventName, logic), { eventName: eventName });
              }
            } else {
              // Not a valid object
              $log.warn('[AngularJS - Leaflet] event-broadcast must be an object, check your model.');
            }
          });
        }
      };
    }
  ]);
  angular.module('leaflet-directive').directive('maxbounds', [
    '$log',
    'leafletMapDefaults',
    'leafletBoundsHelpers',
    function ($log, leafletMapDefaults, leafletBoundsHelpers) {
      return {
        restrict: 'A',
        scope: false,
        replace: false,
        require: 'leaflet',
        link: function (scope, element, attrs, controller) {
          var leafletScope = controller.getLeafletScope(), isValidBounds = leafletBoundsHelpers.isValidBounds;
          controller.getMap().then(function (map) {
            leafletScope.$watch('maxbounds', function (maxbounds) {
              if (!isValidBounds(maxbounds)) {
                // Unset any previous maxbounds
                map.setMaxBounds();
                return;
              }
              var bounds = [
                  [
                    maxbounds.southWest.lat,
                    maxbounds.southWest.lng
                  ],
                  [
                    maxbounds.northEast.lat,
                    maxbounds.northEast.lng
                  ]
                ];
              map.setMaxBounds(bounds);
              map.fitBounds(bounds);
            });
          });
        }
      };
    }
  ]);
  angular.module('leaflet-directive').service('leafletData', [
    '$log',
    '$q',
    'leafletHelpers',
    function ($log, $q, leafletHelpers) {
      var getDefer = leafletHelpers.getDefer, getUnresolvedDefer = leafletHelpers.getUnresolvedDefer, setResolvedDefer = leafletHelpers.setResolvedDefer;
      var maps = {};
      var tiles = {};
      var layers = {};
      var paths = {};
      var markers = {};
      var geoJSON = {};
      this.setMap = function (leafletMap, scopeId) {
        var defer = getUnresolvedDefer(maps, scopeId);
        defer.resolve(leafletMap);
        setResolvedDefer(maps, scopeId);
      };
      this.getMap = function (scopeId) {
        var defer = getDefer(maps, scopeId);
        return defer.promise;
      };
      this.unresolveMap = function (scopeId) {
        var id = leafletHelpers.obtainEffectiveMapId(maps, scopeId);
        maps[id] = undefined;
      };
      this.getPaths = function (scopeId) {
        var defer = getDefer(paths, scopeId);
        return defer.promise;
      };
      this.setPaths = function (leafletPaths, scopeId) {
        var defer = getUnresolvedDefer(paths, scopeId);
        defer.resolve(leafletPaths);
        setResolvedDefer(paths, scopeId);
      };
      this.getMarkers = function (scopeId) {
        var defer = getDefer(markers, scopeId);
        return defer.promise;
      };
      this.setMarkers = function (leafletMarkers, scopeId) {
        var defer = getUnresolvedDefer(markers, scopeId);
        defer.resolve(leafletMarkers);
        setResolvedDefer(markers, scopeId);
      };
      this.getLayers = function (scopeId) {
        var defer = getDefer(layers, scopeId);
        return defer.promise;
      };
      this.setLayers = function (leafletLayers, scopeId) {
        var defer = getUnresolvedDefer(layers, scopeId);
        defer.resolve(leafletLayers);
        setResolvedDefer(layers, scopeId);
      };
      this.setTiles = function (leafletTiles, scopeId) {
        var defer = getUnresolvedDefer(tiles, scopeId);
        defer.resolve(leafletTiles);
        setResolvedDefer(tiles, scopeId);
      };
      this.getTiles = function (scopeId) {
        var defer = getDefer(tiles, scopeId);
        return defer.promise;
      };
      this.setGeoJSON = function (leafletGeoJSON, scopeId) {
        var defer = getUnresolvedDefer(geoJSON, scopeId);
        defer.resolve(leafletGeoJSON);
        setResolvedDefer(geoJSON, scopeId);
      };
      this.getGeoJSON = function (scopeId) {
        var defer = getDefer(geoJSON, scopeId);
        return defer.promise;
      };
    }
  ]);
  angular.module('leaflet-directive').factory('leafletMapDefaults', [
    '$q',
    'leafletHelpers',
    function ($q, leafletHelpers) {
      function _getDefaults() {
        return {
          keyboard: true,
          dragging: true,
          worldCopyJump: false,
          doubleClickZoom: true,
          scrollWheelZoom: true,
          zoomControl: true,
          zoomsliderControl: false,
          zoomControlPosition: 'topleft',
          attributionControl: true,
          controls: {
            layers: {
              visible: true,
              position: 'topright',
              collapsed: true
            }
          },
          crs: L.CRS.EPSG3857,
          tileLayer: 'http://{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, obtainEffectiveMapId = leafletHelpers.obtainEffectiveMapId, defaults = {};
      // Get the _defaults dictionary, and override the properties defined by the user
      return {
        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,
              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.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 (isDefined(userDefaults.crs) && isDefined(L.CRS[userDefaults.crs])) {
              newDefaults.crs = L.CRS[userDefaults.crs];
            }
            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;
            }
          }
          var mapId = obtainEffectiveMapId(defaults, scopeId);
          defaults[mapId] = newDefaults;
          return newDefaults;
        }
      };
    }
  ]);
  angular.module('leaflet-directive').factory('leafletEvents', [
    '$rootScope',
    '$q',
    '$log',
    'leafletHelpers',
    function ($rootScope, $q, $log, leafletHelpers) {
      var safeApply = leafletHelpers.safeApply, isDefined = leafletHelpers.isDefined, isObject = leafletHelpers.isObject, Helpers = leafletHelpers;
      var _getAvailableLabelEvents = function () {
        return [
          'click',
          'dblclick',
          'mousedown',
          'mouseover',
          'mouseout',
          'contextmenu'
        ];
      };
      var genLabelEvents = function (leafletScope, logic, marker, name) {
        var labelEvents = _getAvailableLabelEvents();
        var scopeWatchName = 'markers.' + name;
        for (var i = 0; i < labelEvents.length; i++) {
          var eventName = labelEvents[i];
          marker.label.on(eventName, genDispatchLabelEvent(leafletScope, eventName, logic, marker.label, scopeWatchName));
        }
      };
      var genDispatchMarkerEvent = function (eventName, logic, leafletScope, marker, name, markerData) {
        return function (e) {
          var broadcastName = 'leafletDirectiveMarker.' + eventName;
          // 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 () {
              markerData.lat = marker.getLatLng().lat;
              markerData.lng = marker.getLatLng().lng;
            });
            if (markerData.message && markerData.focus === true) {
              marker.openPopup();
            }
          }
          safeApply(leafletScope, function (scope) {
            if (logic === 'emit') {
              scope.$emit(broadcastName, {
                markerName: name,
                leafletEvent: e
              });
            } else {
              $rootScope.$broadcast(broadcastName, {
                markerName: name,
                leafletEvent: e
              });
            }
          });
        };
      };
      var genDispatchPathEvent = function (eventName, logic, leafletScope, marker, name) {
        return function (e) {
          var broadcastName = 'leafletDirectivePath.' + eventName;
          safeApply(leafletScope, function (scope) {
            if (logic === 'emit') {
              scope.$emit(broadcastName, {
                pathName: name,
                leafletEvent: e
              });
            } else {
              $rootScope.$broadcast(broadcastName, {
                pathName: name,
                leafletEvent: e
              });
            }
          });
        };
      };
      var genDispatchLabelEvent = function (scope, eventName, logic, label, scope_watch_name) {
        return function (e) {
          // Put together broadcast name
          var broadcastName = 'leafletDirectiveLabel.' + eventName;
          var markerName = scope_watch_name.replace('markers.', '');
          // Safely broadcast the event
          safeApply(scope, function (scope) {
            if (logic === 'emit') {
              scope.$emit(broadcastName, {
                leafletEvent: e,
                label: label,
                markerName: markerName
              });
            } else if (logic === 'broadcast') {
              $rootScope.$broadcast(broadcastName, {
                leafletEvent: e,
                label: label,
                markerName: markerName
              });
            }
          });
        };
      };
      var _getAvailableMarkerEvents = function () {
        return [
          'click',
          'dblclick',
          'mousedown',
          'mouseover',
          'mouseout',
          'contextmenu',
          'dragstart',
          'drag',
          'dragend',
          'move',
          'remove',
          'popupopen',
          'popupclose'
        ];
      };
      var _getAvailablePathEvents = function () {
        return [
          'click',
          'dblclick',
          'mousedown',
          'mouseover',
          'mouseout',
          'contextmenu',
          'add',
          'remove',
          'popupopen',
          'popupclose'
        ];
      };
      return {
        getAvailableMapEvents: function () {
          return [
            'click',
            'dblclick',
            'mousedown',
            'mouseup',
            'mouseover',
            'mouseout',
            'mousemove',
            'contextmenu',
            'focus',
            'blur',
            'preclick',
            'load',
            'unload',
            'viewreset',
            'movestart',
            'move',
            'moveend',
            'dragstart',
            'drag',
            'dragend',
            'zoomstart',
            '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'
          ];
        },
        genDispatchMapEvent: function (scope, eventName, logic) {
          return function (e) {
            // Put together broadcast name
            var broadcastName = 'leafletDirectiveMap.' + eventName;
            // Safely broadcast the event
            safeApply(scope, function (scope) {
              if (logic === 'emit') {
                scope.$emit(broadcastName, { leafletEvent: e });
              } else if (logic === 'broadcast') {
                $rootScope.$broadcast(broadcastName, { leafletEvent: e });
              }
            });
          };
        },
        getAvailableMarkerEvents: _getAvailableMarkerEvents,
        getAvailablePathEvents: _getAvailablePathEvents,
        notifyCenterChangedToBounds: function (scope) {
          scope.$broadcast('boundsChanged');
        },
        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);
          }
        },
        bindMarkerEvents: function (marker, name, markerData, leafletScope) {
          var markerEvents = [];
          var i;
          var eventName;
          var logic = 'broadcast';
          if (!isDefined(leafletScope.eventBroadcast)) {
            // Backward compatibility, if no event-broadcast attribute, all events are broadcasted
            markerEvents = _getAvailableMarkerEvents();
          } else if (!isObject(leafletScope.eventBroadcast)) {
            // Not a valid object
            $log.error('[AngularJS - Leaflet] event-broadcast must be an object check your model.');
          } else {
            // We have a possible valid object
            if (!isDefined(leafletScope.eventBroadcast.marker)) {
              // We do not have events enable/disable do we do nothing (all enabled by default)
              markerEvents = _getAvailableMarkerEvents();
            } else if (!isObject(leafletScope.eventBroadcast.marker)) {
              // Not a valid object
              $log.warn('[AngularJS - Leaflet] event-broadcast.marker must be an object check your model.');
            } else {
              // We have a possible valid map object
              // Event propadation logic
              if (leafletScope.eventBroadcast.marker.logic !== undefined && leafletScope.eventBroadcast.marker.logic !== null) {
                // We take care of possible propagation logic
                if (leafletScope.eventBroadcast.marker.logic !== 'emit' && leafletScope.eventBroadcast.marker.logic !== 'broadcast') {
                  // This is an error
                  $log.warn('[AngularJS - Leaflet] Available event propagation logic are: \'emit\' or \'broadcast\'.');
                } else if (leafletScope.eventBroadcast.marker.logic === 'emit') {
                  logic = 'emit';
                }
              }
              // Enable / Disable
              var markerEventsEnable = false, markerEventsDisable = false;
              if (leafletScope.eventBroadcast.marker.enable !== undefined && leafletScope.eventBroadcast.marker.enable !== null) {
                if (typeof leafletScope.eventBroadcast.marker.enable === 'object') {
                  markerEventsEnable = true;
                }
              }
              if (leafletScope.eventBroadcast.marker.disable !== undefined && leafletScope.eventBroadcast.marker.disable !== null) {
                if (typeof leafletScope.eventBroadcast.marker.disable === 'object') {
                  markerEventsDisable = true;
                }
              }
              if (markerEventsEnable && markerEventsDisable) {
                // Both are active, this is an error
                $log.warn('[AngularJS - Leaflet] can not enable and disable events at the same time');
              } else if (!markerEventsEnable && !markerEventsDisable) {
                // Both are inactive, this is an error
                $log.warn('[AngularJS - Leaflet] must enable or disable events');
              } else {
                // At this point the marker object is OK, lets enable or disable events
                if (markerEventsEnable) {
                  // Enable events
                  for (i = 0; i < leafletScope.eventBroadcast.marker.enable.length; i++) {
                    eventName = leafletScope.eventBroadcast.marker.enable[i];
                    // Do we have already the event enabled?
                    if (markerEvents.indexOf(eventName) !== -1) {
                      // Repeated event, this is an error
                      $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' is already enabled');
                    } else {
                      // Does the event exists?
                      if (_getAvailableMarkerEvents().indexOf(eventName) === -1) {
                        // The event does not exists, this is an error
                        $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist');
                      } else {
                        // All ok enable the event
                        markerEvents.push(eventName);
                      }
                    }
                  }
                } else {
                  // Disable events
                  markerEvents = _getAvailableMarkerEvents();
                  for (i = 0; i < leafletScope.eventBroadcast.marker.disable.length; i++) {
                    eventName = leafletScope.eventBroadcast.marker.disable[i];
                    var index = markerEvents.indexOf(eventName);
                    if (index === -1) {
                      // The event does not exist
                      $log.warn('[AngularJS - Leaflet] This event ' + eventName + ' does not exist or has been already disabled');
                    } else {
                      markerEvents.splice(index, 1);
                    }
                  }
                }
              }
            }
          }
          for (i = 0; i < markerEvents.length; i++) {
            eventName = markerEvents[i];
            marker.on(eventName, genDispatchMarkerEvent(eventName, logic, leafletScope, marker, name, markerData));
          }
          if (Helpers.LabelPlugin.isLoaded() && isDefined(marker.label)) {
            genLabelEvents(leafletScope, logic, marker, name);
          }
        },
        bindPathEvents: function (path, name, pathData, leafletScope) {
          var pathEvents = [];
          var i;
          var eventName;
          var logic = 'broadcast';
          window.lls = leafletScope;
          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('[AngularJS - Leaflet] 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('[AngularJS - Leaflet] 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('[AngularJS - Leaflet] Available event propagation logic are: \'emit\' or \'broadcast\'.');
                } else if (leafletScope.eventBroadcast.path.logic === 'emit') {
                  logic = 'emit';
                }
              }
              // Enable / Disable
              var pathEventsEnable = false, 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('[AngularJS - Leaflet] can not enable and disable events at the same time');
              } else if (!pathEventsEnable && !pathEventsDisable) {
                // Both are inactive, this is an error
                $log.warn('[AngularJS - Leaflet] 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('[AngularJS - Leaflet] 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('[AngularJS - Leaflet] 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('[AngularJS - Leaflet] 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];
            path.on(eventName, genDispatchPathEvent(eventName, logic, leafletScope, pathEvents, name));
          }
          if (Helpers.LabelPlugin.isLoaded() && isDefined(path.label)) {
            genLabelEvents(leafletScope, logic, path, name);
          }
        }
      };
    }
  ]);
  angular.module('leaflet-directive').factory('leafletLayerHelpers', [
    '$rootScope',
    '$log',
    'leafletHelpers',
    function ($rootScope, $log, leafletHelpers) {
      var Helpers = leafletHelpers, isString = leafletHelpers.isString, isObject = leafletHelpers.isObject, isDefined = leafletHelpers.isDefined;
      var layerTypes = {
          xyz: {
            mustHaveUrl: true,
            createLayer: function (params) {
              return L.tileLayer(params.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);
            }
          },
          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 && 'string' === typeof options.crs) {
                /*jshint -W061 */
                options.crs = eval(options.crs);
              }
              return new L.GeoJSON.WFS(params.url, params.layer, options);
            }
          },
          group: {
            mustHaveUrl: false,
            createLayer: function () {
              return L.layerGroup();
            }
          },
          google: {
            mustHaveUrl: false,
            createLayer: function (params) {
              var type = params.type || 'SATELLITE';
              if (!Helpers.GoogleLayerPlugin.isLoaded()) {
                return;
              }
              return new L.Google(type, params.options);
            }
          },
          china: {
            mustHaveUrl: false,
            createLayer: function (params) {
              var type = params.type || '';
              if (!Helpers.ChinaLayerPlugin.isLoaded()) {
                return;
              }
              return L.tileLayer.chinaProvider(type, 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;
            }
          },
          dynamic: {
            mustHaveUrl: true,
            createLayer: function (params) {
              if (!Helpers.DynamicMapLayerPlugin.isLoaded()) {
                return;
              }
              return L.esri.dynamicMapLayer(params.url, params.options);
            }
          },
          markercluster: {
            mustHaveUrl: false,
            createLayer: function (params) {
              if (!Helpers.MarkerClusterPlugin.isLoaded()) {
                $log.error('[AngularJS - Leaflet] 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);
            }
          },
          heatmap: {
            mustHaveUrl: false,
            mustHaveData: true,
            createLayer: function (params) {
              if (!Helpers.HeatMapLayerPlugin.isLoaded()) {
                return;
              }
              var layer = new L.TileLayer.WebGLHeatMap(params.options);
              if (isDefined(params.data)) {
                layer.setData(params.data);
              }
              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);
            }
          }
        };
      function isValidLayerType(layerDefinition) {
        // Check if the baselayer has a valid type
        if (!isString(layerDefinition.type)) {
          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;
        }
        return true;
      }
      return {
        createLayer: function (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,
              type: layerDefinition.layerType,
              bounds: layerDefinition.bounds,
              key: layerDefinition.key,
              pluginOptions: layerDefinition.pluginOptions
            };
          //TODO Add $watch to the layer properties
          return layerTypes[layerDefinition.type].createLayer(params);
        }
      };
    }
  ]);
  angular.module('leaflet-directive').factory('leafletControlHelpers', [
    '$rootScope',
    '$log',
    'leafletHelpers',
    'leafletMapDefaults',
    function ($rootScope, $log, leafletHelpers, leafletMapDefaults) {
      var isObject = leafletHelpers.isObject, isDefined = leafletHelpers.isDefined;
      var _layersControl;
      var _controlLayersMustBeVisible = function (baselayers, overlays) {
        var numberOfLayers = 0;
        if (isObject(baselayers)) {
          numberOfLayers += Object.keys(baselayers).length;
        }
        if (isObject(overlays)) {
          numberOfLayers += Object.keys(overlays).length;
        }
        return numberOfLayers > 1;
      };
      var _createLayersControl = function (mapId) {
        var defaults = leafletMapDefaults.getDefaults(mapId);
        var controlOptions = {
            collapsed: defaults.controls.layers.collapsed,
            position: defaults.controls.layers.position
          };
        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;
      };
      return {
        layersControlMustBeVisible: _controlLayersMustBeVisible,
        updateLayersControl: function (map, mapId, loaded, baselayers, overlays, leafletLayers) {
          var i;
          var mustBeLoaded = _controlLayersMustBeVisible(baselayers, overlays);
          if (isDefined(_layersControl) && loaded) {
            for (i in leafletLayers.baselayers) {
              _layersControl.removeLayer(leafletLayers.baselayers[i]);
            }
            for (i in leafletLayers.overlays) {
              _layersControl.removeLayer(leafletLayers.overlays[i]);
            }
            _layersControl.removeFrom(map);
          }
          if (mustBeLoaded) {
            _layersControl = _createLayersControl(mapId);
            for (i in baselayers) {
              if (isDefined(leafletLayers.baselayers[i])) {
                _layersControl.addBaseLayer(leafletLayers.baselayers[i], baselayers[i].name);
              }
            }
            for (i in overlays) {
              if (isDefined(leafletLayers.overlays[i])) {
                _layersControl.addOverlay(leafletLayers.overlays[i], overlays[i].name);
              }
            }
            _layersControl.addTo(map);
          }
          return mustBeLoaded;
        }
      };
    }
  ]);
  angular.module('leaflet-directive').factory('leafletLegendHelpers', function () {
    var _updateArcGISLegend = function (div, legendData) {
      div.innerHTML = '';
      if (legendData.error) {
        div.innerHTML += '<div class="info-title alert alert-danger">' + legendData.error.message + '</div>';
      } else {
        for (var i = 0; i < legendData.layers.length; i++) {
          var layer = legendData.layers[i];
          div.innerHTML += '<div class="info-title">' + layer.layerName + '</div>';
          for (var j = 0; j < layer.legend.length; j++) {
            var leg = layer.legend[j];
            div.innerHTML += '<div class="inline"><img src="data:' + leg.contentType + ';base64,' + leg.imageData + '" /></div>' + '<div class="info-label">' + leg.label + '</div>';
          }
        }
      }
    };
    var _getOnAddArcGISLegend = function (legendData, legendClass) {
      return function () {
        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);
        }
        _updateArcGISLegend(div, legendData);
        return div;
      };
    };
    var _getOnAddArrayLegend = function (legend, legendClass) {
      return function () {
        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 {
      getOnAddArcGISLegend: _getOnAddArcGISLegend,
      getOnAddArrayLegend: _getOnAddArrayLegend,
      updateArcGISLegend: _updateArcGISLegend
    };
  });
  angular.module('leaflet-directive').factory('leafletPathsHelpers', [
    '$rootScope',
    '$log',
    'leafletHelpers',
    function ($rootScope, $log, leafletHelpers) {
      var isDefined = leafletHelpers.isDefined, isArray = leafletHelpers.isArray, isNumber = leafletHelpers.isNumber, isValidPoint = leafletHelpers.isValidPoint;
      function _convertToLeafletLatLngs(latlngs) {
        return latlngs.filter(function (latlng) {
          return isValidPoint(latlng);
        }).map(function (latlng) {
          return new L.LatLng(latlng.lat, latlng.lng);
        });
      }
      function _convertToLeafletLatLng(latlng) {
        return new L.LatLng(latlng.lat, latlng.lng);
      }
      function _convertToLeafletMultiLatLngs(paths) {
        return paths.map(function (latlngs) {
          return _convertToLeafletLatLngs(latlngs);
        });
      }
      function _getOptions(path, defaults) {
        var availableOptions = [
            'stroke',
            'weight',
            'color',
            'opacity',
            'fill',
            'fillColor',
            'fillOpacity',
            'dashArray',
            'lineCap',
            'lineJoin',
            'clickable',
            'pointerEvents',
            'className',
            'smoothFactor',
            'noClip'
          ];
        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) {
        if (isDefined(data.weight)) {
          path.setStyle({ weight: data.weight });
        }
        if (isDefined(data.color)) {
          path.setStyle({ color: data.color });
        }
        if (isDefined(data.opacity)) {
          path.setStyle({ opacity: data.opacity });
        }
      };
      var _isValidPolyline = function (latlngs) {
        if (!isArray(latlngs)) {
          return false;
        }
        for (var i in latlngs) {
          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').factory('leafletBoundsHelpers', [
    '$log',
    'leafletHelpers',
    function ($log, leafletHelpers) {
      var isArray = leafletHelpers.isArray, isNumber = leafletHelpers.isNumber;
      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]
            }
          };
        }
      };
    }
  ]);
  angular.module('leaflet-directive').factory('leafletMarkersHelpers', [
    '$rootScope',
    'leafletHelpers',
    '$log',
    function ($rootScope, leafletHelpers, $log) {
      var isDefined = leafletHelpers.isDefined, MarkerClusterPlugin = leafletHelpers.MarkerClusterPlugin, AwesomeMarkersPlugin = leafletHelpers.AwesomeMarkersPlugin, safeApply = leafletHelpers.safeApply, Helpers = leafletHelpers, isString = leafletHelpers.isString, isNumber = leafletHelpers.isNumber, isObject = leafletHelpers.isObject, groups = {};
      var createLeafletIcon = function (iconData) {
        if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'awesomeMarker') {
          if (!AwesomeMarkersPlugin.isLoaded()) {
            $log.error('[AngularJS - Leaflet] The AwesomeMarkers Plugin is not loaded.');
          }
          return new L.AwesomeMarkers.icon(iconData);
        }
        if (isDefined(iconData) && isDefined(iconData.type) && iconData.type === 'div') {
          return new L.divIcon(iconData);
        }
        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)) {
          return new L.Icon.Default({
            iconUrl: base64icon,
            shadowUrl: base64shadow
          });
        }
        if (!isDefined(iconData.iconUrl)) {
          iconData.iconUrl = base64icon;
          iconData.shadowUrl = base64shadow;
        }
        return new L.Icon.Default(iconData);
      };
      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) {
              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);
        }
      };
      return {
        deleteMarker: _deleteMarker,
        createMarker: function (markerData) {
          if (!isDefined(markerData)) {
            $log.error('[AngularJS - Leaflet] The marker definition is not valid.');
            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
            };
          return new L.marker(markerData, markerOptions);
        },
        addMarkerToGroup: function (marker, groupName, map) {
          if (!isString(groupName)) {
            $log.error('[AngularJS - Leaflet] The marker group you have specified is invalid.');
            return;
          }
          if (!MarkerClusterPlugin.isLoaded()) {
            $log.error('[AngularJS - Leaflet] The MarkerCluster plugin is not loaded.');
            return;
          }
          if (!isDefined(groups[groupName])) {
            groups[groupName] = new L.MarkerClusterGroup();
            map.addLayer(groups[groupName]);
          }
          groups[groupName].addLayer(marker);
        },
        listenMarkerEvents: function (marker, markerData, leafletScope) {
          marker.on('popupopen', function () {
            safeApply(leafletScope, function () {
              markerData.focus = true;
            });
          });
          marker.on('popupclose', function () {
            safeApply(leafletScope, function () {
              markerData.focus = false;
            });
          });
        },
        addMarkerWatcher: function (marker, name, leafletScope, layers, map) {
          var clearWatch = leafletScope.$watch('markers.' + name, function (markerData, oldMarkerData) {
              if (!isDefined(markerData)) {
                _deleteMarker(marker, map, layers);
                clearWatch();
                return;
              }
              if (!isDefined(oldMarkerData)) {
                return;
              }
              // Update the lat-lng property (always present in marker properties)
              if (!(isNumber(markerData.lat) && isNumber(markerData.lng))) {
                $log.warn('There are problems with lat-lng data, please verify your marker model');
                _deleteMarker(marker, map, layers);
                return;
              }
              // 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 (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('[AngularJS - Leaflet] 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)) {
                  $log.error('[AngularJS - Leaflet] A marker can only be added to a layer of type "group"');
                  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);
                  }
                }
              }
              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);
                }
              }
              // Update the Popup message property
              if (!isString(markerData.message) && isString(oldMarkerData.message)) {
                marker.closePopup();
                marker.unbindPopup();
              }
              // Update the label content
              if (Helpers.LabelPlugin.isLoaded() && isDefined(markerData.label) && isDefined(markerData.label.message) && !angular.equals(markerData.label.message, oldMarkerData.label.message)) {
                marker.updateLabelContent(markerData.label.message);
              }
              // 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);
                if (markerData.focus === true) {
                  // If the focus is set, we must open the popup, because we do not know if it was opened before
                  marker.openPopup();
                }
              }
              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 && oldMarkerData.focus !== true) {
                marker.openPopup();
                updatedFocus = true;
              }
              if (oldMarkerData.focus === true && markerData.focus === true) {
                // Reopen the popup when focus is still true
                marker.openPopup();
                updatedFocus = true;
              }
              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 (markerLatLng.lat !== markerData.lat || markerLatLng.lng !== markerData.lng) {
                marker.setLatLng([
                  markerData.lat,
                  markerData.lng
                ]);
              }
            }, true);
        }
      };
    }
  ]);
  angular.module('leaflet-directive').factory('leafletHelpers', [
    '$q',
    '$log',
    function ($q, $log) {
      function _obtainEffectiveMapId(d, mapId) {
        var id, i;
        if (!angular.isDefined(mapId)) {
          if (Object.keys(d).length === 1) {
            for (i in d) {
              if (d.hasOwnProperty(i)) {
                id = i;
              }
            }
          } else if (Object.keys(d).length === 0) {
            id = 'main';
          } else {
            $log.error('[AngularJS - Leaflet] - 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), 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;
      }
      return {
        isEmpty: function (value) {
          return Object.keys(value).length === 0;
        },
        isUndefinedOrEmpty: function (value) {
          return angular.isUndefined(value) || value === null || Object.keys(value).length === 0;
        },
        isDefined: function (value) {
          return angular.isDefined(value) && value !== null;
        },
        isNumber: function (value) {
          return angular.isNumber(value);
        },
        isString: function (value) {
          return angular.isString(value);
        },
        isArray: function (value) {
          return angular.isArray(value);
        },
        isObject: function (value) {
          return angular.isObject(value);
        },
        isFunction: function (value) {
          return angular.isFunction(value);
        },
        equals: function (o1, o2) {
          return angular.equals(o1, o2);
        },
        isValidCenter: function (center) {
          return angular.isDefined(center) && angular.isNumber(center.lat) && angular.isNumber(center.lng) && angular.isNumber(center.zoom);
        },
        isValidPoint: function (point) {
          return angular.isDefined(point) && angular.isNumber(point.lat) && angular.isNumber(point.lng);
        },
        isSameCenterOnMap: function (centerModel, map) {
          var mapCenter = map.getCenter();
          var zoom = map.getZoom();
          if (mapCenter.lat === centerModel.lat && mapCenter.lng === centerModel.lng && 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.$apply(fn);
          }
        },
        obtainEffectiveMapId: _obtainEffectiveMapId,
        getDefer: function (d, mapId) {
          var id = _obtainEffectiveMapId(d, mapId), 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;
        },
        AwesomeMarkersPlugin: {
          isLoaded: function () {
            if (angular.isDefined(L.AwesomeMarkers) && angular.isDefined(L.AwesomeMarkers.Icon)) {
              return true;
            } else {
              return false;
            }
          },
          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;
            }
          }
        },
        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;
            }
          }
        },
        ChinaLayerPlugin: {
          isLoaded: function () {
            return angular.isDefined(L.tileLayer.chinaProvider);
          }
        },
        HeatMapLayerPlugin: {
          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;
            }
          }
        },
        AGSLayerPlugin: {
          isLoaded: function () {
            return lvector !== undefined && lvector.AGS !== undefined;
          },
          is: function (layer) {
            if (this.isLoaded()) {
              return layer instanceof lvector.AGS;
            } 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;
            }
          }
        },
        DynamicMapLayerPlugin: {
          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;
            }
          }
        },
        GeoJSONPlugin: {
          isLoaded: function () {
            return angular.isDefined(L.TileLayer.GeoJSON);
          },
          is: function (layer) {
            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;
              }
            }
          }
        }
      };
    }
  ]);
}());