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

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

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"],
    indicators = ["ocupadosDesde", "ocupadosHacia", "ocupadosEntre", "poicDesde", "poicHacia", "poicEntre"],
    indicatorsNames = ["Viajes de ocupados desde centros de mercado", "Viajes de ocupados hacia centros de mercado",
        "Viajes de ocupados entre zonas", "Viajes de personas en OIC desde centros de mercado",
        "Viajes de personas en OIC hacia centros de mercado", "Viajes de personas en OIC entre zonas"
    ],
    indicatorsShortNames = ["Ocupados desde <br/> centros de mercado", "Ocupados hacia <br/> centros de mercado",
        "Ocupados entre zonas", "OIC desde centros <br/> de mercado",
        "OIC hacia centros <br/> de mercado", "OIC entre zonas"
    ],
    //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`
    };
});

let intervals = {
    "ocupadosDesde": {
        "classes": ["Menos de 1,000", "1,001 - 2,000", "2,001 - 6,000", "6,001 - 7,200"],
        "values": [1000, 2000, 6000, 7200],
        "colors": ["#00c5ff", "#008fdc", "#005ce6", "#4c0073"],
        "thickness": [0.5, 1.5, 2, 10],
        "animThickness": [0.5, 1.5, 4, 15]
    },
    "ocupadosHacia": {
        "classes": ["Menos de 1,000", "1,001 - 2,000", "2,001 - 6,000", "6,001 - 32,000", "32,001 - 53,300"],
        "values": [1000, 2000, 6000, 32000, 53300],
        "colors": ["#ffbee8", "#ff73df", "#ff00c5", "#ad027d", "#80006b"],
        "thickness": [0.5, 1.5, 2, 4, 10],
        "animThickness": [0.5, 1.5, 4, 10, 15]
    },
    "ocupadosEntre": {
        "classes": ["Menos de 1,000", "1,001 - 2,000", "2,001 - 3,500", "3,501 - 8,000"],
        "values": [1000, 2000, 3500, 8000],
        "colors": ["#70a800", "#ffaa00", "#e64c00", "#a80000"],
        "thickness": [0.5, 1.5, 2, 4],
        "animThickness": [0.5, 1.5, 4, 10]
    },
    "poicDesde": {
        "classes": ["Menos de 150", "151 - 500", "501 - 1,000", "1,001 - 1,500"],
        "values": [150, 500, 1000, 1500],
        "colors": ["#00c5ff", "#008fe6", "#005ce6", "#002673"],
        "thickness": [0.5, 1.5, 2, 4],
        "animThickness": [0.5, 1.5, 4, 10]
    },
    "poicHacia": {
        "classes": ["Menos de 200", "201 - 400", "401 - 600", "601 - 3,000", "3,001 - 10,700"],
        "values": [200, 400, 600, 3000, 10700],
        "colors": ["#ffbee8", "#ff73df", "#ff00c5", "#ad027d", "#80006b"],
        "thickness": [0.5, 1.5, 2, 4, 10],
        "animThickness": [0.5, 1.5, 4, 10, 15]
    },
    "poicEntre": {
        "classes": ["150 - 500", "501 - 700", "701 - 950", "951 - 1,200"],
        "values": [500, 700, 950, 1200],
        "colors": ["#70a800", "#ffaa00", "#e64c00", "#a80000"],
        "thickness": [0.5, 1.5, 2, 4],
        "animThickness": [0.5, 1.5, 4, 10]
    }
}

