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; }