CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/cartodb/table/mapview.js

Summary

Maintainability
F
1 wk
Test Coverage
/**
 * map tab shown in cartodb admin
 */

/**
 * inside the UI all the cartodb layers should be shown merged.
 * the problem is that the editor needs the layers separated to work
 * with them so this class transform from multiple cartodb layers
 * and create only a view to represent all merged in a single layer group
 */
function GrouperLayerMapView(mapViewClass) {

  return {

    initialize: function() {
      this.groupLayer = null;
      this.activeLayerModel = null;
      mapViewClass.prototype.initialize.call(this);
    },

    _removeLayers: function() {
      var self = this;
      _.each(this.map.layers.getLayersByType('CartoDB'), function(layer) {
        layer.unbind(null, null, self);
      });
      cdb.geo.MapView.prototype._removeLayers.call(this);

      if(this.groupLayer) {
        this.groupLayer.model.unbind();
      }
      this.groupLayer = null;
    },

    _removeLayer: function(layer) {
      // if the layer is in layergroup
      if(layer.cid in this.layers) {
        if(this.layers[layer.cid] === this.groupLayer) {
          this._updateLayerDefinition(layer);
          layer.unbind(null, null, this);
          delete this.layers[layer.cid];
          this.trigger('removeLayerView', this);
        } else {
          this.trigger('removeLayerView', this);
          cdb.geo.MapView.prototype._removeLayer.call(this, layer);
        }
      } else {
        cdb.log.info("removing non existing layer");
      }
    },

    setActiveLayer: function(layer) {
      this.activeLayerModel = layer;
      this._setInteraction();
    },

    disableInteraction: function() {
      if (this.groupLayer) {
        this.groupLayer._clearInteraction();
      }
    },

    enableInteraction: function() {
      this._setInteraction();
    },

    // set interaction only for the active layer
    _setInteraction: function() {
      if(!this.groupLayer) return;
      if(this.activeLayerModel) {
        this.groupLayer._clearInteraction();
        var idx = this.map.layers.getLayerDefIndex(this.activeLayerModel);
        // when layer is not found idx == -1 so the interaction is
        // disabled for all the layers
        for(var i = 0; i < this.groupLayer.getLayerCount(); ++i) {
          this.groupLayer.setInteraction(i, i == idx);
        }
      }
    },

    _updateLayerDefinition: function(layer) {
      if(!layer) throw "layer must be a valid layer (not null)";
      if(this.groupLayer) {
        if(this.map.layers.getLayersByType('CartoDB').length === 0) {
          this.groupLayer.remove();
          this.groupLayer = null;
        } else {
          var def = this.map.layers.getLayerDef();
          this.groupLayer.setLayerDefinition(def);
          this._setInteraction();
        }
      }
    },

    /**
     * when merged layers raises an error this function send the error to the
     * layer that actually caused it
     */
    _routeErrors: function(errors) {
      var styleRegExp = /style(\d+)/;
      var postgresExp = /layer(\d+):/i;
      var generalPostgresExp = /PSQL error/i;
      var syntaxErrorExp = /syntax error/i;
      var webMercatorErrorExp = /"the_geom_webmercator" does not exist/i;
      var tilerError = /Error:/i;
      var layers = this.map.layers.where({ visible: true, type: 'CartoDB' });
      for(var i in errors) {
        var err = errors[i];
        // filter empty errors
        if(err && err.length) {
          var match = styleRegExp.exec(err);
          if(match) {
            var layerIndex = parseInt(match[1], 10);
            layers[layerIndex].trigger('parseError', [err]);
          } else {
            var match = postgresExp.exec(err);
            if(match) {
              var layerIndex = parseInt(match[1], 10);
              if (webMercatorErrorExp.exec(err)) {
                err = _t("you should select the_geom_webmercator column");
                layers[layerIndex].trigger('sqlNoMercator', [err]);
              } else {
                layers[layerIndex].trigger('sqlParseError', [err]);
              }
            } else if(generalPostgresExp.exec(err) || syntaxErrorExp.exec(err) || tilerError.exec(err)) {
              var error = 'sqlError';
              if (webMercatorErrorExp.exec(err)) {
                error = 'sqlNoMercator';
                err = _t("you should select the_geom_webmercator column");
              }
              _.each(layers, function(lyr) { lyr.trigger(error, err); });
            } else {
              _.each(layers, function(lyr) { lyr.trigger('error', err); });
            }
          }
        }
      }
    },

    _routeSignal: function(signal) {
      var self = this;
      return function() {
        var layers = self.map.layers.where({ visible: true, type: 'CartoDB' });
        var args = [signal].concat(arguments);
        _.each(layers, function(lyr) { lyr.trigger.apply(lyr, args); });
      }
    },

    _addLayer: function(layer, layers, opts) {
      opts = opts || {};
      // create group layer to acumulate cartodb layers
      if (layer.get('type') === 'CartoDB') {
        var self = this;
        if(!this.groupLayer) {
          // create model
          var m = new cdb.geo.CartoDBGroupLayer(
            _.extend(layer.toLayerGroup(), {
              user_name: this.options.user.get("username"),
              maps_api_template: cdb.config.get('maps_api_template'),
              no_cdn: false,
              force_cors: true // use CORS to control error management in a better way
            })
          );

          var layer_view = mapViewClass.prototype._addLayer.call(this, m, layers, _.extend({}, opts, { silent: true }));
          delete this.layers[m.cid];
          this.layers[layer.cid] = layer_view;
          this.groupLayer = layer_view;
          m.bind('error', this._routeErrors, this);
          m.bind('tileOk', this._routeSignal('tileOk'), this);
          this.trigger('newLayerView', layer_view, layer, this);
        } else {
          this.layers[layer.cid] = this.groupLayer;
          this._updateLayerDefinition(layer);
          this.trigger('newLayerView', this.groupLayer, layer, this);
        }

        layer.bind('change:tile_style change:query change:query_wrapper change:interactivity change:visible', this._updateLayerDefinition, this);
        this._addLayerToMap(this.groupLayer, opts);
        delete this.layers[this.groupLayer.model.cid];
      } else {
        mapViewClass.prototype._addLayer.call(this, layer, layers, opts);
      }
    }
  }
};

