Home Reference Source Test

src/Gisplay/Maps/Map.js

import { BGMapWrapper } from './Background Maps/BGMapWrapper';
import { Aesthetic } from '../Aesthetic';
import { GisplayOptions } from '../GisplayOptions';
import { WebGLUtils } from '../Helpers/WebGLUtils';
import { ColorBrewer } from '../Helpers/ColorBrewer';

/**
 * 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).
 * @see Diogo's thesis 57-60 + 64/65
 */
export class Map {

    /**
     * Map constructor. All Map subclasses should call this first with super(...).
     * @param {BGMapWrapper|Object} bgmap - Background map.
     * @param {JSON} geometry - Geometry read from the file.
     * @param {Object} userOptions - The user defined options. 
     * @todo With the type of the map we can do an if statement inside constructor to use or not some of the variables.
     */
    constructor(bgmap, geometry, userOptions) {
        /**
          * This object holds for each point it's long, lat and associated properties. 
          * This points will be used by k-d Tree. And k-d Tree is used to find the closest point to where the user clicked.
          * @type {Array<{lon: number, lat: number, properties: JSON}>}
          */
        this.treepoints = [];

        // ####################### DOT MAP ONLY
        /**
          * Holds the points for each Aesthethic object.
          * @type {Array<Array<number>>}
          */
        this.tempAestheticPoints = new Array();

        // ####################### ALL MAPS
        /**
         * Aesthetic objects that were saved.
         * @type {Array<Aesthetic>}
         */
        this.aesthetics = new Array();
        /**
         * JSON object with the geometry that was read from the file.
         * @type {JSON}
         */
        this.geometry = geometry;
        /**
         * This map unique identifier.
         * @type {number}
         */
        this.id = window.mapcount++;

        /**
         * KdTree object to hold that will be used to calculate the closest point to the coordinates where the user made a click.
         * @type {kdTree}
         * @see Diogo's thesis Page 61.
         * @see https://github.com/ubilabs/kd-tree-javascript
         */
        this.kdtree = undefined;
        /**
         * RBush(Rtree implementation) that is used to calculate the polygon closest to the coordinates where the user made a click.
         * @type {PolygonLookup}
         * @see Diogo's thesis page 61.
         * @see https://github.com/pelias/polygon-lookup
         * @see https://github.com/mourner/rbush
         */
        this.rtree = undefined;
        /**
         * Polygons do/do not exist. This is used to know if it to create a rtree or not on buildTrees method.
         * @type {boolean}
         */
        this.hasPolygons = false;

        /**
         * This is the maixmum value found on the dataset. The final value for the break. E.g. [2, 37[ 37 is the max. 
         * Used for PSymbol Map and Change Map because on those maps it's important to take in consideration this values to define the Feature color.
         * @see Diogo's thesis page 63 5.1.3 
         * @type {number}
         */
        this.max = null;
        /**
         * This is the minimum value found on the dataset. The initial value for the break. E.g. [2, 37[ 2 is the min.
         * Used for PSymbol Map and Change Map because on those maps it's important to take in consideration this values to define the Feature color.
         * @see Diogo's thesis page 63 5.1.3
         * @type {number}
         */
        this.min = null;
    }

    /**
     * M1) Loads user and default options. For each case if it isn't provided by the user, then it uses
     * the default option. 1st method to be called.
     * @param {Object} userOptions - Options given by the user. 
     * @param {Object} bgmap  - The background map provider.
     */
    loadOptions(userOptions, bgmap) {
        /**
         * All the options available in the Gisplay API.
         * @type {GisplayOptions}
         */
        this.gisplayOptions = new GisplayOptions(userOptions);

        /**
         * This is the Gisplay API Background map wrapper. It contains the background map provider object to access it's methods (zoom, coordinates,etc).
         * This map(Gisplay Map) will  be drawn over the background map(bGMap).
         * @type {BGMapWrapper}
         */
        this.bGMap = bgmap;
        if (this.gisplayOptions.showLoader)
            this.showLoader();
    }

    /**
     * M2) Calls the Background Map Wrapper to create the loader to be used later when the user feeds data to the API.
     */
    showLoader() {
        this.bGMap.showLoader();
    }

    /**
     * M3) Initializes the API by: creating canvas, the WebGL program and setting up all needed events.
     * @return {void}
     */
    initializeCanvasAndEvents() {
        this.createCanvas();
        WebGLUtils.createWebGLProgram(this._webgl);
        this.setupEvents(this.id);
    }

