# Source: map_module.js

    /**
     * Módulo mapa, utilizado para crear y gestionar el mapa en nicho ecológico y comunidad ecológica.
     *
     * @namespace map_module
     */
    var map_module = (function(url_geoserver, workspace, verbose, url_zacatuche) {

        var map;

        var _VERBOSE = verbose;

        var _grid_d3, _grid_map;
        var _grid_map_hash = d3.map([]);

        var _DELETE_STATE_POINTS = false;

        var _url_geoserver = url_geoserver,
                _workspace = workspace;

        var _url_zacatuche = url_zacatuche;

        var _OSM_layer,
                _grid_wms,
                _species_layer,
                _states_layer,
                _eco_layer,
                _markersLayer,
                _baseMaps,
                _overlayMaps,
                _layer_control,
                _specie_target;

        // estilos para eliminar puntos
        var _geojsonMarkerOptions = {
            radius: 5,
            fillColor: "#4F9F37",
            color: "#488336",
            weight: 2,
            opacity: 1,
            fillOpacity: 0.6
        },
        _geojsonMarkerOptionsDelete = {
            radius: 5,
            fillColor: "#E10C2C",
            color: "#833643",
            weight: 2,
            opacity: 1,
            fillOpacity: 0.6
        },
        _customOptions = {
            'maxWidth': '500',
            'className': 'custom'
        };

        // estilos para herrameintas de estados
        var _geojsonStyleDefault = {
            radius: 7,
            fillColor: "#E2E613",
            color: "#ACAE36",
            weight: 1,
            opacity: 1,
            fillOpacity: 0.6
        },
        _geojsonHighlightStyle = {
            radius: 7,
            fillColor: "#16EEDC",
            color: "#36AEA4",
            weight: 1,
            opacity: 1,
            fillOpacity: 0.6
        },
        _geojsonMouseOverStyle = {
            radius: 7,
            fillColor: "#CED122",
            color: "#8C8E3A",
            weight: 1,
            opacity: 1,
            fillOpacity: 0.6
        };

        var _allowedPoints = d3.map([]),
                _geojsonFeature = [],
                _discardedPoints = d3.map([]),
                _discardedPointsFilter = d3.map([]),
                _computed_occ_cells = d3.map([]),
                _computed_discarded_cells = d3.map([]);

        var NUM_SECTIONS = 9;

        var _display_module, _language_module, _histogram_module;

        var _iTrans;

        var _MODULO_NICHO = 0, _MODULO_NET = 1;

        var _tipo_modulo;

        // Es un layer ficticio que sirve para controlar el layer hecho en D3 con los eventos del componente que maneja los layers
        var _lineLayer = L.Class.extend({
            initialize: function() {
                return;
            },
            onAdd: function(map) {
                _grid_d3.style("display", "block");
            },
            onRemove: function(map) {
                _grid_d3.style("display", "none");
            }
        });

        var _switchD3Layer;

        var _toastr = toastr;

        var _range_limits_red = [];
        var _range_limits_blue = [];
        var _range_limits_total = [];
        var _resultado_grid;

        /**
         * Método getter del controlador de capas.
         *
         * @function get_layerControl
         * @public
         * @memberof! map_module
         *
         */
        function get_layerControl() {
            return _layer_control;
        }

        /**
         * Método getter de la especie objetivo seleccionada.
         *
         * @function get_specieTarget
         * @public
         * @memberof! map_module
         *
         */
        function get_specieTarget() {
            return _specie_target;
        }

        /**
         * Método setter de la especie objetivo seleccionada.
         *
         * @function set_specieTarget
         * @public
         * @memberof! map_module
         *
         * @param {json} specie_target - Json con la información de la especie objetivo seleccionada
         */
        // Asigna el valor de una especie seleccionada a una variable global del módulo.
        function set_specieTarget(specie_target) {
            _specie_target = specie_target;
        }

        /**
         * Método getter de las ocurrencias de la especie objetivo consideradas para el análisis de nicho o comunidad ecológica.
         *
         * @function get_allowedPoints
         * @public
         * @memberof! map_module
         *
         */
        function get_allowedPoints() {
            return _allowedPoints;
        }

        /**
         * Método getter de las ocurrencias descartadas de la especie objetivo para el análisis de nicho o comunidad ecológica.
         *
         * @function get_allowedPoints
         * @public
         * @memberof! map_module
         *
         */
        function get_discardedPoints() {
            return _discardedPoints;
        }

        /**
         * Método getter de las ocurrencias descartadas por filtros de la especie objetivo para el análisis de nicho o comunidad ecológica.
         *
         * @function get_discardedPointsFilter
         * @public
         * @memberof! map_module
         *
         */
        function get_discardedPointsFilter() {
            return _discardedPointsFilter;
        }

        /**
         * Método getter de las celdas decartadas para el análisis de nicho o comunidad ecológica.
         *
         * @function get_discardedCellFilter
         * @public
         * @memberof! map_module
         *
         */
        function get_discardedCellFilter() {
            return _computed_discarded_cells;
        }

        /**
         * Método getter de las celdas consideradas para el análisis de nicho o comunidad ecológica.
         *
         * @function get_allowedCells
         * @public
         * @memberof! map_module
         *
         */
        function get_allowedCells() {
            return _computed_occ_cells;
        }

        /**
         * Método getter del mapa utilizado en el análisis de nicho o comunidad ecológica.
         *
         * @function getMap
         * @public
         * @memberof! map_module
         *
         */
        function getMap() {
            return map;
        }

        /**
         * Método setter del controlador de nicho o comunidad ecológica.
         *
         * @function setDisplayModule
         * @public
         * @memberof! map_module
         *
         * @param {object} display_module - Referencia al controlador de nicho o comunidad ecológica
         */
        function setDisplayModule(display_module) {
            _display_module = display_module;
        }

        // ******************************************************************* geojson-vt
        var _tileIndex;
        var _tileOptions = {
            maxZoom: 20, // max zoom to preserve detail on
            tolerance: 5, // simplification tolerance (higher means simpler)
            extent: 4096, // tile extent (both width and height)
            buffer: 64, // tile buffer on each side
            debug: 0, // logging level (0 to disable, 1 or 2)

            indexMaxZoom: 0, // max zoom in the initial tile index
            indexMaxPoints: 100000, // max number of points per tile in the index
        };
        var _tileLayer, _tileBoudary;
        var _pad;

        function whenClicked(e) {
            // e = event
            console.log(e);
            // You can make your ajax call declaration here
            //$.ajax(...
        }

        function onEachFeature(feature, layer) {

            console.log("onEachFeature");
            //bind click
            layer.on({
                click: whenClicked
            });
        }

        /**
         * Éste método realiza la creación del objeto mapa, configura el módulo de lenguaje, genera lso controles para interactur con el mapa y realiza petición de la malla.
         *
         * @function _mapConfigure
         * @private
         * @memberof! map_module
         *
         * @param {object} language_module - Módulo lenguaje
         * @param {integer} tipo_modulo - Tipo de módulo donde será asignado el mapa, nicho o comunidad ecológica
         * @param {object} histogram_module - Módulo histograma
         */
        function _mapConfigure(language_module, tipo_modulo, histogram_module) {

            _VERBOSE ? console.log("_mapConfigure") : _VERBOSE;

            _tipo_modulo = tipo_modulo;

            _histogram_module = histogram_module;

            _language_module = language_module;
            _iTrans = _language_module.getI18();

            _toastr.options = {
                "debug": false,
                "onclick": null,
                "fadeIn": 300,
                "fadeOut": 1000,
                "timeOut": 2000,
                "extendedTimeOut": 2000,
                "positionClass": "toast-bottom-center",
                "preventDuplicates": true,
                "progressBar": true
            };

            var milliseconds = new Date().getTime();
            var url = _url_geoserver + "t=" + milliseconds;
            var espacio_capa = _workspace + ":sp_grid_terrestre";

            // normal osm map: http://{s}.tile.osm.org/{z}/{x}/{y}.png
            // black and white map: http://{s}.tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png
            // relieve: http://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png
            // cartoDB: 'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'
            _OSM_layer = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png');

            // ******************************************************************* geojson-vt
            _tileIndex = geojsonvt([], _tileOptions);
            _tileLayer = L.canvasTiles()
                    .params({
                        debug: false,
                        padding: 5,
                        onEachFeature: onEachFeature
                    })
                    .drawing(_drawingOnCanvas);

    //        var centro_mapa = (_tipo_modulo == _MODULO_NICHO) ? [30.5, -99] : [30.5, -102];
    //          var zoom_module = (_tipo_modulo == _MODULO_NICHO) ? 4 : 3;
            var centro_mapa = (_tipo_modulo == _MODULO_NICHO) ? [23.5, -99] : [23.5, -102];
            var zoom_module = (_tipo_modulo == _MODULO_NICHO) ? 5 : 4;

            // ambos div tienen el id = 'map', tanto en nicho como en comunidad
            map = L.map('map', {
                center: centro_mapa,
                zoom: zoom_module,
                layers: [
                    _OSM_layer,
                    _tileLayer
                ]
            });

            _baseMaps = {
                "Open Street Maps": _OSM_layer
            };
            _overlayMaps = {
                "Malla": _tileLayer
    //            ,"País": _tileBoudary
            };
            _layer_control = L.control.layers(_baseMaps, _overlayMaps).addTo(map);

            map.scrollWheelZoom.disable();

            if (_tipo_modulo == _MODULO_NICHO) {
                // document.getElementById("tbl_hist").style.display = "none";
                // document.getElementById("dShape").style.display = "none";
                _addControls();
            }

            _loadD3GridMX();

        }

        var _zoom_level;
        var _xhr = null;

        /**
         * Realiza la petición de la malla al servidor
         *
         * @function _loadD3GridMX
         * @public
         * @memberof! map_module
         *
         */
        // TODO: sincronizar meotodo para que sea realizado antes de que se busque una especie y de un error.
        function _loadD3GridMX() {

            _VERBOSE ? console.log("_loadD3GridMX") : _VERBOSE;

            $.ajax({
                url: _url_zacatuche + "/niche/especie",
                type: 'post',
                dataType: "json",
                data: {
                    "qtype": "getGridGeoJsonMX"
                },
                // success : function (jsonc){
                success: function(json) {

                    // Asegura que el grid este cargado antes de realizar una generacion por enlace
                    $("#loadData").prop("disabled", false);

                    _grid_map = json;

                    // importacion con otras APIs
                    // // Se comprime json del lado del servidor, se descomprime en el cliente
                    // json = JSONC.decompress(jsonc);
                    // // console.log(json);

                    // _loadD3GridEU();
                    colorizeFeatures(_grid_map, true);
                    _pad = 0;
                    _tileIndex = geojsonvt(_grid_map, _tileOptions);
                    _tileLayer.redraw();

                    // agrega listener para generar pop en celda
                    map.on('click', function(e) {
                        console.log(e.latlng.lat + ", " + e.latlng.lng);

                        if (_tipo_modulo == _MODULO_NICHO) {
                            _display_module.showGetFeatureInfo(e.latlng.lat, e.latlng.lng);
                        }

                    });

                },
                error: function() {
                    // alert("Existe un error en la conexión con el servidor, intente mas tarde");
                    console.log("abort");
                }

            });

        }

        /**
         * Asigna color y borde a las celdas que componen la malla en nicho ecológico.
         *
         * @function colorizeFeatures
         * @public
         * @memberof! map_module
         *
         * @param {json} grid_map_color - GeoJson de la malla
         * @param {boolean} first - Bandera para conocer si es la primera carga de la malla
         */
        function colorizeFeatures(grid_map_color, first) {

            _VERBOSE ? console.log("colorizeFeatures") : _VERBOSE;

            if (first) {
                console.log("first loaded");
                for (var i = 0; i < _grid_map.features.length; i++) {
                    _grid_map.features[i].properties.color = '';
                }
            }
            else {
                for (var i = 0; i < _grid_map.features.length; i++) {

                    if (grid_map_color.has(_grid_map.features[i].properties.gridid)) {
                        _grid_map.features[i].properties.opacity = 1;
                        _grid_map.features[i].properties.color = grid_map_color.get(_grid_map.features[i].properties.gridid);  //'hsl(' + 360 * Math.random() + ', 50%, 50%)';
                    }
                    else {
                        _grid_map.features[i].properties.color = 'rgba(255,0,0,0)';
                        // _grid_map.features[i].properties.opacity = 0;
                    }
                }
            }

            _tileLayer.redraw();

        }

        /**
         * Asigna color y borde a las celdas que componen la malla en comunidad ecológica.
         *
         * @function colorizeFeaturesNet
         * @public
         * @memberof! map_module
         *
         * @param {array} arg_gridid - Array con los ids de la malla
         * @param {type} arg_count - Número de ocurrencias por celda contenidas en la malla
         * @param {function} link_color_brewer - Función que asigna color a cada celda de la malla
         */
        function colorizeFeaturesNet(arg_gridid, arg_count, link_color_brewer) {

            _VERBOSE ? console.log("colorizeFeaturesNet") : _VERBOSE;

            console.log(_grid_map);

            for (var i = 0; i < _grid_map.features.length; i++) {

                index_grid = arg_gridid.indexOf(_grid_map.features[i].properties.gridid);

                if (index_grid != -1) {

                    _grid_map.features[i].properties.opacity = 1;
                    _grid_map.features[i].properties.color = link_color_brewer(arg_count[index_grid]);

                }
                else {

                    _grid_map.features[i].properties.color = 'rgba(255,0,0,0)';

                }

            }

            _tileLayer.redraw();

        }

        /**
         * Crea y configura la malla embebida en la capa del mapa.
         *
         * @function _drawingOnCanvas
         * @private
         * @memberof! map_module
         *
         * @param {object} canvasOverlay - Objecto canvas donde esta contenida la malla
         * @param {json} params - Json con los parámetros para configurar la malla
         */
        function _drawingOnCanvas(canvasOverlay, params) {

            var bounds = params.bounds;
            params.tilePoint.z = params.zoom,
                    elemLeft = params.canvas.offsetLeft,
                    elemTop = params.canvas.offsetTop;

            var ctx = params.canvas.getContext('2d');
            ctx.globalCompositeOperation = 'source-over';

            ctx.canvas.addEventListener('click', function(event) {

                var x = event.pageX - elemLeft,
                        y = event.pageY - elemTop;

    //            console.log(event);
    //            console.log(x);
    //            console.log(y);

            }, false);

            var tile = _tileIndex.getTile(params.tilePoint.z, params.tilePoint.x, params.tilePoint.y);
            if (!tile) {
                return;
            }

            ctx.clearRect(0, 0, params.canvas.width, params.canvas.height);

            var features = tile.features;

            // borde de la malla
            ctx.strokeStyle = 'rgba(255,0,0,0)';
            // ctx.strokeStyle = 'grey'; // hace malla visible

            for (var i = 0; i < features.length; i++) {
                var feature = features[i],
                        type = feature.type;

                // background de la celda
                ctx.fillStyle = feature.tags.color ? feature.tags.color : 'rgba(255,0,0,0)';
                ctx.beginPath();

                for (var j = 0; j < feature.geometry.length; j++) {
                    var geom = feature.geometry[j];

                    if (type === 1) {
                        ctx.arc(geom[0] * ratio + _pad, geom[1] * ratio + _pad, 2, 0, 2 * Math.PI, false);
                        continue;
                    }

                    for (var k = 0; k < geom.length; k++) {
                        var p = geom[k];
                        var extent = 4096;

                        var x = p[0] / extent * 256;
                        var y = p[1] / extent * 256;
                        if (k)
                            ctx.lineTo(x + _pad, y + _pad);
                        else
                            ctx.moveTo(x + _pad, y + _pad);
                    }
                }

                if (type === 3 || type === 1) {
                    ctx.fill('evenodd');
                }

                ctx.stroke();
            }

        };

        /**
         * Agrega capas al controlador de capas.
         *
         * @function addMapLayer
         * @public
         * @memberof! map_module
         *
         * @param {object} layer - Capa para ser agregada al controlador de capas
         * @param {String} name - Nombre de la capa
         */
        function addMapLayer(layer, name) {

            _VERBOSE ? console.log("addMapLayer") : _VERBOSE;

            map.addLayer(layer);
            _layer_control.addOverlay(layer, name);

        }

        /**
         * Elimina capas del controlador de capas.
         *
         * @function removeMapLayer
         * @public
         * @memberof! map_module
         *
         * @param {object} layer - Capa para ser agregada al controlador de capas
         * @param {String} name - Nombre de la capa
         */
        function removeMapLayer(layer, name) {

            _VERBOSE ? console.log("removeMapLayer") : _VERBOSE;

            map.removeLayer(layer);
            _layer_control.removeLayer(layer);

        }

        /**
         * Agrega controles a la instancia del mapa.
         *
         * @function addMapControl
         * @public
         * @memberof! map_module
         *
         * @param {object} control - Objeto tipo control
         */
        function addMapControl(control) {

            map.addControl(control);

        }

        /**
         * Elimina controles a la instancia del mapa.
         *
         * @function removeMapControl
         * @public
         * @memberof! map_module
         *
         * @param {object} control - Objeto tipo control
         */
        function removeMapControl(control) {

            map.removeControl(control);

        }

        var _fisrtMap = true;

        /**
         * Despliega la tabla con los elementos que contiene la celda seleccioanda.
         *
         * @function showPopUp
         * @public
         * @memberof! map_module
         *
         * @param {String} htmltable - Tabla estructurada en formato HTML con la información por celda del análisis de nicho ecológico
         * @param {object} latlng - Objeto que contiene las coordenadas donde fue seleccionada la celda
         */
        function showPopUp(htmltable, latlng) {

            _VERBOSE ? console.log("showPopUp") : _VERBOSE;

            var popup = L.popup();
            popup.setLatLng(latlng).setContent(htmltable).openOn(map);

        }

        /**
         * Agrega control personalizado para la eliminación de puntos.
         *
         * @function _addControls
         * @private
         * @memberof! map_module
         *
         */
        function _addControls() {

            _VERBOSE ? console.log("_addControls") : _VERBOSE;

            PointDeleteControl = L.Control.extend({
                options: {
                    position: 'topleft',
                },
                onAdd: function(map) {
                    var controlDiv = L.DomUtil.create('div', 'leaflet-control-command ');

                    L.DomEvent
                            .addListener(controlDiv, 'click', L.DomEvent.stopPropagation)
                            .addListener(controlDiv, 'click', L.DomEvent.preventDefault)
                            .addListener(controlDiv, 'click', function() {
                                _deletePoints();
                            });

                    _VERBOSE ? console.log(_iTrans.prop('lb_borra_puntos')) : _VERBOSE;

                    var controlUI = L.DomUtil.create('div', 'leaflet-control-command-interior glyphicon glyphicon-erase', controlDiv);
                    controlUI.title = _iTrans.prop('lb_borra_puntos');
                    controlUI.id = "deletePointsButton";

                    return controlDiv;
                }
            });

            L.control.command = function(options) {
                return new PointDeleteControl(options);
            };

            map.addControl(new PointDeleteControl());

        }

        var _lin_inf = undefined;
        var _lin_sup = undefined;
        var _sin_fecha = undefined;

        /**
         * Busca las ocurrencias de una especie asignando filtros por fecha.
         *
         * @function busca_especie_filtros
         * @public
         * @memberof! map_module
         *
         * @param {array} rango - Array con el rango de fechas
         * @param {boolean} sfecha - Bandera para saber si serán considerados los registros sin fecha
         */
        function busca_especie_filtros(rango, sfecha) {

            _VERBOSE ? console.log("busca_especie") : _VERBOSE;

            _lin_inf = rango ? rango[0] : undefined;
            _lin_sup = rango ? rango[1] : undefined;
            _sin_fecha = sfecha;

            busca_especie(true);

            _toastr.info("Recalculando ocurrencias de especie");

        }

        /**
         * Éste método obtiene las ocurrencias de una especie seleccionada en el análisis de nicho ecológico.
         *
         * @function busca_especie
         * @public
         * @memberof! map_module
         *
         * @param {boolean} cfiltros - Bandera para saber si serán considerados los filtros temporales
         */
        function busca_especie(cfiltros) {

            _VERBOSE ? console.log("busca_especie") : _VERBOSE;

            var milliseconds = new Date().getTime();
            _sin_fecha = $("#chkFecha").is(':checked') ? true : false;

            $.ajax({
                url: _url_zacatuche + "/niche/especie",
                type: 'post',
                dataType: "json",
                data: {
                    "qtype": "getSpecies",
                    "id": _specie_target.spid,
                    "idtime": milliseconds,
                    "lim_inf": _lin_inf,
                    "lim_sup": _lin_sup,
                    "sfecha": _sin_fecha
                },
                beforeSend: function(xhr) {
                    xhr.setRequestHeader('X-Test-Header', 'test-value');
                    xhr.setRequestHeader("Accept", "text/json");
                },
                success: function(resp) {

                    d = resp.data;

                    try {
                        _markersLayer.clearLayers();
                        _layer_control.removeLayer(_markersLayer);

                    } catch (e) {
                        _VERBOSE ? console.log("primera vez") : _VERBOSE;
                    }

                    _discardedPoints = d3.map([]);			// puntos descartados por eliminacion

                    _allowedPoints = d3.map([]);			// puntos para analisis
                    _discardedPointsFilter = d3.map([]); 	// puntos descartados por filtros

                    _computed_occ_cells = d3.map([]);		// celdas para analisis
                    // _computed_discarded_cells = d3.map([]);	// celdas descartadas por filtros

                    // var computed_occ_cells_totals = d3.map([]);
                    var distinctPoints = d3.map([]);

                    if (d.length == 0) {
                        _VERBOSE ? console.log("No hay registros de especie") : _VERBOSE;
                        return;
                    }

                    // obtiene registros unicos en coordenadas
                    for (i = 0; i < d.length; i++) {

                        item_id = JSON.parse(d[i].json_geom).coordinates;

                        // console.log(d[i].gridid);
                        distinctPoints.set(item_id, d[i]);
                        _computed_occ_cells.set(parseInt(d[i].gridid), d[i]);
                    }

                    occ_cell = _computed_occ_cells.values().length;

                    $.each(distinctPoints.values(), function(index, item) {

                        item_id = JSON.parse(item.json_geom).coordinates.toString();

                        // this map is fill with the records in the database from an specie, so it discards repetive elemnts.
                        _allowedPoints.set(item_id, {
                            "type": "Feature",
                            "properties": {"url": item.urlejemplar, "fecha": item.fechacolecta, "specie": _specie_target.label, "gridid": item.gridid},
                            "geometry": JSON.parse(item.json_geom)
                        });

                    });

                    try {
                        map.removeLayer(_switchD3Layer);
                    }
                    catch (e) {
                        _VERBOSE ? console.log("layer no creado") : _VERBOSE;
                    }

                    _addPointLayer();

                    if (_tipo_modulo == _MODULO_NICHO) {

                        _histogram_module.createBarChartFecha(distinctPoints.values());
                    }

                    _fillSpeciesData(_allowedPoints.values().length, occ_cell);

                    $("#deletePointsButton").attr("title", $.i18n.prop('lb_borra_puntos'));

                },
                error: function(jqXHR, textStatus, errorThrown) {
                    _VERBOSE ? console.log("error: " + textStatus) : _VERBOSE;
                    _VERBOSE ? console.log(errorThrown) : _VERBOSE;
                    _VERBOSE ? console.log(jqXHR.responseText) : _VERBOSE;
                }

            });

        };

        /**
         * Éste método despliega la información de la especie seleccionada en el análisis de nicho ecológico.
         *
         * @function _fillSpeciesData
         * @private
         * @memberof! map_module
         *
         * @param {integer} occ - Número de ocurrencias de la especie
         * @param {integer} occ_cell - Número de celdas ocupadas por la especie
         */
        function _fillSpeciesData(occ, occ_cell) {

            _VERBOSE ? console.log("_specie_target") : _VERBOSE;

            $("#lb_sum_reino_res").text(_specie_target.reino);
            $("#lb_sum_phylum_res").text(_specie_target.phylum);
            $("#lb_sum_clase_res").text(_specie_target.clase);
            $("#lb_sum_orden_res").text(_specie_target.orden);
            $("#lb_sum_familia_res").text(_specie_target.familia);
            $("#lb_sum_genero_res").text(_specie_target.genero);
            $("#lb_sum_especie_res").text(_specie_target.especie);

            $("#num_occ").text(occ);
            $("#num_occ_celda").text(occ_cell);

        }

        /**
         * Éste método realiza la carga de una capa en el mapa con las ocurrencias de la especie objetivo seleccionada en el análisis de nicho ecológico.
         *
         * @function _addPointLayer
         * @private
         * @memberof! map_module
         *
         */
        function _addPointLayer() {

            _VERBOSE ? console.log("_addPointLayer") : _VERBOSE;

            _geojsonFeature = {"type": "FeatureCollection",
                "features": _allowedPoints.values()};

            _markersLayer = L.markerClusterGroup({maxClusterRadius: 30, chunkedLoading: true});

            _species_layer = L.geoJson(_geojsonFeature, {
                pointToLayer: function(feature, latlng) {

                    return L.circleMarker(latlng, _geojsonMarkerOptions);
                },
                onEachFeature: function(feature, layer) {
                    var message = _getMessagePopup(feature);
                    layer.bindPopup(message, _customOptions);
                }

            });

            _markersLayer.addLayer(_species_layer);
            map.addLayer(_markersLayer);

            _layer_control.addOverlay(_markersLayer, _specie_target.label);
        }

        /**
         * Éste método controla la eliminación de puntos de una capa de ocurrencias de la especie objetivo seleccionada en el análisis de nicho ecológico.
         *
         * @function _deletePoints
         * @private
         * @memberof! map_module
         *
         */
        function _deletePoints() {

            _VERBOSE ? console.log("_deletePoints") : _VERBOSE;

            if (!(_markersLayer))
                return;

            if (_DELETE_STATE_POINTS) {

                try {
                    map.addLayer(_switchD3Layer);
                } catch (e) {
                    _VERBOSE ? console.log("layer no creado") : _VERBOSE;
                }

                _DELETE_STATE_POINTS = false;

                $("#deletePointsButton").css("backgroundColor", "#fff");

                _markersLayer.getLayers().forEach(function(item) {

                    item.setStyle(_geojsonMarkerOptions);

                    item.off('click');

                    item.on('click', function() {

                        L.DomEvent.stopPropagation;

                        var popup = L.popup({className: 'custom'});
                        // popup.className('custom');

                        var message = _getMessagePopup(item.feature);
                        popup.setLatLng([item.feature.geometry.coordinates[1], item.feature.geometry.coordinates[0]]).setContent(message).openOn(map);

                    });

                });

            }
            else {

                // remueve el layer del grid para poder eliminar puntos
                try {
                    map.removeLayer(_switchD3Layer);
                } catch (e) {
                    _VERBOSE ? console.log("layer no creado") : _VERBOSE;
                }

                _DELETE_STATE_POINTS = true;

                $("#deletePointsButton").css("backgroundColor", "#BFADB6");

                _markersLayer.getLayers().forEach(function(item) {

                    item.off('click');

                    item.on('click', function() {

                        L.DomEvent.stopPropagation;

                        item_id = item.feature.geometry.coordinates.toString();

                        _discardedPoints.set(item_id, item);

                        if (_allowedPoints.remove(item_id)) {
                            _VERBOSE ? console.log("deleted") : _VERBOSE;
                        }
                        else {
                            _VERBOSE ? console.log("Error: point not deleted") : _VERBOSE;
                        }
                        _markersLayer.removeLayer(item);

                    });

                    item.setStyle(_geojsonMarkerOptionsDelete);

                });

            }

        }

        /**
         * Éste método despliega el mensaje informativo de la ocurrencia de la especie seleccionada en el análisis de nicho ecológico.
         *
         * @function _getMessagePopup
         * @private
         * @memberof! map_module
         *
         * @param {object} feature - Objeto tipo punto de la ocucurrencia seleccionada para el análisis de nicho ecológico
         */
        function _getMessagePopup(feature) {

            coordinates = parseFloat(feature.geometry.coordinates[1]).toFixed(2) + ", " + parseFloat(feature.geometry.coordinates[0]).toFixed(2)

            var fecha = feature.properties.fecha == null ? "" : feature.properties.fecha;

            var url = "";

            if (feature.properties.url.startsWith("http://")) {
                url = feature.properties.url.replace("http://", "");
            }
            else if (feature.properties.url.startsWith("https://")) {
                url = feature.properties.url.replace("https://", "");
            }
            else {
                url = feature.properties.url;
            }

            var message = "INFORMACIÓN ESPECIE<br/>Nombre: " + feature.properties.specie + "<br/>Colecta: " + fecha + "<br/>Coordenadas: " + coordinates + "<br/><a target='_blank' class='enlace_sp' href='http://" + url + "'>" + _iTrans.prop("link_sp") + "</a>";
            return message;
        }

        /**
         * Éste método realiza la partición en deciles y la asignación de escala de colores de un conjunto de celdas con valores de score asignado.
         *
         * @function createDecilColor
         * @public
         * @memberof! map_module
         *
         * @param {json} json - Json con el conjunto de celdas y score asignado resultado del análisis de nicho ecológico
         * @param {boolean} mapa_prob - Bandera para saber si el mapa despliega el color con probalidad por celda
         */
        function createDecilColor(json, mapa_prob) {

            _VERBOSE ? console.log("createDecilColor") : _VERBOSE;

            var red_arg = [];
            var blue_arg = [];
            var prob_arg = [];
            var t_chunks = [];
            var prob_chunks = [];
            var red_chunks = [];
            var blue_chunks = [];

            grid_color = d3.map([]);

            if (!mapa_prob) {

                _VERBOSE ? console.log("Sin probabilidad") : _VERBOSE;

                $.each(json, function(index, d) {

                    d.tscore = parseFloat(d.tscore);

                    if (d.tscore >= 0) {
                        red_arg.push(d)
                    }
                    else {
                        blue_arg.push(d);
                    }

                });

                if (blue_arg.length > 0 && red_arg.length > 0) {

                    _VERBOSE ? console.log("ambos") : _VERBOSE;

                    var min_json = d3.min(blue_arg.map(function(d) {
                        return parseFloat(d.tscore)
                    }));
                    var max_json = d3.max(red_arg.map(function(d) {
                        return parseFloat(d.tscore)
                    }));

                    _VERBOSE ? console.log("min_json: " + min_json) : _VERBOSE;
                    _VERBOSE ? console.log("max_json: " + max_json) : _VERBOSE;

                    if (Math.abs(min_json) > Math.abs(max_json)) {

                        _VERBOSE ? console.log("primero blue") : _VERBOSE;

                        _resultado_grid = 0;

                        // _VERBOSE ? console.log(blue_arg.length) : _VERBOSE;
                        t_chunks = _getDividedChunks(blue_arg, red_arg, false, mapa_prob);
                        blue_chunks = t_chunks[0];
                        red_chunks = t_chunks[1];

                        // red_chunks.reverse();

                        // when blues go first the collection goes from min to max

                    }
                    else {

                        _VERBOSE ? console.log("primero red") : _VERBOSE;

                        _resultado_grid = 1;

                        t_chunks = _getDividedChunks(red_arg, blue_arg, true, mapa_prob);
                        red_chunks = t_chunks[0];
                        blue_chunks = t_chunks[1];

                        // when reds go first the collection goes from max to min
                        red_chunks.reverse();

                    }

                }
                else if (blue_arg.length == 0 && red_arg.length > 0) {

                    _VERBOSE ? console.log("positivos") : _VERBOSE;

                    t_chunks = _getDividedChunks(red_arg, [], true, mapa_prob);
                    red_chunks = t_chunks[0];
                    blue_chunks = t_chunks[1];

                    red_chunks.reverse();

                }
                else if (blue_arg.length > 0 && red_arg.length == 0) {

                    _VERBOSE ? console.log("negativos") : _VERBOSE;

                    t_chunks = _getDividedChunks(blue_arg, [], false, mapa_prob);
                    blue_chunks = t_chunks[0];
                    red_chunks = t_chunks[1];

                }

                _VERBOSE ? console.log(blue_chunks) : _VERBOSE;
                _VERBOSE ? console.log(red_chunks) : _VERBOSE;

                color_scale = colorbrewer.Reds[9];
                $.each(red_chunks, function(index, value) {

                    value.forEach(function(d) {

                        grid_color.set(d.gridid, color_scale[index]);

                    });

                });

                color_scale = colorbrewer.Blues[9];
                $.each(blue_chunks, function(index, value) {

                    value.forEach(function(d) {

                        grid_color.set(d.gridid, color_scale[index]);

                    });

                });

            }
            else {

                _VERBOSE ? console.log("Probabilidad") : _VERBOSE;

                prob_arg = json;

                console.log(colorbrewer.RdBu[11]);
                var link_color = d3.scale.quantize().domain([1, 0]).range(colorbrewer.RdBu[11]);

                $.each(prob_arg, function(index, value) {

                    grid_color.set(value.gridid, link_color(parseFloat(value.tscore)));

                });

            }

            _VERBOSE ? console.log(grid_color.values()) : _VERBOSE;

            _cargaPaletaColor(mapa_prob);

            return grid_color;

        }

        /**
         * Éste método obtiene la escala de colores para la coloración del mapa
         *
         * @function _cargaPaletaColor
         * @private
         * @memberof! map_module
         *
         * @param {boolean} mapa_prob - Bandera para saber si el mapa despliega el color con probalidad por celda
         */
        function _cargaPaletaColor(mapa_prob) {

            _VERBOSE ? console.log("_cargaPaletaColor") : _VERBOSE;

            $("#escala_color").empty();

            // _VERBOSE ? console.log(_range_limits_red) : _VERBOSE;
            _VERBOSE ? console.log(_range_limits_blue) : _VERBOSE;
            // _VERBOSE ? console.log(_resultado_grid) : _VERBOSE;

            var data = [];

            var rojos = colorbrewer.Reds[9].slice();
            ;
            var azules = colorbrewer.Blues[9].slice();

            // _VERBOSE ? console.log(colorbrewer.Reds[9]) : _VERBOSE;

            if (mapa_prob) {
                data = colorbrewer.RdBu[11];
            }
            else {
                if (_range_limits_red.length > 0 && _range_limits_blue.length > 0 && _resultado_grid == 0) { // ambos, primero negativos

                    _range_limits_total = _range_limits_blue.reverse().concat(_range_limits_red);

                    data = d3.merge([rojos.reverse(), azules]);
                }
                else if (_range_limits_red.length > 0 && _range_limits_blue.length > 0 && _resultado_grid == 1) { // ambos, primero positivos

                    _range_limits_total = _range_limits_blue.reverse().concat(_range_limits_red.reverse());

                    data = d3.merge([rojos.reverse(), azules]);
                }

                else if (_range_limits_red.length > 0 && _range_limits_blue.length == 0) {// solo positivos
                    _VERBOSE ? console.log("solo positivos") : _VERBOSE;

                    _range_limits_total = d3.merge([[{right_limit: 0, left_limit: 0}], _range_limits_red]);
                    data = d3.merge([rojos, ["#ffffff"]]);
                    // data = colorbrewer.Reds[9];
                }

                else if (_range_limits_red.length == 0 && _range_limits_blue.length > 0) {// solo negativos
                    _VERBOSE ? console.log("solo negativos") : _VERBOSE;

                    _range_limits_total = d3.merge([_range_limits_blue.reverse(), [{right_limit: 0, left_limit: 0}]]);
                    data = d3.merge([["#ffffff"], azules]);
                    ;
                }

            }

            console.log(_range_limits_total);
            console.log(data);

            gradient_data = [];

            $.each(data, function(index, item) {

                // console.log(parseFloat((index)/data.length*100).toFixed(2)+"%");
                gradient_data.push({offset: parseFloat((index) / data.length * 100).toFixed(2) + "%", color: item});

            });

            // console.log(gradient_data);

            var w = 70, h = 300;

            var key = d3.select("#escala_color").append("svg")
                    .attr("width", w)
                    .attr("height", h);
            // .attr("transform", "translate(10,0)");

            var legend = key.append("defs")
                    .append("svg:linearGradient")
                    .attr("id", "gradient")
                    .attr("x1", "100%")
                    .attr("y1", "0%")
                    .attr("x2", "100%")
                    .attr("y2", "100%")
                    .attr("spreadMethod", "pad");

            legend.selectAll("stop")
                    .data(gradient_data)
                    .enter().append("stop")
                    .attr("offset", function(d) {
                        return d.offset;
                    })
                    .attr("stop-color", function(d) {
                        return d.color;
                    });

            key.append("rect")
                    .attr("width", w - 50)
                    .attr("height", h - 100)
                    .style("fill", "url(#gradient)")
                    .attr("transform", "translate(50,10)");

            if (mapa_prob) {
                var y = d3.scale.linear().range([h - 100, 0]).domain([1, 100]);
            }
            else {
                var y = d3.scale.ordinal().rangeBands([h - 100, 0], .5);
                y.domain(_range_limits_total.map(function(d) {
                    // console.log(d.right_limit);
                    return parseFloat(d.left_limit).toFixed(2);
                }));
            }

            var yAxis = d3.svg.axis()
                    .scale(y)
                    .orient("left");

            var text_legend = mapa_prob ? "Porcentaje probabilidad" : "Score";

            key.append("g")
                    .attr("class", "y axis")
                    .attr("transform", "translate(49,10)")
                    .call(yAxis)
                    .append("text")
                    .attr("transform", "rotate(-90)")
                    .attr("y", -45)
                    .attr("dy", ".71em")
                    .style("text-anchor", "end")
                    .text(text_legend);

        }

        /**
         * Éste método obtiene los límites de las particiones realizadas por deciles que serán utilizadas para la asignación de color a las celdas.
         *
         * @function _cargaPaletaColor
         * @private
         * @memberof! map_module
         *
         * @param {array} arg_1 - Array de valores positivos o negativos resultado del análisis de nicho ecológico.
         * @param {array} arg_2 - Array de valores positivos o negativos resultado del análisis de nicho ecológico.
         * @param {boolean} first_pos - Bandera para indicar que array de valores esta en el primer parámetro de la función, true para positivos y false para negativos
         * @param {boolean} mapa_prob - Bandera para indicar si el análisis de nicho ecológico se hizo con probabilidad
         */
        function _getDividedChunks(arg_1, arg_2, first_pos, mapa_prob) {

            _VERBOSE ? console.log("_getDividedChunks") : _VERBOSE;

            _range_limits_red = [];
            _range_limits_blue = [];
            _range_limits_total = [];

            // _VERBOSE ? console.log(arg_1.length) : _VERBOSE;
            decil_length_1 = arg_1.length / NUM_SECTIONS;
            module_1 = arg_1.length % NUM_SECTIONS;

            // si esta completo
            arg_result_1 = chunkify(arg_1, NUM_SECTIONS, true);
            // arg_result_1 = _chunks(arg_1, decil_length_1, NUM_SECTIONS, module_1);

            if (mapa_prob) {
                [arg_result_1, []];
            }

            first = true;
            r_limit = 0.0;

            // getting boundaries of each decil
            $.each(arg_result_1, function(index, decil) {

                // se estan quedando elementos fuera, ya que no estan tocamdo el cero
                if (first) {

                    first = false;

                    if (first_pos) {

                        max_decil = d3.max(decil.map(function(d) {
                            return parseFloat(d.tscore)
                        }));
                        r_limit = d3.min(decil.map(function(d) {
                            return parseFloat(d.tscore)
                        }));

                        _range_limits_red.push({right_limit: max_decil, left_limit: r_limit});

                    }
                    else {
                        max_decil = 0;
                        r_limit = d3.min(decil.map(function(d) {
                            return parseFloat(d.tscore)
                        }));

                        _range_limits_blue.push({right_limit: max_decil, left_limit: r_limit});
                    }

                }
                else if (index == NUM_SECTIONS - 1) {

                    if (first_pos) {
                        max_decil = d3.max(decil.map(function(d) {
                            return parseFloat(d.tscore)
                        }));
                        r_limit = 0;

                        _range_limits_red.push({right_limit: max_decil, left_limit: r_limit});

                    }
                    else {
                        max_decil = r_limit;
                        r_limit = d3.min(decil.map(function(d) {
                            return parseFloat(d.tscore)
                        }));

                        _range_limits_blue.push({right_limit: max_decil, left_limit: r_limit});
                    }

                }
                else {

                    // avoiding spaces between decil boundaries
                    max_decil = r_limit;
                    r_limit = d3.min(decil.map(function(d) {
                        return parseFloat(d.tscore)
                    }));

                    if (first_pos) {
                        _range_limits_red.push({right_limit: max_decil, left_limit: r_limit});
                    }
                    else {
                        _range_limits_blue.push({right_limit: max_decil, left_limit: r_limit});
                    }

                }

            });

            var range = first_pos ? _range_limits_red : _range_limits_blue;

            if (arg_2.length > 0) {

                // clustering items of the second array
                arg_result_2 = [];

                $.each(range, function(i, r_item) {

                    if (first_pos) {

                        rlimit = r_item.right_limit * -1;
                        llimit = r_item.left_limit * -1;

                        _range_limits_blue.push({right_limit: llimit, left_limit: rlimit});

                    }
                    else {

                        rlimit = r_item.right_limit * -1;
                        llimit = r_item.left_limit * -1;

                        _range_limits_red.push({right_limit: llimit, left_limit: rlimit});

                    }

                });

                var rangeinverse = first_pos ? _range_limits_blue : _range_limits_red;
                if (first_pos) {
                    rangeinverse.reverse();
                }
                _VERBOSE ? console.log(rangeinverse) : _VERBOSE;

                $.each(rangeinverse, function(i, limits) {

                    var decil_item = [];

                    $.each(arg_2, function(j, item) {

                        // score = Math.abs(item.tscore);
                        score = parseFloat(item.tscore);
                        llimit = limits.left_limit;
                        rlimit = limits.right_limit;

                        // when the negative values (blues scale) is sent as arg_1, the boundaries must be changed
                        if (first_pos) {

                            if (score >= llimit && score < rlimit) {
                                decil_item.push(item);
                            }
                        }
                        else {

                            if (score >= llimit && score < rlimit) {
                                decil_item.push(item);
                            }
                        }

                    });

                    arg_result_2.push(decil_item);

                });

                return [arg_result_1, arg_result_2];

            }

            return [arg_result_1, []];

        }

        /**
         * Éste método secciona el array de celdas y score realcionado en deciles
         *
         * @function _cargaPaletaColor
         * @private
         * @memberof! map_module
         *
         * @param {array} a - Array de celdas y score relacionado
         * @param {integer} n - Núemro de particiones
         * @param {boolean} balanced - Bandera para indicar si las particiones serán balanceadas
         */
        function chunkify(a, n, balanced) {

            console.log("chunkify");
            console.log(a);

            if (n < 2)
                return [a];

            var len = a.length,
                    out = [],
                    i = 0,
                    size;
            console.log("len: " + len);
            console.log("n: " + n);

            if (len % n === 0) {

                console.log("caso uno");

                size = Math.floor(len / n);

                console.log("size: " + size);

                while (i < len) {
                    out.push(a.slice(i, i += size));
                }
            }

            else if (balanced) {
                console.log("caso dos");

                while (i < len) {
                    size = Math.ceil((len - i) / n--);
                    out.push(a.slice(i, i += size));
                }
            }

            else {

                console.log("caso tres");

                n--;
                size = Math.floor(len / n);
                if (len % size === 0)
                    size--;
                while (i < size * n) {
                    out.push(a.slice(i, i += size));
                }
                out.push(a.slice(size * n));

            }

            return out;
        }

        /**
         * Éste método inicializa la configuración del mapa
         *
         * @function startMap
         * @public
         * @memberof! map_module
         *
         * @param {array} language_module - Módulo lenguaje
         * @param {integer} tipo_modulo - Tipo de módulo donde será creado el mapa, nicho o comunidad ecológica
         * @param {boolean} histogram_module - Módulo histograma
         */
        function startMap(language_module, tipo_modulo, histogram_module) {
            _VERBOSE ? console.log("startMap") : _VERBOSE;
            _mapConfigure(language_module, tipo_modulo, histogram_module);
        }

        return{
            map: map,
            busca_especie: busca_especie,
            busca_especie_filtros: busca_especie_filtros,
            set_specieTarget: set_specieTarget,
            get_specieTarget: get_specieTarget,
            get_allowedPoints: get_allowedPoints,
            get_discardedPoints: get_discardedPoints,
            get_discardedPointsFilter: get_discardedPointsFilter,
            get_discardedCellFilter: get_discardedCellFilter,
            get_allowedCells: get_allowedCells,
            createDecilColor: createDecilColor,
            setDisplayModule: setDisplayModule,
            showPopUp: showPopUp,
            get_layerControl: get_layerControl,
            addMapLayer: addMapLayer,
            removeMapLayer: removeMapLayer,
            addMapControl: addMapControl,
            removeMapControl: removeMapControl,
            getMap: getMap,
            colorizeFeatures: colorizeFeatures,
            colorizeFeaturesNet: colorizeFeaturesNet,
            startMap: startMap
        }

    });