Home Reference Source

src/Gisplay/Maps/Map.js

import { BGMapWrapper } from './BGMapWrapper';
import { Aesthetic } from '../Aesthetic';

/**
 * This class contains the Map class which represents the current map.
 * Each map has a group of functions available. There is only one map yet(maybe there will be two if we want to compare two).
 */
export class Map {

    constructor(type, geometry, options) {
        console.log("Map constructor called -> super()")
    }

    program() {
        this._webgl.program = this._webgl.gl.createProgram();
        this._webgl.heatmapProgram = [];
        this._webgl.heatmapProgram[0] = this._webgl.gl.createProgram();
        this._webgl.heatmapProgram[1] = this._webgl.gl.createProgram();

        const source_code = this.generateShaders();

        const vertex_shader = this.shader(this._webgl.gl.VERTEX_SHADER, source_code.vertex, this._webgl);
        const fragment_shader = this.shader(this._webgl.gl.FRAGMENT_SHADER, source_code.fragment, this._webgl);

        this._webgl.gl.attachShader(this._webgl.program, vertex_shader);
        this._webgl.gl.attachShader(this._webgl.program, fragment_shader);

        this._webgl.gl.linkProgram(this._webgl.program);
        this._webgl.gl.useProgram(this._webgl.program);
    }


    shader(type, source_code, _webgl) { //Added HERE by Rui
        var shader = _webgl.gl.createShader(type);

        _webgl.gl.shaderSource(shader, source_code);
        _webgl.gl.compileShader(shader);

        console.log("shader " + (type.valueOf() == 35633 ? "vertex" : "fragment") + ": " + _webgl.gl.getShaderInfoLog(shader));

        return shader;
    }

    generateShaders() { //Added HERE by Rui @TODO Refactor to use ES6 `` instead of +=
        //general

        let vertexSourceCode = " attribute vec4 vertexCoord; ";
        vertexSourceCode += "\n	attribute float aPointSize; ";
        vertexSourceCode += "\n	uniform mat4 projection; ";
        vertexSourceCode += "\n	attribute float a_opacity; ";
        vertexSourceCode += "\n	varying float v_opacity; ";
        //vertexSourceCode+= "\n	varying vec4 u_color; " ; //delete
        vertexSourceCode += "\n	void main() {";
        vertexSourceCode += "\n		gl_Position = (projection * vertexCoord); ";
        vertexSourceCode += "\n		gl_PointSize = aPointSize; v_opacity = a_opacity; ";
        vertexSourceCode += "\n}";

        let fragmentSourceCode = "precision mediump float;";
        fragmentSourceCode += "\n		uniform vec4 u_color;";//uniform
        fragmentSourceCode += "\n		varying float v_opacity; ";
        fragmentSourceCode += "\n 		uniform float isPoint;";
        fragmentSourceCode += "\n		void main(){";
        fragmentSourceCode += "\n			float border = 0.5;";
        fragmentSourceCode += "\n			float radius = 0.5;";
        fragmentSourceCode += "\n			float centerDist = length(gl_PointCoord - 0.5);";
        fragmentSourceCode += "\n			float alpha;";
        fragmentSourceCode += "\n			if (u_color[3] == -1.0){";   //unnecessary??
        fragmentSourceCode += "\n				alpha =  v_opacity * step(centerDist, radius);";//unnecessary??
        fragmentSourceCode += "\n			}";//unnecessary??
        fragmentSourceCode += "\n			else{";//unnecessary??
        fragmentSourceCode += "\n				alpha =  u_color[3] * step(centerDist, radius);";
        fragmentSourceCode += "\n			}";//unnecessary??
        fragmentSourceCode += "\n			if(isPoint == 1.0 ){";
        fragmentSourceCode += "\n			if (alpha < 0.1) discard;";
        fragmentSourceCode += "\n				gl_FragColor = vec4(u_color[0], u_color[1], u_color[2], alpha);}";
        fragmentSourceCode += "\n 			else";
        fragmentSourceCode += "\n				gl_FragColor = vec4(u_color[0], u_color[1], u_color[2], u_color[3]);";
        fragmentSourceCode += "\n		}";

        return { vertex: vertexSourceCode, fragment: fragmentSourceCode };
    }

    addAesthetic(aes) {
        this.aesthetics.push(aes);
    }

    setAesthetic(id, aes) {
        for (let i = 0; i < aesthetics.length; i++) {
            if (id == aesthetics[i].id) {
                aesthetics[i] = aes;
                break;
            }
        }
    }

    buildLegend() {
        const mapCanvas = document.getElementById(`mapCanvas${this.id}`);
        const legendDiv = document.createElement('div');
        legendDiv.id = `legendDiv${this.id}`;
        legendDiv.style.position = 'absolute';
        legendDiv.style.backgroundColor = 'white';
        //legendDiv.style.height = 200;//(mapCanvas.height / 10);
        legendDiv.style.width = 250;//(mapCanvas.width / 10);
        legendDiv.style.bottom = 20;
        legendDiv.style.right = 0;
        legendDiv.style.borderColor = 'black';
        legendDiv.style.border = 'solid';

        const table = document.createElement('table');
        const thvalue = document.createElement('th');
        const thcolor = document.createElement('th');
        //thvalue.style.width = 125;
        table.style.zIndex = "2000";
        thcolor.style.width = 100;
        table.appendChild(thcolor);
        table.appendChild(thvalue);


        for (const currentaes of this.aesthetics) {
            //if(currentaes._features.length > 0 || currentaes._allFeatures.length > 0){
            const row = document.createElement('tr');
            const value = document.createElement('td');
            const color = document.createElement('td');
            const ptext = document.createElement('p');
            let text;
            if (typeof currentaes.range[0] === 'number')
                text = document.createTextNode(`[${currentaes.range[0]}, ${currentaes.range[1]}[`);
            else
                text = document.createTextNode(currentaes.range[0]);
            ptext.appendChild(text);
            value.appendChild(ptext);

            const colorDiv = document.createElement('div');
            colorDiv.style.position = 'relative';
            const rgbc = `rgba(${currentaes.fillColor[0]},${currentaes.fillColor[1]},${currentaes.fillColor[2]},${currentaes.fillColor[3]})`;
            //console.log(rgbc);
            colorDiv.style['backgroundColor'] = rgbc;
            colorDiv.style.height = 25;//(mapCanvas.height / 10);
            colorDiv.style.width = 80;//(mapCanvas.width / 10);



            color.appendChild(colorDiv);

            row.appendChild(color);
            row.appendChild(value);

            table.appendChild(row);
            //}
        }

        legendDiv.appendChild(table);
        this.map.getContainer().appendChild(legendDiv);
    }

