
L.MagnifyingGlass = L.Layer.extend({
  options: {
    radius: 100,
    zoomOffset: 3,
    layers: [ ],
    fixedPosition: false,
    latLng: [0, 0],
    fixedZoom: -1
  },

  initialize: function(options) {
    L.Util.setOptions(this, options);
    this._fixedZoom = (this.options.fixedZoom != -1);
    this._mainMap = null;
    this._glassMap = null;
  },

  getMap: function(){
    return this._glassMap;
  },

  _createMiniMap: function(elt) {
    return new L.Map(elt, {
      layers: this.options.layers,
      zoom: this._getZoom(),
      maxZoom: this._mainMap.getMaxZoom(),
      minZoom: this._mainMap.getMinZoom(),
      crs: this._mainMap.options.crs,
      fadeAnimation: false,
      // disable every controls and interaction means
      attributionControl: false,
      zoomControl: false,
      boxZoom: false,
      touchZoom: false,
      scrollWheelZoom: false,
      doubleClickZoom: false,
      dragging: false,
      keyboard: false,
    });
  },

  _getZoom: function() {
    return (this._fixedZoom) ?
      this.options.fixedZoom :
      this._mainMap.getZoom() + this.options.zoomOffset;
  },

  _updateZoom: function() {
    this._glassMap.setZoom(this._getZoom());
  },

  setRadius: function(radius) {
    this.options.radius = radius;
    if(this._wrapperElt) {
      this._wrapperElt.style.width = this.options.radius * 2 + 'px';
      this._wrapperElt.style.height = this.options.radius * 2 + 'px';
    }
  },

  setLatLng: function(latLng) {
    this.options.latLng = latLng;
    this._update(latLng);
  },

  _updateFromMouse: function(evt) {
    this._update(evt.latlng, evt.layerPoint);
  },

  _updateFixed: function() {
    this._update(this.options.latLng);
  },

  _update: function(latLng, layerPoint) {
    // update mini map view, forcing no animation
    this._glassMap.setView(latLng, this._getZoom(), {
      pan : { animate: false }
    });

    // update the layer element position on the main map,
    // using the one provided or reprojecting it
    layerPoint = layerPoint || this._mainMap.latLngToLayerPoint(latLng);
    this._wrapperElt.style.left = layerPoint.x - this.options.radius + 'px';
    this._wrapperElt.style.top = layerPoint.y - this.options.radius + 'px';
  },

  /**
  As defined by ILayer
  */
  onAdd: function(map) {
    this._mainMap = map;
    // create a wrapper element and a container for the map inside it
    this._wrapperElt = L.DomUtil.create('div', 'leaflet-magnifying-glass');
    var glassMapElt = L.DomUtil.create('div', '', this._wrapperElt);
    // Webkit border-radius clipping workaround (see CSS)
    if(L.Browser.webkit)
      L.DomUtil.addClass(glassMapElt, 'leaflet-magnifying-glass-webkit');
    // build the map
    this._glassMap = this._createMiniMap(glassMapElt);

    // forward some DOM events as Leaflet events
    L.DomEvent.addListener(this._wrapperElt, 'click', this._fireClick, this);

    var opts = this.options;

    this.setRadius(opts.radius);
    this.setLatLng(opts.latLng);

    this._glassMap.whenReady(function() {
      if(opts.fixedPosition) {
        this._mainMap.on('zoomend', this._updateFixed, this);
        // for now, hide the elements during zoom transitions
        L.DomUtil.addClass(this._wrapperElt, ('leaflet-zoom-hide'));
      } else {
        this._mainMap.on('mousemove', this._updateFromMouse, this);
        if(!this._fixedZoom) {
          this._mainMap.on('zoomend', this._updateZoom, this);
        }
      }
    }, this);

    // add the magnifying glass as a layer to the top-most pane
    map.getPanes().popupPane.appendChild(this._wrapperElt);

    // needed after the element has been added, otherwise tile loading is messy
    this._glassMap.invalidateSize();

    return this;
  },

  _fireClick: function(domMouseEvt) {
    this.fire('click', domMouseEvt);
    L.DomEvent.stopPropagation(domMouseEvt);
  },

  /**
  As defined by ILayer
  */
  onRemove: function(map) {
    map.off('viewreset', this._updateFixed, this);
    map.off('mousemove', this._updateFromMouse, this);
    map.off('zoomend', this._updateZoom, this);
    // layers must be explicitely removed before map destruction,
    // otherwise they can't be reused if the mg is re-added
    for(var i=0, l=this.options.layers.length; i<l; i++) {
      this._glassMap.removeLayer(this.options.layers[i]);
    }
    this._glassMap.remove();
    L.DomEvent.removeListener(this._wrapperElt, 'click', this._fireClick);
    map.getPanes().popupPane.removeChild(this._wrapperElt);
    this._mainMap = null;

    return this;
  }
});

L.magnifyingGlass = function (options) {
  return new L.MagnifyingGlass(options);
};
