diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js index 92519751c..08c2a27a5 100644 --- a/js/id/graph/relation.js +++ b/js/id/graph/relation.js @@ -8,5 +8,68 @@ iD.Relation = iD.Entity.extend({ geometry: function() { return 'relation'; + }, + + // Returns an array [A0, ... An], each Ai being an array of node arrays [Nds0, ... Ndsm], + // where Nds0 is an outer ring and subsequent Ndsi's (if any i > 0) being inner rings. + // + // This corresponds to the structure needed for rendering a multipolygon path using a + // `evenodd` fill rule, as well as the structure of a GeoJSON MultiPolygon geometry. + // + // In the case of invalid geometries, this function will still return a result which + // includes the nodes of all way members, but some Nds may be unclosed and some inner + // rings not matched with the intended outer ring. + // + multipolygon: function(resolver) { + var members = this.members + .filter(function (m) { return m.type === 'way'; }) + .map(function (m) { return { role: m.role || 'outer', id: m.id, nodes: resolver.fetch(m.id).nodes }; }); + + var outers = members.filter(function (m) { return m.role === 'outer'; }), + inners = members.filter(function (m) { return m.role === 'inner'; }); + + var result = [], current, first, last, i, how, what; + + while (outers.length) { + current = outers.pop().nodes.slice(); + result.push([current]); + + while (outers.length && _.first(current) !== _.last(current)) { + first = _.first(current); + last = _.last(current); + + for (i = 0; i < outers.length; i++) { + what = outers[i].nodes; + + if (last === _.first(what)) { + how = current.push; + what = what.slice(1); + break; + } else if (last === _.last(what)) { + how = current.push; + what = what.slice(0, -1).reverse(); + break; + } else if (first == _.last(what)) { + how = current.unshift; + what = what.slice(0, -1); + break; + } else if (first == _.first(what)) { + how = current.unshift; + what = what.slice(1).reverse(); + break; + } else { + what = how = null; + } + } + + if (!what) + break; // Invalid geometry (unclosed ring) + + outers.splice(i, 1); + how.apply(current, what); + } + } + + return result; } }); diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js index dfeeb4e5c..128c12a2a 100644 --- a/test/spec/graph/relation.js +++ b/test/spec/graph/relation.js @@ -38,4 +38,121 @@ describe('iD.Relation', function () { describe("#extent", function () { it("returns the minimal extent containing the extents of all members"); }); + + describe("#multipolygon", function () { + specify("single polygon consisting of a single way", function () { + var a = iD.Node(), + b = iD.Node(), + c = iD.Node(), + 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]]]); + }); + + specify("single polygon consisting of multiple ways", function () { + var a = iD.Node(), + b = iD.Node(), + c = iD.Node(), + d = iD.Node(), + 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 + }); + + specify("single polygon consisting of multiple ways, one needing reversal", function () { + var a = iD.Node(), + b = iD.Node(), + c = iD.Node(), + d = iD.Node(), + 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 + }); + + 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(), + 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]]]); + }); + + specify("invalid geometry: unclosed ring consisting of a single way", function () { + var a = iD.Node(), + b = iD.Node(), + c = iD.Node(), + 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]]]); + }); + + specify("invalid geometry: unclosed ring consisting of multiple ways", function () { + var a = iD.Node(), + b = iD.Node(), + c = iD.Node(), + d = iD.Node(), + 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]]]); + }); + + 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(), + 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]]]); + }); + + 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(), + 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]]]); + }); + + specify("invalid geometry: unclosed ring consisting of multiple ways, one needing reversal, alternate order", function () { + var a = iD.Node(), + b = iD.Node(), + c = iD.Node(), + d = iD.Node(), + w1 = iD.Way({nodes: [c.id, d.id]}), + w2 = iD.Way({nodes: [c.id, b.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]]]); + }); + }); });