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