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
    }

});