/*
 * Copyright 2019 - All rights reserved.
 * Rodrigo Tapia-McClung
 *
 * August-November 2019
 */

/* globals omnivore, Promise, chroma, makeBaseMap, makeIndicatorGraph, getData, getDataInSelection */
/* exported indicators. userFiles, userDates, timeParse, layerControl, updateCharts */

let timeParse,
    timeFormat,
    timeDimensionControl,
    userFiles = [],
    monthArray = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
    dateArray = [],
    dateMin,
    dateMax,
    minUserDate,
    maxUserDate,
    userDates,
    map,
    overlay,
    glmap,
    osmLayer, cartoLightLayer, cartoDarkLayer,
    timeLayer,
    layerControl,
    scale;

// define empty objects and indicators
let maxIndicators = {},
    minIndicators = {},
    indicators = ["area", "perimeter", "costa", "df"],
    indicatorsNames = ["Área", "Perímetro", "Desarrollo de la línea de costa", "Dimensión fractal"],
    indicatorsUnits = ["m\u00B2", "m", "", ""],
    indicatorsxAxisFormat = [".2s", ".2s", ".2f", ".2f"],
    indicatorVars = {},
    cols = [];

// Add options to combo box, and set their chart variables
// chart containers must already exist in index.php
indicators.forEach( (indicator, index) => {
    // colnames for queries
    cols.push(`min(${indicator}) as min${indicator}`);
    cols.push(`max(${indicator}) as max${indicator}`);
    // initialize min and max objects to hold values for each indicator and add select options
    maxIndicators[indicator] = 0;
    minIndicators[indicator] = 1e30;
    $("#indicatorSelect").append(`<option value="${indicator}"> ${indicatorsNames[index]}</option>`);
    // chart variables
    indicatorVars[indicator] = {
        "chart": `${indicator}Chart`,
        "chartaData": `${indicator}Data`,
        "container": `#${indicator}-graph`
    };
});

indicatorVars.area.explanation = "Muestra la suma del &aacute;rea contenida en los pol&iacute;gonos dentro de la selecci&oacute;n o en toda la regi&oacute;n de estudio.";
indicatorVars.perimeter.explanation = "Muestra la suma del per&iacute;metro de los pol&iacute;gonos dentro de la selecci&oacute;n o en toda la regi&oacute;n de estudio.";
indicatorVars.costa.explanation = "Relaciona el per&iacute;metro de un cuerpo de agua o longitud de l&iacute;nea de costa con el per&iacute;metro de un c&iacute;rculo de igual  \
    &aacute;rea (A) que el cuerpo. El valor m&iacute;nimo es 1, el cual corresponde a un c&iacute;rculo perfecto, mientras que el valor m&aacute;ximo no tiene l&iacute;mite. \
    En varios estudios se han obtenido valores mayores de 20, como en algunos lagos en Finlandia. Los valores cercanos a 1 son indicativos de formas circulares, mientras que \
    los valores mayores de 3 son referencia de cuerpos de agua con contornos de formas alargadas. \
    <br/><br/> \
    Se calcula como \
    <p class=\"equation\">DI = <span class=\"frac\"><sup>P</sup><span>&frasl;</span><sub>2&radic;<span style=\"text-decoration: overline;\">&pi; A</span></sub></span>,</p> \
    donde P es el per&iacute;metro y A es el &aacute;rea.";
indicatorVars.df.explanation = "La geometr&iacute;a fractal se usa para hacer referencia a objetos demasiado irregulares y que tienen la propiedad de ser invariantes ante \
    cambios de escala. En el caso de la hidrolog&iacute;a,  este &iacute;ndice nos refiere la sinuosidad del contorno de un cuerpo de agua. La dimensi&oacute;n presenta valores \
    reales no negativos entre 1 y 2. Los valores de este &iacute;ndice aumentan conforme el contorno del cuerpo de agua es más sinuoso. El an&aacute;lisis fractal se ha utilizado \
    con &eacute;xito para medir y caracterizar rasgos lineales irregulares como las costas. \
    <br/></br> \
    Se calcula como \
    <p class=\"equation\">DF = <span class=\"frac\"><sup>2 ln(P/4)</sup><span>&frasl;</span><sub>ln(A)</sub></span>,</p> \
    donde P es el per&iacute;metro y A es el &aacute;rea.";

