From 1d707b0eb2d5b60e1713cb70480c570fcb18ac06 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Sun, 13 Jan 2013 18:17:16 -0800 Subject: [PATCH] Match inners with outers After thinking about how the `evenodd` fill rule works, I pretty sure we don't actually need to do this. If I'm right, I'll back it out, and it will be in the history if we need it for some other purpose. --- js/id/graph/relation.js | 103 +++++++++++++++++++++++------------- js/id/util.js | 12 +++++ test/spec/graph/relation.js | 78 +++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 36 deletions(-) diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js index 08c2a27a5..4788ad0d1 100644 --- a/js/id/graph/relation.js +++ b/js/id/graph/relation.js @@ -25,49 +25,80 @@ iD.Relation = iD.Entity.extend({ .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'; }); + function join(ways) { + var joined = [], current, first, last, i, how, what; - var result = [], current, first, last, i, how, what; + while (ways.length) { + current = ways.pop().nodes.slice(); + joined.push(current); - while (outers.length) { - current = outers.pop().nodes.slice(); - result.push([current]); + while (ways.length && _.first(current) !== _.last(current)) { + first = _.first(current); + last = _.last(current); - while (outers.length && _.first(current) !== _.last(current)) { - first = _.first(current); - last = _.last(current); + for (i = 0; i < ways.length; i++) { + what = ways[i].nodes; - 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 (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) + + ways.splice(i, 1); + how.apply(current, what); } - - if (!what) - break; // Invalid geometry (unclosed ring) - - outers.splice(i, 1); - how.apply(current, what); } + + return joined; + } + + function findOuter(inner) { + var o, outer; + + inner = _.pluck(inner, 'loc'); + + for (o = 0; o < outers.length; o++) { + outer = _.pluck(outers[o], 'loc'); + if (iD.util.geo.polygonContainsPolygon(outer, inner)) + return o; + } + + for (o = 0; o < outers.length; o++) { + outer = _.pluck(outers[o], 'loc'); + if (iD.util.geo.polygonIntersectsPolygon(outer, inner)) + return o; + } + } + + var outers = join(members.filter(function (m) { return m.role === 'outer'; })), + inners = join(members.filter(function (m) { return m.role === 'inner'; })), + result = outers.map(function (o) { return [o]; }); + + for (var i = 0; i < inners.length; i++) { + var o = findOuter(inners[i]); + if (o !== undefined) + result[o].push(inners[i]); + else + result.push(inners[i]); // Invalid geometry } return result; diff --git a/js/id/util.js b/js/id/util.js index 20cb722dc..0ce60c2bd 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -133,3 +133,15 @@ iD.util.geo.pointInPolygon = function(point, polygon) { return inside; }; + +iD.util.geo.polygonContainsPolygon = function(outer, inner) { + return _.every(inner, function (point) { + return iD.util.geo.pointInPolygon(point, outer); + }); +}; + +iD.util.geo.polygonIntersectsPolygon = function(outer, inner) { + return _.some(inner, function (point) { + return iD.util.geo.pointInPolygon(point, outer); + }); +}; diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js index 128c12a2a..26a1a2022 100644 --- a/test/spec/graph/relation.js +++ b/test/spec/graph/relation.js @@ -154,5 +154,83 @@ describe('iD.Relation', function () { expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); }); + + specify("single polygon with single single-way inner", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [1, 0]}), + c = iD.Node({loc: [0, 1]}), + d = iD.Node({loc: [0.1, 0.1]}), + e = iD.Node({loc: [0.2, 0.1]}), + f = iD.Node({loc: [0.1, 0.2]}), + outer = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + inner = iD.Way({nodes: [d.id, e.id, f.id, d.id]}), + 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]]]); + }); + + specify("single polygon with single multi-way inner", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [1, 0]}), + c = iD.Node({loc: [0, 1]}), + d = iD.Node({loc: [0.1, 0.1]}), + e = iD.Node({loc: [0.2, 0.1]}), + f = iD.Node({loc: [0.2, 0.1]}), + outer = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + inner1 = iD.Way({nodes: [d.id, e.id]}), + inner2 = iD.Way({nodes: [e.id, f.id, d.id]}), + r = iD.Relation({members: [ + {id: outer.id, type: 'way'}, + {id: inner2.id, role: 'inner', type: 'way'}, + {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]]]); + }); + + specify("single polygon with multiple single-way inners", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [1, 0]}), + c = iD.Node({loc: [0, 1]}), + d = iD.Node({loc: [0.1, 0.1]}), + e = iD.Node({loc: [0.2, 0.1]}), + f = iD.Node({loc: [0.1, 0.2]}), + g = iD.Node({loc: [0.2, 0.2]}), + h = iD.Node({loc: [0.3, 0.2]}), + i = iD.Node({loc: [0.2, 0.3]}), + outer = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + inner1 = iD.Way({nodes: [d.id, e.id, f.id, d.id]}), + inner2 = iD.Way({nodes: [g.id, h.id, i.id, g.id]}), + r = iD.Relation({members: [ + {id: outer.id, type: 'way'}, + {id: inner2.id, role: 'inner', type: 'way'}, + {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]]]); + }); + + specify("multiple polygons with single single-way inner", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [1, 0]}), + c = iD.Node({loc: [0, 1]}), + d = iD.Node({loc: [0.1, 0.1]}), + e = iD.Node({loc: [0.2, 0.1]}), + f = iD.Node({loc: [0.1, 0.2]}), + g = iD.Node({loc: [0, 0]}), + h = iD.Node({loc: [-1, 0]}), + i = iD.Node({loc: [0, -1]}), + outer1 = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + outer2 = iD.Way({nodes: [g.id, h.id, i.id, g.id]}), + inner = iD.Way({nodes: [d.id, e.id, f.id, d.id]}), + r = iD.Relation({members: [ + {id: outer2.id, type: 'way'}, + {id: outer1.id, type: 'way'}, + {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]]]); + }); }); });