diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..ba4c66e
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,8 @@
+{
+    "name": "drupal/leaflet",
+    "description": "Provides the leaflet module.",
+    "type": "drupal-module",
+    "license": "GPL-2.0+",
+    "minimum-stability": "dev",
+    "require": {}
+}
diff --git a/leaflet.api.php b/leaflet.api.php
index 844d5e6..a9dd892 100644
--- a/leaflet.api.php
+++ b/leaflet.api.php
@@ -58,6 +58,8 @@ function hook_leaflet_map_info() {
       //   'shadowSize'    => array('x' => '25', 'y' => '27'),
       //   'shadowAnchor'  => array('x' => '0', 'y' => '27'),
       // ),
+      // Enable and configure plugins in the plugins array.
+      'plugins' => array(),
     ),
   );
 }
diff --git a/leaflet.drupal.js b/leaflet.drupal.js
index 8d3793c..2e1b94f 100644
--- a/leaflet.drupal.js
+++ b/leaflet.drupal.js
@@ -1,294 +1,352 @@
 (function ($) {
 
   Drupal.behaviors.leaflet = {
-    attach:function (context, settings) {
+    attach: function (context, settings) {
 
-      $(settings.leaflet).each(function () {
+      $.each(settings.leaflet, function (m, data) {
+        $('#' + data.mapId, context).each(function () {
+          var $container = $(this);
 
-       for (var m in this) {
+          // If the attached context contains any leaflet maps, make sure we have a Drupal.leaflet_widget object.
+          if ($container.data('leaflet') == undefined) {
+            $container.data('leaflet', new Drupal.Leaflet(L.DomUtil.get(data.mapId), data.mapId, data.map));
+            $container.data('leaflet').add_features(data.features, true);
 
-        // bail if the map already exists
-        var container = L.DomUtil.get(this[m].mapId);
-        if (!container || container._leaflet) {
-          return false;
-        }
-        var thismap = this[m].map;
-        
-        // load a settings object with all of our map settings
-        var settings = {};
-        for (var setting in thismap.settings) {
-          settings[setting] = thismap.settings[setting];
-        }
-
-        // instantiate our new map
-        var lMap = new L.Map(this[m].mapId, settings);
-
-        // add map layers
-        var layers = {}, overlays = {};
-        var i = 0;
-        for (var key in thismap.layers) {
-          var layer = thismap.layers[key];
-          var map_layer = Drupal.leaflet.create_layer(layer, key);
-
-          layers[key] = map_layer;
-
-          // add the  layer to the map
-          if (i >= 0) {
-            lMap.addLayer(map_layer);
-          }
-          i++;
-        }
-
-        // add features
-        for (i = 0; i < this[m].features.length; i++) {
-          var feature = this[m].features[i];
-          var lFeature;
-
-          // dealing with a layer group
-          if (feature.group) {
-            var lGroup = new L.LayerGroup();
-            for (var groupKey in feature.features) {
-              var groupFeature = feature.features[groupKey];
-              lFeature = leaflet_create_feature(groupFeature);
-              if (groupFeature.popup) {
-                lFeature.bindPopup(groupFeature.popup);
-              }
-              lGroup.addLayer(lFeature);
-            }
-
-            // add the group to the layer switcher
-            overlays[feature.label] = lGroup;
-
-            lMap.addLayer(lGroup);
+            // Add the leaflet map to our settings object to make it accessible
+            data.lMap = $container.data('leaflet').lMap;
           }
           else {
-            lFeature = leaflet_create_feature(feature);
-            lMap.addLayer(lFeature);
-
-            if (feature.popup) {
-              lFeature.bindPopup(feature.popup);
+            // If we already had a map instance, add new features.
+            // @todo Does this work? Needs testing.
+            if (data.features != undefined) {
+              $container.data('leaflet').add_features(data.features);
             }
           }
+        });
+        // Destroy features so that an AJAX reload does not get parts of the old set.
+        // Required when the View has "Use AJAX" set to Yes.
+        // @todo Is this still necessary? Needs testing.
+        data.features = null;
+      });
+    }
+  };
+
+  Drupal.Leaflet = function (container, mapId, map_definition) {
+    this.container = container;
+    this.mapId = mapId;
+    this.map_definition = map_definition;
+    this.settings = this.map_definition.settings;
+    this.bounds = [];
+    this.base_layers = {};
+    this.overlays = {};
+    this.lMap = null;
+    this.layer_control = null;
+
+    this.initialise();
+  };
+
+  Drupal.Leaflet.prototype.initialise = function () {
+    // Instantiate a new Leaflet map.
+    this.lMap = new L.Map(this.mapId, this.settings);
+
+    // Add map base layers.
+    for (var key in this.map_definition.layers) {
+      var layer = this.map_definition.layers[key];
+      this.add_base_layer(key, layer);
+    }
 
-          // Allow others to do something with the feature that was just added to the map
-          $(document).trigger('leaflet.feature', [lFeature, feature]);
-        }
-
-        // add layer switcher
-        if (thismap.settings.layerControl) {
-          lMap.addControl(new L.Control.Layers(layers, overlays));
-        }
+    // Set initial view, fallback to displaying the whole world.
+    if (this.settings.center && this.settings.zoom) {
+      this.lMap.setView(new L.LatLng(this.settings.center.lat, this.settings.center.lng), this.settings.zoom);
+    }
+    else {
+      this.lMap.fitWorld();
+    }
 
-        // center the map
-        if (thismap.center) {
-          lMap.setView(new L.LatLng(thismap.center.lat, thismap.center.lon), thismap.settings.zoom);
-        }
-        // if we have provided a zoom level, then use it after fitting bounds
-        else if (thismap.settings.zoom) {
-          Drupal.leaflet.fitbounds(lMap);
-          lMap.setZoom(thismap.settings.zoom);
-        }
-        // fit to bounds
-        else {
-          Drupal.leaflet.fitbounds(lMap);
-        }
+    // Add attribution
+    if (this.settings.attributionControl && this.map_definition.attribution) {
+      this.lMap.attributionControl.setPrefix(this.map_definition.attribution.prefix);
+      this.attributionControl.addAttribution(this.map_definition.attribution.text);
+    }
 
-        // add attribution
-        if (thismap.settings.attributionControl && thismap.attribution) {
-          lMap.attributionControl.setPrefix(thismap.attribution.prefix);
-          lMap.attributionControl.addAttribution(thismap.attribution.text);
-        }
+    // allow other modules to get access to the map object using jQuery's trigger method
+    $(document).trigger('leaflet.map', [this.map_definition, this.lMap, this]);
+  };
+
+  Drupal.Leaflet.prototype.initialise_layer_control = function () {
+    var count_layers = function (obj) {
+      // Browser compatibility: Chrome, IE 9+, FF 4+, or Safari 5+
+      // @see http://kangax.github.com/es5-compat-table/
+      return Object.keys(obj).length;
+    };
+
+    // Only add a layer switcher if it is enabled in settings, and we have
+    // at least two base layers or at least one overlay.
+    if (this.layer_control == null && this.settings.layerControl && (count_layers(this.base_layers) > 1 || count_layers(this.overlays) > 0)) {
+      // Only add base-layers if we have more than one, i.e. if there actually
+      // is a choice for the user.
+      var _layers = this.base_layers.length > 1 ? this.base_layers : [];
+      // Instantiate layer control, using settings.layerControl as settings.
+      this.layer_control = new L.Control.Layers(_layers, this.overlays, this.settings.layerControl);
+      this.lMap.addControl(this.layer_control);
+    }
+  };
 
-        // add the leaflet map to our settings object to make it accessible
-        this[m].lMap = lMap;
+  Drupal.Leaflet.prototype.add_base_layer = function (key, definition) {
+    var map_layer = this.create_layer(definition, key);
+    this.base_layers[key] = map_layer;
+    this.lMap.addLayer(map_layer);
 
-        // allow other modules to get access to the map object using jQuery's trigger method
-        $(document).trigger('leaflet.map', [thismap, lMap]);
+    if (this.layer_control == null) {
+      this.initialise_layer_control();
+    }
+    else {
+      // If we already have a layer control, add the new base layer to it.
+      this.layer_control.addBaseLayer(map_layer, key);
+    }
+  };
 
-        // Destroy features so that an AJAX reload does not get parts of the old set.
-        // Required when the View has "Use AJAX" set to Yes.
-        this[m].features = null;
-       }
-      });
+  Drupal.Leaflet.prototype.add_overlay = function (label, layer) {
+    this.overlays[label] = layer;
+    this.lMap.addLayer(layer);
 
-      function leaflet_create_feature(feature) {
-        var lFeature;
-        switch (feature.type) {
-          case 'point':
-            lFeature = Drupal.leaflet.create_point(feature);
-            break;
-          case 'linestring':
-            lFeature = Drupal.leaflet.create_linestring(feature);
-            break;
-          case 'polygon':
-            lFeature = Drupal.leaflet.create_polygon(feature);
-            break;
-          case 'multipolygon':
-          case 'multipolyline':
-            lFeature = Drupal.leaflet.create_multipoly(feature);
-            break;
-          case 'json':
-            lFeature = Drupal.leaflet.create_json(feature.json)
-            break;
+    if (this.layer_control == null) {
+      this.initialise_layer_control();
+    }
+    else {
+      // If we already have a layer control, add the new overlay to it.
+      this.layer_control.addOverlay(layer, label);
+    }
+  };
+
+  Drupal.Leaflet.prototype.add_features = function (features, initial) {
+    for (var i = 0; i < features.length; i++) {
+      var feature = features[i];
+      var lFeature;
+
+      // dealing with a layer group
+      if (feature.group) {
+        var lGroup = this.create_feature_group(feature);
+        for (var groupKey in feature.features) {
+          var groupFeature = feature.features[groupKey];
+          lFeature = this.create_feature(groupFeature);
+          if (lFeature != undefined) {
+            if (groupFeature.popup) {
+              lFeature.bindPopup(groupFeature.popup);
+            }
+            lGroup.addLayer(lFeature);
+          }
         }
 
-        // assign our given unique ID, useful for associating nodes
-        if (feature.leaflet_id) {
-          lFeature._leaflet_id = feature.leaflet_id;
-        }
+        // Add the group to the layer switcher.
+        this.add_overlay(feature.label, lGroup);
+      }
+      else {
+        lFeature = this.create_feature(feature);
+        if (lFeature != undefined) {
+          this.lMap.addLayer(lFeature);
 
-        var options = {};
-        if (feature.options) {
-          for (var option in feature.options) {
-            options[option] = feature.options[option];
+          if (feature.popup) {
+            lFeature.bindPopup(feature.popup);
           }
-          lFeature.setStyle(options);
         }
-
-        return lFeature;
       }
 
+      // Allow others to do something with the feature that was just added to the map
+      $(document).trigger('leaflet.feature', [lFeature, feature, this]);
     }
-  }
 
-  Drupal.leaflet = {
+    // Fit bounds after adding features.
+    this.fitbounds();
+
+    // Allow plugins to do things after features have been added.
+    $(document).trigger('leaflet.features', [initial || false, this])
+  };
+
+  Drupal.Leaflet.prototype.create_feature_group = function (feature) {
+    return new L.LayerGroup();
+  };
+
+  Drupal.Leaflet.prototype.create_feature = function (feature) {
+    var lFeature;
+    switch (feature.type) {
+      case 'point':
+        lFeature = this.create_point(feature);
+        break;
+      case 'linestring':
+        lFeature = this.create_linestring(feature);
+        break;
+      case 'polygon':
+        lFeature = this.create_polygon(feature);
+        break;
+      case 'multipolygon':
+      case 'multipolyline':
+        lFeature = this.create_multipoly(feature);
+        break;
+      case 'json':
+        lFeature = this.create_json(feature.json);
+        break;
+      default:
+        return; // Crash and burn.
+    }
 
-    bounds: [],
+    // assign our given unique ID, useful for associating nodes
+    if (feature.leaflet_id) {
+      lFeature._leaflet_id = feature.leaflet_id;
+    }
 
-    create_layer: function (layer, key) {
-      var map_layer = new L.TileLayer(layer.urlTemplate);
-      map_layer._leaflet_id = key;
+    var options = {};
+    if (feature.options) {
+      for (var option in feature.options) {
+        options[option] = feature.options[option];
+      }
+      lFeature.setStyle(options);
+    }
 
-      if (layer.options) {
-        for (var option in layer.options) {
-          map_layer.options[option] = layer.options[option];
-        }
+    return lFeature;
+  };
+
+  Drupal.Leaflet.prototype.create_layer = function (layer, key) {
+    var map_layer = new L.TileLayer(layer.urlTemplate);
+    map_layer._leaflet_id = key;
+
+    if (layer.options) {
+      for (var option in layer.options) {
+        map_layer.options[option] = layer.options[option];
       }
+    }
 
-      // layers served from TileStream need this correction in the y coordinates
-      // TODO: Need to explore this more and find a more elegant solution
-      if (layer.type == 'tilestream') {
-        map_layer.getTileUrl = function (tilePoint) {
-          this._adjustTilePoint(tilePoint);
-          var zoom = this._getZoomForUrl();
-          return L.Util.template(this._url, L.Util.extend({
-            s: this._getSubdomain(tilePoint),
-            z: zoom,
-            x: tilePoint.x,
-            y: Math.pow(2, zoom) - tilePoint.y - 1
-          }, this.options));
-        }
+    // layers served from TileStream need this correction in the y coordinates
+    // TODO: Need to explore this more and find a more elegant solution
+    if (layer.type == 'tilestream') {
+      map_layer.getTileUrl = function (tilePoint) {
+        this._adjustTilePoint(tilePoint);
+        var zoom = this._getZoomForUrl();
+        return L.Util.template(this._url, L.Util.extend({
+          s: this._getSubdomain(tilePoint),
+          z: zoom,
+          x: tilePoint.x,
+          y: Math.pow(2, zoom) - tilePoint.y - 1
+        }, this.options));
       }
-      return map_layer;
-    },
+    }
+    return map_layer;
+  };
 
-    create_point: function(marker) {
-      var latLng = new L.LatLng(marker.lat, marker.lon);
-      this.bounds.push(latLng);
-      var lMarker;
+  Drupal.Leaflet.prototype.create_icon = function (options) {
+    var icon = new L.Icon({iconUrl: options.iconUrl});
 
-      if (marker.icon) {
-        var icon = new L.Icon({iconUrl: marker.icon.iconUrl});
+    // override applicable marker defaults
+    if (options.iconSize) {
+      icon.options.iconSize = new L.Point(parseInt(options.iconSize.x), parseInt(options.iconSize.y));
+    }
+    if (options.iconAnchor) {
+      icon.options.iconAnchor = new L.Point(parseFloat(options.iconAnchor.x), parseFloat(options.iconAnchor.y));
+    }
+    if (options.popupAnchor) {
+      icon.options.popupAnchor = new L.Point(parseFloat(options.popupAnchor.x), parseFloat(options.popupAnchor.y));
+    }
+    if (options.shadowUrl !== undefined) {
+      icon.options.shadowUrl = options.shadowUrl;
+    }
+    if (options.shadowSize) {
+      icon.options.shadowSize = new L.Point(parseInt(options.shadowSize.x), parseInt(options.shadowSize.y));
+    }
+    if (options.shadowAnchor) {
+      icon.options.shadowAnchor = new L.Point(parseInt(options.shadowAnchor.x), parseInt(options.shadowAnchor.y));
+    }
 
-        // override applicable marker defaults
-        if (marker.icon.iconSize) {
-          icon.options.iconSize = new L.Point(parseInt(marker.icon.iconSize.x), parseInt(marker.icon.iconSize.y));
-        }
-        if (marker.icon.iconAnchor) {
-          icon.options.iconAnchor = new L.Point(parseFloat(marker.icon.iconAnchor.x), parseFloat(marker.icon.iconAnchor.y));
-        }
-        if (marker.icon.popupAnchor) {
-          icon.options.popupAnchor = new L.Point(parseFloat(marker.icon.popupAnchor.x), parseFloat(marker.icon.popupAnchor.y));
-        }
-        if (marker.icon.shadowUrl !== undefined) {
-          icon.options.shadowUrl = marker.icon.shadowUrl;
-        }
-        if (marker.icon.shadowSize) {
-          icon.options.shadowSize = new L.Point(parseInt(marker.icon.shadowSize.x), parseInt(marker.icon.shadowSize.y));
-        }
-        if (marker.icon.shadowAnchor) {
-          icon.options.shadowAnchor = new L.Point(parseInt(marker.icon.shadowAnchor.x), parseInt(marker.icon.shadowAnchor.y));
-        }
+    return icon;
+  };
 
-        lMarker = new L.Marker(latLng, {icon:icon});
-      }
-      else {
-        lMarker = new L.Marker(latLng);
-      }
-      return lMarker;
-    },
+  Drupal.Leaflet.prototype.create_point = function (marker) {
+    var latLng = new L.LatLng(marker.lat, marker.lon);
+    this.bounds.push(latLng);
+    var lMarker;
 
-    create_linestring: function(polyline) {
-      var latlngs = [];
-      for (var i = 0; i < polyline.points.length; i++) {
-        var latlng = new L.LatLng(polyline.points[i].lat, polyline.points[i].lon);
-        latlngs.push(latlng);
-        this.bounds.push(latlng);
-      }
-      return new L.Polyline(latlngs);
-    },
+    if (marker.icon) {
+      var icon = this.create_icon(marker.icon);
+      lMarker = new L.Marker(latLng, {icon: icon});
+    }
+    else {
+      lMarker = new L.Marker(latLng);
+    }
+    return lMarker;
+  };
+
+  Drupal.Leaflet.prototype.create_linestring = function (polyline) {
+    var latlngs = [];
+    for (var i = 0; i < polyline.points.length; i++) {
+      var latlng = new L.LatLng(polyline.points[i].lat, polyline.points[i].lon);
+      latlngs.push(latlng);
+      this.bounds.push(latlng);
+    }
+    return new L.Polyline(latlngs);
+  };
+
+  Drupal.Leaflet.prototype.create_polygon = function (polygon) {
+    var latlngs = [];
+    for (var i = 0; i < polygon.points.length; i++) {
+      var latlng = new L.LatLng(polygon.points[i].lat, polygon.points[i].lon);
+      latlngs.push(latlng);
+      this.bounds.push(latlng);
+    }
+    return new L.Polygon(latlngs);
+  };
 
-    create_polygon: function(polygon) {
+  Drupal.Leaflet.prototype.create_multipoly = function (multipoly) {
+    var polygons = [];
+    for (var x = 0; x < multipoly.component.length; x++) {
       var latlngs = [];
+      var polygon = multipoly.component[x];
       for (var i = 0; i < polygon.points.length; i++) {
         var latlng = new L.LatLng(polygon.points[i].lat, polygon.points[i].lon);
         latlngs.push(latlng);
         this.bounds.push(latlng);
       }
-      return new L.Polygon(latlngs);
-    },
-
-    create_multipoly: function(multipoly) {
-      var polygons = [];
-      for (var x = 0; x < multipoly.component.length; x++) {
-        var latlngs = [];
-        var polygon = multipoly.component[x];
-        for (var i = 0; i < polygon.points.length; i++) {
-          var latlng = new L.LatLng(polygon.points[i].lat, polygon.points[i].lon);
-          latlngs.push(latlng);
-          this.bounds.push(latlng);
-        }
-        polygons.push(latlngs);
-      }
-      if (multipoly.multipolyline) {
-        return new L.MultiPolyline(polygons);
-      }
-      else {
-        return new L.MultiPolygon(polygons);
-      }
-    },
+      polygons.push(latlngs);
+    }
+    if (multipoly.multipolyline) {
+      return new L.MultiPolyline(polygons);
+    }
+    else {
+      return new L.MultiPolygon(polygons);
+    }
+  };
 
-    create_json: function(json) {
-      lJSON = new L.GeoJSON();
+  Drupal.Leaflet.prototype.create_json = function (json) {
+    lJSON = new L.GeoJSON();
 
-      lJSON.on('featureparse', function (e) {
-        e.layer.bindPopup(e.properties.popup);
+    lJSON.on('featureparse', function (e) {
+      e.layer.bindPopup(e.properties.popup);
 
-        for (var layer_id in e.layer._layers) {
-          for (var i in e.layer._layers[layer_id]._latlngs) {
-            Drupal.leaflet.bounds.push(e.layer._layers[layer_id]._latlngs[i]);
-          }
+      for (var layer_id in e.layer._layers) {
+        for (var i in e.layer._layers[layer_id]._latlngs) {
+          Drupal.Leaflet.bounds.push(e.layer._layers[layer_id]._latlngs[i]);
         }
+      }
 
-        if (e.properties.style) {
-          e.layer.setStyle(e.properties.style);
-        }
+      if (e.properties.style) {
+        e.layer.setStyle(e.properties.style);
+      }
 
-        if (e.properties.leaflet_id) {
-          e.layer._leaflet_id = e.properties.leaflet_id;
-        }
-      });
+      if (e.properties.leaflet_id) {
+        e.layer._leaflet_id = e.properties.leaflet_id;
+      }
+    });
 
-      lJSON.addData(json);
-      return lJSON;
-    },
+    lJSON.addData(json);
+    return lJSON;
+  };
 
-    fitbounds: function(lMap) {
-      if (this.bounds.length > 0) {
-        lMap.fitBounds(new L.LatLngBounds(this.bounds));
-      }
+  Drupal.Leaflet.prototype.fitbounds = function () {
+    if (this.bounds.length > 0) {
+      this.lMap.fitBounds(new L.LatLngBounds(this.bounds));
     }
-
-  }
+    // If we have provided a zoom level, then use it after fitting bounds.
+    if (this.settings.zoom) {
+      this.lMap.setZoom(this.settings.zoom);
+    }
+  };
 
 })(jQuery);
diff --git a/leaflet.libraries.yml b/leaflet.libraries.yml
new file mode 100644
index 0000000..0dffe3a
--- /dev/null
+++ b/leaflet.libraries.yml
@@ -0,0 +1,19 @@
+leaflet:
+  remote: http://leaflet.cloudmade.com
+  version: 0.7.3
+  license:
+    name: Leaflet-License
+    url: https://github.com/Leaflet/Leaflet/blob/v0.7.3/LICENSE
+    gpl-compatible: true
+  js:
+    //cdn.leafletjs.com/leaflet-0.7.3/leaflet.js: {}
+  css:
+    component:
+      //cdn.leafletjs.com/leaflet-0.7.3/leaflet.css: {}
+
+leaflet-drupal:
+    version: VERSION
+    js:
+      leaflet.drupal.js: {}
+    dependencies:
+      - leaflet/leaflet
diff --git a/leaflet.module b/leaflet.module
index 2721d11..954fc99 100644
--- a/leaflet.module
+++ b/leaflet.module
@@ -5,7 +5,11 @@
 function leaflet_theme($existing, $type, $theme, $path) {
   return array(
     'leaflet_map' => array(
-      'arguments' => array('map_id' => NULL, 'height' => '400px'),
+      'variables' => array(
+        'map_id' => NULL,
+        'height' => '400px',
+        'map' => array(),
+      ),
       // When theme('leaflet_map'...) is called, the system will look for
       // /leaflet/templates/leaflet_map.html.twig.
       'template' => 'leaflet_map',
@@ -13,64 +17,6 @@ function leaflet_theme($existing, $type, $theme, $path) {
   );
 }
 
-function leaflet_libraries_get_path($base) {
-  return module_exists('libraries') ? libraries_get_path($base) : "sites/all/libraries/$base";
-}
-
-/**
- * Implements hook_library_info().
- */
-function leaflet_library_info() {
-  $leaflet_lib_path = leaflet_libraries_get_path('leaflet');
-
-  // Not sure this format is right for D8
-  $libraries['leaflet'] = array(
-    'title' => 'Leaflet',
-    'website' => 'http://leaflet.cloudmade.com',
-    'version' => '0.6',
-    'js' => array(
-      array(
-        'type' => 'inline',
-        'data' => 'L_ROOT_URL = "' . base_path() . $leaflet_lib_path . '/";',
-        'group' => JS_LIBRARY,
-        'preprocess' => FALSE
-      ),
-      array(
-        'type' => 'file',
-        'data' => "$leaflet_lib_path/leaflet.js",
-        'group' => JS_LIBRARY,
-        'preprocess' => FALSE
-      ),
-    ),
-    'css' => array(
-      "$leaflet_lib_path/leaflet.css" => array(
-        'type' => 'file',
-        'media' => 'screen',
-      ),
-      "$leaflet_lib_path/leaflet.ie.css" => array(
-        'browsers' => array(
-          'IE' => 'lte IE 8',
-          '!IE' => FALSE,
-        ),
-      ),
-    ),
-  );
-
-  $libraries['leaflet-drupal'] = array(
-    'title' => 'Leaflet initialization',
-    'website' => 'http://drupal.org/project/leaflet',
-    'version' => VERSION,
-    'js' => array(
-      drupal_get_path('module', 'leaflet') . '/leaflet.drupal.js' => array(),
-    ),
-    'dependencies' => array(
-      array('leaflet', 'leaflet'),
-    ),
-  );
-
-  return $libraries;
-}
-
 /**
  * Load all Leaflet required client files and return markup for a map.
  *
@@ -78,10 +24,10 @@ function leaflet_library_info() {
  * @param array $features
  * @param string $height
  *
- * @return string map markup
+ * @return array render array
  */
 function leaflet_render_map($map, $features = array(), $height = '400px') {
-  $map_id = drupal_html_id('leaflet_map');
+  $map_id = Drupal\Component\Utility\Html::getUniqueId('leaflet_map');
 
   // Allow map definitions to provide a default icon.
   if (isset($map['icon']['iconUrl'])) {
@@ -91,19 +37,28 @@ function leaflet_render_map($map, $features = array(), $height = '400px') {
       }
     }
   }
-  $settings[0] = array(
+  $settings[$map_id] = array(
     'mapId' => $map_id,
     'map' => $map,
-    'features' => $features,
+    // JS only works with arrays, make sure we have one with numeric keys.
+    'features' => array_values($features),
+  );
+  return array(
+    '#theme' => 'leaflet_map',
+    '#map_id' => $map_id,
+    '#height' => $height,
+    '#map' => $map,
+    '#attached' => array(
+      'library' => array('leaflet/leaflet-drupal'),
+      'js' => array(
+        array('type' => 'setting', 'data' => array('leaflet' => $settings)),
+      ),
+    ),
   );
-  drupal_add_library('leaflet', 'leaflet-drupal');
-  drupal_add_js(array('leaflet' => array($settings)), 'setting');
-
-  return theme('leaflet_map', array('map_id' => $map_id, 'height' => $height));
 }
 
 /**
- * Get all avaialble Leaflet map definitions.
+ * Get all available Leaflet map definitions.
  *
  * @param string $map
  */
@@ -115,16 +70,16 @@ function leaflet_map_get_info($map = NULL) {
   $map_info = &$drupal_static_fast['leaflet_map_info'];
 
   if (empty($map_info)) {
-    if ($cached = cache()->get('leaflet_map_info')) {
+    if ($cached = Drupal::cache()->get('leaflet_map_info')) {
       $map_info = $cached->data;
     }
     else {
-      $map_info = module_invoke_all('leaflet_map_info');
+      $map_info = Drupal::moduleHandler()->invokeAll('leaflet_map_info');
 
       // Let other modules alter the map info.
-      drupal_alter('leaflet_map_info', $map_info);
+      Drupal::moduleHandler()->alter('leaflet_map_info', $map_info);
 
-      cache()->set('leaflet_map_info', $map_info);
+      Drupal::cache()->set('leaflet_map_info', $map_info);
     }
   }
 
@@ -144,33 +99,33 @@ function leaflet_map_get_info($map = NULL) {
 function leaflet_leaflet_map_info() {
   return array(
     'OSM Mapnik' =>
-    array(
-      'label' => 'OSM Mapnik',
-      'description' => t('Leaflet default map.'),
-      'settings' => array(
-        // 'zoom' => 18,
-        'minZoom' => 0,
-        'maxZoom' => 18,
-        'dragging' => TRUE,
-        'touchZoom' => TRUE,
-        'scrollWheelZoom' => TRUE,
-        'doubleClickZoom' => TRUE,
-        'zoomControl' => TRUE,
-        'attributionControl' => TRUE,
-        'trackResize' => TRUE,
-        'fadeAnimation' => TRUE,
-        'zoomAnimation' => TRUE,
-        'closePopupOnClick' => TRUE,
-      ),
-      'layers' => array(
-        'earth' => array(
-          'urlTemplate' => 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
-          'options' => array(
-            'attribution' => 'OSM Mapnik'
-          )
+      array(
+        'label' => 'OSM Mapnik',
+        'description' => t('Leaflet default map.'),
+        'settings' => array(
+          // 'zoom' => 18,
+          'minZoom' => 0,
+          'maxZoom' => 18,
+          'dragging' => TRUE,
+          'touchZoom' => TRUE,
+          'scrollWheelZoom' => TRUE,
+          'doubleClickZoom' => TRUE,
+          'zoomControl' => TRUE,
+          'attributionControl' => TRUE,
+          'trackResize' => TRUE,
+          'fadeAnimation' => TRUE,
+          'zoomAnimation' => TRUE,
+          'closePopupOnClick' => TRUE,
+        ),
+        'layers' => array(
+          'earth' => array(
+            'urlTemplate' => 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+            'options' => array(
+              'attribution' => 'OSM Mapnik'
+            )
+          ),
         ),
       ),
-    ),
   );
 }
 
@@ -269,26 +224,49 @@ function leaflet_requirements($phase) {
   if ($phase != 'runtime') {
     return $requirements;
   }
-  // Ensure js library is installed. Show number of maps avaialble.
-  if (file_exists(leaflet_libraries_get_path('leaflet') . '/leaflet.js')) {
-    $requirements['leaflet'] = array(
-      'title' => t('Leaflet library'),
-      'value' => t('Installed. @maps available.', array(
-        '@maps' => format_plural(count(leaflet_map_get_info()), 'One map', '@count maps'))),
-      'severity' => REQUIREMENT_OK,
-    );
+  $library = Drupal::service('library.discovery')
+    ->getLibraryByName('leaflet', 'leaflet');
+
+  $requirements['leaflet'] = array(
+    'title' => Drupal::translation()->translate('Leaflet library')
+  );
+  $maps_info = Drupal::translation()->translate('@maps available.', array(
+      '@maps' => Drupal::translation()
+        ->formatPlural(count(leaflet_map_get_info()), 'One map', '@count maps')
+    )
+  );
+
+  // Check the defined type of the leaflet.js file; if it is external then
+  // assume that we are using a CDN version.
+  if ($library['js'][0]['type'] == 'external') {
+    $requirements['leaflet']['value'] = Drupal::translation()
+        ->translate('Using CDN version @version.', array(
+          '@version' => $library['version']
+        )) . ' ' . $maps_info;
   }
+  // If leaflet.js is defined to be a local file, check that it exists and show
+  // an error if it does not exist.
   else {
-    $requirements['leaflet'] = array(
-      'title' => t('Leaflet library not found'),
-      'value' => t('The !leaflet javascript library was not found. Please !download it into the libraries folder. Also ensure that the library is named leaflet with a lower case "l".',
-        array(
-          '!leaflet' => l('leaflet', 'http://leaflet.cloudmade.com'),
-          '!download' => l('download', 'http://leaflet.cloudmade.com/download.html'),
-        )
-      ),
-      'severity' => REQUIREMENT_ERROR,
-    );
+    if (file_exists($library['js'][0]['data'])) {
+      $requirements['leaflet']['value'] = Drupal::translation()
+          ->translate('Leaflet @version library installed at @path.', array(
+            '@version' => $library['version'],
+            '@path' => $library['js'][0]['data'],
+          )) . ' ' . $maps_info;
+      $requirements['leaflet']['severity'] = REQUIREMENT_OK;
+    }
+    else {
+      $requirements['leaflet']['value'] = Drupal::translation()
+        ->translate('Leaflet @version library not found at @path. Please !download it to @directory, or undo your changes to the libraries registry to use the CDN version.',
+          array(
+            '@version' => $library['version'],
+            '@path' => $library['js'][0]['data'],
+            '@directory' => dirname($library['js'][0]['data']),
+            '!download' => Drupal::l('download', Drupal\Core\Url::fromUri($library['remote'])),
+          )
+        );
+      $requirements['leaflet']['severity'] = REQUIREMENT_ERROR;
+    }
   }
 
   return $requirements;
diff --git a/leaflet_views/leaflet_views.api.php b/leaflet_views/leaflet_views.api.php
new file mode 100644
index 0000000..8ca2cfe
--- /dev/null
+++ b/leaflet_views/leaflet_views.api.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @file
+ * Hook documentation for leaflet_views module.
+ */
+
+/**
+ * Adjust the array representing a leaflet feature/marker.
+ *
+ * @param array $feature
+ *   The leaflet feature. Available keys are:
+ *   - type: Indicates the type of feature (usually one of these: point,
+ *     polygon, linestring, multipolygon, multipolyline).
+ *   - popup: This value is displayed in a popup after the user clicks on the
+ *     feature.
+ *   - label: Not used at the moment.
+ *   - Other possible keys include "lat", "lon", "points", "component",
+ *     depending on feature type. {@see leaflet_process_geofield()} for details.
+ * @param \Drupal\views\ResultRow $row
+ *   The views result row.
+ * @param \Drupal\leaflet_views\Plugin\views\row\LeafletMarker $rowPlugin
+ *   The row plugin used for rendering the feature.
+ */
+function hook_leaflet_views_feature_alter(array &$feature, \Drupal\views\ResultRow $row, \Drupal\leaflet_views\Plugin\views\row\LeafletMarker $rowPlugin) {
+}
+
+/**
+ * Adjust the array representing a leaflet feature group.
+ *
+ * @param array $group
+ *   The leaflet feature group. Available keys are:
+ *   - group: Indicates whether the contained features should be rendered as a
+ *     layer group. Set to FALSE to render contained features ungrouped.
+ *   - features: List of features contained in this group.
+ *   - label: The group label, e.g. used for the layer control widget.
+ * @param \Drupal\leaflet_views\Plugin\views\style\MarkerDefault $stylePlugin
+ *   The style plugin used for rendering the feature group.
+ */
+function hook_leaflet_views_feature_group_alter(array &$group, \Drupal\leaflet_views\Plugin\views\style\MarkerDefault $stylePlugin) {
+}
diff --git a/leaflet_views/lib/Drupal/leaflet_views/Plugin/views/style/LeafletMap.php b/leaflet_views/lib/Drupal/leaflet_views/Plugin/views/style/LeafletMap.php
deleted file mode 100644
index b41eb6e..0000000
--- a/leaflet_views/lib/Drupal/leaflet_views/Plugin/views/style/LeafletMap.php
+++ /dev/null
@@ -1,393 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\leaflet_views\Plugin\views\style\LeafletMap.
- */
-
-namespace Drupal\leaflet_views\Plugin\views\style;
-
-use Drupal\Component\Annotation\Plugin;
-use Drupal\Core\Annotation\Translation;
-use Drupal\views\ViewExecutable;
-use Drupal\views\Plugin\views\display\DisplayPluginBase;
-use Drupal\views\Plugin\views\style\StylePluginBase;
-
-
-/**
- * Style plugin to render a View output as a Leaflet map.
- *
- * @ingroup views_style_plugins
- *
- * Attributes set below end up in the $this->definition[] array.
- *
- * @Plugin(
- *   id = "leafet_map",
- *   title = @Translation("Leaflet map"),
- *   help = @Translation("Displays a View as a Leaflet map."),
- *   type = "normal",
- *   theme = "leaflet-map",
- *   even_empty = TRUE
- * )
- */
-class LeafletMap extends StylePluginBase {
-
-  /**
-   * If this view is displaying an entity, save the entity type and info.
-   */
-  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
-
-    // Set these before calling parent::init() as it uses these.
-    $this->definition['even empty'] = TRUE; // cannot have space in annotation, so doing it here
-    $this->usesOptions = TRUE;
-    $this->usesRowPlugin = FALSE;
-    $this->usesRowClass = FALSE;
-    $this->usesGrouping = FALSE;
-    $this->usesFields = TRUE;
-
-    parent::init($view, $display, $options);
-
-    // For later use, set entity info related to the View's base table.
-    $base_tables = array_keys($view->getBaseTables());
-    $base_table = reset($base_tables);
-    foreach (entity_get_info() as $key => $info) {
-      if (isset($info['base_table']) && $info['base_table'] == $base_table) {
-        $this->entity_type = $key;
-        $this->entity_info = $info;
-        return;
-      }
-    }
-  }
-
-  /**
-   * Set default options
-   */
-  protected function defineOptions() {
-    $options = parent::defineOptions();
-    $options['data_source'] = array('default' => '');
-    $options['name_field'] = array('default' => '');
-    $options['description_field'] = array('default' => '');
-    $options['view_mode'] = array('default' => 'full');
-    $options['map'] = array('default' => '');
-    $options['height'] = array('default' => '400');
-    $options['icon'] = array('default' => array());
-    return $options;
-  }
-
-  /**
-   * Options form
-   */
-  public function buildOptionsForm(&$form, &$form_state) {
-    parent::buildOptionsForm($form, $form_state);
-
-    // Get a list of fields and a sublist of geo data fields in this view
-    $fields = array();
-    $fields_geo_data = array();
-    foreach ($this->displayHandler->getHandlers('field') as $field_id => $handler) {
-      $label = $handler->label() ?: $field_id;
-      $fields[$field_id] = $label;
-      if (!empty($handler->field_info['type']) && $handler->field_info['type'] == 'geofield') {
-        $fields_geo_data[$field_id] = $label;
-      }
-    }
-
-    // Check whether we have a geo data field we can work with
-    if (!count($fields_geo_data)) {
-      $form['error'] = array(
-        '#markup' => t('Please add at least one geofield to the view.'),
-      );
-      return;
-    }
-
-    // Map preset.
-    $form['data_source'] = array(
-      '#type' => 'select',
-      '#title' => t('Data Source'),
-      '#description' => t('Which field contains geodata?'),
-      '#options' => $fields_geo_data,
-      '#default_value' => $this->options['data_source'],
-      '#required' => TRUE,
-    );
-
-    // Name field
-    $form['name_field'] = array(
-      '#type' => 'select',
-      '#title' => t('Title Field'),
-      '#description' => t('Choose the field which will appear as a title on tooltips.'),
-      '#options' => array_merge(array('' => ''), $fields),
-      '#default_value' => $this->options['name_field'],
-    );
-
-    $desc_options = array_merge(array('' => ''), $fields);
-    // Add an option to render the entire entity using a view mode
-    if ($this->entity_type) {
-      $desc_options += array(
-        '#rendered_entity' => '<' . t('!entity entity', array('!entity' => $this->entity_type)) . '>',
-      );
-    }
-
-    $form['description_field'] = array(
-      '#type' => 'select',
-      '#title' => t('Description Field'),
-      '#description' => t('Choose the field or rendering method which will appear as a description on tooltips or popups.'),
-      '#required' => FALSE,
-      '#options' => $desc_options,
-      '#default_value' => $this->options['description_field'],
-    );
-
-    if ($this->entity_type) {
-
-      // Get the human readable labels for the entity view modes.
-      $view_mode_options = array();
-      foreach (entity_get_view_modes($this->entity_type) as $key => $view_mode) {
-        $view_mode_options[$key] = $view_mode['label'];
-      }
-      // The View Mode drop-down is visibile conditional on "#rendered_entity"
-      // being selected in the Description drop-down above.
-      $form['view_mode'] = array(
-        '#type' => 'select',
-        '#title' => t('View mode'),
-        '#description' => t('View modes are ways of displaying entities.'),
-        '#options' => $view_mode_options,
-        '#default_value' => !empty($this->options['view_mode']) ? $this->options['view_mode'] : 'full',
-        '#states' => array(
-          'visible' => array(
-            ':input[name="style_options[description_field]"]' => array(
-              'value' => '#rendered_entity')
-          )
-        )
-      );
-    }
-
-    // Choose a map preset
-    $map_options = array();
-    foreach (leaflet_map_get_info() as $key => $map) {
-      $map_options[$key] = t($map['label']);
-    }
-    $form['map'] = array(
-      '#title' => t('Map'),
-      '#type' => 'select',
-      '#options' => $map_options,
-      '#default_value' => $this->options['map'] ?: '',
-      '#required' => TRUE,
-    );
-
-    $form['height'] = array(
-      '#title' => t('Map height'),
-      '#type' => 'textfield',
-      '#field_suffix' => t('px'),
-      '#size' => 4,
-      '#default_value' => $this->options['height'],
-      '#required' => FALSE,
-    );
-
-    $form['icon'] = array(
-      '#title' => t('Map Icon'),
-      '#type' => 'fieldset',
-      '#collapsible' => TRUE,
-      '#collapsed' => !isset($this->options['icon']['iconUrl']),
-    );
-
-    $form['icon']['iconUrl'] = array(
-      '#title' => t('Icon URL'),
-      '#description' => t('Can be an absolute or relative URL.'),
-      '#type' => 'textfield',
-      '#maxlength' => 999,
-      '#default_value' => $this->options['icon']['iconUrl'] ?: '',
-    );
-
-    $form['icon']['shadowUrl'] = array(
-      '#title' => t('Icon Shadow URL'),
-      '#type' => 'textfield',
-      '#maxlength' => 999,
-      '#default_value' => $this->options['icon']['shadowUrl'] ?: '',
-    );
-
-    $form['icon']['iconSize'] = array(
-      '#title' => t('Icon Size'),
-      '#type' => 'fieldset',
-      '#collapsible' => FALSE,
-      '#description' => t('Size of the icon image in pixels.')
-    );
-
-    $form['icon']['iconSize']['x'] = array(
-      '#title' => t('Width'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => isset($this->options['icon']['iconSize']['x']) ? $this->options['icon']['iconSize']['x'] : '',
-      '#element_validate' => array('form_validate_number'),
-    );
-
-    $form['icon']['iconSize']['y'] = array(
-      '#title' => t('Height'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => isset($this->options['icon']['iconSize']['y']) ? $this->options['icon']['iconSize']['y'] : '',
-      '#element_validate' => array('form_validate_number'),
-    );
-
-    $form['icon']['iconAnchor'] = array(
-      '#title' => t('Icon Anchor'),
-      '#type' => 'fieldset',
-      '#collapsible' => FALSE,
-      '#description' => t('The coordinates of the "tip" of the icon (relative to its top left corner). The icon will be aligned so that this point is at the marker\'s geographical location.')
-    );
-
-    $form['icon']['iconAnchor']['x'] = array(
-      '#title' => t('X'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => isset($this->options['icon']['iconAnchor']['x']) ? $this->options['icon']['iconAnchor']['x'] : '',
-      '#element_validate' => array('form_validate_number'),
-    );
-
-    $form['icon']['iconAnchor']['y'] = array(
-      '#title' => t('Y'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => isset($this->options['icon']['iconAnchor']['y']) ? $this->options['icon']['iconAnchor']['y'] : '',
-      '#element_validate' => array('form_validate_number'),
-    );
-
-    $form['icon']['shadowAnchor'] = array(
-      '#title' => t('Shadow Anchor'),
-      '#type' => 'fieldset',
-      '#collapsible' => FALSE,
-      '#description' => t('The point from which the shadow is shown.')
-    );
-    $form['icon']['shadowAnchor']['x'] = array(
-      '#title' => t('X'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => isset($this->options['icon']['shadowAnchor']['x']) ? $this->options['icon']['shadowAnchor']['x'] : '',
-      '#element_validate' => array('form_validate_number'),
-    );
-    $form['icon']['shadowAnchor']['y'] = array(
-      '#title' => t('Y'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => isset($this->options['icon']['shadowAnchor']['y']) ? $this->options['icon']['shadowAnchor']['y'] : '',
-      '#element_validate' => array('form_validate_number'),
-    );
-
-    $form['icon']['popupAnchor'] = array(
-      '#title' => t('Popup Anchor'),
-      '#type' => 'fieldset',
-      '#collapsible' => FALSE,
-      '#description' => t('The point from which the marker popup opens, relative to the anchor point.')
-    );
-
-    $form['icon']['popupAnchor']['x'] = array(
-      '#title' => t('X'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => isset($this->options['icon']['popupAnchor']['x']) ? $this->options['icon']['popupAnchor']['x'] : '',
-      '#element_validate' => array('form_validate_number'),
-    );
-
-    $form['icon']['popupAnchor']['y'] = array(
-      '#title' => t('Y'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => isset($this->options['icon']['popupAnchor']['y']) ? $this->options['icon']['popupAnchor']['y'] : '',
-      '#element_validate' => array('form_validate_number'),
-    );
-  }
-
-  /**
-   * Validates the options form.
-   */
-  public function validateOptionsForm(&$form, &$form_state) {
-    parent::validateOptionsForm($form, $form_state);
-
-    $style_options = $form_state['values']['style_options'];
-    if (!is_numeric($style_options['height']) || $style_options['height'] <= 0) {
-      form_error($form['height'], t('Map height needs to be a positive number.'));
-    }
-    $icon_options = $style_options['icon'];
-    if (!empty($icon_options['iconUrl']) && !valid_url($icon_options['iconUrl'])) {
-      form_error($form['icon']['iconUrl'], t('Icon URL is invalid.'));
-    }
-    if (!empty($icon_options['shadowUrl']) && !valid_url($icon_options['shadowUrl'])) {
-      form_error($form['icon']['shadowUrl'], t('Shadow URL is invalid.'));
-    }
-    if (!is_numeric($icon_options['iconSize']['x']) || $icon_options['iconSize']['x'] <= 0) {
-      form_error($form['icon']['iconSize']['x'], t('Icon width needs to be a positive number.'));
-    }
-    if (!is_numeric($icon_options['iconSize']['y']) || $icon_options['iconSize']['y'] <= 0) {
-      form_error($form['icon']['iconSize']['x'], t('Icon height needs to be a positive number.'));
-    }
-  }
-
-  /**
-   * Renders the View.
-   */
-  function render() {
-    $data = array();
-    $geofield_name = $this->options['data_source'];
-    if ($this->options['data_source']) {
-      $this->renderFields($this->view->result);
-      foreach ($this->view->result as $id => $result) {
-
-        $geofield_value = $this->getFieldValue($id, $geofield_name);
-
-        if (empty($geofield_value)) {
-          // In case the result is not among the raw results, get it from the
-          // rendered results.
-          $geofield_value = $this->rendered_fields[$id][$geofield_name];
-        }
-        if (!empty($geofield_value)) {
-          $points = leaflet_process_geofield($geofield_value);
-
-          // Render the entity with the selected view mode
-          if ($this->options['description_field'] === '#rendered_entity' && is_object($result)) {
-            $entity = entity_load($this->entity_type, $result->{$this->entity_info['entity_keys']['id']});
-            $build = entity_view($entity, $this->options['view_mode']);
-            $description = drupal_render($build);
-          }
-          // Normal rendering via fields
-          elseif ($this->options['description_field']) {
-            $description = $this->rendered_fields[$id][$this->options['description_field']];
-          }
-
-          // Attach pop-ups if we have a description field
-          if (isset($description)) {
-            foreach ($points as &$point) {
-              $point['popup'] = $description;
-            }
-          }
-
-          // Attach also titles, they might be used later on
-          if ($this->options['name_field']) {
-            foreach ($points as &$point) {
-              $point['label'] = $this->rendered_fields[$id][$this->options['name_field']];
-            }
-          }
-
-          $data = array_merge($data, $points);
-
-          if (!empty($this->options['icon']) && $this->options['icon']['iconUrl']) {
-            foreach ($data as $key => $feature) {
-              $data[$key]['icon'] = $this->options['icon'];
-            }
-          }
-        }
-      }
-
-      $map = leaflet_map_get_info($this->options['map']);
-
-      if (!empty($data)) {
-        return leaflet_render_map($map, $data, $this->options['height'] . 'px');
-      }
-    }
-    return '';
-  }
-}
diff --git a/leaflet_views/src/Plugin/views/display/LeafletAttachment.php b/leaflet_views/src/Plugin/views/display/LeafletAttachment.php
new file mode 100644
index 0000000..b58cf84
--- /dev/null
+++ b/leaflet_views/src/Plugin/views/display/LeafletAttachment.php
@@ -0,0 +1,128 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\leaflet_views\Plugin\views\display\LeafletDataAttachment.
+ */
+
+namespace Drupal\leaflet_views\Plugin\views\display;
+
+
+use Drupal\views\Plugin\views\display\Attachment;
+use Drupal\views\ViewExecutable;
+
+/**
+ * The plugin which handles attachment of additional leaflet features to
+ * leaflet map views.
+ *
+ * @ingroup views_display_plugins
+ *
+ * @ViewsDisplay(
+ *   id = "leaflet_attachment",
+ *   title = @Translation("Leaflet Attachment"),
+ *   help = @Translation("Add additional markers to a leaflet map."),
+ * )
+ *
+ * @todo We only use very few features from the parent class Attachment, so this
+ *       should probably just extend DisplayPluginBase to simplify things.
+ */
+class LeafletAttachment extends Attachment {
+
+  /**
+   * Whether the display allows the use of a pager or not.
+   *
+   * @var bool
+   */
+  protected $usesPager = FALSE;
+
+  /**
+   * Whether the display allows the use of a 'more' link or not.
+   *
+   * @var bool
+   */
+  protected $usesMore = FALSE;
+
+  /**
+   * Whether the display allows area plugins.
+   *
+   * @var bool
+   */
+  protected $usesAreas = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function usesLinkDisplay() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function attachTo(ViewExecutable $view, $display_id, array &$build) {
+    $displays = $this->getOption('displays');
+
+    if (empty($displays[$display_id])) {
+      return;
+    }
+
+    if (!$this->access()) {
+      return;
+    }
+
+    $args = $this->getOption('inherit_arguments') ? $this->view->args : array();
+    $view->setArguments($args);
+    $view->setDisplay($this->display['id']);
+    if ($this->getOption('inherit_pager')) {
+      $view->display_handler->usesPager = $this->view->displayHandlers->get($display_id)
+        ->usesPager();
+      $view->display_handler->setOption('pager', $this->view->displayHandlers->get($display_id)
+        ->getOption('pager'));
+    }
+    if ($render = $view->render()) {
+      $this->view->attachment_before[] = $render + array(
+          '#leaflet-attachment' => TRUE,
+        );
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $rows = (!empty($this->view->result) || $this->view->style_plugin->evenEmpty()) ? $this->view->style_plugin->render($this->view->result) : array();
+
+    // The element is rendered during preview only; when used as an attachment
+    // in the Leaflet class, only the 'rows' property is used.
+    $element = array(
+      '#markup' => print_r($rows, TRUE),
+      '#prefix' => '<pre>',
+      '#suffix' => '</pre>',
+      '#attached' => &$this->view->element['#attached'],
+      'rows' => $rows,
+    );
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getType() {
+    return 'leaflet';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+
+    // Overrides for standard stuff.
+    $options['style']['contains']['type']['default'] = 'leaflet_marker_default';
+    $options['defaults']['default']['style'] = FALSE;
+    $options['row']['contains']['type']['default'] = 'leaflet_marker';
+    $options['defaults']['default']['row'] = FALSE;
+
+    return $options;
+  }
+}
diff --git a/leaflet_views/src/Plugin/views/row/LeafletMarker.php b/leaflet_views/src/Plugin/views/row/LeafletMarker.php
new file mode 100644
index 0000000..0a26fd6
--- /dev/null
+++ b/leaflet_views/src/Plugin/views/row/LeafletMarker.php
@@ -0,0 +1,247 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\leaflet_views\Plugin\views\row\LeafletMarker.
+ */
+
+namespace Drupal\leaflet_views\Plugin\views\row;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\Plugin\views\row\RowPluginBase;
+use Drupal\views\ResultRow;
+use Drupal\views\ViewExecutable;
+
+/**
+ * Plugin which formats a row as a leaflet marker.
+ *
+ * @ViewsRow(
+ *   id = "leaflet_marker",
+ *   title = @Translation("Leaflet Marker"),
+ *   help = @Translation("Display the row as a leaflet marker."),
+ *   display_types = {"leaflet"},
+ * )
+ */
+class LeafletMarker extends RowPluginBase {
+
+  /**
+   * Overrides Drupal\views\Plugin\Plugin::$usesOptions.
+   */
+  protected $usesOptions = TRUE;
+
+  /**
+   * Does the row plugin support to add fields to it's output.
+   *
+   * @var bool
+   */
+  protected $usesFields = TRUE;
+
+  /**
+   * @var string The main entity type id for the view base table.
+   */
+  protected $entityTypeId;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+    parent::init($view, $display, $options);
+    // First base table should correspond to main entity type.
+    $base_table = key($this->view->getBaseTables());
+    $views_definition = \Drupal::service('views.views_data')->get($base_table);
+    $this->entityTypeId = $views_definition['table']['entity type'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::buildOptionsForm($form, $form_state);
+
+    // Get a list of fields and a sublist of geo data fields in this view
+    // @todo use $fields = $this->displayHandler->getFieldLabels();
+    $fields = array();
+    $fields_geo_data = array();
+    foreach ($this->displayHandler->getHandlers('field') as $field_id => $handler) {
+      $label = $handler->adminLabel() ?: $field_id;
+      $fields[$field_id] = $label;
+      if (is_a($handler, '\Drupal\field\Plugin\views\field\Field')) {
+        $field_storage_definitions = \Drupal::entityManager()
+          ->getFieldStorageDefinitions($handler->getEntityType());
+        $field_storage_definition = $field_storage_definitions[$handler->definition['field_name']];
+
+        if ($field_storage_definition->getType() == 'geofield') {
+          $fields_geo_data[$field_id] = $label;
+        }
+      }
+    }
+
+    // Check whether we have a geo data field we can work with
+    if (!count($fields_geo_data)) {
+      $form['error'] = array(
+        '#markup' => $this->t('Please add at least one geofield to the view.'),
+      );
+      return;
+    }
+
+    // Map preset.
+    $form['data_source'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Data Source'),
+      '#description' => $this->t('Which field contains geodata?'),
+      '#options' => $fields_geo_data,
+      '#default_value' => $this->options['data_source'],
+      '#required' => TRUE,
+    );
+
+    // Name field
+    $form['name_field'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Title Field'),
+      '#description' => $this->t('Choose the field which will appear as a title on tooltips.'),
+      '#options' => $fields,
+      '#default_value' => $this->options['name_field'],
+      '#empty_value' => '',
+    );
+
+    $desc_options = $fields;
+    // Add an option to render the entire entity using a view mode
+    if ($this->entityTypeId) {
+      $desc_options += array(
+        '#rendered_entity' => '<' . $this->t('Rendered !entity entity', array('!entity' => $this->entityTypeId)) . '>',
+      );
+    }
+
+    $form['description_field'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Description Field'),
+      '#description' => $this->t('Choose the field or rendering method which will appear as a description on tooltips or popups.'),
+      '#options' => $desc_options,
+      '#default_value' => $this->options['description_field'],
+      '#empty_value' => '',
+    );
+
+    if ($this->entityTypeId) {
+
+      // Get the human readable labels for the entity view modes.
+      $view_mode_options = array();
+      foreach (\Drupal::entityManager()
+                 ->getViewModes($this->entityTypeId) as $key => $view_mode) {
+        $view_mode_options[$key] = $view_mode['label'];
+      }
+      // The View Mode drop-down is visible conditional on "#rendered_entity"
+      // being selected in the Description drop-down above.
+      $form['view_mode'] = array(
+        '#type' => 'select',
+        '#title' => $this->t('View mode'),
+        '#description' => $this->t('View modes are ways of displaying entities.'),
+        '#options' => $view_mode_options,
+        '#default_value' => !empty($this->options['view_mode']) ? $this->options['view_mode'] : 'full',
+        '#states' => array(
+          'visible' => array(
+            ':input[name="row_options[description_field]"]' => array(
+              'value' => '#rendered_entity'
+            )
+          )
+        )
+      );
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render($row) {
+    $geofield_value = $this->view->getStyle()
+      ->getField($row->index, $this->options['data_source']);
+
+    if (empty($geofield_value)) {
+      return FALSE;
+    }
+
+    // @todo This assumes that the user has selected WKT as the geofield output
+    // formatter in the views field settings, and fails otherwise. Very brittle.
+    $result = leaflet_process_geofield($geofield_value);
+
+    // Convert the list of geo data points into a list of leaflet markers.
+    return $this->renderLeafletMarkers($result, $row);
+  }
+
+  /**
+   * Converts the given list of geo data points into a list of leaflet markers.
+   *
+   * @param $points
+   *   A list of geofield points from {@link leaflet_process_geofield()}.
+   * @param ResultRow $row
+   *   The views result row.
+   * @return array
+   *   List of leaflet markers.
+   */
+  protected function renderLeafletMarkers($points, ResultRow $row) {
+    // Render the entity with the selected view mode
+    $popup_body = '';
+    if ($this->options['description_field'] === '#rendered_entity' && is_object($row->_entity)) {
+      $entity = $row->_entity;
+      $build = entity_view($entity, $this->options['view_mode']);
+      $popup_body = drupal_render($build);
+    }
+    // Normal rendering via fields
+    elseif ($this->options['description_field']) {
+      $popup_body = $this->view->getStyle()
+        ->getField($row->index, $this->options['description_field']);
+    }
+
+    $label = $this->view->getStyle()
+      ->getField($row->index, $this->options['name_field']);
+
+    foreach ($points as &$point) {
+      $point['popup'] = $popup_body;
+      $point['label'] = $label;
+
+      // Allow sub-classes to adjust the marker.
+      $this->alterLeafletMarker($point, $row);
+
+      // Allow modules to adjust the marker
+      \Drupal::moduleHandler()
+        ->alter('leaflet_views_feature', $point, $row, $this);
+    }
+    return $points;
+  }
+
+  /**
+   * Chance for sub-classes to adjust the leaflet marker array.
+   *
+   * For example, this can be used to add in icon configuration.
+   *
+   * @param array $point
+   * @param ResultRow $row
+   */
+  protected function alterLeafletMarker(array &$point, ResultRow $row) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate() {
+    $errors = parent::validate();
+    // @todo raise validation error if we have no geofield.
+    if (empty($this->options['data_source'])) {
+      $errors[] = $this->t('Row @row requires the data source to be configured.', array('@row' => $this->definition['title']));
+    }
+    return $errors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+
+    $options['data_source'] = array('default' => '');
+    $options['name_field'] = array('default' => '');
+    $options['description_field'] = array('default' => '');
+    $options['view_mode'] = array('default' => 'teaser');
+
+    return $options;
+  }
+}
diff --git a/leaflet_views/src/Plugin/views/style/Leaflet.php b/leaflet_views/src/Plugin/views/style/Leaflet.php
new file mode 100644
index 0000000..00edf9f
--- /dev/null
+++ b/leaflet_views/src/Plugin/views/style/Leaflet.php
@@ -0,0 +1,159 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\leaflet_views\Plugin\views\style\Leaflet.
+ */
+
+namespace Drupal\leaflet_views\Plugin\views\style;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\views\style\StylePluginBase;
+
+/**
+ * Style plugin to render a View output as a Leaflet map.
+ *
+ * @ingroup views_style_plugins
+ *
+ * Attributes set below end up in the $this->definition[] array.
+ *
+ * @ViewsStyle(
+ *   id = "leaflet",
+ *   title = @Translation("Leaflet"),
+ *   help = @Translation("Displays a View as a Leaflet map."),
+ *   display_types = {"normal"},
+ *   theme = "leaflet-map"
+ * )
+ */
+class Leaflet extends StylePluginBase {
+
+  /**
+   * Does the style plugin support grouping of rows.
+   *
+   * @var bool
+   */
+  protected $usesGrouping = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::buildOptionsForm($form, $form_state);
+
+    // Choose a map preset
+    $map_options = array();
+    foreach (leaflet_map_get_info() as $key => $map) {
+      $map_options[$key] = $this->t($map['label']);
+    }
+    $form['map'] = array(
+      '#title' => $this->t('Map'),
+      '#type' => 'select',
+      '#options' => $map_options,
+      '#default_value' => $this->options['map'] ?: '',
+      '#required' => TRUE,
+    );
+
+    $form['height'] = array(
+      '#title' => $this->t('Map height'),
+      '#type' => 'textfield',
+      '#field_suffix' => $this->t('px'),
+      '#size' => 4,
+      '#default_value' => $this->options['height'],
+      '#required' => TRUE,
+    );
+
+    // @todo add note about adding leaflet attachments for data points.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::validateOptionsForm($form, $form_state);
+
+    $height = $form_state->getValue(array('style_options', 'height'));
+    if (!empty($style_options['height']) && (!is_numeric($height) || $height <= 0)) {
+      $form_state->setError($form['height'], $this->t('Map height needs to be a positive number.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function evenEmpty() {
+    // Render map even if there is no data.
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    // Avoid querying the database; all feature data comes from attachments.
+    $this->built = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    $features = array();
+    foreach ($this->view->attachment_before as $id => $attachment) {
+      if (!empty($attachment['#leaflet-attachment'])) {
+        $features = array_merge($features, $attachment['rows']);
+        $this->view->element['#attached'] = NestedArray::mergeDeep($this->view->element['#attached'], $attachment['#attached']);
+        unset($this->view->attachment_before[$id]);
+      }
+    }
+
+    $map_info = leaflet_map_get_info($this->options['map']);
+    // Enable layer control by default, if we have more than one feature group.
+    if (self::hasFeatureGroups($features)) {
+      $map_info['settings'] += array('layerControl' => TRUE);
+    }
+    $element = leaflet_render_map($map_info, $features, $this->options['height'] . 'px');
+
+    // Merge #attached libraries.
+    $this->view->element['#attached'] = NestedArray::mergeDeep($this->view->element['#attached'], $element['#attached']);
+    $element['#attached'] =& $this->view->element['#attached'];
+
+    return $element;
+  }
+
+  /**
+   * Checks whether the given array of features contains any groups, i.e.
+   * elements having the "group" key set to TRUE.
+   *
+   * @param array $features
+   * @return bool
+   */
+  protected static function hasFeatureGroups(array $features) {
+    foreach ($features as $feature) {
+      if (!empty($feature['group'])) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate() {
+    $errors = parent::validate();
+    if (empty($this->options['map'])) {
+      $errors[] = $this->t('Style @style requires a leaflet map to be configured.', array('@style' => $this->definition['title']));
+    }
+    return $errors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+    $options['map'] = array('default' => '');
+    $options['height'] = array('default' => '400');
+    return $options;
+  }
+}
diff --git a/leaflet_views/src/Plugin/views/style/LeafletMap.php b/leaflet_views/src/Plugin/views/style/LeafletMap.php
new file mode 100644
index 0000000..fe6f4a1
--- /dev/null
+++ b/leaflet_views/src/Plugin/views/style/LeafletMap.php
@@ -0,0 +1,405 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\leaflet_views\Plugin\views\style\LeafletMap.
+ */
+
+namespace Drupal\leaflet_views\Plugin\views\style;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\Plugin\views\style\StylePluginBase;
+use Drupal\views\ViewExecutable;
+
+
+/**
+ * Style plugin to render a View output as a Leaflet map.
+ *
+ * @ingroup views_style_plugins
+ *
+ * Attributes set below end up in the $this->definition[] array.
+ *
+ * @ViewsStyle(
+ *   id = "leafet_map",
+ *   title = @Translation("Leaflet map (old)"),
+ *   help = @Translation("Displays a View as a Leaflet map."),
+ *   display_types = {"normal"},
+ *   theme = "leaflet-map"
+ * )
+ *
+ * @deprecated Should be removed in favor of other plugins.
+ */
+class LeafletMap extends StylePluginBase {
+
+  /**
+   * Does the style plugin for itself support to add fields to it's output.
+   *
+   * @var bool
+   */
+  protected $usesFields = TRUE;
+
+  /**
+   * If this view is displaying an entity, save the entity type and info.
+   */
+  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+    parent::init($view, $display, $options);
+
+    // For later use, set entity info related to the View's base table.
+    $base_tables = array_keys($view->getBaseTables());
+    $base_table = reset($base_tables);
+    foreach (\Drupal::entityManager()->getDefinitions() as $key => $info) {
+      if ($info->getBaseTable() == $base_table) {
+        $this->entity_type = $key;
+        $this->entity_info = $info;
+        return;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function evenEmpty() {
+    // Render map even if there is no data.
+    return TRUE;
+  }
+
+  /**
+   * Options form
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::buildOptionsForm($form, $form_state);
+
+    // Get a list of fields and a sublist of geo data fields in this view
+    $fields = array();
+    $fields_geo_data = array();
+    foreach ($this->displayHandler->getHandlers('field') as $field_id => $handler) {
+      $label = $handler->adminLabel() ?: $field_id;
+      $fields[$field_id] = $label;
+      if (is_a($handler, '\Drupal\field\Plugin\views\field\Field')) {
+        $field_storage_definitions = \Drupal::entityManager()
+          ->getFieldStorageDefinitions($handler->getEntityType());
+        $field_storage_definition = $field_storage_definitions[$handler->definition['field_name']];
+
+        if ($field_storage_definition->getType() == 'geofield') {
+          $fields_geo_data[$field_id] = $label;
+        }
+      }
+    }
+
+    // Check whether we have a geo data field we can work with
+    if (!count($fields_geo_data)) {
+      $form['error'] = array(
+        '#markup' => $this->t('Please add at least one geofield to the view.'),
+      );
+      return;
+    }
+
+    // Map preset.
+    $form['data_source'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Data Source'),
+      '#description' => $this->t('Which field contains geodata?'),
+      '#options' => $fields_geo_data,
+      '#default_value' => $this->options['data_source'],
+      '#required' => TRUE,
+    );
+
+    // Name field
+    $form['name_field'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Title Field'),
+      '#description' => $this->t('Choose the field which will appear as a title on tooltips.'),
+      '#options' => array_merge(array('' => ''), $fields),
+      '#default_value' => $this->options['name_field'],
+    );
+
+    $desc_options = array_merge(array('' => ''), $fields);
+    // Add an option to render the entire entity using a view mode
+    if ($this->entity_type) {
+      $desc_options += array(
+        '#rendered_entity' => '<' . $this->t('!entity entity', array('!entity' => $this->entity_type)) . '>',
+      );
+    }
+
+    $form['description_field'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Description Field'),
+      '#description' => $this->t('Choose the field or rendering method which will appear as a description on tooltips or popups.'),
+      '#required' => FALSE,
+      '#options' => $desc_options,
+      '#default_value' => $this->options['description_field'],
+    );
+
+    if ($this->entity_type) {
+
+      // Get the human readable labels for the entity view modes.
+      $view_mode_options = array();
+      foreach (\Drupal::entityManager()
+                 ->getViewModes($this->entity_type) as $key => $view_mode) {
+        $view_mode_options[$key] = $view_mode['label'];
+      }
+      // The View Mode drop-down is visible conditional on "#rendered_entity"
+      // being selected in the Description drop-down above.
+      $form['view_mode'] = array(
+        '#type' => 'select',
+        '#title' => $this->t('View mode'),
+        '#description' => $this->t('View modes are ways of displaying entities.'),
+        '#options' => $view_mode_options,
+        '#default_value' => !empty($this->options['view_mode']) ? $this->options['view_mode'] : 'full',
+        '#states' => array(
+          'visible' => array(
+            ':input[name="style_options[description_field]"]' => array(
+              'value' => '#rendered_entity'
+            )
+          )
+        )
+      );
+    }
+
+    // Choose a map preset
+    $map_options = array();
+    foreach (leaflet_map_get_info() as $key => $map) {
+      $map_options[$key] = $this->t($map['label']);
+    }
+    $form['map'] = array(
+      '#title' => $this->t('Map'),
+      '#type' => 'select',
+      '#options' => $map_options,
+      '#default_value' => $this->options['map'] ?: '',
+      '#required' => TRUE,
+    );
+
+    $form['height'] = array(
+      '#title' => $this->t('Map height'),
+      '#type' => 'textfield',
+      '#field_suffix' => $this->t('px'),
+      '#size' => 4,
+      '#default_value' => $this->options['height'],
+      '#required' => TRUE,
+    );
+
+    $form['icon'] = array(
+      '#title' => $this->t('Map Icon'),
+      '#type' => 'fieldset',
+      '#collapsible' => TRUE,
+      '#collapsed' => !isset($this->options['icon']['iconUrl']),
+    );
+
+    $form['icon']['iconUrl'] = array(
+      '#title' => $this->t('Icon URL'),
+      '#description' => $this->t('Can be an absolute or relative URL.'),
+      '#type' => 'textfield',
+      '#maxlength' => 999,
+      '#default_value' => $this->options['icon']['iconUrl'] ?: '',
+    );
+
+    $form['icon']['shadowUrl'] = array(
+      '#title' => $this->t('Icon Shadow URL'),
+      '#type' => 'textfield',
+      '#maxlength' => 999,
+      '#default_value' => $this->options['icon']['shadowUrl'] ?: '',
+    );
+
+    $form['icon']['iconSize'] = array(
+      '#title' => $this->t('Icon Size'),
+      '#type' => 'fieldset',
+      '#collapsible' => FALSE,
+      '#description' => $this->t('Size of the icon image in pixels.')
+    );
+
+    $form['icon']['iconSize']['x'] = array(
+      '#title' => $this->t('Width'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => isset($this->options['icon']['iconSize']['x']) ? $this->options['icon']['iconSize']['x'] : '',
+      '#element_validate' => array('form_validate_number'),
+    );
+
+    $form['icon']['iconSize']['y'] = array(
+      '#title' => $this->t('Height'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => isset($this->options['icon']['iconSize']['y']) ? $this->options['icon']['iconSize']['y'] : '',
+      '#element_validate' => array('form_validate_number'),
+    );
+
+    $form['icon']['iconAnchor'] = array(
+      '#title' => $this->t('Icon Anchor'),
+      '#type' => 'fieldset',
+      '#collapsible' => FALSE,
+      '#description' => $this->t('The coordinates of the "tip" of the icon (relative to its top left corner). The icon will be aligned so that this point is at the marker\'s geographical location.')
+    );
+
+    $form['icon']['iconAnchor']['x'] = array(
+      '#title' => $this->t('X'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => isset($this->options['icon']['iconAnchor']['x']) ? $this->options['icon']['iconAnchor']['x'] : '',
+      '#element_validate' => array('form_validate_number'),
+    );
+
+    $form['icon']['iconAnchor']['y'] = array(
+      '#title' => $this->t('Y'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => isset($this->options['icon']['iconAnchor']['y']) ? $this->options['icon']['iconAnchor']['y'] : '',
+      '#element_validate' => array('form_validate_number'),
+    );
+
+    $form['icon']['shadowAnchor'] = array(
+      '#title' => $this->t('Shadow Anchor'),
+      '#type' => 'fieldset',
+      '#collapsible' => FALSE,
+      '#description' => $this->t('The point from which the shadow is shown.')
+    );
+    $form['icon']['shadowAnchor']['x'] = array(
+      '#title' => $this->t('X'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => isset($this->options['icon']['shadowAnchor']['x']) ? $this->options['icon']['shadowAnchor']['x'] : '',
+      '#element_validate' => array('form_validate_number'),
+    );
+    $form['icon']['shadowAnchor']['y'] = array(
+      '#title' => $this->t('Y'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => isset($this->options['icon']['shadowAnchor']['y']) ? $this->options['icon']['shadowAnchor']['y'] : '',
+      '#element_validate' => array('form_validate_number'),
+    );
+
+    $form['icon']['popupAnchor'] = array(
+      '#title' => $this->t('Popup Anchor'),
+      '#type' => 'fieldset',
+      '#collapsible' => FALSE,
+      '#description' => $this->t('The point from which the marker popup opens, relative to the anchor point.')
+    );
+
+    $form['icon']['popupAnchor']['x'] = array(
+      '#title' => $this->t('X'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => isset($this->options['icon']['popupAnchor']['x']) ? $this->options['icon']['popupAnchor']['x'] : '',
+      '#element_validate' => array('form_validate_number'),
+    );
+
+    $form['icon']['popupAnchor']['y'] = array(
+      '#title' => $this->t('Y'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => isset($this->options['icon']['popupAnchor']['y']) ? $this->options['icon']['popupAnchor']['y'] : '',
+      '#element_validate' => array('form_validate_number'),
+    );
+  }
+
+  /**
+   * Validates the options form.
+   */
+  public function validateOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::validateOptionsForm($form, $form_state);
+
+    $style_options = $form_state->getValue('style_options');
+    if (!empty($style_options['height']) && (!is_numeric($style_options['height']) || $style_options['height'] <= 0)) {
+      $form_state->setError($form['height'], $this->t('Map height needs to be a positive number.'));
+    }
+    $icon_options = $style_options['icon'];
+    if (!empty($icon_options['iconUrl']) && !UrlHelper::isValid($icon_options['iconUrl'])) {
+      $form_state->setError($form['icon']['iconUrl'], $this->t('Icon URL is invalid.'));
+    }
+    if (!empty($icon_options['shadowUrl']) && !UrlHelper::isValid($icon_options['shadowUrl'])) {
+      $form_state->setError($form['icon']['shadowUrl'], $this->t('Shadow URL is invalid.'));
+    }
+    if (!empty($icon_options['iconSize']['x']) && (!is_numeric($icon_options['iconSize']['x']) || $icon_options['iconSize']['x'] <= 0)) {
+      $form_state->setError($form['icon']['iconSize']['x'], $this->t('Icon width needs to be a positive number.'));
+    }
+    if (!empty($icon_options['iconSize']['y']) && (!is_numeric($icon_options['iconSize']['y']) || $icon_options['iconSize']['y'] <= 0)) {
+      $form_state->setError($form['icon']['iconSize']['y'], $this->t('Icon height needs to be a positive number.'));
+    }
+  }
+
+  /**
+   * Renders the View.
+   */
+  function render() {
+    $data = array();
+    $geofield_name = $this->options['data_source'];
+    if ($this->options['data_source']) {
+      $this->renderFields($this->view->result);
+      foreach ($this->view->result as $id => $result) {
+
+        $geofield_value = $this->getFieldValue($id, $geofield_name);
+
+        if (empty($geofield_value)) {
+          // In case the result is not among the raw results, get it from the
+          // rendered results.
+          $geofield_value = $this->rendered_fields[$id][$geofield_name];
+        }
+        if (!empty($geofield_value)) {
+          $points = leaflet_process_geofield($geofield_value);
+
+          // Render the entity with the selected view mode
+          if ($this->options['description_field'] === '#rendered_entity' && is_object($result)) {
+            $entity = entity_load($this->entity_type, $result->{$this->entity_info->getKey('id')});
+            $build = entity_view($entity, $this->options['view_mode']);
+            $description = drupal_render($build);
+          }
+          // Normal rendering via fields
+          elseif ($this->options['description_field']) {
+            $description = $this->rendered_fields[$id][$this->options['description_field']];
+          }
+
+          // Attach pop-ups if we have a description field
+          if (isset($description)) {
+            foreach ($points as &$point) {
+              $point['popup'] = $description;
+            }
+          }
+
+          // Attach also titles, they might be used later on
+          if ($this->options['name_field']) {
+            foreach ($points as &$point) {
+              $point['label'] = $this->rendered_fields[$id][$this->options['name_field']];
+            }
+          }
+
+          $data = array_merge($data, $points);
+
+          if (!empty($this->options['icon']) && $this->options['icon']['iconUrl']) {
+            foreach ($data as $key => $feature) {
+              $data[$key]['icon'] = $this->options['icon'];
+            }
+          }
+        }
+      }
+    }
+
+    // Always render the map, even if we do not have any data.
+    $map = leaflet_map_get_info($this->options['map']);
+    return leaflet_render_map($map, $data, $this->options['height'] . 'px');
+  }
+
+  /**
+   * Set default options
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+    $options['data_source'] = array('default' => '');
+    $options['name_field'] = array('default' => '');
+    $options['description_field'] = array('default' => '');
+    $options['view_mode'] = array('default' => 'full');
+    $options['map'] = array('default' => '');
+    $options['height'] = array('default' => '400');
+    $options['icon'] = array('default' => array());
+    return $options;
+  }
+}
diff --git a/leaflet_views/src/Plugin/views/style/MarkerDefault.php b/leaflet_views/src/Plugin/views/style/MarkerDefault.php
new file mode 100644
index 0000000..046aec8
--- /dev/null
+++ b/leaflet_views/src/Plugin/views/style/MarkerDefault.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\leaflet_views\Plugin\views\style\MarkerDefault.
+ */
+
+namespace Drupal\leaflet_views\Plugin\views\style;
+
+use Drupal\views\Plugin\views\style\StylePluginBase;
+use Drupal\views\ResultRow;
+
+/**
+ * Style plugin to render leaflet markers.
+ *
+ * @ingroup views_style_plugins
+ *
+ * @ViewsStyle(
+ *   id = "leaflet_marker_default",
+ *   title = @Translation("Markers"),
+ *   help = @Translation("Render data as leaflet markers."),
+ *   display_types = {"leaflet"},
+ * )
+ */
+class MarkerDefault extends StylePluginBase {
+
+  /**
+   * Does the style plugin allows to use style plugins.
+   *
+   * @var bool
+   */
+  protected $usesRowPlugin = TRUE;
+
+  /**
+   * Does the style plugin support custom css class for the rows.
+   *
+   * @var bool
+   */
+  protected $usesRowClass = FALSE;
+
+  /**
+   * Does the style plugin support grouping of rows.
+   *
+   * @var bool
+   */
+  protected $usesGrouping = FALSE;
+
+  /**
+   * Does the style plugin for itself support to add fields to it's output.
+   *
+   * This option only makes sense on style plugins without row plugins, like
+   * for example table.
+   *
+   * @var bool
+   */
+  protected $usesFields = TRUE;
+
+  public function renderGroupingSets($sets, $level = 0) {
+    $output = array();
+    foreach ($sets as $set) {
+      if ($this->usesRowPlugin()) {
+        foreach ($set['rows'] as $index => $row) {
+          $this->view->row_index = $index;
+          $set['rows'][$index] = $this->view->rowPlugin->render($row);
+          $this->alterLeafletMarkerPoints($set['rows'][$index], $row);
+        }
+      }
+      $set['features'] = array();
+      foreach ($set['rows'] as $group) {
+        $set['features'] = array_merge($set['features'], $group);
+      }
+
+      // Abort if we haven't managed to build any features.
+      if (empty($set['features'])) {
+        continue;
+      }
+
+      if ($featureGroup = $this->renderLeafletGroup($set['features'], $set['group'], $level)) {
+        // Allow modules to adjust the feature group.
+        \Drupal::moduleHandler()
+          ->alter('leaflet_views_feature_group', $featureGroup, $this);
+
+        // If the rendered "feature group" is actually only a list of features,
+        // merge them into the output; else simply append the feature group.
+        if (empty($featureGroup['group'])) {
+          $output = array_merge($output, $featureGroup['features']);
+        }
+        else {
+          $output[] = $featureGroup;
+        }
+      }
+    }
+    unset($this->view->row_index);
+    return $output;
+  }
+
+  /**
+   * Alter the marker definition generated from the row plugin.
+   *
+   * @param array $points
+   * @param ResultRow $row
+   */
+  protected function alterLeafletMarkerPoints(&$points, ResultRow $row) {
+  }
+
+  /**
+   * Render a single group of leaflet markers.
+   *
+   * @param array $features
+   *   The list of leaflet features / points.
+   * @param $title
+   *   The group title.
+   * @param $level
+   *   The current group level.
+   * @return array
+   *   Definition of leaflet markers, compatible with leaflet_render_map().
+   */
+  protected function renderLeafletGroup(array $features = array(), $title, $level) {
+    return array(
+      'group' => FALSE,
+      'features' => $features,
+    );
+  }
+
+}
diff --git a/leaflet_views/src/Plugin/views/style/MarkerLayerGroup.php b/leaflet_views/src/Plugin/views/style/MarkerLayerGroup.php
new file mode 100644
index 0000000..b632ed2
--- /dev/null
+++ b/leaflet_views/src/Plugin/views/style/MarkerLayerGroup.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * @file
+ * Contains Drupal\leaflet_views\Plugin\views\style\MarkerLayerGroup.
+ */
+
+namespace Drupal\leaflet_views\Plugin\views\style;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Style plugin to render leaflet features in layer groups.
+ *
+ * @ingroup views_style_plugins
+ *
+ * @ViewsStyle(
+ *   id = "leaflet_marker_group",
+ *   title = @Translation("Grouped Markers"),
+ *   help = @Translation("Render data as leaflet markers, grouped in layers."),
+ *   display_types = {"leaflet"},
+ * )
+ */
+class MarkerLayerGroup extends MarkerDefault {
+
+  /**
+   * Does the style plugin support grouping of rows.
+   *
+   * @var bool
+   */
+  protected $usesGrouping = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    parent::buildOptionsForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderGrouping($records, $groupings = array(), $group_rendered = NULL) {
+    $sets = parent::renderGrouping($records, $groupings, $group_rendered);
+    if (!$groupings) {
+      // Set group label to display label, if empty.
+      $attachment_title = $this->view->getDisplay()->getOption('title');
+      $sets['']['group'] = $attachment_title ? $attachment_title : $this->t('Label missing');
+    }
+    return $sets;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function renderLeafletGroup(array $features = array(), $title = '', $level = 0) {
+    return array(
+      'group' => TRUE,
+      'label' => $title,
+      'features' => $features,
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    return parent::defineOptions();
+    // Add group options.
+  }
+
+}
diff --git a/lib/Drupal/leaflet/Plugin/field/formatter/LeafletDefaultFormatter.php b/lib/Drupal/leaflet/Plugin/field/formatter/LeafletDefaultFormatter.php
deleted file mode 100644
index a5178a8..0000000
--- a/lib/Drupal/leaflet/Plugin/field/formatter/LeafletDefaultFormatter.php
+++ /dev/null
@@ -1,249 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\leafet\Plugin\field\formatter\LeafletDefaultFormatter.
- */
-
-namespace Drupal\leaflet\Plugin\field\formatter;
-
-use Drupal\field\Annotation\FieldFormatter;
-use Drupal\Core\Annotation\Translation;
-use Drupal\field\Plugin\Type\Formatter\FormatterBase;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\Field\FieldInterface;
-
-/**
- * Plugin implementation of the 'leaflet_default' formatter.
- *
- * @FieldFormatter(
- *   id = "leaflet_formatter_default",
- *   label = @Translation("Leaflet map"),
- *   field_types = {
- *     "geofield"
- *   },
- *   settings = {
- *     "leaflet_map" = "",
- *     "height" = "400",
- *     "popup" = 0,
- *     "icon" = {
- *       "icon_url" = "",
- *       "shadow_url" = "",
- *       "icon_size" = {
- *         "x" = "",
- *         "y" = ""
- *       },
- *       "icon_anchor" = {
- *         "x" = "",
- *         "y" = ""
- *       },
- *       "schadow_anchor" = {
- *         "x" = "",
- *         "y" = ""
- *       },
- *       "popop_anchor" = {
- *         "x" = "",
- *         "y" = ""
- *       }
- *     }
- *   }
- * )
- */
-class LeafletDefaultFormatter extends FormatterBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function settingsForm(array $form, array &$form_state) {
-    $elements = parent::settingsForm($form, $form_state);
-
-    $options = array('' => t('-- Select --'));
-    foreach (leaflet_map_get_info() as $key => $map) {
-      $options[$key] = t($map['label']);
-    }
-    $elements['leaflet_map'] = array(
-      '#title' => t('Leaflet Map'),
-      '#type' => 'select',
-      '#options' => $options,
-      '#default_value' => $this->getSetting('leaflet_map'),
-      '#required' => TRUE,
-    );
-    $elements['height'] = array(
-      '#title' => t('Map Height'),
-      '#type' => 'textfield',
-      '#default_value' => $this->getSetting('height'),
-      '#field_suffix' => t('px'),
-      '#element_validate' => array('form_validate_number'),
-    );
-    $elements['popup'] = array(
-      '#title' => t('Popup'),
-      '#description' => t('Show a popup for single location fields.'),
-      '#type' => 'checkbox',
-      '#default_value' => $this->getSetting('popup'),
-    );
-    $icon = $this->getSetting('icon');
-    $elements['icon'] = array(
-      '#title' => t('Map Icon'),
-      '#description' => t('These settings will overwrite the icon settings defined in the map definition.'),
-      '#type' => 'fieldset',
-      '#collapsible' => TRUE,
-      '#collapsed' => empty($icon),
-    );
-    $elements['icon']['icon_url'] = array(
-      '#title' => t('Icon URL'),
-      '#description' => t('Can be an absolute or relative URL.'),
-      '#type' => 'textfield',
-      '#maxlength' => 999,
-      '#default_value' => $icon['icon_url'],
-      '#element_validate' => array(array($this, 'validateUrl')),
-    );
-    $elements['icon']['shadow_url'] = array(
-      '#title' => t('Icon Shadow URL'),
-      '#type' => 'textfield',
-      '#maxlength' => 999,
-      '#default_value' => $icon['shadow_url'],
-      '#element_validate' => array(array($this, 'validateUrl')),
-    );
-
-    $elements['icon']['icon_size'] = array(
-      '#title' => t('Icon Size'),
-      '#type' => 'fieldset',
-      '#collapsible' => FALSE,
-      '#description' => t('Size of the icon image in pixels.')
-    );
-    $elements['icon']['icon_size']['x'] = array(
-      '#title' => t('Width'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => $icon['icon_size']['x'],
-      '#element_validate' => array('form_validate_number'),
-    );
-    $elements['icon']['icon_size']['y'] = array(
-      '#title' => t('Height'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => $icon['icon_size']['y'],
-      '#element_validate' => array('form_validate_number'),
-    );
-    $elements['icon']['icon_anchor'] = array(
-      '#title' => t('Icon Anchor'),
-      '#type' => 'fieldset',
-      '#collapsible' => FALSE,
-      '#description' => t('The coordinates of the "tip" of the icon (relative to
-        its top left corner). The icon will be aligned so that this point is at the marker\'s geographical location.')
-    );
-    $elements['icon']['icon_anchor']['x'] = array(
-      '#title' => t('X'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => $icon['icon_anchor']['y'],
-      '#element_validate' => array('form_validate_number'),
-    );
-    $elements['icon']['icon_anchor']['y'] = array(
-      '#title' => t('Y'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => $icon['icon_anchor']['y'],
-      '#element_validate' => array('form_validate_number'),
-    );
-    $elements['icon']['shadow_anchor'] = array(
-      '#title' => t('Shadow Anchor'),
-      '#type' => 'fieldset',
-      '#collapsible' => FALSE,
-      '#description' => t('The point from which the shadow is shown.')
-    );
-    $elements['icon']['shadow_anchor']['x'] = array(
-      '#title' => t('X'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => $icon['shadow_anchor']['x'],
-      '#element_validate' => array('form_validate_number'),
-    );
-    $elements['icon']['shadow_anchor']['y'] = array(
-      '#title' => t('Y'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => $icon['shadow_anchor']['y'],
-      '#element_validate' => array('form_validate_number'),
-    );
-    $elements['icon']['popup_anchor'] = array(
-      '#title' => t('Popup Anchor'),
-      '#type' => 'fieldset',
-      '#collapsible' => FALSE,
-      '#description' => t('The point from which the marker popup opens, relative
-        to the anchor point.')
-    );
-    $elements['icon']['popup_anchor']['x'] = array(
-      '#title' => t('X'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => $icon['popup_anchor']['x'],
-      '#element_validate' => array('form_validate_number'),
-    );
-    $elements['icon']['popup_anchor']['y'] = array(
-      '#title' => t('Y'),
-      '#type' => 'textfield',
-      '#maxlength' => 3,
-      '#size' => 3,
-      '#default_value' => $icon['popup_anchor']['y'],
-      '#element_validate' => array('form_validate_number'),
-    );
-
-    return $elements;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function settingsSummary() {
-    $summary = array();
-    $summary[] = t('Leaflet map: @map', array('@map' => $this->getSetting('leaflet_map')));
-    $summary[] = t('Map height: @height px', array('@height' => $this->getSetting('height')));
-    return $summary;
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * This function is called from parent::view().
-   */
-  public function viewElements(EntityInterface $entity, $langcode, FieldInterface $items) {
-
-    $settings = $this->getSettings();
-    $icon_url = $settings['icon']['icon_url'];
-    
-    $map = leaflet_map_get_info($settings['leaflet_map']);
-
-    $elements = array();
-    foreach ($items as $delta => $item) {
-
-      $features = leaflet_process_geofield($item->value);
-
-      // If only a single feature, set the popup content to the entity title.
-      if ($settings['popup'] && count($items) == 1) {
-        $features[0]['popup'] = $entity->label();
-      }
-      if (!empty($icon_url)) {
-        foreach ($features as $key => $feature) {
-          $features[$key]['icon'] = $icon_url;
-        }
-      }
-      $elements[$delta] = array('#markup' => leaflet_render_map($map, $features, $settings['height'] . 'px'));
-    }
-    return $elements;
-  }
-
-  public function validateUrl($element, &$form_state) {
-    if (!empty($element['#value']) && !valid_url($element['#value'])) {
-      form_error($element, t("Icon Url is not valid."));
-    }
-  }
-
-}
diff --git a/src/Plugin/Field/FieldFormatter/LeafletDefaultFormatter.php b/src/Plugin/Field/FieldFormatter/LeafletDefaultFormatter.php
new file mode 100644
index 0000000..a658c3d
--- /dev/null
+++ b/src/Plugin/Field/FieldFormatter/LeafletDefaultFormatter.php
@@ -0,0 +1,244 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\leaflet\Plugin\Field\FieldFormatter\LeafletDefaultFormatter.
+ */
+
+namespace Drupal\leaflet\Plugin\Field\FieldFormatter;
+
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Field\Annotation\FieldFormatter;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FormatterBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Plugin implementation of the 'leaflet_default' formatter.
+ *
+ * @FieldFormatter(
+ *   id = "leaflet_formatter_default",
+ *   label = @Translation("Leaflet map"),
+ *   field_types = {
+ *     "geofield"
+ *   }
+ * )
+ */
+class LeafletDefaultFormatter extends FormatterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return array(
+      'leaflet_map' => '',
+      'height' => 400,
+      'popup' => False,
+      'icon' => array(
+        'icon_url' => '',
+        'shadow_url' => '',
+        'icon_size' => array('x' => 0, 'y' => 0),
+        'icon_anchor' => array('x' => 0, 'y' => 0),
+        'shadow_anchor' => array('x' => 0, 'y' => 0),
+        'popup_anchor' => array('x' => 0, 'y' => 0),
+      ),
+    ) + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $elements = parent::settingsForm($form, $form_state);
+
+    $options = array('' => $this->t('-- Select --'));
+    foreach (leaflet_map_get_info() as $key => $map) {
+      $options[$key] = $this->t($map['label']);
+    }
+    $elements['leaflet_map'] = array(
+      '#title' => $this->t('Leaflet Map'),
+      '#type' => 'select',
+      '#options' => $options,
+      '#default_value' => $this->getSetting('leaflet_map'),
+      '#required' => TRUE,
+    );
+    $elements['height'] = array(
+      '#title' => $this->t('Map Height'),
+      '#type' => 'textfield',
+      '#default_value' => $this->getSetting('height'),
+      '#field_suffix' => $this->t('px'),
+      '#element_validate' => array('form_validate_number'),
+    );
+    $elements['popup'] = array(
+      '#title' => $this->t('Popup'),
+      '#description' => $this->t('Show a popup for single location fields.'),
+      '#type' => 'checkbox',
+      '#default_value' => $this->getSetting('popup'),
+    );
+    $icon = $this->getSetting('icon');
+    $elements['icon'] = array(
+      '#title' => $this->t('Map Icon'),
+      '#description' => $this->t('These settings will overwrite the icon settings defined in the map definition.'),
+      '#type' => 'fieldset',
+      '#collapsible' => TRUE,
+      '#collapsed' => empty($icon),
+    );
+    $elements['icon']['icon_url'] = array(
+      '#title' => $this->t('Icon URL'),
+      '#description' => $this->t('Can be an absolute or relative URL.'),
+      '#type' => 'textfield',
+      '#maxlength' => 999,
+      '#default_value' => $icon['icon_url'],
+      '#element_validate' => array(array($this, 'validateUrl')),
+    );
+    $elements['icon']['shadow_url'] = array(
+      '#title' => $this->t('Icon Shadow URL'),
+      '#type' => 'textfield',
+      '#maxlength' => 999,
+      '#default_value' => $icon['shadow_url'],
+      '#element_validate' => array(array($this, 'validateUrl')),
+    );
+
+    $elements['icon']['icon_size'] = array(
+      '#title' => $this->t('Icon Size'),
+      '#type' => 'fieldset',
+      '#collapsible' => FALSE,
+      '#description' => $this->t('Size of the icon image in pixels.')
+    );
+    $elements['icon']['icon_size']['x'] = array(
+      '#title' => $this->t('Width'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => $icon['icon_size']['x'],
+      '#element_validate' => array('form_validate_number'),
+    );
+    $elements['icon']['icon_size']['y'] = array(
+      '#title' => $this->t('Height'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => $icon['icon_size']['y'],
+      '#element_validate' => array('form_validate_number'),
+    );
+    $elements['icon']['icon_anchor'] = array(
+      '#title' => $this->t('Icon Anchor'),
+      '#type' => 'fieldset',
+      '#collapsible' => FALSE,
+      '#description' => $this->t('The coordinates of the "tip" of the icon (relative to
+        its top left corner). The icon will be aligned so that this point is at the marker\'s geographical location.')
+    );
+    $elements['icon']['icon_anchor']['x'] = array(
+      '#title' => $this->t('X'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => $icon['icon_anchor']['x'],
+      '#element_validate' => array('form_validate_number'),
+    );
+    $elements['icon']['icon_anchor']['y'] = array(
+      '#title' => $this->t('Y'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => $icon['icon_anchor']['y'],
+      '#element_validate' => array('form_validate_number'),
+    );
+    $elements['icon']['shadow_anchor'] = array(
+      '#title' => $this->t('Shadow Anchor'),
+      '#type' => 'fieldset',
+      '#collapsible' => FALSE,
+      '#description' => $this->t('The point from which the shadow is shown.')
+    );
+    $elements['icon']['shadow_anchor']['x'] = array(
+      '#title' => $this->t('X'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => $icon['shadow_anchor']['x'],
+      '#element_validate' => array('form_validate_number'),
+    );
+    $elements['icon']['shadow_anchor']['y'] = array(
+      '#title' => $this->t('Y'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => $icon['shadow_anchor']['y'],
+      '#element_validate' => array('form_validate_number'),
+    );
+    $elements['icon']['popup_anchor'] = array(
+      '#title' => $this->t('Popup Anchor'),
+      '#type' => 'fieldset',
+      '#collapsible' => FALSE,
+      '#description' => $this->t('The point from which the marker popup opens, relative
+        to the anchor point.')
+    );
+    $elements['icon']['popup_anchor']['x'] = array(
+      '#title' => $this->t('X'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => $icon['popup_anchor']['x'],
+      '#element_validate' => array('form_validate_number'),
+    );
+    $elements['icon']['popup_anchor']['y'] = array(
+      '#title' => $this->t('Y'),
+      '#type' => 'textfield',
+      '#maxlength' => 3,
+      '#size' => 3,
+      '#default_value' => $icon['popup_anchor']['y'],
+      '#element_validate' => array('form_validate_number'),
+    );
+
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = array();
+    $summary[] = $this->t('Leaflet map: @map', array('@map' => $this->getSetting('leaflet_map')));
+    $summary[] = $this->t('Map height: @height px', array('@height' => $this->getSetting('height')));
+    return $summary;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * This function is called from parent::view().
+   */
+  public function viewElements(FieldItemListInterface $items) {
+    $settings = $this->getSettings();
+    $icon_url = $settings['icon']['icon_url'];
+
+    $map = leaflet_map_get_info($settings['leaflet_map']);
+
+    $elements = array();
+    foreach ($items as $delta => $item) {
+
+      $features = leaflet_process_geofield($item->value);
+
+      // If only a single feature, set the popup content to the entity title.
+      if ($settings['popup'] && count($items) == 1) {
+        $features[0]['popup'] = $items->getEntity()->label();
+      }
+      if (!empty($icon_url)) {
+        foreach ($features as $key => $feature) {
+          $features[$key]['icon'] = $icon_url;
+        }
+      }
+      $elements[$delta] = leaflet_render_map($map, $features, $settings['height'] . 'px');
+    }
+    return $elements;
+  }
+
+  public function validateUrl($element, FormStateInterface $form_state) {
+    if (!empty($element['#value']) && !UrlHelper::isValid($element['#value'])) {
+      $form_state->setError($element, $this->t("Icon Url is not valid."));
+    }
+  }
+
+}
