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() { // 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 type = 'crossing_ways'; 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.coordinates; 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') ); } })); } 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 }, coordinates: crossing.crossPoint, fixes: fixes }); } validation.type = type; return validation; }