Skip to content

refactor isOneWay to properly support bidirectional ways #10730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion modules/osm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export {
osmVertexTags,
osmSetVertexTags,
osmNodeGeometriesForTags,
osmOneWayTags,
osmPavedTags,
osmIsInterestingTag,
osmLifecyclePrefixes,
Expand Down
31 changes: 28 additions & 3 deletions modules/osm/tags.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { merge } from 'lodash-es';

export function osmIsInterestingTag(key) {
return key !== 'attribution' &&
key !== 'created_by' &&
Expand Down Expand Up @@ -124,7 +126,7 @@ export function osmNodeGeometriesForTags(nodeTags) {
return geometries;
}

export var osmOneWayTags = {
export const osmOneWayForwardTags = {
'aerialway': {
'chair_lift': true,
'drag_lift': true,
Expand All @@ -138,8 +140,6 @@ export var osmOneWayTags = {
},
'conveying': {
'forward': true,
'backward': true,
'reversible': true,
},
'highway': {
'motorway': true
Expand All @@ -152,6 +152,9 @@ export var osmOneWayTags = {
'goods_conveyor': true,
'piste:halfpipe': true
},
'oneway': {
'yes': true,
},
'piste:type': {
'downhill': true,
'sled': true,
Expand All @@ -176,6 +179,28 @@ export var osmOneWayTags = {
'tidal_channel': true
}
};
export const osmOneWayBackwardTags = {
'conveying': {
'backward': true,
},
'oneway': {
'-1': true,
},
};
export const osmOneWayBiDirectionalTags = {
'conveying': {
'reversible': true,
},
'oneway': {
'alternating': true,
'reversible': true,
},
};
export const osmOneWayTags = merge(
osmOneWayForwardTags,
osmOneWayBackwardTags,
osmOneWayBiDirectionalTags,
);

// solid and smooth surfaces akin to the assumed default road surface in OSM
export var osmPavedTags = {
Expand Down
51 changes: 27 additions & 24 deletions modules/osm/way.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { geoArea as d3_geoArea } from 'd3-geo';
import { geoExtent, geoVecCross } from '../geo';
import { osmEntity } from './entity';
import { osmLanes } from './lanes';
import { osmTagSuggestingArea, osmOneWayTags, osmRightSideIsInsideTags, osmRemoveLifecyclePrefix } from './tags';
import { utilArrayUniq } from '../util';
import { osmTagSuggestingArea, osmRightSideIsInsideTags, osmRemoveLifecyclePrefix, osmOneWayBiDirectionalTags, osmOneWayBackwardTags, osmOneWayForwardTags, osmOneWayTags } from './tags';
import { utilArrayUniq, utilCheckTagDictionary } from '../util';


export function osmWay() {
Expand Down Expand Up @@ -138,29 +138,32 @@ Object.assign(osmWay.prototype, {
},


isOneWay: function() {
// explicit oneway tag..
var values = {
'yes': true,
'1': true,
'-1': true,
'reversible': true,
'alternating': true,
'no': false,
'0': false
};
if (values[this.tags.oneway] !== undefined) {
return values[this.tags.oneway];
}
/** @returns {boolean} for example, if `oneway=yes` */
isOneWayForwards() {
if (this.tags.oneway === 'no') return false;

// implied oneway tag..
for (var key in this.tags) {
if (key in osmOneWayTags &&
(this.tags[key] in osmOneWayTags[key])) {
return true;
}
}
return false;
return !!utilCheckTagDictionary(this.tags, osmOneWayForwardTags);
},

/** @returns {boolean} for example, if `oneway=-1` */
isOneWayBackwards() {
if (this.tags.oneway === 'no') return false;

return !!utilCheckTagDictionary(this.tags, osmOneWayBackwardTags);
},

/** @returns {boolean} for example, if `oneway=alternating` */
isBiDirectional() {
if (this.tags.oneway === 'no') return false;

return !!utilCheckTagDictionary(this.tags, osmOneWayBiDirectionalTags);
},

/** @returns {boolean} */
isOneWay() {
if (this.tags.oneway === 'no') return false;

return !!utilCheckTagDictionary(this.tags, osmOneWayTags);
},

// Some identifier for tag that implies that this way is "sided",
Expand Down
15 changes: 2 additions & 13 deletions modules/svg/lines.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,19 +265,8 @@ export function svgLines(projection, context) {
var onewayArr = v.filter(function(d) { return d.isOneWay(); });
var onewaySegments = svgMarkerSegments(
projection, graph, 35,
function shouldReverse(entity) {
return (
entity.tags.oneway === '-1'
|| entity.tags.conveying === 'backward'
);
},
function bothDirections(entity) {
return (
entity.tags.oneway === 'alternating'
|| entity.tags.oneway === 'reversible'
|| entity.tags.conveying === 'reversible'
);
}
entity => entity.isOneWayBackwards(),
entity => entity.isBiDirectional(),
);
onewaydata[k] = utilArrayFlatten(onewayArr.map(onewaySegments));

Expand Down
10 changes: 3 additions & 7 deletions modules/ui/fields/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import { utilRebind } from '../../util/rebind';
import { t } from '../../core/localizer';
import { actionReverse } from '../../actions/reverse';
import { osmOneWayTags } from '../../osm';
import { svgIcon } from '../../svg/icon';

export { uiFieldCheck as uiFieldDefaultCheck };
Expand Down Expand Up @@ -61,12 +60,9 @@ export function uiFieldCheck(field, context) {
// where implied oneway tag exists (e.g. `junction=roundabout`) #2220, #1841
if (field.id === 'oneway') {
var entity = context.entity(_entityIDs[0]);
for (var key in entity.tags) {
if (key in osmOneWayTags && (entity.tags[key] in osmOneWayTags[key])) {
_impliedYes = true;
texts[0] = t.html('_tagging.presets.fields.oneway_yes.options.undefined');
break;
}
if (entity.type === 'way' && entity.isOneWay()) {
_impliedYes = true;
texts[0] = t.html('_tagging.presets.fields.oneway_yes.options.undefined');
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion modules/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export { utilHashcode } from './util';
export { utilHighlightEntities } from './util';
export { utilKeybinding } from './keybinding';
export { utilNoAuto } from './util';
export { utilObjectOmit } from './object';
export { utilObjectOmit, utilCheckTagDictionary } from './object';
export { utilCompareIDs } from './util';
export { utilOldestID } from './util';
export { utilPrefixCSSProperty } from './util';
Expand Down
23 changes: 23 additions & 0 deletions modules/util/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,26 @@ export function utilObjectOmit(obj, omitKeys) {
return result;
}, {});
}

/**
* @template T
* @typedef {{ [key: string]: { [value: string]: T } }} TagDictionary<T>
*/

/**
* searches a dictionary for a match, such as `osmOneWayForwardTags`,
* `osmAreaKeysExceptions`, etc.
* @template T
* @param {Tags} tags
* @param {TagDictionary<T>} tagDictionary
* @returns {T | undefined}
*/
export function utilCheckTagDictionary(tags, tagDictionary) {
for (const key in tags) {
const value = tags[key];
if (tagDictionary[key] && value in tagDictionary[key]) {
return tagDictionary[key][value];
}
}
return undefined;
}
18 changes: 3 additions & 15 deletions modules/validations/impossible_oneway.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { t, localizer } from '../core/localizer';
import { modeDrawLine } from '../modes/draw_line';
import { actionReverse } from '../actions/reverse';
import { utilDisplayLabel } from '../util/utilDisplayLabel';
import { osmFlowingWaterwayTagValues, osmOneWayTags, osmRoutableHighwayTagValues } from '../osm/tags';
import { osmFlowingWaterwayTagValues, osmRoutableHighwayTagValues } from '../osm/tags';
import { validationIssue, validationIssueFix } from '../core/validation';
import { services } from '../services';

Expand All @@ -17,7 +17,7 @@ export function validationImpossibleOneway() {

if (!typeForWay(entity)) return [];

if (!isOneway(entity)) return [];
if (!entity.isOneWay()) return [];

var firstIssues = issuesForNode(entity, entity.first());
var lastIssues = issuesForNode(entity, entity.last());
Expand All @@ -32,18 +32,6 @@ export function validationImpossibleOneway() {
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 occurrences = 0;
for (var index in way.nodes) {
Expand Down Expand Up @@ -129,7 +117,7 @@ export function validationImpossibleOneway() {
if (wayType === 'waterway' && attachedWaysOfSameType.length === 0) return [];

var attachedOneways = attachedWaysOfSameType.filter(function(attachedWay) {
return isOneway(attachedWay);
return attachedWay.isOneWay();
});

// ignore if the way is connected to some non-oneway features
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"@types/chai": "^5.0.1",
"@types/d3": "^7.4.3",
"@types/happen": "^0.3.0",
"@types/lodash-es": "^4.17.12",
"@types/sinon": "^17.0.3",
"@types/sinon-chai": "^4.0.0",
"autoprefixer": "^10.4.20",
Expand Down
1 change: 0 additions & 1 deletion test/spec/osm/way.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ describe('iD.osmWay', function() {

it('returns true when the way has tag oneway=yes', function() {
expect(iD.osmWay({tags: { oneway: 'yes' }}).isOneWay(), 'oneway yes').to.be.true;
expect(iD.osmWay({tags: { oneway: '1' }}).isOneWay(), 'oneway 1').to.be.true;
expect(iD.osmWay({tags: { oneway: '-1' }}).isOneWay(), 'oneway -1').to.be.true;
});

Expand Down
13 changes: 13 additions & 0 deletions test/spec/util/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,16 @@ describe('iD.utilObjectOmit', function() {
});
});


describe('iD.utilCheckTagDictionary', () => {
it('can search a standard tag-dictionary', () => {
expect(iD.utilCheckTagDictionary({}, iD.osmPavedTags)).toBeUndefined();
expect(iD.utilCheckTagDictionary({ surface: 'asphalt' }, iD.osmPavedTags)).toBe(true);
});

it('works for falsy values', () => {
const dictionary = { surface: { paved: 0 } };
expect(iD.utilCheckTagDictionary({}, dictionary)).toBeUndefined();
expect(iD.utilCheckTagDictionary({ surface: 'paved' }, dictionary)).toBe(0);
});
});