<commit_before>(function(root) {

  'use strict';

  root.app = root.app || {};
  root.app.View = root.app.View || {};
  root.app.Model = root.app.Model || {};
  root.app.pubsub = root.app.pubsub || {};
  root.app.Helper = root.app.Helper || {};

  var Status = Backbone.Model.extend({
    defaults: { relationshipsVisible: true }
  });

  root.app.View.mapView = Backbone.View.extend({

    el: '.l-map',

    events: {
      'change .js-relationships-checkbox': 'triggerRelationshipsVisibility'
    },

    popupTemplate: HandlebarsTemplates['popup_template'],

    initialize: function(options) {
      this.router = options.router;
      this.status = new Status();

      this.actorsCollection = new root.app.Collection.actorsCollection(null, {
        router: this.router
      });
      this.actionsCollection = new root.app.Collection.actionsCollection(null, {
        router: this.router
      });
      /* actorModel and actionModel are used to store the information about the
       * maker whose popup is open. Their data can be fetched by this view, or
       * synced by another one using the pubsub object. */
      this.actorModel = new root.app.Model.actorModel();
      this.actionModel = new root.app.Model.actionModel();

      this.$legend = this.$el.find('#map-legend');
      this.$zoomButtons = this.$el.find('.leaflet-control-zoom');
      this.$relationshipsToggle = this.$el.find('.js-relationships-checkbox');
      this.$buttons = this.$el.find('#map-buttons');
      this.$credits = this.$el.find('.leaflet-control-attribution');
      /* Cache for the relationships part of the legend */
      this.$actorToActionLegend = this.$el.find('.js-actor-to-action');
      this.$actorToActorLegend = this.$el.find('.js-actor-to-actor');
      this.$actionToActionLegend = this.$el.find('.js-action-to-action');

      this.setListeners();

      this.initMap();
    },

    setListeners: function() {
      this.listenTo(root.app.pubsub, 'relationships:visibility',
        this.onRelationshipsVisibilityChange);
      this.listenTo(root.app.pubsub, 'sidebar:visibility',
        this.onSidebarVisibilityChange);

      this.listenTo(this.actorModel, 'sync', this.onActorModelSync);
      this.listenTo(this.actionModel, 'sync', this.onActionModelSync);
      this.listenTo(root.app.pubsub, 'sync:actorModel',
        this.onActorModelRemoteSync);
      this.listenTo(root.app.pubsub, 'sync:actionModel',
        this.onActionModelRemoteSync);
      this.listenTo(this.router, 'change:queryParams', this.onFiltering);
      this.listenTo(root.app.pubsub, 'click:goBack', this.onGoBack);
    },

    /* GETTERS */

    /* Return the marker (DOM element) corresponding at the type, id and
     * locationId passed as arguments. If not found, display a warning in the
     * console.
     * NOTE: in case the locationId is omited, return all the entity's markers
     */
    getMarker: function(type, id, locationId) {
      var entityClass = type === 'actors' ? '.js-actor-marker' :
        '.js-action-marker';
      var selector = entityClass + '[data-id="' + id + '"]' +
        (locationId ? '[data-location="' + locationId + '"]' : '');

      var marker = locationId ? document.querySelector(selector) :
        document.querySelectorAll(selector);

      return marker;
    },

    /* Return the type, id and locationId of the active marker */
    getActiveMarkerInfo: function() {
      var route = this.router.getCurrentRoute();
      var markerInfo = {};

      if(route.name === 'actors' || route.name === 'actions') {
        markerInfo = {
          type: route.name,
          id: route.params[0],
          locationId: route.params[1]
        };
      }

      return markerInfo;
    },

    /* Return all the map's highlighted markers as a NodeList */
    getAllHighlightedMarkers: function() {
      var selector = '.js-actor-marker.-active, .js-action-marker.-active';
      return document.querySelectorAll(selector);
    },

    /* EVENT HANDLERS */

    onMapClick: function() {
      var route = this.router.getCurrentRoute();

      this.resetMarkersHighlight();
      this.removeRelations();
      this.updateLegendRelationships();
      if(route.name === 'actions' || route.name === 'actors') {
        this.highlightActiveMarkers();
        this.renderActiveMarkerRelations();
      }
    },

    onMarkerClick: function(e) {
      var markers = this.getMarker(e.target.options.type,
        e.target.options.id);

      this.resetMarkersHighlight();
      this.removeRelations();
      this.highlightMarkers(markers);
      this.updateLegendRelationships(e.target);

      this.fetchModelFor(e.target.options.type, e.target.options.id)
        .then(function() {
          this.renderMarkerRelations(e.target.options.type, e.target.options.id,
            e.target.options.locationId);
          this.renderPopupFor(e.target);
        }.bind(this));
    },

    onMoreInfoButtonClick: function(marker) {
      this.router.navigate([
        '/' + marker.options.type,
        marker.options.id,
        marker.options.locationId
      ].join('/'), { trigger: true });

      root.app.pubsub.trigger('show:' + marker.options.type.slice(0, -1), {
        id: marker.options.id,
        locationId: marker.options.locationId
      });

      this.map.closePopup(marker.getPopup());
    },

    onFiltering: function() {
      this.map.closePopup();
      this.fetchFilteredCollections()
        .then(function() {
          this.removeMarkers();
          this.removeRelations();
          this.addFilteredMarkers();
          this.updateLegendRelationships();
        }.bind(this));
    },

    onRelationshipsVisibilityChange: function(options) {
      var isVisible = options.visible;
      /* We toggle the part concerning the relationships from the legend */
      this.$legend.toggleClass('-reduced', !isVisible);
      /* We move the zoom buttons according to the legend move */
      this.$zoomButtons.toggleClass('-slided', !isVisible);
      /* We toggle the switch button concerning the relationships */
      this.$relationshipsToggle.prop('checked', isVisible);
      /* We save the visibility to the model */
      this.status.set({ relationshipsVisible: isVisible });
      /* We toggle the relations' visibility */
      this.toggleRelationsVisibility();
    },

    onSidebarVisibilityChange: function(options) {
      this.$buttons.toggleClass('-slided', options.isHidden);
      this.$credits.toggleClass('-slided', options.isHidden);
    },

    /* Trigger an event through the pubsub object to inform about the new state
     * of the actor model */
    onActorModelSync: function() {
      root.app.pubsub.trigger('sync:actorModel', this.actorModel);
    },

    /* Trigger an event through the pubsub object to inform about the new state
     * of the action model */
    onActionModelSync: function() {
      root.app.pubsub.trigger('sync:actionModel', this.actionModel);
    },

    /* Set the content of this.actorModel with the content of the passed model
     * NOTE: as the view itself can trigger this method by the sync event of its
     * own model, we make the comprobation that the id of the passed model is
     * different from the one stored in the current model */
    onActorModelRemoteSync: function(model) {
      if(!_.isEmpty(this.actorModel.attributes) &&
        this.actorModel.get('id') === model.get('id')) {
        return;
      }

      this.actorModel.clear({ silent: true });
      this.actorModel.set(model.toJSON());
    },

    /* Set the content of this.actionModel with the content of the passed model
     * NOTE: as the view itself can trigger this method by the sync event of its
     * own model, we make the comprobation that the id of the passed model is
     * different from the one stored in the current model */
    onActionModelRemoteSync: function(model) {
      if(!_.isEmpty(this.actionModel.attributes) &&
        this.actionModel.get('id') === model.get('id')) {
        return;
      }

      this.actionModel.clear({ silent: true });
      this.actionModel.set(model.toJSON());
    },

    onGoBack: function() {
      this.map.closePopup();
      this.updateLegendRelationships();
      this.resetMarkersHighlight();
      this.removeRelations();
    },

    /* LOGIC */

    initMap: function() {
      this.renderMap()
        .then(this.fetchFilteredCollections.bind(this))
        .then(function() {
          this.addFilteredMarkers();
          this.highlightActiveMarkers();
          this.renderActiveMarkerRelations();
          this.updateLegendRelationships();
        }.bind(this));
    },

    /* Render the map and return a deferred */
    renderMap: function() {
      this.map = new L.Map('map', {
        center: [14.91, -23.51],
        zoom: 13,
        minZoom: 8,
        maxBounds: [
          L.latLng(13.637819, -28.389729),
          L.latLng(18.228372, -19.292213)
        ]
      });

      this.map.zoomControl.setPosition('bottomleft');
      this.map.on('click', this.onMapClick.bind(this));
      this.map.on('zoomend', this.updateMarkersSize.bind(this));

      var deferred = $.Deferred();
      cartodb.createLayer(this.map,
        'https://simbiotica.cartodb.com/api/v2/viz/d26b8254-78d1-11e5-b910-0ecfd53eb7d3/viz.json')
        .addTo(this.map)
        .on('done', deferred.resolve)
        .on('error', function(error) {
          console.error('Unable to render the map: ' + error);
          deferred.reject();
        });

      return deferred;
    },

    /* Fetch only the collections that are not filtered out and return a
     * deferred object */
    fetchFilteredCollections: function() {
      var queryParams = this.router.getQueryParams();

      /* If one of the required filter doesn't have any value, we just don't
       * fetch the collections, the user will see a warning in the sidebar
       * anyway */
      if(queryParams.types && queryParams.types.length === 0 ||
        queryParams.levels && queryParams.levels.length === 0 ||
        queryParams.domains_ids && queryParams.domains_ids.length === 0) {
        console.error('A required parameter hasn\'t been provided');
        return;
      }

      var params = {};
      if(queryParams.types && queryParams.types.length !== 2) {
        params.only = queryParams.types[0];
      }

      return this.fetchCollections(params);
    },

    /* Fetch the actors and actions collections and returns a deferred object
     * Options:
     *  - only ("actors" or "actions"): restrict the fetch to only one
     *    collection */
    fetchCollections: function(options) {
      var collectionsToFetch = [];

      if(!(options && options.only) || options && options.only === 'actors') {
        collectionsToFetch.push(this.actorsCollection);
      }
      if(!(options && options.only) || options && options.only === 'actions') {
        collectionsToFetch.push(this.actionsCollection);
      }

      var deferred = $.Deferred();
      $.when.apply(null, _.invoke(collectionsToFetch, 'fetch'))
        .done(deferred.resolve)
        .fail(function() {
          console.error('Unable to fetch the collections');
          deferred.reject();
        });

      return deferred;
    },

    /* Only add the markers of the collections that haven't been filtered out */
    addFilteredMarkers: function() {
      var queryParams = this.router.getQueryParams();

      var params = {};
      if(queryParams.types && queryParams.types.length !== 2) {
        params.only = queryParams.types[0];
      }

      this.addMarkers(params);
    },

    /* Add markers for each location of each entity of the collection.
    * Options:
    *  - only ("actors" or "actions"): restrict to only one collection */
    addMarkers: function(options) {
      /* Return the icon corresponding to each specific entity and location */
      var makeIcon = function(type, level, id, locationId) {
        return L.divIcon({
          html: '<svg class="map-marker ' +
            ((type === 'actors') ? '-actor js-actor-marker"' : '-action js-action-marker"') +
            ' data-id="' + id + '" data-location="' + locationId + '">' +
            '<use xlink:href="#' + level + 'MarkerIcon" x="0" y="0" />' +
            '<use xlink:href="#' + level + 'OutlineMarkerIcon" x="0" y="0" />' +
            '</svg>',
          className: type === 'actors' ? 'actor' : 'action',
          iconSize: L.point(22, 22),
          iconAnchor: L.point(11, 11),
          popupAnchor: L.point(0, -10)
        });
      };

      /* Method which actually adds the markers. Expects the collection (the
       * JSON object) and the type ("actions" or "actions") */
      var addEntityMarkers = function(collection, type) {
        var marker, popup;
        _.each(collection, function(entity) {
          _.each(entity.locations, function(location) {

            marker = L.marker([location.lat, location.long], {
              icon: makeIcon(type, entity.level, entity.id, location.id),
              type: type,
              id: entity.id,
              locationId: location.id
            });
            marker.addTo(this.map);

            /* We bind the basic popup */
            popup = L.popup({
              closeButton: false,
              minWidth: 220,
              maxWidth: 276, /* 20px padding + 4 icons */
              className: 'popup -' + type + ' -' + entity.level
            }).setContent('<div class="message -loading"><svg class="icon">' +
               '<use xlink:href="#waitIcon" x="0" y="0" /></svg>' +
               I18n.translate('front.loading') +
               '</message>');
            marker.bindPopup(popup);

            marker.on('click', this.onMarkerClick.bind(this));
          }, this);
        }, this);
      };

      /* We close the current popup if exists */
      this.map.closePopup();

      if(!(options && options.only) || options && options.only === 'actors') {
        addEntityMarkers.apply(this,
          [ this.actorsCollection.toJSON(), 'actors' ]);
      }
      if(!(options && options.only) || options && options.only === 'actions') {
        addEntityMarkers.apply(this,
          [ this.actionsCollection.toJSON(), 'actions' ]);
      }
    },

    /* Highlight the marker associated to the actor/action present in the URL if
     * exists, otherwise do nothing */
    highlightActiveMarkers: function() {
      var activeMarkerInfo = this.getActiveMarkerInfo();

      if(!_.isEmpty(activeMarkerInfo)) {
        var activeMarkers = this.getMarker(activeMarkerInfo.type,
          activeMarkerInfo.id);
        this.highlightMarkers(activeMarkers);
      }
    },

    /* Highlight the marker passed as argument or display a warning in the
     * console if the marker is evaluated as false (ie null or undefined) */
    highlightMarker: function(marker) {
      if(!marker) {
        console.warn('Unable to highlight a marker on the map');
        return;
      }
      marker.classList.add('-active');
    },<commit_msg>First version of the optimal positioning<commit_after>(function(root) {

  'use strict';

  root.app = root.app || {};
  root.app.View = root.app.View || {};
  root.app.Model = root.app.Model || {};
  root.app.pubsub = root.app.pubsub || {};
  root.app.Helper = root.app.Helper || {};

  var Status = Backbone.Model.extend({
    defaults: { relationshipsVisible: true }
  });

  root.app.View.mapView = Backbone.View.extend({

    el: '.l-map',

    events: {
      'change .js-relationships-checkbox': 'triggerRelationshipsVisibility'
    },

    popupTemplate: HandlebarsTemplates['popup_template'],

    initialize: function(options) {
      this.router = options.router;
      this.status = new Status();

      this.actorsCollection = new root.app.Collection.actorsCollection(null, {
        router: this.router
      });
      this.actionsCollection = new root.app.Collection.actionsCollection(null, {
        router: this.router
      });
      /* actorModel and actionModel are used to store the information about the
       * maker whose popup is open. Their data can be fetched by this view, or
       * synced by another one using the pubsub object. */
      this.actorModel = new root.app.Model.actorModel();
      this.actionModel = new root.app.Model.actionModel();

      this.$legend = this.$el.find('#map-legend');
      this.$zoomButtons = this.$el.find('.leaflet-control-zoom');
      this.$relationshipsToggle = this.$el.find('.js-relationships-checkbox');
      this.$buttons = this.$el.find('#map-buttons');
      this.$credits = this.$el.find('.leaflet-control-attribution');
      /* Cache for the relationships part of the legend */
      this.$actorToActionLegend = this.$el.find('.js-actor-to-action');
      this.$actorToActorLegend = this.$el.find('.js-actor-to-actor');
      this.$actionToActionLegend = this.$el.find('.js-action-to-action');

      this.setListeners();

      this.initMap();
    },

    setListeners: function() {
      this.listenTo(root.app.pubsub, 'relationships:visibility',
        this.onRelationshipsVisibilityChange);
      this.listenTo(root.app.pubsub, 'sidebar:visibility',
        this.onSidebarVisibilityChange);

      this.listenTo(this.actorModel, 'sync', this.onActorModelSync);
      this.listenTo(this.actionModel, 'sync', this.onActionModelSync);
      this.listenTo(root.app.pubsub, 'sync:actorModel',
        this.onActorModelRemoteSync);
      this.listenTo(root.app.pubsub, 'sync:actionModel',
        this.onActionModelRemoteSync);
      this.listenTo(this.router, 'change:queryParams', this.onFiltering);
      this.listenTo(root.app.pubsub, 'click:goBack', this.onGoBack);
    },

    /* GETTERS */

    /* Return the marker (DOM element) corresponding at the type, id and
     * locationId passed as arguments. If not found, display a warning in the
     * console.
     * NOTE: in case the locationId is omited, return all the entity's markers
     */
    getMarker: function(type, id, locationId) {
      var entityClass = type === 'actors' ? '.js-actor-marker' :
        '.js-action-marker';
      var selector = entityClass + '[data-id="' + id + '"]' +
        (locationId ? '[data-location="' + locationId + '"]' : '');

      var marker = locationId ? document.querySelector(selector) :
        document.querySelectorAll(selector);

      return marker;
    },

    /* Return the type, id and locationId of the active marker */
    getActiveMarkerInfo: function() {
      var route = this.router.getCurrentRoute();
      var markerInfo = {};

      if(route.name === 'actors' || route.name === 'actions') {
        markerInfo = {
          type: route.name,
          id: route.params[0],
          locationId: route.params[1]
        };
      }

      return markerInfo;
    },

    /* Return all the map's highlighted markers as a NodeList */
    getAllHighlightedMarkers: function() {
      var selector = '.js-actor-marker.-active, .js-action-marker.-active';
      return document.querySelectorAll(selector);
    },

    /* EVENT HANDLERS */

    onMapClick: function() {
      var route = this.router.getCurrentRoute();

      this.resetMarkersHighlight();
      this.removeRelations();
      this.updateLegendRelationships();
      if(route.name === 'actions' || route.name === 'actors') {
        this.highlightActiveMarkers();
        this.renderActiveMarkerRelations();
      }
    },

    onMarkerClick: function(e) {
      var markers = this.getMarker(e.target.options.type,
        e.target.options.id);

      this.resetMarkersHighlight();
      this.removeRelations();
      this.highlightMarkers(markers);
      this.updateLegendRelationships(e.target);

      this.fetchModelFor(e.target.options.type, e.target.options.id)
        .then(function() {
          this.renderMarkerRelations(e.target.options.type, e.target.options.id,
            e.target.options.locationId);
          this.renderPopupFor(e.target);
        }.bind(this));
    },

    onMoreInfoButtonClick: function(marker) {
      this.router.navigate([
        '/' + marker.options.type,
        marker.options.id,
        marker.options.locationId
      ].join('/'), { trigger: true });

      root.app.pubsub.trigger('show:' + marker.options.type.slice(0, -1), {
        id: marker.options.id,
        locationId: marker.options.locationId
      });

      this.map.closePopup(marker.getPopup());
    },

    onFiltering: function() {
      this.map.closePopup();
      this.fetchFilteredCollections()
        .then(function() {
          this.removeMarkers();
          this.removeRelations();
          this.addFilteredMarkers();
          this.updateLegendRelationships();
        }.bind(this));
    },

    onRelationshipsVisibilityChange: function(options) {
      var isVisible = options.visible;
      /* We toggle the part concerning the relationships from the legend */
      this.$legend.toggleClass('-reduced', !isVisible);
      /* We move the zoom buttons according to the legend move */
      this.$zoomButtons.toggleClass('-slided', !isVisible);
      /* We toggle the switch button concerning the relationships */
      this.$relationshipsToggle.prop('checked', isVisible);
      /* We save the visibility to the model */
      this.status.set({ relationshipsVisible: isVisible });
      /* We toggle the relations' visibility */
      this.toggleRelationsVisibility();
    },

    onSidebarVisibilityChange: function(options) {
      this.$buttons.toggleClass('-slided', options.isHidden);
      this.$credits.toggleClass('-slided', options.isHidden);
    },

    /* Trigger an event through the pubsub object to inform about the new state
     * of the actor model */
    onActorModelSync: function() {
      root.app.pubsub.trigger('sync:actorModel', this.actorModel);
    },

    /* Trigger an event through the pubsub object to inform about the new state
     * of the action model */
    onActionModelSync: function() {
      root.app.pubsub.trigger('sync:actionModel', this.actionModel);
    },

    /* Set the content of this.actorModel with the content of the passed model
     * NOTE: as the view itself can trigger this method by the sync event of its
     * own model, we make the comprobation that the id of the passed model is
     * different from the one stored in the current model */
    onActorModelRemoteSync: function(model) {
      if(!_.isEmpty(this.actorModel.attributes) &&
        this.actorModel.get('id') === model.get('id')) {
        return;
      }

      this.actorModel.clear({ silent: true });
      this.actorModel.set(model.toJSON());
    },

    /* Set the content of this.actionModel with the content of the passed model
     * NOTE: as the view itself can trigger this method by the sync event of its
     * own model, we make the comprobation that the id of the passed model is
     * different from the one stored in the current model */
    onActionModelRemoteSync: function(model) {
      if(!_.isEmpty(this.actionModel.attributes) &&
        this.actionModel.get('id') === model.get('id')) {
        return;
      }

      this.actionModel.clear({ silent: true });
      this.actionModel.set(model.toJSON());
    },

    onGoBack: function() {
      this.map.closePopup();
      this.updateLegendRelationships();
      this.resetMarkersHighlight();
      this.removeRelations();
    },

    /* LOGIC */

    initMap: function() {
      this.renderMap()
        .then(this.fetchFilteredCollections.bind(this))
        .then(function() {
          this.addFilteredMarkers();
          this.highlightActiveMarkers();
          this.renderActiveMarkerRelations();
          this.updateLegendRelationships();
        }.bind(this));
    },

    /* Render the map and return a deferred */
    renderMap: function() {
      this.map = new L.Map('map', {
        center: [14.91, -23.51],
        zoom: 13,
        minZoom: 8,
        maxBounds: [
          L.latLng(13.637819, -28.389729),
          L.latLng(18.228372, -19.292213)
        ]
      });

      this.map.zoomControl.setPosition('bottomleft');
      this.map.on('click', this.onMapClick.bind(this));
      this.map.on('zoomend', this.updateMarkersSize.bind(this));

      var deferred = $.Deferred();
      cartodb.createLayer(this.map,
        'https://simbiotica.cartodb.com/api/v2/viz/d26b8254-78d1-11e5-b910-0ecfd53eb7d3/viz.json')
        .addTo(this.map)
        .on('done', deferred.resolve)
        .on('error', function(error) {
          console.error('Unable to render the map: ' + error);
          deferred.reject();
        });

      return deferred;
    },

    /* Fetch only the collections that are not filtered out and return a
     * deferred object */
    fetchFilteredCollections: function() {
      var queryParams = this.router.getQueryParams();

      /* If one of the required filter doesn't have any value, we just don't
       * fetch the collections, the user will see a warning in the sidebar
       * anyway */
      if(queryParams.types && queryParams.types.length === 0 ||
        queryParams.levels && queryParams.levels.length === 0 ||
        queryParams.domains_ids && queryParams.domains_ids.length === 0) {
        console.error('A required parameter hasn\'t been provided');
        return;
      }

      var params = {};
      if(queryParams.types && queryParams.types.length !== 2) {
        params.only = queryParams.types[0];
      }

      return this.fetchCollections(params);
    },

    /* Fetch the actors and actions collections and returns a deferred object
     * Options:
     *  - only ("actors" or "actions"): restrict the fetch to only one
     *    collection */
    fetchCollections: function(options) {
      var collectionsToFetch = [];

      if(!(options && options.only) || options && options.only === 'actors') {
        collectionsToFetch.push(this.actorsCollection);
      }
      if(!(options && options.only) || options && options.only === 'actions') {
        collectionsToFetch.push(this.actionsCollection);
      }

      var deferred = $.Deferred();
      $.when.apply(null, _.invoke(collectionsToFetch, 'fetch'))
        .done(deferred.resolve)
        .fail(function() {
          console.error('Unable to fetch the collections');
          deferred.reject();
        });

      return deferred;
    },

    /* Only add the markers of the collections that haven't been filtered out */
    addFilteredMarkers: function() {
      var queryParams = this.router.getQueryParams();

      var params = {};
      if(queryParams.types && queryParams.types.length !== 2) {
        params.only = queryParams.types[0];
      }

      this.addMarkers(params);
    },

    /* Return the icon corresponding to each specific entity and location */
    /* Internal method used by _addEntityMarkers to create the icons of the
     * markers. Expects the type of markers, its level, its id and location id
     * NOTE: it shouldn't be called outside of _addEntityMarkers */
    _generateMarkerIcon: function(type, level, id, locationId) {
      return L.divIcon({
        html: '<svg class="map-marker ' +
          ((type === 'actors') ? '-actor js-actor-marker"' : '-action js-action-marker"') +
          ' data-id="' + id + '" data-location="' + locationId + '">' +
          '<use xlink:href="#' + level + 'MarkerIcon" x="0" y="0" />' +
          '<use xlink:href="#' + level + 'OutlineMarkerIcon" x="0" y="0" />' +
          '</svg>',
        className: type === 'actors' ? 'actor' : 'action',
        iconSize: L.point(22, 22),
        iconAnchor: L.point(11, 11),
        popupAnchor: L.point(0, -10)
      });
    },

    /* Internal method used by addMarkers to add markers to the map. Expects the
     * collection of actors/actions (the JSON object) and its type ("actions" or
     * "actions")
     * NOTE: it shouldn't be called outside of addMarkers */
    _addEntityMarkers: function(collection, type) {
      var marker, popup;
      _.each(collection, function(entity) {
        _.each(entity.locations, function(location) {

          marker = L.marker([location.lat, location.long], {
            icon: this._generateMarkerIcon(type, entity.level, entity.id,
              location.id),
            type: type,
            id: entity.id,
            locationId: location.id
          });

          this.markersLayer.addLayer(marker);

          /* We bind the basic popup */
          popup = L.popup({
            closeButton: false,
            minWidth: 220,
            maxWidth: 276, /* 20px padding + 4 icons */
            className: 'popup -' + type + ' -' + entity.level
          }).setContent('<div class="message -loading"><svg class="icon">' +
             '<use xlink:href="#waitIcon" x="0" y="0" /></svg>' +
             I18n.translate('front.loading') +
             '</message>');
          marker.bindPopup(popup);

          marker.on('click', this.onMarkerClick.bind(this));
        }, this);
      }, this);
    },

    /* Add markers for each location of each entity of the collection.
    * Options:
    *  - only ("actors" or "actions"): restrict to only one collection */
    addMarkers: function(options) {
      /* We close the current popup if exists */
      this.map.closePopup();

      this.markersLayer = L.layerGroup();

      if(!(options && options.only) || options && options.only === 'actors') {
        this._addEntityMarkers(this.actorsCollection.toJSON(), 'actors');
      }
      if(!(options && options.only) || options && options.only === 'actions') {
        this._addEntityMarkers(this.actionsCollection.toJSON(), 'actions');
      }

      this.computeMarkersOptimalPosition();
      this.markersLayer.addTo(this.map);
    },

    // /* Return a LatLng Leaflet object representing the coordinates of the
    //  * centroid of a set of markers (passed as arguments) */
    // _computeCentroidLatLng: function() {
    //   var markers      = arguments[0],
    //       markersCount = arguments[0].length,
    //       centroidLat  = 0,
    //       centroidLng  = 0;
    //
    //   var latLng;
    //   _.each(markers, function(m) {
    //     latLng = m.getLatLng();
    //     centroidLat += latLng.lat;
    //     centroidLng += latLng.lng;
    //   });
    //
    //   return L.latLng(centroidLat / markersCount, centroidLng / markersCount);
    // },

    /* Compute the position of each marker depending on the position of the
     * other ones. If several markers have the exact same position, we move them
     * along an archimede spiral. Expects the layer of markers. */
    computeMarkersOptimalPosition: function() {
      /* Constants used as parameters for the spiral */
      var spiralGap             = 0.001, /* unitless param */
          spiralInitialDistance = 0.005, /* unitless param */
          spiralAngleFactor     = Math.PI / 3; /* rad */

      var markers = this.markersLayer.getLayers();

      /* By grouping and filtering the markers, we get an array of all the
       * groups of markers which share the same location (ie an array of arrays
       * of markers with the same location) */
      var latLng;
      var conflictingMarkersGroups = _.filter(_.groupBy(markers, function(m) {
        latLng = m.getLatLng();
        return ''.concat(latLng.lat, latLng.lng);
      }), function(group) {
        return group.length > 1;
      });

      var centroidLatLng;
      _.each(conflictingMarkersGroups, function(conflictingMarkersGroup) {
        /* The centroid is the position of any marker as they all share the same
         * coordinates */
        centroidLatLng = conflictingMarkersGroup[0].getLatLng();

        L.marker(centroidLatLng).addTo(this.map);

        /* We compute the position of each marker from the centroid position.
         * Basically, we add a small deviance that follow the path of this
         * Archimedean spiral:
         * rho = a * theta + b (polar coordinates)
         *    where: a     is spiralGap
         *           b     is spiralInitialDistance
         *           theta is angle
         */
        var angle = spiralAngleFactor,
            optimalLat,
            optimalLng;
        _.each(conflictingMarkersGroup, function(conflictingMarker) {
          optimalLat = centroidLatLng.lat +
            (spiralInitialDistance + spiralGap * angle) * Math.cos(angle);
          optimalLng = centroidLatLng.lng +
            (spiralInitialDistance + spiralGap * angle) * Math.sin(angle);

          conflictingMarker.setLatLng(L.latLng(optimalLat, optimalLng));
          conflictingMarker.options.originalLatLng = centroidLatLng;

          angle += spiralAngleFactor;
        });

      }, this);
    },

    /* Highlight the marker associated to the actor/action present in the URL if
     * exists, otherwise do nothing */
    highlightActiveMarkers: function() {
      var activeMarkerInfo = this.getActiveMarkerInfo();

      if(!_.isEmpty(activeMarkerInfo)) {
        var activeMarkers = this.getMarker(activeMarkerInfo.type,
          activeMarkerInfo.id);
        this.highlightMarkers(activeMarkers);
      }
    },

    /* Highlight the marker passed as argument or display a warning in the
     * console if the marker is evaluated as false (ie null or undefined) */
    highlightMarker: function(marker) {
      if(!marker) {
        console.warn('Unable to highlight a marker on the map');
        return;
      }
      marker.classList.add('-active');
    },
{'fraction_non_alphanumeric': 0.13153871518930424, 'fraction_numerical': 0.006624898827902515, 'mean_word_length': 2.4782608695652173, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 22, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2396446', 'n_tokens_mistral': 9766, 'n_tokens_neox': 9331, 'n_words': 2282}