modules/validations/disconnected_way.js (165 lines of code) (raw):

import { t, textDirection } from '../util/locale'; import { modeDrawLine } from '../modes/draw_line'; import { operationDelete } from '../operations/delete'; import { utilDisplayLabel } from '../util'; import { osmRoutableHighwayTagValues } from '../osm/tags'; import { validationIssue, validationIssueFix } from '../core/validation'; import { services } from '../services'; export function validationDisconnectedWay() { var type = 'disconnected_way'; function isTaggedAsHighway(entity) { return osmRoutableHighwayTagValues[entity.tags.highway]; } var validation = function checkDisconnectedWay(entity, graph) { var routingIslandWays = routingIslandForEntity(entity); if (!routingIslandWays) return []; return [new validationIssue({ type: type, subtype: 'highway', severity: 'warning', message: function(context) { if (this.entityIds.length === 1) { var entity = context.hasEntity(this.entityIds[0]); return entity ? t('issues.disconnected_way.highway.message', { highway: utilDisplayLabel(entity, context) }) : ''; } return t('issues.disconnected_way.routable.message.multiple', { count: this.entityIds.length.toString() }); }, reference: showReference, entityIds: Array.from(routingIslandWays).map(function(way) { return way.id; }), dynamicFixes: makeFixes })]; function makeFixes(context) { var fixes = []; var singleEntity = this.entityIds.length === 1 && context.hasEntity(this.entityIds[0]); if (singleEntity) { if (singleEntity.type === 'way' && !singleEntity.isClosed()) { var startFix = makeContinueDrawingFixIfAllowed(singleEntity.first(), 'start'); if (startFix) fixes.push(startFix); var endFix = makeContinueDrawingFixIfAllowed(singleEntity.last(), 'end'); if (endFix) fixes.push(endFix); } if (!fixes.length) { fixes.push(new validationIssueFix({ title: t('issues.fix.connect_feature.title') })); } fixes.push(new validationIssueFix({ icon: 'iD-operation-delete', title: t('issues.fix.delete_feature.title'), entityIds: [singleEntity.id], onClick: function(context) { var id = this.issue.entityIds[0]; var operation = operationDelete([id], context); if (!operation.disabled()) { operation(); } } })); } else { fixes.push(new validationIssueFix({ title: t('issues.fix.connect_features.title') })); } return fixes; } function showReference(selection) { selection.selectAll('.issue-reference') .data([0]) .enter() .append('div') .attr('class', 'issue-reference') .text(t('issues.disconnected_way.routable.reference')); } function routingIslandForEntity(entity) { var routingIsland = new Set(); // the interconnected routable features var waysToCheck = []; // the queue of remaining routable ways to traverse function queueParentWays(node) { graph.parentWays(node).forEach(function(parentWay) { if (!routingIsland.has(parentWay) && // only check each feature once isRoutableWay(parentWay, false)) { // only check routable features routingIsland.add(parentWay); waysToCheck.push(parentWay); } }); } if (entity.type === 'way' && isRoutableWay(entity, true)) { routingIsland.add(entity); waysToCheck.push(entity); } else if (entity.type === 'node' && isRoutableNode(entity)) { routingIsland.add(entity); queueParentWays(entity); } else { // this feature isn't routable, cannot be a routing island return null; } while (waysToCheck.length) { var wayToCheck = waysToCheck.pop(); var childNodes = graph.childNodes(wayToCheck); for (var i in childNodes) { var vertex = childNodes[i]; if (isConnectedVertex(vertex)) { // found a link to the wider network, not a routing island return null; } if (isRoutableNode(vertex)) { routingIsland.add(vertex); } queueParentWays(vertex); } } // no network link found, this is a routing island, return its members return routingIsland; } function isConnectedVertex(vertex) { // assume ways overlapping unloaded tiles are connected to the wider road network - #5938 var osm = services.osm; if (osm && !osm.isDataLoaded(vertex.loc)) return true; // entrances are considered connected if (vertex.tags.entrance && vertex.tags.entrance !== 'no') return true; if (vertex.tags.amenity === 'parking_entrance') return true; return false; } function isRoutableNode(node) { // treat elevators as distinct features in the highway network if (node.tags.highway === 'elevator') return true; return false; } function isRoutableWay(way, ignoreInnerWays) { if (isTaggedAsHighway(way) || way.tags.route === 'ferry') return true; return graph.parentRelations(way).some(function(parentRelation) { if (parentRelation.tags.type === 'route' && parentRelation.tags.route === 'ferry') return true; if (parentRelation.isMultipolygon() && isTaggedAsHighway(parentRelation) && (!ignoreInnerWays || parentRelation.memberById(way.id).role !== 'inner')) return true; }); } function makeContinueDrawingFixIfAllowed(vertexID, whichEnd) { var vertex = graph.entity(vertexID); if (vertex.tags.noexit === 'yes') return null; var useLeftContinue = (whichEnd === 'start' && textDirection === 'ltr') || (whichEnd === 'end' && textDirection === 'rtl'); return new validationIssueFix({ icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''), title: t('issues.fix.continue_from_' + whichEnd + '.title'), entityIds: [vertexID], onClick: function(context) { var wayId = this.issue.entityIds[0]; var way = context.hasEntity(wayId); var vertexId = this.entityIds[0]; var vertex = context.hasEntity(vertexId); if (!way || !vertex) return; // 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: wayId, startGraph: context.graph(), baselineGraph: context.graph(), affix: way.affix(vertexId) }) ); } }); } }; validation.type = type; return validation; }