    preProcessData(geojson, numberOf, algorithm, colorscheme) {

        const aesarray = [];
        const values = [];
        const strings = [];
        let breaks;
        let fcolor;
        for (let g = 0; g < geojson.features.length && (this.maxfeatures == undefined || g < this.maxfeatures); g++) {
            if (geojson.features[g].properties[this.attr] != null && typeof geojson.features[g].properties[this.attr] == 'number') {
                values.push(geojson.features[g].properties[this.attr]);
                this.max = Math.max(this.max, geojson.features[g].properties[this.attr]);
                this.min = Math.min(this.min, geojson.features[g].properties[this.attr]);
            }
            else
                if (!strings.includes(geojson.features[g].properties[this.attr]))
                    strings.push(geojson.features[g].properties[this.attr]);
        }
        if (values.length > 0) {//quantitative
            if (this.breaks == undefined) {
                if (numberOf > 1)
                    breaks = this.calcClassBreaks(values, algorithm, numberOf);
                else
                    breaks = [this.min, this.max];
            }
            else {
                breaks = this.breaks;
            }
            if (breaks.length > 2) {
                fcolor = chroma.scale(colorscheme).colors(breaks.length - 1);
                for (var i = 0; i < breaks.length - 1; i++) {
                    var color = chroma(fcolor[i]).rgb();
                    if (i != breaks.length - 2) {
                        var aes = new Aesthetic(i, this.attr, [Math.round(color[0]), Math.round(color[1]), Math.round(color[2]), this.alpha], [0, 0, 0, 1], null, [breaks[i], breaks[i + 1]]);
                    }
                    else {
                        var aes = new Aesthetic(i, this.attr, [Math.round(color[0]), Math.round(color[1]), Math.round(color[2]), this.alpha], [0, 0, 0, 1], null, [breaks[i], breaks[i + 1]]);
                        aes.outer = true;
                    }
                    aesarray.push(aes);
                }
            }
            else {
                color = chroma(colorscheme[0]).rgb();
                var aes = new Aesthetic(i, this.attr, [Math.round(color[0]), Math.round(color[1]), Math.round(color[2]), this.alpha], [0, 0, 0, 1], null, [breaks[0], breaks[1]]);
                aes.outer = true;
                aesarray.push(aes);
            }
        }

        else {//qualitative
            if (strings.length > 0) {
                breaks = strings;
                if (typeof colorscheme === 'string') {//string
                    fcolor = chroma.scale(colorscheme).colors(breaks.length);
                }
                else { //array
                    fcolor = chroma.scale(colorscheme).colors(breaks.length);
                }
                for (var i = 0; i < breaks.length; i++) {
                    var color = chroma(fcolor[i]).rgb();
                    var aes = new Aesthetic(i, this.attr, [Math.round(color[0]), Math.round(color[1]), Math.round(color[2]), 1], [0, 0, 0, 1], null, [strings[i]]);
                    aesarray.push(aes);
                }
            }
        }

        this.aesthetics = aesarray;
        //return aesarray;
    }

    calcClassBreaks(values, algorithm, numberOf) {
        let breaks;
        switch (algorithm) {
            case 'equidistant':
                breaks = chroma.limits(values, 'e', numberOf);
                break;

            case 'quantile':
                breaks = chroma.limits(values, 'q', numberOf);
                break;

            case 'k-means':
                breaks = chroma.limits(values, 'k', numberOf);
                break;

            default:
                breaks = chroma.limits(values, 'q', numberOf);
                break;
        }
        return breaks;
    }

    /**
     * 
     * @param {JSON} geojson 
     */
    processData(geojson) {
        this.loadGeoJSON(geojson);
    }

    loadGeoJSON(geojson) {
        for (let g = 0; g < geojson.features.length && (this.maxfeatures == undefined || g < this.maxfeatures); g++) {
            geojson.features[g].properties['_gisplayid'] = g;
            const geometry = geojson.features[g].geometry;
            const properties = geojson.features[g].properties;
            this.createAndInsertFeature(g, geometry, properties);
        }
        this.buildTrees(geojson);
    }

