<!DOCTYPE html>
<html data-ng-app="Leaflet">

  <head>
    <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
    
    <!-- Font Awesome -->
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" />
    
    <!-- Leaflet-->
    <link rel="stylesheet" href="//cdn.jsdelivr.net/leaflet/0.7.3/leaflet.css" />
    <script src="//cdn.jsdelivr.net/leaflet/0.7.3/leaflet.js"></script>
  
    <!-- Leaflet Draw -->
    <link rel="stylesheet" href="http://leaflet.github.io/Leaflet.draw/leaflet.draw.css">
    <script src="http://leaflet.github.io/Leaflet.draw/leaflet.draw.js"></script>
  
    <!-- ESRI Leaflet -->
    <script src="http://cdn-geoweb.s3.amazonaws.com/esri-leaflet/1.0.0-rc.5/esri-leaflet.js"></script>
    
    <!-- Marker Cluster -->
    <link rel="stylesheet" href="http://leaflet.github.io/Leaflet.markercluster/dist/MarkerCluster.css" />
	  <link rel="stylesheet" href="http://leaflet.github.io/Leaflet.markercluster/dist/MarkerCluster.Default.css" />
	  <script src="http://leaflet.github.io/Leaflet.markercluster/dist/leaflet.markercluster-src.js"></script>

    <!-- Fullscreen -->
    <link href='https://rawgithub.com/brunob/leaflet.fullscreen/master/Control.FullScreen.css' rel='stylesheet' />
    <script src="https://rawgithub.com/brunob/leaflet.fullscreen/master/Control.FullScreen.js"></script>
    
    <!-- Context Menu -->
    <link rel="stylesheet" href="https://cdn.rawgit.com/aratcliffe/Leaflet.contextmenu/master/dist/leaflet.contextmenu.css">
    <script type="text/javascript" src="https://cdn.rawgit.com/aratcliffe/Leaflet.contextmenu/master/dist/leaflet.contextmenu-src.js"></script>
    
    <!-- Bookmarks -->
    <link rel="stylesheet" type="text/css" href="https://rawgit.com/w8r/Leaflet.Bookmarks/master/dist/leaflet.bookmarks.css">
    <script type="text/javascript" src="https://rawgit.com/w8r/Leaflet.Bookmarks/master/dist/Leaflet.Bookmarks.js"></script>
    
    <!-- Zoom Box -->
    <link rel="stylesheet" href="http://consbio.github.io/Leaflet.ZoomBox/L.Control.ZoomBox.css">
    <script src="http://consbio.github.io/Leaflet.ZoomBox/L.Control.ZoomBox.js"></script>
    
    <!-- Coordinates Control -->
    <link rel="stylesheet" href="http://mrmufflon.github.io/Leaflet.Coordinates/dist/Leaflet.Coordinates-0.1.3.css">
    <script type="text/javascript" src="http://mrmufflon.github.io/Leaflet.Coordinates/dist/Leaflet.Coordinates-0.1.3.min.js"></script>
    
    <!-- Measure Control -->
    <script src="http://makinacorpus.github.io/Leaflet.MeasureControl/leaflet.measurecontrol.js"></script>
    <link rel="stylesheet" href="http://makinacorpus.github.io/Leaflet.MeasureControl/leaflet.measurecontrol.css" />
    
    <!-- Loading Control -->
    <link rel="stylesheet" href="https://rawgithub.com/ebrelsford/Leaflet.loading/master/src/Control.Loading.css">
    <script src="https://rawgithub.com/ebrelsford/Leaflet.loading/master/src/Control.Loading.js"></script>
    
    <!-- Angular -->
    <script data-require="angular.js@1.2.15" data-semver="1.2.15" src="http://code.angularjs.org/1.2.15/angular.js"></script>
    
    <!-- Demo -->
    <link rel="stylesheet" href="style.css" />
    <script src="_declaration.js"></script>
    <script src="service.js"></script>
    <script src="directive.js"></script>
    <script src="example.js"></script>
  </head>

  <body data-ng-controller="MyCtrl as vm">
    <div leaflet="" options="vm.mapOptions" style="height:640px;width:800px;position:relative;"></div>
  </body>

