[merge] from upstream master

This commit is contained in:
amercader
2012-04-16 17:45:25 +02:00
21 changed files with 459 additions and 216 deletions

View File

@@ -17,10 +17,20 @@ this.recline.Backend = this.recline.Backend || {};
// ## recline.Backend.Base
//
// Base class for backends providing a template and convenience functions.
// You do not have to inherit from this class but even when not it does provide guidance on the functions you must implement.
// You do not have to inherit from this class but even when not it does
// provide guidance on the functions you must implement.
//
// Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.
my.Base = Backbone.Model.extend({
// ### __type__
//
// 'type' of this backend. This should be either the class path for this
// object as a string (e.g. recline.Backend.Memory) or for Backends within
// recline.Backend module it may be their class name.
//
// This value is used as an identifier for this backend when initializing
// backends (see recline.Model.Dataset.initialize).
__type__: 'base',
// ### sync
//

View File

@@ -17,6 +17,7 @@ this.recline.Backend = this.recline.Backend || {};
//
// Note that this is a **read-only** backend.
my.DataProxy = my.Base.extend({
__type__: 'dataproxy',
defaults: {
dataproxy_url: 'http://jsonpdataproxy.appspot.com'
},
@@ -71,7 +72,5 @@ this.recline.Backend = this.recline.Backend || {};
return dfd.promise();
}
});
recline.Model.backends['dataproxy'] = new my.DataProxy();
}(jQuery, this.recline.Backend));

View File

@@ -20,6 +20,7 @@ this.recline.Backend = this.recline.Backend || {};
//
// <pre>http://localhost:9200/twitter/tweet</pre>
my.ElasticSearch = my.Base.extend({
__type__: 'elasticsearch',
_getESUrl: function(dataset) {
var out = dataset.get('elasticsearch_url');
if (out) return out;
@@ -115,7 +116,6 @@ this.recline.Backend = this.recline.Backend || {};
return dfd.promise();
}
});
recline.Model.backends['elasticsearch'] = new my.ElasticSearch();
}(jQuery, this.recline.Backend));

View File

@@ -17,6 +17,7 @@ this.recline.Backend = this.recline.Backend || {};
// );
// </pre>
my.GDoc = my.Base.extend({
__type__: 'gdoc',
getUrl: function(dataset) {
var url = dataset.get('url');
if (url.indexOf('feeds/list') != -1) {
@@ -134,7 +135,6 @@ this.recline.Backend = this.recline.Backend || {};
return results;
}
});
recline.Model.backends['gdocs'] = new my.GDoc();
}(jQuery, this.recline.Backend));

View File

@@ -20,7 +20,7 @@ this.recline.Backend = this.recline.Backend || {};
if (!metadata.id) {
metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
}
var backend = recline.Model.backends['memory'];
var backend = new recline.Backend.Memory();
var datasetInfo = {
documents: data,
metadata: metadata
@@ -35,7 +35,7 @@ this.recline.Backend = this.recline.Backend || {};
}
}
backend.addDataset(datasetInfo);
var dataset = new recline.Model.Dataset({id: metadata.id}, 'memory');
var dataset = new recline.Model.Dataset({id: metadata.id}, backend);
dataset.fetch();
return dataset;
};
@@ -70,6 +70,7 @@ this.recline.Backend = this.recline.Backend || {};
// etc ...
// </pre>
my.Memory = my.Base.extend({
__type__: 'memory',
initialize: function() {
this.datasets = {};
},
@@ -209,6 +210,5 @@ this.recline.Backend = this.recline.Backend || {};
return facetResults;
}
});
recline.Model.backends['memory'] = new my.Memory();
}(jQuery, this.recline.Backend));

View File