    createAndInsertFeature(id, geometry, properties) {
        const gl = this._webgl.gl;
        if (this.minuend != undefined && this.subtrahend != undefined && typeof properties[this.minuend] == 'number'
            && properties[this.subtrahend] != undefined && typeof properties[this.subtrahend] == 'number'
            && properties[this.subtrahend] != undefined) {
            properties[this.attr] = properties[this.minuend] - properties[this.subtrahend];
        }

        if (geometry.type == "Polygon" || geometry.type == "MultiPolygon") {
            this.hasPolygons = true;
            const polygons = this.processPolygon({ geometry, properties });

            const currentBorders = [];
            const currentTriangles = [];
            const bufferT = [];
            const bufferB = [];

            for (let j = 0; j < polygons.length; j++) {
                const trianglespolygon = polygons[j].triangles;
                const border = polygons[j].vertex;
                currentTriangles[j] = new Array();
                currentBorders[j] = new Array();
                for (let h = 0; h < trianglespolygon.length; h++) {
                    var pixel = this.latLongToPixelXY(border[trianglespolygon[h] * 2], border[trianglespolygon[h] * 2 + 1]);
                    currentTriangles[j].push(pixel.x, pixel.y);

                    if (h == trianglespolygon.length - 1) {
                        bufferT.push(gl.createBuffer());

                        var vertArray = new Float32Array(currentTriangles[j]);

                        gl.fsize = vertArray.BYTES_PER_ELEMENT;
                        gl.bindBuffer(gl.ARRAY_BUFFER, bufferT[j]);
                        gl.bufferData(gl.ARRAY_BUFFER, vertArray, gl.STATIC_DRAW);

                        bufferT[j].itemSize = 2;
                        bufferT[j].numItems = vertArray.length / 2;
                    }
                }


                for (var y = 0; y < border.length; y += 2) {
                    var pixel = this.latLongToPixelXY(border[y], border[y + 1]);
                    currentBorders[j].push(pixel.x, pixel.y);

                    if (y == border.length - 2) {
                        bufferB.push(gl.createBuffer());

                        var vertArray = new Float32Array(currentBorders[j]);

                        gl.fsize = vertArray.BYTES_PER_ELEMENT;
                        gl.bindBuffer(gl.ARRAY_BUFFER, bufferB[j]);
                        gl.bufferData(gl.ARRAY_BUFFER, vertArray, gl.STATIC_DRAW);

                        bufferB[j].itemSize = 2;
                        bufferB[j].numItems = vertArray.length / 2;
                    }
                }
            }
            //polygon
            this.insertFeature(id, properties, bufferT, bufferB, []);
        }

        else if (geometry.type == "Point" && this.dynamic == true) {
            //dum
            const currentPoints = [];
            currentPoints[0] = new Array();
            var pixel = this.latLongToPixelXY(geometry.coordinates[0], geometry.coordinates[1]);
            currentPoints[0].push(pixel.x, pixel.y);
            const bufferP = [];
            bufferP.push(gl.createBuffer());

            var vertArray = new Float32Array(currentPoints[0]);

            gl.fsize = vertArray.BYTES_PER_ELEMENT;
            gl.bindBuffer(gl.ARRAY_BUFFER, bufferP[0]);
            gl.bufferData(gl.ARRAY_BUFFER, vertArray, gl.STATIC_DRAW);

            bufferP[0].itemSize = 2;
            bufferP[0].numItems = vertArray.length / 2;

            this.insertFeature(id, properties, [], [], bufferP);

            if (this.treepoints == null || this.treepoints == undefined)
                this.treepoints = [];
            this.treepoints.push({ lon: geometry.coordinates[0], lat: geometry.coordinates[1], properties });
        }

        else if (geometry.type == "Point" && this.dynamic == false) {
            //debugger;
            var pixel = this.latLongToPixelXY(geometry.coordinates[0], geometry.coordinates[1]);
            if (this.tempPoints == null || this.tempPoints == undefined) {
                this.tempPoints = new Array();
                for (let a = 0; a < this.aesthetics.length; a++) {
                    this.tempPoints[a] = [];
                }
            }

            const aesarrays = this.fitFeature(properties);
            for (var y = 0; y < aesarrays.length; y++) {
                this.tempPoints[aesarrays[y]].push(pixel.x, pixel.y);
            }

            if (this.treepoints == null)
                this.treepoints = [];
            this.treepoints.push({ lon: geometry.coordinates[0], lat: geometry.coordinates[1], properties });
        }
    }

    processPolygon(polygon) {
        if (polygon.geometry.type == "Polygon") {
            var outsidepolygon = polygon.geometry.coordinates[0];

            var insidepolygons = [];
            for (var k = 1; k < polygon.geometry.coordinates.length; k++) {
                //todo inside polygon
                //insidepolygons.push(polygon.geometry.coordinates[i][k]);
            }
            var tempVerts = new Array();
            for (var out = 0; out < outsidepolygon.length - 1; out++) {
                tempVerts.push(outsidepolygon[out][0], outsidepolygon[out][1]);
                _vertexcount += (outsidepolygon.length + 1) / 2;
                //console.log("lon: " + outsidepolygon[out][0] + " lat: " + outsidepolygon[out][1]);
            }

            var triangles_vert = earcut(tempVerts);
            _tricount += (triangles_vert.length / 3);
            polyarray.push({ triangles: triangles_vert, vertex: tempVerts });
        }

        else if (polygon.geometry.type == "MultiPolygon") {
            var polyarray = [];
            for (let i = 0; i < polygon.geometry.coordinates.length; i++) {

                var outsidepolygon = polygon.geometry.coordinates[i][0];

                var insidepolygons = [];
                for (var k = 1; k < polygon.geometry.coordinates[i].length; k++) {
                    //todo inside polygon
                    insidepolygons.push(polygon.geometry.coordinates[i][k]);
                }
                var tempVerts = new Array();
                _vertexcount += outsidepolygon.length;
                for (var out = 0; out < outsidepolygon.length - 1; out++) {
                    tempVerts.push(outsidepolygon[out][0], outsidepolygon[out][1]);

                    //console.log("lon: " + outsidepolygon[out][0] + " lat: " + outsidepolygon[out][1]);
                }

                var triangles_vert = earcut(tempVerts);
                //var temp = earcut.flatten(polygon.geometry.coordinates[i]);
                //var triangles_vert = earcut(temp.vertices, temp.holes, temp.dimensions);
                _tricount += (triangles_vert.length / 3);
                polyarray.push({ triangles: triangles_vert, vertex: tempVerts });
                //console.log(polyarray);
            }
            return polyarray;
        }
    }