    /**
     * M4) Creates a canvas element and WebGL associated information.
     */
    createCanvas() {
        const canvas = this.bGMap.createCanvas(this.id);

        /**
         * @type {Object} _webgl - WebGL object.  Init webgl properties
         * @property  {WebGLRenderingContext} _webgl.gl - The WebGLRenderingContext to be used.
         * @property  {WebGLProgram} _webgl.program -  The WebGLProgram to be used. 
         * @property  {Float32Array} _webgl.projection - The projection to be used. Deprecated in favor of better projection to work with multiple map background providers.
         * @see Diogo's thesis page 64+
         */
        this._webgl = {
            gl: null,
            program: null
        };

        this._webgl.gl = canvas.getContext("webgl");
        window.canvas = canvas;
        this._webgl.gl.viewport(0, 0, this.bGMap.getContainer().offsetWidth, this.bGMap.getContainer().offsetHeight);
        this._webgl.gl.disable(this._webgl.gl.DEPTH_TEST);
    }

    /**
     * M8) Setup all events used by the API. Right now the API uses: drag, zoom and click events. 
     * This events will be fired by the background map provider and we can use them to draw(zoom and pan) or alert information(click).
     * Pan/Zoom = move, click = click
     * rtree will be used to find the closest polygon to the where the clicked event happened.
     * kdtree will be used to find the closest point to where the click event happened.
     * @todo This method is doing uneccessary job if the user set interative to false and mapOnClickFunction is undefined.
     * @param {number} mappos - This map id. 
     * @return {void}
     * @see https://www.mapbox.com/mapbox-gl-js/api/#events
     * @see http://stackoverflow.com/a/34349737
     */
    setupEvents(mappos) {
        /*if(!this.gisplayOptions.interactive && this.gisplayOptions.mapOnClickFunction === undefined)
            return;*/
        this.bGMap.addPanEvent(() => this.draw());
        this.bGMap.addZoomEvent(() => this.draw());
        this.bGMap.addClickEvent(this);
    }

    /**
     * To be called when the user clicks on the map.
     * @param {number} lng - Longitude of the click event. 
     * @param {number} lat - Latitude of the click event.
     * @memberOf Map
     */
    clickEvent(lng, lat) {
        /**
         * When dealing with polygons rtree will be used.
         * @see Diogo's thesis Page 62
         */
        if (this.rtree !== undefined)
            this.searchRTree(lng, lat);

        /**
         * When dealing with points kdtree will be used.
         * @see Diogo's thesis page 62
         */
        if (this.kdtree !== undefined)
            this.searchKdTree(lng, lat);
    }

    /**
     * Search the rtree for the closest polygon from the lng, lat that was clicked.
     * @param {number} lng - The longitude of the click.
     * @param {number} lat - The latitude of the click.
     * @returns {void}
     * @memberOf Map
     */
    searchRTree(lng, lat) {
        let rtreeSearchResult = this.rtree.search(lng, lat);
        if (rtreeSearchResult === undefined)
            return;
        else {
            let res = "";
            let showPrtOnClick = this.gisplayOptions.showPropertiesOnClick;
            if (showPrtOnClick !== null) {
                for (let i = 0; i < showPrtOnClick.length; i += 2) {
                    if (i === 0)
                        res += `${showPrtOnClick[i + 1]}: ${rtreeSearchResult.properties[showPrtOnClick[i]]}`;
                    else
                        res += `\n${showPrtOnClick[i + 1]}: ${rtreeSearchResult.properties[showPrtOnClick[i]]}`;
                }
            }
            else {
                let keys = Object.keys(rtreeSearchResult.properties);
                for (let [i, key] of keys.entries())
                    if (key !== "_gisplayid")
                        i == 0 ? res += `${key}: ${rtreeSearchResult.properties[key]}` : res += `\n${key}: ${rtreeSearchResult.properties[key]}`;
            }

            if (this.gisplayOptions.interactive)
                alert(res);//todo
            if (this.gisplayOptions.mapOnClickFunction !== undefined)
                this.gisplayOptions.mapOnClickFunction(rtreeSearchResult);
        }
    }

    /**
     * Search the rtree for the closest point from the lng, lat that was clicked. 
     * @param {number} lon - The longitude of the click.
     * @param {number} lat - The latitude of the click.
     * @returns {void}
     * @memberOf Map
     */
    searchKdTree(lon, lat) {
        let nearest = this.kdtree.nearest({ lon, lat }, 1, 128 / ((2 ** (this.bGMap.getZoom() * 2))));
        if (nearest.length <= 0)
            return;
        else {
            let kdTreeSearchResult = nearest[0][0];
            let res = "";
            let showPrtOnClick = this.gisplayOptions.showPropertiesOnClick;
            if (showPrtOnClick !== null) { //TODO: Remove if dentro do for e passar a começar no i=1 e passar o if para antes do for
                for (let i = 0; i < showPrtOnClick.length; i += 2) {
                    if (i === 0)
                        res += `${showPrtOnClick[i + 1]}: ${kdTreeSearchResult.properties[showPrtOnClick[i]]}`;
                    else
                        res += `\n${showPrtOnClick[i + 1]}: ${kdTreeSearchResult.properties[showPrtOnClick[i]]}`;
                }
            }
            else {
                let keys = Object.keys(kdTreeSearchResult.properties);
                for (let [i, key] of keys.entries())
                    if (key !== "_gisplayid")
                        i == 0 ? res += `${key}: ${kdTreeSearchResult.properties[key]}` : res += `\n${key}: ${kdTreeSearchResult.properties[key]}`;
            }

            if (this.gisplayOptions.interactive)
                alert(res);
            if (this.gisplayOptions.mapOnClickFunction !== undefined)
                this.gisplayOptions.mapOnClickFunction(kdTreeSearchResult);
        }
    }