@@ -18,7 +18,7 @@ this.recline.Model = this.recline.Model || {};
//
// @property {number} docCount: total number of documents in this dataset
//
// @property {Backend} backend: the Backend (instance) for this Dataset
// @property {Backend} backend: the Backend (instance) for this Dataset.
//
// @property {Query} queryState: `Query` object which stores current
// queryState. queryState may be edited by other components (e.g. a query
@@ -28,14 +28,24 @@ this.recline.Model = this.recline.Model || {};
// Facets.
my.Dataset = Backbone.Model.extend({
__type__: 'Dataset',
// ### initialize
//
// Sets up instance properties (see above)
//
// @param {Object} model: standard set of model attributes passed to Backbone models
//
// @param {Object or String} backend: Backend instance (see
// `recline.Backend.Base`) or a string specifying that instance. The
// string specifying may be a full class path e.g.
// 'recline.Backend.ElasticSearch' or a simple name e.g.
// 'elasticsearch' or 'ElasticSearch' (in this case must be a Backend in
// recline.Backend module)
initialize: function(model, backend) {
_.bindAll(this, 'query');
this.backend = backend;
if (backend && backend.constructor == String) {
this.backend = my.backends[backend];
if (typeof(backend) === 'string') {
this.backend = this._backendFromString(backend);
}
this.fields = new my.FieldList();
this.currentDocuments = new my.DocumentList();
@@ -99,9 +109,68 @@ my.Dataset = Backbone.Model.extend({
data.docCount = this.docCount;
data.fields = this.fields.toJSON();
return data;
},
// ### _backendFromString(backendString)
//
// See backend argument to initialize for details
_backendFromString: function(backendString) {
var parts = backendString.split('.');
// walk through the specified path xxx.yyy.zzz to get the final object which should be backend class
var current = window;
for(ii=0;ii<parts.length;ii++) {
if (!current) {
break;
}
current = current[parts[ii]];
}
if (current) {
return new current();
}
// alternatively we just had a simple string
var backend = null;
if (recline && recline.Backend) {
_.each(_.keys(recline.Backend), function(name) {
if (name.toLowerCase() === backendString.toLowerCase()) {
backend = new recline.Backend[name]();
}
});
}
return backend;
}
});
// ### Dataset.restore
//
// Restore a Dataset instance from a serialized state. Serialized state for a
// Dataset is an Object like:
//
// <pre>
// {
// backend: {backend type - i.e. value of dataset.backend.__type__}
// dataset: {result of dataset.toJSON()}
// ...
// }
my.Dataset.restore = function(state) {
// hack-y - restoring a memory dataset does not mean much ...
var dataset = null;
if (state.backend === 'memory') {
dataset = recline.Backend.createDataset(
[{stub: 'this is a stub dataset because we do not restore memory datasets'}],
[],
state.dataset
);
} else {
dataset = new recline.Model.Dataset(
state.dataset,
state.backend
);
}
return dataset;
};
// ## <a id="document">A Document (aka Row)</a>
//
// A single entry or row in the dataset

View File

@@ -23,7 +23,7 @@ this.recline.View = this.recline.View || {};
my.Graph = Backbone.View.extend({
tagName: "div",
className: "recline-graph-container",
className: "recline-graph",
template: ' \
<div class="editor"> \
@@ -275,9 +275,9 @@ my.Graph = Backbone.View.extend({
createSeries: function () {
var self = this;
var series = [];
$.each(this.state.attributes.series, function (seriesIndex, field) {
_.each(this.state.attributes.series, function(field) {
var points = [];
$.each(self.model.currentDocuments.models, function (index, doc) {
_.each(self.model.currentDocuments.models, function(doc, index) {
var x = doc.get(self.state.attributes.group);
var y = doc.get(field);
if (typeof x === 'string') {

View File

@@ -12,21 +12,21 @@ this.recline.View = this.recline.View || {};
// [GeoJSON](http://geojson.org) objects or two fields with latitude and
// longitude coordinates.
//
// Initialization arguments:
//
// * options: initial options. They must contain a model:
//
// {
// model: {recline.Model.Dataset}
// }
//
// * config: (optional) map configuration hash (not yet used)
//
// Initialization arguments are as standard for Dataset Views. State object may
// have the following (optional) configuration options:
//
// <pre>
// {
// // geomField if specified will be used in preference to lat/lon
// geomField: {id of field containing geometry in the dataset}
// lonField: {id of field containing longitude in the dataset}
// latField: {id of field containing latitude in the dataset}
// }
// </pre>
my.Map = Backbone.View.extend({
tagName: 'div',
className: 'data-map-container',
className: 'recline-map',
template: ' \
<div class="editor"> \
@@ -101,14 +101,12 @@ my.Map = Backbone.View.extend({
'change #editor-auto-zoom': 'onAutoZoomChange'
},
initialize: function(options, config) {
initialize: function(options) {
var self = this;
this.el = $(this.el);
// Listen to changes in the fields
this.model.bind('change', function() {
this.model.fields.bind('change', function() {
self._setupGeometryField();
});
this.model.fields.bind('add', this.render);
@@ -136,9 +134,17 @@ my.Map = Backbone.View.extend({
self.visible = false;
});
var stateData = _.extend({
geomField: null,
lonField: null,
latField: null
},
options.state
);
this.state = new recline.Model.ObjectState(stateData);
this.autoZoom = true;
this.mapReady = false;
this.render();
},
@@ -155,12 +161,12 @@ my.Map = Backbone.View.extend({
this.$map = this.el.find('.panel.map');
if (this.geomReady && this.model.fields.length){
if (this._geomFieldName){
this._selectOption('editor-geom-field',this._geomFieldName);
if (this.state.get('geomField')){
this._selectOption('editor-geom-field',this.state.get('geomField'));
$('#editor-field-type-geom').attr('checked','checked').change();
} else{
this._selectOption('editor-lon-field',this._lonFieldName);
this._selectOption('editor-lat-field',this._latFieldName);
this._selectOption('editor-lon-field',this.state.get('lonField'));
this._selectOption('editor-lat-field',this.state.get('latField'));
$('#editor-field-type-latlon').attr('checked','checked').change();
}
}
@@ -189,9 +195,7 @@ my.Map = Backbone.View.extend({
// * refresh: Clear existing features and add all current documents
//
redraw: function(action,doc){
var self = this;
action = action || 'refresh';
if (this.geomReady && this.mapReady){
@@ -227,14 +231,19 @@ my.Map = Backbone.View.extend({
onEditorSubmit: function(e){
e.preventDefault();
if ($('#editor-field-type-geom').attr('checked')){
this._geomFieldName = $('.editor-geom-field > select > option:selected').val();
this._latFieldName = this._lonFieldName = false;
this.state.set({
geomField: $('.editor-geom-field > select > option:selected').val(),
lonField: null,
latField: null
});
} else {
this._geomFieldName = false;
this._latFieldName = $('.editor-lat-field > select > option:selected').val();
this._lonFieldName = $('.editor-lon-field > select > option:selected').val();
this.state.set({
geomField: null,
lonField: $('.editor-lon-field > select > option:selected').val(),
latField: $('.editor-lat-field > select > option:selected').val()
});
}
this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
this.redraw();
return false;
@@ -327,16 +336,16 @@ my.Map = Backbone.View.extend({
//
_getGeometryFromDocument: function(doc){
if (this.geomReady){
if (this._geomFieldName){
if (this.state.get('geomField')){
// We assume that the contents of the field are a valid GeoJSON object
return doc.attributes[this._geomFieldName];
} else if (this._lonFieldName && this._latFieldName){
return doc.attributes[this.state.get('geomField')];
} else if (this.state.get('lonField') && this.state.get('latField')){
// We'll create a GeoJSON like point object from the two lat/lon fields
return {
type: 'Point',
coordinates: [
doc.attributes[this._lonFieldName],
doc.attributes[this._latFieldName]
doc.attributes[this.state.get('lonField')],
doc.attributes[this.state.get('latField')]
]
};
}
@@ -350,12 +359,12 @@ my.Map = Backbone.View.extend({
// If not found, the user can define them via the UI form.
_setupGeometryField: function(){
var geomField, latField, lonField;
this._geomFieldName = this._checkField(this.geometryFieldNames);
this._latFieldName = this._checkField(this.latitudeFieldNames);
this._lonFieldName = this._checkField(this.longitudeFieldNames);
this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
this.state.set({
geomField: this._checkField(this.geometryFieldNames),
latField: this._checkField(this.latitudeFieldNames),
lonField: this._checkField(this.longitudeFieldNames)
});
this.geomReady = (this.state.get('geomField') || (this.state.get('latField') && this.state.get('lonField')));
},
// Private: Check if a field in the current model exists in the provided

View File

@@ -1,16 +1,83 @@
/*jshint multistr:true */
// # Core View Functionality plus Data Explorer
// # Recline Views
//
// ## Common view concepts
// Recline Views are Backbone Views and in keeping with normal Backbone views
// are Widgets / Components displaying something in the DOM. Like all Backbone
// views they have a pointer to a model or a collection and is bound to an
// element.
//
// Views provided by core Recline are crudely divided into two types:
//
// * Dataset Views: a View intended for displaying a recline.Model.Dataset
// in some fashion. Examples are the Grid, Graph and Map views.
// * Widget Views: a widget used for displaying some specific (and
// smaller) aspect of a dataset or the application. Examples are
// QueryEditor and FilterEditor which both provide a way for editing (a
// part of) a `recline.Model.Query` associated to a Dataset.
//
// ## Dataset View
//
// These views are just Backbone views with a few additional conventions:
//
// 1. The model passed to the View should always be a recline.Model.Dataset instance
// 2. Views should generate their own root element rather than having it passed
// in.
// 3. Views should apply a css class named 'recline-{view-name-lower-cased} to
// the root element (and for all CSS for this view to be qualified using this
// CSS class)
// 4. Read-only mode: CSS for this view should respect/utilize
// recline-read-only class to trigger read-only behaviour (this class will
// usually be set on some parent element of the view's root element.
// 5. State: state (configuration) information for the view should be stored on
// an attribute named state that is an instance of a Backbone Model (or, more
// speficially, be an instance of `recline.Model.ObjectState`). In addition,
// a state attribute may be specified in the Hash passed to a View on
// iniitialization and this information should be used to set the initial
// state of the view.
//
// Example of state would be the set of fields being plotted in a graph
// view.
//
// More information about State can be found below.
//
// To summarize some of this, the initialize function for a Dataset View should
// look like:
//
// <pre>
// initialize: {
// model: {a recline.Model.Dataset instance}
// // el: {do not specify - instead view should create}
// state: {(optional) Object / Hash specifying initial state}
// ...
// }
// </pre>
//
// Note: Dataset Views in core Recline have a common layout on disk as
// follows, where ViewName is the named of View class:
//
// <pre>
// src/view-{lower-case-ViewName}.js
// css/{lower-case-ViewName}.css
// test/view-{lower-case-ViewName}.js
// </pre>
//
// ### State
//
// TODO
// State information exists in order to support state serialization into the
// url or elsewhere and reloading of application from a stored state.
//
// ### Read-only
// State is available not only for individual views (as described above) but
// for the dataset (e.g. the current query). For an example of pulling together
// state from across multiple components see `recline.View.DataExplorer`.
//
// ### Writing your own Views
//
// TODO
// See the existing Views.
//
// ----
// Standard JS module setup
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -38,7 +105,8 @@ this.recline.View = this.recline.View || {};
//
// **views**: (optional) the dataset views (Grid, Graph etc) for
// DataExplorer to show. This is an array of view hashes. If not provided
// just initialize a Grid with id 'grid'. Example:
// initialize with (recline.View.)Grid, Graph, and Map views (with obvious id
// and labels!).
//
// <pre>
// var views = [
@@ -59,10 +127,25 @@ this.recline.View = this.recline.View || {};
// ];
// </pre>
//
// **state**: state config for this view. Options are:
// **state**: standard state config for this view. This state is slightly
// special as it includes config of many of the subviews.
//
// * readOnly: true/false (default: false) value indicating whether to
// operate in read-only mode (hiding all editing options).
// <pre>
// state = {
// query: {dataset query state - see dataset.queryState object}
// view-{id1}: {view-state for this view}
// view-{id2}: {view-state for }
// ...
// // Explorer
// currentView: id of current view (defaults to first view if not specified)
// readOnly: (default: false) run in read-only mode
// }
// </pre>
//
// Note that at present we do *not* serialize information about the actual set
// of views in use -- e.g. those specified by the views argument -- but instead
// expect either that the default views are fine or that the client to have
// initialized the DataExplorer with the relevant views themselves.
my.DataExplorer = Backbone.View.extend({
template: ' \
<div class="recline-data-explorer"> \
@@ -71,7 +154,7 @@ my.DataExplorer = Backbone.View.extend({
<div class="header"> \
<ul class="navigation"> \
{{#views}} \
<li><a href="#{{id}}" class="btn">{{label}}</a> \
<li><a href="#{{id}}" data-view="{{id}}" class="btn">{{label}}</a> \
{{/views}} \
</ul> \
<div class="recline-results-info"> \
@@ -94,7 +177,8 @@ my.DataExplorer = Backbone.View.extend({
</div> \
',
events: {
'click .menu-right a': 'onMenuClick'
'click .menu-right a': '_onMenuClick',
'click .navigation a': '_onSwitchView'
},
initialize: function(options) {
@@ -108,14 +192,26 @@ my.DataExplorer = Backbone.View.extend({
id: 'grid',
label: 'Grid',
view: new my.Grid({
model: this.model
})
model: this.model
})
}, {
id: 'graph',
label: 'Graph',
view: new my.Graph({
model: this.model
})
}, {
id: 'map',
label: 'Map',
view: new my.Map({
model: this.model
})
}];
}
this.state = new recline.Model.ObjectState();
// these must be called after pageViews are created
this._setupState(options.state);
this.render();
// should come after render as may need to interact with elements in the view
this._setupState(options.state);
this.router = new Backbone.Router();
this.setupRouting();
@@ -163,7 +259,7 @@ my.DataExplorer = Backbone.View.extend({
},
setReadOnly: function() {
this.el.addClass('read-only');
this.el.addClass('recline-read-only');
},
render: function() {
@@ -204,10 +300,10 @@ my.DataExplorer = Backbone.View.extend({
});
},
updateNav: function(pageName, queryString) {
updateNav: function(pageName) {
this.el.find('.navigation li').removeClass('active');
this.el.find('.navigation li a').removeClass('disabled');
var $el = this.el.find('.navigation li a[href=#' + pageName + ']');
var $el = this.el.find('.navigation li a[data-view="' + pageName + '"]');
$el.parent().addClass('active');
$el.addClass('disabled');
// show the specific page
@@ -222,7 +318,7 @@ my.DataExplorer = Backbone.View.extend({
});
},
onMenuClick: function(e) {
_onMenuClick: function(e) {
e.preventDefault();
var action = $(e.target).attr('data-action');
if (action === 'filters') {
@@ -232,27 +328,47 @@ my.DataExplorer = Backbone.View.extend({
}
},
_onSwitchView: function(e) {
e.preventDefault();
var viewName = $(e.target).attr('data-view');
this.updateNav(viewName);
this.state.set({currentView: viewName});
},
// create a state object for this view and do the job of
//
// a) initializing it from both data passed in and other sources (e.g. hash url)
//
// b) ensure the state object is updated in responese to changes in subviews, query etc.
_setupState: function(initialState) {
var self = this;
// get data from the query string / hash url plus some defaults
var qs = my.parseHashQueryString();
var query = qs.reclineQuery;
query = query ? JSON.parse(query) : self.model.queryState.toJSON();
// backwards compatability (now named view-graph but was named graph)
var graphState = qs['view-graph'] || qs.graph;
graphState = graphState ? JSON.parse(graphState) : {};
// now get default data + hash url plus initial state and initial our state object with it
var stateData = _.extend({
readOnly: false,
query: query,
'view-graph': graphState,
currentView: null
backend: this.model.backend.__type__,
dataset: this.model.toJSON(),
currentView: this.pageViews[0].id,
readOnly: false
},
initialState);
this.state.set(stateData);
this.state = new recline.Model.ObjectState(stateData);
// now do updates based on state
if (this.state.get('readOnly')) {
this.setReadOnly();
}
if (this.state.get('currentView')) {
this.updateNav(this.state.get('currentView'));
}
_.each(this.pageViews, function(pageView) {
var viewId = 'view-' + pageView.id;
if (viewId in self.state.attributes) {
@@ -260,7 +376,7 @@ my.DataExplorer = Backbone.View.extend({
}
});
// bind for changes state in associated objects
// finally ensure we update our state object when state of sub-object changes so that state is always up to date
this.model.queryState.bind('change', function() {
self.state.set({queryState: self.model.queryState.toJSON()});
});
@@ -276,14 +392,21 @@ my.DataExplorer = Backbone.View.extend({
});
}
});
},
// Get the current state of dataset and views
getState: function() {
return this.state;
}
});
// ### DataExplorer.restore
//
// Restore a DataExplorer instance from a serialized state including the associated dataset
my.DataExplorer.restore = function(state) {
var dataset = recline.Model.Dataset.restore(state);
var explorer = new my.DataExplorer({
model: dataset,
state: state
});
return explorer;
}
my.QueryEditor = Backbone.View.extend({
className: 'recline-query-editor',
template: ' \