modules/validations/impossible_oneway.js (182 lines of code) (raw):
import { t, textDirection } from '../util/locale';
import { modeDrawLine } from '../modes/draw_line';
import { actionReverse } from '../actions/reverse';
import { utilDisplayLabel } from '../util';
import { osmFlowingWaterwayTagValues, osmOneWayTags, osmRoutableHighwayTagValues } from '../osm/tags';
import { validationIssue, validationIssueFix } from '../core/validation';
import { services } from '../services';
export function validationImpossibleOneway() {
var type = 'impossible_oneway';
var validation = function checkImpossibleOneway(entity, graph) {
if (entity.type !== 'way' || entity.geometry(graph) !== 'line') return [];
if (entity.isClosed()) return [];
if (!typeForWay(entity)) return [];
if (!isOneway(entity)) return [];
var firstIssues = issuesForNode(entity, entity.first());
var lastIssues = issuesForNode(entity, entity.last());
return firstIssues.concat(lastIssues);
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 isOneway(way) {
if (way.tags.oneway === 'yes') return true;
if (way.tags.oneway) return false;
for (var key in way.tags) {
if (osmOneWayTags[key] && osmOneWayTags[key][way.tags[key]]) {
return true;
}
}
return false;
}
function nodeOccursMoreThanOnce(way, nodeID) {
var occurences = 0;
for (var index in way.nodes) {
if (way.nodes[index] === nodeID) {
occurences += 1;
if (occurences > 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) {
var isFirst = nodeID === way.first();
var wayType = typeForWay(way);
// ignore if this way is self-connected at this node
if (nodeOccursMoreThanOnce(way, nodeID)) return [];
var osm = services.osm;
if (!osm) return [];
var 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 [];
var attachedWaysOfSameType = graph.parentWays(node).filter(function(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 [];
var attachedOneways = attachedWaysOfSameType.filter(function(attachedWay) {
return isOneway(attachedWay);
});
// ignore if the way is connected to some non-oneway features
if (attachedOneways.length < attachedWaysOfSameType.length) return [];
if (attachedOneways.length) {
var connectedEndpointsOkay = attachedOneways.some(function(attachedOneway) {
if ((isFirst ? attachedOneway.first() : attachedOneway.last()) !== nodeID) return true;
if (nodeOccursMoreThanOnce(attachedOneway, nodeID)) return true;
return false;
});
if (connectedEndpointsOkay) return [];
}
var placement = isFirst ? 'start' : 'end',
messageID = wayType + '.',
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('issues.impossible_oneway.' + messageID + '.message', {
feature: utilDisplayLabel(entity, context)
}) : '';
},
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('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'));
}
}));
}
if (node.tags.noexit !== 'yes') {
var useLeftContinue = (isFirst && textDirection === 'ltr') ||
(!isFirst && textDirection === 'rtl');
fixes.push(new validationIssueFix({
icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''),
title: t('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')
.text(t('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, {
wayID: way.id,
startGraph: context.graph(),
baselineGraph: context.graph(),
affix: way.affix(vertex.id)
})
);
}
validation.type = type;
return validation;
}