Files
iD/modules/validations/crossing_ways.js
2019-02-14 15:56:04 -05:00

421 lines
17 KiB
JavaScript

import _clone from 'lodash-es/clone';
import _map from 'lodash-es/map';
import _flattenDeep from 'lodash-es/flatten';
import { actionAddMidpoint, actionMergeNodes } from '../actions';
import { geoExtent, geoLineIntersection, geoSphericalClosestNode } from '../geo';
import { osmNode } from '../osm';
import { t } from '../util/locale';
import { utilDisplayLabel } from '../util';
import { validationIssue, validationIssueFix } from '../core/validator';
export function validationCrossingWays() {
var type = 'crossing_ways';
// Check if the edge going from n1 to n2 crosses (without a connection node)
// any edge on way. Return the cross point if so.
function findEdgeToWayCrossCoords(n1, n2, way, graph) {
var crossCoords = [];
var nA, nB;
var segment1 = [n1.loc, n2.loc];
var segment2;
var nodes = graph.childNodes(way);
for (var j = 0; j < nodes.length - 1; j++) {
nA = nodes[j];
nB = nodes[j + 1];
if (nA.id === n1.id || nA.id === n2.id ||
nB.id === n1.id || nB.id === n2.id) {
// n1 or n2 is a connection node; skip
continue;
}
segment2 = [nA.loc, nB.loc];
var point = geoLineIntersection(segment1, segment2);
if (point) {
crossCoords.push({ edge: [nA.id, nB.id], point: point });
}
}
return crossCoords;
}
// 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);
}
// only validate certain waterway features
var waterways = ['canal', 'ditch', 'drain', 'river', 'stream'];
// ignore certain highway and railway features
var ignoredHighways = ['rest_area', 'services'];
var ignoredRailways = ['train_wash'];
function getFeatureTypeForTags(tags) {
if (hasTag(tags, 'building')) return 'building';
// don't check non-building areas
if (hasTag(tags, 'area')) return null;
if (hasTag(tags, 'highway') && ignoredHighways.indexOf(tags.highway) === -1) return 'highway';
if (hasTag(tags, 'railway') && ignoredRailways.indexOf(tags.railway) === -1) return 'railway';
if (hasTag(tags, 'waterway') && waterways.indexOf(tags.waterway) !== -1) return 'waterway';
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);
var 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 === 'waterway') ||
(featureType1 === 'railway' && featureType2 === 'waterway')) {
// 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 === 'waterway' && featureType2 === 'waterway') {
// 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 === 'waterway' && 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;
}
// highway values for which we shouldn't recommend connecting to waterways
var highwaysDisallowingFords = [
'motorway', 'motorway_link', 'trunk', 'trunk_link',
'primary', 'primary_link', 'secondary', 'secondary_link'
];
var pathHighways = [
'path', 'footway', 'cycleway', 'bridleway', 'pedestrian', 'steps'
];
function tagsForConnectionNodeIfAllowed(entity1, entity2) {
var featureType1 = getFeatureTypeForTags(entity1.tags);
var featureType2 = getFeatureTypeForTags(entity2.tags);
if (featureType1 === featureType2) {
if (featureType1 === 'highway') {
var entity1IsPath = pathHighways.indexOf(entity1.tags.highway) !== -1;
var entity2IsPath = pathHighways.indexOf(entity2.tags.highway) !== -1;
if ((entity1IsPath || entity2IsPath) && entity1IsPath !== entity2IsPath) {
// one feature is a path but not both, use a crossing
var pathFeature = entity1IsPath ? entity1 : entity2;
if (pathFeature.tags.highway === 'footway' &&
pathFeature.tags.footway === 'crossing' &&
['marked', 'unmarked'].indexOf(pathFeature.tags.crossing) !== -1) {
// if the path is a crossing, match the crossing type
return { highway: 'crossing', crossing: pathFeature.tags.crossing };
}
return { highway: 'crossing' };
}
return {};
}
if (featureType1 === 'waterway') return {};
if (featureType1 === 'railway') return {};
} else {
var featureTypes = [featureType1, featureType2];
if (featureTypes.indexOf('highway') !== -1) {
if (featureTypes.indexOf('building') !== -1) return {};
if (featureTypes.indexOf('railway') !== -1) {
if (pathHighways.indexOf(entity1.tags.highway) !== -1 ||
pathHighways.indexOf(entity2.tags.highway) !== -1) {
// path-rail connections use this tag
return { railway: 'crossing' };
} else {
// road-rail connections use this tag
return { railway: 'level_crossing' };
}
}
if (featureTypes.indexOf('waterway') !== -1) {
// do not allow fords on structures
if (hasTag(entity1.tags, 'tunnel') && hasTag(entity2.tags, 'tunnel')) return null;
if (hasTag(entity1.tags, 'bridge') && hasTag(entity2.tags, 'bridge')) return null;
if (highwaysDisallowingFords.indexOf(entity1.tags.highway) !== -1 ||
highwaysDisallowingFords.indexOf(entity2.tags.highway) !== -1) {
// do not allow fords on major highways
return null;
}
return { ford: 'yes' };
}
}
}
return null;
}
function findCrossingsByWay(primaryWay, graph, tree) {
var edgeCrossInfos = [];
if (primaryWay.type !== 'way') return edgeCrossInfos;
var primaryFeatureType = getFeatureTypeForCrossingCheck(primaryWay, graph);
if (primaryFeatureType === null) return edgeCrossInfos;
for (var i = 0; i < primaryWay.nodes.length - 1; i++) {
var nid1 = primaryWay.nodes[i];
var nid2 = primaryWay.nodes[i + 1];
var n1 = graph.entity(nid1);
var n2 = graph.entity(nid2);
var 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])
]
]);
var 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];
var wayFeatureType = getFeatureTypeForCrossingCheck(way, graph);
if (wayFeatureType === null ||
isLegitCrossing(primaryWay, primaryFeatureType, way, wayFeatureType, graph) ||
isLegitCrossing(way, wayFeatureType, primaryWay, primaryFeatureType, graph)) {
continue;
}
var crossCoords = findEdgeToWayCrossCoords(n1, n2, way, graph);
for (var k = 0; k < crossCoords.length; k++) {
var crossingInfo = crossCoords[k];
edgeCrossInfos.push({
ways: [primaryWay, way],
featureTypes: [primaryFeatureType, wayFeatureType],
edges: [[n1.id, n2.id], crossingInfo.edge],
crossPoint: crossingInfo.point
});
}
}
}
return edgeCrossInfos;
}
var validation = function(entity, context) {
var graph = context.graph();
var tree = context.history().tree();
var waysToCheck = _flattenDeep(_map([entity], function(entity) {
if (!getFeatureTypeForTags(entity.tags)) {
return [];
}
if (entity.type === 'way') {
return entity;
} else if (entity.type === 'relation' &&
entity.tags.type === 'multipolygon' &&
// only check multipolygons if they are buildings
hasTag(entity.tags, 'building')) {
return _map(entity.members, function(member) {
if (context.hasEntity(member.id)) {
var entity = context.entity(member.id);
if (entity.type === 'way') {
return entity;
}
}
return [];
});
}
return [];
}));
var crossings = waysToCheck.reduce(function(array, way) {
return array.concat(findCrossingsByWay(way, graph, tree));
}, []);
var issues = [];
crossings.forEach(function(crossing) {
issues.push(createIssue(crossing, context));
});
return issues;
};
function createIssue(crossing, context) {
var graph = context.graph();
// use the entities with the tags that define the feature type
var entities = crossing.ways.sort(function(entity1, entity2) {
var type1 = getFeatureTypeForCrossingCheck(entity1, graph);
var type2 = getFeatureTypeForCrossingCheck(entity2, graph);
if (type1 === type2) {
return utilDisplayLabel(entity1, context) > utilDisplayLabel(entity2, context);
} else if (type1 === 'waterway') {
return true;
} else if (type2 === 'waterway') {
return false;
}
return type1 < type2;
});
entities = _map(entities, function(way) {
return getFeatureWithFeatureTypeTagsForWay(way, graph);
});
var connectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1]);
var crossingTypeID;
if (hasTag(entities[0].tags, 'tunnel') && hasTag(entities[1].tags, 'tunnel')) {
crossingTypeID = 'tunnel-tunnel';
if (connectionTags) {
crossingTypeID += '_connectable';
}
} else if (hasTag(entities[0].tags, 'bridge') && hasTag(entities[1].tags, 'bridge')) {
crossingTypeID = 'bridge-bridge';
if (connectionTags) {
crossingTypeID += '_connectable';
}
} else {
crossingTypeID = crossing.featureTypes.sort().join('-');
}
var messageDict = {
feature: utilDisplayLabel(entities[0], context),
feature2: utilDisplayLabel(entities[1], context)
};
var fixes = [];
if (connectionTags) {
fixes.push(new validationIssueFix({
title: t('issues.fix.add_connection_vertex.title'),
onClick: function() {
var loc = this.issue.loc;
var connectionTags = this.issue.info.connectionTags;
var edges = this.issue.info.edges;
context.perform(
function actionConnectCrossingWays(graph) {
// create the new node for the points
var node = osmNode({ loc: loc, tags: connectionTags });
graph = graph.replace(node);
var nodesToMerge = [node.id];
var mergeThresholdInMeters = 0.75;
edges.forEach(function(edge) {
var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];
var closestNodeInfo = geoSphericalClosestNode(edgeNodes, loc);
// if there is already a point nearby, use that
if (closestNodeInfo.distance < mergeThresholdInMeters) {
nodesToMerge.push(closestNodeInfo.node.id);
// else add the new node to the way
} else {
graph = actionAddMidpoint({loc: loc, edge: edge}, node)(graph);
}
});
if (nodesToMerge.length > 1) {
// if we're using nearby nodes, merge them with the new node
graph = actionMergeNodes(nodesToMerge, loc)(graph);
}
return graph;
},
t('issues.fix.add_connection_vertex.annotation')
);
}
}));
}
fixes.push(new validationIssueFix({
title: t('issues.fix.reposition_features.title')
}));
return new validationIssue({
type: type,
severity: 'warning',
message: t('issues.crossing_ways.message', messageDict),
tooltip: t('issues.crossing_ways.'+crossingTypeID+'.tip'),
entities: entities,
info: { edges: crossing.edges, connectionTags: connectionTags },
loc: crossing.crossPoint,
fixes: fixes
});
}
validation.type = type;
return validation;
}