Merge pull request #7309 from openstreetmap/endpoints-fix

Fix almost junction behaviour for close endpoints
This commit is contained in:
Quincy Morgan
2020-02-27 13:14:28 -08:00
committed by GitHub
2 changed files with 470 additions and 242 deletions
+295 -237
View File
@@ -1,7 +1,7 @@
import {
geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon,
geoSphericalDistance, geoVecInterp, geoHasSelfIntersections,
geoSphericalClosestNode
geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon,
geoSphericalDistance, geoVecInterp, geoHasSelfIntersections,
geoSphericalClosestNode, geoAngle
} from '../geo';
import { actionAddMidpoint } from '../actions/add_midpoint';
@@ -18,255 +18,313 @@ 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'
&& 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 isHighway(entity) {
return entity.type === 'way' &&
osmRoutableHighwayTagValues[entity.tags.highway];
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: {
midId: extendableNodeInfo.mid.id,
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 annotation = t('issues.fix.connect_almost_junction.annotation');
const [, endNodeId, crossWayId] = this.issue.entityIds;
const midNode = context.entity(this.issue.data.midId);
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);
// 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;
}
function isTaggedAsNotContinuing(node) {
return node.tags.noexit === 'yes' ||
node.tags.amenity === 'parking_entrance' ||
(node.tags.entrance && node.tags.entrance !== 'no');
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;
}
var validation = function checkAlmostJunction(entity, graph) {
if (!isHighway(entity)) return [];
if (entity.isDegenerate()) return [];
var tree = context.history().tree();
var issues = [];
var extendableNodeInfos = findConnectableEndNodesByExtension(entity);
extendableNodeInfos.forEach(function(extendableNodeInfo) {
issues.push(new validationIssue({
type: type,
subtype: 'highway-highway',
severity: 'warning',
message: function(context) {
var 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]);
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) {
var 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);
var annotation = t('issues.fix.connect_almost_junction.annotation');
// already a point nearby, just connect to that
if (closestNodeInfo.distance < 0.75) {
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
);
}
}
})];
var 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);
tags.noexit = 'yes';
context.perform(
actionChangeTags(nodeID, tags),
t('issues.fix.tag_as_disconnected.annotation')
);
}
}));
}
return fixes;
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;
function showReference(selection) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.text(t('issues.almost_junction.highway-highway.reference'));
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 findNearbyEndNodes(node, way) {
return [
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
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;
function isExtendableCandidate(node, way) {
// can not accurately test vertices on tiles not downloaded from osm - #5938
var osm = services.osm;
if (osm && !osm.isDataLoaded(node.loc)) {
return false;
}
if (isTaggedAsNotContinuing(node) || graph.parentWays(node).length !== 1) {
return false;
}
return null;
}
var occurences = 0;
for (var index in way.nodes) {
if (way.nodes[index] === node.id) {
occurences += 1;
if (occurences > 1) {
return false;
}
}
}
return true;
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
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) {
return {
mid: midNode,
node: tipNode,
wid: way2.id,
edge: [nA.id, nB.id],
cross_loc: crossLoc
};
}
}
}
return null;
}
};
validation.type = type;
function findConnectableEndNodesByExtension(way) {
var 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);
if (!isExtendableCandidate(node, way)) return;
var 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
var layer1 = way.tags.layer || '0',
layer2 = way2.tags.layer || '0';
if (layer1 !== layer2) return false;
var level1 = way.tags.level || '0',
level2 = way2.tags.level || '0';
if (level1 !== level2) return false;
return true;
}
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([
[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);
// 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];
if (!isHighway(way2)) continue;
if (!canConnectWays(way, way2)) continue;
for (var j = 0; j < way2.nodes.length - 1; j++) {
var nAid = way2.nodes[j],
nBid = way2.nodes[j + 1];
if (nAid === tipNid || nBid === tipNid) continue;
var nA = graph.entity(nAid),
nB = graph.entity(nBid);
var crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]);
if (crossLoc) {
return {
node: tipNode,
wid: way2.id,
edge: [nA.id, nB.id],
cross_loc: crossLoc
};
}
}
}
return null;
}
};
validation.type = type;
return validation;
return validation;
}
+175 -5
View File
@@ -126,6 +126,101 @@ 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)
);
}
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)
);
}
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)
);
}
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)
);
}
function validate() {
var validator = iD.validationAlmostJunction(context);
var changes = context.history().changes();
@@ -142,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);
@@ -172,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);
@@ -202,22 +297,97 @@ 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);
});
it('joins close endpoints if insignificant angle change', function() {
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');
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() {
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');
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() {
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');
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() {
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');
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;
});
});