diff --git a/css/map.css b/css/map.css index fc54d7499..bf600b18d 100644 --- a/css/map.css +++ b/css/map.css @@ -154,6 +154,10 @@ path.multipolygon { fill-rule: evenodd; } +path.area.member-type-multipolygon { + fill: none; +} + path.area.selected { stroke-width:4 !important; } diff --git a/index.html b/index.html index 64d839d93..16dce5779 100644 --- a/index.html +++ b/index.html @@ -43,6 +43,7 @@ + diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js index e77fe8a4d..5a3c31288 100644 --- a/js/id/svg/areas.js +++ b/js/id/svg/areas.js @@ -52,7 +52,8 @@ iD.svg.Areas = function(projection) { paths .order() .attr('d', lineString) - .call(iD.svg.TagClasses()); + .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)); paths.exit() .remove(); diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 167a9e75e..375a88f05 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -33,27 +33,27 @@ iD.svg.Lines = function(projection) { return as - bs; } - function drawPaths(group, lines, filter, classes, lineString) { - var paths = group.selectAll('path') - .filter(filter) - .data(lines, iD.Entity.key); - - paths.enter() - .append('path') - .attr('class', classes); - - paths - .order() - .attr('d', lineString) - .call(iD.svg.TagClasses()); - - paths.exit() - .remove(); - - return paths; - } - return function drawLines(surface, graph, entities, filter) { + function drawPaths(group, lines, filter, classes, lineString) { + var paths = group.selectAll('path') + .filter(filter) + .data(lines, iD.Entity.key); + + paths.enter() + .append('path') + .attr('class', classes); + + paths + .order() + .attr('d', lineString) + .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)); + + paths.exit() + .remove(); + + return paths; + } if (!alength) { var arrow = surface.append('text').text(arrowtext); diff --git a/js/id/svg/member_classes.js b/js/id/svg/member_classes.js new file mode 100644 index 000000000..b675288c3 --- /dev/null +++ b/js/id/svg/member_classes.js @@ -0,0 +1,32 @@ +iD.svg.MemberClasses = function(graph) { + var tagClassRe = /^member-?/; + + return function memberClassesSelection(selection) { + selection.each(function memberClassesEach(d, i) { + var classes, value = this.className; + + if (value.baseVal !== undefined) value = value.baseVal; + + classes = value.trim().split(/\s+/).filter(function(name) { + return name.length && !tagClassRe.test(name); + }).join(' '); + + var relations = graph.parentRelations(d); + + if (relations.length) { + classes += ' member'; + } + + relations.forEach(function (relation) { + classes += ' member-type-' + relation.tags.type; + classes += ' member-role-' + _.find(relation.members, function (member) { return member.id == d.id; }).role; + }); + + classes = classes.trim(); + + if (classes !== value) { + d3.select(this).attr('class', classes); + } + }); + }; +}; diff --git a/js/id/svg/multipolygons.js b/js/id/svg/multipolygons.js index bad07ff41..6330cf685 100644 --- a/js/id/svg/multipolygons.js +++ b/js/id/svg/multipolygons.js @@ -40,7 +40,8 @@ iD.svg.Multipolygons = function(projection) { paths .order() .attr('d', lineString) - .call(iD.svg.TagClasses()); + .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)); paths.exit() .remove(); diff --git a/js/id/svg/points.js b/js/id/svg/points.js index 37c4bbf91..5862a5cb9 100644 --- a/js/id/svg/points.js +++ b/js/id/svg/points.js @@ -45,7 +45,8 @@ iD.svg.Points = function(projection) { .attr('transform', 'translate(-8, -8)'); groups.attr('transform', iD.svg.PointTransform(projection)) - .call(iD.svg.TagClasses()); + .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)); // Selecting the following implicitly // sets the data (point entity) on the element diff --git a/js/id/svg/vertices.js b/js/id/svg/vertices.js index ff775fa3e..ce81dface 100644 --- a/js/id/svg/vertices.js +++ b/js/id/svg/vertices.js @@ -31,6 +31,7 @@ iD.svg.Vertices = function(projection) { groups.attr('transform', iD.svg.PointTransform(projection)) .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)) .classed('shared', function(entity) { return graph.parentWays(entity).length > 1; }); // Selecting the following implicitly diff --git a/test/index.html b/test/index.html index 7f63be2a3..0777751d5 100644 --- a/test/index.html +++ b/test/index.html @@ -45,6 +45,7 @@ + @@ -159,6 +160,8 @@ + + diff --git a/test/index_packaged.html b/test/index_packaged.html index 042a0227e..070a2666b 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -64,6 +64,8 @@ + + diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 112e25d03..4f984540a 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -28,6 +28,18 @@ describe("iD.svg.Areas", function () { expect(surface.select('.area')).to.be.classed('tag-building-yes'); }); + it("adds member classes", function () { + var area = iD.Way({tags: {area: 'yes'}}), + relation = iD.Relation({members: [{id: area.id, role: 'outer'}], tags: {type: 'multipolygon'}}), + graph = iD.Graph([area, relation]); + + surface.call(iD.svg.Areas(projection), graph, [area], filter); + + expect(surface.select('.area')).to.be.classed('member'); + expect(surface.select('.area')).to.be.classed('member-role-outer'); + expect(surface.select('.area')).to.be.classed('member-type-multipolygon'); + }); + it("preserves non-area paths", function () { var area = iD.Way({tags: {area: 'yes'}}), graph = iD.Graph([area]); diff --git a/test/spec/svg/member_classes.js b/test/spec/svg/member_classes.js new file mode 100644 index 000000000..651ba2342 --- /dev/null +++ b/test/spec/svg/member_classes.js @@ -0,0 +1,54 @@ +describe("iD.svg.MemberClasses", function () { + var selection; + + beforeEach(function () { + selection = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'g')); + }); + + it("adds no classes to elements that aren't a member of any relations", function() { + var node = iD.Node(), + graph = iD.Graph([node]); + + selection + .datum(node) + .call(iD.svg.MemberClasses(graph)); + + expect(selection.attr('class')).to.equal(null); + }); + + it("adds tags for member, role, and type", function() { + var node = iD.Node(), + relation = iD.Relation({members: [{id: node.id, role: 'r'}], tags: {type: 't'}}), + graph = iD.Graph([node, relation]); + + selection + .datum(node) + .call(iD.svg.MemberClasses(graph)); + + expect(selection.attr('class')).to.equal('member member-type-t member-role-r'); + }); + + it('removes classes for tags that are no longer present', function() { + var node = iD.Entity(), + graph = iD.Graph([node]); + + selection + .attr('class', 'member member-type-t member-role-r') + .datum(node) + .call(iD.svg.MemberClasses(graph)); + + expect(selection.attr('class')).to.equal(''); + }); + + it("preserves existing non-'member-'-prefixed classes", function() { + var node = iD.Entity(), + graph = iD.Graph([node]); + + selection + .attr('class', 'selected') + .datum(node) + .call(iD.svg.MemberClasses(graph)); + + expect(selection.attr('class')).to.equal('selected'); + }); +});