Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

ability to create multipolygons #1247

Merged
merged 5 commits into from

2 participants

@ansis
Owner

Right now it overloads merge to just take two or more multipolygons and closed ways and try to make them into one multipolygon. It might be useful to have an extend operation for single-selected areas so that a user can just draw the hole instead of drawing an area and multiselecting.

How should this deal with intersecting (not contained) polygons?

  • create the invalid geometry
  • union all intersecting polygons (there would be undefined behavior in some cases)
  • no action

It can extend multipolygons with rings formed by multiple ways, but it can't create new rings with multiple ways. Not going to implement this unless anyone sees a good reason to.

@jfirebaugh jfirebaugh referenced this pull request
Closed

Multipolygon editing #994

@jfirebaugh jfirebaugh merged commit a997dbb into master
@jfirebaugh jfirebaugh deleted the create-multipolygon branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
1  index.html
@@ -124,6 +124,7 @@
<script src='js/id/actions/disconnect.js'></script>
<script src='js/id/actions/join.js'></script>
<script src='js/id/actions/merge.js'></script>
+ <script src='js/id/actions/merge_polygon.js'></script>
<script src='js/id/actions/move_node.js'></script>
<script src='js/id/actions/move.js'></script>
<script src='js/id/actions/rotate_way.js'></script>
View
109 js/id/actions/merge_polygon.js
@@ -0,0 +1,109 @@
+iD.actions.MergePolygon = function(ids, newRelationId) {
+
+ 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 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) {
+ return polygons.concat(m.joinMemberWays(null, graph));
+ }, []).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
+ // 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(d.locs, w.locs);
+ });
+ });
+
+ // 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)) {
+ d.ids.forEach(function(id) {
+ members.push({
+ type: 'way',
+ id: id,
+ role: outer ? 'outer' : 'inner'
+ });
+ });
+ }
+ });
+ outer = !outer;
+ }
+
+ // Move all tags to one relation
+ var relation = entities.multipolygon[0] ||
+ iD.Relation({ id: newRelationId, 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: {} }));
+ });
+
+ return graph.replace(relation.update({
+ members: members,
+ tags: _.omit(relation.tags, 'area')
+ }));
+ };
+
+ 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;
+};
View
113 js/id/core/relation.js
@@ -168,52 +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 = [], current, first, last, i, how, what;
-
- while (ways.length) {
- current = ways.pop().nodes.slice();
- 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)
-
- ways.splice(i, 1);
- how.apply(current, what);
- }
- }
-
- return joined.map(function(nodes) { return _.pluck(nodes, 'loc'); });
- }
-
function findOuter(inner) {
var o, outer;
@@ -230,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++) {
@@ -243,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')
+ };
+ });
}
+
});
View
10 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() {
View
2  test/index.html
@@ -110,6 +110,7 @@
<script src='../js/id/actions/disconnect.js'></script>
<script src='../js/id/actions/join.js'></script>
<script src='../js/id/actions/merge.js'></script>
+ <script src='../js/id/actions/merge_polygon.js'></script>
<script src='../js/id/actions/move_node.js'></script>
<script src='../js/id/actions/move.js'></script>
<script src='../js/id/actions/rotate_way.js'></script>
@@ -192,6 +193,7 @@
<script src='spec/actions/disconnect.js'></script>
<script src="spec/actions/join.js"></script>
<script src='spec/actions/merge.js'></script>
+ <script src="spec/actions/merge_polygon.js"></script>
<script src="spec/actions/move_node.js"></script>
<script src="spec/actions/move.js"></script>
<script src="spec/actions/noop.js"></script>
View
137 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');
+ });
+});
Something went wrong with that request. Please try again.