let currentTiles = {},
    allTiles = {};

d3.json("https://unpkg.com/d3-time-format@2.1.1/locale/es-MX.json").then(locale => {

    d3.timeFormatDefaultLocale(locale);

    timeParse = d3.timeParse("%B_%Y");
    timeFormat = d3.timeFormat("%B_%Y");

    setupTimeDimensionControl();
    setupDates()
        .then(dates => populateDates(dates))
        .then(userData => setupMap(userData))
        .then(map => populateMap(map));
});

const sortInitialDateAscending = (a, b) => {
    // Dates will be cast to numbers automagically:
    return a - b;
}

// query available dates on DB
const setupDates = () => {
    return new Promise( resolve => {
        const baseUrl = new URL(`/data`, window.location.href).href;
        let layersQuery = `${baseUrl}/list_layers`;
        d3.json(layersQuery).then(layers => {
            layers.forEach(layer => {
                dateArray.push(timeParse(layer.f_table_name)); // convert filenames to dates
            })

            dateArray = dateArray.sort(sortInitialDateAscending); // order dates
            dateMin = d3.min(dateArray);
            dateMax = d3.max(dateArray);
            //userFiles = dateArray.map( month => timeFormat(month)); // order table names by date

            const dates = {min:dateMin, max:dateMax, dates:dateArray};
            resolve(dates);
        });
    });
}

const populateDates = (dates) => { // fill out date pickers with available dates
    return new Promise( resolve => {
        $.datepicker.regional["es"] = {
            closeText: "Cerrar",
            prevText: "&#x3c;Ant",
            nextText: "Sig&#x3e;",
            currentText: "Hoy",
            monthNames: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
                "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
            ],
            monthNamesShort: ["Ene", "Feb", "Mar", "Abr", "May", "Jun",
                "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"
            ],
            dayNames: ["Domingo", "Lunes", "Martes", "Mi&eacute;rcoles", "Jueves", "Viernes", "S&aacute;bado"],
            dayNamesShort: ["Dom", "Lun", "Mar", "Mi&eacute;", "Juv", "Vie", "S&aacute;b"],
            dayNamesMin: ["Do", "Lu", "Ma", "Mi", "Ju", "Vi", "S&aacute;"],
            weekHeader: "Sm",
            dateFormat: "yy/mm/dd",
            firstDay: 1,
            isRTL: false,
            showMonthAfterYear: false,
            yearSuffix: ""
        }
        $.datepicker.setDefaults($.datepicker.regional["es"]);

        // month pickers
        $("#date-initial").datepicker({
            minDate: dates.min,
            maxDate: dates.max,
            defaultDate: dates.min,
            changeMonth: true,
            changeYear: true,
            showButtonPanel: true,
            dateFormat: "M yy",
            onClose: function() {
                let month = $("#ui-datepicker-div .ui-datepicker-month :selected").val();
                let year = $("#ui-datepicker-div .ui-datepicker-year :selected").val();
                minUserDate = new Date(year, month, 1); // initial date
                $(this).datepicker("setDate", minUserDate);
                $("#date-final").datepicker("option", {
                    "minDate": minUserDate,
                    disabled: false
                });
                // hack to avoid needing to change date twice in second datepicker
                setTimeout(() => { $("#date-final").datepicker("show") }, 10);
            },
            beforeShow: el => {
                $("#ui-datepicker-div").toggleClass("hide-calendar", $(el).is("[data-calendar=\"false\"]"));
            }
        });

        $("#date-final").datepicker({
            maxDate: dates.max,
            defaultDate: dates.max,
            changeMonth: true,
            changeYear: true,
            showButtonPanel: true,
            disabled: true,
            dateFormat: "M yy",
            onClose: function() {
                let month = $("#ui-datepicker-div .ui-datepicker-month :selected").val();
                let year = $("#ui-datepicker-div .ui-datepicker-year :selected").val();
                maxUserDate = new Date(year, month, 1); // final date
                $(this).datepicker("setDate", maxUserDate);
                // use .setUTCHours(6,0,0) to adjust DST offset fror some months
                let startUserDate = new Date(minUserDate.setUTCHours(6,0,0));
                let endUserDate = new Date(maxUserDate.setUTCHours(6,0,0));

                // pass new timeinterval to timeDimension player
                userDates = L.TimeDimension.Util.explodeTimeRange(startUserDate, endUserDate, "P1M");
                userFiles = userDates.map( month => timeFormat(month)); // order table names by date

                // change date selectors from main screen to map
                $("#date-initial").detach().appendTo("#datePickers");
                $("#date-final").detach().appendTo("#datePickers");
                $("#datePickers").css("display", "block");

                // hide initial screen and show map
                $("#startHeader").remove();
                $("#initial-backdrop").remove();
                $("#mainContainer")[0].style.setProperty("display", "flex", "important")
                $("#mexmap").show();

                // When closing final-date, either setup or update map
                if (!map) {
                    resolve({min: startUserDate, max: endUserDate});
                } else {
                    // FIXME: no need to pass data here?
                    updateMap({map: map, min: startUserDate, max: endUserDate});
                }
            },
            beforeShow: (el, inst) => {
                inst.input.datepicker("refresh");
                $("#ui-datepicker-div").toggleClass("hide-calendar", $(el).is("[data-calendar=\"false\"]"));
            }
        });
    })
}

