diff --git a/dist/locales/en.json b/dist/locales/en.json index dd3b7bab7..1ce00720e 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -193,7 +193,9 @@ "restriction": "These features can't be merged because it would damage a \"{relation}\" relation.", "relation": "These features can't be merged because they have conflicting relation roles.", "incomplete_relation": "These features can't be merged because at least one hasn't been fully downloaded.", - "conflicting_tags": "These features can't be merged because some of their tags have conflicting values." + "conflicting_tags": "These features can't be merged because some of their tags have conflicting values.", + "paths_intersect": "These features can't be merged because the resulting path would intersect itself" + }, "move": { "title": "Move", diff --git a/modules/actions/join.js b/modules/actions/join.js index 421cda3ce..cee8a7aa7 100644 --- a/modules/actions/join.js +++ b/modules/actions/join.js @@ -1,8 +1,10 @@ import _extend from 'lodash-es/extend'; import _groupBy from 'lodash-es/groupBy'; +import _intersection from 'lodash-es/intersection'; import { actionDeleteWay } from './delete_way'; import { osmIsInterestingTag, osmJoinWays } from '../osm'; +import { geoPathIntersections } from '../geo'; // Join ways at the end node they share. @@ -70,6 +72,27 @@ export function actionJoin(ids) { if (joined.length > 1) return 'not_adjacent'; + // Loop through all combinations of path-pairs to check potential intersections + // between all pairs + for (var i = 0; i < ids.length-1; i++) { + for (var j = i+1; j < ids.length; j++) { + var path1 = graph.childNodes(graph.entity(ids[i])).map(function(e) { + return e.loc; + }); + var path2 = graph.childNodes(graph.entity(ids[j])).map(function(e) { + return e.loc; + }); + var intersections = geoPathIntersections(path1, path2); + + // Check if intersections are just nodes lying on top of each other/the line, + // as opposed to crossing it + if (_intersection( + joined[0].nodes.map(function(n) { return n.loc.toString(); }), + intersections.map(function(n) { return n.toString(); }) + ).length !== intersections.length) return 'paths_intersect'; + } + } + var nodeIds = joined[0].nodes.map(function(n) { return n.id; }).slice(1, -1); var relation; var tags = {}; diff --git a/test/spec/actions/join.js b/test/spec/actions/join.js index ba37ae329..2f8ed21c5 100644 --- a/test/spec/actions/join.js +++ b/test/spec/actions/join.js @@ -3,9 +3,9 @@ describe('iD.actionJoin', function () { it('returns falsy for ways that share an end/start node', function () { // a --> b ==> c var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['b', 'c']}) ]); @@ -16,9 +16,9 @@ describe('iD.actionJoin', function () { it('returns falsy for ways that share a start/end node', function () { // a <-- b <== c var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['b', 'a']}), iD.osmWay({id: '=', nodes: ['c', 'b']}) ]); @@ -29,9 +29,9 @@ describe('iD.actionJoin', function () { it('returns falsy for ways that share a start/start node', function () { // a <-- b ==> c var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['b', 'a']}), iD.osmWay({id: '=', nodes: ['b', 'c']}) ]); @@ -42,9 +42,9 @@ describe('iD.actionJoin', function () { it('returns falsy for ways that share an end/end node', function () { // a --> b <== c var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['c', 'b']}) ]); @@ -55,10 +55,10 @@ describe('iD.actionJoin', function () { it('returns falsy for more than two ways when connected, regardless of order', function () { // a --> b ==> c ~~> d var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), + iD.osmNode({id: 'd', loc: [6,0]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['b', 'c']}), iD.osmWay({id: '~', nodes: ['c', 'd']}) @@ -74,7 +74,7 @@ describe('iD.actionJoin', function () { it('returns \'not_eligible\' for non-line geometries', function () { var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}) + iD.osmNode({id: 'a', loc: [0,0]}) ]); expect(iD.actionJoin(['a']).disabled(graph)).to.equal('not_eligible'); @@ -85,10 +85,10 @@ describe('iD.actionJoin', function () { // | // d var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), + iD.osmNode({id: 'd', loc: [2,2]}), iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), iD.osmWay({id: '=', nodes: ['b', 'd']}) ]); @@ -102,9 +102,9 @@ describe('iD.actionJoin', function () { // to: = // via: b var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['b', 'c']}), iD.osmRelation({id: 'r', tags: {type: 'restriction'}, members: [ @@ -125,10 +125,10 @@ describe('iD.actionJoin', function () { // to: | // via: b var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), + iD.osmNode({id: 'd', loc: [2,2]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['b', 'c']}), iD.osmWay({id: '|', nodes: ['b', 'd']}), @@ -142,6 +142,25 @@ describe('iD.actionJoin', function () { expect(iD.actionJoin(['-', '=']).disabled(graph)).to.equal('restriction'); }); + it('returns \'paths_intersect\' if resulting way intersects itself', function () { + // d + // | + // a ---b + // | / + // | / + // c + var graph = iD.coreGraph([ + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [0,10]}), + iD.osmNode({id: 'c', loc: [5,5]}), + iD.osmNode({id: 'd', loc: [-5,5]}), + iD.osmWay({id: '-', nodes: ['a', 'b', 'c']}), + iD.osmWay({id: '=', nodes: ['c', 'd']}), + ]); + + expect(iD.actionJoin(['-', '=']).disabled(graph)).to.equal('paths_intersect'); + }); + it('returns falsy in situations where a turn restriction wouldn\'t be damaged (a)', function () { // a --> b ==> c // | @@ -150,10 +169,10 @@ describe('iD.actionJoin', function () { // to: | // via: a var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), + iD.osmNode({id: 'd', loc: [0,2]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['b', 'c']}), iD.osmWay({id: '|', nodes: ['a', 'd']}), @@ -177,10 +196,11 @@ describe('iD.actionJoin', function () { // to: \ // via: b var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), + iD.osmNode({id: 'd', loc: [2,-2]}), + iD.osmNode({id: 'e', loc: [3,2]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['b', 'c']}), iD.osmWay({id: '|', nodes: ['d', 'b']}), @@ -197,9 +217,9 @@ describe('iD.actionJoin', function () { it('returns \'conflicting_tags\' for two entities that have conflicting tags', function () { var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {highway: 'primary'}}), iD.osmWay({id: '=', nodes: ['b', 'c'], tags: {highway: 'secondary'}}) ]); @@ -209,9 +229,9 @@ describe('iD.actionJoin', function () { it('takes tag reversals into account when calculating conflicts', function () { var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {'oneway': 'yes'}}), iD.osmWay({id: '=', nodes: ['c', 'b'], tags: {'oneway': '-1'}}) ]); @@ -221,9 +241,9 @@ describe('iD.actionJoin', function () { it('returns falsy for exceptions to tag conflicts: missing tag', function () { var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {highway: 'primary'}}), iD.osmWay({id: '=', nodes: ['b', 'c'], tags: {}}) ]); @@ -233,9 +253,9 @@ describe('iD.actionJoin', function () { it('returns falsy for exceptions to tag conflicts: uninteresting tag', function () { var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {'tiger:cfcc': 'A41'}}), iD.osmWay({id: '=', nodes: ['b', 'c'], tags: {'tiger:cfcc': 'A42'}}) ]); @@ -248,9 +268,9 @@ describe('iD.actionJoin', function () { // Expected result: // a --> b --> c var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['b', 'c']}) ]); @@ -265,9 +285,9 @@ describe('iD.actionJoin', function () { // Expected result: // a <-- b <-- c var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['b', 'a']}), iD.osmWay({id: '=', nodes: ['c', 'b']}) ]); @@ -281,9 +301,9 @@ describe('iD.actionJoin', function () { // Expected result: // a --> b --> c var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['b', 'a'], tags: {'lanes:forward': 2}}), iD.osmWay({id: '=', nodes: ['b', 'c']}) ]); @@ -300,9 +320,9 @@ describe('iD.actionJoin', function () { // a --> b --> c // tags on === reversed var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['c', 'b'], tags: {'lanes:forward': 2}}) ]); @@ -319,11 +339,11 @@ describe('iD.actionJoin', function () { // a --> b --> c --> d --> e // tags on === reversed var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), - iD.osmNode({id: 'e'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), + iD.osmNode({id: 'd', loc: [6,0]}), + iD.osmNode({id: 'e', loc: [8,0]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['c', 'b'], tags: {'lanes:forward': 2}}), iD.osmWay({id: '+', nodes: ['d', 'c']}), @@ -345,10 +365,10 @@ describe('iD.actionJoin', function () { // Expected result: // a ==> b ==> c ==> d var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), + iD.osmNode({id: 'd', loc: [6,0]}), iD.osmWay({id: 'w-1', nodes: ['a', 'b']}), iD.osmWay({id: 'w1', nodes: ['b', 'c']}), iD.osmWay({id: 'w-2', nodes: ['c', 'd']}) @@ -363,10 +383,11 @@ describe('iD.actionJoin', function () { it('merges tags', function () { var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), - iD.osmNode({id: 'd'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), + iD.osmNode({id: 'd', loc: [6,0]}), + iD.osmNode({id: 'e', loc: [8,0]}), iD.osmWay({id: '-', nodes: ['a', 'b'], tags: {a: 'a', b: '-', c: 'c'}}), iD.osmWay({id: '=', nodes: ['b', 'c'], tags: {a: 'a', b: '=', d: 'd'}}), iD.osmWay({id: '+', nodes: ['c', 'd'], tags: {a: 'a', b: '=', e: 'e'}}) @@ -379,9 +400,9 @@ describe('iD.actionJoin', function () { it('merges relations', function () { var graph = iD.coreGraph([ - iD.osmNode({id: 'a'}), - iD.osmNode({id: 'b'}), - iD.osmNode({id: 'c'}), + iD.osmNode({id: 'a', loc: [0,0]}), + iD.osmNode({id: 'b', loc: [2,0]}), + iD.osmNode({id: 'c', loc: [4,0]}), iD.osmWay({id: '-', nodes: ['a', 'b']}), iD.osmWay({id: '=', nodes: ['b', 'c']}), iD.osmRelation({id: 'r1', members: [