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;
};
})();