UNPKG

@carto/airship-bridge

Version:

Airship bridge to other libs (CARTO VL, CARTO.js)

347 lines (346 loc) 15.4 kB
import semver from 'semver'; import { getColumnName, getExpression } from '../util/Utils'; import { AnimationControls } from './animation-controls/AnimationControls'; import { CategoryFilter } from './category/CategoryFilter'; import { CategoricalHistogramFilter } from './histogram/CategoricalHistogramFilter'; import { NumericalHistogramFilter } from './histogram/NumericalHistogramFilter'; import { GlobalRangeFilter } from './range/GlobalRangeFilter'; import { TimeSeries } from './time-series/TimeSeries'; var VL_VERSION = '^1.2.3'; /** * This class is the main interface to bind a VL layer to one or more Airship widgets. * * The normal usage is create an instance and use its public methods to generate filters for * different widgets. * * After you have specified all the required filters, simply call the method `build` and all will be * handled for you. Internally, a new layer will be created with an invisible Viz, as a source for all * the widget's data. * * Some caveats: * - You can create as many filters for a column you want, but only one per widget. * - If you enable non-read-only capabilities, it is recommended that the Viz filter property not to * be changed, as it will be changed internally by each filter. * * @export * @class VLBridge */ var VLBridge = /** @class */ (function () { /** * Creates an instance of VLBridge. * @param {VLBridgeOptions} { carto, map, layer, source } * - carto: CARTO VL namespace * - map: CARTO VL map instance (Mapbox GL) * - layer: CARTO VL layer * - source: CARTO VL source * @memberof VLBridge */ function VLBridge(_a) { var carto = _a.carto, map = _a.map, layer = _a.layer, source = _a.source; this._vizFilters = []; this._carto = carto; this._map = map; this._layer = layer; this._source = source; this._id = this._layer.id; this._rebuildFilters = this._rebuildFilters.bind(this); this._updateDataLayerVariables = this._updateDataLayerVariables.bind(this); if (!this._carto.expressions.globalHistogram) { throw new Error("Provided VL version " + this._carto.version + " lacks globalHistogram support."); } if (!semver.satisfies(this._carto.version, VL_VERSION)) { throw new Error("Provided VL version " + this._carto.version + " not supported. Must satisfy " + VL_VERSION); } } /** * Create a numerical histogram filter. See {@link NumericalHistogramOptions} for more details * * @param {(any | string)} widget Your widget or a * selector to locate it * @param {string} column The column you want to pull data from * @param {NumericalHistogramOptions} [options={}] Options for the bridge * @returns {NumericalHistogramFilter} * @memberof VLBridge */ VLBridge.prototype.numericalHistogram = function (widget, column, options) { if (options === void 0) { options = {}; } var buckets = options.buckets, bucketRanges = options.bucketRanges, weight = options.weight, readOnly = options.readOnly, totals = options.totals; var colName = getColumnName(column); var expression = getExpression(column); var histogram = new NumericalHistogramFilter(this._carto, this._layer, widget, colName, buckets, weight, this._source, bucketRanges, readOnly, totals, expression); this._addFilter(histogram); return histogram; }; /** * Create a categorical histogram filter. See {@link CategoricalHistogramOptions} for more details * * @param {(any | string)} widget Your widget or a selector to locate it * @param {string} column The column to pull data from * @param {CategoricalHistogramOptions} [options={}] Options for this particular filter * @returns {CategoricalHistogramFilter} * @memberof VLBridge */ VLBridge.prototype.categoricalHistogram = function (widget, column, options) { if (options === void 0) { options = {}; } var readOnly = options.readOnly, totals = options.totals, weight = options.weight; var colName = getColumnName(column); var expression = getExpression(column); var histogram = new CategoricalHistogramFilter(this._carto, this._layer, widget, colName, this._source, readOnly, weight, totals, expression); this._addFilter(histogram); return histogram; }; /** * Creates a numerical or categorical histogram, depending on the arguments. * * If neither buckets or bucketRanges are provided, a categorical one will be created. A numerical one otherwise * * @param {(any | string)} widget Your widget or a selector * @param {string} column The column to pull data from * @param {NumericalHistogramOptions} options Options for the Histogram * @returns {NumericalHistogramFilter | CategoricalHistogramFilter} * @memberof VLBridge */ VLBridge.prototype.histogram = function (widget, column, options) { if (options === void 0) { options = {}; } var buckets = options.buckets, bucketRanges = options.bucketRanges, readOnly = options.readOnly, weight = options.weight, totals = options.totals; if (buckets === undefined && bucketRanges === undefined) { var histogramWidget = widget; return this.categoricalHistogram(histogramWidget, column, { readOnly: readOnly, totals: totals, weight: weight }); } return this.numericalHistogram(widget, column, { bucketRanges: bucketRanges, buckets: buckets, readOnly: readOnly, totals: totals, weight: weight }); }; /** * Creates a category widget filter. * * You can provide an HTML element (like a button) on the options, so the filtering takes place * after the user clicks on it. This option is ignored if readOnly * * @param {any | string} widget An airship category widget, or a selector * @param {string} column Column to pull data from * @param {CategoryOptions} [options={}] * @returns * @memberof VLBridge */ VLBridge.prototype.category = function (widget, column, options) { if (options === void 0) { options = {}; } var readOnly = options.readOnly, button = options.button, weight = options.weight; var colName = getColumnName(column); var expression = getExpression(column); var category = new CategoryFilter(this._carto, this._layer, widget, colName, this._source, weight, readOnly, button, expression); this._addFilter(category); return category; }; /** * Creates a time series widget filter. * * Internally this creates a {@link NumericalHistogramFilter} and instances a {@link TimeSeries}. * * One will take care of the histogram part and the other of the animation parts. * * There can only be one animation per layer (per VLBridge instance) * * @param {(any | string)} widget The Time series widget, or a selector * @param {string} column The string to pull data from it * @param {AnimationOptions} [options={}] * @memberof VLBridge */ VLBridge.prototype.timeSeries = function (widget, column, options) { var _this = this; if (options === void 0) { options = {}; } if (this._animation) { throw new Error('There can only be one animation'); } var buckets = options.buckets, bucketRanges = options.bucketRanges, readOnly = options.readOnly, totals = options.totals, weight = options.weight, duration = options.duration, fade = options.fade, variableName = options.variableName, propertyName = options.propertyName, autoplay = options.autoplay; this._animation = new TimeSeries(this._carto, this._layer, column, widget, function () { if (propertyName === 'filter') { _this._rebuildFilters(); } }, duration, fade, variableName, propertyName, autoplay); var histogram = this.numericalHistogram(widget, column, { bucketRanges: bucketRanges, buckets: buckets, readOnly: readOnly, totals: totals, weight: weight }); histogram.setTimeSeries(true); histogram.on('rangeChanged', function (range) { _this._animation.setRange(range); }); return this._animation; }; /** * Creates an animation controls widget * * By default, the animation is set to the 'filter' property, * but it is possible to animate any style property using the 'propertyName' option. * * There can only be one animation per layer (per VLBridge instance) * * @param {(any | string)} widget The Animation Controls widget, or a selector * @param {string} column The name of the column in the dataset used in the animation * @param {AnimationControlsOptions} [options={}] * @param {number} [options.duration] Animation duration in seconds. It is 10 by default * @param {[number, number]} [options.fade] Animation fade in and out. * @param {string} [options.variableName] Name of the viz variable that has the animation assigned * @param {string} [options.propertyName] Name of the style property to apply the animation, 'filter' by default. * @memberof VLBridge */ VLBridge.prototype.animationControls = function (widget, column, options) { var _this = this; if (options === void 0) { options = {}; } var duration = options.duration, fade = options.fade, variableName = options.variableName, _a = options.propertyName, propertyName = _a === void 0 ? 'filter' : _a, autoplay = options.autoplay; this._animation = new AnimationControls(widget, this._carto, column, variableName, propertyName, duration, fade, autoplay, this._layer, function () { if (propertyName === 'filter') { _this._rebuildFilters(); } }, null); return this._animation; }; /** * Creates a global range slider filter. * * @param {(any | string)} widget A range slider widget or a selector * @param {string} column The column to pull data from * @returns {GlobalRangeFilter} * @memberof VLBridge */ VLBridge.prototype.globalRange = function (widget, column) { var range = new GlobalRangeFilter(this._carto, this._layer, widget, column, this._source); this._addFilter(range); return range; }; /** * Call this method after creating all the different filters you require. * * It will internally do the following: * - Add new variables to your Viz, with the columns of all the non-read-only filters * - Create a new layer as the filters' data source * @returns * @memberof VLBridge */ VLBridge.prototype.build = function () { var _this = this; if (this._vizFilters.length === 0) { return; } var onLoaded = function () { _this._appendVariables(); _this._buildDataLayer(); }; if (!this._layer.viz) { this._layer.on('loaded', onLoaded); } else { onLoaded(); } }; VLBridge.prototype._addFilter = function (filter) { filter.on('filterChanged', this._rebuildFilters); filter.on('expressionReady', this._updateDataLayerVariables); this._vizFilters.push(filter); }; /** * This will append extra variables with all the columns of non-read-only filters. * * This is required so that whenever the filter is changed, the original viz layer * can be filtered by it. * * @private * @memberof VLBridge */ VLBridge.prototype._appendVariables = function () { var _this = this; var s = this._carto.expressions; this._vizFilters.forEach(function (filter) { return _this._layer.viz.variables[filter.columnPropName] = s.prop(filter.column); }); }; /** * This will create a new Layer using the same source as the original, add it to the map, and * pass it to all the filters so they can hook up to read the data * * It has style properties to make it invisible, plus all the expressions created by each filter. * * @private * @memberof VLBridge */ VLBridge.prototype._buildDataLayer = function () { var _this = this; var variables = this._getVariables(); var s = this._carto.expressions; var viz = new this._carto.Viz({ color: s.rgba(0, 0, 0, 0), strokeWidth: 0, variables: variables, }); this._readOnlyLayer = new this._carto.Layer("asbind_ro_" + this._id, this._source, viz); this._readOnlyLayer.addTo(this._map); this._vizFilters.forEach(function (filter) { return filter.setDataLayer(_this._readOnlyLayer); }); }; VLBridge.prototype._getVariables = function () { var variables = this._readOnlyLayer !== undefined ? this._readOnlyLayer.viz.variables : {}; for (var _i = 0, _a = this._vizFilters; _i < _a.length; _i++) { var filter = _a[_i]; var name_1 = filter.name; if (filter.globalExpression) { variables[name_1 + "_global"] = filter.globalExpression; } if (filter.expression) { variables[name_1] = filter.expression; } variables[filter.columnPropName] = this._carto.expressions.prop(filter.column); } return variables; }; VLBridge.prototype._updateDataLayerVariables = function (payload) { if (!this._readOnlyLayer.viz) { return; } this._readOnlyLayer.viz.variables[payload.name] = payload.expression; }; /** * Gather all the VL filters from each filter, combine them and filter both the data layer and * the original layer with it. * * If there is an animation involved, it uses @animation and all the filters. * * @private * @memberof VLBridge */ VLBridge.prototype._rebuildFilters = function () { var newFilter = this._combineFilters(this._vizFilters .filter(function (hasFilter) { return hasFilter.filter !== null; }) .map(function (hasFilter) { return hasFilter.filter; })); // Update (if required) the readonly layer if (this._readOnlyLayer) { this._readOnlyLayer.viz.filter.blendTo(newFilter, 0); } if (this._layer.viz.filter.isAnimated() && this._animation) { if (this._layer.viz.variables[this._animation.variableName]) { newFilter = "@" + this._animation.variableName + " and " + newFilter; } } // Update the Visualization filter this._layer.viz.filter.blendTo(newFilter, 0); if (this._animation) { this._animation.restart(); } }; VLBridge.prototype._combineFilters = function (filters) { if (filters.length === 0) { return '1'; } return filters.join(' and '); }; return VLBridge; }()); export default VLBridge;