    /** ########################   METHOD from Gisplay.js    ######################## */
    /**
     * M9) Method that executes all the process associated with the creation of the thematic map.
     * @memberOf Map
     */
    makeMap() {
        let opts = this.gisplayOptions;
        setTimeout(() => { //TODO: Remove setTimeout(it is only here to allo the loader to appear)
            if (opts.numberOfClasses === undefined)
                opts.numberOfClasses = this.defaults().numberOfClasses;
            this.preProcessData(this.geometry, opts.numberOfClasses, opts.classBreaksMethod, opts.colorScheme);

            this.loadGeoJSON(this.geometry);
            this.draw();

            if (opts.showLegend)
                this.buildLegend();
            if (opts.showLoader) //@TODO: Change this if to be the 1st thing done inside the setTimeout
                this.showLoader();
        }, 1);
    }

    /**
     * M11) Creates Aesthetic objects. If there's the need to calculate the class intervals,
     * the method calcClassBreaks is called 
     * @param {JSON} geojson - GeoJSON object.
     * @param {number} numberOfClasses - Number of classes that the Legend will contain.
     * @param {string} classBreaksMethod - Algorithm to be used to calculate class breaks. Only used it class breaks are not given by the user.
     * @param {string[]} colorScheme - Color scheme to be used by this map.
     * @memberOf Map
     */
    preProcessData(geojson, numberOfClasses, classBreaksMethod, colorScheme) {
        /**
         * @type {Array<Aesthetic>}
         */
        const aesarray = []; //Array of aesthetic objects loaded from the file
        const numberValues = []; //The attr is a number
        const stringValues = []; //The attr is a string
        let classBreaks; //Class Breaks given by the user or calculated by the API
        let fcolor; //Fill Colors to be used
        let opts = this.gisplayOptions; //Options given by the user and defaults

        let geoJsonFeaturesLength = geojson.features.length;
        for (let i = 0; i < geoJsonFeaturesLength && (i < opts.maxFeatures); i++) { //@TODO?: Two fors one if attr is a string another if it a number
            let attrValue = geojson.features[i].properties[opts.attr];
            if (attrValue !== null && typeof attrValue === 'number') { //If "f3" exists and its a number
                numberValues.push(attrValue);
                this.max = Math.max(this.max, attrValue);
                this.min = Math.min(this.min, attrValue);
            }
            else if (!stringValues.includes(attrValue)) //If its a string
                stringValues.push(geojson.features[i].properties[opts.attr]);
        }

        if (numberValues.length > 0) { //Quantitative
            if (opts.classBreaks === undefined) { //Not given by the user then calculate them
                if (numberOfClasses > 1)
                    classBreaks = this.calcClassBreaks(numberValues, classBreaksMethod, numberOfClasses);
                else
                    classBreaks = [this.min, this.max]; //Change Map
            }
            else //Given by the user
                classBreaks = opts.classBreaks;

            if (classBreaks.length > 2) {
                if (colorScheme !== undefined)
                    fcolor = chroma.scale(colorScheme).colors(classBreaks.length - 1);
                else
                    fcolor = this.getDefaultColors(classBreaks.length - 1);//chroma.scale(colorscheme).colors(classBreaks.length);

                for (let i = 0; i < classBreaks.length - 1; i++) {
                    let [r, g, b] = chroma(fcolor[i]).rgb(); // let color = chroma(fcolor[i]).rgb();
                    let aes;
                    if (i !== classBreaks.length - 2)
                        aes = new Aesthetic(i, opts.attr, [Math.round(r), Math.round(g), Math.round(b), opts.alpha], [0, 0, 0, 1], null, [classBreaks[i], classBreaks[i + 1]]);
                    else {
                        aes = new Aesthetic(i, opts.attr, [Math.round(r), Math.round(g), Math.round(b), opts.alpha], [0, 0, 0, 1], null, [classBreaks[i], classBreaks[i + 1]]);
                        aes.outer = true;
                    }
                    aesarray.push(aes);
                }
            }
            else {
                colorScheme = this.getDefaultColors(classBreaks.length);
                let [r, g, b] = chroma(colorScheme[0]).rgb();// let color = chroma(colorscheme[0]).rgb();
                let aes = new Aesthetic(0, opts.attr, [Math.round(r), Math.round(g), Math.round(b), opts.alpha], [0, 0, 0, 1], null, [classBreaks[0], classBreaks[1]]);
                aes.outer = true;
                aesarray.push(aes);
            }
        }
        else { //Qualitative
            if (stringValues.length > 0) {
                classBreaks = stringValues;
                if (colorScheme !== undefined)//User defined colorScheme
                    fcolor = chroma.scale(colorScheme).colors(classBreaks.length);
                else
                    fcolor = this.getDefaultColors(classBreaks.length, 'Qualitative');//chroma.scale(colorscheme).colors(classBreaks.length);

                for (let i = 0; i < classBreaks.length; i++) {
                    let [r, g, b] = chroma(fcolor[i]).rgb();// let color = chroma(fcolor[i]).rgb();
                    let aes = new Aesthetic(i, opts.attr, [Math.round(r), Math.round(g), Math.round(b), 1], [0, 0, 0, 1], null, [stringValues[i]]);
                    aesarray.push(aes);
                }
            }
        }

        /**
         * All the Aesthetics that were read from the GeoJSON file.
         * @type {Array<Aesthetic>}
         */
        this.aesthetics = aesarray;
    }