const setupMap = (dates) => {
    return new Promise( resolve => {

        // make body tag to have style height: 100%
        $("body").css("height", "100%");
        osmLayer = L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
            maxZoom: 19,
            attribution: "&copy; <a href=\"http://osm.org/copyright\" target=\"_blank\">OpenStreetMap</a> contributors"
        });
        cartoLightLayer = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png", {
            maxZoom: 19,
            attribution: "Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
        });
        cartoDarkLayer = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png", {
            maxZoom: 19,
            attribution: "Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
        });

        //let bounds = cuencaBufferMask.getBounds();
        let  southWest = L.latLng(15.08, -94.42),
            northEast = L.latLng(18.82, -91.52),
            bounds = L.latLngBounds(southWest, northEast);

        map = L.map("mexmap", {
            //center: [17.22, -92.28],
            minZoom: 7, //8
            zoom: 7, //6
            attributionControl: false,
            timeDimension: true,
            timeDimensionOptions: {
                times: userDates,
                currentTime: dates.min
            },
            maxBounds: bounds
        }).setView([16.96, -92.97], 8);

        cartoDarkLayer.addTo(map);

        // set sync map on right
        let osmLayerOverlay = L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
                maxZoom: 19,
                attribution: "&copy; <a href=\"http://osm.org/copyright\" target=\"_blank\">OpenStreetMap</a> contributors"
            }),
            cartoLightLayerOverlay = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png", {
                maxZoom: 19,
                attribution: "Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
            }),
            cartoDarkLayerOverlay = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png", {
                maxZoom: 19,
                attribution: "Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
            });

        overlay = L.map("overlaydiv", {
            zoomControl: false,
            inertia: false,
            keyboard: false,
            //dragging: false,
            scrollWheelZoom: true,
            attributionControl: false,
            zoomAnimation: true
        }).setView([16.96, -92.97], 8);

        cartoDarkLayerOverlay.addTo(overlay);
        L.control.attribution({position: "bottomright"}).addTo(overlay);

        // on base layer on main map, change base layer on overlay map
        map.on("baselayerchange", event => {
            overlay.eachLayer(l => overlay.removeLayer(l));
            if (event.name == "Carto Dark") {
                cartoDarkLayerOverlay.addTo(overlay);
            } else if (event.name == "Carto Light") {
                cartoLightLayerOverlay.addTo(overlay);
            } else if (event.name == "OpenStreetMap") {
                osmLayerOverlay.addTo(overlay);
            }
        });

        const offsetGlobal = (center, zoom, refMap, tgtMap) => {
            var refC = refMap.getContainer();
            var tgtC = tgtMap.getContainer();
            var pt = refMap.project(center, zoom)
                .subtract([refC.offsetLeft, refC.offsetTop])
                .subtract(refMap.getSize().divideBy(2))
                .add([tgtC.offsetLeft, tgtC.offsetTop])
                .add(tgtMap.getSize().divideBy(2));
            return refMap.unproject(pt, zoom);
        }

        map.sync(overlay, {offsetFn: offsetGlobal});

        //console.log(userFiles);
        // query db to get min/max values per month and indicator and store them in an object
        queryFiles().then( minmax => {
            minmax.map( minmaxMonth => {
                indicators.forEach( (indicator) => {
                    minIndicators[indicator] = Math.min(minIndicators[indicator], minmaxMonth[`min${indicator}`]);
                    maxIndicators[indicator] = Math.max(maxIndicators[indicator], minmaxMonth[`max${indicator}`]);
                });
            });
            resolve({"map": map, "minIndicators": minIndicators, "maxIndicators": maxIndicators});
        });
    });
}