    insertFeature(id, properties, triangles, borders, points) {
        let flag = false;
        for (let i = 0; i < this.aesthetics.length; i++) {
            if (this.aesthetics[i].checkProperty(properties[this.aesthetics[i].getAttr()]) == true) {
                this.aesthetics[i].addFeature(id, properties, triangles, borders, points);
                flag = true;
            }
        }
        if (!flag) {
            //TODO
            //console.log("TODO: feature does not fit into any of the aesthetics defined.\n Value: " + properties[this.attr]);
        }
    }

    latLongToPixelXY(longitude, latitude) {
        const pi_180 = Math.PI / 180.0;
        const pi_4 = Math.PI * 4;
        const sinLatitude = Math.sin(latitude * pi_180);
        const pixelY = (0.5 - Math.log((1 + sinLatitude) / (1 - sinLatitude)) / (pi_4)) * 256;
        const pixelX = ((longitude + 180) / 360) * 256;

        const pixel = { x: pixelX, y: pixelY };
        return pixel;
    }

    fitFeature(properties) {
        const result = [];
        for (let a = 0; a < this.aesthetics.length; a++) {
            if (this.aesthetics[a].checkProperty(properties[this.aesthetics[a].getAttr()]) == true)
                result.push(a);
        }
        return result;
    }

    buildTrees(geojson) {
        const gl = this._webgl.gl;
        if (this.tempPoints != null) {
            for (let t = 0; t < this.tempPoints.length; t++) {
                if (this.tempPoints[t].length > 0) {
                    const bufferP = [];
                    bufferP.push(gl.createBuffer());

                    const vertArray = new Float32Array(this.tempPoints[t]);

                    gl.fsize = vertArray.BYTES_PER_ELEMENT;
                    gl.bindBuffer(gl.ARRAY_BUFFER, bufferP[0]);
                    gl.bufferData(gl.ARRAY_BUFFER, vertArray, gl.STATIC_DRAW);

                    bufferP[0].itemSize = 2;
                    bufferP[0].numItems = vertArray.length / 2;
                    this.insertGroupedFeature(t, [], [], bufferP);
                }
            }
        }
        //console.log(geojson)
        if (this.treepoints != null)
            this.kdtree = new kdTree(this.treepoints, (a, b) => (a.lon - b.lon) ** 2 + (a.lat - b.lat) ** 2, ["lon", "lat", "properties"]);
        if (this.hasPolygons == true)
            this.rtree = new PolygonLookup(geojson);
    }

    insertGroupedFeature(idaes, triangles, borders, points) {
        this.aesthetics[idaes].addGroupedFeature(null, triangles, borders, points);
    }

    createCanvas() {
        const canvas = this.map.createCanvas(this.id);

        //init webgl properties
        this._webgl = {
            gl: null,
            program: null,
            projection: null
        };

        this._webgl.gl = canvas.getContext("webgl");
        this._webgl.projection = new Float32Array(16);
        this._webgl.projection.set([2 / canvas.width, 0, 0, 0, 0, -2 / canvas.height, 0, 0, 0, 0, 0, 0, -1, 1, 0, 1]);

        this._webgl.gl.viewport(0, 0, this.map.getContainer().offsetWidth, this.map.getContainer().offsetHeight);
        this._webgl.gl.disable(this._webgl.gl.DEPTH_TEST);
    }

    getNumberOfFeatures() {
        let count = 0;
        for (let i = 0; i < this.aesthetics.length; i++) {
            count += this.aesthetics[i]._features.length;
        }
        return count;
    }

    scaleProjection(matrix, scaleX, scaleY) {
        // scaling x and y, which is just scaling first two rows of matrix
        matrix[0] *= scaleX;
        matrix[1] *= scaleX;
        matrix[2] *= scaleX;
        matrix[3] *= scaleX;

        matrix[4] *= scaleY;
        matrix[5] *= scaleY;
        matrix[6] *= scaleY;
        matrix[7] *= scaleY;
    }

    translateProjection(matrix, tx, ty) {
        // translation is in last row of matrix
        matrix[12] += matrix[0] * tx + matrix[4] * ty;
        matrix[13] += matrix[1] * tx + matrix[5] * ty;
        matrix[14] += matrix[2] * tx + matrix[6] * ty;
        matrix[15] += matrix[3] * tx + matrix[7] * ty;
    }

    clear() {
        const gl = this._webgl.gl;
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.disable(gl.DEPTH_TEST);
    }

