<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>PGServer Info</title>
    <style>
        
        .medium {width: 300px; height: auto;}
        #mapcontainer {
            display: flex;
            flex-wrap: wrap;
        }
        #map {display: inline-block; width: 500px; height: 500px; border: 1 px solid lightblue;}
        #legendcontainer {display: flex;}
        #legend {min-width: 250px;}
        #featurecontainer {
            display: flex;
            flex-direction: column;
            justify-content: space-between;
        }
        #featureinfo {
            display: inline-block;
            max-width: 400px;
        }
        #graphs { 
            display: flex; 
            flex-wrap: wrap;
        }
        #graphs div {
            margin: 5px;
        }
        #colorschemes {
            display: flex;
            align-content: center;
        }
        #colorschemes div {
            padding: 2px;
            margin: 2px;
            cursor: pointer;
        }
        #colorschemes div:hover {
            background-color: lightgray;
        }
        #colorschemes div.selected {
            background-color: darkgray;
        }
        #colorschemes svg {
            display: block;
        }
        #colorschemes rect {
            stroke: #333;
            stroke-width: 0.5;
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
    <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.2.0/mapbox-gl.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.2.0/mapbox-gl.css' rel='stylesheet' />
    <link href='loader.css' rel='stylesheet' />
    <script src="./colorbrewer.js"></script>
    <script src="./ticks.js"></script>
    <script>
    "use strict";

    let map = null;
    let globalStats = null;
    let selectedColorScheme = 0;
    
    function init() {
        const urlParams = new URLSearchParams(window.location.search);
        const fullTableName = urlParams.get('table');
        const attrName = urlParams.get("column");
        const attrType = urlParams.get("columntype");
        const geomType = urlParams.get('geomtype');
        const geomColumn = urlParams.get('geom_column');
        document.querySelector('#tablename').innerHTML = `Layer: ${fullTableName}<br>Geometry: ${geomColumn}`;
        document.querySelector('#columnname').innerHTML = `Attribute: ${attrName} (${attrType})`;
        document.querySelector('#back').innerHTML = `<a href="tableinfo.html?table=${fullTableName}&geom_column=${geomColumn}">Back to layer info</a>`;
        document.querySelectorAll('[name="colorscheme"]').forEach(radio=>radio.onchange=updateLegendColorSchemes);
        document.querySelectorAll('[name="classtype"]').forEach(radio=>radio.onchange=applyLegendToMap);
        document.querySelector('#classcount').onchange = updateLegendColorSchemes;
        document.querySelector('input[name="colorsreversed"]').onchange = updateLegendColorSchemes

        initMap();
        
        fetch(`data/${fullTableName}/colstats/${attrName}?geom_column=${geomColumn}`).then(response=>{
            const loadingElement = document.querySelector('#loader');
            loadingElement.innerHTML = "";
            if (!response.ok) {
                try {
                    response.json(json=>loadingElement.innerHtml = json.error);
                } catch(err) {
                    loadingElement.innerHTML = err;
                }
                return;
            }
            response.json()
            .then(json=>{
                if (json.datatype === 'timestamptz' || json.datatype === 'date') {
                    json.percentiles = json.percentiles.map(percentile=>{
                        percentile.from = percentile.from?new Date(percentile.from):null;
                        percentile.to = percentile.to?new Date(percentile.to):null;
                        return percentile;
                    })
                    json.values = json.values.map(value=>{
                        value.value = value.value?new Date(value.value):null;
                        return value;
                    })
                }
                graphStats(json)
                globalStats = json;
            })
            .catch(err=>
                loadingElement.innerHTML = `Failed to parse response, message: ${err.message}`
            );
        })
    }

    function initMap()
    {
        const urlParams = new URLSearchParams(window.location.search);
        const fullTableName = urlParams.get('table');
        const geomType = urlParams.get('geomtype');
        const geomColumn = urlParams.get('geom_column');
        const attrName = urlParams.get("column");
        
        const mapDefinition = {
            container: 'map',
            "style": {
                "version": 8,
                "name": "DefaultBaseStyle",
                "id": "defaultbasestyle",
                "glyphs": `https://free.tilehosting.com/fonts/{fontstack}/{range}.pbf?key=`,
                "sources": {
                    "osmgray": {
                        "type":"raster",
                        "tileSize":256,
                        "tiles":[
                          "https://tiles.edugis.nl/mapproxy/osm/tiles/osmgrayscale_EPSG900913/{z}/{x}/{y}.png?origin=nw"
                        ],
                        "attribution":"&copy; <a href=\"https://www.openstreetmap.org/about\" target=\"copyright\">OpenStreetMap contributors</a>",
                        "maxzoom":19
                    }
                },
                "layers": [
                    {
                        "id": "osmgray",
                        "type": "raster",
                        "source": "osmgray"
                    }
                ]
            }
        }
        const bboxll = urlParams.get('bboxll');
        if (bboxll) {
            mapDefinition.bounds = JSON.parse(bboxll);
        }
        map = new mapboxgl.Map(mapDefinition);
        map.on('load', () => {
            let layerType, paint;
            switch (geomType) {
                case 'MULTIPOLYGON':
                case 'POLYGON':
                    layerType = 'fill';
                    paint = {
                        "fill-color": "red",
                        "fill-outline-color": "white",
                        "fill-opacity": 0.8
                    }
                    break;
                case 'MULTILINESTRING':
                case 'LINESTRING':
                    layerType = 'line';
                    paint = {
                        "line-color": "red",
                        "line-width": 1
                    }
                    break;
                case 'MULTIPOINT':
                case 'POINT': 
                    layerType = "circle";
                    paint = {
                        "circle-radius": 5,
                        "circle-color": "red",
                        "circle-stroke-color": "white",
                        "circle-stroke-width" : 1
                    }
                    break;
                default: 
                    break;
            }
            if (!layerType) {
                document.querySelector("#layerjson").innerHTML = `Field geom of type: '${geomType}' not supported<br>Supported types: (MULTI-) POINT/LINE/POLYGON<p>`
            } else {
                const baseUrl = new URL(`/data`, window.location.href).href;
                const layer = {
                    "id": "attrlayer",
                    "type": layerType,
                    "source": {
                        "type": "vector",
                        "tiles": [`${baseUrl}/${fullTableName}/mvt/{z}/{x}/{y}?geom_column=${geomColumn}&columns=${attrName}&include_nulls=0`],
                    },
                    "source-layer": fullTableName,
                    "paint": paint,
                    //"filter": ['has', attrName]
                }
                map.addLayer(layer);
                addLegendLine('red', fullTableName, layerType);
                updateLayerJsonDisplay();
                
            }
        })
        map.on('mousemove', function (e) {
            var features = map.queryRenderedFeatures(e.point).map(function(feature){ return {layer: {id: feature.layer.id, type: feature.layer.type}, properties:(feature.properties)};});
            document.querySelector('#featureinfo').innerHTML = JSON.stringify(features.map(feature=>feature.properties), null, 2);
        });
    }

    function updateLayerJsonDisplay() {
        const layerjson = map.getStyle().layers.filter(l=>l.id==='attrlayer')[0];
        layerjson.source = map.getSource(layerjson.source).serialize();
        document.querySelector("#layerjson").innerHTML = `<pre>${JSON.stringify(layerjson, null, 2)}</pre>`;
    }

    function prepareGraphColors(classType) {
        globalStats.graphColors = [];
        globalStats.classType = classType;
    }

    function addGraphColors(color, upperValue) {
        globalStats.graphColors.push({color: color, upperValue: upperValue});
    }

    let graphNoData = null;
    let graphValues = null;
    let graphMostFrequent = null;

    function destroyGraphs() {
        if (graphNoData) {
            graphNoData.destroy();
        }
        if (graphValues) {
            graphValues.destroy();
        }
        if (graphMostFrequent) {
            graphMostFrequent.destroy();
        }
    }

    function graphStats(stats) {
        destroyGraphs();
        const nullValues = stats.values.filter(value=>value.value === null).reduce((result, value)=>result+value.count,0);
        const rowCount = stats.percentiles.reduce((result, percentile)=>result + percentile.count, 0);
        graphNoData = new Chart(document.querySelector('#graphnodata canvas'), {
            type: 'doughnut',
            data: {
                labels: ['no data','data',],
                datasets: [{
                    backgroundColor: ['lightgray', 'red'],
                    borderColor: 'white',
                    borderWidth: 0,
                    data: [nullValues, rowCount]
                }]
                
            }
        });
        if (stats.percentiles.length && typeof(stats.percentiles[0].from) !== 'string') {
            graphValues = new Chart(document.querySelector('#graphvalues canvas'), {
                type: 'line',
                data: {
                    labels: stats.percentiles.map((percentile, index, arr)=>Math.round((index/(arr.length - 1)) * 100)),
                    datasets: [{
                        backgroundColor: stats.graphColors? stats.percentiles.map((percentile)=>{
                            let color = stats.classType === 'mostfrequent' ? 
                                stats.graphColors.find(graphColor=>percentile.to == graphColor.upperValue) : 
                                    stats.graphColors.find(graphColor=>percentile.to < graphColor.upperValue);
                            if (!color) {
                                color = stats.graphColors[stats.graphColors.length - 1];
                            }
                            return color.color;
                        }):'red',
                        //borderColor: 'lighgray',
                        data: stats.percentiles.map((percentile,index,arr)=>(index===arr.length-1)?percentile.to:percentile.from),
                        fill: false
                    }]
                },
                options : {
                    title: {
                        display: false,
                        text : 'some title'
                    },
                    legend: {
                        display: false
                    },
                    scales: {
                        yAxes: [
                            {
                                scaleLabel: {
                                    display: true,
                                    labelString: stats.column
                                }
                            }
                        ],
                        xAxes: [
                            {
                                scaleLabel: {
                                    display: true,
                                    labelString: 'percentage of rows',
                                    padding: 0
                                }
                            }
                        ]
                    }
                }
            })
        }
        const valuesSummary = stats.values.filter(value=>value.value !== null).slice(0,10);
        const valuesSummeryRowCount = valuesSummary.reduce((result, value)=>result+value.count,0);
        if (rowCount > valuesSummeryRowCount) {
            valuesSummary.push({
                value:"other", 
                count: rowCount - valuesSummeryRowCount
            })
        }
        graphMostFrequent = new Chart(document.querySelector('#graphmostfrequent canvas'), {
            type: "horizontalBar",
            data: {
                labels: valuesSummary.map(value=>value.value),
                datasets: [
                    {
                        backgroundColor: stats.graphColors? valuesSummary.map((value)=>{
                            let color = stats.graphColors.find(graphColor=>value.value === graphColor.upperValue) 
                            if (!color) {
                                color = stats.graphColors[stats.graphColors.length - 1];
                            }
                            return color.color;
                        }):'red',
                        data: valuesSummary.map(value=>value.count)
                    }
                ]
                
            },
            options : {
                legend: {
                    display: false,
                },
                scales: {
                    xAxes: [
                        {
                            ticks: {
                                min: 0
                            }
                        }
                    ]
                }
            }
        })
    }

    function addLegendLine(color, label, type) {
        if (!type) {
            type = 'fill';
        }
        const legend = document.querySelector('#legend');
        let svg;
        switch (type) {
            case 'fill':
                svg = `<svg width="30" height="15">
                            <rect width="30" height="15" style="fill:${color};fill-opacity:1;stroke-width:1;stroke:#444"></rect>
                        </svg>`
                break;
            case 'line': 
                svg = `<svg width="30" height="15">
                        <line x1="0" y1="15" x2="30" y2="0" style="stroke:${color};stroke-width:${color.width};" />
                      </svg>`
                break;
            case 'circle':
                svg = `<svg width="12" height="12">
          <circle cx="6" cy="6" r="5" style="fill:${color};fill-opacity:1 stroke-width:1;stroke:white" />
        </svg>` 

        }
        const legendLine = document.createElement('div');
        legendLine.innerHTML = `<div> <span>${svg}</span> <span>${label}<span></div>`
        legend.appendChild(legendLine);
    }

    function prepareLegend() {
        if (globalStats) {
            document.querySelector('#legend').innerHTML = '';
            return true;
        }
        let messageElem = document.querySelector('#legend.message');
        if (messageElem) {
            return false;
        }
        messageElem = document.createElement('div');
        messageElem.classList.add('message');
        messageElem.innerHTML = "waiting for stats, retry later...";
        return false;
    }

    // schemeTypes 'div', 'qual', 'seq'
    // for diverging, qualitative and sequential legends
    function getColorSchemes(numClasses, schemeType, reversed) {
        for (; numClasses > 2; numClasses--) {
            let result = colorbrewer.filter(scheme=>scheme.type===schemeType && scheme.sets.length > numClasses - 3)
                .map(scheme=>{
                    let result = Object.assign({}, scheme.sets[numClasses - 3]);
                    result.name = scheme.name;
                    result.type = scheme.type;
                    return result;
                });
            if (result.length) {
                if (reversed) {
                    result.forEach(scheme=>scheme.colors = scheme.colors.map(c=>c).reverse())
                }
                return result;
            }
        }
        if (numClasses === 2) {
            let result = colorbrewer.filter(scheme=>scheme.type===schemeType)
                .map(scheme=>{
                    const result = {colors: [scheme.sets[0].colors[0],scheme.sets[0].colors[2]]}
                    result.name = scheme.name;
                    result.type = scheme.type;
                    return result;
                });
            if (reversed) {
                result.forEach(scheme=>scheme.colors = scheme.colors.map(c=>c).reverse());
            }
            return result;

        }
        return [{colors:['#ff0000']}];
    }

    function selectColorScheme(schemeIndex)
    {
        let schemeDiv = document.querySelector('#colorschemes');
        schemeDiv.querySelectorAll('div').forEach(div=>div.classList.remove('selected'));
        schemeDiv.querySelectorAll('div').forEach((div,index)=>{if (index === schemeIndex) {div.classList.add('selected')}});
        selectedColorScheme = schemeIndex;
        applyLegendToMap();
    }

    // display the available colorschemes for current scheme type and class number
    function updateLegendColorSchemes() {
        let schemeDiv = document.querySelector('#colorschemes');
        schemeDiv.innerHTML = '';
        let classCount = Number(document.querySelector('#classcount').value);
        let schemeType = document.querySelector('input[name="colorscheme"]:checked').value;
        let reversed = document.querySelector('input[name="colorsreversed"]').checked;
        let schemes = getColorSchemes(classCount, schemeType, reversed);
        classCount = schemes[0].colors.length;
        if (selectedColorScheme > schemes.length - 1) {
            selectedColorScheme = schemes.length - 1;
        }
        schemes.forEach((scheme, schemeIndex) => {
            let ramp = document.createElement('div');
            if (schemeIndex === selectedColorScheme) {
                ramp.classList.add('selected');
                selectedColorScheme = schemeIndex;
            }
            let svg = `<svg width="15" height="${classCount * 15}">${
                scheme.colors.map((color, index)=>`<rect fill="${color}" width="15" height="15" y="${index * 15}"></rect>`).join('\n')
            }</svg>`;
            ramp.innerHTML = svg;
            ramp.onclick = ()=>selectColorScheme(schemeIndex);
            schemeDiv.appendChild(ramp);
        })
        applyLegendToMap();
    }
    
    function applyLegendToMap() {
        let classType = document.querySelector('input[name="classtype"]:checked').value;
        let reversed = document.querySelector('input[name="colorsreversed"]').checked;
        if (prepareLegend()) {
            prepareGraphColors(classType);
            let classCount = Number(document.querySelector('#classcount').value);
            if (classCount === 1) {
                // special case, single classification
            } else {
                // classCount > 1
                let layerType = map.getLayer('attrlayer').type;
                let rowCount = globalStats.percentiles.reduce((result, percentile)=>result + percentile.count, 0);
                let mapboxPaint;
                let schemeType = document.querySelector('input[name="colorscheme"]:checked').value;
                switch(classType) {
                    case 'mostfrequent':
                        let schemes = getColorSchemes(classCount, schemeType, reversed);
                        classCount = schemes[0].colors.length;
                        let classValues = globalStats.values.filter(value=>value.value !== null);
                        let needsSlice = classValues.length > classCount;
                        if (needsSlice) {
                            classValues = classValues.slice(0, classCount - 1);
                            let classValuesRowCount = classValues.reduce((result, value)=>result+value.count,0);
                            classValues.push({
                                value:"other", 
                                count: rowCount - classValuesRowCount
                            })
                        }
                        if (globalStats.datatype === 'numeric') {
                            mapboxPaint = [
                                "match",
                                ["to-number", ["get",`${globalStats.column}`], 0]
                            ];
                        } else {
                            mapboxPaint = [
                                "match",
                                ["get",`${globalStats.column}`]
                            ];
                        }
                        classValues.forEach((value, index) => {
                            addLegendLine(schemes[selectedColorScheme].colors[index], value.value, layerType);
                            addGraphColors(schemes[selectedColorScheme].colors[index], value.value)
                            if (index < classValues.length - 1 || !needsSlice) {
                                mapboxPaint.push(value.value);
                                mapboxPaint.push(schemes[selectedColorScheme].colors[index]);
                            }
                        });
                        mapboxPaint.push(schemes[selectedColorScheme].colors[classValues.length -1]);
                        break;
                    case 'quantile':
                        let percentileBreaks = globalStats.percentiles.reduce((result, percentile)=>{
                            if (result.length === 0) {
                                result.push(Object.assign({}, percentile)); // spread operator not supported by current Edge
                                return result;
                            }
                            if (result[result.length - 1].from === percentile.from) {
                                result[result.length - 1].to = percentile.to;
                                result[result.length - 1].count += percentile.count;
                                return result;
                            }
                            result.push(Object.assign({}, percentile)); 
                            return result;
                        },[]);
                        let seqSchemes = getColorSchemes(classCount, schemeType, reversed);
                        classCount = seqSchemes[selectedColorScheme].colors.length;
                        if (classCount > percentileBreaks.length) {
                            classCount = percentileBreaks.length
                        } else {
                            let totalRowCount = percentileBreaks.reduce((result, percentile)=>result+percentile.count, 0);
                            let rowCountPerClass = totalRowCount / classCount;
                            let sumRowCount = 0;
                            let sumClassCount = 0
                            percentileBreaks = percentileBreaks.reduce((result, percentile)=>{
                                sumRowCount += percentile.count;
                                if (sumRowCount > sumClassCount * rowCountPerClass && result.length < classCount) {
                                    // new class
                                    result.push(percentile);
                                    sumClassCount++;
                                } else {
                                    result[result.length - 1].to = percentile.to;
                                    result[result.length - 1].count += percentile.count;                                    
                                }
                                return result;
                            },[])
                        }
                        mapboxPaint = ["case"]
                        percentileBreaks.forEach((brk, index, arr)=>{
                            addLegendLine(seqSchemes[selectedColorScheme].colors[index], `${brk.from} - ${brk.to}`, layerType);
                            addGraphColors(seqSchemes[selectedColorScheme].colors[index], brk.to);
                            if (globalStats.datatype === 'numeric') {
                                mapboxPaint.push(["<",["to-number", ["get", `${globalStats.column}`], 0],brk.to],seqSchemes[selectedColorScheme].colors[index]);
                            } else {
                                mapboxPaint.push(["<",["get", `${globalStats.column}`],brk.to],seqSchemes[selectedColorScheme].colors[index]);
                            }
                        })
                        mapboxPaint.push(seqSchemes[selectedColorScheme].colors[classCount - 1])
                        break;
                    case 'interval':
                        let min = globalStats.percentiles[0].from;
                        let max = globalStats.percentiles[globalStats.percentiles.length - 1].to;
                        if (typeof min === "number") {
                            let intervalSchemes = getColorSchemes(classCount, schemeType, reversed);
                            classCount = intervalSchemes[0].colors.length;
                            let classTicks = getIntervalClassTicks(min, max, classCount);
                            mapboxPaint = ["case"];
                            classTicks.classes.forEach((classStart, index)=>{
                                let legendFrom = classStart;
                                let legendTo = (index === classCount - 1 ? classTicks.max : classTicks.classes[index + 1]);
                                if (globalStats.datatype === 'int4' || globalStats.datatype === 'int8') {
                                    legendFrom = Math.floor(legendFrom);
                                    legendTo = Math.floor(legendTo);
                                }
                                addLegendLine(intervalSchemes[selectedColorScheme].colors[index], `${(legendFrom)} - ${legendTo}`, layerType);
                                addGraphColors(intervalSchemes[selectedColorScheme].colors[index], legendTo)
                                if (globalStats.datatype === 'numeric') {
                                    // convert javscript string to number
                                    mapboxPaint.push(["<", ["to-number",["get", globalStats.column], 0],legendTo], intervalSchemes[selectedColorScheme].colors[index]);
                                } else {
                                    mapboxPaint.push(["<", ["get", globalStats.column],legendTo], intervalSchemes[selectedColorScheme].colors[index]);
                                }                                
                            })
                            mapboxPaint.push(intervalSchemes[selectedColorScheme].colors[classCount - 1])
                        }
                        break;
                }
                if (mapboxPaint) {
                    let colorprop = `${layerType}-color`;
                    map.setPaintProperty('attrlayer', colorprop, mapboxPaint);
                    updateLayerJsonDisplay();
                    graphStats(globalStats);
                }
            }
            
            let nullValues = globalStats.values.filter(value=>value.value === null).reduce((result, value)=>result+value.count,0);
            let checkButtonNullValues = document.querySelector('#hidenulls')
            if (nullValues) {
                checkButtonNullValues.removeAttribute('disabled');
            } else {
                checkButtonNullValues.setAttribute('disabled', '')
            }
        }
    }
    
    </script>

