From 62ee84c1a227aca07964862bea62a2665d47dd31 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 3 Apr 2013 10:56:14 -0400 Subject: [PATCH 1/4] ability to create multipolygons --- index.html | 1 + js/id/actions/merge_polygon.js | 106 +++++++++++++++++++++++++++++++++ js/id/operations/merge.js | 10 +++- 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 js/id/actions/merge_polygon.js diff --git a/index.html b/index.html index e8148cd36..eec705bf4 100644 --- a/index.html +++ b/index.html @@ -124,6 +124,7 @@ + diff --git a/js/id/actions/merge_polygon.js b/js/id/actions/merge_polygon.js new file mode 100644 index 000000000..8eb94766f --- /dev/null +++ b/js/id/actions/merge_polygon.js @@ -0,0 +1,106 @@ +iD.actions.MergePolygon = function(ids) { + + function groupEntities(graph) { + var entities = ids.map(graph.getEntity); + return _.extend({ + closedWay: [], + multipolygon: [], + other: [] + }, _.groupBy(entities, function(entity) { + if (entity.type === 'way' && entity.isClosed()) { + return 'closedWay'; + } else if (entity.type === 'relation' && entity.isMultipolygon()) { + return 'multipolygon'; + } else { + return 'other'; + } + })); + } + + var action = function(graph) { + var entities = groupEntities(graph); + + // an array of all the polygons to be merged + var polygons = _.unique(entities.multipolygon.reduce(function(polygons, m) { + return polygons.concat(m.members.filter(function(d) { + return d.type === 'way'; + }).map(function(d) { + return graph.entity(d.id); + })); + }, entities.closedWay)); + + // contained is an array of arrays of boolean values, + // where contained[j][k] is true iff the jth way is + // contained by the kth way. + var contained = polygons.map(function(w, i) { + return polygons.map(function(d, n) { + if (i === n) return null; + return iD.geo.polygonContainsPolygon(getLocs(d), getLocs(w)); + }); + }); + + function getLocs(way) { + return graph.childNodes(way).map(function(d) { return d.loc; }); + } + + + // Sort all polygons as either outer or inner ways + var members = []; + outer = true; + + while (polygons.length) { + extractUncontained(polygons); + polygons = polygons.filter(isContained); + contained = contained.filter(isContained).map(filterContained); + } + + function isContained(d, i) { + return _.any(contained[i]); + } + + function filterContained(d, i) { + return d.filter(isContained); + } + + function extractUncontained(polygons) { + polygons.forEach(function(d, i) { + if (!isContained(d, i)) { + members.push({ + type: 'way', + id: d.id, + role: outer ? 'outer' : 'inner' + }); + } + }); + outer = !outer; + } + + // Move all tags to one relation + var relation = entities.multipolygon[0] || + iD.Relation({ tags: { type: 'multipolygon' }}); + + entities.multipolygon.slice(1).forEach(function(m) { + relation = relation.mergeTags(m.tags); + graph = graph.remove(m); + }); + + members.forEach(function(m) { + var entity = graph.entity(m.id); + relation = relation.mergeTags(entity.tags); + graph = graph.replace(entity.update({ tags: {} })); + }); + + delete relation.tags.area; + + return graph.replace(relation.update({ members: members })); + }; + + action.disabled = function(graph) { + var entities = groupEntities(graph); + if (entities.other.length > 0 || + entities.closedWay.length + entities.multipolygon.length < 2) + return 'not_eligible'; + }; + + return action; +}; diff --git a/js/id/operations/merge.js b/js/id/operations/merge.js index 8cea1dcad..e7b2d5e43 100644 --- a/js/id/operations/merge.js +++ b/js/id/operations/merge.js @@ -1,6 +1,7 @@ iD.operations.Merge = function(selection, context) { var join = iD.actions.Join(selection), - merge = iD.actions.Merge(selection); + merge = iD.actions.Merge(selection), + mergePolygon = iD.actions.MergePolygon(selection); var operation = function() { var annotation = t('operations.merge.annotation', {n: selection.length}), @@ -8,8 +9,10 @@ iD.operations.Merge = function(selection, context) { if (!join.disabled(context.graph())) { action = join; - } else { + } else if (!merge.disabled(context.graph())) { action = merge; + } else { + action = mergePolygon; } var difference = context.perform(action, annotation); @@ -22,7 +25,8 @@ iD.operations.Merge = function(selection, context) { operation.disabled = function() { return join.disabled(context.graph()) && - merge.disabled(context.graph()); + merge.disabled(context.graph()) && + mergePolygon.disabled(context.graph()); }; operation.tooltip = function() { From 0de68c9e4adc1a3bbe0e5734fc9fbec5f79f494c Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 3 Apr 2013 13:18:48 -0400 Subject: [PATCH 2/4] handle extending multipolygons with multiway rings --- js/id/actions/merge_polygon.js | 46 +++++++++++++++++++++------------- js/id/core/relation.js | 13 +++++++--- test/index.html | 1 + 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/js/id/actions/merge_polygon.js b/js/id/actions/merge_polygon.js index 8eb94766f..7472837ae 100644 --- a/js/id/actions/merge_polygon.js +++ b/js/id/actions/merge_polygon.js @@ -20,14 +20,27 @@ iD.actions.MergePolygon = function(ids) { var action = function(graph) { var entities = groupEntities(graph); - // an array of all the polygons to be merged - var polygons = _.unique(entities.multipolygon.reduce(function(polygons, m) { - return polygons.concat(m.members.filter(function(d) { - return d.type === 'way'; - }).map(function(d) { - return graph.entity(d.id); - })); - }, entities.closedWay)); + // An array of objects representing all the polygons that are part of the multipolygon. + // + // Each object has two properties: + // ids - an array of ids of entities that are part of that polygon + // locs - an array of the locations forming the polygon + var polygons = entities.multipolygon.reduce(function(polygons, m) { + m.multipolygon(graph).forEach(function(group) { + group.forEach(function(ring) { + polygons.push({ + ids: ring.ids, + locs: ring + }); + }); + }); + return polygons; + }, []).concat(entities.closedWay.map(function(d) { + return { + ids: [d.id], + locs: graph.childNodes(d).map(function(n) { return n.loc; }) + }; + })); // contained is an array of arrays of boolean values, // where contained[j][k] is true iff the jth way is @@ -35,15 +48,10 @@ iD.actions.MergePolygon = function(ids) { var contained = polygons.map(function(w, i) { return polygons.map(function(d, n) { if (i === n) return null; - return iD.geo.polygonContainsPolygon(getLocs(d), getLocs(w)); + return iD.geo.polygonContainsPolygon(d.locs, w.locs); }); }); - function getLocs(way) { - return graph.childNodes(way).map(function(d) { return d.loc; }); - } - - // Sort all polygons as either outer or inner ways var members = []; outer = true; @@ -65,10 +73,12 @@ iD.actions.MergePolygon = function(ids) { function extractUncontained(polygons) { polygons.forEach(function(d, i) { if (!isContained(d, i)) { - members.push({ - type: 'way', - id: d.id, - role: outer ? 'outer' : 'inner' + d.ids.forEach(function(id) { + members.push({ + type: 'way', + id: id, + role: outer ? 'outer' : 'inner' + }); }); } }); diff --git a/js/id/core/relation.js b/js/id/core/relation.js index 13f7dc294..90de844e8 100644 --- a/js/id/core/relation.js +++ b/js/id/core/relation.js @@ -169,10 +169,12 @@ _.extend(iD.Relation.prototype, { .map(function(m) { return { role: m.role || 'outer', id: m.id, nodes: resolver.childNodes(resolver.entity(m.id)) }; }); function join(ways) { - var joined = [], current, first, last, i, how, what; + var joined = [], way, current, first, last, i, how, what; while (ways.length) { - current = ways.pop().nodes.slice(); + way = ways.pop(); + current = way.nodes.slice(); + current.ids = [way.id]; joined.push(current); while (ways.length && _.first(current) !== _.last(current)) { @@ -206,12 +208,17 @@ _.extend(iD.Relation.prototype, { if (!what) break; // Invalid geometry (unclosed ring) + current.ids.push(ways[i].id); ways.splice(i, 1); how.apply(current, what); } } - return joined.map(function(nodes) { return _.pluck(nodes, 'loc'); }); + return joined.map(function(nodes) { + var locs = _.pluck(nodes, 'loc'); + locs.ids = nodes.ids; + return locs; + }); } function findOuter(inner) { diff --git a/test/index.html b/test/index.html index 569ebe82b..c2f9662a1 100644 --- a/test/index.html +++ b/test/index.html @@ -110,6 +110,7 @@ + From 8470090d7cd0cfce446a3e7a3ec075528814f692 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 3 Apr 2013 15:08:03 -0400 Subject: [PATCH 3/4] add multipolygon creation tests --- js/id/actions/merge_polygon.js | 13 +-- test/spec/actions/merge_polygon.js | 137 +++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 test/spec/actions/merge_polygon.js diff --git a/js/id/actions/merge_polygon.js b/js/id/actions/merge_polygon.js index 7472837ae..d4fded992 100644 --- a/js/id/actions/merge_polygon.js +++ b/js/id/actions/merge_polygon.js @@ -1,4 +1,4 @@ -iD.actions.MergePolygon = function(ids) { +iD.actions.MergePolygon = function(ids, newRelationId) { function groupEntities(graph) { var entities = ids.map(graph.getEntity); @@ -53,7 +53,7 @@ iD.actions.MergePolygon = function(ids) { }); // Sort all polygons as either outer or inner ways - var members = []; + var members = [], outer = true; while (polygons.length) { @@ -87,7 +87,7 @@ iD.actions.MergePolygon = function(ids) { // Move all tags to one relation var relation = entities.multipolygon[0] || - iD.Relation({ tags: { type: 'multipolygon' }}); + iD.Relation({ id: newRelationId, tags: { type: 'multipolygon' }}); entities.multipolygon.slice(1).forEach(function(m) { relation = relation.mergeTags(m.tags); @@ -100,9 +100,10 @@ iD.actions.MergePolygon = function(ids) { graph = graph.replace(entity.update({ tags: {} })); }); - delete relation.tags.area; - - return graph.replace(relation.update({ members: members })); + return graph.replace(relation.update({ + members: members, + tags: _.omit(relation.tags, 'area') + })); }; action.disabled = function(graph) { diff --git a/test/spec/actions/merge_polygon.js b/test/spec/actions/merge_polygon.js new file mode 100644 index 000000000..a972cc8f8 --- /dev/null +++ b/test/spec/actions/merge_polygon.js @@ -0,0 +1,137 @@ +describe("iD.actions.MergePolygon", function () { + + function node(id, x, y) { + e[id] = iD.Node({ id: id, loc: [x, y] }); + } + + function way(id, nodes) { + e[id] = iD.Way({ id: id, nodes: nodes.map(function(n) { return 'n' + n; }) }); + } + + var e = {}; + + node('n0', 0, 0); + node('n1', 5, 0); + node('n2', 5, 5); + node('n3', 0, 5); + + node('n4', 1, 1); + node('n5', 4, 1); + node('n6', 4, 4); + node('n7', 1, 4); + + node('n8', 2, 2); + node('n9', 3, 2); + node('n10', 3, 3); + node('n11', 2, 3); + + node('n13', 8, 8); + node('n14', 8, 9); + node('n15', 9, 9); + + way('w0', [0, 1, 2, 3, 0]); + way('w1', [4, 5, 6, 7, 4]); + way('w2', [8, 9, 10, 11, 8]); + + way('w3', [4, 5, 6]); + way('w4', [6, 7, 4]); + + way('w5', [13, 14, 15, 13]); + + var graph; + + beforeEach(function() { + graph = iD.Graph(e); + }); + + function find(relation, id) { + return _.find(relation.members, function(d) { + return d.id === id; + }); + } + + it("creates a multipolygon from two closed ways", function() { + graph = iD.actions.MergePolygon(['w0', 'w1'], 'r')(graph); + var r = graph.entity('r'); + expect(!!r).to.equal(true); + expect(r.geometry()).to.equal('area'); + expect(r.isMultipolygon()).to.equal(true); + expect(r.members.length).to.equal(2); + expect(find(r, 'w0').role).to.equal('outer'); + expect(find(r, 'w0').type).to.equal('way'); + expect(find(r, 'w1').role).to.equal('inner'); + expect(find(r, 'w1').type).to.equal('way'); + }); + + it("creates a multipolygon from a closed way and a multipolygon relation", function() { + graph = iD.actions.MergePolygon(['w0', 'w1'], 'r')(graph); + graph = iD.actions.MergePolygon(['r', 'w2'])(graph); + var r = graph.entity('r'); + expect(r.members.length).to.equal(3); + }); + + it("creates a multipolygon from two multipolygon relations", function() { + graph = iD.actions.MergePolygon(['w0', 'w1'], 'r')(graph); + graph = iD.actions.MergePolygon(['w2', 'w5'], 'r2')(graph); + graph = iD.actions.MergePolygon(['r', 'r2'])(graph); + + // Delete other relation + expect(graph.entity('r2')).to.equal(undefined); + + var r = graph.entity('r'); + expect(find(r, 'w0').role).to.equal('outer'); + expect(find(r, 'w1').role).to.equal('inner'); + expect(find(r, 'w2').role).to.equal('outer'); + expect(find(r, 'w5').role).to.equal('outer'); + }); + + it("moves all tags to the relation", function() { + graph = graph.replace(e.w0.update({ tags: { 'building': 'yes' }})); + graph = graph.replace(e.w1.update({ tags: { 'natural': 'water' }})); + graph = iD.actions.MergePolygon(['w0', 'w1'], 'r')(graph); + var r = graph.entity('r'); + expect(graph.entity('w0').tags.building).to.equal(undefined); + expect(graph.entity('w1').tags.natural).to.equal(undefined); + expect(r.tags.natural).to.equal('water'); + expect(r.tags.building).to.equal('yes'); + }); + + it("doesn't copy area tags from ways", function() { + graph = graph.replace(e.w0.update({ tags: { 'area': 'yes' }})); + graph = iD.actions.MergePolygon(['w0', 'w1'], 'r')(graph); + var r = graph.entity('r'); + expect(r.tags.area).to.equal(undefined); + }); + + it("creates a multipolygon with two disjunct outer rings", function() { + graph = iD.actions.MergePolygon(['w0', 'w5'], 'r')(graph); + var r = graph.entity('r'); + expect(find(r, 'w0').role).to.equal('outer'); + expect(find(r, 'w5').role).to.equal('outer'); + }); + + it("creates a multipolygon with an island in a hole", function() { + graph = iD.actions.MergePolygon(['w0', 'w1'], 'r')(graph); + graph = iD.actions.MergePolygon(['r', 'w2'])(graph); + var r = graph.entity('r'); + expect(find(r, 'w0').role).to.equal('outer'); + expect(find(r, 'w1').role).to.equal('inner'); + expect(find(r, 'w2').role).to.equal('outer'); + }); + + it("extends a multipolygon with multi-way rings", function() { + console.log('start'); + var r = iD.Relation({ id: 'r', tags: { type: 'multipolygon' }, members: [ + { type: 'way', role: 'outer', id: 'w0' }, + { type: 'way', role: 'inner', id: 'w3' }, + { type: 'way', role: 'inner', id: 'w4' } + ]}); + graph = graph.replace(r); + graph = iD.actions.MergePolygon(['r', 'w2'])(graph); + r = graph.entity('r'); + expect(find(r, 'w0').role).to.equal('outer'); + expect(find(r, 'w2').role).to.equal('outer'); + expect(find(r, 'w3').role).to.equal('inner'); + expect(find(r, 'w4').role).to.equal('inner'); + }); +}); From 05e63c2e1796b187871f79b61d23c8b020457d8e Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 3 Apr 2013 15:53:02 -0400 Subject: [PATCH 4/4] expose relation.joinMemberWays --- js/id/actions/merge_polygon.js | 10 +-- js/id/core/relation.js | 120 ++++++++++++++++++--------------- test/index.html | 1 + 3 files changed, 67 insertions(+), 64 deletions(-) diff --git a/js/id/actions/merge_polygon.js b/js/id/actions/merge_polygon.js index d4fded992..85579e79b 100644 --- a/js/id/actions/merge_polygon.js +++ b/js/id/actions/merge_polygon.js @@ -26,15 +26,7 @@ iD.actions.MergePolygon = function(ids, newRelationId) { // ids - an array of ids of entities that are part of that polygon // locs - an array of the locations forming the polygon var polygons = entities.multipolygon.reduce(function(polygons, m) { - m.multipolygon(graph).forEach(function(group) { - group.forEach(function(ring) { - polygons.push({ - ids: ring.ids, - locs: ring - }); - }); - }); - return polygons; + return polygons.concat(m.joinMemberWays(null, graph)); }, []).concat(entities.closedWay.map(function(d) { return { ids: [d.id], diff --git a/js/id/core/relation.js b/js/id/core/relation.js index 90de844e8..c86bf1186 100644 --- a/js/id/core/relation.js +++ b/js/id/core/relation.js @@ -168,59 +168,6 @@ _.extend(iD.Relation.prototype, { .filter(function(m) { return m.type === 'way' && resolver.entity(m.id); }) .map(function(m) { return { role: m.role || 'outer', id: m.id, nodes: resolver.childNodes(resolver.entity(m.id)) }; }); - function join(ways) { - var joined = [], way, current, first, last, i, how, what; - - while (ways.length) { - way = ways.pop(); - current = way.nodes.slice(); - current.ids = [way.id]; - joined.push(current); - - while (ways.length && _.first(current) !== _.last(current)) { - first = _.first(current); - last = _.last(current); - - for (i = 0; i < ways.length; i++) { - what = ways[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) - - current.ids.push(ways[i].id); - ways.splice(i, 1); - how.apply(current, what); - } - } - - return joined.map(function(nodes) { - var locs = _.pluck(nodes, 'loc'); - locs.ids = nodes.ids; - return locs; - }); - } - function findOuter(inner) { var o, outer; @@ -237,8 +184,8 @@ _.extend(iD.Relation.prototype, { } } - var outers = join(members.filter(function(m) { return m.role === 'outer'; })), - inners = join(members.filter(function(m) { return m.role === 'inner'; })), + var outers = _.pluck(this.joinMemberWays(members.filter(function(m) { return m.role === 'outer'; })), 'locs'), + inners = _.pluck(this.joinMemberWays(members.filter(function(m) { return m.role === 'inner'; })), 'locs'), result = outers.map(function(o) { return [o]; }); for (var i = 0; i < inners.length; i++) { @@ -250,5 +197,68 @@ _.extend(iD.Relation.prototype, { } return result; + }, + + joinMemberWays: function(ways, resolver) { + var joined = [], way, current, first, last, i, how, what; + + ways = ways || this.members.filter(function(m) { + return m.type === 'way'; + }).map(function(m) { + return { + id: m.id, + nodes: resolver.childNodes(resolver.entity(m.id)) + }; + }); + + while (ways.length) { + way = ways.pop(); + current = way.nodes.slice(); + current.ids = [way.id]; + joined.push(current); + + while (ways.length && _.first(current) !== _.last(current)) { + first = _.first(current); + last = _.last(current); + + for (i = 0; i < ways.length; i++) { + what = ways[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) + + current.ids.push(ways[i].id); + ways.splice(i, 1); + how.apply(current, what); + } + } + return joined.map(function(nodes) { + return { + ids: nodes.ids, + locs: _.pluck(nodes, 'loc') + }; + }); } + }); diff --git a/test/index.html b/test/index.html index c2f9662a1..973180028 100644 --- a/test/index.html +++ b/test/index.html @@ -193,6 +193,7 @@ +