    drawTriangles(aes) {
        const gl = this._webgl.gl;
        if (gl == null) return;
        const matrixProjection = new Float32Array(16);

        //gl.clear(gl.COLOR_BUFFER_BIT);
        //gl.disable(gl.DEPTH_TEST);

        //gl.enable(gl.BLEND);
        //gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);


        const currentZoom = this.map.getZoom();
        const pointSize = Math.max(currentZoom - 5.0, 1.0);

        matrixProjection.set(this._webgl.projection);

        const scale = 2 ** currentZoom;
        this.scaleProjection(matrixProjection, scale, scale);

        const offset = this.latLongToPixelXY(this.map.getLngBound(), this.map.getLatBound());
        this.translateProjection(matrixProjection, -offset.x, -offset.y);

        const projectionLocation = gl.getUniformLocation(this._webgl.program, 'projection');
        gl.uniformMatrix4fv(projectionLocation, false, matrixProjection);

        const vertexSizeLocation = gl.getAttribLocation(this._webgl.program, 'aPointSize');
        gl.vertexAttrib1f(vertexSizeLocation, pointSize);

        const isPointLocation = gl.getUniformLocation(this._webgl.program, 'isPoint');
        gl.uniform1f(isPointLocation, 0.0);



        const vertexCoordLocation = gl.getAttribLocation(this._webgl.program, 'vertexCoord');


        const vertexColorLocation = gl.getUniformLocation(this._webgl.program, "u_color");



        /** 
         * 
         *  Draw Polygons' Interior
         *  **/
        const fsize = Float32Array.BYTES_PER_ELEMENT;
        //console.log("Numero de Buffers: ", buffers.length);

        gl.uniform4f(vertexColorLocation, aes.fillColor[0] / 255, aes.fillColor[1] / 255, aes.fillColor[2] / 255, aes.fillColor[3]);




        for (let i = 0; i < aes._features.length; i++) {
            for (let y = 0; y < aes._features[i]._triangles.length; y++) {

                gl.bindBuffer(gl.ARRAY_BUFFER, aes._features[i]._triangles[y]);

                gl.enableVertexAttribArray(vertexCoordLocation);
                gl.vertexAttribPointer(vertexCoordLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                //gl.vertexAttribPointer(vertexColorLocation, 4, gl.FLOAT, false, fsize * 6, fsize * 2);
                //gl.enableVertexAttribArray(vertexColorLocation);



                gl.drawArrays(gl.TRIANGLES, 0, aes._features[i]._triangles[y].numItems);
            }
        }
    }

    drawBorders(aes) {
        const gl = this._webgl.gl;
        if (gl == null) return;
        const matrixProjection = new Float32Array(16);

        //gl.clear(gl.COLOR_BUFFER_BIT);
        //gl.disable(gl.DEPTH_TEST);

        gl.enable(gl.BLEND);
        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

        const currentZoom = this.map.getZoom();
        const pointSize = Math.max(currentZoom - 5.0, 1.0);

        matrixProjection.set(this._webgl.projection);

        const scale = 2 ** currentZoom;
        this.scaleProjection(matrixProjection, scale, scale);

        const offset = this.latLongToPixelXY(this.map.getLngBound(), this.map.getLatBound());
        this.translateProjection(matrixProjection, -offset.x, -offset.y);

        const projectionLocation = gl.getUniformLocation(this._webgl.program, 'projection');
        gl.uniformMatrix4fv(projectionLocation, false, matrixProjection);

        const vertexSizeLocation = gl.getAttribLocation(this._webgl.program, 'aPointSize');
        gl.vertexAttrib1f(vertexSizeLocation, pointSize);

        const vertexCoordLocation = gl.getAttribLocation(this._webgl.program, 'vertexCoord');


        const vertexColorLocation = gl.getUniformLocation(this._webgl.program, "u_color");

        const isPointLocation = gl.getUniformLocation(this._webgl.program, 'isPoint');
        gl.uniform1f(isPointLocation, 0.0);


        /** 
         * 
         *  Draw Polygons' Interior
         *  **/
        const fsize = Float32Array.BYTES_PER_ELEMENT;
        //console.log("Numero de Buffers: ", buffers.length);

        gl.uniform4f(vertexColorLocation, aes.strokeColor[0] / 255, aes.strokeColor[1] / 255, aes.strokeColor[2] / 255, aes.strokeColor[3]);

        for (let i = 0; i < aes._features.length; i++) {
            for (let y = 0; y < aes._features[i]._borders.length; y++) {

                gl.bindBuffer(gl.ARRAY_BUFFER, aes._features[i]._borders[y]);

                gl.enableVertexAttribArray(vertexCoordLocation);
                gl.vertexAttribPointer(vertexCoordLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                //gl.vertexAttribPointer(vertexColorLocation, 4, gl.FLOAT, false, fsize * 6, fsize * 2);
                //gl.enableVertexAttribArray(vertexColorLocation);



                gl.drawArrays(gl.LINE_LOOP, 0, aes._features[i]._borders[y].numItems);
            }
        }


    }

    drawPoints(aes) {


        const gl = this._webgl.gl;
        if (gl == null) return;
        const matrixProjection = new Float32Array(16);


        //gl.clear(gl.COLOR_BUFFER_BIT);
        //gl.disable(gl.DEPTH_TEST);

        gl.enable(gl.BLEND);
        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

        const currentZoom = this.map.getZoom();
        const pointSize = Math.max(currentZoom - 4.0 + aes.pointSize, aes.pointSize);

        matrixProjection.set(this._webgl.projection);

        const scale = 2 ** currentZoom;
        this.scaleProjection(matrixProjection, scale, scale);

        const offset = this.latLongToPixelXY(this.map.getLngBound(), this.map.getLatBound());
        this.translateProjection(matrixProjection, -offset.x, -offset.y);

        const projectionLocation = gl.getUniformLocation(this._webgl.program, 'projection');
        gl.uniformMatrix4fv(projectionLocation, false, matrixProjection);

        const vertexSizeLocation = gl.getAttribLocation(this._webgl.program, 'aPointSize');
        gl.vertexAttrib1f(vertexSizeLocation, pointSize);

        const vertexCoordLocation = gl.getAttribLocation(this._webgl.program, 'vertexCoord');


        const vertexColorLocation = gl.getUniformLocation(this._webgl.program, "u_color");

        const isPointLocation = gl.getUniformLocation(this._webgl.program, 'isPoint');
        gl.uniform1f(isPointLocation, 1.0);

        /** 
         * 
         *  Draw Polygons' Interior
         *  **/
        const fsize = Float32Array.BYTES_PER_ELEMENT;
        //console.log("Numero de Buffers: ", buffers.length);

        gl.uniform4f(vertexColorLocation, aes.fillColor[0] / 255, aes.fillColor[1] / 255, aes.fillColor[2] / 255, aes.fillColor[3]);









        for (var i = 0; i < aes._features.length && this.dynamic == true; i++) {
            for (var y = 0; y < aes._features[i]._points.length; y++) {

                gl.bindBuffer(gl.ARRAY_BUFFER, aes._features[i]._points[y]);

                gl.enableVertexAttribArray(vertexCoordLocation);
                gl.vertexAttribPointer(vertexCoordLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                //gl.vertexAttribPointer(vertexColorLocation, 4, gl.FLOAT, false, fsize * 6, fsize * 2);
                //gl.enableVertexAttribArray(vertexColorLocation);



                gl.drawArrays(gl.POINTS, 0, aes._features[i]._points[y].numItems);
                //1);
            }
        }


        for (var i = 0; this.dynamic == false && aes._allFeatures != null && i < aes._allFeatures.length; i++) {
            for (var y = 0; y < aes._allFeatures[i]._points.length; y++) {

                gl.bindBuffer(gl.ARRAY_BUFFER, aes._allFeatures[i]._points[y]);

                gl.enableVertexAttribArray(vertexCoordLocation);
                gl.vertexAttribPointer(vertexCoordLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                //gl.vertexAttribPointer(vertexColorLocation, 4, gl.FLOAT, false, fsize * 6, fsize * 2);
                //gl.enableVertexAttribArray(vertexColorLocation);



                gl.drawArrays(gl.POINTS, 0, aes._allFeatures[i]._points[y].numItems);
                //gl.drawArrays(gl.TRIANGLE_STRIP, 0, aes._allFeatures[i]._points[y].numItems-2);	
                //1);
            }
        }




    }

    drawContinuousPolygons(aes) {

        const gl = this._webgl.gl;
        if (gl == null) return;
        const matrixProjection = new Float32Array(16);

        //gl.clear(gl.COLOR_BUFFER_BIT);
        //gl.disable(gl.DEPTH_TEST);

        //gl.enable(gl.BLEND);
        //gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);


        const currentZoom = this.map.getZoom();
        const pointSize = Math.max(currentZoom - 5.0, 1.0);

        matrixProjection.set(this._webgl.projection);

        const scale = 2 ** currentZoom;
        this.scaleProjection(matrixProjection, scale, scale);

        const offset = this.latLongToPixelXY(this.map.getLngBound(), this.map.getLatBound());
        this.translateProjection(matrixProjection, -offset.x, -offset.y);

        const projectionLocation = gl.getUniformLocation(this._webgl.program, 'projection');
        gl.uniformMatrix4fv(projectionLocation, false, matrixProjection);

        const vertexSizeLocation = gl.getAttribLocation(this._webgl.program, 'aPointSize');
        gl.vertexAttrib1f(vertexSizeLocation, pointSize);

        const isPointLocation = gl.getUniformLocation(this._webgl.program, 'isPoint');
        gl.uniform1f(isPointLocation, 0.0);



        const vertexCoordLocation = gl.getAttribLocation(this._webgl.program, 'vertexCoord');


        const vertexColorLocation = gl.getUniformLocation(this._webgl.program, "u_color");



        /** 
         * 
         *  Draw Polygons' Interior
         *  **/
        const fsize = Float32Array.BYTES_PER_ELEMENT;
        //console.log("Numero de Buffers: ", buffers.length);






        for (let i = 0; i < aes._features.length; i++) {
            let ucolor;
            let color;
            const diff = aes._features[i]._properties[this.attr];
            if (diff == 0)
                color = aes.fillColor(0.5).rgb();
            else {
                if (diff > 0) {
                    color = aes.fillColor(0.5 + diff / this.max / 2).rgb();


                } else {
                    color = aes.fillColor(0.5 - diff / this.min / 2).rgb();
                }

            }
            ucolor = [Math.round(color[0]), Math.round(color[1]), Math.round(color[2]), this.alpha];

            gl.uniform4f(vertexColorLocation, ucolor[0] / 255, ucolor[1] / 255, ucolor[2] / 255, this.alpha);
            for (let y = 0; y < aes._features[i]._triangles.length; y++) {

                gl.bindBuffer(gl.ARRAY_BUFFER, aes._features[i]._triangles[y]);

                gl.enableVertexAttribArray(vertexCoordLocation);
                gl.vertexAttribPointer(vertexCoordLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                //gl.vertexAttribPointer(vertexColorLocation, 4, gl.FLOAT, false, fsize * 6, fsize * 2);
                //gl.enableVertexAttribArray(vertexColorLocation);



                gl.drawArrays(gl.TRIANGLES, 0, aes._features[i]._triangles[y].numItems);
            }
        }


    }

    drawProporcionalPoints(aes) {

        const gl = this._webgl.gl;
        if (gl == null) return;
        const matrixProjection = new Float32Array(16);



        gl.enable(gl.BLEND);
        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

        const currentZoom = this.map.getZoom();


        matrixProjection.set(this._webgl.projection);

        const scale = 2 ** currentZoom;
        this.scaleProjection(matrixProjection, scale, scale);

        const offset = this.latLongToPixelXY(this.map.getLngBound(), this.map.getLatBound());
        this.translateProjection(matrixProjection, -offset.x, -offset.y);

        const projectionLocation = gl.getUniformLocation(this._webgl.program, 'projection');
        gl.uniformMatrix4fv(projectionLocation, false, matrixProjection);



        const vertexCoordLocation = gl.getAttribLocation(this._webgl.program, 'vertexCoord');


        const vertexColorLocation = gl.getUniformLocation(this._webgl.program, "u_color");

        const isPointLocation = gl.getUniformLocation(this._webgl.program, 'isPoint');
        gl.uniform1f(isPointLocation, 1.0);

        /** 
         * 
         *  Draw Polygons' Interior
         *  **/
        const fsize = Float32Array.BYTES_PER_ELEMENT;
        //console.log("Numero de Buffers: ", buffers.length);

        gl.uniform4f(vertexColorLocation, aes.fillColor[0] / 255, aes.fillColor[1] / 255, aes.fillColor[2] / 255, this.alpha);










        if (this.dynamic == true) {
            for (const i in aes._features) {
                for (const y in aes._features[i]._points) {

                    gl.bindBuffer(gl.ARRAY_BUFFER, aes._features[i]._points[y]);
                    const propvalue = parseFloat(aes._features[i]._properties[this.attr]);
                    const temppointsize = ((this.maxpointsize - this.minpointsize) / (this.max - this.min)) * (propvalue - this.min);
                    const pointSize = Math.max(currentZoom - 4.0 + temppointsize * currentZoom / 4, 2);
                    const vertexSizeLocation = gl.getAttribLocation(this._webgl.program, 'aPointSize');
                    gl.vertexAttrib1f(vertexSizeLocation, pointSize);

                    gl.enableVertexAttribArray(vertexCoordLocation);
                    gl.vertexAttribPointer(vertexCoordLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                    //gl.vertexAttribPointer(vertexColorLocation, 4, gl.FLOAT, false, fsize * 6, fsize * 2);
                    //gl.enableVertexAttribArray(vertexColorLocation);



                    gl.drawArrays(gl.POINTS, 0, aes._features[i]._points[y].numItems);
                    //1);
                }
            }
        }
    }

    initialize() {
        this.max = null;
        this.min = null;
        this.createCanvas();
        this.program();
        let mappos;
        for (let i = 0; i < maps.length; i++)
            if (maps[i].id == this.id)
                mappos = i;
        this.map.onEvent('move', () => {
            console.log("@Rui: mudar o profiling do window para outra coisa?")
            if (window.profiling == true)
                var start = Date.now();
            maps[mappos].draw();
            if (window.profiling == true) {
                const end = Date.now();
                window.console.log(`Tempo de processamento de Zoom/Pan (segundos):${(end - start) / 1000}`);
            }
        }
        );

        this.setupOnclick(mappos);
    }

    setupOnclick(mappos) {
        console.log("@Rui: profiling mudar de window para? | Rtree e KdTree")
        maps[mappos].map.onEvent('click', e => {
            if (window.profiling == true)
                var start = Date.now();
            const lat = e.latlng.lat;
            const lon = e.latlng.lng;

            if (maps[mappos].rtree != undefined) {
                var bool = maps[0].rtree.search(lon, lat);
                if (bool == undefined)
                    return;
                else {
                    //console.log
                    var s = "";
                    var first = true;
                    if (maps[mappos].showPropertiesOnClick != null) {
                        for (var i = 0; i < maps[mappos].showPropertiesOnClick.length; i += 2) {
                            if (first) {
                                s += `${maps[mappos].showPropertiesOnClick[i + 1]}: ${bool.properties[maps[mappos].showPropertiesOnClick[i]]}`;
                                first = false;
                            }
                            else {
                                s += `\n${maps[mappos].showPropertiesOnClick[i + 1]}: ${bool.properties[maps[mappos].showPropertiesOnClick[i]]}`;
                            }

                        }
                    }
                    else {

                        var keys = Object.keys(bool.properties);

                        for (var i = 0; i < keys.length; i++) {
                            if (keys[i] != "_gisplayid") {
                                if (first) {
                                    s += `${keys[i]}: ${bool.properties[keys[i]]}`;
                                    first = false;
                                }
                                else {
                                    s += `\n${keys[i]}: ${bool.properties[keys[i]]}`;
                                }
                            }
                        }
                    }
                    if (maps[mappos].interactive == true)
                        alert(s);//todo
                    if (maps[mappos].mapOnClickCall != undefined && maps[mappos].mapOnClickCall != null)
                        maps[mappos].mapOnClickCall(bool);
                }
            }
            if (maps[mappos].kdtree != undefined) {

                const nearest = maps[mappos].kdtree.nearest({ lat, lon }, 1, 128 / ((2 ** (map.getZoom() * 2))));
                if (nearest.length <= 0)
                    return;
                else {
                    var bool = nearest[0][0];
                    //console.log
                    var s = "";
                    var first = true;
                    if (maps[mappos].showPropertiesOnClick != null) {
                        for (var i = 0; i < maps[mappos].showPropertiesOnClick.length; i += 2) {
                            if (first) {
                                s += `${maps[mappos].showPropertiesOnClick[i + 1]}: ${bool.properties[maps[mappos].showPropertiesOnClick[i]]}`;
                                first = false;
                            }
                            else {
                                s += `\n${maps[mappos].showPropertiesOnClick[i + 1]}: ${bool.properties[maps[mappos].showPropertiesOnClick[i]]}`;
                            }

                        }
                    }
                    else {

                        var keys = Object.keys(bool.properties);

                        for (var i = 0; i < keys.length; i++) {
                            if (keys[i] != "_gisplayid") {
                                if (first) {
                                    s += `${keys[i]}: ${bool.properties[keys[i]]}`;
                                    first = false;
                                }
                                else {
                                    s += `\n${keys[i]}: ${bool.properties[keys[i]]}`;
                                }
                            }
                        }
                    }
                    if (maps[mappos].interactive == true)
                        alert(s);
                    if (maps[mappos].mapOnClickCall != undefined && maps[mappos].mapOnClickCall != null)
                        maps[mappos].mapOnClickCall(bool);
                }
            }

            if (window.profiling == true) {
                const end = Date.now();
                window.console.log(`Tempo de processamento de um click (segundos): ${(end - start) / 1000}`);
            }
        });

    }

    loadOptions(options, bgmap) {
        if (options.customMapService == true)
            this.map = bgmap;
        else
            this.map = new BGMapWrapper(bgmap);
        if (options.loader != false)
            this.loader();

        if (options.showPropertiesOnClick == true) {
            this.showPropertiesOnClick = null;
            //append on bgmap object
        }
        else if (options.showPropertiesOnClick == false) {
            //nada
        }
        else if (options.showPropertiesOnClick != undefined) {
            this.showPropertiesOnClick = options.showPropertiesOnClick;
        }
        this.alpha = options.alpha != undefined ? options.alpha : 0.8;
        this.interactive = options.interactive == undefined ? true : !options.interactive;
        this.attr = options.attr;
        this.dynamic = options.memorySaver == undefined ? false : !options.memorySaver;
        this.maxfeatures = options.maxFeatures;
        this.breaks = options.classBreaks;
        this.colorscheme = options.colorScheme;
        this.numberofclasses = options.numberOfClasses;
        this.algorithm = options.classBreaksMethod;
        this.legendOnClickCall = options.legendOnClickFunction;
        this.mapOnClickCall = options.mapOnClickFunction;
        this.minuend = options.minuend;
        this.subtrahend = options.subtrahend;
        this.legendTitle = options.legendTitle != undefined ? options.legendTitle : (this.attr != undefined ? this.attr : `${this.minuend} - ${this.subtrahend}`);
        this.numberOfLegendItems = options.numberOfLegendItems != undefined ? options.numberOfLegendItems : 2;
    }

    loader() {
        this.map.loader();
    }

    getNumberOfPolygons() {
        let count = 0;
        for (let i = 0; i < this.aesthetics.length; i++) {
            for (let z = 0; z < this.aesthetics[i]._features.length; z++) {
                count += this.aesthetics[i]._features[z]._triangles.length;
            }
        }
        return count;
    }

    //STATIC METHODS(should be implemented by sub-classes)
    draw() {
        alert("draw() not implemented");//throw new NotImplementedError();
    }

    /**
     * @deprecated This was created for Heat Maps. Another idea will probably be used.
     * @param {Aesthetic} aes 
     */
    drawHeatPoints(aes) {
        const gl = this._webgl.gl;

        if (gl == null) return;
        gl.useProgram(this._webgl.heatmapProgram[0]);
        const matrixProjection = new Float32Array(16);

        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.enable(gl.BLEND);

        gl.blendFunc(gl.ONE, gl.ONE);

        const currentZoom = map.getZoom();
        const pointSize = Math.max(currentZoom - 5.0, 1.0);

        matrixProjection.set(this._webgl.projection);

        const scale = 2 ** currentZoom;
        this.scaleProjection(matrixProjection, scale, scale);

        const offset = this.latLongToPixelXY(this.map.getLngBound(), this.map.getLatBound());
        this.translateProjection(matrixProjection, -offset.x, -offset.y);


        const projectionLocation = gl.getUniformLocation(this._webgl.heatmapProgram[0], 'projection');
        gl.uniformMatrix4fv(projectionLocation, false, matrixProjection);


        const vertexCoordLocation = gl.getAttribLocation(this._webgl.heatmapProgram[0], 'position');
        const deltaLocation = gl.getAttribLocation(this._webgl.heatmapProgram[0], 'delta');
        const intensityLoc = gl.getAttribLocation(this._webgl.heatmapProgram[0], 'intensity');
        const vertexSizeLocation = gl.getAttribLocation(this._webgl.heatmapProgram[0], 'aPointSize');

        gl.vertexAttrib1f(vertexSizeLocation, pointSize);

        gl.enableVertexAttribArray(vertexCoordLocation);
        gl.enableVertexAttribArray(deltaLocation);
        gl.enableVertexAttribArray(intensityLoc);



        const fsize = Float32Array.BYTES_PER_ELEMENT;

        gl.bindBuffer(gl.ARRAY_BUFFER, aes._allFeatures[0]._points[0]);
        gl.vertexAttribPointer(vertexCoordLocation, 2, gl.FLOAT, false, fsize * 8, 0 * 2);
        gl.vertexAttribPointer(deltaLocation, 2, gl.FLOAT, false, fsize * 8, 2 * 4);
        gl.vertexAttribPointer(intensityLoc, 4, gl.FLOAT, false, fsize * 8, 4 * 4);



        console.log(aes._allFeatures[0]._points[0].numItems);
        gl.drawArrays(gl.TRIANGLES, 0, aes._allFeatures[0]._points[0].numItems);


        gl.useProgram(this._webgl.heatmapProgram[1]);

        gl.disable(gl.BLEND);

        //console.log("fase 1 concluida");

        const canvas = document.getElementById(`mapCanvas${this.id}`);



        const source = gl.createTexture();

        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, source);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);

        function isPowerOf2(value) {
            return (value & (value - 1)) == 0;
        };

        function steupTextureFilteringAndMips(width, height, gl) {
            if (isPowerOf2(width) && isPowerOf2(height)) {
                // the dimensions are power of 2 so generate mips and turn on 
                // tri-linear filtering.
                gl.generateMipmap(gl.TEXTURE_2D);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
            } else {
                // at least one of the dimensions is not a power of 2 so set the filtering
                // so WebGL will render it.
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
                gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            }
        };

        steupTextureFilteringAndMips(canvas.width, canvas.height, gl);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

        const vertices = new Float32Array([1, 1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1]);
        const buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
        const positionLoc = gl.getAttribLocation(this._webgl.heatmapProgram[1], 'position');
        const sourceLoc = gl.getUniformLocation(this._webgl.heatmapProgram[1], 'source');
        gl.enableVertexAttribArray(positionLoc);
        gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
        gl.uniform1i(sourceLoc, 0);
        gl.drawArrays(gl.TRIANGLES, 0, 6);


        gl.disableVertexAttribArray(positionLoc);
        //defaults to general program
        //console.log("fase 2 concluida");
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
        this._webgl.gl.useProgram(this._webgl.program);
    }
}