diff --git a/data/core.yaml b/data/core.yaml
index f3818418c..d81019a74 100644
--- a/data/core.yaml
+++ b/data/core.yaml
@@ -930,6 +930,9 @@ en:
crossing_ways:
message: Crossing ways without connection
tooltip: "Roads are crossing other roads, buildings, railroads, or waterways without connection nodes or a bridge tag."
+ highway_almost_junction:
+ message: Almost junction
+ tooltip: "This node is very close but not connected to way {wid}."
fix:
delete_feature:
title: Delete this feature
diff --git a/dist/locales/en.json b/dist/locales/en.json
index 55e049825..b75419b59 100644
--- a/dist/locales/en.json
+++ b/dist/locales/en.json
@@ -1117,6 +1117,10 @@
"message": "Crossing ways without connection",
"tooltip": "Roads are crossing other roads, buildings, railroads, or waterways without connection nodes or a bridge tag."
},
+ "highway_almost_junction": {
+ "message": "Almost junction",
+ "tooltip": "This node is very close but not connected to way {wid}."
+ },
"fix": {
"delete_feature": {
"title": "Delete this feature"
diff --git a/modules/validations/highway_almost_junction.js b/modules/validations/highway_almost_junction.js
new file mode 100644
index 000000000..d6a0bab6a
--- /dev/null
+++ b/modules/validations/highway_almost_junction.js
@@ -0,0 +1,115 @@
+import {
+ geoExtent,
+ geoLineIntersection,
+ geoMetersToLat,
+ geoMetersToLon,
+ geoSphericalDistance,
+ geoVecInterp,
+} from '../geo';
+import { set as d3_set } from 'd3-collection';
+import { t } from '../util/locale';
+import {
+ ValidationIssueType,
+ ValidationIssueSeverity,
+ validationIssue,
+} from './validation_issue';
+
+
+/**
+ * Look for roads that can be connected to other roads with a short extension
+ */
+export function validationHighwayAlmostJunction() {
+
+ function isHighway(entity) {
+ return entity.type === 'way' && entity.tags.highway;
+ }
+
+ function findConnectableEndNodesByExtension(way, graph, tree) {
+ var results = [],
+ nidFirst = way.nodes[0],
+ nidLast = way.nodes[way.nodes.length - 1],
+ nodeFirst = graph.entity(nidFirst),
+ nodeLast = graph.entity(nidLast);
+
+ if (nidFirst === nidLast) return results;
+ if (!nodeFirst.tags.noexit && graph.parentWays(nodeFirst).length === 1) {
+ var widNearFirst = canConnectByExtend(way, 0, graph, tree);
+ if (widNearFirst !== null) {
+ results.push({
+ node: nodeFirst,
+ wid: widNearFirst,
+ });
+ }
+ }
+ if (!nodeLast.tags.noexit && graph.parentWays(nodeLast).length === 1) {
+ var widNearLast = canConnectByExtend(way, way.nodes.length - 1, graph, tree);
+ if (widNearLast !== null) {
+ results.push({
+ node: nodeLast,
+ wid: widNearLast,
+ });
+ }
+ }
+ return results;
+ }
+
+ function canConnectByExtend(way, endNodeIdx, graph, tree) {
+ var EXTEND_TH_METERS = 5,
+ tipNid = way.nodes[endNodeIdx], // the 'tip' node for extension point
+ midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2], // the other node of the edge
+ tipNode = graph.entity(tipNid),
+ midNode = graph.entity(midNid),
+ lon = tipNode.loc[0],
+ lat = tipNode.loc[1],
+ lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2,
+ lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2,
+ 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),
+ t = EXTEND_TH_METERS / edgeLen + 1.0,
+ 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++) {
+ if (!isHighway(intersected[i]) || intersected[i].id === way.id) continue;
+ var way2 = intersected[i];
+ for (var j = 0; j < way2.nodes.length - 1; j++) {
+ var nA = graph.entity(way2.nodes[j]),
+ nB = graph.entity(way2.nodes[j + 1]);
+ if (geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc])) {
+ return way2.id;
+ }
+ }
+ }
+ return null;
+ }
+
+ var validation = function(changes, graph, tree) {
+ var edited = changes.created.concat(changes.modified),
+ issues = [];
+ for (var i = 0; i < edited.length; i++) {
+ if (!isHighway(edited[i])) continue;
+ var extendableNodes = findConnectableEndNodesByExtension(edited[i], graph, tree);
+ for (var j = 0; j < extendableNodes.length; j++) {
+ issues.push(new validationIssue({
+ type: ValidationIssueType.highway_almost_junction,
+ severity: ValidationIssueSeverity.warning,
+ message: t('issues.highway_almost_junction.message'),
+ tooltip: t('issues.highway_almost_junction.tooltip', {wid: extendableNodes[j].wid}),
+ entities: [extendableNodes[j].node, graph.entity(extendableNodes[j].wid)],
+ coordinates: extendableNodes[j].node.loc,
+ }));
+ }
+ }
+
+ return issues;
+ };
+
+
+ return validation;
+}
diff --git a/modules/validations/index.js b/modules/validations/index.js
index 51b3300c3..ed2dc4af3 100644
--- a/modules/validations/index.js
+++ b/modules/validations/index.js
@@ -1,6 +1,7 @@
export { validationDeprecatedTag } from './deprecated_tag';
export { validationDisconnectedHighway } from './disconnected_highway';
export { validationHighwayCrossingOtherWays } from './crossing_ways';
+export { validationHighwayAlmostJunction } from './highway_almost_junction';
export { ValidationIssueType, ValidationIssueSeverity } from './validation_issue';
export { validationManyDeletions } from './many_deletions';
export { validationMapCSSChecks } from './mapcss_checks';
diff --git a/modules/validations/validation_issue.js b/modules/validations/validation_issue.js
index 531a4264e..c8ffe5780 100644
--- a/modules/validations/validation_issue.js
+++ b/modules/validations/validation_issue.js
@@ -10,7 +10,8 @@ var ValidationIssueType = Object.freeze({
old_multipolygon: 'old_multipolygon',
tag_suggests_area: 'tag_suggests_area',
map_rule_issue: 'map_rule_issue',
- crossing_ways: 'crossing_ways'
+ crossing_ways: 'crossing_ways',
+ highway_almost_junction: 'highway_almost_junction',
});
@@ -47,7 +48,7 @@ export function validationIssue(attrs) {
this.tooltip = attrs.tooltip;
this.entities = attrs.entities; // expect an array of entities
this.coordinates = attrs.coordinates; // expect a [lon, lat] array
- this.info = attrs.info; // an object containing arbitrary extra information
+ this.info = attrs.info; // an object containing arbitrary extra information
this.fixes = attrs.fixes; // expect an array of functions for possible fixes
if (this.fixes) {
diff --git a/test/index.html b/test/index.html
index 116dda260..718d42921 100644
--- a/test/index.html
+++ b/test/index.html
@@ -149,6 +149,7 @@
+