diff --git a/css/map.css b/css/map.css index 749aeb7b3..7b35f6bc5 100644 --- a/css/map.css +++ b/css/map.css @@ -181,95 +181,72 @@ path.shadow.selected { } path.area.stroke, -path.multipolygon { +path.line.member-type-multipolygon.stroke { stroke-width:2; - stroke:#fff; } -path.area.fill, -path.multipolygon { - fill:#fff; - fill-opacity:0.3; -} - -path.multipolygon { - fill-rule: evenodd; -} - -path.area.fill.member-type-multipolygon { - fill: none; -} - -path.area.stroke.selected { +path.area.stroke.selected, +path.line.member-type-multipolygon.stroke.selected { stroke-width:4 !important; } -path.area.stroke.tag-natural, -path.multipolygon.tag-natural { +path.area.stroke { + stroke:#fff; +} +path.area.fill { + fill:#fff; + fill-opacity:0.3; + fill-rule: evenodd; +} + +path.stroke.tag-natural { stroke: #b6e199; stroke-width:1; } -path.area.fill.tag-natural, -path.multipolygon.tag-natural { +path.fill.tag-natural { fill: #b6e199; } -path.area.stroke.tag-natural-water, -path.multipolygon.tag-natural-water { +path.stroke.tag-natural-water { stroke: #77d3de; } -path.area.fill.tag-natural-water, -path.multipolygon.tag-natural-water { +path.fill.tag-natural-water { fill: #77d3de; } -path.area.stroke.tag-building, -path.multipolygon.tag-building { +path.stroke.tag-building { stroke: #e06e5f; stroke-width: 1; } -path.area.fill.tag-building, -path.multipolygon.tag-building { +path.fill.tag-building { fill: #e06e5f; } -path.area.stroke.tag-landuse, -path.area.stroke.tag-natural-wood, -path.area.stroke.tag-natural-tree, -path.area.stroke.tag-natural-grassland, -path.area.stroke.tag-leisure-park, -path.multipolygon.tag-landuse, -path.multipolygon.tag-natural-wood, -path.multipolygon.tag-natural-tree, -path.multipolygon.tag-natural-grassland, -path.multipolygon.tag-leisure-park { +path.stroke.tag-landuse, +path.stroke.tag-natural-wood, +path.stroke.tag-natural-tree, +path.stroke.tag-natural-grassland, +path.stroke.tag-leisure-park { stroke: #8cd05f; stroke-width: 1; } -path.area.fill.tag-landuse, -path.area.fill.tag-natural-wood, -path.area.fill.tag-natural-tree, -path.area.fill.tag-natural-grassland, -path.area.fill.tag-leisure-park, -path.multipolygon.tag-landuse, -path.multipolygon.tag-natural-wood, -path.multipolygon.tag-natural-tree, -path.multipolygon.tag-natural-grassland, -path.multipolygon.tag-leisure-park { +path.fill.tag-landuse, +path.fill.tag-natural-wood, +path.fill.tag-natural-tree, +path.fill.tag-natural-grassland, +path.fill.tag-leisure-park { fill: #8cd05f; fill-opacity: 0.2; } -path.area.stroke.tag-amenity-parking, -path.multipolygon.tag-amenity-parking { +path.stroke.tag-amenity-parking { stroke: #aaa; stroke-width: 1; } -path.area.fill.tag-amenity-parking, -path.multipolygon.tag-amenity-parking { +path.fill.tag-amenity-parking { fill: #aaa; } -path.multipolygon.tag-boundary { +path.fill.tag-boundary { fill: none; } @@ -526,7 +503,7 @@ path.casing.tag-railway-subway { /* waterways */ -path.area.fill.tag-waterway { +path.fill.tag-waterway { fill: #77d3de; } @@ -686,9 +663,7 @@ text.point { } .mode-select .area, -.mode-browse .area, -.mode-select .multipolygon, -.mode-browse .multipolygon { +.mode-browse .area { cursor: url(../img/cursor-select-area.png), pointer; } @@ -701,7 +676,6 @@ text.point { .vertex:active, .line:active, .area:active, -.multipolygon:active, .midpoint:active, .mode-select .selected { cursor: url(../img/cursor-select-acting.png), pointer; diff --git a/index.html b/index.html index afcb8e6be..a58aeb8f2 100644 --- a/index.html +++ b/index.html @@ -44,7 +44,6 @@ - diff --git a/js/id/behavior/draw_way.js b/js/id/behavior/draw_way.js index 6b648db0e..8c4c4a5cc 100644 --- a/js/id/behavior/draw_way.js +++ b/js/id/behavior/draw_way.js @@ -28,7 +28,13 @@ iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) { function move(datum) { var loc = context.map().mouseCoordinates(); - if (datum.type === 'node') { + if (datum.id === end.id || datum.id === segment.id) { + context.surface().selectAll('.way, .node') + .filter(function (d) { + return d.id === end.id || d.id === segment.id; + }) + .classed('active', true); + } else if (datum.type === 'node') { loc = datum.loc; } else if (datum.type === 'way') { loc = iD.geo.chooseIndex(datum, d3.mouse(context.surface().node()), context).loc; diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js index 1ed9e8ff1..f8540db1f 100644 --- a/js/id/graph/relation.js +++ b/js/id/graph/relation.js @@ -26,7 +26,7 @@ _.extend(iD.Relation.prototype, { }, geometry: function() { - return 'relation'; + return this.isMultipolygon() ? 'area' : 'relation'; }, // Return the first member with the given role. A copy of the member object @@ -83,6 +83,31 @@ _.extend(iD.Relation.prototype, { return r; }, + asGeoJSON: function(resolver) { + if (this.isMultipolygon()) { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'MultiPolygon', + coordinates: this.multipolygon(resolver) + } + }; + } else { + return { + type: 'FeatureCollection', + properties: this.tags, + features: this.members.map(function(member) { + return _.extend({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver)); + }) + }; + } + }, + + isMultipolygon: function() { + return this.tags.type === 'multipolygon'; + }, + isRestriction: function() { return !!(this.tags.type && this.tags.type.match(/^restriction:?/)); }, @@ -145,22 +170,20 @@ _.extend(iD.Relation.prototype, { } } - return joined; + return joined.map(function (nodes) { return _.pluck(nodes, 'loc'); }); } function findOuter(inner) { var o, outer; - inner = _.pluck(inner, 'loc'); - for (o = 0; o < outers.length; o++) { - outer = _.pluck(outers[o], 'loc'); + outer = outers[o]; if (iD.geo.polygonContainsPolygon(outer, inner)) return o; } for (o = 0; o < outers.length; o++) { - outer = _.pluck(outers[o], 'loc'); + outer = outers[o]; if (iD.geo.polygonIntersectsPolygon(outer, inner)) return o; } diff --git a/js/id/graph/way.js b/js/id/graph/way.js index aae89a6ce..1830e61a4 100644 --- a/js/id/graph/way.js +++ b/js/id/graph/way.js @@ -49,6 +49,7 @@ _.extend(iD.Way.prototype, { isArea: function() { return this.tags.area === 'yes' || (this.isClosed() && + !_.isEmpty(this.tags) && this.tags.area !== 'no' && !this.tags.highway && !this.tags.barrier); @@ -103,13 +104,24 @@ _.extend(iD.Way.prototype, { }, asGeoJSON: function(resolver) { - return { - type: 'Feature', - properties: this.tags, - geometry: { - type: 'LineString', - coordinates: _.pluck(resolver.childNodes(this), 'loc') - } - }; + if (this.isArea()) { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'Polygon', + coordinates: [_.pluck(resolver.childNodes(this), 'loc')] + } + }; + } else { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'LineString', + coordinates: _.pluck(resolver.childNodes(this), 'loc') + } + }; + } } }); diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 3b0ad82dd..532faaca8 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -143,13 +143,22 @@ iD.modes.Select = function(context, selection, initial) { } } + function selected(entity) { + if (!entity) return false; + if (selection.indexOf(entity.id) >= 0) return true; + return d3.select(this).classed('stroke') && + _.any(context.graph().parentRelations(entity), function(parent) { + return selection.indexOf(parent.id) >= 0; + }); + } + d3.select(document) .call(keybinding); context.surface() .on('dblclick.select', dblclick) .selectAll("*") - .filter(function(d) { return d && selection.indexOf(d.id) >= 0; }) + .filter(selected) .classed('selected', true); radialMenu = iD.ui.RadialMenu(operations); diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 19354d7f3..5074abd92 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -19,7 +19,6 @@ iD.Map = function(context) { vertices = iD.svg.Vertices(roundedProjection), lines = iD.svg.Lines(roundedProjection), areas = iD.svg.Areas(roundedProjection), - multipolygons = iD.svg.Multipolygons(roundedProjection), midpoints = iD.svg.Midpoints(roundedProjection), labels = iD.svg.Labels(roundedProjection), tail = d3.tail(), @@ -87,7 +86,6 @@ iD.Map = function(context) { .call(vertices, graph, all, filter) .call(lines, graph, all, filter) .call(areas, graph, all, filter) - .call(multipolygons, graph, all, filter) .call(midpoints, graph, all, filter) .call(labels, graph, all, filter, dimensions, !difference); } diff --git a/js/id/svg.js b/js/id/svg.js index aadb6b830..3b662b2e7 100644 --- a/js/id/svg.js +++ b/js/id/svg.js @@ -27,5 +27,17 @@ iD.svg = { return projection(n.loc); }).join('L')); }; + }, + + MultipolygonMemberTags: function (graph) { + return function (entity) { + var tags = entity.tags; + graph.parentRelations(entity).forEach(function (relation) { + if (relation.isMultipolygon()) { + tags = _.extend({}, relation.tags, tags); + } + }); + return tags; + } } }; diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js index 991b5644c..cd9525d4d 100644 --- a/js/id/svg/areas.js +++ b/js/id/svg/areas.js @@ -1,38 +1,39 @@ iD.svg.Areas = function(projection) { return function drawAreas(surface, graph, entities, filter) { - var areas = []; + var path = d3.geo.path().projection(projection), + areas = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; if (entity.geometry(graph) === 'area') { - var points = graph.childNodes(entity).map(function(n) { - return projection(n.loc); - }); - areas.push({ entity: entity, - area: entity.isDegenerate() ? 0 : Math.abs(d3.geom.polygon(points).area()) + area: Math.abs(path.area(entity.asGeoJSON(graph))) }); } } areas.sort(function(a, b) { return b.area - a.area; }); - var lineString = iD.svg.LineString(projection, graph); + function drawPaths(group, areas, filter, klass) { + var tagClasses = iD.svg.TagClasses(); + + if (klass === 'stroke') { + tagClasses.tags(iD.svg.MultipolygonMemberTags(graph)); + } - function drawPaths(group, areas, filter, classes) { var paths = group.selectAll('path.area') .filter(filter) .data(areas, iD.Entity.key); paths.enter() .append('path') - .attr('class', classes); + .attr('class', function (d) { return d.type + ' area ' + klass; }); paths .order() - .attr('d', lineString) - .call(iD.svg.TagClasses()) + .attr('d', function (entity) { return path(entity.asGeoJSON(graph)); }) + .call(tagClasses) .call(iD.svg.MemberClasses(graph)); paths.exit() @@ -43,9 +44,14 @@ iD.svg.Areas = function(projection) { areas = _.pluck(areas, 'entity'); + var strokes = areas.filter(function (area) { + return area.type === 'way'; + }); + var fill = surface.select('.layer-fill'), - stroke = surface.select('.layer-stroke'), - fills = drawPaths(fill, areas, filter, 'way area fill'), - strokes = drawPaths(stroke, areas, filter, 'way area stroke'); + stroke = surface.select('.layer-stroke'); + + drawPaths(fill, areas, filter, 'fill'); + drawPaths(stroke, strokes, filter, 'stroke'); }; }; diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 9b2cbd1e9..31d2f03d5 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -335,9 +335,8 @@ iD.svg.Labels = function(projection) { } function getAreaLabel(entity, width, height) { - var nodes = _.pluck(graph.childNodes(entity), 'loc') - .map(iD.svg.RoundProjection(projection)), - centroid = d3.geom.polygon(nodes).centroid(), + var path = d3.geo.path().projection(projection), + centroid = path.centroid(entity.asGeoJSON(graph)), extent = entity.extent(graph), entitywidth = projection(extent[1])[0] - projection(extent[0])[0]; diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index a1151204b..2fd7b9152 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -34,19 +34,25 @@ iD.svg.Lines = function(projection) { } return function drawLines(surface, graph, entities, filter) { - function drawPaths(group, lines, filter, classes, lineString) { + function drawPaths(group, lines, filter, klass, lineString) { + var tagClasses = iD.svg.TagClasses(); + + if (klass === 'stroke') { + tagClasses.tags(iD.svg.MultipolygonMemberTags(graph)); + } + var paths = group.selectAll('path.line') .filter(filter) .data(lines, iD.Entity.key); paths.enter() .append('path') - .attr('class', classes); + .attr('class', 'way line ' + klass); paths .order() .attr('d', lineString) - .call(iD.svg.TagClasses()) + .call(tagClasses) .call(iD.svg.MemberClasses(graph)); paths.exit() @@ -65,8 +71,7 @@ iD.svg.Lines = function(projection) { container.remove(); } - var lines = [], - lineStrings = {}; + var lines = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -84,9 +89,9 @@ iD.svg.Lines = function(projection) { stroke = surface.select('.layer-stroke'), defs = surface.select('defs'), text = surface.select('.layer-text'), - shadows = drawPaths(shadow, lines, filter, 'way line shadow', lineString), - casings = drawPaths(casing, lines, filter, 'way line casing', lineString), - strokes = drawPaths(stroke, lines, filter, 'way line stroke', lineString); + shadows = drawPaths(shadow, lines, filter, 'shadow', lineString), + casings = drawPaths(casing, lines, filter, 'casing', lineString), + strokes = drawPaths(stroke, lines, filter, 'stroke', lineString); // Determine the lengths of oneway paths var lengths = {}, diff --git a/js/id/svg/multipolygons.js b/js/id/svg/multipolygons.js deleted file mode 100644 index b4f050ad5..000000000 --- a/js/id/svg/multipolygons.js +++ /dev/null @@ -1,55 +0,0 @@ -iD.svg.Multipolygons = function(projection) { - return function(surface, graph, entities, filter) { - var multipolygons = []; - - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - if (entity.geometry(graph) === 'relation' && entity.tags.type === 'multipolygon') { - multipolygons.push(entity); - } - } - - var lineStrings = {}; - - function lineString(entity) { - if (lineStrings[entity.id] !== undefined) { - return lineStrings[entity.id]; - } - - var multipolygon = entity.multipolygon(graph); - if (entity.members.length === 0 || !multipolygon) { - return (lineStrings[entity.id] = null); - } - - multipolygon = _.flatten(multipolygon, true); - return (lineStrings[entity.id] = - multipolygon.map(function (ring) { - return 'M' + ring.map(function (node) { return projection(node.loc); }).join('L'); - }).join("")); - } - - function drawPaths(group, multipolygons, filter, classes) { - var paths = group.selectAll('path.multipolygon') - .filter(filter) - .data(multipolygons, 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; - } - - var fill = surface.select('.layer-fill'), - paths = drawPaths(fill, multipolygons, filter, 'relation multipolygon'); - }; -}; diff --git a/js/id/svg/tag_classes.js b/js/id/svg/tag_classes.js index 5ce585add..dad388ffe 100644 --- a/js/id/svg/tag_classes.js +++ b/js/id/svg/tag_classes.js @@ -3,10 +3,11 @@ iD.svg.TagClasses = function() { 'highway', 'railway', 'waterway', 'power', 'motorway', 'amenity', 'natural', 'landuse', 'building', 'oneway', 'bridge', 'boundary', 'leisure', 'construction' - ]), tagClassRe = /^tag-/; + ]), tagClassRe = /^tag-/, + tags = function(entity) { return entity.tags; }; - return function tagClassesSelection(selection) { - selection.each(function tagClassesEach(d, i) { + var tagClasses = function(selection) { + selection.each(function tagClassesEach(entity) { var classes, value = this.className; if (value.baseVal !== undefined) value = value.baseVal; @@ -15,11 +16,10 @@ iD.svg.TagClasses = function() { return name.length && !tagClassRe.test(name); }).join(' '); - var tags = d.tags; - for (var k in tags) { + var t = tags(entity); + for (var k in t) { if (!keys[k]) continue; - classes += ' tag-' + k + ' ' + - 'tag-' + k + '-' + tags[k]; + classes += ' tag-' + k + ' ' + 'tag-' + k + '-' + t[k]; } classes = classes.trim(); @@ -29,4 +29,12 @@ iD.svg.TagClasses = function() { } }); }; + + tagClasses.tags = function(_) { + if (!arguments.length) return tags; + tags = _; + return tagClasses; + }; + + return tagClasses; }; diff --git a/test/index.html b/test/index.html index 5f7c19b5a..522f26092 100644 --- a/test/index.html +++ b/test/index.html @@ -47,7 +47,6 @@ - @@ -184,7 +183,6 @@ - diff --git a/test/index_packaged.html b/test/index_packaged.html index d21d48ff2..8dd73d666 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -66,7 +66,6 @@ - diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js index 6e00dcacb..b07f9bee7 100644 --- a/test/spec/graph/relation.js +++ b/test/spec/graph/relation.js @@ -146,107 +146,135 @@ describe('iD.Relation', function () { }); }); + describe("#asGeoJSON", function (){ + it('converts a multipolygon to a GeoJSON MultiPolygon feature', function() { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + g = iD.Graph([a, b, c, w, r]), + json = r.asGeoJSON(g); + + expect(json.type).to.equal('Feature'); + expect(json.properties).to.eql({type: 'multipolygon'}); + expect(json.geometry.type).to.equal('MultiPolygon'); + expect(json.geometry.coordinates).to.eql([[[[1, 1], [2, 2], [3, 3], [1, 1]]]]); + }); + + it('converts a relation to a GeoJSON FeatureCollection', function() { + var a = iD.Node({loc: [1, 1]}), + r = iD.Relation({tags: {type: 'type'}, members: [{id: a.id, role: 'role'}]}), + g = iD.Graph([a, r]), + json = r.asGeoJSON(g); + + expect(json.type).to.equal('FeatureCollection'); + expect(json.properties).to.eql({type: 'type'}); + expect(json.features).to.eql([_.extend({role: 'role'}, a.asGeoJSON(g))]); + }); + }); + describe("#multipolygon", function () { specify("single polygon consisting of a single way", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), r = iD.Relation({members: [{id: w.id, type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]); }); specify("single polygon consisting of multiple ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [c.id, d.id, a.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d, a]]]); // TODO: not the only valid ordering + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc, a.loc]]]); // TODO: not the only valid ordering }); specify("single polygon consisting of multiple ways, one needing reversal", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [a.id, d.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d, a]]]); // TODO: not the only valid ordering + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc, a.loc]]]); // TODO: not the only valid ordering }); specify("multiple polygons consisting of single ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), - e = iD.Node(), - f = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), + e = iD.Node({loc: [5, 5]}), + f = iD.Node({loc: [6, 6]}), w1 = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), w2 = iD.Way({nodes: [d.id, e.id, f.id, d.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, e, f, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]], [[d, e, f, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]], [[d.loc, e.loc, f.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of a single way", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id]}), r = iD.Relation({members: [{id: w.id, type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [c.id, d.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, alternate order", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [c.id, d.id]}), w2 = iD.Way({nodes: [a.id, b.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, one needing reversal", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [d.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, one needing reversal, alternate order", function () { @@ -259,7 +287,7 @@ describe('iD.Relation', function () { r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("single polygon with single single-way inner", function () { @@ -274,7 +302,7 @@ describe('iD.Relation', function () { r = iD.Relation({members: [{id: outer.id, type: 'way'}, {id: inner.id, role: 'inner', type: 'way'}]}), g = iD.Graph([a, b, c, d, e, f, outer, inner, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a], [d, e, f, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]); }); specify("single polygon with single multi-way inner", function () { @@ -293,7 +321,7 @@ describe('iD.Relation', function () { {id: inner1.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, outer, inner1, inner2, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]); }); specify("single polygon with multiple single-way inners", function () { @@ -315,7 +343,7 @@ describe('iD.Relation', function () { {id: inner1.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer, inner1, inner2, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d], [g, h, i, g]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc], [g.loc, h.loc, i.loc, g.loc]]]); }); specify("multiple polygons with single single-way inner", function () { @@ -337,30 +365,30 @@ describe('iD.Relation', function () { {id: inner.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer1, outer2, inner, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]], [[g, h, i, g]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]], [[g.loc, h.loc, i.loc, g.loc]]]); }); specify("invalid geometry: unmatched inner", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), r = iD.Relation({members: [{id: w.id, role: 'inner', type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]); }); specify("incomplete relation", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way(), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, w1, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]); }); }); }); diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js index 6b6b1543f..05864d5c3 100644 --- a/test/spec/graph/way.js +++ b/test/spec/graph/way.js @@ -95,8 +95,12 @@ describe('iD.Way', function() { expect(iD.Way({tags: { area: 'yes' }}).isArea()).to.equal(true); }); - it('returns true if the way is closed and has no tags', function() { - expect(iD.Way({nodes: ['n1', 'n1']}).isArea()).to.equal(true); + it('returns false if the way is closed and has no tags', function() { + expect(iD.Way({nodes: ['n1', 'n1']}).isArea()).to.equal(false); + }); + + it('returns true if the way is closed and has tags', function() { + expect(iD.Way({nodes: ['n1', 'n1'], tags: {a: 'b'}}).isArea()).to.equal(true); }); it('returns false if the way is closed and has tag area=no', function() { @@ -207,7 +211,7 @@ describe('iD.Way', function() { }); describe("#asGeoJSON", function () { - it("converts to a GeoJSON LineString features", function () { + it("converts a line to a GeoJSON LineString features", function () { var a = iD.Node({loc: [1, 2]}), b = iD.Node({loc: [3, 4]}), w = iD.Way({tags: {highway: 'residential'}, nodes: [a.id, b.id]}), @@ -219,5 +223,19 @@ describe('iD.Way', function() { expect(json.geometry.type).to.equal('LineString'); expect(json.geometry.coordinates).to.eql([[1, 2], [3, 4]]); }); + + it("converts an area to a GeoJSON Polygon features", function () { + var a = iD.Node({loc: [1, 2]}), + b = iD.Node({loc: [3, 4]}), + c = iD.Node({loc: [5, 6]}), + w = iD.Way({tags: {area: 'yes'}, nodes: [a.id, b.id, c.id, a.id]}), + graph = iD.Graph([a, b, c, w]), + json = w.asGeoJSON(graph); + + expect(json.type).to.equal('Feature'); + expect(json.properties).to.eql({area: 'yes'}); + expect(json.geometry.type).to.equal('Polygon'); + expect(json.geometry.coordinates).to.eql([[[1, 2], [3, 4], [5, 6], [1, 2]]]); + }); }); }); diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 807a799f4..bbf8674ae 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -69,4 +69,46 @@ describe("iD.svg.Areas", function () { expect(surface.select('.area:nth-child(1)')).to.be.classed('tag-landuse-park'); expect(surface.select('.area:nth-child(2)')).to.be.classed('tag-building-yes'); }); + + it("renders fills for multipolygon areas", function () { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + graph = iD.Graph([a, b, c, w, r]), + areas = [w, r]; + + surface.call(iD.svg.Areas(projection), graph, areas, filter); + + expect(surface.select('.fill')).to.be.classed('relation'); + }); + + it("renders no strokes for multipolygon areas", function () { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + graph = iD.Graph([a, b, c, w, r]), + areas = [w, r]; + + surface.call(iD.svg.Areas(projection), graph, areas, filter); + + expect(surface.selectAll('.stroke')[0].length).to.equal(0); + }); + + it("adds stroke classes for the tags of the parent relation of multipolygon members", function() { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({tags: {area: 'yes'}, nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({members: [{id: w.id}], tags: {type: 'multipolygon', natural: 'wood'}}), + graph = iD.Graph([a, b, c, w, r]); + + surface.call(iD.svg.Areas(projection), graph, [w], filter); + + expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); + expect(surface.select('.fill')).not.to.be.classed('tag-natural-wood'); + }); }); diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index 61c3229a9..989022632 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -39,6 +39,16 @@ describe("iD.svg.Lines", function () { expect(surface.select('.line')).to.be.classed('member-type-route'); }); + it("adds stroke classes for the tags of the parent relation of multipolygon members", function() { + var line = iD.Way(), + relation = iD.Relation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}), + graph = iD.Graph([line, relation]); + + surface.call(iD.svg.Lines(projection), graph, [line], filter); + + expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); + }); + it("preserves non-line paths", function () { var line = iD.Way(), graph = iD.Graph([line]); diff --git a/test/spec/svg/multipolygons.js b/test/spec/svg/multipolygons.js deleted file mode 100644 index 67f44ebae..000000000 --- a/test/spec/svg/multipolygons.js +++ /dev/null @@ -1,43 +0,0 @@ -describe("iD.svg.Multipolygons", function () { - var surface, - projection = Object, - filter = d3.functor(true); - - beforeEach(function () { - surface = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg')) - .call(iD.svg.Surface()); - }); - - it("adds relation and multipolygon classes", function () { - var relation = iD.Relation({tags: {type: 'multipolygon'}}), - graph = iD.Graph([relation]); - - surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); - - expect(surface.select('path')).to.be.classed('relation'); - expect(surface.select('path')).to.be.classed('multipolygon'); - }); - - it("adds tag classes", function () { - var relation = iD.Relation({tags: {type: 'multipolygon', boundary: "administrative"}}), - graph = iD.Graph([relation]); - - surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); - - expect(surface.select('.relation')).to.be.classed('tag-boundary'); - expect(surface.select('.relation')).to.be.classed('tag-boundary-administrative'); - }); - - it("preserves non-multipolygon paths", function () { - var relation = iD.Relation({tags: {type: 'multipolygon'}}), - graph = iD.Graph([relation]); - - surface.select('.layer-fill') - .append('path') - .attr('class', 'other'); - - surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); - - expect(surface.selectAll('.other')[0].length).to.equal(1); - }); -}); diff --git a/test/spec/svg/tag_classes.js b/test/spec/svg/tag_classes.js index dd899e08e..048378815 100644 --- a/test/spec/svg/tag_classes.js +++ b/test/spec/svg/tag_classes.js @@ -19,6 +19,13 @@ describe("iD.svg.TagClasses", function () { expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); + it('adds tags based on the result of the `tags` accessor', function() { + selection + .datum(iD.Entity()) + .call(iD.svg.TagClasses().tags(d3.functor({highway: 'primary'}))); + expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); + }); + it('removes classes for tags that are no longer present', function() { selection .attr('class', 'tag-highway tag-highway-primary')