const flatten = arrays => {
    return [].concat(arrays);
}

const queryFiles = () => {
    return Promise.all(userFiles.map(getMinMax))
        // the result is an array of arrays, so we'll flatten them here
        .then(flatten);
}

const getMinMax = table => {
    return new Promise( resolve => {
        const baseUrl = new URL(`/data`, window.location.href).href;
        let minmaxQuery = `${baseUrl}/query/${table}?columns=${cols.join(", ")}`;
        d3.json(minmaxQuery).then( minmax => {
            resolve(minmax[0]);
        });
    });
}

const populateMap = async (mapData) => {

    const chartData = await getData();

    // Define charts with reusable components
    indicators.map( async (indicator, index) => {
        // indicatorVars[indicator].chart gives areaChart, perimeterChart, etc.
        // First, make all charts with same general options
        indicatorVars[indicator].chart = makeIndicatorGraph()
            .width(Math.floor($(indicatorVars[indicator].container)[0].offsetParent.offsetWidth * 0.95))
            .height(Math.floor($(indicatorVars[indicator].container)[0].offsetParent.offsetHeight * 0.95))
            .lineVariables([`${indicator}0`]) // d.area is used to draw stuff on y axis
            .displayName("date") // d.date is used to draw stuff on x axis
            .lineAxisLabel("(m)")
            .id(indicator)
            .xAxisFormat(d3.timeFormat("%b '%y")) // label x axis as mm 'yy
            .yAxisFormat(d3.format(indicatorsxAxisFormat[index])) // yAxis label in SI or other
            // Then, specify some parameters for each chart
            .tooltipUnits(indicatorsUnits[index])
            .tooltipFormat(d3.format(indicatorsxAxisFormat[index])) // tooltip format in SI or other
            .title(indicatorsUnits[index] == "" ? indicatorsNames[index] :
                `${indicatorsNames[index]} (${indicatorsUnits[index]})`);

        // Finally, set chart data with async function calling stuff from DB
        indicatorVars[indicator].chart.data( chartData[indicator]);
        // create chart with passed options
        d3.select(indicatorVars[indicator].container)
            .call(indicatorVars[indicator].chart);
        // Reload chart data and force chart update
        indicatorVars[indicator].chartData = indicatorVars[indicator].chart.data(); // get chart data
        indicatorVars[indicator].chart.data(indicatorVars[indicator].chartData); // set chart data

        // Highlight plot title according to selected option
        let option = $("#indicatorSelect").val(); // option selected from dropdrown
        d3.select(indicatorVars[option].container).select("svg text.title").classed("active", true);
    });


    let map = mapData.map,
        minIndicators = mapData.minIndicators,
        maxIndicators = mapData.maxIndicators;

    let cuencaLayer = L.geoJson(null, {
        style: {
            stroke: true,
            weight: 5,
            color: "grey",
            opacity: .35,
            fillOpacity: 0.05
        },
        interactive: false
    });
    let cuencaBufferMask = omnivore.topojson("data/buffer_cuenca.topojson", null, cuencaLayer);
    cuencaBufferMask.addTo(map);
    //map.createPane("wb-Tiles");
    //map.getPane("wb-Tiles").style.zIndex = 450;

    // create mvt layers
    userFiles.forEach( f => {
        f = mapboxLayer(f);
    });

    glmap = L.mapboxGL({
        accessToken: "no-token",
        style: {
            "version": 8,
            "sources": {},
            "layers": []
        }
    }).addTo(map);

    // after mapboxGL map is ready with styles do this:
    glmap.getMapboxMap().on("style.load", () => {
        userFiles.forEach(monthYear => {
            glmap.getMapboxMap().addLayer(currentTiles[monthYear]);
        });

        Object.keys(allTiles).forEach(layer => {
            if (layer == userFiles[0]) {
                glmap.getMapboxMap().setPaintProperty(layer, "fill-opacity", 0.7)
            }
        });

        timeDimensionControl.addTo(map);
        // Pass dummy geojson layer to timeDimension in order to register and sync
        timeLayer = L.timeDimension.layer.Tile(L.geoJSON(), {
            updateTimeDimension: true,
            updateTimeDimensionMode: "replace",
            waitForReady: true,
            duration: "P1M"
        });
        timeLayer.addTo(map);

        // style currentTiles
        let option = $("#indicatorSelect").val(); // option selected from dropdrown
        styleTiles(option, minIndicators, maxIndicators)
            .then(legend.addTo(map)); // add legend control -> it updates

        let baseLayers = {
            "Carto Dark": cartoDarkLayer,
            "Carto Light": cartoLightLayer,
            "OpenStreetMap": osmLayer
        };
        var overlays = {
            "<span id=\"cuencaOverlay\">Agua en la cuenca del Grijalva</span>": timeLayer
        };

        layerControl = L.control.layers(baseLayers, overlays).addTo(map);
        makeBaseMap(); // basemap.js

        // fix for leaflet-mapbox-gl v. 0.0.11 that adds map's to tile pane:
        // get children of map tile pane, create array from it and iterate 
        // setting their z-index
        let glmapChildren = map.getPanes().tilePane.children,
            children = Array.from(glmapChildren);
        children.forEach( c => c.style.zIndex = "inherit" );
    });
}

