Skip to content

Commit

Permalink
Add one-sided triangular markers to ways with sides (e.g. natural=cli…
Browse files Browse the repository at this point in the history
…ff).

This generalizes the oneway arrow logic for adding SVG markers along a
line. Using that functionality, certain tags get arrows on their
right-hand side, indicating which side is "inside", e.g. the
right-side of a cliff is the lower side.

The list of tags considered to be sided (unless there's a
two_sided=yes tag) is:

- natural=cliff
- barrier=retaining_wall
- barrier=kerb
- barrier=guard_rail
- barrier=city_wall
- man_made=embankment

The triangles attempt to be reminiscent of the triangles used for
rendering cliffs on OSM (and elsewhere).

Fixes openstreetmap#1475.
  • Loading branch information
huonw committed Nov 27, 2018
1 parent f356c6a commit caff3ae
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 42 deletions.
3 changes: 2 additions & 1 deletion css/20_map.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ text {
}

.onewaygroup path.oneway,
.viewfieldgroup path.viewfield {
.viewfieldgroup path.viewfield,
.sidedgroup path.sided {
stroke-width: 6px;
}

Expand Down
15 changes: 15 additions & 0 deletions modules/osm/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,18 @@ export var osmPavedTags = {
'grade1': true
}
};

export var osmRightSideIsInsideTags = {
'natural': {
'cliff': true,
},
'barrier': {
'retaining_wall': true,
'kerb': true,
'guard_rail': true,
'city_wall': true,
},
'man_made': {
'embankment': true
}
};
20 changes: 19 additions & 1 deletion modules/osm/way.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { geoArea as d3_geoArea } from 'd3-geo';
import { geoExtent, geoVecCross } from '../geo';
import { osmEntity } from './entity';
import { osmLanes } from './lanes';
import { osmOneWayTags } from './tags';
import { osmOneWayTags, osmRightSideIsInsideTags } from './tags';
import { areaKeys } from '../core/context';


