/**
* Main spot object.
*
* @class Spot
*/
var BaseModel = require('./util/base');
var Dataview = require('./dataview');
var Datasets = require('./dataset/collection');
var driverClient = require('./driver/client');
var driverServer = require('./driver/server');
var utildx = require('./util/crossfilter');
var timeUtil = require('./util/time');
var io = require('socket.io-client');
/**
* Connect to the spot-server using a websocket and setup callbacks
*
* @function
* @param {address} Optional. IP address and port number to connect to. fi. 'http://localhost:3000'
*
* @memberof! Spot
*/
function connectToServer (address) {
var me = this;
var socket;
if (address) {
// connect to specified address
// necessary for when window.location is not availble (node.js)
socket = io.connect(address);
} else {
// Use socket.io fallback to autodetect address
// ie. when a website wants to connect, use the window.location
socket = io.connect();
}
socket.on('connect', function () {
me.isConnected = true;
console.log('Connected to server');
});
socket.on('disconnect', function () {
me.isConnected = false;
});
socket.on('syncDatasets', function (req) {
// do an incremental update, as we typically start without datasets
me.datasets.add(req.data, { merge: true });
});
socket.on('syncDataview', function (req) {
me.dataview.reset(req.data);
});
socket.on('syncFacets', function (req) {
// do an incremental update, as we typically update only a few properties of a facet
// Also, a full reset will orphan the view.model objects in spot-app (ie. crashes)
var dataset = me.datasets.get(req.datasetId);
dataset.facets.add(req.data, { merge: true });
dataset.trigger('syncFacets');
});
socket.on('newData', function (req) {
var filter = me.dataview.filters.get(req.filterId);
if (req.data) {
filter.data = req.data;
// for text filters, rebuild partition and count
filter.partitions.forEach(function (partition, p) {
var columnToName = {1: 'a', 2: 'b', 3: 'c', 4: 'd'};
if (partition.isText) {
partition.groups.reset(null, {silent: true});
filter.data.forEach(function (d) {
var count = (parseFloat(d.aa) || parseInt(d.count)) || 0;
if (count) {
partition.groups.add({
min: 0,
max: 100,
count: count,
label: d[columnToName[(p + 1)]],
value: d[columnToName[(p + 1)]]
}, {silent: true});
}
});
partition.groups.sort();
}
});
filter.trigger('newData');
}
});
socket.on('newMetaData', function (req) {
me.dataview.dataTotal = parseInt(req.dataTotal);
me.dataview.dataSelected = parseInt(req.dataSelected);
me.dataview.trigger('newMetaData');
});
socket.connect();
me.socket = socket;
}
/**
* Disconnect from the spot-server
*
* @function
* @memberof! Spot
*/
function disconnectFromServer () {
this.socket.disconnect();
}
/**
* Request a list of available datasets from the server
*
* Depending on the driver, this can be an asyncrhonous function.
* It returns a Promise that resolves to the dataset collection
*
* @function
* @returns {Promise}
*
* @memberof! Spot
*/
function getDatasets () {
var me = this;
return new Promise(function (resolve, reject) {
me.socket.emit('getDatasets');
me.datasets.once('reset', function () {
resolve(me.datasets);
});
});
}
/**
* Reset min, max, and categories for all facets in the dataview
*
* @param {Spot} me Main spot instance
*
* @memberof! Spot
*/
function resetDataview (me) {
// rescan min/max values and categories for the newly added facets
me.dataview.facets.forEach(function (facet) {
var newFacet = me.dataview.facets.get(facet.name, 'name');
if (newFacet.isContinuous || newFacet.isDatetime || newFacet.isDuration) {
me.setFacetMinMax(facet);
} else if (newFacet.isCategorial) {
me.setFacetCategories(facet);
}
});
}
/*
* Add or remove facets from a dataset to the global (merged) dataset
*
* @memberof! Spot
* @param {Spot} me Main spot instance
* @param {Dataset} dataset Dataset set add or remove
*/
function toggleDatasetFacets (me, dataset) {
if (dataset.isActive) {
// remove active facets in dataset from the global dataset...
dataset.facets.forEach(function (facet) {
if (!facet.isActive) {
return;
}
// ...but only when no other active dataset contains it
var facetIsUnique = true;
me.datasets.forEach(function (otherDataset) {
if (!otherDataset.isActive || otherDataset === dataset) {
return;
}
if (otherDataset.facets.get(facet.name, 'name')) {
facetIsUnique = false;
}
});
if (facetIsUnique) {
var toRemove = me.dataview.facets.get(facet.name, 'name');
me.dataview.facets.remove(toRemove);
}
});
} else if (!dataset.isActive) {
// copy facets
dataset.facets.forEach(function (facet) {
// do nothing if facet is not active
if (!facet.isActive) {
return;
}
// default options for all facet types
var options = {
name: facet.name,
accessor: facet.name,
description: facet.description,
type: facet.transform.transformedType,
units: facet.units, // TODO: transformed units?
isActive: true
};
// do not add if a similar facet already exists
if (!me.dataview.facets.get(facet.name, 'name')) {
me.dataview.facets.add(options);
}
});
}
}
/*
* Add or remove data from a dataset to the global (merged) dataset
*
* @memberof! Spot
* @param {Spot} me Main spot instance
* @param {Dataset} dataset Dataset set add or remove
*/
function toggleDatasetData (me, dataset) {
if (dataset.isActive) {
// if dataset is active, remove it:
// ...clear all crossfilter filters
me.dataview.filters.forEach(function (filter) {
// BUGFIX: when loading sessions, the dataset is not initialized properly
// so check for it to be sure
if (filter.dimension) {
filter.dimension.filterAll();
}
});
// ...filter all data, originating from the dataset from the dataset
var dimension = me.dataview.crossfilter.dimension(function (d) {
return d._OriginalDatasetId;
});
dimension.filter(dataset.getId());
// ...remove matching data
me.dataview.crossfilter.remove();
// ...restore original filters
dimension.filterAll();
dimension.dispose();
me.dataview.filters.forEach(function (filter) {
filter.updateDataFilter();
});
} else if (!dataset.isActive) {
// if dataset is not active, add it
// ...find facets to copy
var dataTransforms = [];
dataset.facets.forEach(function (facet) {
// do nothing if facet is not active
if (!facet.isActive) {
return;
}
dataTransforms.push({
key: facet.name,
fn: utildx.valueFn(facet)
});
});
// ...transform data
var data = dataset.data;
var transformedData = [];
data.forEach(function (datum) {
var transformedDatum = {};
dataTransforms.forEach(function (transform) {
transformedDatum[transform.key] = transform.fn(datum);
});
transformedDatum._OriginalDatasetId = dataset.getId();
transformedData.push(transformedDatum);
});
// ...add to merged dataset
me.dataview.crossfilter.add(transformedData);
}
// update counts
me.dataview.dataTotal = me.dataview.crossfilter.size();
me.dataview.dataSelected = me.dataview.countGroup.value();
}
/**
* Add or remove a dataset from the dataview
* @param {Dataset} dataset Dataset set add or remove
*
* @function
* @memberof! Spot
*/
function toggleDataset (dataset) {
// Update the list of active datasets in the dataview
if (dataset.isActive) {
// remove datasetId
var i = this.dataview.datasetIds.indexOf(dataset.getId());
if (i > 0) {
this.dataview.datasetIds.splice(i, 1);
}
} else {
// add datasetId
this.dataview.datasetIds.push(dataset.getId());
}
if (this.sessionType === 'server') {
toggleDatasetFacets(this, dataset);
} else {
// release all filters
this.dataview.filters.forEach(function (filter) {
filter.releaseDataFilter();
});
// manually merge the datasets
toggleDatasetFacets(this, dataset);
toggleDatasetData(this, dataset);
}
dataset.isActive = !dataset.isActive;
resetDataview(this);
}
function setFacetMinMax (facet) {
// This should work for all kinds of facets:
// numbers, durations, and datatimes all implement the relevant operations
var datasets = this.datasets;
var first = true;
datasets.forEach(function (dataset) {
if (dataset.isActive) {
var subFacet = dataset.facets.get(facet.name, 'name');
if (first) {
facet.minvalAsText = subFacet.transform.transformedMinAsText;
facet.maxvalAsText = subFacet.transform.transformedMaxAsText;
first = false;
} else {
if (subFacet.minval < facet.minval) {
facet.minvalAsText = subFacet.transform.transformedMinAsText;
}
if (subFacet.maxval > facet.maxval) {
facet.maxvalAsText = subFacet.transform.transformedMaxAsText;
}
}
}
});
}
function setFacetCategories (facet) {
var datasets = this.datasets;
var categories = {};
// get categories by combining the sets for the separate datasets
datasets.forEach(function (dataset) {
if (dataset.isActive) {
var subFacet = dataset.facets.get(facet.name, 'name');
if (subFacet.isCategorial) {
subFacet.categorialTransform.rules.forEach(function (rule) {
categories[rule.expression] = rule.group;
});
} else if (subFacet.isDatetime) {
var groups = timeUtil.timeParts.get(subFacet.datetimeTransform.transformedFormat, 'description').groups;
groups.forEach(function (group) {
categories[group] = group;
});
} else {
console.error('Not implemented');
}
}
});
facet.categorialTransform.reset();
Object.keys(categories).forEach(function (cat) {
facet.categorialTransform.rules.add({
expression: cat,
count: 0, // FIXME
group: categories[cat]
});
});
}
module.exports = BaseModel.extend({
type: 'user',
props: {
/**
* Is there a connection with a spot sever?
* @memberof! Spot
* @type {boolean}
*/
isConnected: ['boolean', true, false],
/**
* When the app in locked down, facets and datasets cannot be edited
* @memberof! Spot
* @type {boolean}
*/
isLockedDown: ['boolean', true, false],
/**
* Type of spot session. Must be 'client' or 'server'
* @memberof! Spot
* @type {string}
*/
sessionType: {
type: 'string',
required: true,
default: 'client',
values: ['client', 'server'],
setOnce: true
}
},
children: {
/**
* A union of all active datasets
* @memberof! Spot
* @type {Dataview}
*/
dataview: Dataview
},
collections: {
/**
* Collection of all datasets
* @memberof! Spot
* @type {Dataset[]}
*/
datasets: Datasets
},
initialize: function () {
// first do parent class initialization
BaseModel.prototype.initialize.apply(this, arguments);
// default to client side (crossfilter) sessions
this.driver = driverClient;
// assign backend driver
if (arguments && arguments[0] && arguments[0].sessionType) {
if (arguments[0].sessionType === 'client') {
this.driver = driverClient;
} else if (arguments[0].sessionType === 'server') {
this.driver = driverServer;
} else {
console.error('No driver for type', arguments[0].sessionType);
}
}
},
connectToServer: connectToServer,
disconnectFromServer: disconnectFromServer,
getDatasets: getDatasets,
setFacetMinMax: setFacetMinMax,
setFacetCategories: setFacetCategories,
toggleDataset: toggleDataset
});
module.exports.util = {
dx: utildx,
misval: require('./util/misval'),
time: timeUtil
};
module.exports.transforms = {
categorial: require('./facet/categorial-transform'),
continuous: require('./facet/continuous-transform'),
datetime: require('./facet/datetime-transform'),
duration: require('./facet/duration-transform')
};
module.exports.constructors = {
Dataview: Dataview,
Dataset: require('./dataset'),
Datasets: Datasets
};