text: /**
   * 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();
    }
  
  });
  