mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 09:12:52 +00:00
201 lines
7.5 KiB
JavaScript
201 lines
7.5 KiB
JavaScript
import {
|
|
geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon,
|
|
geoSphericalDistance, geoVecInterp, geoHasSelfIntersections,
|
|
geoSphericalClosestNode
|
|
} from '../geo';
|
|
|
|
import { actionAddMidpoint, actionChangeTags, actionMergeNodes } from '../actions';
|
|
import { t } from '../util/locale';
|
|
import { utilDisplayLabel } from '../util';
|
|
import { validationIssue, validationIssueFix } from '../core/validator';
|
|
|
|
|
|
/**
|
|
* Look for roads that can be connected to other roads with a short extension
|
|
*/
|
|
export function validationAlmostJunction() {
|
|
var type = 'almost_junction';
|
|
|
|
|
|
function isHighway(entity) {
|
|
return entity.type === 'way' &&
|
|
entity.tags.highway &&
|
|
entity.tags.highway !== 'no' &&
|
|
entity.tags.highway !== 'proposed';
|
|
}
|
|
|
|
function isNoexit(node) {
|
|
return node.tags.noexit && node.tags.noexit === 'yes';
|
|
}
|
|
|
|
function isExtendableCandidate(node, way, graph) {
|
|
if (isNoexit(node) || graph.parentWays(node).length !== 1) {
|
|
return false;
|
|
}
|
|
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 findConnectableEndNodesByExtension(way, graph, tree) {
|
|
var results = [];
|
|
if (way.isClosed()) return results;
|
|
|
|
var testNodes;
|
|
var endpointIndicies = [0, way.nodes.length - 1];
|
|
endpointIndicies.forEach(function(nodeIndex) {
|
|
|
|
var nodeID = way.nodes[nodeIndex];
|
|
var node = graph.entity(nodeID);
|
|
|
|
if (!isExtendableCandidate(node, way, graph)) return;
|
|
|
|
var connectionInfo = canConnectByExtend(way, nodeIndex, graph, tree);
|
|
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 canConnectByExtend(way, endNodeIdx, graph, tree) {
|
|
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++) {
|
|
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]);
|
|
var nB = graph.entity(way2.nodes[j + 1]);
|
|
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;
|
|
}
|
|
|
|
|
|
var validation = function checkAlmostJunction(entity, context) {
|
|
if (!isHighway(entity)) return [];
|
|
if (entity.isDegenerate()) return [];
|
|
|
|
var graph = context.graph();
|
|
var tree = context.history().tree();
|
|
var issues = [];
|
|
|
|
var extendableNodeInfos = findConnectableEndNodesByExtension(entity, graph, tree);
|
|
extendableNodeInfos.forEach(function(extendableNodeInfo) {
|
|
var node = extendableNodeInfo.node;
|
|
var edgeHighway = graph.entity(extendableNodeInfo.wid);
|
|
|
|
var fixes = [new validationIssueFix({
|
|
icon: 'iD-icon-abutment',
|
|
title: t('issues.fix.connect_features.title'),
|
|
onClick: function() {
|
|
var endNode = this.issue.entities[1];
|
|
var targetEdge = this.issue.info.edge;
|
|
var crossLoc = this.issue.info.cross_loc;
|
|
var edgeNodes = [context.graph().entity(targetEdge[0]), context.graph().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
|
|
);
|
|
}
|
|
}
|
|
})];
|
|
|
|
if (Object.keys(node.tags).length === 0) {
|
|
// node has no tags, suggest noexit fix
|
|
fixes.push(new validationIssueFix({
|
|
icon: 'maki-barrier',
|
|
title: t('issues.fix.tag_as_disconnected.title'),
|
|
onClick: function() {
|
|
var nodeID = this.issue.entities[1].id;
|
|
context.perform(
|
|
actionChangeTags(nodeID, { noexit: 'yes' }),
|
|
t('issues.fix.tag_as_disconnected.annotation')
|
|
);
|
|
}
|
|
}));
|
|
}
|
|
|
|
issues.push(new validationIssue({
|
|
type: type,
|
|
severity: 'warning',
|
|
message: t('issues.almost_junction.message', {
|
|
feature: utilDisplayLabel(entity, context),
|
|
feature2: utilDisplayLabel(edgeHighway, context)
|
|
}),
|
|
tooltip: t('issues.almost_junction.highway-highway.tip'),
|
|
entities: [entity, node, edgeHighway],
|
|
loc: extendableNodeInfo.node.loc,
|
|
info: {
|
|
edge: extendableNodeInfo.edge,
|
|
cross_loc: extendableNodeInfo.cross_loc
|
|
},
|
|
fixes: fixes
|
|
}));
|
|
});
|
|
|
|
return issues;
|
|
};
|
|
|
|
validation.type = type;
|
|
|
|
return validation;
|
|
}
|