cdb.admin.LeafletMapView = cdb.geo.LeafletMapView.extend(GrouperLayerMapView(cdb.geo.LeafletMapView));

if (typeof(google) !== 'undefined') {
  cdb.admin.GoogleMapsMapView = cdb.geo.GoogleMapsMapView.extend(GrouperLayerMapView(cdb.geo.GoogleMapsMapView));
}

cdb.admin.MapTab = cdb.core.View.extend({

  events: {
    'click .toggle_slides.button': '_toggleSlides',
    'click .add_overlay.button':   'killEvent',
    'click .canvas_setup.button':  'killEvent',
    'click .export_image.button':  '_exportImage',
    'click .sqlview .clearview':   '_clearView',
    'click .sqlview .export_query':'_tableFromQuery',
    'keydown':'_onKeyDown'
  },

  _TEXTS: {
    no_interaction_warn: _t("Map interaction is disabled, select cartodb_id to enable it")
  },

  className: 'map',
  animation_time: 300,

  initialize: function() {

    this.template = this.getTemplate('table/views/maptab');

    this.map  = this.model;
    this.user = this.options.user;
    this.vis  = this.options.vis;
    this.authTokens = this.options.authTokens || [];
    this.master_vis  = this.options.master_vis;

    this.canvas  = new cdb.core.Model({ mode: "desktop" });

    this.map_enabled     = false;
    this.georeferenced   = false;
    this.featureHovered  = null;
    this.activeLayerView = null;
    this.layerDataView   = null;
    this.layerModel      = null;
    this.legends         = [];
    this.overlays        = null;

    this.add_related_model(this.map);
    this.add_related_model(this.canvas);
    this.add_related_model(this.map.layers);

    this._addBindings();

  },

  _addBindings: function() {

    // Actions triggered in the right panel
    cdb.god.bind("panel_action", function(action) {
      this._moveInfo(action);
    }, this);

    this.add_related_model(cdb.god);

    this.map.bind('change:provider',       this.switchMapType, this);
    this.map.bind('change:legends',        this._toggleLegends, this);
    this.map.layers.bind('change:visible', this._addLegends, this);
    this.map.layers.bind('change:visible', this._addTimeline, this);
    this.map.layers.bind('change:tile_style', this._addTimeline, this);
    this.map.layers.bind('remove reset',   this._addLegends, this);
    this.map.layers.bind('remove reset',   this._addTimeline, this);

    _.bindAll(this, 'showNoGeoRefWarning', "_exportImage");

  },

  isMapEnabled: function() {
    return this.map_enabled;
  },

  deactivated: function() {
    if(this.map_enabled) {
      this.clearMap();
    }
  },

  clearMap: function() {

    clearTimeout(this.autoSaveBoundsTimer);

    this.mapView.clean();

    if (this.exportImageView) {
      this.exportImageView.clean();
      this.exportImageView = null;
    }

    if (this.overlaysDropdown) this.overlaysDropdown.clean();
    if (this.mapOptionsDropdown) this.mapOptionsDropdown.clean();
    if (this.basemapDropdown) this.basemapDropdown.clean();
    if (this.configureCanvasDropdown) this.configureCanvasDropdown.clean();

    if (this.zoom) {
      this.zoom.clean();
    }

    if (this.infowindow) {
      this.infowindow.clean();
    }

    if (this.overlays) {
      this.overlays._cleanOverlays();
    }

    this._cleanLegends();

    if (this.stackedLegend) {
      this.stackedLegend.clean();
    }

    if (this.timeline) {
      this.timeline.clean();
      this.timeline = null;
    }

    if (this.geometryEditor) this.geometryEditor.clean();

    if (this.table) {
      this.table.unbind(null, null, this);
    }

    delete this.mapView;
    delete this.overlaysDropdown;
    delete this.basemapDropdown;
    delete this.mapOptionsDropdown;
    delete this.configureCanvasDropdown;

    delete this.zoom;
    delete this.infowindow;
    delete this.layer_selector;
    delete this.header;
    delete this.share;
    delete this.legends;
    delete this.overlays;
    delete this.legend;
    delete this.stackedLegend;
    delete this.geometryEditor;

    this.map_enabled = false;

    // place the map DOM object again
    this.render();
  },


  /**
   *  Hide the infowindow when a query is applied or cleared
   */
  _hideInfowindow: function() {
    if(this.infowindow) {
      this.infowindow.model.set('visibility', false);
    }
  },


  /**
   * this function is used when the map library is changed. Each map library
   * works in different way and need to recreate all the components again
   */
  switchMapType: function() {

    if (this.map_enabled) {
      this.clearMap();
      this.enableMap();
    }

  },

  _showGMapsDeprecationDialog: function() {
    var dialog = cdb.editor.ViewFactory.createDialogByTemplate('common/dialogs/confirm_gmaps_basemap_to_leaflet_conversion');

    var self = this;
    dialog.ok = function() {
      self.map.set('provider', 'leaflet', { silent: true });
      self.setupMap();
      this.close && this.close();
    };

    dialog.cancel = function() {
      if (self.user.isInsideOrg()) {
        window.location = "/u/" + self.user.get("username") + "/dashboard";
      } else {
        window.location = "/dashboard";
      }
    };

    dialog.appendToBody();
  },

  /**
   * map can't be loaded from the beggining, it needs the DOM to be loaded
   * so we wait until is actually shown to create the mapview and show it
   */
  enableMap: function() {

    this.render();

    var baseLayer = this.map.getBaseLayer();

    // check if this user has google maps enabled. In case not and the provider is google maps
    // show a message
    if ( typeof cdb.admin.GoogleMapsMapView === 'undefined') {
      if (baseLayer && this.map.isProviderGmaps()) {
        this._showGMapsDeprecationDialog();
        return;
      }
    }

    this.setupMap();

  },

  setupMap: function() {

    this.$('.tipsy').remove();

    var self = this;

    if (!this.map_enabled) {

      this._addMapView();

      this.clickTimeout = null;

      this._bindMissingClickEvents();

      this.map_enabled = true;

      $(".map")
      .append('<div class="map-options" />')
      .append("<div class='mobile_bkg' />");

      this._addBasemapDropdown();
      this._addInfowindow();
      this._addTooltip();
      this._addLegends();
      this._addOverlays();

      this._showPecan();

      this._showScratchDialog();

      if (this.user.featureEnabled('slides')) {
        this._addSlides();
      };

      var torqueLayer;

      var type = this.vis.get("type");

      if (type !== "table") {

        this._addOverlaysDropdown();
        this._addConfigureCanvasDropdown();
        this._addMapOptionsDropdown();

        this.canvas.on("change:mode", this._onChangeCanvasMode, this);

      }

      this.master_vis.on("change:type", function() {
        if (this.master_vis.previous('type') === 'table') {
          // reaload the map to show overlays and other visualization related stuff
          this.switchMapType();
        }
      }, this);

      // HACK
      // wait a little bit to give time to the mapview
      // to estabilize
      this.autoSaveBoundsTimer = setTimeout(function() {
        //self.mapView.setAutoSaveBounds();
        self.mapView.on('dragend zoomend', function() {
          self.mapView._saveLocation();
        });
      }, 1000);

    }

  },

  _addMapView: function() {

    var div = this.$('.cartodb-map');

    var mapViewClass = cdb.admin.LeafletMapView;
    if (this.map.get('provider') === 'googlemaps') {
      var mapViewClass = cdb.admin.GoogleMapsMapView;
    }

    this.mapView = new mapViewClass({
      el: div,
      map: this.map,
      user: this.user
    });

    this.mapView.bind('removeLayerView', function(layerView) {
      if (this.layer_selector) this.layer_selector.render();
    }, this);

    this.mapView.bind('newLayerView', function(layerView, model) {
      if(this.activeLayerView && this.activeLayerView.model.id === model.id) {
        this._bindDataLayer(this.activeLayerView, model);

        if (this.layer_selector) {
          this.layer_selector.render();
        }
      }
      this._addTimeline();
    }, this);

    if (this.activeLayerView) {
      this._bindDataLayer(this.activeLayerView, this.activeLayerView.model);
    }

  },

  _addConfigureCanvasDropdown: function() {
    if (!this.configureCanvasDropdown) {
      this.configureCanvasDropdown = new cdb.admin.ConfigureCanvasDropdown({
        target: $('.canvas_setup'),
        position: "position",
        canvas: this.canvas,
        template_base: "table/views/canvas_setup_dropdown",
        tick: "left",
        horizontal_position: "left",
        horizontal_offset: "40px"
      });

      this.addView(this.configureCanvasDropdown);

      this.configureCanvasDropdown.bind("onDropdownShown", function(){
        this.exportImageView && this.exportImageView.hide();
      }, this);

      cdb.god.bind("closeDialogs", this.configureCanvasDropdown.hide, this.configureCanvasDropdown);
      $(".canvas_setup").append(this.configureCanvasDropdown.render().el);
    }
  },

  _addOverlaysDropdown: function() {

    if (!this.overlaysDropdown) {

      this.overlaysDropdown = new cdb.admin.OverlaysDropdown({
        vis: this.master_vis,
        canvas: this.canvas,
        mapView: this.mapView,
        target: $('.add_overlay'),
        position: "position",
        collection: this.vis.overlays,
        template_base: "table/views/widget_dropdown",
        tick: "left",
        horizontal_position: "left",
        horizontal_offset: "40px"
      });

      this.addView(this.overlaysDropdown);

      this.overlaysDropdown.bind("onOverlayDropdownOpen", function(){
        this.slidesPanel && this.slidesPanel.hide();
        this.exportImageView && this.exportImageView.hide();
      }, this);


      cdb.god.bind("closeDialogs", this.overlaysDropdown.hide, this.overlaysDropdown);
      cdb.god.bind("closeOverlayDropdown", this.overlaysDropdown.hide, this.overlaysDropdown);

      $(".add_overlay").append(this.overlaysDropdown.render().el);
    }

  },

  _addBasemapDropdown: function() {

    if (!this.basemapDropdown) {

      if (this.vis.get("type") !== "table") {
        // TODO: use templates and _t for texts
        var $options = $('<a href="#" class="option-button dropdown basemap_dropdown"><div class="thumb"></div>Change basemap</a>');

        $(".map-options").append($options);

      }

      this.basemapDropdown = new cdb.admin.DropdownBasemap({
        target: $('.basemap_dropdown'),
        position: "position",
        template_base: "table/views/basemap/basemap_dropdown",
        model: this.map,
        mapview: this.mapView,
        user: this.user,
        baseLayers: this.options.baseLayers,
        tick: "left",
        vertical_offset: 40,
        horizontal_position: "left",
        vertical_position: this.vis.get("type") === 'table' ? "down" : "up",
        horizontal_offset: this.vis.get("type") === 'table' ? 42 : 0
      });

      this.addView(this.basemapDropdown);

      this.basemapDropdown.bind("onDropdownShown", function() {
        cdb.god.trigger("closeDialogs");
      });

      cdb.god.bind("closeDialogs", this.basemapDropdown.hide, this.basemapDropdown);

      $(".basemap_dropdown").append(this.basemapDropdown.render().el);

    }

    // Set active base layer if it already exists
    if (this.map.getBaseLayer()) {
      this.basemapDropdown.setActiveBaselayer();
    }

  },

  bindGeoRefCheck: function() {
    if(!this.table.data().fetched) {
      this.table.bind('dataLoaded', function() {
        this.checkGeoRef();
        if (!this.scratchDialog) {
          this._showScratchDialog();
        }
        if (!this.pecanView) {
          this._showPecan();
        }
      }, this);
    } else {
      this.checkGeoRef();
    }
  },

  activated: function() {
    this.checkGeoRef();
    $(window).scrollTop(0);
  },

  checkGeoRef: function() {
    if (this.options && this.table) {
      this.georeferenced = this.table.isGeoreferenced();
      if (this.noGeoRefDialog) {
        this.noGeoRefDialog.hide();
      }
      if (!this.georeferenced) {
        if (this.table.data().length > 0) {
          this[ this.table.isSync() ? '_showNoGeoWarning' : 'showNoGeoRefWarning' ]();
        }
      }
    }
  },

  // Shows a warning dialog when your current dialog doesn't have any
  // geometry on it and it is synchronized
  _showNoGeoWarning: function() {
    var noGeoWarningDialog = 'noGeoWarningDialog_' + this.table.id + '_' + this.table.get('map_id');
    if (this.noGeoWarningDialog || localStorage[noGeoWarningDialog]) {
      return;
    }

    this.noGeoWarningDialog = cdb.editor.ViewFactory.createDialogByTemplate(
      'table/views/no_geo_warning_template', {
        clean_on_hide: true
      }
    );

    this.noGeoWarningDialog.bind("hide", function() {
      localStorage[noGeoWarningDialog] = true;
    });

    this.noGeoWarningDialog.appendToBody();
  },

  _showPecan: function() {

    var hasPecan     = this.user.featureEnabled('pecan_cookies');

    var hasData = this.options.table && this.options.table.data() && this.options.table.data().length > 0 ? true : false;

    if (hasPecan && hasData) {

      var skipPencanDialog = 'pecan_' + this.options.user.get("username") + "_" + this.options.table.id;

      if (!localStorage[skipPencanDialog]) {

        this.pecanView = new cdb.editor.PecanView({
          table: this.options.table,
          backgroundPollingModel: this.options.backgroundPollingModel
        });
      }
    }
  },

  _showScratchDialog: function() {
    if (this.options.table && this.options.table.data().fetched && this.options.table.data().length === 0) {

      var skipScratchDialog = 'scratchDialog_' + this.options.table.id + '_' + this.options.table.get('map_id');

      if (!localStorage[skipScratchDialog]) {

        this.scratchDialog = new cdb.editor.ScratchView({
          table: this.options.table
        });

        this.scratchDialog.appendToBody();

        this.scratchDialog.bind("newGeometry", function(type) {
          this._addGeometry(type);
        }, this);

        this.scratchDialog.bind("skip", function() {
          localStorage[skipScratchDialog] = true;
        });
      }
    }
  },

  /**
   * this function binds click and dblclick events
   * in order to not raise click when user does a dblclick
   *
   * it raises a missingClick when the user clicks on the map
   * but not over a feature or ui component
   */
  _bindMissingClickEvents: function() {
    var self = this;
    this.mapView.bind('click', function(e) {
      if(self.clickTimeout === null) {
        self.clickTimeout = setTimeout(function() {
          self.clickTimeout = null;
          if(!self.featureHovered) {
            self.trigger('missingClick');
          }
        }, 150);
      }
      //google maps does not send an event
      if(!self.featureHovered && e.preventDefault) {
        e.preventDefault();
        e.stopPropagation();
      }
    });

    this.mapView.bind('dblclick', function() {
      if(self.clickTimeout !== null) {
        clearTimeout(self.clickTimeout);
        self.clickTimeout = null;
      }
    });
  },

  setActiveLayer: function(layerView) {
    this.activeLayerView = layerView;
    // check if the map is rendered and the layer is in the map
    if(this.mapView && this.mapView.getLayerByCid(layerView.model.cid)) {
      var layerModel = layerView.model;
      this._bindDataLayer(layerView, layerModel);
    }
  },

  /**
   * when the layer view is created this method is called
   * to attach all the click events
   */
  _bindDataLayer: function(layerView, layer) {
    var self = this;
    var layerType = layer.get('type');

    if (layerType === 'CartoDB' || layerType === 'torque') { // unbind previos stuff

      // Set data layer bindings
      if (self.layerDataView) {
        self.layerDataView.unbind(null, null, this);
      }

      if (self.layerModel) {
        self.layerModel.unbind(null, null, this);
      }

      if (self.options.geocoder) {
        self.options.geocoder.unbind(null, null, this);
      }

      self.infowindowModel  = layer.infowindow;
      self.tooltipModel     = layer.tooltip;
      self.legendModel      = layer.legend;

      self._bindTable(layer.table);
      self._bindSQLView(layer.sqlView);
      self.layerDataView = self.mapView.getLayerByCid(layer.cid);

      self.mapView.setActiveLayer(layer);
      self._addLegends();
      self._addTimeline();

      if (self.layerDataView) {
        self.layerDataView.bind('featureClick', self.featureClick, self);
        self.layerDataView.bind('featureOut',   self.featureOut,   self);
        self.layerDataView.bind('featureOver',  self.featureOver,  self);
        self.layerDataView.bind('loading',      self.loadingTiles, self);
        self.layerDataView.bind('load',         self.loadTiles,    self);
        self.layerDataView.bind('error',        self.loadTiles,    self);
        self.tooltip
          .setLayer(self.layerDataView)
          .enable();

      }

      // Set layer model binding
      if (layerView && layer) {
        layer.unbind('startEdition',this._addGeometry, this);
        layer.bind('startEdition', this._addGeometry, this);
      }

      if(layer) {
        self.layerModel = layer;
        //TODO: unbind this at some point
        layer.bind('change:interactivity', this._updateSQLHeader, this);
        this._updateSQLHeader();
      }

      if (self.options.geocoder) {
        self.options.geocoder.bind('geocodingComplete geocodingError geocodingCanceled', this.updateDataLayerView, this);
        self.add_related_model(self.options.geocoder);
      }

    }
  },

  _cleanLegends: function() {

    if (this.legends) {
      _.each(this.legends, function(legend) {
        legend.clean();
      });

    }

    this.legends = [];

  },


  _getCartoDBLayers: function() {

    return this.map.layers.models.filter(function(layerModel) {
      return layerModel.get("type") === 'CartoDB'
    });

  },

  _onKeyDown: function(e) {
    if (this.overlays && e.which == 86 && (e.ctrlKey || e.metaKey)) {
      this.overlays.paste();
    }
  },

  _onChangeCanvasMode: function() {

    var self = this;

    cdb.god.trigger("closeDialogs");

    var mode = this.canvas.get("mode");

    if (mode === "desktop") {

      this._showDesktopCanvas(mode);

      if (this.overlays.loader && this.overlays.fullscreen) {
        setTimeout(function() {
          self.overlays && self.overlays._positionOverlaysVertically(true);
        }, 500);
      }

    } else if (mode === "mobile") {

      this._showMobileCanvas(mode);

      setTimeout(function() {
        self.overlays && self.overlays._positionOverlaysVertically(true);
      }, 300);

    }

  },

  _showMobileCanvas: function(mode) {

    var self = this;

    var width  = 288;
    var height = 476;

    this.overlays._hideOverlays("desktop");

    var $map = $("div.map div.cartodb-map");

    this.$el.addClass(mode);

    // Animations step - 1
    var onBackgroundShown = function() {

      $map.animate(
        { width: width, marginLeft: -Math.round(width/2) - 1, left: "50%" },
        { easing: "easeOutQuad", duration: 200, complete: onCanvasLandscapeStretched }
      );

    };

    // Animations step - 2
    var onCanvasPortraitStretched = function() {

      self.$el.find(".mobile_bkg").animate(
        { opacity: 1 },
        { duration: 250 }
      );

      self.overlays._showOverlays(mode);

      // Let's set center view for mobile mode
      var center = self.map.get('center');
      self.mapView.invalidateSize();
      $map.fadeOut(250);

      setTimeout(function() {
        self.mapView.map.setCenter(center);
        $map.fadeIn(250);
      },300);

    };

    // Animations step - 3
    var onCanvasLandscapeStretched = function() {

      $map.animate(
        { height: height, marginTop: -Math.round(height/2) + 23,  top:  "50%" },
        { easing: "easeOutQuad", duration: 200, complete: onCanvasPortraitStretched }
      );

    };

    onBackgroundShown();

    this._enableAnimatedMap();
    this._enableMobileLayout();

  },

  _enableMobileLayout: function() {

    if (!this.mobile) {

      var torqueLayer;

      this.mobile = new cdb.admin.overlays.Mobile({
        mapView: this.mapView,
        overlays: this.overlays,
        map: this.map
      });

      this.mapView.$el.append(this.mobile.render().$el);

    } else {
      this.mobile.show();
    }

  },

  _disableMobileLayout: function() {

    if (this.mobile) this.mobile.hide();

  },

  _showDesktopCanvas: function(mode) {

    var self = this;

    this.overlays._hideOverlays("mobile");

    this.$el.removeClass("mobile");

    this.$el.find(".mobile_bkg").animate({ opacity: 0}, 250);

    var
    $map       = $("div.map div.cartodb-map"),
    top        = $map.css("top"),
    left       = $map.css("left"),
    mTop       = $map.css("marginTop"),
    mLeft      = $map.css("marginLeft"),
    curWidth   = $map.width(),
    curHeight  = $map.height(),
    autoWidth  = $map.css({width:  'auto', marginLeft: 0, left: "15px"}).width();  //temporarily change to auto and get the width.
    autoHeight = $map.css({height: 'auto', marginTop: 0,  top: "82px" }).height(); //temporarily change to auto and get the width.

    $map.height(curHeight);
    $map.width(curWidth);

    $map.css({ top: top, left: left, marginLeft: mLeft, marginTop: mTop, height: curHeight, width: curWidth });

    var onSecondAnimationFinished = function() {

      $map.css('width', 'auto');
      self.overlays._showOverlays(mode);

      // Let's set center view for desktop mode
      var center = self.map.get('center');
      self.mapView.invalidateSize();

      setTimeout(function() {
        self.mapView.map.setCenter(center);
      },300);

    };

    var onFirstAnimationFinished = function() {

      $map.css('height', 'auto');
      $map.animate(
        { width: autoWidth, left: "15px", marginLeft: "0"},
        { easing: "easeOutQuad", duration: 200, complete: onSecondAnimationFinished }
      );

    };

    var stretchMapLandscape = function() {
      $map.animate(
        { height: autoHeight, top: "82", marginTop: "0"},
        { easing: "easeOutQuad", duration: 200, complete: onFirstAnimationFinished }
      );
    };

    stretchMapLandscape();

    this._disableAnimatedMap();
    this._disableMobileLayout();

  },

  _enableAnimatedMap: function() {

    var self = this;

    setTimeout(function() {
      self.$el.addClass("animated");
    }, 800)

  },

  _disableAnimatedMap: function() {
    this.$el.removeClass("animated");
  },

  _addMapOptionsDropdown: function() {

    if (!this.mapOptionsDropdown) {

      var $options = $("<a href='#show-options' class='option-button show-table-options'>Options</a>");

      this.$options = $options;

      $(".map-options").append($options);

      this.mapOptionsDropdown = new cdb.admin.MapOptionsDropdown({
        target:              $('.show-table-options'),
        template_base:       "table/views/map_options_dropdown",
        table:               table,
        model:               this.map,
        mapview:             this.mapView,
        collection:          this.vis.overlays,
        user:                this.user,
        vis:                 this.vis,
        canvas:              this.canvas,
        position:            "position",
        tick:                "left",
        vertical_position:   "up",
        horizontal_position: "left",
        horizontal_offset:   "-3px"
      });

      this._bindMapOptions();

      this.addView(this.mapOptionsDropdown);

      $(".show-table-options").append(this.mapOptionsDropdown.render().el);

    }

  },

  _bindMapOptions: function() {

    this.mapOptionsDropdown.bind("onDropdownShown", function() {
      cdb.god.trigger("closeDialogs");
      this.$options.addClass("open");
    }, this);

    this.mapOptionsDropdown.bind("onDropdownHidden", function() {
      this.$options.removeClass("open");
    }, this);

    this.mapOptionsDropdown.bind("createOverlay", function(overlay_type, property) {
      this.vis.overlays.createOverlayByType(overlay_type, property);
    }, this);

    cdb.god.bind("closeDialogs", this.mapOptionsDropdown.hide, this.mapOptionsDropdown);

  },

  _addOverlays: function() {
    this.overlays = new cdb.admin.MapOverlays({
      headerMessageIsVisible: this._shouldAddSQLViewHeader(),
      vis: this.vis,
      canvas: this.canvas,
      mapView: this.mapView,
      master_vis: this.master_vis,
      mapToolbar: this.$el.find(".map_toolbar")
    });

  },

  _exportImage: function(e) {

    this.killEvent(e);

    if (this.exportImageView) {
      return;
    }

    this.exportImageView = new cdb.admin.ExportImageView({
      authTokens: this.authTokens,
      height: this.mapView.$el.height(),
      map: this.map,
      mapView: this.mapView,
      overlays: this.overlays,
      user: this.options.user,
      vis: this.vis,
      vizjson: this.vis.vizjsonURL(),
      width: this.mapView.$el.width()
    });

    this.exportImageView.bind("was_removed", function() {
      this.exportImageView = null;
    }, this);

    this.mapView.$el.append(this.exportImageView.render().$el);

    cdb.god.bind("panel_action", function(action) {
      if (action !== "hide" && this.exportImageView) {
        this.exportImageView.hide();
      }
    }, this);
  },

  _addSlides: function() {

    if (!this.vis.isVisualization()) return;

    this.slidesPanel = new cdb.admin.SlidesPanel({
      user: this.user,
      slides:  this.vis.slides,
      toggle: this.$el.find(".toggle_slides")
    });

    this.slidesPanel.bind("onChangeVisible", function() {
      this.exportImageView && this.exportImageView.hide();
    }, this);

    this.$el.append(this.slidesPanel.render().el);

    this.addView(this.slidesPanel);

  },

  _addLegends: function() {

    var self = this;

    this._cleanLegends();

    if (!this.map.get("legends")) {
      return;
    }

    var models = this.map.layers.models;

    for (var i = models.length - 1; i >= 0; --i) {
      var layer = models[i];
      self._addLegend(layer);
    }

  },

  _addLegend: function(layer) {

    var type = layer.get('type');

    if (type === 'CartoDB' || type === 'torque') {

      if (this.table && this.mapView) {

        if (this.legend) this.legend.clean();

        if (layer.get("visible")) {

          var legend = new cdb.geo.ui.Legend({
            model:   layer.legend,
            mapView: this.mapView,
            table:   this.table
          });

          if (this.legends) {
            this.legends.push(legend);
            this._renderStackedLengeds();
          }

        }
      }
    }

  },

  _toggleLegends: function() {
    if (this.map.get("legends")) {
      this._addLegends();
    } else {
      this._cleanLegends();
    }
  },

  _addTimeline: function() {
    if (!this.mapView) return;
    // check if there is some torque layer
    if(!this.map.layers.any(function(lyr) { return lyr.get('type') === 'torque' && lyr.get('visible'); })) {
      this.timeline && this.timeline.clean();
      this.timeline = null;
    } else {
      var layer = this.map.layers.getLayersByType('torque')[0];
      var steps = layer.wizard_properties.get('torque-frame-count');

      if (this.timeline) {
        // check if the model is different
        if (this.timeline.torqueLayer.model.cid !== layer.cid) {
          this.timeline.clean();
          this.timeline = null;
        }
      }

      layerView = this.mapView.getLayerByCid(layer.cid);

      if (layerView && typeof layerView.getStep !== "undefined" && steps > 1) {
        if (!this.timeline) {
          this.timeline = new cdb.geo.ui.TimeSlider({
            layer: layerView,
            width: "auto"
          });

          this.mapView.$el.append(this.timeline.render().$el);
          this.addView(this.timeline);
        } else {
          this.timeline.setLayer(layerView);
        }
      }
      else if (this.timeline) {
        this.timeline.clean();
        this.timeline = null;
      }
    }
  },

  _renderStackedLengeds: function() {

    if (this.stackedLegend) this.stackedLegend.clean();
    if (this.legend)        this.legend.clean();

    this.stackedLegend = new cdb.geo.ui.StackedLegend({
      legends: this.legends
    });

    this.mapView.$el.append(this.stackedLegend.render().$el);
    this.addView(this.stackedLegend);

  },

  _renderLegend: function() {

    if (this.legend) this.legend.clean();

    this.legend = this.legends[0];

    this.mapView.$el.append(this.legend.render().$el);

    if (!this.legend.model.get("type")) this.legend.hide();
    else this.legend.show();

    this.addView(this.legend);

  },

  _addTooltip: function() {
    if(this.tooltip) this.tooltip.clean();
    if(this.table && this.mapView) {
      this.tooltip = new cdb.admin.Tooltip({
        model: this.tooltipModel,
        table: this.table,
        mapView: this.mapView,
        omit_columns: ['cartodb_id'] // don't show cartodb_id while hover
      });
      this.mapView.$el.append(this.tooltip.render().el);
      this.tooltip.bind('editData', this._editData, this);
      this.tooltip.bind('removeGeom', this._removeGeom, this);
      this.tooltip.bind('editGeom', this._editGeom, this);
      if (this.layerDataView) {
        this.tooltip
          .setLayer(this.layerDataView)
          .enable();
      }
    }
  },

  _addInfowindow: function() {
    if(this.infowindow) this.infowindow.clean();
    if(this.table && this.mapView) {
      this.infowindow = new cdb.admin.MapInfowindow({
        model: this.infowindowModel,
        mapView: this.mapView,
        table: this.table
      });
      this.mapView.$el.append(this.infowindow.el);

      // Editing geometry
      if(this.geometryEditor) {
        this.geometryEditor.discard();
        this.geometryEditor.clean();
      }

      this.geometryEditor = new cdb.admin.GeometryEditor({
        user: this.user,
        model: this.table
      });

      this.geometryEditor.mapView = this.mapView;
      this.mapView.$el.append(this.geometryEditor.render().el);
      this.geometryEditor.hide();

      this.geometryEditor.bind('editStart', this.hideDataLayer, this);
      this.geometryEditor.bind('editDiscard', this.showDataLayer, this);
      this.geometryEditor.bind('editFinish', this.showDataLayer, this);
      this.geometryEditor.bind('editFinish', this.updateDataLayerView, this);
      this.geometryEditor.bind('geomCreated', function(row) {
        this.table.data().add(row);
      }, this);

      var self = this;

      this.infowindow.bind('editData', this._editData, this);
      this.infowindow.bind('removeGeom', this._removeGeom, this);
      this.infowindow.bind('editGeom', this._editGeom, this);

      this.infowindow.bind('openInfowindowPanel', function() {
        this.activeLayerView.showModule('infowindow', 'fields');
      }, this);

      this.infowindow.bind('close', function() {
        if (this.tooltip) {
          this.tooltip.setFilter(null);
        }
      }, this);

      this.table.bind('remove:row', this.updateDataLayerView, this);

      this.table.bind('change:dataSource', function() {
        if (this.geometryEditor) this.geometryEditor.discard();
      }, this);

      this.map.bind('change:provider', function() {
        if (this.geometryEditor) this.geometryEditor.discard();
      }, this);
    }
  },

  _editGeom: function(row) {
    // when provider is leaflet move the world to [-180, 180]
    // because vector features are only rendered on that slice
    if (this.map.get('provider') === 'leaflet') {
      this.map.clamp();
    }
    this.geometryEditor.editGeom(row);
  },

  /**
   * Shows edit data modal window
   */
  _editData: function(row) {
    if (!this.table.isReadOnly()) {
      var self = this;
      row.fetch({ cache: false, no_geom: true, success: function() {
        var dlg = new cdb.editor.FeatureDataView({
          row: row,
          provider: self.map.get('provider'),
          baseLayer: self.map.getBaseLayer().clone(),
          dataLayer: self.layerModel.clone(),
          currentZoom: self.map.getZoom(),
          enter_to_confirm: false,
          table: self.table,
          user: self.user,
          clean_on_hide: true,
          onDone: self.updateDataLayerView.bind(self) // Refreshing layer when changes have been done
        });

        dlg.appendToBody();
      }});

      return false;
    }
  },

  /**
   * triggers an removeGeom event when the geometry
   * is removed from the server
   */
  _removeGeom: function(row) {
    if (!this.table.isReadOnly()) {
      var view = new cdb.editor.DeleteRowView({
        name: 'feature',
        table: this.table,
        row: row,
        clean_on_hide: true,
        enter_to_confirm: true,
        wait: true // to not remove from parent collection until server-side confirmed deletion
      });
      view.appendToBody();

      return false;
    }
  },

  _addGeometry: function(type) {
    this.geometryEditor.createGeom(this.table.data().newRow(), type);
  },

  _bindTable: function(table) {
    if (this.table) {
      this.table.unbind(null, null, this);
    }

    this.table = table;

    this.table.bind('change:dataSource', this._hideInfowindow, this);
    this.table.bind('change:dataSource', this._updateSQLHeader, this);
    this.table.bind('change:schema',     this._updateSQLHeader, this);

    this.table.bind('data:saved', this.updateDataLayerView, this);

    this._addInfowindow();

    this._addLegends();
    this._addTooltip();

    this.bindGeoRefCheck();
  },

  _bindSQLView: function(sqlView) {
    if(this.sqlView) {
      this.sqlView.unbind(null, null, this);
    }
    this.sqlView = sqlView;
    this.sqlView.bind('reset error', this._updateSQLHeader, this);
    this.sqlView.bind('loading', this._renderLoading, this);
    this._updateSQLHeader();
  },

  _renderLoading: function(opts) {
    this._removeSQLViewHeader();

    //TODO: remove this hack
    if ($('.table_panel').length > 0) {
      panel_opened = $('.table_panel').css("right").replace("px","") == 0
    }

    var html = this.getTemplate('table/views/sql_view_notice_loading')({
      panel_opened: panel_opened
    });

    if (this.overlays) {
      this.overlays.setHeaderMessageIsVisible(true);
    }

    this.$('.cartodb-map').after(html);
  },

  _updateSQLHeader: function() {
    if (this._shouldAddSQLViewHeader()) {
      this._addSQLViewHeader();
    } else {
      this._removeSQLViewHeader();
    }
  },

  _shouldAddSQLViewHeader: function() {
    return this.table && this.table.isInSQLView();
  },

  loadingTiles: function() {
    if (this.overlays.loader) this.overlays.loader.show();
  },

  loadTiles: function() {
    if (this.overlays.loader) this.overlays.loader.hide();
  },

  featureOver: function(e, latlon, pxPos, data) {
    if(this.infowindowModel.get('disabled')) return;
    this.mapView.setCursor('pointer');
    this.featureHovered = data;
  },

  featureOut: function() {
    if(this.infowindowModel.get('disabled')) return;
    this.mapView.setCursor('auto');
    this.featureHovered = null;
  },

  featureClick: function(e, latlon, pxPos, data) {
    if(this.infowindowModel.get('disabled')) return;
    if(!this.geometryEditor.isEditing()) {
      if(data.cartodb_id) {
        this.infowindow
          .setLatLng(latlon)
          .setFeatureInfo(data.cartodb_id)
          .showInfowindow();

        this.tooltip.setFilter(function(feature) {
          return feature.cartodb_id !== data.cartodb_id;
        }).hide();
      } else {
        cdb.log.error("can't show infowindow, no cartodb_id on data");
      }
    }
  },

  /**
   *  Move all necessary blocks when panel is openned (normal, narrowed,...) or closed
   */
  _moveInfo: function(type) {
    if (type === "show") {
      this.$el
        .removeClass('narrow')
        .addClass('displaced');
    } else if (type === "hide") {
      this.$el.removeClass('narrow displaced');
    } else if (type === "narrow") {
      this.$el.addClass('narrow displaced');
    }
  },

  render: function() {

    this.$el.html('');

    this.$el
    .removeClass("mobile")
    .removeClass("derived")
    .removeClass("table");

    this.$el.addClass(this.vis.isVisualization() ? 'derived': 'table');
    var provider = this.map.get("provider");

    this.$el.append(this.template({
      slides_enabled: this.user.featureEnabled('slides'),
      type: this.vis.get('type'),
      exportEnabled: !this.map.isProviderGmaps()
    }));

    return this;

  },

  showDataLayer: function() {
    this.mapView.enableInteraction();
    this.layerDataView.setOpacity && this.layerDataView.setOpacity(1.0);
  },

  hideDataLayer: function() {
    this.mapView.disableInteraction();
    this.layerDataView.setOpacity && this.layerDataView.setOpacity(0.5);
  },

  /**
   * reload tiles
   */
  updateDataLayerView: function() {
    if(this.layerDataView) {
      this.layerDataView.invalidate();
    }
  },
  /**
   * Paints a dialog with a warning when the user hasn't any georeferenced row
   * @method showNoGeorefWarning
   */
  showNoGeoRefWarning: function() {
    var warningStorageName = 'georefNoContentWarningShowed_' + this.table.id + '_' + this.table.get('map_id');

    // if the dialog already has been shown, we don't show it again
    if(!this.noGeoRefDialog && !this.table.isInSQLView() && (!localStorage[warningStorageName])) {
      localStorage[warningStorageName] = true;

      this.noGeoRefDialog = new cdb.editor.GeoreferenceView({
        table: this.table,
        user: this.user
      });
      this.noGeoRefDialog.appendToBody();
    }

  },

  //adds the green indicator when a query is applied
  _addSQLViewHeader: function() {
    this.$('.sqlview').remove();
    var total = this.table.data().size();
    var warnMsg = null;
    // if the layer does not suppor interactivity do not show the message
    if (this.layerModel && !this.layerModel.get('interactivity') && this.layerModel.wizard_properties.supportsInteractivity()) {
      warnMsg = this._TEXTS.no_interaction_warn;
    }
    if (this.layerModel && !this.layerModel.table.containsColumn('the_geom_webmercator')) {
      warnMsg = _t('the_geom_webmercator column should be selected');
    }
    var html = this.getTemplate('table/views/sql_view_notice')({
      empty: !total,
        isVisualization: this.vis.isVisualization(),
        warnMsg: warnMsg
    });

    this.$('.cartodb-map').after(html);

    if (this.overlays) {
      this.overlays.setHeaderMessageIsVisible(true);
    }
  },

  _removeSQLViewHeader: function() {
    this.$('.sqlview').remove();

    if (this.overlays) {
      this.overlays.setHeaderMessageIsVisible(false);
    }
  },

  _toggleSlides: function(e) {
    this.killEvent(e);
    this.slidesPanel && this.slidesPanel.toggle();
  },

  _clearView: function(e) {
    this.killEvent(e);
    this.activeLayerView.model.clearSQLView();
    return false;
  },

  _tableFromQuery: function(e) {
    this.killEvent(e);

    var duplicate_dialog = new cdb.editor.DuplicateDatasetView({
      model: this.table,
      user: this.user,
      clean_on_hide: true
    });
    duplicate_dialog.appendToBody();
  }

});