Faceted browsing is starting to work.

git-svn-id: http://google-refine.googlecode.com/svn/trunk@13 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
David Huynh 2010-01-30 01:05:30 +00:00
parent dce2ec71aa
commit e24d40c3da
22 changed files with 515 additions and 79 deletions

View File

@ -15,6 +15,7 @@ import org.json.JSONObject;
import org.json.JSONTokener;
import com.metaweb.gridlock.commands.Command;
import com.metaweb.gridlock.commands.ComputeFacetsCommand;
import com.metaweb.gridlock.commands.CreateProjectFromUploadCommand;
import com.metaweb.gridlock.commands.DoTextTransformCommand;
import com.metaweb.gridlock.commands.GetColumnModelCommand;
@ -34,6 +35,7 @@ public class GridlockServlet extends HttpServlet {
_commands.put("get-column-model", new GetColumnModelCommand());
_commands.put("get-rows", new GetRowsCommand());
_commands.put("get-history", new GetHistoryCommand());
_commands.put("compute-facets", new ComputeFacetsCommand());
_commands.put("undo-redo", new UndoRedoCommand());
_commands.put("do-text-transform", new DoTextTransformCommand());
}

View File

@ -11,6 +11,7 @@ import org.json.JSONObject;
import com.metaweb.gridlock.browsing.facets.Facet;
import com.metaweb.gridlock.browsing.facets.ListFacet;
import com.metaweb.gridlock.browsing.filters.RowFilter;
import com.metaweb.gridlock.model.Project;
public class Engine {
@ -29,7 +30,10 @@ public class Engine {
ConjunctiveFilteredRows cfr = new ConjunctiveFilteredRows();
for (Facet facet : _facets) {
if (facet != except) {
cfr.add(facet.getRowFilter());
RowFilter rowFilter = facet.getRowFilter();
if (rowFilter != null) {
cfr.add(rowFilter);
}
}
}
return cfr;
@ -53,7 +57,7 @@ public class Engine {
for (int i = 0; i < length; i++) {
JSONObject fo = a.getJSONObject(i);
String type = fo.getString("type");
String type = fo.has("type") ? fo.getString("type") : "list";
Facet facet = null;
if ("list".equals(type)) {

View File

@ -3,6 +3,6 @@ package com.metaweb.gridlock.browsing;
import com.metaweb.gridlock.model.Row;
public interface RowVisitor {
public void visit(int rowIndex, Row row);
public boolean visit(int rowIndex, Row row);
}

View File

@ -21,7 +21,7 @@ public class CellAccessorNominalRowGrouper implements RowVisitor {
}
@Override
public void visit(int rowIndex, Row row) {
public boolean visit(int rowIndex, Row row) {
if (_cellIndex < row.cells.size()) {
Cell cell = row.cells.get(_cellIndex);
if (cell != null) {
@ -48,5 +48,6 @@ public class CellAccessorNominalRowGrouper implements RowVisitor {
}
}
}
return false;
}
}

View File

@ -22,7 +22,7 @@ public class ExpressionNominalRowGrouper implements RowVisitor {
}
@Override
public void visit(int rowIndex, Row row) {
public boolean visit(int rowIndex, Row row) {
if (_cellIndex < row.cells.size()) {
Cell cell = row.cells.get(_cellIndex);
if (cell != null) {
@ -33,18 +33,30 @@ public class ExpressionNominalRowGrouper implements RowVisitor {
Object value = _evaluable.evaluate(bindings);
if (value != null) {
DecoratedValue dValue = new DecoratedValue(value, value.toString());
if (choices.containsKey(value)) {
choices.get(value).count++;
if (value.getClass().isArray()) {
Object[] a = (Object[]) value;
for (Object v : a) {
processValue(v);
}
} else {
NominalFacetChoice choice = new NominalFacetChoice(dValue);
choice.count = 1;
choices.put(value, choice);
processValue(value);
}
}
}
}
return false;
}
protected void processValue(Object value) {
DecoratedValue dValue = new DecoratedValue(value, value.toString());
if (choices.containsKey(value)) {
choices.get(value).count++;
} else {
NominalFacetChoice choice = new NominalFacetChoice(dValue);
choice.count = 1;
choices.put(value, choice);
}
}
}

View File

@ -76,7 +76,8 @@ public class ListFacet implements Facet {
@Override
public RowFilter getRowFilter() {
return new ExpressionEqualRowFilter(_eval, _cellIndex, createMatches());
return _selection.size() == 0 ? null :
new ExpressionEqualRowFilter(_eval, _cellIndex, createMatches());
}
@Override
@ -88,12 +89,21 @@ public class ListFacet implements Facet {
_choices.clear();
_choices.addAll(grouper.choices.values());
for (NominalFacetChoice choice : _selection) {
if (grouper.choices.containsKey(choice.decoratedValue.value)) {
grouper.choices.get(choice.decoratedValue.value).selected = true;
} else {
choice.count = 0;
_choices.add(choice);
}
}
}
protected Object[] createMatches() {
Object[] a = new Object[_choices.size()];
Object[] a = new Object[_selection.size()];
for (int i = 0; i < a.length; i++) {
a[i] = _choices.get(i).decoratedValue.value;
a[i] = _selection.get(i).decoratedValue.value;
}
return a;
}

View File

@ -153,14 +153,10 @@ public abstract class Command {
}
protected Engine getEngine(HttpServletRequest request, Project project) throws Exception {
Properties properties = new Properties();
readFileUpload(request, properties);
Engine engine = new Engine(project);
if (properties.containsKey("engine")) {
String json = properties.getProperty("engine");
String json = request.getParameter("engine");
if (json != null) {
JSONObject o = jsonStringToObject(json);
engine.initializeFromJSON(o);
}
return engine;

View File

@ -0,0 +1,30 @@
package com.metaweb.gridlock.commands;
import java.io.IOException;
import java.util.Properties;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.metaweb.gridlock.browsing.Engine;
import com.metaweb.gridlock.model.Project;
public class ComputeFacetsCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Project project = getProject(request);
Engine engine = getEngine(request, project);
engine.computeFacets();
Properties options = new Properties();
respondJSON(response, engine.getJSON(options));
} catch (Exception e) {
respondException(response, e);
}
}
}

View File

@ -47,13 +47,14 @@ public class GetRowsCommand extends Command {
}
@Override
public void internalVisit(int rowIndex, Row row) {
public boolean internalVisit(int rowIndex, Row row) {
try {
JSONObject ro = row.getJSON(options);
ro.put("i", rowIndex);
list.add(ro);
} catch (JSONException e) {
}
return false;
}
}.init(a, options);
@ -86,14 +87,18 @@ public class GetRowsCommand extends Command {
}
@Override
public void visit(int rowIndex, Row row) {
public boolean visit(int rowIndex, Row row) {
boolean r = false;
if (total >= start && total < start + limit) {
internalVisit(rowIndex, row);
r = internalVisit(rowIndex, row);
}
total++;
return r;
}
protected void internalVisit(int rowIndex, Row row) {
protected boolean internalVisit(int rowIndex, Row row) {
return false;
}
}
}

View File

@ -8,8 +8,10 @@ import java.util.Map;
import com.metaweb.gridlock.expr.Scanner.NumberToken;
import com.metaweb.gridlock.expr.Scanner.Token;
import com.metaweb.gridlock.expr.Scanner.TokenType;
import com.metaweb.gridlock.expr.functions.Get;
import com.metaweb.gridlock.expr.functions.Replace;
import com.metaweb.gridlock.expr.functions.Slice;
import com.metaweb.gridlock.expr.functions.Split;
import com.metaweb.gridlock.expr.functions.ToLowercase;
import com.metaweb.gridlock.expr.functions.ToTitlecase;
import com.metaweb.gridlock.expr.functions.ToUppercase;
@ -26,7 +28,9 @@ public class Parser {
functionTable.put("toTitlecase", new ToTitlecase());
functionTable.put("slice", new Slice());
functionTable.put("substring", new Slice());
functionTable.put("get", new Get());
functionTable.put("replace", new Replace());
functionTable.put("split", new Split());
}
public Parser(String s) throws Exception {
@ -196,7 +200,7 @@ public class Parser {
List<Evaluable> args = parseExpressionList("]");
args.add(0, eval);
eval = new FunctionCallExpr(makeArray(args), functionTable.get("slice"));
eval = new FunctionCallExpr(makeArray(args), functionTable.get("get"));
} else {
break;
}

View File

@ -0,0 +1,67 @@
package com.metaweb.gridlock.expr.functions;
import java.util.Properties;
import com.metaweb.gridlock.expr.Function;
public class Get implements Function {
@Override
public Object call(Properties bindings, Object[] args) {
if (args.length > 1 && args.length <= 3) {
Object v = args[0];
Object from = args[1];
Object to = args.length == 3 ? args[2] : null;
if (v != null && from != null && from instanceof Number && (to == null || to instanceof Number)) {
if (v.getClass().isArray()) {
Object[] a = (Object[]) v;
int start = ((Number) from).intValue();
if (start < 0) {
start = a.length + start;
}
start = Math.min(a.length, Math.max(0, start));
if (to == null) {
return a[start];
} else {
int end = to != null && to instanceof Number ?
((Number) to).intValue() : a.length;
if (end < 0) {
end = a.length - end;
}
end = Math.min(a.length, Math.max(start, end));
Object[] a2 = new Object[end - start];
System.arraycopy(a, start, a2, 0, end - start);
return a2;
}
} else {
String s = (v instanceof String ? (String) v : v.toString());
int start = ((Number) from).intValue();
if (start < 0) {
start = s.length() + start;
}
start = Math.min(s.length(), Math.max(0, start));
if (to != null && to instanceof Number) {
int end = ((Number) to).intValue();
if (end < 0) {
end = s.length() - end;
}
end = Math.min(s.length(), Math.max(start, end));
return s.substring(start, end);
} else {
return s.substring(start, start + 1);
}
}
}
}
return null;
}
}

View File

@ -1,6 +1,5 @@
package com.metaweb.gridlock.expr.functions;
import java.lang.reflect.Array;
import java.util.Properties;
import com.metaweb.gridlock.expr.Function;
@ -15,14 +14,14 @@ public class Slice implements Function {
Object to = args.length == 3 ? args[2] : null;
if (v != null && from != null && from instanceof Number && (to == null || to instanceof Number)) {
if (v instanceof Array) {
if (v.getClass().isArray()) {
Object[] a = (Object[]) v;
int start = ((Number) from).intValue();
int end = to != null && to instanceof Number ?
((Number) to).intValue() : a.length;
if (start < 0) {
start = a.length - start;
start = a.length + start;
}
start = Math.min(a.length, Math.max(0, start));
@ -40,7 +39,7 @@ public class Slice implements Function {
int start = ((Number) from).intValue();
if (start < 0) {
start = s.length() - start;
start = s.length() + start;
}
start = Math.min(s.length(), Math.max(0, start));

View File

@ -0,0 +1,21 @@
package com.metaweb.gridlock.expr.functions;
import java.util.Properties;
import com.metaweb.gridlock.expr.Function;
public class Split implements Function {
@Override
public Object call(Properties bindings, Object[] args) {
if (args.length == 2) {
Object v = args[0];
Object split = args[1];
if (v != null && split != null && split instanceof String) {
return (v instanceof String ? (String) v : v.toString()).split((String) split);
}
}
return null;
}
}

View File

@ -1 +1 @@
<html> <head> <title>Gridlock</title> <link rel="stylesheet" href="/styles/common.css" /> <link rel="stylesheet" href="/styles/project.css" /> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script> <script type="text/javascript" src="scripts/util/url.js"></script> <script type="text/javascript" src="scripts/util/string.js"></script> <script type="text/javascript" src="scripts/util/ajax.js"></script> <script type="text/javascript" src="scripts/util/menu.js"></script> <script type="text/javascript" src="scripts/project.js"></script> <script type="text/javascript" src="scripts/project/browsing-engine.js"></script> <script type="text/javascript" src="scripts/project/data-table-view.js"></script> <script type="text/javascript" src="scripts/project/history-widget.js"></script> </head> <body> <div id="header"> <h1 id="title">Gridlock</h1> </div> <div id="body"> Loading ... </div> </body> </html>
<html> <head> <title>Gridlock</title> <link rel="stylesheet" href="/styles/common.css" /> <link rel="stylesheet" href="/styles/project.css" /> <link rel="stylesheet" href="/styles/history.css" /> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script> <script type="text/javascript" src="scripts/util/misc.js"></script> <script type="text/javascript" src="scripts/util/url.js"></script> <script type="text/javascript" src="scripts/util/string.js"></script> <script type="text/javascript" src="scripts/util/ajax.js"></script> <script type="text/javascript" src="scripts/util/menu.js"></script> <script type="text/javascript" src="scripts/project.js"></script> <script type="text/javascript" src="scripts/project/list-facet.js"></script> <script type="text/javascript" src="scripts/project/browsing-engine.js"></script> <script type="text/javascript" src="scripts/project/data-table-view.js"></script> <script type="text/javascript" src="scripts/project/history-widget.js"></script> </head> <body> <div id="header"> <h1 id="title">Gridlock</h1> </div> <div id="body"> Loading ... </div> </body> </html>

View File

@ -1,11 +1,11 @@
function BrowsingEngine(div) {
this._div = div;
this.render();
this._facets = [];
this._initializeUI();
}
BrowsingEngine.prototype.render = function() {
BrowsingEngine.prototype._initializeUI = function() {
var self = this;
var container = this._div.empty();
};
@ -13,7 +13,36 @@ BrowsingEngine.prototype.render = function() {
BrowsingEngine.prototype.getJSON = function() {
var a = { facets: [] };
for (var i = 0; i < this._facets.length; i++) {
a.facets.push(this._facets[i].getJSON());
a.facets.push(this._facets[i].facet.getJSON());
}
return a;
};
BrowsingEngine.prototype.addFacet = function(type, config) {
var div = $('<div></div>').addClass("facet-container").appendTo(this._div);
var facet;
switch (type) {
default:
facet = new ListFacet(div, config, {});
}
this._facets.push({ elmt: div, facet: facet });
this.update();
};
BrowsingEngine.prototype.update = function() {
var self = this;
$.post(
"/command/compute-facets?" + $.param({ project: theProject.id }),
{ engine: JSON.stringify(ui.browsingEngine.getJSON()) },
function(data) {
var facetData = data.facets;
for (var i = 0; i < facetData.length; i++) {
self._facets[i].facet.updateState(facetData[i]);
}
},
"json"
);
};

View File

@ -11,7 +11,7 @@ DataTableView.prototype.render = function() {
var divSummary = $('<div></div>').addClass("viewPanel-summary").appendTo(container);
$('<span>' +
(theProject.rowModel.start + 1) + " to " +
(theProject.rowModel.start + theProject.rowModel.limit) + " of " +
Math.min(theProject.rowModel.filtered, theProject.rowModel.start + theProject.rowModel.limit) + " of " +
(theProject.rowModel.filtered) + " filtered rows, " +
(theProject.rowModel.total) + " total rows" +
'</span>'
@ -30,7 +30,7 @@ DataTableView.prototype.render = function() {
$('<span> &bull; </span>').appendTo(pagingControls);
var nextPage = $('<a href="javascript:{}">next page &raquo;</a>').appendTo(pagingControls);
var lastPage = $('<a href="javascript:{}">last &raquo;</a>').appendTo(pagingControls);
if (theProject.rowModel.start + theProject.rowModel.limit < theProject.rowModel.total) {
if (theProject.rowModel.start + theProject.rowModel.limit < theProject.rowModel.filtered) {
nextPage.addClass("action").click(function(evt) { self._onClickNextPage(this, evt); });
lastPage.addClass("action").click(function(evt) { self._onClickLastPage(this, evt); });
} else {
@ -137,7 +137,7 @@ DataTableView.prototype._onClickFirstPage = function(elmt, evt) {
};
DataTableView.prototype._onClickLastPage = function(elmt, evt) {
this._showRows(Math.floor(theProject.rowModel.total / this._pageSize) * this._pageSize);
this._showRows(Math.floor(theProject.rowModel.filtered / this._pageSize) * this._pageSize);
};
DataTableView.prototype._createMenuForAllColumns = function(elmt) {
@ -172,15 +172,29 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
submenu: [
{
label: "By Nominal Choices",
click: function() {}
click: function() {
ui.browsingEngine.addFacet(
"list",
{
"name" : column.headerLabel,
"cellIndex" : column.cellIndex,
"expression" : "value"
}
);
}
},
{
label: "By Simple Text Search",
click: function() {}
},
{
label: "By Regular Expression",
click: function() {}
label: "By Custom Expression",
click: function() {
var expression = window.prompt("Enter expression", 'value');
if (expression != null) {
self._doFilterByExpression(column, expression);
}
}
},
{
label: "By Reconciliation Features",
@ -282,6 +296,18 @@ DataTableView.prototype._doTextTransform = function(column, expression) {
);
};
DataTableView.prototype.update = function() {
this._showRows(theProject.rowModel.start);
DataTableView.prototype._doFilterByExpression = function(column, expression) {
ui.browsingEngine.addFacet(
"list",
{
"name" : column.headerLabel + ": " + expression,
"cellIndex" : column.cellIndex,
"expression" : expression
}
);
};
DataTableView.prototype.update = function(reset) {
this._showRows(reset ? 0 : theProject.rowModel.start);
};

View File

@ -1,5 +1,11 @@
function HistoryWidget(div) {
this._div = div;
this._div.mouseover(function() {
this.style.height = "300px";
}).mouseout(function() {
this.style.height = "100px";
});
this.update();
}

View File

@ -0,0 +1,142 @@
function ListFacet(div, config, options) {
this._div = div;
this._config = config;
this._selection = [];
this._data = null;
this.render();
}
ListFacet.prototype.getJSON = function() {
var o = cloneDeep(this._config);
o.type = "list";
o.selection = [];
for (var i = 0; i < this._selection.length; i++) {
var choice = cloneDeep(this._selection[i]);
choice.s = true;
o.selection.push(choice);
}
return o;
};
ListFacet.prototype.updateState = function(data) {
this._data = data;
var selection = [];
var choices = data.choices;
for (var i = 0; i < choices.length; i++) {
var choice = choices[i];
if (choice.s) {
selection.push(choice);
}
}
this._selection = selection;
this.render();
};
ListFacet.prototype.render = function() {
var self = this;
var scrollTop = 0;
try {
scrollTop = this._div[0].childNodes[1].scrollTop;
} catch (e) {
}
var container = this._div.empty();
var headerDiv = $('<div></div>').addClass("facet-title").appendTo(container);
$('<span></span>').text(this._config.name).appendTo(headerDiv);
var bodyDiv = $('<div></div>').addClass("facet-body").appendTo(container);
if (this._data == null) {
bodyDiv.html("Loading...");
} else {
var selectionCount = this._selection.length;
if (selectionCount > 0) {
var reset = function() {
self._reset();
};
$('<a href="javascript:{}"></a>').addClass("facet-choice-link").text("reset").click(reset).prependTo(headerDiv);
}
var renderChoice = function(choice) {
var label = choice.v.l;
var count = choice.c;
var choiceDiv = $('<div></div>').addClass("facet-choice").appendTo(bodyDiv);
if (choice.s) {
choiceDiv.addClass("facet-choice-selected");
}
var a = $('<a href="javascript:{}"></a>').addClass("facet-choice-label").text(label).appendTo(choiceDiv);
$('<span></span>').addClass("facet-choice-count").text(count).appendTo(choiceDiv);
var select = function() {
self._select(choice, false);
};
var selectOnly = function() {
self._select(choice, true);
};
var deselect = function() {
self._deselect(choice);
};
if (choice.s) { // selected
if (selectionCount > 1) {
// select only
a.click(selectOnly);
} else {
// deselect
a.click(deselect);
}
// remove link
$('<a href="javascript:{}"></a>').addClass("facet-choice-link").text("remove").click(deselect).prependTo(choiceDiv);
} else if (selectionCount > 0) {
a.click(selectOnly);
// include link
$('<a href="javascript:{}"></a>').addClass("facet-choice-link").text("include").click(select).appendTo(choiceDiv);
} else {
a.click(select);
}
};
var choices = this._data.choices;
for (var i = 0; i < choices.length; i++) {
renderChoice(choices[i]);
}
bodyDiv[0].scrollTop = scrollTop;
}
};
ListFacet.prototype._select = function(choice, only) {
if (only) {
this._selection = [];
}
this._selection.push(choice);
this._updateRest();
};
ListFacet.prototype._deselect = function(choice) {
for (var i = this._selection.length - 1; i >= 0; i--) {
if (this._selection[i] == choice) {
this._selection.splice(i, 1);
break;
}
}
this._updateRest();
};
ListFacet.prototype._reset = function() {
this._selection = [];
this._updateRest();
};
ListFacet.prototype._updateRest = function() {
ui.browsingEngine.update();
ui.dataTableView.update(true);
};

View File

@ -0,0 +1,23 @@
function cloneDeep(o) {
if (o === undefined || o === null) {
return o;
} else if (o instanceof Function) {
return o;
} else if (o instanceof Array) {
var a = [];
for (var i = 0; i < o.length; i++) {
a.push(cloneDeep(o[i]));
}
return a;
} else if (o instanceof Object) {
var a = {};
for (var n in o) {
if (o.hasOwnProperty(n)) {
a[n] = cloneDeep(o[n]);
}
}
return a;
} else {
return o;
}
}

View File

@ -0,0 +1,59 @@
.browsing-panel {
}
.facet-container {
clear: both;
margin-top: 1em;
}
.facet-title {
padding: 5px;
background: #eee;
font-weight: bold;
}
.facet-body {
border: 1px solid #ccc;
padding: 1px;
height: 20em;
overflow: auto;
}
.facet-choice {
padding: 2px 5px;
clear: both;
}
.facet-choice-selected .facet-choice-label {
font-weight: bold;
}
a.facet-choice-label {
margin-right: 0.5em;
text-decoration: none;
color: #004;
}
a.facet-choice-label:hover {
text-decoration: underline;
color: #66f;
}
.facet-choice-count {
color: #aaa;
}
.facet-choice-count:before {
content: "(";
color: #aaa;
}
.facet-choice-count:after {
content: ")";
color: #aaa;
}
a.facet-choice-link {
font-size: 80%;
float: right;
text-decoration: none;
color: #aac;
}
a.facet-choice-link:hover {
text-decoration: underline;
color: #88a;
}

View File

@ -0,0 +1,35 @@
.history-panel {
position: absolute;
top: -1px;
right: 20px;
width: 200px;
padding: 2px;
background: #fffee0;
border: 1px solid #ccc;
height: 100px;
overflow: auto;
}
.history-panel h3 {
margin: 0;
padding: 3px;
background: #fee;
font-size: 100%;
}
.history-past {
padding-bottom: 3px;
border-bottom: 2px solid #aaa;
}
.history-future {
padding-top: 3px;
}
a.history-entry {
display: block;
padding: 3px 5px;
border-bottom: 1px solid #eee;
text-decoration: none;
color: black;
}
a.history-entry:hover {
background: #eee;
color: #a88;
}

View File

@ -33,38 +33,3 @@ img.column-header-menu {
text-align: center;
margin: 1em 0;
}
.history-panel {
position: absolute;
top: -1px;
right: 20px;
width: 200px;
padding: 2px;
background: white;
border: 1px solid #eee;
height: 200px;
overflow: auto;
}
.history-panel h3 {
margin: 0;
padding: 5px;
background: #fee;
}
.history-past {
padding-bottom: 3px;
border-bottom: 2px solid #aaa;
}
.history-future {
padding-top: 3px;
}
a.history-entry {
display: block;
padding: 3px 5px;
border-bottom: 1px solid #eee;
text-decoration: none;
color: black;
}
a.history-entry:hover {
background: #eee;
color: #a88;
}