</html>
/* Leaflet Overrides */
.leaflet-control-layers-toggle {
  background-image: url(images/layers.png);
}
.leaflet-retina .leaflet-control-layers-toggle {
  background-image: url(images/layers-2x.png);
}
.leaflet-container {
  height:100%;
}
.leaflet-control a:hover {
  background-color: #f6f6f6;
}
.leaflet-control-zoom,
.leaflet-control-layers,
.leaflet-bar {
  border-radius: 0;
  box-shadow: 0 2px 6px rgba(50,50,50,.75);
}

.leaflet-bar a,
.leaflet-bar a:hover {
  width: 32px;
  height: 32px;
  line-height: 32px;
}
.leaflet-bar a:first-child,
.leaflet-bar a:last-child {
  border-radius:0;
}
/* Bookmark Overrides */
.leaflet-right .leaflet-bookmarks-control {
  margin-top: 10px;
  margin-right: 10px;
}
.leaflet-bookmarks-control.expanded .bookmarks-icon-wrapper {
  background: transparent;
  padding: 4px 3px 0.25em 7px;
  border-radius: 0 0 0 4px;
  position: relative;
}
.leaflet-bookmarks-control .bookmarks-icon {
  font-size: 14px;
}
.leaflet-bookmarks-control .bookmarks-header {
height: 1.4em;
}
.leaflet-bookmarks-form .leaflet-bookmarks-form-submit {
  margin: 0;
  height: 23px;
  width: 26px;
  cursor: pointer;
}
Because angular-leaflet is just too complex...
{
    "records":[
        {"id":"1",  "name": "Kramer", "geometry":{"type":"Point", "coordinates":[33.71, -117.81]}},
        {"id":"2",  "name": "Jerry", "geometry":{"type":"Point", "coordinates":[33.75, -117.82]}},
        {"id":"3",  "name": "George", "geometry":{"type":"Point", "coordinates":[33.70, -117.79]}},
        {"id":"4",  "name": "Elaine", "geometry":{"type":"Point", "coordinates":[33.78, -117.8]}}
    ]
}
angular.module('Leaflet', []);
angular.module('Leaflet')
.controller('MyCtrl', function MyCtrl($scope, RecordService) {
  var vm = this;
      vm.mapOptions= {
        basemaps:{},
        layers: {},
        map: {
          scrollWheelZoom: false,
          attributionControl: false
        },
        // Control true | falsy | Object
        controls: {
          zoom: true,
          fullscreen: true,
          layers: true,
          scale: true,
          measure: true,
          loading: true,
          coordinate: true,
          zoomBox: true,
          bookmarks: true,
          draw: false
        }
      };
})
.factory('RecordService', function RecordService($http) {
  var service = {
    getPartials = getPartials
  };
  return service;
  
  function getPartials() {
    return $http.get('partials.json').then(function(value) {
      return value.data.records;
    });
  };
})
;
angular.module('Leaflet')
.controller('LeafletCtrl', function LeafletCtrl($scope, $attrs, $element, $parse, $window, $location, $log, RecordService, MapService) {
  // TODO: Configuration:
  // Set Base Layers
  // Set Layers
  // Set Plugins
  // Set Symbolization  http://colorbrewer2.org/
  // Allow for filters

  // TODO:
  // Leaflet Plugins - Permalink
  // Google basemaps vs esri vs mapbox....  all have different pricing models
  // Determine what geocoding service to use -- leaning towards google or opencage
  
  /* Aliases */
  var vm = this;
  var L = $window.L;
  
  /* Expose Controller API to Link Function */
  vm.init = init;
  
  /* Options */
  var options = $parse($attrs.options)($scope) || {controls:{}, map:{}};
  
  var mapOptions = MapService.mapDefaults();
  // TODO: Move to settings
  mapOptions.contextmenu = true;
  mapOptions.contextmenuItems = [{
    text: 'Bookmark this position',
    callback: function(evt) {
      this.fire('bookmark:new', {
        latlng: evt.latlng
      });
    }
  }];
  angular.extend(mapOptions, options.map);
  
  var controlOptions = MapService.controlDefaults();
  angular.extend(controlOptions, options.controls);
  
  /* Internal Configurations */
  var icons = {};
  var dataModel = {
    data: [],
    markers: []
  };

  var basemaps = {
    "Topographic": L.esri.basemapLayer('Topographic'),
    "Streets": L.esri.basemapLayer('Streets'),
    "National Geographic": L.esri.basemapLayer('NationalGeographic'),
    "Oceans": L.esri.basemapLayer('Oceans'),
    "Gray": L.esri.basemapLayer('Gray'),
    "DarkGray": L.esri.basemapLayer('DarkGray'),
    "Imagery": L.esri.basemapLayer('Imagery'),
    "Shaded Relief": L.esri.basemapLayer('ShadedRelief')
  };

  var layers = {};
  layers.markers = L.markerClusterGroup();
  layers.drawnItems = new L.FeatureGroup();

  /* Control Options */
  var drawOptions = {
    draw: {
      position: 'topleft',
      polygon: {
        title: 'Draw a sexy polygon!',
        allowIntersection: false,
        drawError: {
          color: '#b00b00',
          timeout: 1000
        },
        shapeOptions: {
          color: '#bada55'
        },
        showArea: true
      },
      polyline: {
        metric: false
      },
      circle: {
        shapeOptions: {
          color: '#662d91'
        }
      }
    },
    edit: {
      featureGroup: layers.drawnItems
    }
  };

  /* Watchers */
  $scope.$watch(RecordService.partials, function(newValue, oldValue) {
      var data = newValue || [];
      refreshMarkers(data, dataModel.markers, layers.markers);
  });

  /* Events */
  $scope.$on('$destroy', function() {
    // Consider destroying Controls & layers first...  .removeFrom(vm.map)
    // Consider tearing down event listeners
    vm.map.remove();
  });
  ////////////////////
  /**
   * @function init
   */
  function init(id) {
    // Get Data
    RecordService.getPartials();

    // Create map
    vm.map = L.map(id, mapOptions);

    // Set Map Options/Properties
    var view = MapService.getView(); // TODO: consider moving to mapOptions extend function
    vm.map.setView(L.latLng(view.lat, view.lng), view.zoom, true);

    // Setup Controls (in order of position top-right, top-left, bottom-left, bottom-right)
    if (controlOptions.layers) {
      var layersControl = L.control.layers(basemaps, layers).addTo(vm.map);
    }
    if (controlOptions.bookmarks) {
      var bookmarksControl = new L.Control.Bookmarks().addTo(vm.map);
    }
    if (controlOptions.zoom) {
      var zoomControl = L.control.zoom().addTo(vm.map);
    }
    if (controlOptions.zoomBox) {
      var zoomBoxControl = L.control.zoomBox().addTo(vm.map);
    }
    if (controlOptions.fullscreen) {
      var fullscreenControl = new L.Control.FullScreen().addTo(vm.map);
    }
    if (controlOptions.draw) {
      var drawControl = new L.Control.Draw(drawOptions).addTo(vm.map);
    }
    if (controlOptions.measure) {
      var measureControl = L.Control.measureControl().addTo(vm.map);
    }
    if (controlOptions.loading) {
      var loadingControl = L.Control.loading().addTo(vm.map);
    }
    if (controlOptions.scale) {
      var scaleControl = L.control.scale().addTo(vm.map);
    }
    if (controlOptions.coordinate) {
      var coordinateControl = L.control.coordinates().addTo(vm.map);
    }
    
    // Add Layers
    vm.map.addLayer(basemaps.Topographic);

    // Register Event Listeners
    vm.map.on("load", function(event) {
      $log.info('load', event);
      //drawMarkers();
    });
    vm.map.on("click", function(event) {
      $log.info('click', event);
    });
    vm.map.on("moveend", function(event) {
      $log.info('moveend', event);
      if (!this._loaded) {
        return;
      }
      var view = {
        lat: this.getCenter().lat,
        lng: this.getCenter().lng,
        zoom: this.getZoom()
      };
      MapService.setView(view);
    });
    vm.map.on('draw:created', function(event) {
      $log.info('draw:created', event);
      var type = event.layerType;
      var layer = event.layer;
      if (type === 'marker') {
        layer.bindPopup('A popup!');
      }
      layers.drawnItems.addLayer(layer);
    });
  }

  /**
   * @function drawMarkers
   */
  function drawMarkers(markersArray, layer) {
    angular.forEach(markersArray, function(value, key) {
      layer.addLayer(value);
    });
  }
  
  /**
   * @function eraseMarkers
   */
  function eraseMarkers(markersArray, layer) {
    angular.forEach(markersArray, function(value, key) {
      layer.removeLayer(v);
    });
  }

  /**
   * @function createMarkers
   */
  function createMarkers(data) {
    var arr = [];
    angular.forEach(data, function(value, key) {
      var marker = L.marker(value.geometry.coordinates, {
        title: value.id,
        riseOnHover: true,
        showOnMouseOver: true
      });
      marker.bindPopup('<div>' + value.name + '<div>');
      arr.push(marker);
    });
    return arr;
  }
  
  /**
   * @function refreshMarkers
   */
  function refreshMarkers(data, markersArray, layer) {
    if (markersArray.length > 0) {
      eraseMarkers(markersArray, layer);
    }
    markersArray = createMarkers(data);
    drawMarkers(markersArray, layer);
  }
  
})

