goog.provide('M.style.Choropleth'); goog.require('M.Style'); goog.require('M.style.quantification'); /** * @namespace M.style.Choropleth */ (function() { /** * @classdesc * Main constructor of the class. Creates a style choropleth * with parameters specified by the user * * @constructor * @extends {M.Style} * @param {String} * @param {Array<Style>} * @param {M.style.quantification} * @param {object} * @api stable */ M.style.Choropleth = (function(attributeName, styles, quantification = M.style.quantification.JENKS(), options = {}) { if (M.utils.isNullOrEmpty(attributeName)) { M.exception("No se ha especificado el nombre del atributo."); } /** * TODO * @public * @type {String} * @api stable * @expose */ this.attributeName_ = attributeName; /** * @public * @type {Array<M.style.Simple>} * @api stable * @expose */ this.styles_ = styles; /** * @public * @type {M.quantification|function} * @api stable * @expose */ this.quantification_ = quantification; /** * @public * @type {Array<Number>} * @api stable * @expose */ this.dataValues_ = []; /** * @public * @type{Array<Number>} * @api stable * @expose */ this.breakPoints_ = []; goog.base(this, options, {}); }); goog.inherits(M.style.Choropleth, M.Style); /** * This function apply the style to specified layer * @function * @public * @param {M.Layer.Vector} layer - Layer where to apply choropleth style * @api stable */ M.style.Choropleth.prototype.apply = function(layer) { this.layer_ = layer; this.update_(); }; /** * This function return the attribute name defined by user * @function * @public * @return {String} attribute name of Style * @api stable */ M.style.Choropleth.prototype.getAttributeName = function() { return this.attributeName_; }; /** * This function set the attribute name defined by user * @function * @public * @param {String} attributeName - attribute name to set * @api stable */ M.style.Choropleth.prototype.setAttributeName = function(attributeName) { this.attributeName_ = attributeName; this.update_(); return this; }; /** * This function return quantification function defined by user * @function * @public * @return {M.style.quantification|function} quantification function of style * @api stable */ M.style.Choropleth.prototype.getQuantification = function() { return this.quantification_; }; /** * This function set quantification function defined by user * @function * @public * @param {M.style.quantification|function} quantification - quantification function of style * @api stable */ M.style.Choropleth.prototype.setQuantification = function(quantification) { this.quantification_ = quantification; if (!this.styles_.some(style => M.utils.isString(style))) { if (this.styles_.length < this.quantification_().length) { let [startStyle, endStyle] = this.styles_; let startColor = startStyle.get('fill.color'); let endColor = endStyle.get('fill.color'); if (M.utils.isNullOrEmpty(startColor)) { startColor = startStyle.get('stroke.color'); } if (M.utils.isNullOrEmpty(endColor)) { endColor = endStyle.get('stroke.color'); } this.styles_ = [startColor, endColor]; } else { this.styles_ = this.styles_.slice(0, this.quantification_().length); } this.update_(); } return this; }; /** * This function returns the styles defined by user * @function * @public * @return {Array(M.Style)|null} returns the styles defined by user * @api stable */ M.style.Choropleth.prototype.getStyles = function() { return this.styles_; }; /** * This function sets the styles defined by user * @function * @public * @param {Array<M.style.Point>|Array<M.style.Line>|Array<M.style.Polygon>} styles - styles defined by user * @api stable */ M.style.Choropleth.prototype.setStyles = function(styles) { if (!M.utils.isArray(styles)) { styles = [styles]; } this.styles_ = styles; this.update_(); return this; }; /** * TODO * * @public * @function * @api stable */ M.style.Choropleth.prototype.updateCanvas = function() { if (!M.utils.isNullOrEmpty(this.styles_)) { if (this.breakPoints_.length > 0) { let canvasImages = []; this.updateCanvasPromise_ = new Promise((success, fail) => this.loadCanvasImages_(0, canvasImages, success)); } } }; /** * TODO * * @function * @private * @param {CanvasRenderingContext2D} vectorContext - context of style canvas */ M.style.Choropleth.prototype.loadCanvasImages_ = function(currentIndex, canvasImages, callbackFn) { // base case if (currentIndex === this.styles_.length) { this.drawGeometryToCanvas(canvasImages, callbackFn); } // recursive case else { let startLimit = -1; if (currentIndex > 0) { startLimit = this.breakPoints_[currentIndex - 1]; } let endLimit = this.breakPoints_[currentIndex]; let image = new Image(); image.crossOrigin = 'Anonymous'; let scope_ = this; image.onload = function() { canvasImages.push({ 'image': this, 'startLimit': M.style.Choropleth.CALC_CANVAS_NUMBER_(startLimit), 'endLimit': M.style.Choropleth.CALC_CANVAS_NUMBER_(endLimit) }); scope_.loadCanvasImages_((currentIndex + 1), canvasImages, callbackFn); }; image.onerror = function() { canvasImages.push({ 'startLimit': M.style.Choropleth.CALC_CANVAS_NUMBER_(startLimit), 'endLimit': M.style.Choropleth.CALC_CANVAS_NUMBER_(endLimit) }); scope_.loadCanvasImages_((currentIndex + 1), canvasImages, callbackFn); }; this.styles_[currentIndex].updateCanvas(); image.src = this.styles_[currentIndex].toImage(); } }; /** * TODO * * @function * @public * @param {CanvasRenderingContext2D} vectorContext - context of style canvas * @api stable */ M.style.Choropleth.prototype.drawGeometryToCanvas = function(canvasImages, callbackFn) { let heights = canvasImages.map(canvasImage => canvasImage['image'].height); let widths = canvasImages.map(canvasImage => canvasImage['image'].width); let vectorContext = this.canvas_.getContext('2d'); vectorContext.canvas.height = heights.reduce((acc, h) => acc + h + 5); vectorContext.textBaseline = "middle"; let maxWidth = Math.max.apply(widths, widths); canvasImages.forEach((canvasImage, index) => { let image = canvasImage['image']; let startLimit = canvasImage['startLimit']; let endLimit = canvasImage['endLimit']; let coordinateY = 0; let prevHeights = heights.slice(0, index); if (!M.utils.isNullOrEmpty(prevHeights)) { coordinateY = prevHeights.reduce((acc, h) => acc + h + 5); coordinateY += 5; } let imageHeight = 0; if (!M.utils.isNullOrEmpty(image)) { imageHeight = image.height; vectorContext.drawImage(image, 0, coordinateY); } if (startLimit < 0) { vectorContext.fillText(` x <= ${endLimit}`, maxWidth + 5, coordinateY + (imageHeight / 2)); } else { vectorContext.fillText(`${startLimit} < x <= ${endLimit}`, maxWidth + 5, coordinateY + (imageHeight / 2)); } }, this); callbackFn(); }; /** * This function gets the numeric features values of layer which attribute * is equal to attribute specified by user * @function * @public * @return {Array<number>} numeric features values of layer * @api stable */ M.style.Choropleth.prototype.getValues = function() { let values = []; if (!M.utils.isNullOrEmpty(this.layer_)) { this.layer_.getFeatures().forEach(function(f) { try { let value = parseFloat(f.getAttribute(this.attributeName_)); if (!isNaN(value)) { values.push(value); } } catch (e) { // M.exception('TODO el atributo no es un número válido'); } }, this); } return values; }; /** * This function updates the style * @function * @private * @api stable */ M.style.Choropleth.prototype.update_ = function() { if (!M.utils.isNullOrEmpty(this.layer_)) { let features = this.layer_.getFeatures(); if (!M.utils.isNullOrEmpty(features)) { this.dataValues_ = this.getValues(); if (M.utils.isNullOrEmpty(this.styles_) || (!M.utils.isNullOrEmpty(this.styles_) && (M.utils.isString(this.styles_[0]) || M.utils.isString(this.styles_[1])))) { this.breakPoints_ = this.quantification_(this.dataValues_); let startColor = this.styles_ && this.styles_[0] ? this.styles_[0] : M.style.Choropleth.START_COLOR_DEFAULT; let endColor = this.styles_ && this.styles_[1] ? this.styles_[1] : M.style.Choropleth.END_COLOR_DEFAULT; let numColors = this.breakPoints_.length; let scaleColor = M.utils.generateColorScale(startColor, endColor, numColors); if (!M.utils.isArray(scaleColor)) { scaleColor = [scaleColor]; } let geometryType = M.utils.getGeometryType(this.layer_); const generateStyle = (scale, defaultStyle) => (scale.map(c => defaultStyle(c))); switch (geometryType) { case M.geom.geojson.type.POINT: case M.geom.geojson.type.MULTI_POINT: this.styles_ = generateStyle(scaleColor, M.style.Choropleth.DEFAULT_STYLE_POINT); break; case M.geom.geojson.type.LINE_STRING: case M.geom.geojson.type.MULTI_LINE_STRING: this.styles_ = generateStyle(scaleColor, M.style.Choropleth.DEFAULT_STYLE_LINE); break; case M.geom.geojson.type.POLYGON: case M.geom.geojson.type.MULTI_POLYGON: this.styles_ = generateStyle(scaleColor, M.style.Choropleth.DEFAULT_STYLE_POLYGON); break; default: return null; } } else { this.breakPoints_ = this.quantification_(this.dataValues_, this.styles_.length); } } for (let i = this.breakPoints_.length - 1; i > -1; i--) { let filterLTE = new M.filter.LTE(this.attributeName_, this.breakPoints_[i]); filterLTE.execute(features).forEach(f => f.setStyle(this.styles_[i])); } this.updateCanvas(); } }; /** * This functions returns a point style by default * @function * @public * @param {String} c - color in hexadecimal format * @return {M.style.Point} * @api stable */ M.style.Choropleth.DEFAULT_STYLE_POINT = function(c) { return new M.style.Point({ fill: { color: c, opacity: 1 }, stroke: { color: 'black', width: 1 }, radius: 5 }); }; /** * This functions returns a line style by default * @function * @public * @param {String} c - color in hexadecimal format * @return {M.style.Line} * @api stable */ M.style.Choropleth.DEFAULT_STYLE_LINE = function(c) { return new M.style.Line({ stroke: { color: c, width: 1 } }); }; /** * This functions returns a polygon style by default * @function * @public * @param {String} c - color in hexadecimal format * @return {M.style.Polygon} * @api stable */ M.style.Choropleth.DEFAULT_STYLE_POLYGON = function(c) { return new M.style.Polygon({ fill: { color: c, opacity: 1 }, stroke: { color: c, width: 1 } }); }; /** Color style by default * @constant * @api stable */ M.style.Choropleth.START_COLOR_DEFAULT = 'red'; /** * Color style by default * @constant * @api stable */ M.style.Choropleth.END_COLOR_DEFAULT = 'brown'; /** * Accuracy of numbers on canvas * @constant * @api stable */ M.style.Choropleth.ACCURACY_NUMBER_CANVAS = 2; /** * Returns the calculation of the numbers of the canvas * with a given precision * @function * @api stable */ M.style.Choropleth.CALC_CANVAS_NUMBER_ = function(number) { let powPrecision = Math.pow(10, M.style.Choropleth.ACCURACY_NUMBER_CANVAS); return Math.round(number * powPrecision) / powPrecision; }; })();