diff --git a/src/graphics/big-check.psd b/src/graphics/big-check.psd new file mode 100644 index 000000000..25daff4cc Binary files /dev/null and b/src/graphics/big-check.psd differ diff --git a/src/graphics/checks-map.psd b/src/graphics/checks-map.psd new file mode 100644 index 000000000..b11b63bd9 Binary files /dev/null and b/src/graphics/checks-map.psd differ diff --git a/src/graphics/small-checks.psd b/src/graphics/small-checks.psd new file mode 100644 index 000000000..d24ab3ec0 Binary files /dev/null and b/src/graphics/small-checks.psd differ diff --git a/src/main/java/com/metaweb/gridworks/GridworksServlet.java b/src/main/java/com/metaweb/gridworks/GridworksServlet.java index fec01de15..44157887f 100644 --- a/src/main/java/com/metaweb/gridworks/GridworksServlet.java +++ b/src/main/java/com/metaweb/gridworks/GridworksServlet.java @@ -37,6 +37,7 @@ import com.metaweb.gridworks.commands.recon.ApproveNewReconcileCommand; import com.metaweb.gridworks.commands.recon.ApproveReconcileCommand; import com.metaweb.gridworks.commands.recon.DiscardReconcileCommand; import com.metaweb.gridworks.commands.recon.JudgeOneCellCommand; +import com.metaweb.gridworks.commands.recon.JudgeSimilarCellsCommand; import com.metaweb.gridworks.commands.recon.MatchSpecificTopicCommand; import com.metaweb.gridworks.commands.recon.ReconcileCommand; import com.metaweb.gridworks.commands.util.GetExpressionLanguageInfoCommand; @@ -74,11 +75,12 @@ public class GridworksServlet extends HttpServlet { _commands.put("split-multi-value-cells", new SplitMultiValueCellsCommand()); _commands.put("reconcile", new ReconcileCommand()); - _commands.put("recon-approve-best-matches", new ApproveReconcileCommand()); + _commands.put("recon-match-best-candidates", new ApproveReconcileCommand()); _commands.put("recon-mark-new-topics", new ApproveNewReconcileCommand()); _commands.put("recon-discard-judgments", new DiscardReconcileCommand()); _commands.put("recon-match-specific-topic-to-cells", new MatchSpecificTopicCommand()); _commands.put("recon-judge-one-cell", new JudgeOneCellCommand()); + _commands.put("recon-judge-similar-cells", new JudgeSimilarCellsCommand()); _commands.put("save-protograph", new SaveProtographCommand()); diff --git a/src/main/java/com/metaweb/gridworks/commands/recon/JudgeOneCellCommand.java b/src/main/java/com/metaweb/gridworks/commands/recon/JudgeOneCellCommand.java index 7df62932f..4c622fef2 100644 --- a/src/main/java/com/metaweb/gridworks/commands/recon/JudgeOneCellCommand.java +++ b/src/main/java/com/metaweb/gridworks/commands/recon/JudgeOneCellCommand.java @@ -10,6 +10,7 @@ import javax.servlet.http.HttpServletResponse; import org.json.JSONWriter; import com.metaweb.gridworks.commands.Command; +import com.metaweb.gridworks.expr.ExpressionUtils; import com.metaweb.gridworks.history.Change; import com.metaweb.gridworks.history.HistoryEntry; import com.metaweb.gridworks.model.Cell; @@ -105,7 +106,7 @@ public class JudgeOneCellCommand extends Command { protected HistoryEntry createHistoryEntry() throws Exception { Cell cell = _project.rows.get(rowIndex).getCell(cellIndex); - if (cell == null || cell.value == null) { + if (cell == null || ExpressionUtils.isBlank(cell.value)) { throw new Exception("Cell is blank"); } diff --git a/src/main/java/com/metaweb/gridworks/commands/recon/JudgeSimilarCellsCommand.java b/src/main/java/com/metaweb/gridworks/commands/recon/JudgeSimilarCellsCommand.java new file mode 100644 index 000000000..89bb3d62a --- /dev/null +++ b/src/main/java/com/metaweb/gridworks/commands/recon/JudgeSimilarCellsCommand.java @@ -0,0 +1,220 @@ +package com.metaweb.gridworks.commands.recon; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONWriter; + +import com.metaweb.gridworks.browsing.Engine; +import com.metaweb.gridworks.browsing.FilteredRows; +import com.metaweb.gridworks.browsing.RowVisitor; +import com.metaweb.gridworks.commands.Command; +import com.metaweb.gridworks.expr.ExpressionUtils; +import com.metaweb.gridworks.history.HistoryEntry; +import com.metaweb.gridworks.model.Cell; +import com.metaweb.gridworks.model.Column; +import com.metaweb.gridworks.model.Project; +import com.metaweb.gridworks.model.Recon; +import com.metaweb.gridworks.model.ReconCandidate; +import com.metaweb.gridworks.model.Row; +import com.metaweb.gridworks.model.changes.CellChange; +import com.metaweb.gridworks.model.changes.MassCellChange; +import com.metaweb.gridworks.process.QuickHistoryEntryProcess; + +public class JudgeSimilarCellsCommand extends Command { + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + try { + Project project = getProject(request); + Engine engine = getEngine(request, project); + + int rowIndex = Integer.parseInt(request.getParameter("row")); + int cellIndex = Integer.parseInt(request.getParameter("cell")); + String judgment = request.getParameter("judgment"); + + JudgeSimilarCellsProcess process = null; + + if (judgment != null) { + process = new JudgeSimilarCellsProcess( + project, + engine, + "Judge one cell's recon result", + judgment, + rowIndex, + cellIndex, + request.getParameter("candidate") + ); + } else { + ReconCandidate match = new ReconCandidate( + request.getParameter("topicID"), + request.getParameter("topicGUID"), + request.getParameter("topicName"), + request.getParameter("types").split(","), + 100 + ); + + process = new JudgeSimilarCellsProcess( + project, + engine, + "Judge one cell's recon result", + rowIndex, + cellIndex, + match + ); + } + + boolean done = project.processManager.queueProcess(process); + if (done) { + JSONWriter writer = new JSONWriter(response.getWriter()); + writer.object(); + writer.key("code"); writer.value("ok"); + writer.endObject(); + } else { + respond(response, "{ \"code\" : \"pending\" }"); + } + } catch (Exception e) { + respondException(response, e); + } + } + + protected class JudgeSimilarCellsProcess extends QuickHistoryEntryProcess { + final Engine engine; + final int rowIndex; + final int cellIndex; + final String judgment; + final String candidateID; + final ReconCandidate match; + + JudgeSimilarCellsProcess(Project project, Engine engine, String briefDescription, String judgment, int rowIndex, int cellIndex, String candidateID) { + super(project, briefDescription); + this.engine = engine; + this.rowIndex = rowIndex; + this.cellIndex = cellIndex; + this.judgment = judgment; + this.candidateID = candidateID; + this.match = null; + } + + JudgeSimilarCellsProcess(Project project, Engine engine, String briefDescription, int rowIndex, int cellIndex, ReconCandidate match) { + super(project, briefDescription); + this.engine = engine; + this.rowIndex = rowIndex; + this.cellIndex = cellIndex; + this.judgment = null; + this.candidateID = null; + this.match = match; + } + + protected HistoryEntry createHistoryEntry() throws Exception { + Cell cell = _project.rows.get(rowIndex).getCell(cellIndex); + if (cell == null || ExpressionUtils.isBlank(cell.value)) { + throw new Exception("Cell is blank"); + } + + Column column = _project.columnModel.getColumnByCellIndex(cellIndex); + if (column == null) { + throw new Exception("No such column"); + } + + List cellChanges = new ArrayList(_project.rows.size()); + String similarValue = cell.value.toString(); + + RowVisitor rowVisitor = new RowVisitor() { + List cellChanges; + String similarValue; + + public RowVisitor init(List cellChanges, String similarValue) { + this.cellChanges = cellChanges; + this.similarValue = similarValue; + return this; + } + + public boolean visit(Project project, int rowIndex, Row row, boolean contextual) { + Cell cell = row.getCell(cellIndex); + if (cell != null && !ExpressionUtils.isBlank(cell.value) && similarValue.equals(cell.value)) { + Cell newCell = new Cell( + cell.value, + cell.recon == null ? new Recon() : cell.recon.dup() + ); + + if (match != null) { + newCell.recon.judgment = Recon.Judgment.Matched; + newCell.recon.match = match; + } else { + if ("match".equals(judgment)) { + ReconCandidate match = null; + + if (cell.recon != null) { + for (ReconCandidate c : cell.recon.candidates) { + if (candidateID.equals(c.topicID)) { + match = c; + break; + } + } + } + if (match == null) { + return false; + } + + newCell.recon.judgment = Recon.Judgment.Matched; + newCell.recon.match = match; + } else if ("new".equals(judgment)) { + newCell.recon.judgment = Recon.Judgment.New; + } else if ("discard".equals(judgment)) { + newCell.recon.judgment = Recon.Judgment.None; + newCell.recon.match = null; + } + } + + CellChange cellChange = new CellChange(rowIndex, cellIndex, cell, newCell); + cellChanges.add(cellChange); + } + return false; + } + }.init(cellChanges, similarValue); + + FilteredRows filteredRows = engine.getAllFilteredRows(false); + filteredRows.accept(_project, rowVisitor); + + String description = null; + if (match != null) { + description = "Match " + + cellChanges.size() + " cell(s) in column " + + column.getHeaderLabel() + " containing " + + similarValue + " to topic " + + match.topicName + " (" + + match.topicID + ")"; + } else { + if ("match".equals(judgment)) { + description = "Match " + + cellChanges.size() + " cell(s) in column " + + column.getHeaderLabel() + " containing " + + similarValue + " to topic " + + candidateID; + } else if ("new".equals(judgment)) { + description = "Mark to create new topic for " + + cellChanges.size() + " cell(s) in column " + + column.getHeaderLabel() + " containing " + + similarValue; + } else if ("discard".equals(judgment)) { + description = "Discard recon judgments for " + + cellChanges.size() + " cell(s) in column " + + column.getHeaderLabel() + " containing " + + similarValue; + } + } + + MassCellChange massCellChange = new MassCellChange( + cellChanges, column.getHeaderLabel(), false); + + return new HistoryEntry(_project, description, null, massCellChange); + } + } +} diff --git a/src/main/java/com/metaweb/gridworks/model/AbstractOperation.java b/src/main/java/com/metaweb/gridworks/model/AbstractOperation.java index 8da7a1ee6..8bb96262b 100644 --- a/src/main/java/com/metaweb/gridworks/model/AbstractOperation.java +++ b/src/main/java/com/metaweb/gridworks/model/AbstractOperation.java @@ -15,7 +15,9 @@ import com.metaweb.gridworks.process.QuickHistoryEntryProcess; * projects. */ abstract public class AbstractOperation implements Serializable, Jsonizable { - public Process createProcess(Project project, Properties options) throws Exception { + private static final long serialVersionUID = 3916055862440019600L; + + public Process createProcess(Project project, Properties options) throws Exception { return new QuickHistoryEntryProcess(project, getBriefDescription()) { @Override protected HistoryEntry createHistoryEntry() throws Exception { diff --git a/src/main/webapp/images/checks-map.png b/src/main/webapp/images/checks-map.png new file mode 100644 index 000000000..8e39cbd02 Binary files /dev/null and b/src/main/webapp/images/checks-map.png differ diff --git a/src/main/webapp/scripts/project/data-table-cell-ui.js b/src/main/webapp/scripts/project/data-table-cell-ui.js index cbc5dd0be..832edeae9 100644 --- a/src/main/webapp/scripts/project/data-table-cell-ui.js +++ b/src/main/webapp/scripts/project/data-table-cell-ui.js @@ -51,17 +51,27 @@ DataTableCellUI.prototype._render = function() { } else { $(divContent).html(cell.v); $(' ').appendTo(divContent); - $('mark as new') - .addClass("data-table-recon-action") - .appendTo(divContent).click(function(evt) { - self._doMarkAsNew(); - }); + var ul = $('
').addClass("data-table-recon-candidates").appendTo(divContent); if (this._dataTableView._showRecon && "c" in r && r.c.length > 0) { var candidates = r.c; - var ul = $('
    ').addClass("data-table-recon-candidates").appendTo(divContent); var renderCandidate = function(candidate, index) { - var li = $('
  • ').appendTo(ul); + var li = $('
    ').addClass("data-table-recon-candidate").appendTo(ul); + + $(' ') + .addClass("data-table-recon-match-similar") + .attr("title", "Match this topic to this cell and other cells with the same content") + .appendTo(li).click(function(evt) { + self._doMatchTopicToSimilarCells(candidate.id); + }); + + $(' ') + .addClass("data-table-recon-match") + .attr("title", "Match this topic to this cell") + .appendTo(li).click(function(evt) { + self._doMatchTopicToOneCell(candidate.id); + }); + $('') .addClass("data-table-recon-topic") .attr("href", "http://www.freebase.com/view" + candidate.id) @@ -75,11 +85,6 @@ DataTableCellUI.prototype._render = function() { .appendTo(li); $('').addClass("data-table-recon-score").text("(" + Math.round(candidate.score) + ")").appendTo(li); - $('match') - .addClass("data-table-recon-action") - .appendTo(li).click(function(evt) { - self._doSetAsMatch(candidate.id); - }); }; for (var i = 0; i < candidates.length; i++) { @@ -87,6 +92,23 @@ DataTableCellUI.prototype._render = function() { } } + var liNew = $('
    ').addClass("data-table-recon-candidate").appendTo(ul); + $(' ') + .addClass("data-table-recon-match-similar") + .attr("title", "Create a new topic for this cell and other cells with the same content") + .appendTo(liNew).click(function(evt) { + self._doMatchNewTopicToSimilarCells(); + }); + + $(' ') + .addClass("data-table-recon-match") + .attr("title", "Create a new topic for this cell") + .appendTo(liNew).click(function(evt) { + self._doMatchNewTopicToOneCell(); + }); + + $('').text("(New topic)").appendTo(liNew); + $('') .addClass("data-table-recon-search") .click(function(evt) { @@ -103,14 +125,22 @@ DataTableCellUI.prototype._doRematch = function() { this._doJudgment("discard"); }; -DataTableCellUI.prototype._doMarkAsNew = function() { +DataTableCellUI.prototype._doMatchNewTopicToOneCell = function() { this._doJudgment("new"); }; -DataTableCellUI.prototype._doSetAsMatch = function(candidateID) { +DataTableCellUI.prototype._doMatchNewTopicToSimilarCells = function() { + this._doJudgmentForSimilarCells("new"); +}; + +DataTableCellUI.prototype._doMatchTopicToOneCell = function(candidateID) { this._doJudgment("match", { candidate : candidateID }); }; +DataTableCellUI.prototype._doMatchTopicToSimilarCells = function(candidateID) { + this._doJudgmentForSimilarCells("match", { candidate : candidateID }); +}; + DataTableCellUI.prototype._doJudgment = function(judgment, params) { params = params || {}; params.row = this._rowIndex; @@ -119,10 +149,19 @@ DataTableCellUI.prototype._doJudgment = function(judgment, params) { this.doPostThenUpdate("recon-judge-one-cell", params); }; +DataTableCellUI.prototype._doJudgmentForSimilarCells = function(judgment, params) { + params = params || {}; + params.row = this._rowIndex; + params.cell = this._cellIndex; + params.judgment = judgment; + + ui.dataTableView.doPostThenUpdate("recon-judge-similar-cells", params); +}; + DataTableCellUI.prototype._searchForMatch = function() { var self = this; var frame = DialogSystem.createDialog(); - frame.width("200px"); + frame.width("400px"); var header = $('
    ').addClass("dialog-header").text("Search for Match").appendTo(frame); var body = $('
    ').addClass("dialog-body").appendTo(frame); @@ -131,26 +170,41 @@ DataTableCellUI.prototype._searchForMatch = function() { $('

    ').text("Search Freebase for topic to match " + this._cell.v).appendTo(body); var input = $('').attr("value", this._cell.v).appendTo($('

    ').appendTo(body)); + var match = null; input.suggest({}).bind("fb-select", function(e, data) { - var params = { - row: self._rowIndex, - cell: self._cellIndex, - topicID: data.id, - topicGUID: data.guid, - topicName: data.name, - types: $.map(data.type, function(elmt) { return elmt.id; }).join(",") - }; - self.doPostThenUpdate("recon-judge-one-cell", params); - - DialogSystem.dismissUntil(level - 1); + match = data; }); + var pSimilar = $('

    ').appendTo(body); + var checkSimilar = $('').appendTo(pSimilar); + $('').text(" Match other cells with the same content as well").appendTo(pSimilar); + + $('').text("Match").click(function() { + if (match != null) { + var params = { + row: self._rowIndex, + cell: self._cellIndex, + topicID: match.id, + topicGUID: match.guid, + topicName: match.name, + types: $.map(match.type, function(elmt) { return elmt.id; }).join(",") + }; + if (checkSimilar[0].checked) { + ui.dataTableView.doPostThenUpdate("recon-judge-similar-cells", params); + } else { + self.doPostThenUpdate("recon-judge-one-cell", params); + } + + DialogSystem.dismissUntil(level - 1); + } + }).appendTo(footer); + $('').text("Cancel").click(function() { DialogSystem.dismissUntil(level - 1); }).appendTo(footer); var level = DialogSystem.showDialog(frame); - input[0].focus(); + input.focus().data("suggest").textchange(); }; DataTableCellUI.prototype.createUpdateFunction = function(onBefore) { diff --git a/src/main/webapp/scripts/project/data-table-column-header-ui.js b/src/main/webapp/scripts/project/data-table-column-header-ui.js index 1beb9a7b2..1ee90eceb 100644 --- a/src/main/webapp/scripts/project/data-table-column-header-ui.js +++ b/src/main/webapp/scripts/project/data-table-column-header-ui.js @@ -195,24 +195,31 @@ DataTableColumnHeaderUI.prototype._createMenuForColumnHeader = function(elmt) { }, {}, { - label: "Approve Best Candidates", - tooltip: "Approve best reconciliaton candidate per cell in this column for all current filtered rows", + label: "Match Each Cell to Its Best Candidate", + tooltip: "Match each cell to its best candidate in this column for all current filtered rows", click: function() { - self._doApproveBestCandidates(); + self._doReconMatchBestCandidates(); } }, { - label: "Approve As New Topics", - tooltip: "Set to create new topics for cells in this column for all current filtered rows", + label: "Create a New Topic for Each Cell", + tooltip: "Mark to create one new topic for each cell in this column for all current filtered rows", click: function() { - self._doApproveNewTopics(); + self._doReconMarkNewTopics(false); } }, { - label: "Discard Reconciliation Results", + label: "Create One New Topic for All Cells", + tooltip: "Mark to create one new, common topic for all cells in this column for all current filtered rows", + click: function() { + self._doReconMarkNewTopics(true); + } + }, + { + label: "Discard Reconciliation Judgments", tooltip: "Discard reconciliaton results in this column for all current filtered rows", click: function() { - self._doDiscardReconResults(); + self._doReconDiscardJudgments(); } }, {}, @@ -438,21 +445,21 @@ DataTableColumnHeaderUI.prototype._doReconcile = function() { }; -DataTableColumnHeaderUI.prototype._doDiscardReconResults = function() { +DataTableColumnHeaderUI.prototype._doReconDiscardJudgments = function() { this._dataTableView.doPostThenUpdate( "recon-discard-judgments", { columnName: this._column.headerLabel } ); }; -DataTableColumnHeaderUI.prototype._doApproveBestCandidates = function() { +DataTableColumnHeaderUI.prototype._doReconMatchBestCandidates = function() { this._dataTableView.doPostThenUpdate( - "recon-approve-best-matches", + "recon-match-best-candidates", { columnName: this._column.headerLabel } ); }; -DataTableColumnHeaderUI.prototype._doApproveNewTopics = function() { +DataTableColumnHeaderUI.prototype._doReconMarkNewTopics = function() { this._dataTableView.doPostThenUpdate( "recon-mark-new-topics", { columnName: this._column.headerLabel } @@ -462,7 +469,7 @@ DataTableColumnHeaderUI.prototype._doApproveNewTopics = function() { DataTableColumnHeaderUI.prototype._doSearchToMatch = function() { var self = this; var frame = DialogSystem.createDialog(); - frame.width("200px"); + frame.width("400px"); var header = $('
    ').addClass("dialog-header").text("Search for Match").appendTo(frame); var body = $('
    ').addClass("dialog-body").appendTo(frame); @@ -491,7 +498,7 @@ DataTableColumnHeaderUI.prototype._doSearchToMatch = function() { }).appendTo(footer); var level = DialogSystem.showDialog(frame); - input[0].focus(); + input.focus().data("suggest").textchange(); }; DataTableColumnHeaderUI.prototype._doAddColumn = function(initialExpression) { diff --git a/src/main/webapp/scripts/project/recon-dialog.js b/src/main/webapp/scripts/project/recon-dialog.js index dd6b6e750..fb2e32533 100644 --- a/src/main/webapp/scripts/project/recon-dialog.js +++ b/src/main/webapp/scripts/project/recon-dialog.js @@ -97,6 +97,6 @@ ReconDialog.prototype._createDialog = function() { var level = DialogSystem.showDialog(frame); - input[0].focus(); + input.focus().data("suggest").textchange(); }; diff --git a/src/main/webapp/styles/data-table-view.css b/src/main/webapp/styles/data-table-view.css index e8d2a67ad..37bdd905b 100644 --- a/src/main/webapp/styles/data-table-view.css +++ b/src/main/webapp/styles/data-table-view.css @@ -52,12 +52,15 @@ img.column-header-menu { } -ul.data-table-recon-candidates { - margin: 0.75em 1.5em; - padding: 0; - list-style: circle; +div.data-table-recon-candidates { + margin: 0.5em 0; + min-width: 15em; color: #88a; } +div.data-table-recon-candidate { + padding-left: 35px; + position: relative; +} a.data-table-recon-topic { text-decoration: none; @@ -82,3 +85,35 @@ a.data-table-recon-action:hover, a.data-table-recon-search:hover { text-decoration: underline; color: #008; } + +a.data-table-recon-match { + display: block; + width: 16px; + height: 16px; + background-image: url('../images/checks-map.png'); + background-repeat: no-repeat; + background-position: -17px 0px; + text-decoration: none; + position: absolute; + top: 0px; + left: 0px; +} +a.data-table-recon-match:hover { + background-position: 0px 0px; +} + +a.data-table-recon-match-similar { + display: block; + width: 16px; + height: 16px; + background-image: url('../images/checks-map.png'); + background-repeat: no-repeat; + background-position: -17px -17px; + text-decoration: none; + position: absolute; + top: 0px; + left: 16px; +} +a.data-table-recon-match-similar:hover { + background-position: 0px -17px; +}