var inherit = require('./inherit');
var feature = require('./feature');
var transform = require('./transform');
var util = require('./util');
/**
* Heatmap feature specification.
*
* @typedef {geo.feature.spec} geo.heatmapFeature.spec
* @extends geo.feature.spec
* @property {geo.geoPosition|function} [position] Position of the data.
* Default is (data).
* @property {function} [intensity] Scalar value of each data point. The
* scalar value must be a positive real number and is used to compute the
* weight for each data point.
* @property {number} [maxIntensity=null] Maximum intensity of the data.
* Maximum intensity must be a positive real number and is used to normalize
* all intensities within a dataset. If `null`, it is computed.
* @property {number} [minIntensity=null] Minimum intensity of the data.
* Minimum intensity must be a positive real number and is used to normalize
* all intensities within a dataset. If `null`, it is computed.
* @property {number} [updateDelay=-1] Delay in milliseconds after a zoom,
* rotate, or pan event before recomputing the heatmap. If 0, this is double
* the last render time. If negative, it is roughly the last render time
* plus the absolute value of the specified number of refresh intervals.
* compute a delay based on the last heatmap render time.
* @property {boolean|number|'auto'} [binned='auto'] If `true` or a number,
* spatially bin data as part of producing the heatmap. If falsy, each
* datapoint stands on its own. If `'auto'`, bin data if there are more data
* points than there would be bins. Using `true` or `auto` uses bins that
* are `max(Math.floor((radius + blurRadius) / 8), 3)`.
* @property {object} [style.color] An object where the keys are numbers from
* [0-1] and the values are {@link geo.geoColor}. This is used to transform
* normalized intensity.
* @property {number} [style.radius=10] Radius of a point in pixels.
* @property {number} [style.blurRadius=10] Blur radius for each point in
* pixels.
* @property {boolean} [style.gaussian=true] If truthy, approximate a gaussian
* distribution for each point using a multi-segment linear radial
* approximation. The total weight of the gaussian area is approximately the
* `9/16 r^2`. The sum of `radius + blurRadius` is used as the radius for
* the gaussian distribution.
* @property {boolean} [scaleWithZoom=false] If truthy, the value for radius
* and blurRadius scale with zoom. In this case, the values for radius and
* blurRadius are the values at zoom-level zero. If the scaled radius is
* less than 0.5 or more than 8192 screen pixels, the heatmap will not
* render.
*/
/**
* Create a new instance of class heatmapFeature.
*
* @class
* @alias geo.heatmapFeature
* @param {geo.heatmapFeature.spec} arg Feature specification.
* @extends geo.feature
* @returns {geo.heatmapFeature}
*/
var heatmapFeature = function (arg) {
'use strict';
if (!(this instanceof heatmapFeature)) {
return new heatmapFeature(arg);
}
arg = arg || {};
feature.call(this, arg);
/**
* @private
*/
var m_this = this,
m_position,
m_intensity,
m_maxIntensity,
m_minIntensity,
m_updateDelay,
m_binned,
m_gcsPosition,
s_init = this._init;
this.featureType = 'heatmap';
m_position = arg.position || util.identityFunction;
m_intensity = arg.intensity || function (d) { return 1; };
m_maxIntensity = arg.maxIntensity !== undefined ? arg.maxIntensity : null;
m_minIntensity = arg.minIntensity !== undefined ? arg.minIntensity : null;
m_binned = arg.binned !== undefined ? arg.binned : 'auto';
m_updateDelay = (arg.updateDelay || arg.updateDelay === 0) ? parseInt(arg.updateDelay, 10) : -1;
/**
* Get/Set maxIntensity.
*
* @param {number|null} [val] If not specified, return the current value.
* If a number, use this as the maximum intensity. If `null`, compute
* the maximum intensity.
* @returns {number|null|this}
*/
this.maxIntensity = function (val) {
if (val === undefined) {
return m_maxIntensity;
} else {
m_maxIntensity = val;
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Get/Set minIntensity.
*
* @param {number|null} [val] If not specified, return the current value.
* If a number, use this as the minimum intensity. If `null`, compute
* the minimum intensity.
* @returns {number|null|this}
*/
this.minIntensity = function (val) {
if (val === undefined) {
return m_minIntensity;
} else {
m_minIntensity = val;
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Get/Set updateDelay.
*
* @param {number} [val] If not specified, return the current update delay.
* If specified, this is the delay in milliseconds after a zoom, rotate,
* or pan event before recomputing the heatmap. If 0, this is double the
* last render time. If negative, it is roughly the last render time plus
* the absolute value of the specified number of refresh intervals.
* @returns {number|this}
*/
this.updateDelay = function (val) {
if (val === undefined) {
return m_updateDelay;
} else {
m_updateDelay = parseInt(val, 10);
}
return m_this;
};
/**
* Get/Set binned value.
*
* @param {boolean|number|'auto'} [val] If not specified, return the current
* binned value. If `true` or a number, spatially bin data as part of
* producing the heatmap. If falsy, each datapoint stands on its own.
* If `'auto'`, bin data if there are more data points than there would be
* bins. Using `true` or `auto` uses bins that are
* `max(Math.floor((radius + blurRadius) / 8), 3)`.
* @returns {boolean|number|'auto'|this}
*/
this.binned = function (val) {
if (val === undefined) {
return m_binned;
} else {
if (val === 'true') {
val = true;
} else if (val === 'false') {
val = false;
} else if (val !== 'auto' && val !== true && val !== false) {
val = parseInt(val, 10);
if (val <= 0 || isNaN(val)) {
val = false;
}
}
m_binned = val;
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Get/Set position accessor.
*
* @param {geo.geoPosition|function} [val] If not specified, return the
* current position accessor. If specified, use this for the position
* accessor and return `this`. If a function is given, this is called
* with `(dataElement, dataIndex)`.
* @returns {geo.geoPosition|function|this} The current position or this
* feature.
*/
this.position = function (val) {
if (val === undefined) {
return m_position;
} else {
m_position = val;
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Get pre-computed gcs position accessor.
*
* @returns {geo.heatmap}
*/
this.gcsPosition = function () {
m_this._update();
return m_gcsPosition;
};
/**
* Get/Set intensity.
*
* @param {function} [val] If not specified, the current intensity accessor.
* Otherwise, a function that returns the intensity of each data point.
* @returns {function|this}
*/
this.intensity = function (val) {
if (val === undefined) {
return m_intensity;
} else {
m_intensity = val;
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Initialize.
*
* @param {geo.heatmapFeature.spec} arg
*/
this._init = function (arg) {
s_init.call(m_this, arg);
var defaultStyle = Object.assign(
{},
{
radius: 10,
blurRadius: 10,
gaussian: true,
color: {
0: {r: 0, g: 0, b: 0.0, a: 0.0},
0.25: {r: 0, g: 0, b: 1, a: 0.5},
0.5: {r: 0, g: 1, b: 1, a: 0.6},
0.75: {r: 1, g: 1, b: 0, a: 0.7},
1: {r: 1, g: 0, b: 0, a: 0.8}},
scaleWithZoom: false
},
arg.style === undefined ? {} : arg.style
);
m_this.style(defaultStyle);
if (m_position) {
m_this.dataTime().modified();
}
};
/**
* Build the feature.
*
* @returns {this}
*/
this._build = function () {
var data = m_this.data(),
intensity = null,
position = [],
setMax = (m_maxIntensity === null || m_maxIntensity === undefined),
setMin = (m_minIntensity === null || m_minIntensity === undefined);
data.forEach(function (d, i) {
position.push(m_this.position()(d, i));
if (setMax || setMin) {
intensity = m_this.intensity()(d, i);
if (m_maxIntensity === null || m_maxIntensity === undefined) {
m_maxIntensity = intensity;
}
if (m_minIntensity === null || m_minIntensity === undefined) {
m_minIntensity = intensity;
}
if (setMax && intensity > m_maxIntensity) {
m_maxIntensity = intensity;
}
if (setMin && intensity < m_minIntensity) {
m_minIntensity = intensity;
}
}
});
if (setMin && setMax && m_minIntensity === m_maxIntensity) {
m_minIntensity -= 1;
}
m_gcsPosition = transform.transformCoordinates(
m_this.gcs(), m_this.layer().map().gcs(), position);
m_this.buildTime().modified();
return m_this;
};
this._init(arg);
return this;
};
inherit(heatmapFeature, feature);
module.exports = heatmapFeature;