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");