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
- natural=coastline
- 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). The different tags get
different renderings (e.g. colors that match the main way, and
different spacings). In addition, natural=coastline is special-cased
to have blue markers (despite having a green way), to emphasise that
the "inside" of a coastline is the water.

Fixes openstreetmap#1475.
  • Loading branch information
huonw committed Nov 27, 2018
1 parent f356c6a commit dd0be84
Show file tree
Hide file tree
Showing 9 changed files with 243 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
16 changes: 16 additions & 0 deletions modules/osm/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,19 @@ export var osmPavedTags = {
'grade1': true
}
};

export var osmRightSideIsInsideTags = {
'natural': {
'cliff': true,
'coastline': 'coastline',
},
'barrier': {
'retaining_wall': true,
'kerb': true,
'guard_rail': true,
'city_wall': true,
},
'man_made': {
'embankment': true
}
};
30 changes: 29 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,34 @@ _extend(osmWay.prototype, {
return false;
},

// Some identifier for 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).
sidednessIdentifier: function() {
for (var key in this.tags) {
var value = this.tags[key];
if (key in osmRightSideIsInsideTags && (value in osmRightSideIsInsideTags[key])) {
if (osmRightSideIsInsideTags[key][value] === true) {
return key;
} else {
// if the map's value is something other than a
// literal true, we should use it so we can
// special case some keys (e.g. natural=coastline
// is handled differently to other naturals).
return osmRightSideIsInsideTags[key][value];
}
}
}

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

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

lanes: function() {
return osmLanes(this);
Expand Down
32 changes: 32 additions & 0 deletions modules/svg/defs.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,38 @@ 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);
// for a coastline, the arrows are (somewhat unintuitively) on
// the water side, so let's color them blue (with a gap) to
// give a stronger indication
addSidedMarker('coastline', '#77dede', 1);
// 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).sidednessIdentifier();
return 'url(#sided-marker-' + category + ')';
});
});

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

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

it('returns null when tag does not have implied sidedness', function() {
expect(iD.Way({tags: { natural: 'ridge' }}).sidednessIdentifier()).to.be.null;
expect(iD.Way({tags: { barrier: 'fence' }}).sidednessIdentifier()).to.be.null;
expect(iD.Way({tags: { man_made: 'dyke' }}).sidednessIdentifier()).to.be.null;
expect(iD.Way({tags: { highway: 'motorway' }}).sidednessIdentifier()).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: { natural: 'coastline' }}).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: { natural: 'coastline', 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: { natural: 'coastline', 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
37 changes: 37 additions & 0 deletions test/spec/svg/lines.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,41 @@ 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 d = iD.osmNode({id: 'd', loc: [1e-2, 1e-2]});

var i_n = iD.osmWay({id: 'implied-natural', tags: {natural: 'cliff'}, nodes: [a.id, b.id]});
var i_nc = iD.osmWay({id: 'implied-coastline', tags: {natural: 'coastline'}, nodes: [a.id, c.id]});
var i_b = iD.osmWay({id: 'implied-barrier', tags: {barrier: 'city_wall'}, nodes: [a.id, d.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, d, i_n, i_nc, i_b, i_mm]);

surface.call(iD.svgLines(projection, context), graph, [i_n, i_nc, i_b, i_mm], all);
var selection = surface.selectAll('g.sidedgroup > path');
expect(selection.size()).to.eql(4);
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-coastline)');
expect(selection.nodes()[2].attributes['marker-mid'].nodeValue).to.eql('url(#sided-marker-barrier)');
expect(selection.nodes()[3].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 dd0be84

Please sign in to comment.