Merge pull request #5529 from huonw/arrows-for-cliffs

Add one-sided triangular markers to ways with sides (e.g. natural=cliff).
This commit is contained in:
Bryan Housel
2018-11-27 11:15:48 -05:00
committed by GitHub
9 changed files with 299 additions and 42 deletions
+2 -1
View File
@@ -205,7 +205,8 @@ text {
}
.onewaygroup path.oneway,
.viewfieldgroup path.viewfield {
.viewfieldgroup path.viewfield,
.sidedgroup path.sided {
stroke-width: 6px;
}
+16
View File
@@ -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
}
};
+29 -1
View File
@@ -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';
@@ -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);
+32
View File
@@ -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')
+5 -5
View File
@@ -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;
@@ -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
@@ -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];
+1 -1
View File
@@ -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';
+56 -34
View File
@@ -7,7 +7,7 @@ import _map from 'lodash-es/map';
import { range as d3_range } from 'd3-array';
import {
svgOneWaySegments,
svgMarkerSegments,
svgPath,
svgRelationMemberTags,
svgSegmentWay,
@@ -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++) {
@@ -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));
});
@@ -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..
+65
View File
@@ -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();
+93
View File
@@ -129,4 +129,97 @@ describe('iD.svgLines', function () {
});
});
describe('oneway-markers', function() {
it('has marker layer for oneway ways', function() {
// use 1e-2 to make sure segments are long enough to get
// markers, but not so long that they get split and have
// multiple marker segments.
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_o = iD.osmWay({id: 'implied-oneway', tags: {waterway: 'stream'}, nodes: [a.id, b.id]});
var e_o = iD.osmWay({id: 'explicit-oneway', tags: {highway: 'residential', oneway: 'yes'}, nodes: [a.id, c.id]});
var e_b = iD.osmWay({id: 'explicit-backwards', tags: {highway: 'residential', oneway: '-1'}, nodes: [b.id, c.id]});
var graph = iD.coreGraph([a, b, c, i_o, e_o, e_b]);
surface.call(iD.svgLines(projection, context), graph, [i_o, e_o, e_b], all);
var selection = surface.selectAll('g.onewaygroup > path');
expect(selection.size()).to.eql(3);
expect(selection.nodes()[0].attributes['marker-mid'].nodeValue).to.eql('url(#oneway-marker)');
expect(selection.nodes()[1].attributes['marker-mid'].nodeValue).to.eql('url(#oneway-marker)');
expect(selection.nodes()[2].attributes['marker-mid'].nodeValue).to.eql('url(#oneway-marker)');
});
it('has two marker layers for alternating oneway ways', function() {
var a = iD.osmNode({id: 'a', loc: [0, 0]});
var b = iD.osmNode({id: 'b', loc: [1e-2, 0]});
var e_a = iD.osmWay({id: 'explicit-alternating', tags: {highway: 'residential', oneway: 'alternating'}, nodes: [a.id, b.id]});
var graph = iD.coreGraph([a, b, e_a]);
surface.call(iD.svgLines(projection, context), graph, [e_a], all);
var selection = surface.selectAll('g.onewaygroup > path');
expect(selection.size()).to.eql(2);
expect(selection.nodes()[0].attributes['marker-mid'].nodeValue).to.eql('url(#oneway-marker)');
expect(selection.nodes()[1].attributes['marker-mid'].nodeValue).to.eql('url(#oneway-marker)');
});
it('has no marker layer for oneway=no ways', 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 e_no = iD.osmWay({id: 'explicit-no-oneway', tags: {highway: 'residential', oneway: 'no'}, nodes: [a.id, b.id]});
var i_no = iD.osmWay({id: 'implied-no-oneway', tags: {highway: 'residential' }, nodes: [a.id, c.id]});
var graph = iD.coreGraph([a, b, c, e_no, i_no]);
surface.call(iD.svgLines(projection, context), graph, [i_no, e_no], all);
var selection = surface.selectAll('g.onewaygroup > path');
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;
});
});
});