Files
iD/modules/validations/crossing_ways.js

269 lines
12 KiB
JavaScript

import _clone from 'lodash-es/clone';
import _map from 'lodash-es/map';
import { geoExtent, geoLineIntersection } from '../geo';
import { set as d3_set } from 'd3-collection';
import { utilDisplayLabel } from '../util';
import { t } from '../util/locale';
import {
ValidationIssueType,
ValidationIssueSeverity,
validationIssue,
validationIssueFix
} from './validation_issue';
import { osmNode } from '../osm';
import { actionAddMidpoint } from '../actions';
import { geoChooseEdge } from '../geo';
export function validationHighwayCrossingOtherWays(context) {
// Check if the edge going from n1 to n2 crosses (without a connection node)
// any edge on way. Return the corss point if so.
function findEdgeToWayCrossCoords(n1, n2, way, graph, edgePairsVisited) {
var crossCoords = [];
for (var j = 0; j < way.nodes.length - 1; j++) {
var nidA = way.nodes[j],
nidB = way.nodes[j + 1];
if (nidA === n1.id || nidA === n2.id ||
nidB === n1.id || nidB === n2.id) {
// n1 or n2 is a connection node; skip
continue;
}
var edgePair = edgePairString(n1.id, n2.id, nidA, nidB);
if (edgePairsVisited.has(edgePair)) continue;
edgePairsVisited.add(edgePair);
var nA = graph.entity(nidA),
nB = graph.entity(nidB),
point = geoLineIntersection([n1.loc, n2.loc], [nA.loc, nB.loc]);
if (point) crossCoords.push(point);
}
return crossCoords;
}
// n1 and n2 from one edge, nA and nB from the other edge
function edgePairString(n1, n2, nA, nB) {
return n1 > nA ? n1 + n2 + nA + nB : nA + nB + n1 + n2;
}
// returns the way or its parent relation, whichever has a useful feature type
function getFeatureWithFeatureTypeTagsForWay(way, graph) {
if (getFeatureTypeForTags(way.tags) === null) {
// if the way doesn't match a feature type, check is parent relations
var parentRels = graph.parentRelations(way);
for (var i = 0; i < parentRels.length; i++) {
var rel = parentRels[i];
if (getFeatureTypeForTags(rel.tags) !== null) {
return rel;
}
}
}
return way;
}
function hasTag(tags, key) {
return tags[key] !== undefined && tags[key] !== 'no';
}
function getFeatureTypeForCrossingCheck(way, graph) {
var tags = getFeatureWithFeatureTypeTagsForWay(way, graph).tags;
return getFeatureTypeForTags(tags);
}
function getFeatureTypeForTags(tags) {
if (hasTag(tags, 'highway')) return 'highway';
if (hasTag(tags, 'building')) return 'building';
if (hasTag(tags, 'railway')) return 'railway';
if (hasTag(tags, 'waterway') || tags.natural === 'water') return 'water';
return null;
}
function extendTagsByInferredLayer(tags, way) {
if (!hasTag(tags, 'layer')) {
tags.layer = way.layer().toString();
}
return tags;
}
function isLegitCrossing(way1, featureType1, way2, featureType2, graph) {
var tags1 = _clone(getFeatureWithFeatureTypeTagsForWay(way1, graph).tags),
tags2 = _clone(getFeatureWithFeatureTypeTagsForWay(way2, graph).tags);
tags1 = extendTagsByInferredLayer(tags1, way1);
tags2 = extendTagsByInferredLayer(tags2, way2);
// For better readability, not chaining all the true conditions into one if statement.
if ((featureType1 === 'highway' && featureType2 === 'highway') ||
(featureType1 === 'highway' && featureType2 === 'railway') ||
(featureType1 === 'railway' && featureType2 === 'railway')) {
// Legit cases:
// (1) they're on different layers
// (2) only one of the two ways is on a bridge
// (3) only one of the two ways is in a tunnel
if (tags1.layer !== tags2.layer) return true;
if (hasTag(tags1, 'bridge') && !hasTag(tags2, 'bridge')) return true;
if (!hasTag(tags1, 'bridge') && hasTag(tags2, 'bridge')) return true;
if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
}
if ((featureType1 === 'highway' && featureType2 === 'water') ||
(featureType1 === 'railway' && featureType2 === 'water')) {
// Legit cases:
// (1) highway/railway is on a bridge
// (2) only one of the two ways is in a tunnel
// (3) both are in tunnels but on different layers
if (hasTag(tags1, 'bridge')) return true;
if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
if (hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel') && tags1.layer !== tags2.layer) return true;
}
if ((featureType1 === 'highway' && featureType2 === 'building') ||
(featureType1 === 'railway' && featureType2 === 'building')) {
// Legit cases:
// (1) highway/railway has a bridge or tunnel tag
// (2) highway/railway has a covered tag
if (hasTag(tags1, 'bridge') || hasTag(tags1, 'tunnel') || hasTag(tags1, 'covered')) return true;
}
if (featureType1 === 'water' && featureType2 === 'water') {
// Legit cases:
// (1) only one of the water is in a tunnel
// (2) both are in tunnels but on differnt layers
if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
if (hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel') && tags1.layer !== tags2.layer) return true;
}
if (featureType1 === 'water' && featureType2 === 'building') {
// Legit cases:
// (1) water is in a tunnel
// (2) water has a covered tag
if (hasTag(tags1, 'tunnel') || hasTag(tags1, 'covered')) return true;
}
if (featureType1 === 'building' && featureType2 === 'building') {
// Legit case: they're on different layers
if (tags1.layer !== tags2.layer) return true;
}
return false;
}
function findCrossingsByWay(entity, graph, tree, edgePairsVisited) {
var edgeCrossInfos = [];
if (entity.geometry(graph) !== 'line' || entity.type !== 'way') return edgeCrossInfos;
var entFeatureType = getFeatureTypeForCrossingCheck(entity, graph);
if (entFeatureType === null) return edgeCrossInfos;
for (var i = 0; i < entity.nodes.length - 1; i++) {
var nid1 = entity.nodes[i],
nid2 = entity.nodes[i + 1],
n1 = graph.entity(nid1),
n2 = graph.entity(nid2),
extent = geoExtent([
[
Math.min(n1.loc[0], n2.loc[0]),
Math.min(n1.loc[1], n2.loc[1])
],
[
Math.max(n1.loc[0], n2.loc[0]),
Math.max(n1.loc[1], n2.loc[1])
]
]),
intersected = tree.intersects(extent, graph);
for (var j = 0; j < intersected.length; j++) {
if (intersected[j].type !== 'way') continue;
// only check crossing highway, waterway, building, and railway
var way = intersected[j],
wayFeatureType = getFeatureTypeForCrossingCheck(way, graph);
if (wayFeatureType === null ||
isLegitCrossing(entity, entFeatureType, way, wayFeatureType, graph) ||
isLegitCrossing(way, wayFeatureType, entity, entFeatureType, graph)) {
continue;
}
var crossCoords = findEdgeToWayCrossCoords(n1, n2, way, graph, edgePairsVisited);
for (var k = 0; k < crossCoords.length; k++) {
edgeCrossInfos.push({
ways: [entity, way],
featureTypes: [entFeatureType, wayFeatureType],
cross_point: crossCoords[k],
});
}
}
}
return edgeCrossInfos;
}
var validation = function(changes, graph, tree) {
// create one issue per crossing point
var edited = changes.created.concat(changes.modified),
edgePairsVisited = d3_set(),
issues = [];
for (var i = 0; i < edited.length; i++) {
var crosses = findCrossingsByWay(edited[i], graph, tree, edgePairsVisited);
for (var j = 0; j < crosses.length; j++) {
var crossing = crosses[j];
// use the entities with the tags that define the feature type
var entities = _map(crossing.ways, function(way) {
return getFeatureWithFeatureTypeTagsForWay(way, graph);
});
var crossingTypeID = crossing.featureTypes.sort().join('-') + '_crossing';
var messageDict = {};
messageDict[crossing.featureTypes[0]] = utilDisplayLabel(entities[0], context);
var key2 = crossing.featureTypes[1];
if (crossing.featureTypes[0] === crossing.featureTypes[1]) {
key2 += '2';
}
messageDict[key2] = utilDisplayLabel(entities[1], context);
issues.push(new validationIssue({
type: ValidationIssueType.crossing_ways,
severity: ValidationIssueSeverity.warning,
message: t('issues.'+crossingTypeID+'.message', messageDict),
tooltip: t('issues.'+crossingTypeID+'.tooltip'),
entities: entities,
info: {'ways': crossing.ways},
coordinates: crossing.cross_point,
fixes: [
new validationIssueFix({
title: t('issues.fix.add_connection_vertex.title'),
action: function() {
var loc = this.issue.coordinates;
var ways = this.issue.info.ways;
context.perform(
function actionConnectCrossingWays(graph) {
var node = osmNode();
graph = graph.replace(node);
var way0 = ways[0];
var choice0 = geoChooseEdge(context.childNodes(way0), loc, context.projection);
var edge0 = [way0.nodes[choice0.index - 1], way0.nodes[choice0.index]];
graph = actionAddMidpoint({loc: choice0.loc, edge: edge0}, node)(graph);
var way1 = ways[1];
var choice1 = geoChooseEdge(context.childNodes(way1), loc, context.projection);
var edge1 = [way1.nodes[choice1.index - 1], way1.nodes[choice1.index]];
graph = actionAddMidpoint({loc: choice1.loc, edge: edge1}, node)(graph);
return graph;
},
t('issues.fix.add_connection_vertex.undo_redo')
);
}
})
]
}));
}
}
return issues;
};
return validation;
}