Merge pull request #1247 from systemed/create-multipolygon

ability to create multipolygons
This commit is contained in:
John Firebaugh
2013-05-28 16:56:22 -07:00
6 changed files with 321 additions and 51 deletions

View File

@@ -134,6 +134,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 File

@@ -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 File

@@ -176,52 +176,6 @@ _.extend(iD.Relation.prototype, {
.filter(function(m) { return m.type === 'way' && resolver.hasEntity(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;
@@ -238,8 +192,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++) {
@@ -251,5 +205,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 File

@@ -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 File

@@ -116,6 +116,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>
@@ -201,6 +202,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 File

@@ -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');
});
});