/*
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();
    // FIX: Need to remove setup and populate dates and only setup and populate map
    /*setupDates()
        .then(dates => populateDates(dates))
        .then(userData => setupMap(userData))
        .then(map => populateMap(map));*/

    //$("#showMap").on("click", () => {
    // hide initial screen and show map

    // TODO: don't need outer call to d3.json, do I? Move this to the bottom after functions definitions
    //TODO: remove unused functions
    $("#startHeader").remove();
    $("#initial-backdrop").remove();
    $("#mainContainer")[0].style.setProperty("display", "flex", "important")
    $("#mexmap").show();

    setupMap()
        .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(19.87, -102.87),
            northEast = L.latLng(23.02, -99.31),
            bounds = L.latLngBounds(southWest, northEast);

        map = L.map("mexmap", {
            //center: [17.22, -92.28],
            minZoom: 8,
            zoom: 6,
            attributionControl: false,
            /*timeDimension: true,
            timeDimensionOptions: {
                times: userDates,
                currentTime: dates.min
            },*/
            maxBounds: bounds
        }).setView([21.15, -100.94], 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
        // FIX: comment out to avoid DB calls
        /*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 });
        });*/
        // FIX: resolve with fake values
        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 odClick = (e) => {
    e.target.clearAllPathSelections();
    let origins = "";
    // select and draw flows from origins
    if (e.sharedOriginFeatures.length) {
        e.target.selectFeaturesForPathDisplay(e.sharedOriginFeatures, "SELECTION_NEW");
        /*e.sharedOriginFeatures.forEach( origin => {
        /    origins += `${origin.properties.muni_origen} &rarr; ${origin.properties.muni_destino}: ${origin.properties.viajes} viajes <br>`;
        });*/
    }
    // select and add flows toward those origins, i.e., for dots that are both origins and destinations
    let dests = e.target.originAndDestinationGeoJsonPoints.features.filter(f => f.geometry.coordinates[0] === e.latlng.lng && f.geometry.coordinates[1] === e.latlng.lat);
    e.target.selectFeaturesForPathDisplay(dests, "SELECTION_ADD");
    dests.forEach(dest => {
        //origins += `${dest.properties.muni_origen} &rarr; ${dest.properties.muni_destino}: ${dest.properties.viajes} viajes <br>`;
        // style viajes thousands with #,###
        origins += `<tr><td>${dest.properties.muni_origen}</td><td>${dest.properties.muni_destino}</td><td>${dest.properties.viajes.toLocaleString()}</td></tr>`;
    });
    $("#odCard").hide();
    $("#odTableRow").removeClass("align-self-center");
    $("#tblViajes").show();
    $("#tblViajes tbody").html(origins);

    if (e.sharedDestinationFeatures.length) {
        //e.target.selectFeaturesForPathDisplay(e.sharedDestinationFeatures, "SELECTION_ADD");
    }

    // if dots are only destinations, add paths when clicked on
    /*if (e.sharedDestinationFeatures.length) {
        e.target.selectFeaturesForPathDisplay(e.sharedDestinationFeatures, "SELECTION_NEW");
    }*/

    // TODO: trigger chart update to only draw selected origins and destinations?
}

const populateMap = async(mapData) => {

    // FIX: comment out to avoid DB calls
    //const chartData = await getData();
    // fake null data to avoid DB requests
    chartData = [];
    indicators.map(indicator => {
        chartData[indicator] = [];
        chartData[indicator].push({ name: `${indicator}0`, values: [{ "date": new Date("2016-01-01T06:00:00.000Z"), "value": 0 }] });
    })

    // 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;

    //map.createPane("wb-Tiles");
    //map.getPane("wb-Tiles").style.zIndex = 450;

    // FIX: comment out to avoid DB calls
    // 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", () => {
        // FIX: comment out to avoid DB calls
        /*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);*/

        // FIX: comment out to avoid DB calls
        // 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
            //.then( flowLayers => { });
        legend.addTo(map);

        // 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);

    // FIX: comment out to avoid DB calls
    // 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));
            //  indicatorVars[indicator].cd_chart.data(summarizeData(allData, indicator));
        });
    } else {
        // otherwise use all data
        let allData = await getData();
        indicators.map(async indicator => {
            indicatorVars[indicator].chart.data(allData[indicator]);
            // indicatorVars[indicator].cd_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
            // indicatorVars[indicator].cd_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

    flowMapsArray.forEach(layer => {
        if (layer.options.customLayerId === option) {
            map.addLayer(layer);
            layer.on('click', odClick);
            /*layer.getLayers().forEach(l => {
                console.log(l.feature.properties._isSelectedForPathDisplay)
            });*/

            // TODO: Recover each layer state and populate table according to that
            // layer._layers[i].feature.properties._isSelectedforPathDisplay
        } else {
            layer.off("click", odClick);
            map.removeLayer(layer);
        }

        $("#odTableRow").addClass("align-self-center");
        $("#odCard").show();
        $("#tblViajes").hide();
        $("#tblViajes tbody").html("");

    });

    // on select, update chart data
    let odChart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "topChart")
    odChart.data = odData[option];

    let chordChart = am4core.registry.baseSprites.find(c => c.htmlContainer.id === "bottomLeftChart")
    chordChart.data = odData[option];
    chordChart.data = chordChart.data.filter(function(e) { return e.flowCount > 0 });
    //console.log(amchart2.data.filter(function(e) { return e.flowCount > 0 }))

    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 = function() {
    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]})`;

    //title
    let title = L.DomUtil.create("h6", "");
    title.innerHTML = `${indicatorsShortNames[optionIndex]}`;
    div.appendChild(title);

    // list
    let list = L.DomUtil.create("ul", "");
    div.appendChild(list);

    //var html = `<h6>${indicatorsShortNames[optionIndex]}</h6><ul>`;
    //let domain = 
    //scale = chroma.scale("PuBu").padding([0.5, 0]).domain(domain).classes(5);

    // data

    let classes = intervals[option].classes;
    //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>`;
        }
    });*/

    // create stuff for each type of flow
    classes.forEach((c, idx) => {
        // create list element
        let item = L.DomUtil.create('li');
        // create canvas element with arc of appropriate thickness ad color
        let canvas = L.DomUtil.create('canvas');

        let originProp = L.DomUtil.testProp(['transformOrigin', 'WebkitTransformOrigin', 'msTransformOrigin']);
        canvas.style[originProp] = '50% 50%';
        canvas.width = 18;
        canvas.height = 18;

        var ctx = canvas.getContext("2d");
        ctx.beginPath();
        ctx.arc(18, 18, 15, 1.5 * Math.PI, Math.PI, true);
        ctx.lineWidth = intervals[option].thickness[idx];
        ctx.lineCap = 'round';
        // line color
        ctx.strokeStyle = intervals[option].colors[idx];;
        ctx.stroke();

        // append canvas to list element
        item.appendChild(canvas);

        // create legend item text
        let span = L.DomUtil.create("span", "legendItem");
        span.innerText = c;

        // append text to list item
        item.appendChild(span)
            // lastly, append item to list
        list.appendChild(item);
    });

    return div;
};