const updateMap = (mapData) => {
    //console.log(userFiles);

    // clear tiles
    currentTiles = {};
    //retrieve or create tiles for current dates
    userFiles.forEach( monthYear => {
        if (Object.keys(allTiles).includes(monthYear)) {
            currentTiles[monthYear] = allTiles[monthYear]; // recover tile if it has already been created
            //currentJSONs[monthYear] = allJSONs[monthYear]; // recover json if it has already been created
            return; // if file has already been processed, exit
        } else { // if file cannot be found in allTiles, then add 1 to the number of files to process
            let newTile = mapboxLayer(monthYear);
            glmap.getMapboxMap().addLayer(newTile);
            if (monthYear == userFiles[0]) {
                glmap.getMapboxMap().setPaintProperty(monthYear, "fill-opacity", 0.7)
            }
        }
    });

    // update timeDimension times
    timeLayer._timeDimension.setAvailableTimes(userDates, "replace");
    timeLayer._timeDimension.setCurrentTime(mapData.min);

    // clear minmax indicators objects
    maxIndicators = {},
    minIndicators = {};
    indicators.forEach( (indicator) => {
        maxIndicators[indicator] = 0;
        minIndicators[indicator] = 1e30;
    });

    // query db for new minmax values then style tiles
    new Promise( resolve => {
        queryFiles().then( minmax => {
            minmax.map( minmaxMonth => {
                indicators.forEach( (indicator) => {
                    minIndicators[indicator] = Math.min(minIndicators[indicator], minmaxMonth[`min${indicator}`]);
                    maxIndicators[indicator] = Math.max(maxIndicators[indicator], minmaxMonth[`max${indicator}`]);
                });
            });
            resolve({"map": map, "minIndicators": minIndicators, "maxIndicators": maxIndicators});
        })
    }).then( values => { // once we have new minmax values, style all tiles
        let option = $("#indicatorSelect").val(), // option selected from dropdrown
            min = values.minIndicators,
            max = values.maxIndicators;
        styleTiles(option, min, max)
            .then(legend.addTo(map)); // add legend control -> it updates
    });
    // Update charts
    updateCharts();
}