</head>
<body onload="init()">
    <h1>Attribute information</h1>
    <h2 id="tablename"></h2>
    <h2 id="columnname"></h2>

    <div id="loader">
        <h2>Loading statistics...</h2>
        <div class="loader">
            <div class="dot"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div>
            <div class="dot"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div>
        </div>
    </div>
    <div id="graphs">
        <div id="graphnodata" class="canvascontainer medium"><canvas></canvas></div>
        <div id="graphvalues" class="canvascontainer medium"><canvas></canvas></div>
        <div id="graphmostfrequent" class="canvascontainer medium"><canvas></canvas></div>
    </div>
    <div id="mapcontainer">
        <div id="map"></div>
        <div id="featurecontainer">
            <div id="legendcontainer">
                <div id="legend"></div>
                <div id="legendformcontainer">
                    <select id="classcount" name="classcount">
                        <option value="1" selected>1</option>
                        <option value="2">2</option>
                        <option value="3">3</option>
                        <option value="4">4</option>
                        <option value="5">5</option>
                        <option value="6">6</option>
                        <option value="7">7</option>
                        <option value="8">8</option>
                        <option value="9">9</option>
                        <option value="10">10</option>
                        <option value="11">11</option>
                        <option value="12">12</option>
                    </select><label for="numclasses">number of classes</label><br>
                    Classification methods:<br>
                    <input type="radio" name="classtype" id="interval" value="interval"><label for="interval">equal interval</label>
                    <input type="radio" name="classtype" id="quantile" value="quantile" checked><label for="quantile">quantile</label>
                    <input type="radio" name="classtype" id="mostfrequent" value="mostfrequent"><label for="mostfrequent">most frequent</label><br>
                    Color schemes:<br>
                    <input type="radio" name="colorscheme" id="sequential" value="seq"><label for="sequential">sequential</label>
                    <input type="radio" name="colorscheme" id="diverting" value="div"><label for="diverting">diverting</label>
                    <input type="radio" name="colorscheme" id="qualitative" value="qual" checked><label for="qualitative">qualitative</label><br>
                    <input type="checkbox" name="colorsreversed" id="colorsreversed"><label for="colorsreversed">reverse color order</label><br>
                    <input type="checkbox" id="hidenulls" name="hidenulls" checked><label for="hidenulls">Hide no-data</label><br>
                    <div id="colorschemes"></div>
                </div>
            </div>
            <div id="featureinfo"></div>
        </div>
    </div>
    <div id="layerjson"></div>
    <div id="back"></div>
</body>
</html>