mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 01:02:58 +00:00
Merge pull request #1247 from systemed/create-multipolygon
ability to create multipolygons
This commit is contained in:
@@ -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>
|
||||
|
||||
109
js/id/actions/merge_polygon.js
Normal file
109
js/id/actions/merge_polygon.js
Normal 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;
|
||||
};
|
||||
@@ -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')
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
137
test/spec/actions/merge_polygon.js
Normal file
137
test/spec/actions/merge_polygon.js
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user