    /**
     * M12) Calculates the class breaks using the algorithm given(k-means, quantile or equidistant). 
     * There will be as many class breaks as number of classes(numberOfClasses) given as input. 
     * @param {number[]} numberValues - The data values that will be used to be compute the breaks. 
     * @param {string} classBreakMethod - The algorithm to use to calculate the class breaks.
     * @param {number} numberOfClasses - Number of classes the result expects.
     * @returns {number[]} - The class breaks resulting from the use of the given algorithm and the number of classes.
     * @see http://gka.github.io/chroma.js/#chroma-limits
     * @memberOf Map
     */
    calcClassBreaks(numberValues, classBreakMethod, numberOfClasses) {
        let classBreaks;
        switch (classBreakMethod) {
            case 'equidistant':
                classBreaks = chroma.limits(numberValues, 'e', numberOfClasses);
                break;

            case 'quantile':
                classBreaks = chroma.limits(numberValues, 'q', numberOfClasses);
                break;

            case 'k-means':
                classBreaks = chroma.limits(numberValues, 'k', numberOfClasses);
                break;

            default:
                classBreaks = chroma.limits(numberValues, 'q', numberOfClasses);
                break;
        }
        return classBreaks;
    }

    /**
     * M13) Loads GeoJSON object that came from the file uploaded by the user. 
     * Extracts the Features present in the geometry object and inserts them in Aesthetic object(s)
     * This method will create and insert features to Aesthetic objects and build trees of relationships between points or polygons.
     * @param {JSON} geojson - GeoJSON read from the file.
     * @see https://www.dashingd3js.com/lessons/geojson
     * @memberOf Map
     */
    loadGeoJSON(geojson) {
        for (let i = 0; i < geojson.features.length && (this.gisplayOptions.maxFeatures === undefined || i < this.gisplayOptions.maxFeatures); i++) {
            geojson.features[i].properties['_gisplayid'] = i;
            const geometry = geojson.features[i].geometry;
            const properties = geojson.features[i].properties;
            this.createAndInsertFeature(i, geometry, properties);
        }
        this.buildTrees(geojson); //@TODO: Only call this method if it's a low end device aka options.memorySaver is on
    }