.directive('leaflet', function leaflet() {
  var _id = 'leaflet-' + new Date().getTime();
  return {
    restrict: 'AE',
    controller: 'LeafletCtrl',
    template: function(element, attributes) {
      var id = attributes.leaflet || _id;
      return '<div id="' + id + '" style="position:absolute;top:0;bottom:0;right:0;left:0;"></div>';
    },
    controllerAs: 'vm',
    link: function(scope, element, attributes, controller) {
      var id = attributes.leaflet || _id;
      controller.init(id);
    }
  };
});
angular.module('Leaflet')
.factory('MapService', function MapService($window) {
  var localStorage = $window.localStorage || {};
  var service = {
    getView: getView,
    setView: setView,
    mapDefaults: mapDefaults,
    controlDefaults: controlDefaults
  };
  return service;
  ////////////////////
  function getView() {
    var view = localStorage.mapView;
    if (typeof view != 'undefined') {
      view = $window.JSON.parse(view || '');
      return view;
    } else {
      return {
        lat: 33.7,
        lng: -117.8,
        zoom: 10
      };
    }
  }
  function setView(view) {
    localStorage.mapView = $window.JSON.stringify(view);
  }
  function controlDefaults() {
    return {
        zoom: true,
        fullscreen: true,
        layers: true,
        scale: true,
        measure: false,
        loading: true,
        coordinate: false,
        zoomBox: false,
        bookmarks: false,
        draw: false
      };
  }
  function mapDefaults() {
    return {
      // Default
      center: [33.7, -117.8],
      zoom: 10,
      //layers: layers
      minZoom: undefined,
      maxZoom: undefined,
      maxBounds: undefined,
      dragging: true,
      touchZoom: true,
      scrollWheelZoom: true,
      doubleClickZoom: true,
      boxZoom: true,
      trackResize: true,
      closePopupOnClick: true,
      zoomControl: false,
      attributionControl: false
    };
  }
})
;