const updateCharts = async () => {

    $(".loader").css("display", "block");
    // TODO: add mask while fetching async data
    // if user has drawn polygons, update chart with data inside selection
    if (drawnItems.toGeoJSON().features.length != 0) {
        let allData = await getDataInSelection();
        indicators.map( async indicator => {
            indicatorVars[indicator].chart.data( summarizeData(allData, indicator) );
        });
    } else {
        // otherwise use all data
        let allData = await getData();
        indicators.map( async indicator => {
            indicatorVars[indicator].chart.data( allData[indicator]);
        });
    }
    $(".loader").hide("fade", 750);
}

// define MVT layer for given month table and all indicators
const mapboxLayer = (monthYear) => {
    const baseUrl = new URL(`/data`, window.location.href).href;
    let pbfLayer = {
        id: monthYear,
        source: {
            type: "vector",
            tiles: [`${baseUrl}/${monthYear}/mvt/{z}/{x}/{y}?geom_column=geom&columns=${indicators.join()}`],
            maxzoom: 19,
            minzoom: 6
        },
        "source-layer": monthYear,
        type: "fill",
        "paint": {
            "fill-opacity": 0,
            /*"fill-color": [
                "interpolate",
                ["linear"],
                ["get", "df"],
                1, "rgba(255, 0, 0, 0.5)", // red
                1.3, "rgba(0, 255, 0, 0.5)", // green
            ]/*,
            "fill-outline-color": [
                "interpolate",
                ["linear"],
                ["get", "df"],
                1, "rgba(255, 0, 0, 0.6)", // red
                1.3, "rgba(0, 255, 0, 0.6)", // green
            ]*/
        }
    }
    currentTiles[monthYear] = pbfLayer;
    allTiles[monthYear] = pbfLayer;
    return pbfLayer;
}

const setupTimeDimensionControl = () => {
    L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({
        _getDisplayDateFormat: date => {
            let d = new Date(date);
            let year = d.getFullYear().toString();
            let month = d.getUTCMonth();
            return `${monthArray[month]} ${year}`;
        }
    });

    timeDimensionControl = new L.Control.TimeDimensionCustom({
        loopButton: true,
        /*minSpeed: 1,
        maxSpeed: 5,*/
        timeSteps: 1,
        playReverseButton: true,
        //limitSliders: true,
        playerOptions: {
            //buffer: 5,
            //minBufferReady: 5,
            transitionTime: 125,
            loop: true
        },
        timeZones: ["Local"]
    });
}

