modules/validations/crossing_ways.js (598 lines of code) (raw):

import { actionAddMidpoint } from '../actions/add_midpoint'; import { actionChangeTags } from '../actions/change_tags'; import { actionMergeNodes } from '../actions/merge_nodes'; import { actionSplit } from '../actions/split'; import { modeSelect } from '../modes/select'; import { vecAngle, vecLength, geomLineIntersection } from '@id-sdk/math'; import { geoLatToMeters, geoLonToMeters, geoSphericalClosestPoint, geoSphericalDistance, geoMetersToLat, geoMetersToLon } from '@id-sdk/geo'; import { osmNode } from '../osm/node'; import { osmFlowingWaterwayTagValues, osmPathHighwayTagValues, osmRailwayTrackTagValues, osmRoutableHighwayTagValues } from '../osm/tags'; import { t } from '../core/localizer'; import { utilDisplayLabel } from '../util'; import { validationIssue, validationIssueFix } from '../core/validation'; import { Extent } from '@id-sdk/extent'; export function validationCrossingWays(context) { var type = 'crossing_ways'; // returns the way or its parent relation, whichever has a useful feature type function getFeatureWithFeatureTypeTagsForWay(way, graph) { if (getFeatureType(way, graph) === null) { // if the way doesn't match a feature type, check its parent relations var parentRels = graph.parentRelations(way); for (var i = 0; i < parentRels.length; i++) { var rel = parentRels[i]; if (getFeatureType(rel, graph) !== null) { return rel; } } } return way; } function hasTag(tags, key) { return tags[key] !== undefined && tags[key] !== 'no'; } function taggedAsIndoor(tags) { return hasTag(tags, 'indoor') || hasTag(tags, 'level') || tags.highway === 'corridor'; } function allowsBridge(featureType) { return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway'; } function allowsTunnel(featureType) { return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway'; } // discard var ignoredBuildings = { demolished: true, dismantled: true, proposed: true, razed: true }; function getFeatureType(entity, graph) { var geometry = entity.geometry(graph); if (geometry !== 'line' && geometry !== 'area') return null; var tags = entity.tags; if (hasTag(tags, 'building') && !ignoredBuildings[tags.building]) return 'building'; if (hasTag(tags, 'highway') && osmRoutableHighwayTagValues[tags.highway]) return 'highway'; // don't check railway or waterway areas if (geometry !== 'line') return null; if (hasTag(tags, 'railway') && osmRailwayTrackTagValues[tags.railway]) return 'railway'; if (hasTag(tags, 'waterway') && osmFlowingWaterwayTagValues[tags.waterway]) return 'waterway'; return null; } function isLegitCrossing(tags1, featureType1, tags2, featureType2) { // assume 0 by default var level1 = tags1.level || '0'; var level2 = tags2.level || '0'; if (taggedAsIndoor(tags1) && taggedAsIndoor(tags2) && level1 !== level2) { // assume features don't interact if they're indoor on different levels return true; } // assume 0 by default; don't use way.layer() since we account for structures here var layer1 = tags1.layer || '0'; var layer2 = tags2.layer || '0'; if (allowsBridge(featureType1) && allowsBridge(featureType2)) { if (hasTag(tags1, 'bridge') && !hasTag(tags2, 'bridge')) return true; if (!hasTag(tags1, 'bridge') && hasTag(tags2, 'bridge')) return true; // crossing bridges must use different layers if (hasTag(tags1, 'bridge') && hasTag(tags2, 'bridge') && layer1 !== layer2) return true; } else if (allowsBridge(featureType1) && hasTag(tags1, 'bridge')) return true; else if (allowsBridge(featureType2) && hasTag(tags2, 'bridge')) return true; if (allowsTunnel(featureType1) && allowsTunnel(featureType2)) { if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true; if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true; // crossing tunnels must use different layers if (hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel') && layer1 !== layer2) return true; } else if (allowsTunnel(featureType1) && hasTag(tags1, 'tunnel')) return true; else if (allowsTunnel(featureType2) && hasTag(tags2, 'tunnel')) return true; // don't flag crossing waterways and pier/highways if (featureType1 === 'waterway' && featureType2 === 'highway' && tags2.man_made === 'pier') return true; if (featureType2 === 'waterway' && featureType1 === 'highway' && tags1.man_made === 'pier') return true; if (featureType1 === 'building' || featureType2 === 'building') { // for building crossings, different layers are enough if (layer1 !== layer2) return true; } return false; } // highway values for which we shouldn't recommend connecting to waterways var highwaysDisallowingFords = { motorway: true, motorway_link: true, trunk: true, trunk_link: true, primary: true, primary_link: true, secondary: true, secondary_link: true }; var nonCrossingHighways = { track: true }; function tagsForConnectionNodeIfAllowed(entity1, entity2, graph) { var featureType1 = getFeatureType(entity1, graph); var featureType2 = getFeatureType(entity2, graph); var geometry1 = entity1.geometry(graph); var geometry2 = entity2.geometry(graph); var bothLines = geometry1 === 'line' && geometry2 === 'line'; if (featureType1 === featureType2) { if (featureType1 === 'highway') { var entity1IsPath = osmPathHighwayTagValues[entity1.tags.highway]; var entity2IsPath = osmPathHighwayTagValues[entity2.tags.highway]; if ((entity1IsPath || entity2IsPath) && entity1IsPath !== entity2IsPath) { // one feature is a path but not both var roadFeature = entity1IsPath ? entity2 : entity1; if (nonCrossingHighways[roadFeature.tags.highway]) { // don't mark path connections with certain roads as crossings return {}; } var pathFeature = entity1IsPath ? entity1 : entity2; if (['marked', 'unmarked'].indexOf(pathFeature.tags.crossing) !== -1) { // if the path is a crossing, match the crossing type return bothLines ? { highway: 'crossing', crossing: pathFeature.tags.crossing } : {}; } // don't add a `crossing` subtag to ambiguous crossings return bothLines ? { highway: 'crossing' } : {}; } return {}; } if (featureType1 === 'waterway') return {}; if (featureType1 === 'railway') return {}; } else { var featureTypes = [featureType1, featureType2]; if (featureTypes.indexOf('highway') !== -1) { if (featureTypes.indexOf('railway') !== -1) { if (!bothLines) return {}; var isTram = entity1.tags.railway === 'tram' || entity2.tags.railway === 'tram'; if (osmPathHighwayTagValues[entity1.tags.highway] || osmPathHighwayTagValues[entity2.tags.highway]) { // path-tram connections use this tag if (isTram) return { railway: 'tram_crossing' }; // other path-rail connections use this tag return { railway: 'crossing' }; } else { // path-tram connections use this tag if (isTram) return { railway: 'tram_level_crossing' }; // other 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[entity1.tags.highway] || highwaysDisallowingFords[entity2.tags.highway]) { // do not allow fords on major highways return null; } return bothLines ? { ford: 'yes' } : {}; } } } return null; } function findCrossingsByWay(way1, graph, tree) { var edgeCrossInfos = []; if (way1.type !== 'way') return edgeCrossInfos; var taggedFeature1 = getFeatureWithFeatureTypeTagsForWay(way1, graph); var way1FeatureType = getFeatureType(taggedFeature1, graph); if (way1FeatureType === null) return edgeCrossInfos; var checkedSingleCrossingWays = {}; // declare vars ahead of time to reduce garbage collection var i, j; var extent; var n1, n2, nA, nB, nAId, nBId; var segment1, segment2; var oneOnly; var segmentInfos, segment2Info, way2, taggedFeature2, way2FeatureType; var way1Nodes = graph.childNodes(way1); var comparedWays = {}; for (i = 0; i < way1Nodes.length - 1; i++) { n1 = way1Nodes[i]; n2 = way1Nodes[i + 1]; extent = new Extent( [ 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]) ] ); // Optimize by only checking overlapping segments, not every segment // of overlapping ways segmentInfos = tree.waySegments(extent, graph); for (j = 0; j < segmentInfos.length; j++) { segment2Info = segmentInfos[j]; // don't check for self-intersection in this validation if (segment2Info.wayId === way1.id) continue; // skip if this way was already checked and only one issue is needed if (checkedSingleCrossingWays[segment2Info.wayId]) continue; // mark this way as checked even if there are no crossings comparedWays[segment2Info.wayId] = true; way2 = graph.hasEntity(segment2Info.wayId); if (!way2) continue; taggedFeature2 = getFeatureWithFeatureTypeTagsForWay(way2, graph); // only check crossing highway, waterway, building, and railway way2FeatureType = getFeatureType(taggedFeature2, graph); if (way2FeatureType === null || isLegitCrossing(taggedFeature1.tags, way1FeatureType, taggedFeature2.tags, way2FeatureType)) { continue; } // create only one issue for building crossings oneOnly = way1FeatureType === 'building' || way2FeatureType === 'building'; nAId = segment2Info.nodes[0]; nBId = segment2Info.nodes[1]; if (nAId === n1.id || nAId === n2.id || nBId === n1.id || nBId === n2.id) { // n1 or n2 is a connection node; skip continue; } nA = graph.hasEntity(nAId); if (!nA) continue; nB = graph.hasEntity(nBId); if (!nB) continue; segment1 = [n1.loc, n2.loc]; segment2 = [nA.loc, nB.loc]; var point = geomLineIntersection(segment1, segment2); if (point) { edgeCrossInfos.push({ wayInfos: [ { way: way1, featureType: way1FeatureType, edge: [n1.id, n2.id] }, { way: way2, featureType: way2FeatureType, edge: [nA.id, nB.id] } ], crossPoint: point }); if (oneOnly) { checkedSingleCrossingWays[way2.id] = true; break; } } } } return edgeCrossInfos; } function waysToCheck(entity, graph) { var featureType = getFeatureType(entity, graph); if (!featureType) return []; if (entity.type === 'way') { return [entity]; } else if (entity.type === 'relation') { return entity.members.reduce(function(array, member) { if (member.type === 'way' && // only look at geometry ways (!member.role || member.role === 'outer' || member.role === 'inner')) { var entity = graph.hasEntity(member.id); // don't add duplicates if (entity && array.indexOf(entity) === -1) { array.push(entity); } } return array; }, []); } return []; } var validation = function checkCrossingWays(entity, graph) { var tree = context.history().tree(); var ways = waysToCheck(entity, graph); var issues = []; // declare these here to reduce garbage collection var wayIndex, crossingIndex, crossings; for (wayIndex in ways) { crossings = findCrossingsByWay(ways[wayIndex], graph, tree); for (crossingIndex in crossings) { issues.push(createIssue(crossings[crossingIndex], graph)); } } return issues; }; function createIssue(crossing, graph) { // use the entities with the tags that define the feature type crossing.wayInfos.sort(function(way1Info, way2Info) { var type1 = way1Info.featureType; var type2 = way2Info.featureType; if (type1 === type2) { return utilDisplayLabel(way1Info.way, graph) > utilDisplayLabel(way2Info.way, graph); } else if (type1 === 'waterway') { return true; } else if (type2 === 'waterway') { return false; } return type1 < type2; }); var entities = crossing.wayInfos.map(function(wayInfo) { return getFeatureWithFeatureTypeTagsForWay(wayInfo.way, graph); }); var edges = [crossing.wayInfos[0].edge, crossing.wayInfos[1].edge]; var featureTypes = [crossing.wayInfos[0].featureType, crossing.wayInfos[1].featureType]; var connectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph); var featureType1 = crossing.wayInfos[0].featureType; var featureType2 = crossing.wayInfos[1].featureType; var isCrossingIndoors = taggedAsIndoor(entities[0].tags) && taggedAsIndoor(entities[1].tags); var isCrossingTunnels = allowsTunnel(featureType1) && hasTag(entities[0].tags, 'tunnel') && allowsTunnel(featureType2) && hasTag(entities[1].tags, 'tunnel'); var isCrossingBridges = allowsBridge(featureType1) && hasTag(entities[0].tags, 'bridge') && allowsBridge(featureType2) && hasTag(entities[1].tags, 'bridge'); var subtype = [featureType1, featureType2].sort().join('-'); var crossingTypeID = subtype; if (isCrossingIndoors) { crossingTypeID = 'indoor-indoor'; } else if (isCrossingTunnels) { crossingTypeID = 'tunnel-tunnel'; } else if (isCrossingBridges) { crossingTypeID = 'bridge-bridge'; } if (connectionTags && (isCrossingIndoors || isCrossingTunnels || isCrossingBridges)) { crossingTypeID += '_connectable'; } // Differentiate based on the loc rounded to 4 digits, since two ways can cross multiple times. var uniqueID = '' + crossing.crossPoint[0].toFixed(4) + ',' + crossing.crossPoint[1].toFixed(4); return new validationIssue({ type: type, subtype: subtype, severity: 'warning', message: function(context) { var graph = context.graph(); var entity1 = graph.hasEntity(this.entityIds[0]), entity2 = graph.hasEntity(this.entityIds[1]); return (entity1 && entity2) ? t.html('issues.crossing_ways.message', { feature: utilDisplayLabel(entity1, graph), feature2: utilDisplayLabel(entity2, graph) }) : ''; }, reference: showReference, entityIds: entities.map(function(entity) { return entity.id; }), data: { edges: edges, featureTypes: featureTypes, connectionTags: connectionTags }, hash: uniqueID, loc: crossing.crossPoint, autoArgs: connectionTags && !connectionTags.ford && getConnectWaysAction(crossing.crossPoint, edges, connectionTags), dynamicFixes: function(context) { var mode = context.mode(); if (!mode || mode.id !== 'select' || mode.selectedIDs().length !== 1) return []; var selectedIndex = this.entityIds[0] === mode.selectedIDs()[0] ? 0 : 1; var selectedFeatureType = this.data.featureTypes[selectedIndex]; var otherFeatureType = this.data.featureTypes[selectedIndex === 0 ? 1 : 0]; var fixes = []; if (connectionTags) { fixes.push(makeConnectWaysFix(this.data.connectionTags)); } if (isCrossingIndoors) { fixes.push(new validationIssueFix({ icon: 'iD-icon-layers', title: t.html('issues.fix.use_different_levels.title') })); } else if (isCrossingTunnels || isCrossingBridges || featureType1 === 'building' || featureType2 === 'building') { fixes.push(makeChangeLayerFix('higher')); fixes.push(makeChangeLayerFix('lower')); // can only add bridge/tunnel if both features are lines } else if (context.graph().geometry(this.entityIds[0]) === 'line' && context.graph().geometry(this.entityIds[1]) === 'line') { // don't recommend adding bridges to waterways since they're uncommon if (allowsBridge(selectedFeatureType) && selectedFeatureType !== 'waterway') { fixes.push(makeAddBridgeOrTunnelFix('add_a_bridge', 'temaki-bridge', 'bridge')); } // don't recommend adding tunnels under waterways since they're uncommon var skipTunnelFix = otherFeatureType === 'waterway' && selectedFeatureType !== 'waterway'; if (allowsTunnel(selectedFeatureType) && !skipTunnelFix) { fixes.push(makeAddBridgeOrTunnelFix('add_a_tunnel', 'temaki-tunnel', 'tunnel')); } } // repositioning the features is always an option fixes.push(new validationIssueFix({ icon: 'iD-operation-move', title: t.html('issues.fix.reposition_features.title') })); return fixes; } }); function showReference(selection) { selection.selectAll('.issue-reference') .data([0]) .enter() .append('div') .attr('class', 'issue-reference') .html(t.html('issues.crossing_ways.' + crossingTypeID + '.reference')); } } function makeAddBridgeOrTunnelFix(fixTitleID, iconName, bridgeOrTunnel){ return new validationIssueFix({ icon: iconName, title: t.html('issues.fix.' + fixTitleID + '.title'), onClick: function(context) { var mode = context.mode(); if (!mode || mode.id !== 'select') return; var selectedIDs = mode.selectedIDs(); if (selectedIDs.length !== 1) return; var selectedWayID = selectedIDs[0]; if (!context.hasEntity(selectedWayID)) return; var resultWayIDs = [selectedWayID]; var edge, crossedEdge, crossedWayID; if (this.issue.entityIds[0] === selectedWayID) { edge = this.issue.data.edges[0]; crossedEdge = this.issue.data.edges[1]; crossedWayID = this.issue.entityIds[1]; } else { edge = this.issue.data.edges[1]; crossedEdge = this.issue.data.edges[0]; crossedWayID = this.issue.entityIds[0]; } var crossingLoc = this.issue.loc; var projection = context.projection; var action = function actionAddStructure(graph) { var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])]; var crossedWay = graph.hasEntity(crossedWayID); // use the explicit width of the crossed feature as the structure length, if available var structLengthMeters = crossedWay && crossedWay.tags.width && parseFloat(crossedWay.tags.width); if (!structLengthMeters) { // if no explicit width is set, approximate the width based on the tags structLengthMeters = crossedWay && crossedWay.impliedLineWidthMeters(); } if (structLengthMeters) { if (getFeatureType(crossedWay, graph) === 'railway') { // bridges over railways are generally much longer than the rail bed itself, compensate structLengthMeters *= 2; } } else { // should ideally never land here since all rail/water/road tags should have an implied width structLengthMeters = 8; } var a1 = vecAngle(projection(edgeNodes[0].loc), projection(edgeNodes[1].loc)) + Math.PI; var a2 = vecAngle(projection(graph.entity(crossedEdge[0]).loc), projection(graph.entity(crossedEdge[1]).loc)) + Math.PI; var crossingAngle = Math.max(a1, a2) - Math.min(a1, a2); if (crossingAngle > Math.PI) crossingAngle -= Math.PI; // lengthen the structure to account for the angle of the crossing structLengthMeters = ((structLengthMeters / 2) / Math.sin(crossingAngle)) * 2; // add padding since the structure must extend past the edges of the crossed feature structLengthMeters += 4; // clamp the length to a reasonable range structLengthMeters = Math.min(Math.max(structLengthMeters, 4), 50); function geomToProj(geoPoint) { return [ geoLonToMeters(geoPoint[0], geoPoint[1]), geoLatToMeters(geoPoint[1]) ]; } function projToGeom(projPoint) { var lat = geoMetersToLat(projPoint[1]); return [ geoMetersToLon(projPoint[0], lat), lat ]; } var projEdgeNode1 = geomToProj(edgeNodes[0].loc); var projEdgeNode2 = geomToProj(edgeNodes[1].loc); var projectedAngle = vecAngle(projEdgeNode1, projEdgeNode2); var projectedCrossingLoc = geomToProj(crossingLoc); var linearToSphericalMetersRatio = vecLength(projEdgeNode1, projEdgeNode2) / geoSphericalDistance(edgeNodes[0].loc, edgeNodes[1].loc); function locSphericalDistanceFromCrossingLoc(angle, distanceMeters) { var lengthSphericalMeters = distanceMeters * linearToSphericalMetersRatio; return projToGeom([ projectedCrossingLoc[0] + Math.cos(angle) * lengthSphericalMeters, projectedCrossingLoc[1] + Math.sin(angle) * lengthSphericalMeters ]); } var endpointLocGetter1 = function(lengthMeters) { return locSphericalDistanceFromCrossingLoc(projectedAngle, lengthMeters); }; var endpointLocGetter2 = function(lengthMeters) { return locSphericalDistanceFromCrossingLoc(projectedAngle + Math.PI, lengthMeters); }; // avoid creating very short edges from splitting too close to another node var minEdgeLengthMeters = 0.55; // decide where to bound the structure along the way, splitting as necessary function determineEndpoint(edge, endNode, locGetter) { var newNode; var idealLengthMeters = structLengthMeters / 2; // distance between the crossing location and the end of the edge, // the maximum length of this side of the structure var crossingToEdgeEndDistance = geoSphericalDistance(crossingLoc, endNode.loc); if (crossingToEdgeEndDistance - idealLengthMeters > minEdgeLengthMeters) { // the edge is long enough to insert a new node // the loc that would result in the full expected length var idealNodeLoc = locGetter(idealLengthMeters); newNode = osmNode(); graph = actionAddMidpoint({ loc: idealNodeLoc, edge: edge }, newNode)(graph); } else { var edgeCount = 0; endNode.parentIntersectionWays(graph).forEach(function(way) { way.nodes.forEach(function(nodeID) { if (nodeID === endNode.id) { if ((endNode.id === way.first() && endNode.id !== way.last()) || (endNode.id === way.last() && endNode.id !== way.first())) { edgeCount += 1; } else { edgeCount += 2; } } }); }); if (edgeCount >= 3) { // the end node is a junction, try to leave a segment // between it and the structure - #7202 var insetLength = crossingToEdgeEndDistance - minEdgeLengthMeters; if (insetLength > minEdgeLengthMeters) { var insetNodeLoc = locGetter(insetLength); newNode = osmNode(); graph = actionAddMidpoint({ loc: insetNodeLoc, edge: edge }, newNode)(graph); } } } // if the edge is too short to subdivide as desired, then // just bound the structure at the existing end node if (!newNode) newNode = endNode; var splitAction = actionSplit([newNode.id]) .limitWays(resultWayIDs); // only split selected or created ways // do the split graph = splitAction(graph); if (splitAction.getCreatedWayIDs().length) { resultWayIDs.push(splitAction.getCreatedWayIDs()[0]); } return newNode; } var structEndNode1 = determineEndpoint(edge, edgeNodes[1], endpointLocGetter1); var structEndNode2 = determineEndpoint([edgeNodes[0].id, structEndNode1.id], edgeNodes[0], endpointLocGetter2); var structureWay = resultWayIDs.map(function(id) { return graph.entity(id); }).find(function(way) { return way.nodes.indexOf(structEndNode1.id) !== -1 && way.nodes.indexOf(structEndNode2.id) !== -1; }); var tags = Object.assign({}, structureWay.tags); // copy tags if (bridgeOrTunnel === 'bridge'){ tags.bridge = 'yes'; tags.layer = '1'; } else { var tunnelValue = 'yes'; if (getFeatureType(structureWay, graph) === 'waterway') { // use `tunnel=culvert` for waterways by default tunnelValue = 'culvert'; } tags.tunnel = tunnelValue; tags.layer = '-1'; } // apply the structure tags to the way graph = actionChangeTags(structureWay.id, tags)(graph); return graph; }; context.perform(action, t('issues.fix.' + fixTitleID + '.annotation')); context.enter(modeSelect(context, resultWayIDs)); } }); } function getConnectWaysAction(loc, edges, connectionTags) { var fn = function actionConnectCrossingWays(graph) { var didSomething = false; // Create a new candidate node which will be inserted at the crossing point.. var newNode = osmNode({ loc: loc, tags: connectionTags }); var newGraph = graph.replace(newNode); var nodesToMerge = [newNode.id]; var mergeThresholdInMeters = 0.75; // Insert the new node along the edges (or reuse one already there).. edges.forEach(function(edge) { var n0 = newGraph.hasEntity(edge[0]); var n1 = newGraph.hasEntity(edge[1]); if (!n0 || !n1) return; // graph has changed and these nodes are no longer there? // Look for a suitable existing node nearby to reuse.. var canReuse = false; var edgeNodes = [n0, n1]; var closest = geoSphericalClosestPoint([n0.loc, n1.loc], loc); if (closest && closest.distance < mergeThresholdInMeters) { var closeNode = edgeNodes[closest.index]; // Reuse the close node if it has no interesting tags or if it is already a crossing - #8326 if (!closeNode.hasInterestingTags() || closeNode.isCrossing()) { canReuse = true; nodesToMerge.push(closeNode.id); } } if (!canReuse) { newGraph = actionAddMidpoint({loc: loc, edge: edge}, newNode)(newGraph); // Insert the new node didSomething = true; } }); if (nodesToMerge.length > 1) { // If we're reusing nearby nodes, merge them with the new node newGraph = actionMergeNodes(nodesToMerge, loc)(newGraph); didSomething = true; } return didSomething ? newGraph : graph; }; return [fn, t('issues.fix.connect_crossing_features.annotation')]; } function makeConnectWaysFix(connectionTags) { var fixTitleID = 'connect_features'; if (connectionTags.ford) { fixTitleID = 'connect_using_ford'; } return new validationIssueFix({ icon: 'iD-icon-crossing', title: t.html('issues.fix.' + fixTitleID + '.title'), onClick: function(context) { var loc = this.issue.loc; var edges = this.issue.data.edges; var connectionTags = this.issue.data.connectionTags; var action = getConnectWaysAction(loc, edges, connectionTags); context.perform(action[0], action[1]); // function, annotation } }); } function makeChangeLayerFix(higherOrLower) { return new validationIssueFix({ icon: 'iD-icon-' + (higherOrLower === 'higher' ? 'up' : 'down'), title: t.html('issues.fix.tag_this_as_' + higherOrLower + '.title'), onClick: function(context) { var mode = context.mode(); if (!mode || mode.id !== 'select') return; var selectedIDs = mode.selectedIDs(); if (selectedIDs.length !== 1) return; var selectedID = selectedIDs[0]; if (!this.issue.entityIds.some(function(entityId) { return entityId === selectedID; })) return; var entity = context.hasEntity(selectedID); if (!entity) return; var tags = Object.assign({}, entity.tags); // shallow copy var layer = tags.layer && Number(tags.layer); if (layer && !isNaN(layer)) { if (higherOrLower === 'higher') { layer += 1; } else { layer -= 1; } } else { if (higherOrLower === 'higher') { layer = 1; } else { layer = -1; } } tags.layer = layer.toString(); context.perform( actionChangeTags(entity.id, tags), t('operations.change_tags.annotation') ); } }); } validation.type = type; return validation; }