From 6976251ac27d00cb0f98013eafee5c57066e8f0c Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Thu, 30 Jan 2020 17:37:52 +0000 Subject: [PATCH 01/10] Handle almost junction where end node are close Based on distances at which nodes are joined by the quickfix and the validation extends ahead of an end node, if both ways have close end nodes and joining them would result in a small change of angle for the edited way then they will be joined. Closes #7201 --- modules/validations/almost_junction.js | 52 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index cbf3aa168..ef587ea20 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -1,7 +1,7 @@ import { geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon, geoSphericalDistance, geoVecInterp, geoHasSelfIntersections, - geoSphericalClosestNode + geoSphericalClosestNode, geoAngle } from '../geo'; import { actionAddMidpoint } from '../actions/add_midpoint'; @@ -213,6 +213,42 @@ export function validationAlmostJunction(context) { return true; } + function findNearbyEndNodes(node, way2) { + return [ + way2.nodes[0], + way2.nodes[way2.nodes.length - 1] + ].map(d => graph.entity(d)) + .filter(d => { + // Node cannot be near to itself, but other endnode of same way could be + // 4.25m based on extending 5m ahead and .75m quick fix node joining + return d.id !== node.id + && geoSphericalDistance(node.loc, d.loc) <= 4.25; + }); + } + + function findAlmostCollinear(midNode, tipNode, endNodes) { + // Both nodes could be close, so want to join whichever is closest to collinear + let mostCollinear; + let minAngle = Infinity; + + // Checks midNode -> tipNode -> endNode for collinearity + endNodes.forEach(endNode => { + const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI; + const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI; + const diff = Math.max(a1, a2) - Math.min(a1, a2); + + if (diff < minAngle) { + mostCollinear = endNode; + minAngle = diff; + } + }); + + /* 9° threshold set by considering right angle triangle + based on .75m node joining threshold and 5m extension */ + if (minAngle <= 9 * Math.PI / 180) return mostCollinear; + + return null; + } function canConnectByExtend(way, endNodeIdx) { var EXTEND_TH_METERS = 5; @@ -253,6 +289,20 @@ export function validationAlmostJunction(context) { nB = graph.entity(nBid); var crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); if (crossLoc) { + // When endpoints are close, just join if resulting small change in angle (#7201) + let nearEndNodes = findNearbyEndNodes(tipNode, way2); + if (nearEndNodes.length > 0) { + let collinear = findAlmostCollinear(midNode, tipNode, nearEndNodes); + if (collinear) { + return { + node: tipNode, + wid: way2.id, + edge: [collinear.id, collinear.id], + cross_loc: collinear.loc + }; + } + } + return { node: tipNode, wid: way2.id, From 917c8f0b94da0bece8cab64ce0f9fa0307200222 Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:23:49 +0000 Subject: [PATCH 02/10] Add tests for close endpoint almost junctions --- test/spec/validations/almost_junction.js | 139 +++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index 83cef265e..05223d467 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -126,6 +126,79 @@ describe('iD.validations.almost_junction', function () { ); } + function closeEndNodesSmallAngle() { + // Vertical path + var n1 = iD.osmNode({id: 'n-1', loc: [0.0003247, 22.4423866]}); + var n2 = iD.osmNode({id: 'n-2', loc: [0.0003060, 22.4432671]}); + var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n1), + iD.actionAddEntity(n2), + iD.actionAddEntity(w1) + ); + + // Angled path with end node within 4.25m and change of angle <9° + var n3 = iD.osmNode({id: 'n-3', loc: [0.0003379, 22.4423861]}); + var n4 = iD.osmNode({id: 'n-4', loc: [0.0004354, 22.4421312]}); + var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n3), + iD.actionAddEntity(n4), + iD.actionAddEntity(w2) + ); + + return n1; + } + + function closeEndNodesBigAngle() { + // Vertical path + var n1 = iD.osmNode({id: 'n-1', loc: [0, 22.4427453]}); + var n2 = iD.osmNode({id: 'n-2', loc: [0, 22.4429806]}); + var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n1), + iD.actionAddEntity(n2), + iD.actionAddEntity(w1) + ); + + // Horizontal path with end node within 4.25m and change of angle >9° + var n3 = iD.osmNode({id: 'n-3', loc: [0.0000199, 22.4427801]}); + var n4 = iD.osmNode({id: 'n-4', loc: [0.0002038, 22.4427801]}); + var w2 = iD.osmWay({id: 'w-2', nodes: ['n-3', 'n-4'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n3), + iD.actionAddEntity(n4), + iD.actionAddEntity(w2) + ); + + return n1; + } + + function closeEndNodesSmallAngleSelf() { + // Square path that ends within 4.25m of itself and change of angle <9° + var n1 = iD.osmNode({id: 'n-1', loc: [0, 22.4427453]}); + var n2 = iD.osmNode({id: 'n-2', loc: [0, 22.4429811]}); + var n3 = iD.osmNode({id: 'n-3', loc: [0.0001923, 22.4429811]}); + var n4 = iD.osmNode({id: 'n-4', loc: [0.0001923, 22.4427523]}); + var n5 = iD.osmNode({id: 'n-5', loc: [0.0000134, 22.4427523]}); + var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2', 'n-3', 'n-4', 'n-5'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n1), + iD.actionAddEntity(n2), + iD.actionAddEntity(n3), + iD.actionAddEntity(n4), + iD.actionAddEntity(n5), + iD.actionAddEntity(w1) + ); + + return n1; + } + function validate() { var validator = iD.validationAlmostJunction(context); var changes = context.history().changes(); @@ -220,4 +293,70 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); + // TODO: Test case of both endpoints of another way close, should prioritise smaller angle change + it('joins close endpoints if insignificant angle change', function() { + var n1 = closeEndNodesSmallAngle(); + var issues = validate(); + expect(issues).to.have.lengthOf(1); + var issue = issues[0]; + expect(issue.type).to.eql('almost_junction'); + expect(issue.subtype).to.eql('highway-highway'); + expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds[0]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-3'); + expect(issue.entityIds[2]).to.eql('w-1'); + + // Duplicate edge nodes means endpoints will be joined + expect(issue.data.edge).to.have.lengthOf(2); + expect(issue.data.edge[0]).to.eql('n-1'); + expect(issue.data.edge[1]).to.eql('n-1'); + + // Crossing set to loc of end node means endpoints will be joined + expect(issue.data.cross_loc).to.have.lengthOf(2); + expect(issue.data.cross_loc).to.eql(n1.loc); + }); + + it('won\'t join close endpoints if significant angle change', function() { + var n1 = closeEndNodesBigAngle(); + var issues = validate(); + expect(issues).to.have.lengthOf(1); + var issue = issues[0]; + expect(issue.type).to.eql('almost_junction'); + expect(issue.subtype).to.eql('highway-highway'); + expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds[0]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-3'); + expect(issue.entityIds[2]).to.eql('w-1'); + + // Differing edge nodes means endpoints won't be joined + expect(issue.data.edge).to.have.lengthOf(2); + expect(issue.data.edge[0]).to.eql('n-1'); + expect(issue.data.edge[1]).to.eql('n-2'); + + // Crossing different from loc of end node means endpoints won't be joined + expect(issue.data.cross_loc).to.have.lengthOf(2); + expect(issue.data.cross_loc).to.not.eql(n1.loc); + }); + + it('joins close endpoints of the same way', function() { + var n1 = closeEndNodesSmallAngleSelf(); + var issues = validate(); + expect(issues).to.have.lengthOf(1); + var issue = issues[0]; + expect(issue.type).to.eql('almost_junction'); + expect(issue.subtype).to.eql('highway-highway'); + expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds[0]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-5'); + expect(issue.entityIds[2]).to.eql('w-1'); + + // Duplicate edge nodes means endpoints will be joined + expect(issue.data.edge).to.have.lengthOf(2); + expect(issue.data.edge[0]).to.eql('n-1'); + expect(issue.data.edge[1]).to.eql('n-1'); + + // Crossing set to loc of end node means endpoints will be joined + expect(issue.data.cross_loc).to.have.lengthOf(2); + expect(issue.data.cross_loc).to.eql(n1.loc); + }); }); From ec6ad0000b9fa6c82de94eebed02c4cc0e5386cb Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:37:06 +0000 Subject: [PATCH 03/10] Show derivation of close node thresholds in code --- modules/validations/almost_junction.js | 28 +++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index ef587ea20..a1168b78a 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -18,8 +18,13 @@ import { services } from '../services'; * Look for roads that can be connected to other roads with a short extension */ export function validationAlmostJunction(context) { - var type = 'almost_junction'; - + const type = 'almost_junction'; + const EXTEND_TH_METERS = 5; + const WELD_TH_METERS = 0.75; + // Comes from considering bounding case of parallel ways + const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS; + // Comes from considering bounding case of perpendicular ways + const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS); function isHighway(entity) { return entity.type === 'way' && @@ -89,7 +94,7 @@ export function validationAlmostJunction(context) { var annotation = t('issues.fix.connect_almost_junction.annotation'); // already a point nearby, just connect to that - if (closestNodeInfo.distance < 0.75) { + if (closestNodeInfo.distance < WELD_TH_METERS) { context.perform( actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc), annotation @@ -220,15 +225,14 @@ export function validationAlmostJunction(context) { ].map(d => graph.entity(d)) .filter(d => { // Node cannot be near to itself, but other endnode of same way could be - // 4.25m based on extending 5m ahead and .75m quick fix node joining return d.id !== node.id - && geoSphericalDistance(node.loc, d.loc) <= 4.25; + && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH; }); } - function findAlmostCollinear(midNode, tipNode, endNodes) { + function findSmallJoinAngle(midNode, tipNode, endNodes) { // Both nodes could be close, so want to join whichever is closest to collinear - let mostCollinear; + let joinTo; let minAngle = Infinity; // Checks midNode -> tipNode -> endNode for collinearity @@ -238,14 +242,14 @@ export function validationAlmostJunction(context) { const diff = Math.max(a1, a2) - Math.min(a1, a2); if (diff < minAngle) { - mostCollinear = endNode; + joinTo = endNode; minAngle = diff; } }); - /* 9° threshold set by considering right angle triangle - based on .75m node joining threshold and 5m extension */ - if (minAngle <= 9 * Math.PI / 180) return mostCollinear; + /* Threshold set by considering right angle triangle + based on node joining threshold and extension distance */ + if (minAngle <= SIG_ANGLE_TH) return joinTo; return null; } @@ -292,7 +296,7 @@ export function validationAlmostJunction(context) { // When endpoints are close, just join if resulting small change in angle (#7201) let nearEndNodes = findNearbyEndNodes(tipNode, way2); if (nearEndNodes.length > 0) { - let collinear = findAlmostCollinear(midNode, tipNode, nearEndNodes); + let collinear = findSmallJoinAngle(midNode, tipNode, nearEndNodes); if (collinear) { return { node: tipNode, From 3c7b0925344d14adf586662146c3dc1924b2735b Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:47:19 +0000 Subject: [PATCH 04/10] Update almost junction validation with ES6 syntax --- modules/validations/almost_junction.js | 118 ++++++++++++------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index a1168b78a..a95a6855a 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -27,38 +27,39 @@ export function validationAlmostJunction(context) { const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS); function isHighway(entity) { - return entity.type === 'way' && - osmRoutableHighwayTagValues[entity.tags.highway]; + return entity.type === 'way' + && osmRoutableHighwayTagValues[entity.tags.highway]; } function isTaggedAsNotContinuing(node) { - return node.tags.noexit === 'yes' || - node.tags.amenity === 'parking_entrance' || - (node.tags.entrance && node.tags.entrance !== 'no'); + return node.tags.noexit === 'yes' + || node.tags.amenity === 'parking_entrance' + || (node.tags.entrance && node.tags.entrance !== 'no'); } - var validation = function checkAlmostJunction(entity, graph) { + const validation = function checkAlmostJunction(entity, graph) { if (!isHighway(entity)) return []; if (entity.isDegenerate()) return []; - var tree = context.history().tree(); - var issues = []; + const tree = context.history().tree(); + const extendableNodeInfos = findConnectableEndNodesByExtension(entity); - var extendableNodeInfos = findConnectableEndNodesByExtension(entity); - extendableNodeInfos.forEach(function(extendableNodeInfo) { + let issues = []; + + extendableNodeInfos.forEach(extendableNodeInfo => { issues.push(new validationIssue({ - type: type, + type, subtype: 'highway-highway', severity: 'warning', - message: function(context) { - var entity1 = context.hasEntity(this.entityIds[0]); + message(context) { + const entity1 = context.hasEntity(this.entityIds[0]); if (this.entityIds[0] === this.entityIds[2]) { return entity1 ? t('issues.almost_junction.self.message', { feature: utilDisplayLabel(entity1, context) }) : ''; } else { - var entity2 = context.hasEntity(this.entityIds[2]); + const entity2 = context.hasEntity(this.entityIds[2]); return (entity1 && entity2) ? t('issues.almost_junction.message', { feature: utilDisplayLabel(entity1, context), feature2: utilDisplayLabel(entity2, context) @@ -81,18 +82,18 @@ export function validationAlmostJunction(context) { function makeFixes(context) { - var fixes = [new validationIssueFix({ + let fixes = [new validationIssueFix({ icon: 'iD-icon-abutment', title: t('issues.fix.connect_features.title'), - onClick: function(context) { - var endNodeId = this.issue.entityIds[1]; - var endNode = context.entity(endNodeId); - var targetEdge = this.issue.data.edge; - var crossLoc = this.issue.data.cross_loc; - var edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; - var closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); + onClick(context) { + const endNodeId = this.issue.entityIds[1]; + const endNode = context.entity(endNodeId); + const targetEdge = this.issue.data.edge; + const crossLoc = this.issue.data.cross_loc; + const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; + const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); - var annotation = t('issues.fix.connect_almost_junction.annotation'); + const annotation = t('issues.fix.connect_almost_junction.annotation'); // already a point nearby, just connect to that if (closestNodeInfo.distance < WELD_TH_METERS) { context.perform( @@ -109,15 +110,15 @@ export function validationAlmostJunction(context) { } })]; - var node = context.hasEntity(this.entityIds[1]); + const node = context.hasEntity(this.entityIds[1]); if (node && !node.hasInterestingTags()) { // node has no descriptive tags, suggest noexit fix fixes.push(new validationIssueFix({ icon: 'maki-barrier', title: t('issues.fix.tag_as_disconnected.title'), - onClick: function(context) { - var nodeID = this.issue.entityIds[1]; - var tags = Object.assign({}, context.entity(nodeID).tags); + onClick(context) { + const nodeID = this.issue.entityIds[1]; + const tags = Object.assign({}, context.entity(nodeID).tags); tags.noexit = 'yes'; context.perform( actionChangeTags(nodeID, tags), @@ -143,7 +144,7 @@ export function validationAlmostJunction(context) { function isExtendableCandidate(node, way) { // can not accurately test vertices on tiles not downloaded from osm - #5938 - var osm = services.osm; + const osm = services.osm; if (osm && !osm.isDataLoaded(node.loc)) { return false; } @@ -151,8 +152,8 @@ export function validationAlmostJunction(context) { return false; } - var occurences = 0; - for (var index in way.nodes) { + let occurences = 0; + for (const index in way.nodes) { if (way.nodes[index] === node.id) { occurences += 1; if (occurences > 1) { @@ -165,18 +166,18 @@ export function validationAlmostJunction(context) { function findConnectableEndNodesByExtension(way) { - var results = []; + let results = []; if (way.isClosed()) return results; - var testNodes; - var indices = [0, way.nodes.length - 1]; - indices.forEach(function(nodeIndex) { - var nodeID = way.nodes[nodeIndex]; - var node = graph.entity(nodeID); + let testNodes; + const indices = [0, way.nodes.length - 1]; + indices.forEach(nodeIndex => { + const nodeID = way.nodes[nodeIndex]; + const node = graph.entity(nodeID); if (!isExtendableCandidate(node, way)) return; - var connectionInfo = canConnectByExtend(way, nodeIndex); + const connectionInfo = canConnectByExtend(way, nodeIndex); if (!connectionInfo) return; testNodes = graph.childNodes(way).slice(); // shallow copy @@ -207,11 +208,11 @@ export function validationAlmostJunction(context) { !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; // must have equivalent layers and levels - var layer1 = way.tags.layer || '0', + const layer1 = way.tags.layer || '0', layer2 = way2.tags.layer || '0'; if (layer1 !== layer2) return false; - var level1 = way.tags.level || '0', + const level1 = way.tags.level || '0', level2 = way2.tags.level || '0'; if (level1 !== level2) return false; @@ -255,43 +256,42 @@ export function validationAlmostJunction(context) { } function canConnectByExtend(way, endNodeIdx) { - var EXTEND_TH_METERS = 5; - var tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point - var midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge - var tipNode = graph.entity(tipNid); - var midNode = graph.entity(midNid); - var lon = tipNode.loc[0]; - var lat = tipNode.loc[1]; - var lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2; - var lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2; - var queryExtent = geoExtent([ + const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point + const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge + const tipNode = graph.entity(tipNid); + const midNode = graph.entity(midNid); + const lon = tipNode.loc[0]; + const lat = tipNode.loc[1]; + const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2; + const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2; + const queryExtent = geoExtent([ [lon - lon_range, lat - lat_range], [lon + lon_range, lat + lat_range] ]); // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the "extended tip" location - var edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc); - var t = EXTEND_TH_METERS / edgeLen + 1.0; - var extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t); + const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc); + const t = EXTEND_TH_METERS / edgeLen + 1.0; + const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t); // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways - var intersected = tree.intersects(queryExtent, graph); - for (var i = 0; i < intersected.length; i++) { - var way2 = intersected[i]; + const intersected = tree.intersects(queryExtent, graph); + for (let i = 0; i < intersected.length; i++) { + let way2 = intersected[i]; if (!isHighway(way2)) continue; if (!canConnectWays(way, way2)) continue; - for (var j = 0; j < way2.nodes.length - 1; j++) { - var nAid = way2.nodes[j], + for (let j = 0; j < way2.nodes.length - 1; j++) { + let nAid = way2.nodes[j], nBid = way2.nodes[j + 1]; if (nAid === tipNid || nBid === tipNid) continue; - var nA = graph.entity(nAid), + let nA = graph.entity(nAid), nB = graph.entity(nBid); - var crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); + let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); if (crossLoc) { // When endpoints are close, just join if resulting small change in angle (#7201) let nearEndNodes = findNearbyEndNodes(tipNode, way2); From 006e69244904c1de596af35d6a643c53a4abbdb3 Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:49:19 +0000 Subject: [PATCH 05/10] Convert to 2 space indentation --- modules/validations/almost_junction.js | 588 ++++++++++++------------- 1 file changed, 294 insertions(+), 294 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index a95a6855a..f832aebbb 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -1,7 +1,7 @@ import { - geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon, - geoSphericalDistance, geoVecInterp, geoHasSelfIntersections, - geoSphericalClosestNode, geoAngle + geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon, + geoSphericalDistance, geoVecInterp, geoHasSelfIntersections, + geoSphericalClosestNode, geoAngle } from '../geo'; import { actionAddMidpoint } from '../actions/add_midpoint'; @@ -18,309 +18,309 @@ import { services } from '../services'; * Look for roads that can be connected to other roads with a short extension */ export function validationAlmostJunction(context) { - const type = 'almost_junction'; - const EXTEND_TH_METERS = 5; - const WELD_TH_METERS = 0.75; - // Comes from considering bounding case of parallel ways - const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS; - // Comes from considering bounding case of perpendicular ways - const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS); + const type = 'almost_junction'; + const EXTEND_TH_METERS = 5; + const WELD_TH_METERS = 0.75; + // Comes from considering bounding case of parallel ways + const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS; + // Comes from considering bounding case of perpendicular ways + const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS); - function isHighway(entity) { - return entity.type === 'way' - && osmRoutableHighwayTagValues[entity.tags.highway]; - } + function isHighway(entity) { + return entity.type === 'way' + && osmRoutableHighwayTagValues[entity.tags.highway]; + } - function isTaggedAsNotContinuing(node) { - return node.tags.noexit === 'yes' - || node.tags.amenity === 'parking_entrance' - || (node.tags.entrance && node.tags.entrance !== 'no'); + function isTaggedAsNotContinuing(node) { + return node.tags.noexit === 'yes' + || node.tags.amenity === 'parking_entrance' + || (node.tags.entrance && node.tags.entrance !== 'no'); + } + + + const validation = function checkAlmostJunction(entity, graph) { + if (!isHighway(entity)) return []; + if (entity.isDegenerate()) return []; + + const tree = context.history().tree(); + const extendableNodeInfos = findConnectableEndNodesByExtension(entity); + + let issues = []; + + extendableNodeInfos.forEach(extendableNodeInfo => { + issues.push(new validationIssue({ + type, + subtype: 'highway-highway', + severity: 'warning', + message(context) { + const entity1 = context.hasEntity(this.entityIds[0]); + if (this.entityIds[0] === this.entityIds[2]) { + return entity1 ? t('issues.almost_junction.self.message', { + feature: utilDisplayLabel(entity1, context) + }) : ''; + } else { + const entity2 = context.hasEntity(this.entityIds[2]); + return (entity1 && entity2) ? t('issues.almost_junction.message', { + feature: utilDisplayLabel(entity1, context), + feature2: utilDisplayLabel(entity2, context) + }) : ''; + } + }, + reference: showReference, + entityIds: [entity.id, extendableNodeInfo.node.id, extendableNodeInfo.wid], + loc: extendableNodeInfo.node.loc, + hash: JSON.stringify(extendableNodeInfo.node.loc), + data: { + edge: extendableNodeInfo.edge, + cross_loc: extendableNodeInfo.cross_loc + }, + dynamicFixes: makeFixes + })); + }); + + return issues; + + + function makeFixes(context) { + let fixes = [new validationIssueFix({ + icon: 'iD-icon-abutment', + title: t('issues.fix.connect_features.title'), + onClick(context) { + const endNodeId = this.issue.entityIds[1]; + const endNode = context.entity(endNodeId); + const targetEdge = this.issue.data.edge; + const crossLoc = this.issue.data.cross_loc; + const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; + const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); + + const annotation = t('issues.fix.connect_almost_junction.annotation'); + // already a point nearby, just connect to that + if (closestNodeInfo.distance < WELD_TH_METERS) { + context.perform( + actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc), + annotation + ); + // else add the end node to the edge way + } else { + context.perform( + actionAddMidpoint({loc: crossLoc, edge: targetEdge}, endNode), + annotation + ); + } + } + })]; + + const node = context.hasEntity(this.entityIds[1]); + if (node && !node.hasInterestingTags()) { + // node has no descriptive tags, suggest noexit fix + fixes.push(new validationIssueFix({ + icon: 'maki-barrier', + title: t('issues.fix.tag_as_disconnected.title'), + onClick(context) { + const nodeID = this.issue.entityIds[1]; + const tags = Object.assign({}, context.entity(nodeID).tags); + tags.noexit = 'yes'; + context.perform( + actionChangeTags(nodeID, tags), + t('issues.fix.tag_as_disconnected.annotation') + ); + } + })); + } + + return fixes; } - const validation = function checkAlmostJunction(entity, graph) { - if (!isHighway(entity)) return []; - if (entity.isDegenerate()) return []; - - const tree = context.history().tree(); - const extendableNodeInfos = findConnectableEndNodesByExtension(entity); - - let issues = []; - - extendableNodeInfos.forEach(extendableNodeInfo => { - issues.push(new validationIssue({ - type, - subtype: 'highway-highway', - severity: 'warning', - message(context) { - const entity1 = context.hasEntity(this.entityIds[0]); - if (this.entityIds[0] === this.entityIds[2]) { - return entity1 ? t('issues.almost_junction.self.message', { - feature: utilDisplayLabel(entity1, context) - }) : ''; - } else { - const entity2 = context.hasEntity(this.entityIds[2]); - return (entity1 && entity2) ? t('issues.almost_junction.message', { - feature: utilDisplayLabel(entity1, context), - feature2: utilDisplayLabel(entity2, context) - }) : ''; - } - }, - reference: showReference, - entityIds: [entity.id, extendableNodeInfo.node.id, extendableNodeInfo.wid], - loc: extendableNodeInfo.node.loc, - hash: JSON.stringify(extendableNodeInfo.node.loc), - data: { - edge: extendableNodeInfo.edge, - cross_loc: extendableNodeInfo.cross_loc - }, - dynamicFixes: makeFixes - })); - }); - - return issues; + function showReference(selection) { + selection.selectAll('.issue-reference') + .data([0]) + .enter() + .append('div') + .attr('class', 'issue-reference') + .text(t('issues.almost_junction.highway-highway.reference')); + } - function makeFixes(context) { - let fixes = [new validationIssueFix({ - icon: 'iD-icon-abutment', - title: t('issues.fix.connect_features.title'), - onClick(context) { - const endNodeId = this.issue.entityIds[1]; - const endNode = context.entity(endNodeId); - const targetEdge = this.issue.data.edge; - const crossLoc = this.issue.data.cross_loc; - const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; - const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); + function isExtendableCandidate(node, way) { + // can not accurately test vertices on tiles not downloaded from osm - #5938 + const osm = services.osm; + if (osm && !osm.isDataLoaded(node.loc)) { + return false; + } + if (isTaggedAsNotContinuing(node) || graph.parentWays(node).length !== 1) { + return false; + } - const annotation = t('issues.fix.connect_almost_junction.annotation'); - // already a point nearby, just connect to that - if (closestNodeInfo.distance < WELD_TH_METERS) { - context.perform( - actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc), - annotation - ); - // else add the end node to the edge way - } else { - context.perform( - actionAddMidpoint({loc: crossLoc, edge: targetEdge}, endNode), - annotation - ); - } - } - })]; + let occurences = 0; + for (const index in way.nodes) { + if (way.nodes[index] === node.id) { + occurences += 1; + if (occurences > 1) { + return false; + } + } + } + return true; + } - const node = context.hasEntity(this.entityIds[1]); - if (node && !node.hasInterestingTags()) { - // node has no descriptive tags, suggest noexit fix - fixes.push(new validationIssueFix({ - icon: 'maki-barrier', - title: t('issues.fix.tag_as_disconnected.title'), - onClick(context) { - const nodeID = this.issue.entityIds[1]; - const tags = Object.assign({}, context.entity(nodeID).tags); - tags.noexit = 'yes'; - context.perform( - actionChangeTags(nodeID, tags), - t('issues.fix.tag_as_disconnected.annotation') - ); - } - })); + + function findConnectableEndNodesByExtension(way) { + let results = []; + if (way.isClosed()) return results; + + let testNodes; + const indices = [0, way.nodes.length - 1]; + indices.forEach(nodeIndex => { + const nodeID = way.nodes[nodeIndex]; + const node = graph.entity(nodeID); + + if (!isExtendableCandidate(node, way)) return; + + const connectionInfo = canConnectByExtend(way, nodeIndex); + if (!connectionInfo) return; + + testNodes = graph.childNodes(way).slice(); // shallow copy + testNodes[nodeIndex] = testNodes[nodeIndex].move(connectionInfo.cross_loc); + + // don't flag issue if connecting the ways would cause self-intersection + if (geoHasSelfIntersections(testNodes, nodeID)) return; + + results.push(connectionInfo); + }); + + return results; + } + + function hasTag(tags, key) { + return tags[key] !== undefined && tags[key] !== 'no'; + } + + function canConnectWays(way, way2) { + + // allow self-connections + if (way.id === way2.id) return true; + + // if one is bridge or tunnel, both must be bridge or tunnel + if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) && + !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false; + if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) && + !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; + + // must have equivalent layers and levels + const layer1 = way.tags.layer || '0', + layer2 = way2.tags.layer || '0'; + if (layer1 !== layer2) return false; + + const level1 = way.tags.level || '0', + level2 = way2.tags.level || '0'; + if (level1 !== level2) return false; + + return true; + } + + function findNearbyEndNodes(node, way2) { + return [ + way2.nodes[0], + way2.nodes[way2.nodes.length - 1] + ].map(d => graph.entity(d)) + .filter(d => { + // Node cannot be near to itself, but other endnode of same way could be + return d.id !== node.id + && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH; + }); + } + + function findSmallJoinAngle(midNode, tipNode, endNodes) { + // Both nodes could be close, so want to join whichever is closest to collinear + let joinTo; + let minAngle = Infinity; + + // Checks midNode -> tipNode -> endNode for collinearity + endNodes.forEach(endNode => { + const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI; + const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI; + const diff = Math.max(a1, a2) - Math.min(a1, a2); + + if (diff < minAngle) { + joinTo = endNode; + minAngle = diff; + } + }); + + /* Threshold set by considering right angle triangle + based on node joining threshold and extension distance */ + if (minAngle <= SIG_ANGLE_TH) return joinTo; + + return null; + } + + function canConnectByExtend(way, endNodeIdx) { + const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point + const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge + const tipNode = graph.entity(tipNid); + const midNode = graph.entity(midNid); + const lon = tipNode.loc[0]; + const lat = tipNode.loc[1]; + const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2; + const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2; + const queryExtent = geoExtent([ + [lon - lon_range, lat - lat_range], + [lon + lon_range, lat + lat_range] + ]); + + // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the "extended tip" location + const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc); + const t = EXTEND_TH_METERS / edgeLen + 1.0; + const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t); + + // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways + const intersected = tree.intersects(queryExtent, graph); + for (let i = 0; i < intersected.length; i++) { + let way2 = intersected[i]; + + if (!isHighway(way2)) continue; + + if (!canConnectWays(way, way2)) continue; + + for (let j = 0; j < way2.nodes.length - 1; j++) { + let nAid = way2.nodes[j], + nBid = way2.nodes[j + 1]; + + if (nAid === tipNid || nBid === tipNid) continue; + + let nA = graph.entity(nAid), + nB = graph.entity(nBid); + let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); + if (crossLoc) { + // When endpoints are close, just join if resulting small change in angle (#7201) + let nearEndNodes = findNearbyEndNodes(tipNode, way2); + if (nearEndNodes.length > 0) { + let collinear = findSmallJoinAngle(midNode, tipNode, nearEndNodes); + if (collinear) { + return { + node: tipNode, + wid: way2.id, + edge: [collinear.id, collinear.id], + cross_loc: collinear.loc + }; + } } - return fixes; + return { + node: tipNode, + wid: way2.id, + edge: [nA.id, nB.id], + cross_loc: crossLoc + }; + } } + } + return null; + } + }; + validation.type = type; - function showReference(selection) { - selection.selectAll('.issue-reference') - .data([0]) - .enter() - .append('div') - .attr('class', 'issue-reference') - .text(t('issues.almost_junction.highway-highway.reference')); - } - - - function isExtendableCandidate(node, way) { - // can not accurately test vertices on tiles not downloaded from osm - #5938 - const osm = services.osm; - if (osm && !osm.isDataLoaded(node.loc)) { - return false; - } - if (isTaggedAsNotContinuing(node) || graph.parentWays(node).length !== 1) { - return false; - } - - let occurences = 0; - for (const index in way.nodes) { - if (way.nodes[index] === node.id) { - occurences += 1; - if (occurences > 1) { - return false; - } - } - } - return true; - } - - - function findConnectableEndNodesByExtension(way) { - let results = []; - if (way.isClosed()) return results; - - let testNodes; - const indices = [0, way.nodes.length - 1]; - indices.forEach(nodeIndex => { - const nodeID = way.nodes[nodeIndex]; - const node = graph.entity(nodeID); - - if (!isExtendableCandidate(node, way)) return; - - const connectionInfo = canConnectByExtend(way, nodeIndex); - if (!connectionInfo) return; - - testNodes = graph.childNodes(way).slice(); // shallow copy - testNodes[nodeIndex] = testNodes[nodeIndex].move(connectionInfo.cross_loc); - - // don't flag issue if connecting the ways would cause self-intersection - if (geoHasSelfIntersections(testNodes, nodeID)) return; - - results.push(connectionInfo); - }); - - return results; - } - - function hasTag(tags, key) { - return tags[key] !== undefined && tags[key] !== 'no'; - } - - function canConnectWays(way, way2) { - - // allow self-connections - if (way.id === way2.id) return true; - - // if one is bridge or tunnel, both must be bridge or tunnel - if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) && - !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false; - if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) && - !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; - - // must have equivalent layers and levels - const layer1 = way.tags.layer || '0', - layer2 = way2.tags.layer || '0'; - if (layer1 !== layer2) return false; - - const level1 = way.tags.level || '0', - level2 = way2.tags.level || '0'; - if (level1 !== level2) return false; - - return true; - } - - function findNearbyEndNodes(node, way2) { - return [ - way2.nodes[0], - way2.nodes[way2.nodes.length - 1] - ].map(d => graph.entity(d)) - .filter(d => { - // Node cannot be near to itself, but other endnode of same way could be - return d.id !== node.id - && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH; - }); - } - - function findSmallJoinAngle(midNode, tipNode, endNodes) { - // Both nodes could be close, so want to join whichever is closest to collinear - let joinTo; - let minAngle = Infinity; - - // Checks midNode -> tipNode -> endNode for collinearity - endNodes.forEach(endNode => { - const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI; - const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI; - const diff = Math.max(a1, a2) - Math.min(a1, a2); - - if (diff < minAngle) { - joinTo = endNode; - minAngle = diff; - } - }); - - /* Threshold set by considering right angle triangle - based on node joining threshold and extension distance */ - if (minAngle <= SIG_ANGLE_TH) return joinTo; - - return null; - } - - function canConnectByExtend(way, endNodeIdx) { - const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point - const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge - const tipNode = graph.entity(tipNid); - const midNode = graph.entity(midNid); - const lon = tipNode.loc[0]; - const lat = tipNode.loc[1]; - const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2; - const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2; - const queryExtent = geoExtent([ - [lon - lon_range, lat - lat_range], - [lon + lon_range, lat + lat_range] - ]); - - // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the "extended tip" location - const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc); - const t = EXTEND_TH_METERS / edgeLen + 1.0; - const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t); - - // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways - const intersected = tree.intersects(queryExtent, graph); - for (let i = 0; i < intersected.length; i++) { - let way2 = intersected[i]; - - if (!isHighway(way2)) continue; - - if (!canConnectWays(way, way2)) continue; - - for (let j = 0; j < way2.nodes.length - 1; j++) { - let nAid = way2.nodes[j], - nBid = way2.nodes[j + 1]; - - if (nAid === tipNid || nBid === tipNid) continue; - - let nA = graph.entity(nAid), - nB = graph.entity(nBid); - let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); - if (crossLoc) { - // When endpoints are close, just join if resulting small change in angle (#7201) - let nearEndNodes = findNearbyEndNodes(tipNode, way2); - if (nearEndNodes.length > 0) { - let collinear = findSmallJoinAngle(midNode, tipNode, nearEndNodes); - if (collinear) { - return { - node: tipNode, - wid: way2.id, - edge: [collinear.id, collinear.id], - cross_loc: collinear.loc - }; - } - } - - return { - node: tipNode, - wid: way2.id, - edge: [nA.id, nB.id], - cross_loc: crossLoc - }; - } - } - } - return null; - } - }; - - validation.type = type; - - return validation; + return validation; } From d96fde53dbd28e87c178a2cdd1089225d23c739d Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Fri, 31 Jan 2020 17:58:53 +0000 Subject: [PATCH 06/10] Add test for almost junction with both close ends --- test/spec/validations/almost_junction.js | 54 +++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index 05223d467..b70851d84 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -199,6 +199,36 @@ describe('iD.validations.almost_junction', function () { return n1; } + function closeEndNodesBothSmallAngle() { + // Square path with both endpoints near eachother + var n1 = iD.osmNode({id: 'n-1', loc: [0, 22.4427453]}); + var n2 = iD.osmNode({id: 'n-2', loc: [0, 22.4429810]}); + var n3 = iD.osmNode({id: 'n-3', loc: [0.0000063, 22.4429810]}); + var n4 = iD.osmNode({id: 'n-4', loc: [0.0000063, 22.4427483]}); + var w1 = iD.osmWay({id: 'w-1', nodes: ['n-1', 'n-2', 'n-3', 'n-4'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n1), + iD.actionAddEntity(n2), + iD.actionAddEntity(n3), + iD.actionAddEntity(n4), + iD.actionAddEntity(w1) + ); + + // Horizontal path with end node within 4.25m and change of angle >9° (to both endpoints) + var n5 = iD.osmNode({id: 'n-5', loc: [0.0000124, 22.4427458]}); + var n6 = iD.osmNode({id: 'n-6', loc: [0.0000445, 22.4427449]}); + var w2 = iD.osmWay({id: 'w-2', nodes: ['n-5', 'n-6'], tags: { highway: 'path' }}); + + context.perform( + iD.actionAddEntity(n5), + iD.actionAddEntity(n6), + iD.actionAddEntity(w2) + ); + + return n1; + } + function validate() { var validator = iD.validationAlmostJunction(context); var changes = context.history().changes(); @@ -293,7 +323,6 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); - // TODO: Test case of both endpoints of another way close, should prioritise smaller angle change it('joins close endpoints if insignificant angle change', function() { var n1 = closeEndNodesSmallAngle(); var issues = validate(); @@ -359,4 +388,27 @@ describe('iD.validations.almost_junction', function () { expect(issue.data.cross_loc).to.have.lengthOf(2); expect(issue.data.cross_loc).to.eql(n1.loc); }); + + + it('joins to close endpoint with smaller angle change', function() { + var n1 = closeEndNodesBothSmallAngle(); + var issues = validate(); + expect(issues).to.have.lengthOf(1); + var issue = issues[0]; + expect(issue.type).to.eql('almost_junction'); + expect(issue.subtype).to.eql('highway-highway'); + expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds[0]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-5'); + expect(issue.entityIds[2]).to.eql('w-1'); + + // Duplicate edge nodes means endpoints will be joined + expect(issue.data.edge).to.have.lengthOf(2); + expect(issue.data.edge[0]).to.eql('n-1'); + expect(issue.data.edge[1]).to.eql('n-1'); + + // Crossing set to loc of end node means endpoints will be joined + expect(issue.data.cross_loc).to.have.lengthOf(2); + expect(issue.data.cross_loc).to.eql(n1.loc); + }); }); From 0b4a690a08e435846db2697224b7ebee1026ce9e Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 2 Feb 2020 12:37:15 +0000 Subject: [PATCH 07/10] Move close endpoint handling into quick fix Just a bit more optimised since this is only relevant to check when the quick fix is used --- modules/validations/almost_junction.js | 114 +++++++++++++------------ 1 file changed, 59 insertions(+), 55 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index f832aebbb..d98044c1a 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -54,12 +54,12 @@ export function validationAlmostJunction(context) { severity: 'warning', message(context) { const entity1 = context.hasEntity(this.entityIds[0]); - if (this.entityIds[0] === this.entityIds[2]) { + if (this.entityIds[0] === this.entityIds[3]) { return entity1 ? t('issues.almost_junction.self.message', { feature: utilDisplayLabel(entity1, context) }) : ''; } else { - const entity2 = context.hasEntity(this.entityIds[2]); + const entity2 = context.hasEntity(this.entityIds[3]); return (entity1 && entity2) ? t('issues.almost_junction.message', { feature: utilDisplayLabel(entity1, context), feature2: utilDisplayLabel(entity2, context) @@ -67,7 +67,12 @@ export function validationAlmostJunction(context) { } }, reference: showReference, - entityIds: [entity.id, extendableNodeInfo.node.id, extendableNodeInfo.wid], + entityIds: [ + entity.id, + extendableNodeInfo.mid.id, + extendableNodeInfo.node.id, + extendableNodeInfo.wid, + ], loc: extendableNodeInfo.node.loc, hash: JSON.stringify(extendableNodeInfo.node.loc), data: { @@ -80,20 +85,35 @@ export function validationAlmostJunction(context) { return issues; - function makeFixes(context) { let fixes = [new validationIssueFix({ icon: 'iD-icon-abutment', title: t('issues.fix.connect_features.title'), onClick(context) { - const endNodeId = this.issue.entityIds[1]; + const annotation = t('issues.fix.connect_almost_junction.annotation'); + const [, midNodeId, endNodeId, crossWayId] = this.issue.entityIds; + const midNode = context.entity(midNodeId); const endNode = context.entity(endNodeId); + const crossWay = context.entity(crossWayId); + + // When endpoints are close, just join if resulting small change in angle (#7201) + const nearEndNodes = findNearbyEndNodes(endNode, crossWay); + if (nearEndNodes.length > 0) { + const collinear = findSmallJoinAngle(midNode, endNode, nearEndNodes); + if (collinear) { + context.perform( + actionMergeNodes([collinear.id, endNode.id], collinear.loc), + annotation + ); + return; + } + } + const targetEdge = this.issue.data.edge; const crossLoc = this.issue.data.cross_loc; const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])]; const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc); - const annotation = t('issues.fix.connect_almost_junction.annotation'); // already a point nearby, just connect to that if (closestNodeInfo.distance < WELD_TH_METERS) { context.perform( @@ -110,14 +130,14 @@ export function validationAlmostJunction(context) { } })]; - const node = context.hasEntity(this.entityIds[1]); + const node = context.hasEntity(this.entityIds[2]); if (node && !node.hasInterestingTags()) { // node has no descriptive tags, suggest noexit fix fixes.push(new validationIssueFix({ icon: 'maki-barrier', title: t('issues.fix.tag_as_disconnected.title'), onClick(context) { - const nodeID = this.issue.entityIds[1]; + const nodeID = this.issue.entityIds[2]; const tags = Object.assign({}, context.entity(nodeID).tags); tags.noexit = 'yes'; context.perform( @@ -131,7 +151,6 @@ export function validationAlmostJunction(context) { return fixes; } - function showReference(selection) { selection.selectAll('.issue-reference') .data([0]) @@ -141,7 +160,6 @@ export function validationAlmostJunction(context) { .text(t('issues.almost_junction.highway-highway.reference')); } - function isExtendableCandidate(node, way) { // can not accurately test vertices on tiles not downloaded from osm - #5938 const osm = services.osm; @@ -164,7 +182,6 @@ export function validationAlmostJunction(context) { return true; } - function findConnectableEndNodesByExtension(way) { let results = []; if (way.isClosed()) return results; @@ -192,37 +209,10 @@ export function validationAlmostJunction(context) { return results; } - function hasTag(tags, key) { - return tags[key] !== undefined && tags[key] !== 'no'; - } - - function canConnectWays(way, way2) { - - // allow self-connections - if (way.id === way2.id) return true; - - // if one is bridge or tunnel, both must be bridge or tunnel - if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) && - !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false; - if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) && - !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; - - // must have equivalent layers and levels - const layer1 = way.tags.layer || '0', - layer2 = way2.tags.layer || '0'; - if (layer1 !== layer2) return false; - - const level1 = way.tags.level || '0', - level2 = way2.tags.level || '0'; - if (level1 !== level2) return false; - - return true; - } - - function findNearbyEndNodes(node, way2) { + function findNearbyEndNodes(node, way) { return [ - way2.nodes[0], - way2.nodes[way2.nodes.length - 1] + way.nodes[0], + way.nodes[way.nodes.length - 1] ].map(d => graph.entity(d)) .filter(d => { // Node cannot be near to itself, but other endnode of same way could be @@ -255,6 +245,33 @@ export function validationAlmostJunction(context) { return null; } + function hasTag(tags, key) { + return tags[key] !== undefined && tags[key] !== 'no'; + } + + function canConnectWays(way, way2) { + + // allow self-connections + if (way.id === way2.id) return true; + + // if one is bridge or tunnel, both must be bridge or tunnel + if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) && + !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false; + if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) && + !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false; + + // must have equivalent layers and levels + const layer1 = way.tags.layer || '0', + layer2 = way2.tags.layer || '0'; + if (layer1 !== layer2) return false; + + const level1 = way.tags.level || '0', + level2 = way2.tags.level || '0'; + if (level1 !== level2) return false; + + return true; + } + function canConnectByExtend(way, endNodeIdx) { const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge @@ -293,21 +310,8 @@ export function validationAlmostJunction(context) { nB = graph.entity(nBid); let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]); if (crossLoc) { - // When endpoints are close, just join if resulting small change in angle (#7201) - let nearEndNodes = findNearbyEndNodes(tipNode, way2); - if (nearEndNodes.length > 0) { - let collinear = findSmallJoinAngle(midNode, tipNode, nearEndNodes); - if (collinear) { - return { - node: tipNode, - wid: way2.id, - edge: [collinear.id, collinear.id], - cross_loc: collinear.loc - }; - } - } - return { + mid: midNode, node: tipNode, wid: way2.id, edge: [nA.id, nB.id], From d83675f43ec2faffb5f91434b0d7cb67c528a3a7 Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 2 Feb 2020 13:10:02 +0000 Subject: [PATCH 08/10] Update almost junction tests to reflect change --- test/spec/validations/almost_junction.js | 109 ++++++++++------------- 1 file changed, 47 insertions(+), 62 deletions(-) diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index b70851d84..3f5cd2b14 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -148,8 +148,6 @@ describe('iD.validations.almost_junction', function () { iD.actionAddEntity(n4), iD.actionAddEntity(w2) ); - - return n1; } function closeEndNodesBigAngle() { @@ -174,8 +172,6 @@ describe('iD.validations.almost_junction', function () { iD.actionAddEntity(n4), iD.actionAddEntity(w2) ); - - return n1; } function closeEndNodesSmallAngleSelf() { @@ -195,8 +191,6 @@ describe('iD.validations.almost_junction', function () { iD.actionAddEntity(n5), iD.actionAddEntity(w1) ); - - return n1; } function closeEndNodesBothSmallAngle() { @@ -225,8 +219,6 @@ describe('iD.validations.almost_junction', function () { iD.actionAddEntity(n6), iD.actionAddEntity(w2) ); - - return n1; } function validate() { @@ -252,10 +244,11 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-1'); - expect(issue.entityIds[2]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-2'); + expect(issue.entityIds[2]).to.eql('n-1'); + expect(issue.entityIds[3]).to.eql('w-2'); expect(issue.loc).to.have.lengthOf(2); expect(issue.loc[0]).to.eql(22.42357); @@ -282,10 +275,11 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-1'); - expect(issue.entityIds[2]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-2'); + expect(issue.entityIds[2]).to.eql('n-1'); + expect(issue.entityIds[3]).to.eql('w-2'); expect(issue.loc).to.have.lengthOf(2); expect(issue.loc[0]).to.eql(22.42357); @@ -324,91 +318,82 @@ describe('iD.validations.almost_junction', function () { }); it('joins close endpoints if insignificant angle change', function() { - var n1 = closeEndNodesSmallAngle(); + closeEndNodesSmallAngle(); var issues = validate(); expect(issues).to.have.lengthOf(1); var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-3'); - expect(issue.entityIds[2]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-4'); + expect(issue.entityIds[2]).to.eql('n-3'); + expect(issue.entityIds[3]).to.eql('w-1'); - // Duplicate edge nodes means endpoints will be joined - expect(issue.data.edge).to.have.lengthOf(2); - expect(issue.data.edge[0]).to.eql('n-1'); - expect(issue.data.edge[1]).to.eql('n-1'); - - // Crossing set to loc of end node means endpoints will be joined - expect(issue.data.cross_loc).to.have.lengthOf(2); - expect(issue.data.cross_loc).to.eql(n1.loc); + issue.fixes(context)[0].onClick(context); + var w1 = context.entity('w-1'); + var w2 = context.entity('w-2'); + var joined = w2.nodes[0] === w1.nodes[0]; + expect(joined).to.be.true; }); it('won\'t join close endpoints if significant angle change', function() { - var n1 = closeEndNodesBigAngle(); + closeEndNodesBigAngle(); var issues = validate(); expect(issues).to.have.lengthOf(1); var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-3'); - expect(issue.entityIds[2]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-4'); + expect(issue.entityIds[2]).to.eql('n-3'); + expect(issue.entityIds[3]).to.eql('w-1'); - // Differing edge nodes means endpoints won't be joined - expect(issue.data.edge).to.have.lengthOf(2); - expect(issue.data.edge[0]).to.eql('n-1'); - expect(issue.data.edge[1]).to.eql('n-2'); - - // Crossing different from loc of end node means endpoints won't be joined - expect(issue.data.cross_loc).to.have.lengthOf(2); - expect(issue.data.cross_loc).to.not.eql(n1.loc); + issue.fixes(context)[0].onClick(context); + var w1 = context.entity('w-1'); + var w2 = context.entity('w-2'); + var joined = w2.nodes[0] === w1.nodes[0]; + expect(joined).not.to.be.true; }); it('joins close endpoints of the same way', function() { - var n1 = closeEndNodesSmallAngleSelf(); + closeEndNodesSmallAngleSelf(); var issues = validate(); expect(issues).to.have.lengthOf(1); var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-5'); - expect(issue.entityIds[2]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-4'); + expect(issue.entityIds[2]).to.eql('n-5'); + expect(issue.entityIds[3]).to.eql('w-1'); - // Duplicate edge nodes means endpoints will be joined - expect(issue.data.edge).to.have.lengthOf(2); - expect(issue.data.edge[0]).to.eql('n-1'); - expect(issue.data.edge[1]).to.eql('n-1'); - - // Crossing set to loc of end node means endpoints will be joined - expect(issue.data.cross_loc).to.have.lengthOf(2); - expect(issue.data.cross_loc).to.eql(n1.loc); + issue.fixes(context)[0].onClick(context); + var w = context.entity('w-1'); + var joined = w.nodes[0] === w.nodes[w.nodes.length - 1]; + expect(joined).to.be.true; }); it('joins to close endpoint with smaller angle change', function() { - var n1 = closeEndNodesBothSmallAngle(); + closeEndNodesBothSmallAngle(); var issues = validate(); expect(issues).to.have.lengthOf(1); var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(3); + expect(issue.entityIds).to.have.lengthOf(4); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-5'); - expect(issue.entityIds[2]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-6'); + expect(issue.entityIds[2]).to.eql('n-5'); + expect(issue.entityIds[3]).to.eql('w-1'); - // Duplicate edge nodes means endpoints will be joined - expect(issue.data.edge).to.have.lengthOf(2); - expect(issue.data.edge[0]).to.eql('n-1'); - expect(issue.data.edge[1]).to.eql('n-1'); - - // Crossing set to loc of end node means endpoints will be joined - expect(issue.data.cross_loc).to.have.lengthOf(2); - expect(issue.data.cross_loc).to.eql(n1.loc); + issue.fixes(context)[0].onClick(context); + var w1 = context.entity('w-1'); + var w2 = context.entity('w-2'); + var joined = w2.nodes[0] === w1.nodes[0]; + expect(joined).to.be.true; }); }); From 142126f7fec7bd96b608a26454425de0d78fa32c Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 2 Feb 2020 15:46:41 +0000 Subject: [PATCH 09/10] Store node for quick fix in data not entityIds Realised these are used for highlighting and should remain as is. --- modules/validations/almost_junction.js | 14 ++++---- test/spec/validations/almost_junction.js | 42 ++++++++++-------------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/modules/validations/almost_junction.js b/modules/validations/almost_junction.js index d98044c1a..67fc15979 100644 --- a/modules/validations/almost_junction.js +++ b/modules/validations/almost_junction.js @@ -54,12 +54,12 @@ export function validationAlmostJunction(context) { severity: 'warning', message(context) { const entity1 = context.hasEntity(this.entityIds[0]); - if (this.entityIds[0] === this.entityIds[3]) { + if (this.entityIds[0] === this.entityIds[2]) { return entity1 ? t('issues.almost_junction.self.message', { feature: utilDisplayLabel(entity1, context) }) : ''; } else { - const entity2 = context.hasEntity(this.entityIds[3]); + const entity2 = context.hasEntity(this.entityIds[2]); return (entity1 && entity2) ? t('issues.almost_junction.message', { feature: utilDisplayLabel(entity1, context), feature2: utilDisplayLabel(entity2, context) @@ -69,13 +69,13 @@ export function validationAlmostJunction(context) { reference: showReference, entityIds: [ entity.id, - extendableNodeInfo.mid.id, extendableNodeInfo.node.id, extendableNodeInfo.wid, ], loc: extendableNodeInfo.node.loc, hash: JSON.stringify(extendableNodeInfo.node.loc), data: { + midId: extendableNodeInfo.mid.id, edge: extendableNodeInfo.edge, cross_loc: extendableNodeInfo.cross_loc }, @@ -91,8 +91,8 @@ export function validationAlmostJunction(context) { title: t('issues.fix.connect_features.title'), onClick(context) { const annotation = t('issues.fix.connect_almost_junction.annotation'); - const [, midNodeId, endNodeId, crossWayId] = this.issue.entityIds; - const midNode = context.entity(midNodeId); + const [, endNodeId, crossWayId] = this.issue.entityIds; + const midNode = context.entity(this.issue.data.midId); const endNode = context.entity(endNodeId); const crossWay = context.entity(crossWayId); @@ -130,14 +130,14 @@ export function validationAlmostJunction(context) { } })]; - const node = context.hasEntity(this.entityIds[2]); + const node = context.hasEntity(this.entityIds[1]); if (node && !node.hasInterestingTags()) { // node has no descriptive tags, suggest noexit fix fixes.push(new validationIssueFix({ icon: 'maki-barrier', title: t('issues.fix.tag_as_disconnected.title'), onClick(context) { - const nodeID = this.issue.entityIds[2]; + const nodeID = this.issue.entityIds[1]; const tags = Object.assign({}, context.entity(nodeID).tags); tags.noexit = 'yes'; context.perform( diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index 3f5cd2b14..5cbdce551 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -244,11 +244,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-2'); - expect(issue.entityIds[2]).to.eql('n-1'); - expect(issue.entityIds[3]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-1'); + expect(issue.entityIds[2]).to.eql('w-2'); expect(issue.loc).to.have.lengthOf(2); expect(issue.loc[0]).to.eql(22.42357); @@ -275,11 +274,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-2'); - expect(issue.entityIds[2]).to.eql('n-1'); - expect(issue.entityIds[3]).to.eql('w-2'); + expect(issue.entityIds[1]).to.eql('n-1'); + expect(issue.entityIds[2]).to.eql('w-2'); expect(issue.loc).to.have.lengthOf(2); expect(issue.loc[0]).to.eql(22.42357); @@ -324,11 +322,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-4'); - expect(issue.entityIds[2]).to.eql('n-3'); - expect(issue.entityIds[3]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-3'); + expect(issue.entityIds[2]).to.eql('w-1'); issue.fixes(context)[0].onClick(context); var w1 = context.entity('w-1'); @@ -344,11 +341,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-4'); - expect(issue.entityIds[2]).to.eql('n-3'); - expect(issue.entityIds[3]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-3'); + expect(issue.entityIds[2]).to.eql('w-1'); issue.fixes(context)[0].onClick(context); var w1 = context.entity('w-1'); @@ -364,11 +360,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-1'); - expect(issue.entityIds[1]).to.eql('n-4'); - expect(issue.entityIds[2]).to.eql('n-5'); - expect(issue.entityIds[3]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-5'); + expect(issue.entityIds[2]).to.eql('w-1'); issue.fixes(context)[0].onClick(context); var w = context.entity('w-1'); @@ -384,11 +379,10 @@ describe('iD.validations.almost_junction', function () { var issue = issues[0]; expect(issue.type).to.eql('almost_junction'); expect(issue.subtype).to.eql('highway-highway'); - expect(issue.entityIds).to.have.lengthOf(4); + expect(issue.entityIds).to.have.lengthOf(3); expect(issue.entityIds[0]).to.eql('w-2'); - expect(issue.entityIds[1]).to.eql('n-6'); - expect(issue.entityIds[2]).to.eql('n-5'); - expect(issue.entityIds[3]).to.eql('w-1'); + expect(issue.entityIds[1]).to.eql('n-5'); + expect(issue.entityIds[2]).to.eql('w-1'); issue.fixes(context)[0].onClick(context); var w1 = context.entity('w-1'); From 64ae360dac4f531377d5e29143f1dfa5ced7f4de Mon Sep 17 00:00:00 2001 From: SilentSpike Date: Sun, 2 Feb 2020 15:57:17 +0000 Subject: [PATCH 10/10] Add verbs to test spec for almost junctions --- test/spec/validations/almost_junction.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/spec/validations/almost_junction.js b/test/spec/validations/almost_junction.js index 5cbdce551..89334b08f 100644 --- a/test/spec/validations/almost_junction.js +++ b/test/spec/validations/almost_junction.js @@ -237,7 +237,7 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); - it('horizontal and vertical road, closer than threshold', function() { + it('flags horizontal and vertical road closer than threshold', function() { horizontalVertialCloserThanThd(); var issues = validate(); expect(issues).to.have.lengthOf(1); @@ -267,7 +267,7 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); - it('horizontal and tilted road, closer than threshold', function() { + it('flags horizontal and tilted road closer than threshold', function() { horizontalTiltedCloserThanThd(); var issues = validate(); expect(issues).to.have.lengthOf(1); @@ -297,19 +297,19 @@ describe('iD.validations.almost_junction', function () { expect(issues).to.have.lengthOf(0); }); - it('horizontal and vertical road, further than threshold', function() { + it('ignores horizontal and vertical road further than threshold', function() { horizontalVertialFurtherThanThd(); var issues = validate(); expect(issues).to.have.lengthOf(0); }); - it('horizontal and vertical road, closer than threshold but with noexit tag', function() { + it('ignores horizontal and vertical road closer than threshold, but with noexit tag', function() { horizontalVertialWithNoExit(); var issues = validate(); expect(issues).to.have.lengthOf(0); }); - it('two horizontal roads, closer than threshold', function() { + it('ignores two horizontal roads closer than threshold', function() { twoHorizontalCloserThanThd(); var issues = validate(); expect(issues).to.have.lengthOf(0);