mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-12 16:52:50 +00:00
845 lines
37 KiB
JavaScript
845 lines
37 KiB
JavaScript
import { isEqual } from 'lodash-es';
|
|
|
|
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 { geoAngle, geoExtent, geoLatToMeters, geoLonToMeters, geoLineIntersection,
|
|
geoSphericalClosestNode, geoSphericalDistance, geoVecAngle, geoVecLength, geoMetersToLat, geoMetersToLon } from '../geo';
|
|
import { osmNode } from '../osm/node';
|
|
import { osmFlowingWaterwayTagValues, osmPathHighwayTagValues, osmRailwayTrackTagValues, osmRoutableAerowayTags, osmRoutableHighwayTagValues } from '../osm/tags';
|
|
import { t } from '../core/localizer';
|
|
import { utilDisplayLabel } from '../util/utilDisplayLabel';
|
|
import { validationIssue, validationIssueFix } from '../core/validation';
|
|
|
|
|
|
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' || featureType === 'aeroway';
|
|
}
|
|
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 (tags.aeroway in osmRoutableAerowayTags) return 'aeroway';
|
|
|
|
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' ||
|
|
taggedAsIndoor(tags1) || taggedAsIndoor(tags2)) {
|
|
// 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
|
|
};
|
|
|
|
/**
|
|
* @returns {object | null} the tags for the connecting node, or null if the entities should not be joined
|
|
*/
|
|
function tagsForConnectionNodeIfAllowed(entity1, entity2, graph, lessLikelyTags) {
|
|
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';
|
|
|
|
/**
|
|
* @typedef {NonNullable<ReturnType<getFeatureType>>} FeatureType
|
|
* @type {`${FeatureType}-${FeatureType}`}
|
|
*/
|
|
const featureTypes = [featureType1, featureType2].sort().join('-');
|
|
|
|
if (featureTypes === 'aeroway-aeroway') return {};
|
|
|
|
if (featureTypes === 'aeroway-highway') {
|
|
const isServiceRoad = entity1.tags.highway === 'service' || entity2.tags.highway === 'service';
|
|
const isPath = entity1.tags.highway in osmPathHighwayTagValues || entity2.tags.highway in osmPathHighwayTagValues;
|
|
// only significant roads get the aeroway=aircraft_crossing tag
|
|
return isServiceRoad || isPath ? {} : { aeroway: 'aircraft_crossing' };
|
|
}
|
|
|
|
if (featureTypes === 'aeroway-railway') {
|
|
return { aeroway: 'aircraft_crossing', railway: 'level_crossing' };
|
|
}
|
|
|
|
if (featureTypes === 'aeroway-waterway') return null;
|
|
|
|
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
|
|
|
|
if (!bothLines) return {};
|
|
|
|
var roadFeature = entity1IsPath ? entity2 : entity1;
|
|
var pathFeature = entity1IsPath ? entity1 : entity2;
|
|
// don't mark path connections with tracks as crossings
|
|
if (roadFeature.tags.highway === 'track') {
|
|
return {};
|
|
}
|
|
// a sidewalk crossing a driveway is unremarkable and unlikely to be interrupted by the driveway
|
|
// a sidewalk crossing another kind of service road may be similarly unremarkable
|
|
if (!lessLikelyTags &&
|
|
roadFeature.tags.highway === 'service' &&
|
|
pathFeature.tags.highway === 'footway' && pathFeature.tags.footway === 'sidewalk') {
|
|
return {};
|
|
}
|
|
if (['marked', 'unmarked', 'traffic_signals', 'uncontrolled'].indexOf(pathFeature.tags.crossing) !== -1) {
|
|
// if the path is a crossing, match the crossing type and markings
|
|
var tags = { highway: 'crossing', crossing: pathFeature.tags.crossing };
|
|
if ('crossing:markings' in pathFeature.tags) {
|
|
tags['crossing:markings'] = pathFeature.tags['crossing:markings'];
|
|
}
|
|
return tags;
|
|
}
|
|
// don't add a `crossing` subtag to ambiguous crossings
|
|
return { highway: 'crossing' };
|
|
}
|
|
return {};
|
|
}
|
|
if (featureType1 === 'waterway') return {};
|
|
if (featureType1 === 'railway') return {};
|
|
|
|
} else {
|
|
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 = 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])
|
|
]
|
|
]);
|
|
|
|
// 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 = geoLineIntersection(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.append('issues.crossing_ways.message', {
|
|
feature: utilDisplayLabel(entity1, graph, featureType1 === 'building'),
|
|
feature2: utilDisplayLabel(entity2, graph, featureType2 === 'building')
|
|
}) : '';
|
|
},
|
|
reference: showReference,
|
|
entityIds: entities.map(function(entity) {
|
|
return entity.id;
|
|
}),
|
|
data: {
|
|
edges: edges,
|
|
featureTypes: featureTypes,
|
|
connectionTags: connectionTags
|
|
},
|
|
hash: uniqueID,
|
|
loc: crossing.crossPoint,
|
|
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));
|
|
let lessLikelyConnectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph, true);
|
|
if (lessLikelyConnectionTags && !isEqual(connectionTags, lessLikelyConnectionTags)) {
|
|
fixes.push(makeConnectWaysFix(lessLikelyConnectionTags));
|
|
}
|
|
}
|
|
|
|
if (isCrossingIndoors) {
|
|
fixes.push(new validationIssueFix({
|
|
icon: 'iD-icon-layers',
|
|
title: t.append('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) {
|
|
if (selectedFeatureType === 'waterway') {
|
|
// naming piped waterway "tunnel" is a confusing osmism, culvert should be more clear
|
|
fixes.push(makeAddBridgeOrTunnelFix('add_a_culvert', 'temaki-waste', 'tunnel'));
|
|
} else {
|
|
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.append('issues.fix.reposition_features.title')
|
|
}));
|
|
|
|
return fixes;
|
|
}
|
|
});
|
|
|
|
function showReference(selection) {
|
|
selection.selectAll('.issue-reference')
|
|
.data([0])
|
|
.enter()
|
|
.append('div')
|
|
.attr('class', 'issue-reference')
|
|
.call(t.append('issues.crossing_ways.' + crossingTypeID + '.reference'));
|
|
}
|
|
}
|
|
|
|
function makeAddBridgeOrTunnelFix(fixTitleID, iconName, bridgeOrTunnel){
|
|
return new validationIssueFix({
|
|
icon: iconName,
|
|
title: t.append('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 && isFinite(crossedWay.tags.width) && Number(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 = geoAngle(edgeNodes[0], edgeNodes[1], projection) + Math.PI;
|
|
var a2 = geoAngle(graph.entity(crossedEdge[0]), graph.entity(crossedEdge[1]), projection) + 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 = geoVecAngle(projEdgeNode1, projEdgeNode2);
|
|
|
|
var projectedCrossingLoc = geomToProj(crossingLoc);
|
|
var linearToSphericalMetersRatio = geoVecLength(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 makeConnectWaysFix(connectionTags) {
|
|
|
|
var fixTitleID = 'connect_features';
|
|
var fixIcon = 'iD-icon-crossing';
|
|
if (connectionTags.highway === 'crossing') {
|
|
fixTitleID = 'connect_using_crossing';
|
|
fixIcon = 'temaki-pedestrian';
|
|
}
|
|
if (connectionTags.ford) {
|
|
fixTitleID = 'connect_using_ford';
|
|
fixIcon = 'roentgen-ford';
|
|
}
|
|
|
|
const fix = new validationIssueFix({
|
|
icon: fixIcon,
|
|
title: t.append('issues.fix.' + fixTitleID + '.title'),
|
|
onClick: function(context) {
|
|
var loc = this.issue.loc;
|
|
var edges = this.issue.data.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 nearby = geoSphericalClosestNode(edgeNodes, loc);
|
|
// if there is already a suitable node nearby, use that
|
|
// use the node if node has no interesting tags or if it is a crossing node #8326
|
|
if ((!nearby.node.hasInterestingTags() || nearby.node.isCrossing()) && nearby.distance < mergeThresholdInMeters) {
|
|
nodesToMerge.push(nearby.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.connect_crossing_features.annotation')
|
|
);
|
|
}
|
|
});
|
|
fix._connectionTags = connectionTags;
|
|
return fix;
|
|
}
|
|
|
|
function makeChangeLayerFix(higherOrLower) {
|
|
return new validationIssueFix({
|
|
icon: 'iD-icon-' + (higherOrLower === 'higher' ? 'up' : 'down'),
|
|
title: t.append('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;
|
|
}
|