L.TimeDimension.Layer.Tile = L.TimeDimension.Layer.extend({
    _setAvailableTimes: function () {
        if (this.options.times) {
            return L.TimeDimension.Util.parseTimesExpression(this.options.times);
        } else if (this.options.timeInterval) {
            let tiArray = L.TimeDimension.Util.parseTimeInterval(this.options.timeInterval);
            let period = this.options.period || "P1D";
            let validTimeRange = this.options.validTimeRange || undefined;
            //alert("times");
            return L.TimeDimension.Util.explodeTimeRange(tiArray[0], tiArray[1], period, validTimeRange);
        } else {
            return [];
        }
    },

    onAdd: function (map) {
        // Don't call prototype so this_update() does not get called twice
        //L.TimeDimension.Layer.prototype.onAdd.call(this, map);
        this._map = map;
        if (!this._timeDimension && map.timeDimension) {
            this._timeDimension = map.timeDimension;
        }
        this._timeDimension.on("timeloading", this._onNewTimeLoading, this);
        this._timeDimension.on("timeload", this._update, this);
        this._timeDimension.registerSyncedLayer(this);
        map.addLayer(this._baseLayer);
        this._update();
    },

    onRemove: function (map) {
        this._timeDimension.unregisterSyncedLayer(this);
        this._timeDimension.off("timeloading", this._onNewTimeLoading, this);
        this._timeDimension.off("timeload", this._update, this);
        //this._baseLayer.getContainer().style.display = "none";
        Object.keys(allTiles).forEach(layer => { // hide all tiles
            glmap.getMapboxMap().setPaintProperty(layer, "fill-opacity", 0);
        });
        //this.eachLayer(map.removeLayer, map);
        //this._map = null;
    },

    isReady: function (time) {
        // to be implemented for each type of layer
        return true;
    },

    _update: function () {
        if (!this._baseLayer || !this._map) {
            return;
        }

        var time = this._timeDimension.getCurrentTime();

        // get data for time
        let d = new Date(time),
            year = d.getFullYear().toString(),
            m = d.getUTCMonth(),
            month = monthArray[m].toLowerCase(),
            monthYear = `${month}_${year}`;

        // Update title
        let title = $("#title");
        title.html(`<h2>Cobertura de agua en la cuenca del r&iacute;o Grijalva en ${month} de ${year}</h2>`);

        // Update graphs only on timeload event
        indicators.forEach( indicator => {
            indicatorVars[indicator].chartData = indicatorVars[indicator].chart.data(); // get chart data
            indicatorVars[indicator].chart.data(indicatorVars[indicator].chartData); // set chart data
        });

        //console.time("process");
        //console.log("data for", monthYear);
        //console.log(currentTiles)
        Object.keys(allTiles).forEach(layer => {
            if (layer !== monthYear) { // hide all other months
                glmap.getMapboxMap().setPaintProperty(layer, "fill-opacity", 0);
            } else { // except current one
                glmap.getMapboxMap().setPaintProperty(layer, "fill-opacity", 0.7);
            }
        });
        //console.timeEnd("process");
    }
});

L.timeDimension.layer.Tile = (layer, options) => {
    return new L.TimeDimension.Layer.Tile(layer, options);
};


// When selecting indicator from dropdown, style tiles.
$("#indicatorSelect").on("change", function() {
    // style currentTiles
    let option = this.value; // option selected from dropdrown
    styleTiles(option, minIndicators, maxIndicators)
        .then(legend.addTo(map)); // add legend control -> it updates
    // FIXME: re-adding control updates its contents... why?
    // Highlight plot title according to selected option
    indicators.forEach( indicator => {
        d3.select(indicatorVars[indicator].container).select("svg text.title").classed("active", indicator === option ? true : false);
    });
});

const styleTiles = (option, minIndicators, maxIndicators) => {
    // define color scale domain based on min-max values for selected indicator
    let domain = [minIndicators[option], maxIndicators[option]];
    //console.log(domain)
    scale = chroma.scale("PuBu").padding([0.5, 0]).domain(domain).classes(5);
    Object.keys(currentTiles).forEach(layer => {
        let color = [
            "interpolate",
            ["linear"],
            ["get", option],
            minIndicators[option], scale(minIndicators[option]).hex(),
            maxIndicators[option], scale(maxIndicators[option]).hex()
        ];
        glmap.getMapboxMap().setPaintProperty(layer, "fill-color", color);
    });
    return Promise.resolve(scale);
}

let legend = L.control({
    position: "bottomright"
});

legend.onAdd = () => {
    let div = L.DomUtil.create("div", "info legend leaflet-bar");
    let option = $("#indicatorSelect").val();
    let optionIndex = indicators.indexOf(option);
    let legendText = indicatorsUnits[optionIndex] == "" ? indicatorsNames[optionIndex] :
        `${indicatorsNames[optionIndex]} (${indicatorsUnits[optionIndex]})`;
    var html = `<h6>${legendText}</h6><ul>`;
    let classes = scale.classes();
    classes.forEach( (c, idx, array) => {
        if (idx != array.length - 1) {
            html += `<li><i style="background: ${scale(c).hex()}"></i>${d3.format(",.4~s")(classes[idx])} - ${d3.format(",.4~s")(classes[idx+1])}</li>`;
        }
    });
    html += "</ul>";
    div.innerHTML = html;
    return div;
};