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() {