Expand Down Expand Up @@ -129,6 +129,24 @@ _extend(osmWay.prototype, {
return false;
},

// The name of the tag that implies that this way is "sided",
// i.e. the right side is the 'inside' (e.g. the right side of a
// natural=cliff is lower).
tagImplyingSided: function() {
for (var key in this.tags) {
if (key in osmRightSideIsInsideTags && (this.tags[key] in osmRightSideIsInsideTags[key]))
return key;
}

return null;
},
isSided: function() {
if (this.tags.two_sided === 'yes') {
return false;
}

return this.tagImplyingSided() != null;
},

lanes: function() {
return osmLanes(this);
Expand Down
28 changes: 28 additions & 0 deletions modules/svg/defs.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ export function svgDefs(context) {
.attr('fill', '#000')
.attr('opacity', '0.75');

// SVG markers have to be given a colour where they're defined
// (they can't inherit it from the line they're attached to),
// so we need to manually define markers for each color of tag
// (also, it's slightly nicer if we can control the
// positioning for different tags)
function addSidedMarker(name, color, offset) {
defs
.append('marker')
.attr('id', 'sided-marker-' + name)
.attr('viewBox', '0 0 2 2')
.attr('refX', 1)
.attr('refY', -offset)
.attr('markerWidth', 1.5)
.attr('markerHeight', 1.5)
.attr('markerUnits', 'strokeWidth')
.attr('orient', 'auto')
.append('path')
.attr('class', 'sided-marker-path sided-marker-' + name + '-path')
.attr('d', 'M 0,0 L 1,2 L 2,0 z')
.attr('stroke', 'none')
.attr('fill', color);
}
addSidedMarker('natural', 'rgb(140, 208, 95)', 0);
// barriers have a dashed line, and separating the triangle
// from the line visually suits that
addSidedMarker('barrier', '#ddd', 1);
addSidedMarker('man_made', '#fff', 0);

defs
.append('marker')
.attr('id', 'viewfield-marker')
Expand Down
10 changes: 5 additions & 5 deletions modules/svg/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export function svgPassiveVertex(node, graph, activeID) {
}


export function svgOneWaySegments(projection, graph, dt) {
export function svgMarkerSegments(projection, graph, dt,
shouldReverse,
bothDirections) {
return function(entity) {
var i = 0;
var offset = dt;
Expand All @@ -72,12 +74,10 @@ export function svgOneWaySegments(projection, graph, dt) {
var coordinates = graph.childNodes(entity).map(function(n) { return n.loc; });
var a, b;

if (entity.tags.oneway === '-1') {
if (shouldReverse(entity)) {
coordinates.reverse();
}

var isReversible = (entity.tags.oneway === 'reversible' || entity.tags.oneway === 'alternating');

d3_geoStream({
type: 'LineString',
coordinates: coordinates
Expand Down Expand Up @@ -116,7 +116,7 @@ export function svgOneWaySegments(projection, graph, dt) {
}
segments.push({ id: entity.id, index: i++, d: segment });

if (isReversible) {
if (bothDirections(entity)) {
segment = '';
for (j = coord.length - 1; j >= 0; j--) {
segment += (j === coord.length - 1 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];
Expand Down
2 changes: 1 addition & 1 deletion modules/svg/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export { svgMapillaryImages } from './mapillary_images.js';
export { svgMapillarySigns } from './mapillary_signs.js';
export { svgMidpoints } from './midpoints.js';
export { svgNotes } from './notes.js';
export { svgOneWaySegments } from './helpers.js';
export { svgMarkerSegments } from './helpers.js';
export { svgOpenstreetcamImages } from './openstreetcam_images.js';
export { svgOsm } from './osm.js';
export { svgPassiveVertex } from './helpers.js';
Expand Down
90 changes: 56 additions & 34 deletions modules/svg/lines.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import _map from 'lodash-es/map';
import { range as d3_range } from 'd3-array';

import {
svgOneWaySegments,
svgMarkerSegments,
svgPath,
svgRelationMemberTags,
svgSegmentWay,
Expand Down Expand Up @@ -148,11 +148,45 @@ export function svgLines(projection, context) {
};
}

function addMarkers(layergroup, pathclass, groupclass, groupdata, marker) {
var markergroup = layergroup
.selectAll('g.' + groupclass)
.data([pathclass]);

markergroup = markergroup.enter()
.append('g')
.attr('class', groupclass)
.merge(markergroup);

var markers = markergroup
.selectAll('path')
.filter(filter)
.data(
function data() { return groupdata[this.parentNode.__data__] || []; },
function key(d) { return [d.id, d.index]; }
);

markers.exit()
.remove();

markers = markers.enter()
.append('path')
.attr('class', pathclass)
.attr('marker-mid', marker)
.merge(markers)
.attr('d', function(d) { return d.d; });

if (detected.ie) {
markers.each(function() { this.parentNode.insertBefore(this, this); });
}
}


var getPath = svgPath(projection, graph);
var ways = [];
var pathdata = {};
var onewaydata = {};
var sideddata = {};
var oldMultiPolygonOuters = {};

for (var i = 0; i < entities.length; i++) {
Expand All @@ -170,8 +204,21 @@ export function svgLines(projection, context) {
pathdata = _groupBy(ways, function(way) { return way.layer(); });

_forOwn(pathdata, function(v, k) {
var arr = _filter(v, function(d) { return d.isOneWay(); });
onewaydata[k] = _flatten(_map(arr, svgOneWaySegments(projection, graph, 35)));
var onewayArr = _filter(v, function(d) { return d.isOneWay(); });
var onewaySegments = svgMarkerSegments(
projection, graph, 35,
function shouldReverse(entity) { return entity.tags.oneway === '-1'; },
function bothDirections(entity) {
return entity.tags.oneway === 'reversible' || entity.tags.oneway === 'alternating';
});
onewaydata[k] = _flatten(_map(onewayArr, onewaySegments));

var sidedArr = _filter(v, function(d) { return d.isSided(); });
var sidedSegments = svgMarkerSegments(
projection, graph, 30,
function shouldReverse() { return false; },
function bothDirections() { return false; });
sideddata[k] = _flatten(_map(sidedArr, sidedSegments));
});


Expand Down Expand Up @@ -212,37 +259,12 @@ export function svgLines(projection, context) {
layergroup.selectAll('g.line-stroke-highlighted')
.call(drawLineGroup, 'stroke', true);


var onewaygroup = layergroup
.selectAll('g.onewaygroup')
.data(['oneway']);

onewaygroup = onewaygroup.enter()
.append('g')
.attr('class', 'onewaygroup')
.merge(onewaygroup);

var oneways = onewaygroup
.selectAll('path')
.filter(filter)
.data(
function data() { return onewaydata[this.parentNode.__data__] || []; },
function key(d) { return [d.id, d.index]; }
);

oneways.exit()
.remove();

oneways = oneways.enter()
.append('path')
.attr('class', 'oneway')
.attr('marker-mid', 'url(#oneway-marker)')
.merge(oneways)
.attr('d', function(d) { return d.d; });

if (detected.ie) {
oneways.each(function() { this.parentNode.insertBefore(this, this); });
}
addMarkers(layergroup, 'oneway', 'onewaygroup', onewaydata, 'url(#oneway-marker)');
addMarkers(layergroup, 'sided', 'sidedgroup', sideddata,
function marker(d) {
var category = graph.entity(d.id).tagImplyingSided();
return 'url(#sided-marker-' + category + ')';
});
});

// Draw touch targets..
Expand Down
61 changes: 61 additions & 0 deletions test/spec/osm/way.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,67 @@ describe('iD.osmWay', function() {
});
});

describe('#tagImplyingSided', function() {
it('returns tag when the tag has implied sidedness', function() {
expect(iD.Way({tags: { natural: 'cliff' }}).tagImplyingSided()).to.eql('natural');
expect(iD.Way({tags: { barrier: 'retaining_wall' }}).tagImplyingSided()).to.eql('barrier');
expect(iD.Way({tags: { barrier: 'kerb' }}).tagImplyingSided()).to.eql('barrier');
expect(iD.Way({tags: { barrier: 'guard_rail' }}).tagImplyingSided()).to.eql('barrier');
expect(iD.Way({tags: { barrier: 'city_wall' }}).tagImplyingSided()).to.eql('barrier');
expect(iD.Way({tags: { man_made: 'embankment' }}).tagImplyingSided()).to.eql('man_made');
});

it('returns null when tag does not have implied sidedness', function() {
expect(iD.Way({tags: { natural: 'ridge' }}).tagImplyingSided()).to.be.null;
expect(iD.Way({tags: { barrier: 'fence' }}).tagImplyingSided()).to.be.null;
expect(iD.Way({tags: { man_made: 'dyke' }}).tagImplyingSided()).to.be.null;
expect(iD.Way({tags: { highway: 'motorway' }}).tagImplyingSided()).to.be.null;
});
});
describe('#isSided', function() {
it('returns false when the way has no tags', function() {
expect(iD.Way().isSided()).to.be.false;
});

it('returns false when the way has two_sided=yes', function() {
expect(iD.Way({tags: { two_sided: 'yes' }}).isSided()).to.be.false;
});

it('returns true when the tag has implied sidedness', function() {
expect(iD.Way({tags: { natural: 'cliff' }}).isSided()).to.be.true;
expect(iD.Way({tags: { barrier: 'retaining_wall' }}).isSided()).to.be.true;
expect(iD.Way({tags: { barrier: 'kerb' }}).isSided()).to.be.true;
expect(iD.Way({tags: { barrier: 'guard_rail' }}).isSided()).to.be.true;
expect(iD.Way({tags: { barrier: 'city_wall' }}).isSided()).to.be.true;
expect(iD.Way({tags: { man_made: 'embankment' }}).isSided()).to.be.true;
});

it('returns false when two_sided=yes overrides tag with implied sidedness', function() {
expect(iD.Way({tags: { natural: 'cliff', two_sided: 'yes' }}).isSided()).to.be.false;
expect(iD.Way({tags: { barrier: 'retaining_wall', two_sided: 'yes' }}).isSided()).to.be.false;
expect(iD.Way({tags: { barrier: 'kerb', two_sided: 'yes' }}).isSided()).to.be.false;
expect(iD.Way({tags: { barrier: 'guard_rail', two_sided: 'yes' }}).isSided()).to.be.false;
expect(iD.Way({tags: { barrier: 'city_wall', two_sided: 'yes' }}).isSided()).to.be.false;
expect(iD.Way({tags: { man_made: 'embankment', two_sided: 'yes' }}).isSided()).to.be.false;
});

it('returns true when two_sided=no is on tag with implied sidedness', function() {
expect(iD.Way({tags: { natural: 'cliff', two_sided: 'no' }}).isSided()).to.be.true;
expect(iD.Way({tags: { barrier: 'retaining_wall', two_sided: 'no' }}).isSided()).to.be.true;
expect(iD.Way({tags: { barrier: 'kerb', two_sided: 'no' }}).isSided()).to.be.true;
expect(iD.Way({tags: { barrier: 'guard_rail', two_sided: 'no' }}).isSided()).to.be.true;
expect(iD.Way({tags: { barrier: 'city_wall', two_sided: 'no' }}).isSided()).to.be.true;
expect(iD.Way({tags: { man_made: 'embankment', two_sided: 'no' }}).isSided()).to.be.true;
});

it('returns false when the tag does not have implied sidedness', function() {
expect(iD.Way({tags: { natural: 'ridge' }}).isSided()).to.be.false;
expect(iD.Way({tags: { barrier: 'fence' }}).isSided()).to.be.false;
expect(iD.Way({tags: { man_made: 'dyke' }}).isSided()).to.be.false;
expect(iD.Way({tags: { highway: 'motorway' }}).isSided()).to.be.false;
});
});

describe('#isArea', function() {
before(function() {
iD.Context();
Expand Down
34 changes: 34 additions & 0 deletions test/spec/svg/lines.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,38 @@ describe('iD.svgLines', function () {
expect(selection.empty()).to.be.true;
});
});

describe('sided-markers', function() {
it('has marker layer for sided way', function() {
var a = iD.osmNode({id: 'a', loc: [0, 0]});
var b = iD.osmNode({id: 'b', loc: [1e-2, 0]});
var c = iD.osmNode({id: 'c', loc: [0, 1e-2]});

var i_n = iD.osmWay({id: 'implied-natural', tags: {natural: 'cliff'}, nodes: [a.id, b.id]});
var i_b = iD.osmWay({id: 'implied-barrier', tags: {barrier: 'city_wall'}, nodes: [a.id, c.id]});
var i_mm = iD.osmWay({id: 'implied-man_made', tags: {man_made: 'embankment'}, nodes: [b.id, c.id]});

var graph = iD.coreGraph([a, b, c, i_n, i_b, i_mm]);

surface.call(iD.svgLines(projection, context), graph, [i_n, i_b, i_mm], all);
var selection = surface.selectAll('g.sidedgroup > path');
expect(selection.size()).to.eql(3);
expect(selection.nodes()[0].attributes['marker-mid'].nodeValue).to.eql('url(#sided-marker-natural)');
expect(selection.nodes()[1].attributes['marker-mid'].nodeValue).to.eql('url(#sided-marker-barrier)');
expect(selection.nodes()[2].attributes['marker-mid'].nodeValue).to.eql('url(#sided-marker-man_made)');
});

it('has no marker layer for two_sided way', function() {
var a = iD.osmNode({id: 'a', loc: [0, 0]});
var b = iD.osmNode({id: 'b', loc: [1e-2, 0]});

var e_ts = iD.osmWay({id: 'explicit-two-sided', tags: {barrier: 'city_wall', two_sided: 'yes'}, nodes: [a.id, b.id]});

var graph = iD.coreGraph([a, b, e_ts]);

surface.call(iD.svgLines(projection, context), graph, [e_ts], all);
var selection = surface.selectAll('g.sidedgroup > path');
expect(selection.empty()).to.be.true;
});
});
});

0 comments on commit caff3ae

Please sign in to comment.