    /**
     * M14) Creates a Feature and then calls a method to insert said Feature in one or more Aesthetic objects.
     * @param {number} featureId - Id of the Feature.
     * @param {JSON} geometry - GeoJSON Geometry Object. 
     * @param {JSON} properties - GeoJSON properties Object.
     * @see http://geojson.org/geojson-spec.html#geometry-objects
     * @memberOf Map
     */
    createAndInsertFeature(featureId, geometry, properties) {
        const gl = this._webgl.gl;
        /**
         * @type {GisplayOptions}
         */
        let opts = this.gisplayOptions;
        if (opts.minuend !== undefined && opts.subtrahend !== undefined
            && typeof properties[opts.minuend] === 'number' && typeof properties[opts.subtrahend] === 'number'
            && properties[opts.minuend] !== undefined && properties[opts.subtrahend] !== undefined) {
            properties[opts.attr] = properties[opts.minuend] - properties[opts.subtrahend]; //Used for Change map
        }

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

            const currentTriangles = []; //Polygon Triangles vertices
            const bufferT = []; //WebGL Buffers with  triangles
            const currentBorders = []; //Polygon Borders vertices
            const bufferB = []; //Buffer borders

            for (let i = 0; i < polygons.length; i++) { //For each Polygon
                const trianglesPolygon = polygons[i].triangles;
                const border = polygons[i].vertices;
                currentTriangles[i] = new Array(); //Add This Polygon triangles
                currentBorders[i] = new Array(); //Add this Polygon borders

                for (let j = 0; j < trianglesPolygon.length; j++) { //Triangles
                    currentTriangles[i].push(border[trianglesPolygon[j] * 2], border[trianglesPolygon[j] * 2 + 1]);

                    if (j === trianglesPolygon.length - 1) {
                        bufferT.push(gl.createBuffer());

                        let vertArray = new Float32Array(currentTriangles[i]);
                        gl.fsize = vertArray.BYTES_PER_ELEMENT;
                        gl.bindBuffer(gl.ARRAY_BUFFER, bufferT[i]);
                        gl.bufferData(gl.ARRAY_BUFFER, vertArray, gl.STATIC_DRAW);

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

                for (let k = 0; k < border.length; k += 2) { //Borders
                    currentBorders[i].push(border[k], border[k + 1]);

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

                        let vertArray = new Float32Array(currentBorders[i]);
                        gl.fsize = vertArray.BYTES_PER_ELEMENT;
                        gl.bindBuffer(gl.ARRAY_BUFFER, bufferB[i]);
                        gl.bufferData(gl.ARRAY_BUFFER, vertArray, gl.STATIC_DRAW);

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

        else if (geometry.type === "Point" && opts.isDynamic) {
            const currentPoints = new Array();
            currentPoints.push(geometry.coordinates[0], geometry.coordinates[1]);
            const bufferPoints = [];//Buffer points
            let vertArray = new Float32Array(currentPoints);

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

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

            this.insertFeature(featureId, properties, [], [], bufferPoints);
            this.treepoints.push({ lon: geometry.coordinates[0], lat: geometry.coordinates[1], properties });
        }

        else if (geometry.type === "Point" && !opts.isDynamic) {
            if (this.tempAestheticPoints.length === 0)
                for (let i = 0; i < this.aesthetics.length; i++)
                    this.tempAestheticPoints[i] = [];

            const aesPositions = this.fitFeature(properties);
            for (const aesPos of aesPositions)
                this.tempAestheticPoints[aesPos].push(geometry.coordinates[0], geometry.coordinates[1]);

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

    /**
     * M15) Deals with polygon triangulation.
     * @param {{geometry: JSON, properties: JSON}} polygon - The geometry and properties of the polygon.
     * @returns {{triangles: Array<number>, vertices: Array<number>}} - The triangles and vertices calculated by earcut triangulation. The vertices are the outside of the polygon, the triangles are the inside.
     * @see http://www.macwright.org/2015/03/23/geojson-second-bite.html#polygons
     * @memberOf Map
     */
    processPolygon(polygon) {
        let polyarray = [];
        if (polygon.geometry.type === "Polygon") { //@TODO: [Demos never use this if statement.]
            let outsidepolygon = polygon.geometry.coordinates[0]; //See: http://geojson.org/geojson-spec.html#polygon
            let tempVerts = new Array();
            for (let out = 0; out < outsidepolygon.length - 1; out++)
                tempVerts.push(outsidepolygon[out][0], outsidepolygon[out][1]);//_vertexcount += (outsidepolygon.length + 1) / 2;
            let triangles_vert = earcut(tempVerts); // _tricount += (triangles_vert.length / 3);
            polyarray.push({ triangles: triangles_vert, vertices: tempVerts });
        }
        else if (polygon.geometry.type == "MultiPolygon") { //See http://geojson.org/geojson-spec.html#multipolygon
            for (const cs of polygon.geometry.coordinates) {
                let outsidepolygon = cs[0];
                let tempVerts = new Array();
                for (const c of outsidepolygon)
                    tempVerts.push(c[0], c[1]);
                let triangles_vert = earcut(tempVerts);
                polyarray.push({ triangles: triangles_vert, vertices: tempVerts });
            }
        }
        return polyarray;
    }

    /**
     * M17) Inserts the Feature into one or more Aesthetic objects. If the Feature does not fit in any Aesthetic then does nothing.
     * @param {number} id - The Feature id. 
     * @param {JSON} properties - The Feature properties. 
     * @param {{itemSize: number, numItems: number}} triangles - Triangles, each in one WebGLBuffer.
     * @param {{itemSize: number, numItems: number}} borders - Borders, each in one WebGLBuffer.
     * @param {{itemSize: number, numItems: number}} points - Points, each in one WebGLBuffer.
     * @return {void}
     * @memberOf Map
     */
    insertFeature(id, properties, triangles, borders, points) {
        for (const aes of this.aesthetics)
            if (aes.checkProperty(properties[aes.getAttr()]))
                aes.addFeature(id, properties, triangles, borders, points);
        /* for (let i = 0; i < this.aesthetics.length; i++)
             if (this.aesthetics[i].checkProperty(properties[this.aesthetics[i].getAttr()]))
                 this.aesthetics[i].addFeature(id, properties, triangles, borders, points);*/
    }

    /**
     * M18) Returns an array of Aesthetic ids that tells us the objects where the Feature belongs.
     * @param {JSON} properties - The Feature properties
     * @returns {Array<number>} - The Aesthetic ids where the Feature belongs.
     * @memberOf Map
     */
    fitFeature(properties) {
        const res = [];
        for (let i = 0; i < this.aesthetics.length; i++)
            if (this.aesthetics[i].checkProperty(properties[this.aesthetics[i].getAttr()]))
                res.push(i);
        return res;
    }

    /**
     * M19) Receives the dataset as parameter. This dataset in each row contains geometry and associated properties, then it creates one tree
     * either for points or polygons. This tree can be k-d Treee or RBush(RTree)
     * @param {JSON} geojson 
     * @return {void}
     * @see Diogo's thesis page 62
     * @memberOf Map
     */
    buildTrees(geojson) {
        const gl = this._webgl.gl;
        if (this.tempAestheticPoints.length > 0) {
            for (let i = 0; i < this.tempAestheticPoints.length; i++) {
                if (this.tempAestheticPoints[i].length > 0) {
                    const vertArray = new Float32Array(this.tempAestheticPoints[i]);
                    const bufferP = [];
                    bufferP.push(gl.createBuffer());

                    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(i, [], [], bufferP);
                }
            }
        }
        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)
            this.rtree = new PolygonLookup(geojson);
    }

    /**
     * M20) Similar to insertFeature, in this case inserts a group of Features like it was only one.
     * It creates one WebGLBuffer with all the points instead of one WebGLBuffer per point.
     * This method should only be used when we already grouped the Features by Aesthetic class (fitFeature() method).
     * This method exists to provide one alternative less expensive in terms of memory for low end devices.
     * Used only on Dot Map because on those we can easily end up with millions of different Features.
     * @param {number} id - Aesthethics id. 
     * @param {{itemSize: number, numItems: number}} triangles - Not used ?
     * @param {{itemSize: number, numItems: number}} borders - Not used ?
     * @param {{itemSize: number, numItems: number}} points - All the points for the Aesthethic object in one WebGLBuffer.
     * @memberOf Map
     */
    insertGroupedFeature(id, triangles, borders, points) {
        this.aesthetics[id].addGroupedFeature(null, triangles, borders, points);
    }

    /** ########################    LEGEND METHODS    ######################## */
    /**
     * M17) Creates a Legend element suitable for polygons based on the Aesthethic objects.
     * Should be overriden by subclasses.
     * @return {void}
     * @abstract
     * @memberOf Map
     */
    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.width = 250;
        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');
        table.style.zIndex = "2000";
        thcolor.style.width = 100;
        table.appendChild(thcolor);
        table.appendChild(thvalue);

        for (const currentaes of this.aesthetics) {
            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]})`;
            colorDiv.style['backgroundColor'] = rgbc;
            colorDiv.style.height = 25;
            colorDiv.style.width = 80;
            color.appendChild(colorDiv);

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

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

    /** ########################    WEBGL METHODS    ######################## */
    /**
     * M22) Clear current buffers to preset values.
     * @see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/clear
     * @memberOf Map
     */
    clear() {
        const gl = this._webgl.gl;
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.disable(gl.DEPTH_TEST);
    }

    /**
     * M23) Receiving an Aesthetics object, draws the triangles contained in it's Features,
     * using the color that the Aesthethics object has.
     * @param {Aesthetic} aes - The Aesthetic object.
     * @returns {void}
     * @see Diogo's thesis Page 58/59
     * @memberOf Map
     */
    drawTriangles(aes) {
        const gl = this._webgl.gl;
        if (gl === null)
            return;

        const fsize = Float32Array.BYTES_PER_ELEMENT;
        const currentZoom = this.bGMap.getZoom();
        const pointSize = Math.max(currentZoom - 5.0, 1.0);
        const vertexCoordsLocation = gl.getAttribLocation(this._webgl.program, 'coords');
        const vertexSizeLocation = gl.getAttribLocation(this._webgl.program, 'aPointSize');
        const fragmentColorLocation = gl.getUniformLocation(this._webgl.program, "u_color");
        const isPointLocation = gl.getUniformLocation(this._webgl.program, 'isPoint');
        let [r, g, b, a] = aes.getFillColor();

        this.setMatrices(gl);
        gl.vertexAttrib1f(vertexSizeLocation, pointSize);
        gl.uniform1f(isPointLocation, 0.0);
        gl.uniform4f(fragmentColorLocation, r / 255, g / 255, b / 255, a);// Draw Polygons' Interior

        let features = aes.getFeatures();
        for (const f of features) { // Draw Polygons' Interior
            let triangles = f.getTriangles();
            for (const t of triangles) {
                gl.bindBuffer(gl.ARRAY_BUFFER, t);
                gl.enableVertexAttribArray(vertexCoordsLocation);
                gl.vertexAttribPointer(vertexCoordsLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                gl.drawArrays(gl.TRIANGLES, 0, t.numItems);
            }
        }
    }

    /**
     * M24) Receiving an Aesthetics object, draws the borders contained in it's Features, 
     * aplying the color specified in the Aeshteic object for the line color(Aeshteic.strokeColor).
     * @param {Aesthetic} aes - The Aesthetic object.
     * @returns {void}
     * @memberOf Map
     */
    drawBorders(aes) {
        const gl = this._webgl.gl;
        if (gl === null)
            return;

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

        const fsize = Float32Array.BYTES_PER_ELEMENT;
        const currentZoom = this.bGMap.getZoom();
        const pointSize = Math.max(currentZoom - 5.0, 1.0);
        const vertexCoordsLocation = gl.getAttribLocation(this._webgl.program, 'coords');
        const vertexSizeLocation = gl.getAttribLocation(this._webgl.program, 'aPointSize');
        const fragmentColorLocation = gl.getUniformLocation(this._webgl.program, "u_color");
        const isPointLocation = gl.getUniformLocation(this._webgl.program, 'isPoint');
        let [r, g, b, a] = aes.getStrokeColor();

        this.setMatrices(gl);
        gl.vertexAttrib1f(vertexSizeLocation, pointSize);
        gl.uniform1f(isPointLocation, 0.0);
        gl.uniform4f(fragmentColorLocation, r / 255, g / 255, b / 255, a);

        let features = aes.getFeatures();
        for (const f of features) {
            let borders = f.getBorders();
            for (const b of borders) {
                gl.bindBuffer(gl.ARRAY_BUFFER, b);
                gl.enableVertexAttribArray(vertexCoordsLocation);
                gl.vertexAttribPointer(vertexCoordsLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                gl.drawArrays(gl.LINE_LOOP, 0, b.numItems);
            }
        }
    }

    /**
     * M26) Receiving an Aesthetics object, draws the points contained in it's Features, 
     * aplying the values specified in that Aesthetic object visual variables (color and size).
     * @param {Aesthetic} aes - The Aesthetic object.
     * @returns {void}
     * @memberOf Map
     */
    drawPoints(aes) {
        const gl = this._webgl.gl;
        if (gl === null)
            return;

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

        const fsize = Float32Array.BYTES_PER_ELEMENT;
        const currentZoom = this.bGMap.getZoom();
        const pointSize = Math.max(currentZoom - 4.0 + aes.getPointSize(), aes.getPointSize());
        const vertexCoordsLocation = gl.getAttribLocation(this._webgl.program, 'coords');
        const vertexSizeLocation = gl.getAttribLocation(this._webgl.program, 'aPointSize');
        const fragmentColorLocation = gl.getUniformLocation(this._webgl.program, "u_color");
        const isPointLocation = gl.getUniformLocation(this._webgl.program, 'isPoint');
        let [r, g, b, a] = aes.getFillColor();

        this.setMatrices(gl); //Set M1, M2 and M3
        gl.vertexAttrib1f(vertexSizeLocation, pointSize);
        gl.uniform1f(isPointLocation, 1.0);
        gl.uniform4f(fragmentColorLocation, r / 255, g / 255, b / 255, a);

        let features = aes.getFeatures();
        let allFeatures = aes.getAllFeatures();
        if (this.gisplayOptions.isDynamic && aes.getFeatures().length > 0) {
            for (const f of features) {
                let points = f.getPoints();
                for (const p of points) {
                    gl.bindBuffer(gl.ARRAY_BUFFER, p);
                    gl.enableVertexAttribArray(vertexCoordsLocation);
                    gl.vertexAttribPointer(vertexCoordsLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                    gl.drawArrays(gl.POINTS, 0, p.numItems);
                }
            }
        }
        else if (allFeatures !== null && !this.gisplayOptions.isDynamic) {
            for (const allF of allFeatures) {
                let points = allF.getPoints();
                for (const p of points) {
                    gl.bindBuffer(gl.ARRAY_BUFFER, p);
                    gl.enableVertexAttribArray(vertexCoordsLocation);
                    gl.vertexAttribPointer(vertexCoordsLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                    gl.drawArrays(gl.POINTS, 0, p.numItems);
                }
            }
        }
    }

    /**
     * M27) Receiving an Aesthetics object, draws the points contained in it's Features, 
     * aplying the color specified in the Aeshteic object for the line color(Aeshteic.strokeColor) 
     * and the size of the point is based on the attribute value and the specified limits.
     * @param {Aesthetic} aes - The Aesthetic object. 
     * @returns {void} 
     * @memberOf Map
     */
    drawProportionalPoints(aes) {
        const gl = this._webgl.gl;
        if (gl === null)
            return;

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

        const currentZoom = this.bGMap.getZoom();
        const vertexCoordsLocation = gl.getAttribLocation(this._webgl.program, 'coords');
        const vertexSizeLocation = gl.getAttribLocation(this._webgl.program, 'aPointSize');
        const fragmentColorLocation = gl.getUniformLocation(this._webgl.program, "u_color");
        const isPointLocation = gl.getUniformLocation(this._webgl.program, 'isPoint');
        let [r, g, b] = aes.getFillColor();

        this.setMatrices(gl);
        gl.uniform1f(isPointLocation, 1.0);
        gl.uniform4f(fragmentColorLocation, r / 255, g / 255, b / 255, this.gisplayOptions.alpha); //TODO: Para 2.5D usar alpha a 0?

        const fsize = Float32Array.BYTES_PER_ELEMENT;
        let opts = this.gisplayOptions;
        if (this.gisplayOptions.isDynamic) {
            let features = aes.getFeatures();
            for (const f of features) {
                const propvalue = parseFloat(f.getProperties()[opts.attr]);
                const temppointsize = ((opts.maxPointSize - opts.minPointSize) / (this.max - this.min)) * (propvalue - this.min);
                const pointSize = Math.max(currentZoom - 4.0 + temppointsize * currentZoom / 4, 2);

                let points = f.getPoints();
                for (let p of points) {
                    gl.bindBuffer(gl.ARRAY_BUFFER, p);
                    gl.vertexAttrib1f(vertexSizeLocation, pointSize);
                    gl.enableVertexAttribArray(vertexCoordsLocation);
                    gl.vertexAttribPointer(vertexCoordsLocation, 2, gl.FLOAT, false, fsize * 2, 0);
                    gl.drawArrays(gl.POINTS, 0, p.numItems);
                }
            }
        }
    }

    /**
     * This method will set all matrices needed to compute each point/vertex position. This will use the 
     * Before sending the matrix to webGL we change the last column transformation in X and Y to the last row X and Y 
     * because WebGL is column major. See MMatrix[6] =...
     * @memberOf Map
     */
    setMatrices(gl) {
        let lngCenter = this.bGMap.getCenterLng();
        let latCenter = this.bGMap.getCenterLat();
        let zoom = this.bGMap.getZoom();
        let tileSize = this.gisplayOptions.tileSize;
        let width = this.bGMap.getWidth();
        let height = this.bGMap.getHeight();
        let mercator = WebGLUtils.webMercatorProjection(lngCenter, latCenter, zoom, tileSize, width, height);

        let MMatrix = WebGLUtils.finalMatrix(mercator.scale, width, height, mercator.offsetX, mercator.offsetY);
        const Mloc = gl.getUniformLocation(this._webgl.program, 'M')
        gl.uniformMatrix3fv(Mloc, false, MMatrix);
    }

    /** ########################    ABSTRACT METHODS    ######################## */
    /**
     * M21) Draw map function. Must be overriden by subclasses.
     * @abstract 
     */
    draw() {
        throw new Error("Draw must be implemented by subclass.");
    }

    /**
     * M10) Defaults for each map. Subclasses should override this method.
     * @abstract 
     * @memberOf Map
     */
    defaults() {
        throw new Error("This map has no defaults defined.");
    }

    /**
     * This function should be implemented by any subclass that wants to use another way of processing data.
     * By default it loads GeoJSON but if the user wants to load another type of data it should do so by implementing this method.
     * A function similar to loadGeoJSON should also be implemented
     * @param {Object|JSON} data - Dataset to be used. 
     * TODO: Add static to this method.
     * @abstract
     */
    processData(data) {
        this.loadGeoJSON(geojson);
    }

    /**
     * Returns the colors for this map given the number of classes and the nature of the data (sequential, diverging or qualitative). 
     * @param {number} numClasses - Number of classes. 
     * @param {string} dataNature - Nature of the data.
     * @returns {Array<Array<RGB>>} Default colors for the map given the number of classes and nature of data.
     * @abstract 
     * @memberOf Map
     */
    getDefaultColors(numClasses, dataNature) {
    }
}