diff --git a/css/app.css b/css/app.css index 76f0604c8..fe46af00a 100644 --- a/css/app.css +++ b/css/app.css @@ -261,7 +261,7 @@ ul li { list-style: none;} border-radius: 0 0 3px 3px; } -.toggle-list label > span { +.toggle-list label > span { display: block; overflow: hidden; white-space: nowrap; @@ -569,6 +569,18 @@ button[disabled] .icon.layers { background-position: -300px -40px;} button[disabled] .icon.avatar { background-position: -320px -40px;} button[disabled] .icon.nearby { background-position: -340px -40px;} +.icon.point.deleted { background-position: -302px -80px;} +.icon.line.deleted { background-position: -320px -80px;} +.icon.area.deleted { background-position: -340px -80px;} + +.icon.point.created { background-position: -302px -100px;} +.icon.line.created { background-position: -320px -100px;} +.icon.area.created { background-position: -340px -100px;} + +.icon.point.modified { background-position: -24px 0; } + +.icon.modified { opacity: .5; } + /* Out link is special */ .icon.out-link { height: 14px; width: 14px; background-position: -500px 0;} @@ -2418,6 +2430,8 @@ img.wiki-image { float: none; margin: auto; display: block; + color: white; + font-size: 14px; } .mode-save .user-info img { @@ -2438,12 +2452,15 @@ img.wiki-image { color:#fff; } +.mode-save .commit-info { + margin-bottom: 10px; +} + .mode-save .changeset-list { overflow: auto; border:1px solid #ccc; border-radius: 4px; background:#fff; - max-height: 160px; } .mode-save .warning-section .changeset-list button { @@ -2454,6 +2471,15 @@ img.wiki-image { position: relative; border-top:1px solid #ccc; padding:5px 10px; + cursor: pointer; +} + +.mode-save .changeset-list li:hover { + background-color: #ececec; +} + +.mode-save .changeset-list .alert { + opacity: 0.5; } .changeset-list li span.count { @@ -2461,6 +2487,10 @@ img.wiki-image { color:#555; } +.mode-save .commit-section .changeset-list button { + border-left: 1px solid #CCC; +} + .changeset-list li span.count:before { content: '('; } .changeset-list li span.count:after { content: ')'; } @@ -2698,7 +2728,7 @@ img.wiki-image { } /* Move over tooltips that are near the edge of screen */ .add-point .tooltip { - left: 33.3333% !important; + left: 33.3333% !important; } .curtain-tooltip.intro-points-add .tooltip-arrow, diff --git a/dist/img/sprite.svg b/dist/img/sprite.svg index 411a53f0c..a869d1bd8 100644 --- a/dist/img/sprite.svg +++ b/dist/img/sprite.svg @@ -13,7 +13,7 @@ width="800" height="560" id="svg12393" - inkscape:version="0.48.4 r9939" + inkscape:version="0.48.2 r9819" sodipodi:docname="sprite.svg"> + + diff --git a/js/id/core/difference.js b/js/id/core/difference.js index 9c1a80ca5..12ba48bc3 100644 --- a/js/id/core/difference.js +++ b/js/id/core/difference.js @@ -84,7 +84,6 @@ iD.Difference = function(base, head) { }; difference.addParents = function(entities) { - for (var i in entities) { addParents(head.parentWays(entities[i]), entities); addParents(head.parentRelations(entities[i]), entities); @@ -92,6 +91,55 @@ iD.Difference = function(base, head) { return entities; }; + difference.summary = function() { + var relevant = {}; + + function addEntity(entity, graph, changeType) { + relevant[entity.id] = { + entity: entity, + graph: graph, + changeType: changeType + }; + } + + function addParents(entity) { + var parents = head.parentWays(entity); + for (var j = parents.length - 1; j >= 0; j--) { + var parent = parents[j]; + if (!(parent.id in relevant)) addEntity(parent, head, 'modified'); + } + } + + _.each(changes, function(change) { + if (change.head && change.head.geometry(head) !== 'vertex') { + addEntity(change.head, head, change.base ? 'modified' : 'created'); + + } else if (change.base && change.base.geometry(base) !== 'vertex') { + addEntity(change.base, base, 'deleted'); + + } else if (change.base && change.head) { // modified vertex + var moved = change.base.loc !== change.head.loc, + retagged = change.base.tags !== change.head.tags; + + if (moved) { + addParents(change.head); + } + + if (retagged || (moved && change.head.hasInterestingTags())) { + addEntity(change.head, head, 'modified'); + } + + } else if (change.head && change.head.hasInterestingTags()) { // created vertex + addEntity(change.head, head, 'created'); + + } else if (change.base && change.base.hasInterestingTags()) { // deleted vertex + addEntity(change.base, base, 'deleted'); + } + }); + + return d3.values(relevant); + }; + difference.complete = function(extent) { var result = {}, id, change; diff --git a/js/id/core/history.js b/js/id/core/history.js index 98f27a6d7..4a8edb5d3 100644 --- a/js/id/core/history.js +++ b/js/id/core/history.js @@ -158,10 +158,6 @@ iD.History = function(context) { return this.difference().length() > 0; }, - numChanges: function() { - return this.difference().length(); - }, - imageryUsed: function(sources) { if (sources) { imageryUsed = sources; diff --git a/js/id/modes/save.js b/js/id/modes/save.js index daaaafed2..80d78db4d 100644 --- a/js/id/modes/save.js +++ b/js/id/modes/save.js @@ -1,18 +1,12 @@ iD.modes.Save = function(context) { var ui = iD.ui.Commit(context) .on('cancel', cancel) - .on('fix', fix) .on('save', save); function cancel() { context.enter(iD.modes.Browse(context)); } - function fix(d) { - context.map().zoomTo(d.entity); - context.enter(iD.modes.Select(context, [d.entity.id])); - } - function save(e) { var loading = iD.ui.Loading(context) .message(t('save.uploading')) diff --git a/js/id/ui/commit.js b/js/id/ui/commit.js index 0968cabda..6adc9511f 100644 --- a/js/id/ui/commit.js +++ b/js/id/ui/commit.js @@ -1,31 +1,20 @@ iD.ui.Commit = function(context) { - var event = d3.dispatch('cancel', 'save', 'fix'), - presets = context.presets(); - - function zipSame(d) { - var c = {}, n = -1; - for (var i = 0; i < d.length; i++) { - var desc = { - name: d[i].tags.name || presets.match(d[i], context.graph()).name(), - geometry: d[i].geometry(context.graph()), - count: 1, - tagText: iD.util.tagText(d[i]) - }; - - var fingerprint = desc.name + desc.tagText; - if (c[fingerprint]) { - c[fingerprint].count++; - } else { - c[fingerprint] = desc; - } - } - return _.values(c); - } + var event = d3.dispatch('cancel', 'save'); function commit(selection) { - var changes = context.history().changes(); + var changes = context.history().changes(), + summary = context.history().difference().summary(); - function changesLength(d) { return changes[d].length; } + function zoomToEntity(change) { + var entity = change.entity; + if (change.changeType !== 'deleted' && + context.graph().entity(entity.id).geometry(context.graph()) !== 'vertex') { + context.map().zoomTo(entity); + context.surface().selectAll( + iD.util.entityOrMemberSelector([entity.id], context.graph())) + .classed('hover', true); + } + } var header = selection.append('div') .attr('class', 'header fillL'); @@ -90,7 +79,7 @@ iD.ui.Commit = function(context) { // Confirm Button var saveButton = saveSection.append('button') - .attr('class', 'action col3 button') + .attr('class', 'action col4 button') .on('click.save', function() { event.save({ comment: commentField.node().value @@ -101,11 +90,13 @@ iD.ui.Commit = function(context) { .attr('class', 'label') .text(t('commit.save')); + // Warnings var warnings = body.selectAll('div.warning-section') - .data(iD.validate(changes, context.graph())) + .data([iD.validate(changes, context.graph())]) .enter() .append('div') - .attr('class', 'modal-section warning-section fillL2'); + .attr('class', 'modal-section warning-section fillL2') + .style('display', function(d) { return _.isEmpty(d) ? 'none' : null; }); warnings.append('h3') .text(t('commit.warnings')); @@ -115,52 +106,90 @@ iD.ui.Commit = function(context) { .selectAll('li') .data(function(d) { return d; }) .enter() - .append('li'); + .append('li') + .on('mouseover', mouseover) + .on('mouseout', mouseout) + .on('click', warningClick); - // only show the fix icon when an entity is given - warningLi.filter(function(d) { return d.entity; }) - .append('button') - .attr('class', 'minor') - .on('click', event.fix) - .append('span') - .attr('class', 'icon warning'); + warningLi.append('span') + .attr('class', 'alert icon icon-pre-text'); warningLi.append('strong').text(function(d) { return d.message; }); - var section = body.selectAll('div.commit-section') - .data(['modified', 'deleted', 'created'].filter(changesLength)) + var changeSection = body.selectAll('div.commit-section') + .data([0]) .enter() .append('div') .attr('class', 'commit-section modal-section fillL2'); - section.append('h3') - .text(function(d) { return t('commit.' + d); }) - .append('small') - .attr('class', 'count') - .text(changesLength); + changeSection.append('h3') + .text(summary.length + ' Changes'); - var li = section.append('ul') + var li = changeSection.append('ul') .attr('class', 'changeset-list') .selectAll('li') - .data(function(d) { return zipSame(changes[d]); }) + .data(summary) .enter() - .append('li'); + .append('li') + .on('mouseover', mouseover) + .on('mouseout', mouseout) + .on('click', zoomToEntity); - li.append('strong') - .text(function(d) { - return d.geometry + ' '; + li.append('span') + .attr('class', function(d) { + return d.entity.geometry(d.graph) + ' ' + d.changeType + ' icon icon-pre-text'; }); li.append('span') - .text(function(d) { return d.name; }) - .attr('title', function(d) { return d.tagText; }); + .attr('class', 'change-type') + .text(function(d) { + return d.changeType + ' '; + }); - li.filter(function(d) { return d.count > 1; }) - .append('span') - .attr('class', 'count') - .text(function(d) { return d.count; }); + li.append('strong') + .attr('class', 'entity-type') + .text(function(d) { + return context.presets().match(d.entity, d.graph).name(); + }); + + li.append('span') + .attr('class', 'entity-name') + .text(function(d) { + var name = iD.util.displayName(d.entity) || '', + string = ''; + if (name !== '') string += ':'; + return string += ' ' + name; + }); + + li.style('opacity', 0) + .transition() + .style('opacity', 1); + + li.style('opacity', 0) + .transition() + .style('opacity', 1); + + function mouseover(d) { + if (d.entity) { + context.surface().selectAll( + iD.util.entityOrMemberSelector([d.entity.id], context.graph()) + ).classed('hover', true); + } + } + + function mouseout() { + context.surface().selectAll('.hover') + .classed('hover', false); + } + + function warningClick(d) { + if (d.entity) { + context.map().zoomTo(d.entity); + context.enter(iD.modes.Select(context, [d.entity.id])); + } + } } return d3.rebind(commit, event, 'on'); diff --git a/js/id/ui/save.js b/js/id/ui/save.js index 4d12e0062..926a0e60d 100644 --- a/js/id/ui/save.js +++ b/js/id/ui/save.js @@ -42,13 +42,13 @@ iD.ui.Save = function(context) { var numChanges = 0; context.history().on('change.save', function() { - var _ = history.numChanges(); + var _ = history.difference().summary().length; if (_ === numChanges) return; numChanges = _; tooltip.title(iD.ui.tooltipHtml(t(numChanges > 0 ? - 'save.help' : 'save.no_changes'), key)) + 'save.help' : 'save.no_changes'), key)); button .classed('disabled', numChanges === 0) diff --git a/js/id/validate.js b/js/id/validate.js index c267f8675..19bb18917 100644 --- a/js/id/validate.js +++ b/js/id/validate.js @@ -48,5 +48,5 @@ iD.validate = function(changes, graph) { } } - return warnings.length ? [warnings] : []; + return warnings; }; diff --git a/test/spec/core/difference.js b/test/spec/core/difference.js index 5fb006224..a7920e87c 100644 --- a/test/spec/core/difference.js +++ b/test/spec/core/difference.js @@ -126,6 +126,175 @@ describe("iD.Difference", function () { }); }); + describe("#summary", function () { + var base = iD.Graph({ + 'a': iD.Node({id: 'a', tags: {crossing: 'zebra'}}), + 'b': iD.Node({id: 'b'}), + 'v': iD.Node({id: 'v'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}) + }); + + it("reports a created way as created", function() { + var way = iD.Way({id: '+'}), + head = base.replace(way), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'created', + entity: way, + graph: head + }]); + }); + + it("reports a deleted way as deleted", function() { + var way = base.entity('-'), + head = base.remove(way), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'deleted', + entity: way, + graph: base + }]); + }); + + it("reports a modified way as modified", function() { + var way = base.entity('-').mergeTags({highway: 'primary'}), + head = base.replace(way), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'modified', + entity: way, + graph: head + }]); + }); + + it("reports a way as modified when a member vertex is moved", function() { + var vertex = base.entity('b').move([0,3]), + head = base.replace(vertex), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'modified', + entity: head.entity('-'), + graph: head + }]); + }); + + it("reports a way as modified when a member vertex is added", function() { + var vertex = iD.Node({id: 'c'}), + way = base.entity('-').addNode('c'), + head = base.replace(vertex).replace(way), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'modified', + entity: way, + graph: head + }]); + }); + + it("reports a way as modified when a member vertex is removed", function() { + var way = base.entity('-').removeNode('b'), + head = base.replace(way), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'modified', + entity: way, + graph: head + }]); + }); + + it("reports a created way containing a moved vertex as being created", function() { + var vertex = base.entity('b').move([0,3]), + way = iD.Way({id: '+', nodes: ['b']}), + head = base.replace(way).replace(vertex), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'created', + entity: way, + graph: head + }, { + changeType: 'modified', + entity: head.entity('-'), + graph: head + }]); + }); + + it("reports a created way with a created vertex as being created", function() { + var vertex = iD.Node({id: 'c'}), + way = iD.Way({id: '+', nodes: ['c']}), + head = base.replace(vertex).replace(way), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'created', + entity: way, + graph: head + }]); + }); + + it("reports a vertex as modified when it has tags and they are changed", function() { + var vertex = base.entity('a').mergeTags({highway: 'traffic_signals'}), + head = base.replace(vertex), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'modified', + entity: vertex, + graph: head + }]); + }); + + it("reports a vertex as modified when it has tags and is moved", function() { + var vertex = base.entity('a').move([1, 2]), + head = base.replace(vertex), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'modified', + entity: head.entity('-'), + graph: head + }, { + changeType: 'modified', + entity: vertex, + graph: head + }]); + }); + + it("reports a vertex as deleted when it had tags", function() { + var vertex = base.entity('v'), + head = base.remove(vertex), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'deleted', + entity: vertex, + graph: base + }]); + }); + + it("reports a vertex as created when it has tags", function() { + var vertex = iD.Node({id: 'c', tags: {crossing: 'zebra'}}), + way = base.entity('-').addNode('c'), + head = base.replace(way).replace(vertex), + diff = iD.Difference(base, head); + + expect(diff.summary()).to.eql([{ + changeType: 'modified', + entity: way, + graph: head + }, { + changeType: 'created', + entity: vertex, + graph: head + }]); + }); + }); + describe("#complete", function () { it("includes created entities", function () { var node = iD.Node({id: 'n'}), diff --git a/test/spec/core/history.js b/test/spec/core/history.js index ad68efff4..ec584711e 100644 --- a/test/spec/core/history.js +++ b/test/spec/core/history.js @@ -211,22 +211,6 @@ describe("iD.History", function () { }); }); - describe("#numChanges", function() { - it("is 0 when there are no changes", function() { - expect(history.numChanges()).to.eql(0); - }); - - it("is the sum of all types of changes", function() { - var node1 = iD.Node({id: "n1"}), - node2 = iD.Node(); - history.merge({ n1: node1 }); - history.perform(function (graph) { return graph.remove(node1); }); - expect(history.numChanges()).to.eql(1); - history.perform(function (graph) { return graph.replace(node2); }); - expect(history.numChanges()).to.eql(2); - }); - }); - describe("#reset", function () { it("clears the version stack", function () { history.perform(action, "annotation");