modules/validations/y_shaped_connection.js (111 lines of code) (raw):
import { geoSphericalDistance, vecAngle } from '@id-sdk/math';
import { operationDelete } from '../operations/index';
import { t } from '../core/localizer';
import { validationIssue, validationIssueFix } from '../core/validation';
export function validationYShapedConnection(context) {
/* We want to catch and warn about the following "shapes of connections"
* that may appear in ML-generated roads:
* (1) Two short edges around a connection node, causing a "Y-shaped" connection
* ________ _______
* V
* |
* |
* |
* (2) One short edges around a connection node. The connection is not exactly
* "Y-shaped", but still a little too detailed.
* _______
* ___________ /
* |
* |
* |
* The potential fix is to remove the non-connection nodes causing the short edges,
* so that the shape of the connection becomes more like a "T".
*
* This validation will flag issues on those excessive non-connection nodes around
* Y-shaped connections and suggest deletion or move as possible fixes.
*/
var type = 'y_shaped_connection';
// THD means "threshold"
var SHORT_EDGE_THD_METERS = 12;
var NON_FLAT_ANGLE_THD_DEGREES = 5;
var relatedHighways = {
residential: true, service: true, track: true, unclassified: true,
tertiary: true, secondary: true, primary: true, living_street: true,
cycleway: true, trunk: true, motorway: true, road: true, raceway: true
};
function isTaggedAsRelatedHighway(entity) {
return relatedHighways[entity.tags.highway];
}
function getRelatedHighwayParents(node, graph) {
var parentWays = graph.parentWays(node);
return parentWays.filter(function (way) {
return isTaggedAsRelatedHighway(way);
});
}
function createIssueAndFixForNode(node, context) {
var deletable = !operationDelete(context, [node.id]).disabled();
var fix;
if (deletable) {
fix = new validationIssueFix({
icon: 'iD-operation-delete',
title: t('issues.fix.delete_node_around_conn.title'),
entityIds: [node.id],
onClick: function() {
var id = this.entityIds[0];
var operation = operationDelete(context, [id]);
if (!operation.disabled()) {
operation();
}
}
});
} else {
fix = new validationIssueFix({
icon: 'iD-operation-move',
title: t('issues.fix.move_node_around_conn.title'),
entityIds: [node.id]
});
}
return new validationIssue({
type: type,
severity: 'warning',
message: function() {
return t('issues.y_shaped_connection.message');
},
reference: function(selection) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.text(t('issues.y_shaped_connection.reference'));
},
entityIds: [node.id],
fixes: [fix]
});
}
// Check
// (1) if the edge between connNodeIdx and edgeNodeIdx is a short edge
// (2) if the node at connNodeIdx is a Y-shaped connection
// return true only if both (1) and (2) hold.
function isShortEdgeAndYShapedConnection(graph, way, connNodeIdx, edgeNodeIdx) {
// conditions for connNode to be a possible Y-shaped connection:
// (1) it is a connection node with edges on both side
// (2) at least one edge is short
// (3) the angle between the two edges are not close to 180 degrees
if (connNodeIdx <= 0 || connNodeIdx >= way.nodes.length - 1) return false;
// make sure the node at connNodeIdx is really a connection node
var connNid = way.nodes[connNodeIdx];
var connNode = graph.entity(connNid);
var pways = getRelatedHighwayParents(connNode, graph);
if (pways.length < 2) return false;
// check if the edge between connNode and edgeNode is short
var edgeNid = way.nodes[edgeNodeIdx];
var edgeNode = graph.entity(edgeNid);
var edgeLen = geoSphericalDistance(connNode.loc, edgeNode.loc);
if (edgeLen > SHORT_EDGE_THD_METERS) return false;
// check if connNode is a Y-shaped connection
var prevEdgeAngle = 0;
var nextEdgeAngle = 0;
var angleBetweenEdges = 0;
var otherNodeIdx = connNodeIdx < edgeNodeIdx ? connNodeIdx - 1 : connNodeIdx + 1;
var otherNid = way.nodes[otherNodeIdx];
var otherNode = graph.entity(otherNid);
var other = context.projection(otherNode.loc);
var conn = context.projection(connNode.loc);
var edge = context.projection(edgeNode.loc);
if (otherNodeIdx < edgeNodeIdx) {
// node order along way: otherNode -> connNode -> edgeNode
prevEdgeAngle = vecAngle(other, conn);
nextEdgeAngle = vecAngle(conn, edge);
angleBetweenEdges = Math.abs(nextEdgeAngle - prevEdgeAngle) / Math.PI * 180.0;
} else {
// node order along way: edgeNode -> connNode -> otherNode
prevEdgeAngle = vecAngle(edge, conn);
nextEdgeAngle = vecAngle(conn, other);
angleBetweenEdges = Math.abs(nextEdgeAngle - prevEdgeAngle) / Math.PI * 180.0;
}
return angleBetweenEdges > NON_FLAT_ANGLE_THD_DEGREES;
}
var validation = function(entity, graph) {
// Only flag issue on non-connection nodes on negative ways
if (entity.type !== 'node') return [];
var pways = getRelatedHighwayParents(entity, graph);
if (pways.length !== 1 || !pways[0].id.startsWith('w-')) return [];
// check if either neighbor node on its parent way is a connection node
var issues = [];
var way = pways[0];
var idx = way.nodes.indexOf(entity.id);
if (idx <= 0) return issues;
if (isShortEdgeAndYShapedConnection(graph, way, idx - 1, idx) ||
isShortEdgeAndYShapedConnection(graph, way, idx + 1, idx)) {
issues.push(createIssueAndFixForNode(entity, context));
}
return issues;
};
validation.type = type;
return validation;
}