import { t, localizer } from '../core/localizer'; import { modeDrawLine } from '../modes/draw_line'; import { actionReverse } from '../actions/reverse'; import { utilDisplayLabel } from '../util/utilDisplayLabel'; import { osmFlowingWaterwayTagValues, osmRoutableHighwayTagValues } from '../osm/tags'; import { validationIssue, validationIssueFix } from '../core/validation'; import { services } from '../services'; export function validationImpossibleOneway() { const type = 'impossible_oneway'; const validation = function checkImpossibleOneway(entity, graph) { if (entity.type !== 'way' || entity.geometry(graph) !== 'line') return []; if (entity.isClosed()) return []; if (!typeForWay(entity)) return []; if (!entity.isOneWay()) return []; return [ ...issuesForNode(entity, entity.first()), ...issuesForNode(entity, entity.last()) ]; function typeForWay(way) { if (way.geometry(graph) !== 'line') return null; if (osmRoutableHighwayTagValues[way.tags.highway]) return 'highway'; if (osmFlowingWaterwayTagValues[way.tags.waterway]) return 'waterway'; return null; } function nodeOccursMoreThanOnce(way, nodeID) { let occurrences = 0; for (const index in way.nodes) { if (way.nodes[index] === nodeID) { occurrences++; if (occurrences > 1) return true; } } return false; } function isConnectedViaOtherTypes(way, node) { var wayType = typeForWay(way); if (wayType === 'highway') { // entrances are considered connected if (node.tags.entrance && node.tags.entrance !== 'no') return true; if (node.tags.amenity === 'parking_entrance') return true; } else if (wayType === 'waterway') { if (node.id === way.first()) { // multiple waterways may start at the same spring if (node.tags.natural === 'spring') return true; } else { // multiple waterways may end at the same drain if (node.tags.manhole === 'drain') return true; } } return graph.parentWays(node).some(function(parentWay) { if (parentWay.id === way.id) return false; if (wayType === 'highway') { // allow connections to highway areas if (parentWay.geometry(graph) === 'area' && osmRoutableHighwayTagValues[parentWay.tags.highway]) return true; // count connections to ferry routes as connected if (parentWay.tags.route === 'ferry') return true; return graph.parentRelations(parentWay).some(function(parentRelation) { if (parentRelation.tags.type === 'route' && parentRelation.tags.route === 'ferry') return true; // allow connections to highway multipolygons return parentRelation.isMultipolygon() && osmRoutableHighwayTagValues[parentRelation.tags.highway]; }); } else if (wayType === 'waterway') { // multiple waterways may start or end at a water body at the same node if (parentWay.tags.natural === 'water' || parentWay.tags.natural === 'coastline') return true; } return false; }); } function issuesForNode(way, nodeID) { const isFirst = (nodeID === way.first()) ^ way.isOneWayBackwards(); const wayType = typeForWay(way); // ignore if this way is self-connected at this node if (nodeOccursMoreThanOnce(way, nodeID)) return []; const osm = services.osm; if (!osm) return []; const node = graph.hasEntity(nodeID); // ignore if this node or its tile are unloaded if (!node || !osm.isDataLoaded(node.loc)) return []; if (isConnectedViaOtherTypes(way, node)) return []; const attachedWaysOfSameType = graph.parentWays(node).filter(parentWay => { if (parentWay.id === way.id) return false; return typeForWay(parentWay) === wayType; }); // assume it's okay for waterways to start or end disconnected for now if (wayType === 'waterway' && attachedWaysOfSameType.length === 0) return []; const attachedOneways = attachedWaysOfSameType .filter(attachedWay => attachedWay.isOneWay()); // ignore if the way is connected to some non-oneway features if (attachedOneways.length < attachedWaysOfSameType.length) return []; if (attachedOneways.length) { const connectedEndpointsOkay = attachedOneways.some(attachedOneway => { const isAttachedBackwards = attachedOneway.isOneWayBackwards(); if ((isFirst ^ isAttachedBackwards ? attachedOneway.first() : attachedOneway.last() ) !== nodeID) { return true; } if (nodeOccursMoreThanOnce(attachedOneway, nodeID)) return true; return false; }); if (connectedEndpointsOkay) return []; } const placement = isFirst ? 'start' : 'end'; let messageID = wayType + '.'; let referenceID = wayType + '.'; if (wayType === 'waterway') { messageID += 'connected.' + placement; referenceID += 'connected'; } else { messageID += placement; referenceID += placement; } return [new validationIssue({ type: type, subtype: wayType, severity: 'warning', message: function(context) { var entity = context.hasEntity(this.entityIds[0]); return entity ? t.append('issues.impossible_oneway.' + messageID + '.message', { feature: utilDisplayLabel(entity, context.graph()) }) : ''; }, reference: getReference(referenceID), entityIds: [way.id, node.id], dynamicFixes: function() { var fixes = []; if (attachedOneways.length) { fixes.push(new validationIssueFix({ icon: 'iD-operation-reverse', title: t.append('issues.fix.reverse_feature.title'), entityIds: [way.id], onClick: function(context) { var id = this.issue.entityIds[0]; context.perform(actionReverse(id), t('operations.reverse.annotation.line', { n: 1 })); } })); } if (node.tags.noexit !== 'yes') { var textDirection = localizer.textDirection(); var useLeftContinue = (isFirst && textDirection === 'ltr') || (!isFirst && textDirection === 'rtl'); fixes.push(new validationIssueFix({ icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''), title: t.append('issues.fix.continue_from_' + (isFirst ? 'start' : 'end') + '.title'), onClick: function(context) { var entityID = this.issue.entityIds[0]; var vertexID = this.issue.entityIds[1]; var way = context.entity(entityID); var vertex = context.entity(vertexID); continueDrawing(way, vertex, context); } })); } return fixes; }, loc: node.loc })]; function getReference(referenceID) { return function showReference(selection) { selection.selectAll('.issue-reference') .data([0]) .enter() .append('div') .attr('class', 'issue-reference') .call(t.append('issues.impossible_oneway.' + referenceID + '.reference')); }; } } }; function continueDrawing(way, vertex, context) { // make sure the vertex is actually visible and editable var map = context.map(); if (!context.editable() || !map.trimmedExtent().contains(vertex.loc)) { map.zoomToEase(vertex); } context.enter( modeDrawLine(context, way.id, context.graph(), 'line', way.affix(vertex.id), true) ); } validation.type = type; return validation; }