diff --git a/css/map.css b/css/map.css index dcad6d841..5a7bc4e78 100644 --- a/css/map.css +++ b/css/map.css @@ -774,42 +774,82 @@ path.casing.tag-boundary-national_park { /* bridges */ path.casing.tag-bridge { - stroke-width: 14; - stroke-opacity: 0.5; + stroke-width: 16; + stroke-opacity: 0.6; stroke: #000; + stroke-linecap: butt; } +path.shadow.tag-bridge { + stroke-width: 22; +} + +path.casing.line.tag-railway.tag-bridge, path.casing.tag-highway-living_street.tag-bridge, -path.casing.tag-highway-path.tag-bridge { - stroke-width: 6; -} - -path.casing.line.tag-highway-pedestrian, +path.casing.tag-highway-path.tag-bridge, +path.casing.line.tag-highway-pedestrian.tag-bridge, path.casing.tag-highway-service.tag-bridge, path.casing.tag-highway-track.tag-bridge, path.casing.tag-highway-steps.tag-bridge, path.casing.tag-highway-footway.tag-bridge, path.casing.tag-highway-cycleway.tag-bridge, path.casing.tag-highway-bridleway.tag-bridge { - stroke-width: 8; -} - -path.shadow.tag-highway-residential.tag-bridge { - stroke-width:22; + stroke-width: 10; } +path.shadow.line.tag-railway.tag-bridge, path.shadow.tag-highway-living_street.tag-bridge, path.shadow.tag-highway-path.tag-bridge, -path.shadow.line.tag-highway-pedestrian, +path.shadow.line.tag-highway-pedestrian.tag-bridge, path.shadow.tag-highway-service.tag-bridge, path.shadow.tag-highway-track.tag-bridge, path.shadow.tag-highway-steps.tag-bridge, path.shadow.tag-highway-footway.tag-bridge, path.shadow.tag-highway-cycleway.tag-bridge, path.shadow.tag-highway-bridleway.tag-bridge { - stroke-width: 16; + stroke-width: 17; } + +.low-zoom path.casing.tag-bridge { + stroke-width: 10; + stroke-opacity: 0.6; + stroke: #000; + stroke-linecap: butt; +} + +.low-zoom path.shadow.tag-bridge { + stroke-width: 14; +} + +.low-zoom path.casing.line.tag-railway.tag-bridge, +.low-zoom path.casing.tag-highway-living_street.tag-bridge, +.low-zoom path.casing.tag-highway-path.tag-bridge, +.low-zoom path.casing.line.tag-highway-pedestrian.tag-bridge, +.low-zoom path.casing.tag-highway-service.tag-bridge, +.low-zoom path.casing.tag-highway-track.tag-bridge, +.low-zoom path.casing.tag-highway-steps.tag-bridge, +.low-zoom path.casing.tag-highway-footway.tag-bridge, +.low-zoom path.casing.tag-highway-cycleway.tag-bridge, +.low-zoom path.casing.tag-highway-bridleway.tag-bridge { + stroke-width: 6; +} + +.low-zoom path.shadow.line.tag-railway.tag-bridge, +.low-zoom path.shadow.tag-highway-living_street.tag-bridge, +.low-zoom path.shadow.tag-highway-path.tag-bridge, +.low-zoom path.shadow.line.tag-highway-pedestrian.tag-bridge, +.low-zoom path.shadow.tag-highway-service.tag-bridge, +.low-zoom path.shadow.tag-highway-track.tag-bridge, +.low-zoom path.shadow.tag-highway-steps.tag-bridge, +.low-zoom path.shadow.tag-highway-footway.tag-bridge, +.low-zoom path.shadow.tag-highway-cycleway.tag-bridge, +.low-zoom path.shadow.tag-highway-bridleway.tag-bridge { + stroke-width: 13; +} + + + /* tunnels */ path.stroke.tag-tunnel { diff --git a/js/id/core/way.js b/js/id/core/way.js index 3a6e6d6ec..53cb717fd 100644 --- a/js/id/core/way.js +++ b/js/id/core/way.js @@ -42,6 +42,29 @@ _.extend(iD.Way.prototype, { if (this.nodes[this.nodes.length - 1] === node) return 'suffix'; }, + layer: function() { + // explicit layer tag, clamp between -10, 10.. + if (this.tags.layer !== undefined) { + return Math.max(-10, Math.min(+(this.tags.layer), 10)); + } + + // implied layer tag.. + if (this.tags.location === 'overground') return 1; + if (this.tags.location === 'underground') return -1; + if (this.tags.location === 'underwater') return -10; + + if (this.tags.power === 'line') return 10; + if (this.tags.power === 'minor_line') return 10; + if (this.tags.aerialway) return 10; + if (this.tags.bridge) return 1; + if (this.tags.cutting) return -1; + if (this.tags.tunnel) return -1; + if (this.tags.waterway) return -1; + if (this.tags.man_made === 'pipeline') return -10; + if (this.tags.boundary) return -10; + return 0; + }, + isOneWay: function() { // explicit oneway tag.. if (['yes', '1', '-1'].indexOf(this.tags.oneway) !== -1) { return true; } diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js index 972ebc403..d2ed972d4 100644 --- a/js/id/svg/areas.js +++ b/js/id/svg/areas.js @@ -64,8 +64,17 @@ iD.svg.Areas = function(projection) { fill: areas }; - var paths = surface.selectAll('.layer-shadow, .layer-stroke, .layer-fill') - .selectAll('path.area') + var areagroup = surface + .select('.layer-areas') + .selectAll('g.areagroup') + .data(['fill', 'shadow', 'stroke']); + + areagroup.enter() + .append('g') + .attr('class', function(d) { return 'layer areagroup area-' + d; }); + + var paths = areagroup + .selectAll('path') .filter(filter) .data(function(layer) { return data[layer]; }, iD.Entity.key); @@ -74,7 +83,7 @@ iD.svg.Areas = function(projection) { paths.exit() .remove(); - var fills = surface.selectAll('.layer-fill path.area')[0]; + var fills = surface.selectAll('.area-fill path.area')[0]; var bisect = d3.bisector(function(node) { return -node.__data__.area(graph); diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 61b0efbd5..30a6aaf7b 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -16,80 +16,98 @@ iD.svg.Lines = function(projection) { }; function waystack(a, b) { - if (!a || !b || !a.tags || !b.tags) return 0; - if (a.tags.layer !== undefined && b.tags.layer !== undefined) { - return a.tags.layer - b.tags.layer; - } - if (a.tags.bridge) return 1; - if (b.tags.bridge) return -1; - if (a.tags.tunnel) return -1; - if (b.tags.tunnel) return 1; var as = 0, bs = 0; - if (a.tags.highway && b.tags.highway) { - as -= highway_stack[a.tags.highway]; - bs -= highway_stack[b.tags.highway]; - } + + if (a.tags.highway) { as -= highway_stack[a.tags.highway]; } + if (b.tags.highway) { bs -= highway_stack[b.tags.highway]; } return as - bs; } return function drawLines(surface, graph, entities, filter) { - var lines = [], - path = iD.svg.Path(projection, graph); + var ways = [], pathdata = {}, onewaydata = {}, + getPath = iD.svg.Path(projection, graph); for (var i = 0; i < entities.length; i++) { var entity = entities[i], outer = iD.geo.simpleMultipolygonOuterMember(entity, graph); if (outer) { - lines.push(entity.mergeTags(outer.tags)); + ways.push(entity.mergeTags(outer.tags)); } else if (entity.geometry(graph) === 'line') { - lines.push(entity); + ways.push(entity); } } - lines = lines.filter(path); - lines.sort(waystack); + ways = ways.filter(getPath); - function drawPaths(klass) { - var paths = surface.select('.layer-' + klass) - .selectAll('path.line') - .filter(filter) - .data(lines, iD.Entity.key); + pathdata = _.groupBy(ways, function(way) { return way.layer(); }); - var enter = paths.enter() - .append('path') - .attr('class', function(d) { return 'way line ' + klass + ' ' + d.id; }); + _.forOwn(pathdata, function(v, k) { + onewaydata[k] = _(v) + .filter(function(d) { return d.isOneWay(); }) + .map(iD.svg.OneWaySegments(projection, graph, 35)) + .flatten() + .valueOf(); + }); - // Optimization: call simple TagClasses only on enter selection. This - // works because iD.Entity.key is defined to include the entity v attribute. - if (klass !== 'stroke') { - enter.call(iD.svg.TagClasses()); - } else { - paths.call(iD.svg.TagClasses() - .tags(iD.svg.MultipolygonMemberTags(graph))); - } + var layergroup = surface + .select('.layer-lines') + .selectAll('g.layergroup') + .data(d3.range(-10, 11)); - paths - .order() - .attr('d', path); + layergroup.enter() + .append('g') + .attr('class', function(d) { return 'layer layergroup layer' + String(d); }); - paths.exit() - .remove(); - } - drawPaths('shadow'); - drawPaths('casing'); - drawPaths('stroke'); + var linegroup = layergroup + .selectAll('g.linegroup') + .data(['shadow', 'casing', 'stroke']); - var segments = _(lines) - .filter(function(d) { return d.isOneWay(); }) - .map(iD.svg.OneWaySegments(projection, graph, 35)) - .flatten() - .valueOf(); + linegroup.enter() + .append('g') + .attr('class', function(d) { return 'layer linegroup line-' + d; }); - var oneways = surface.select('.layer-oneway') - .selectAll('path.oneway') + + var lines = linegroup + .selectAll('path') .filter(filter) - .data(segments, function(d) { return [d.id, d.index]; }); + .data( + function() { return pathdata[this.parentNode.parentNode.__data__] || []; }, + iD.Entity.key + ); + + // Optimization: call simple TagClasses only on enter selection. This + // works because iD.Entity.key is defined to include the entity v attribute. + lines.enter() + .append('path') + .attr('class', function(d) { return 'way line ' + this.parentNode.__data__ + ' ' + d.id; }) + .call(iD.svg.TagClasses()); + + lines + .sort(waystack) + .attr('d', getPath) + .call(iD.svg.TagClasses().tags(iD.svg.MultipolygonMemberTags(graph))); + + lines.exit() + .remove(); + + + var onewaygroup = layergroup + .selectAll('g.onewaygroup') + .data(['oneway']); + + onewaygroup.enter() + .append('g') + .attr('class', 'layer onewaygroup'); + + + var oneways = onewaygroup + .selectAll('path') + .filter(filter) + .data( + function() { return onewaydata[this.parentNode.parentNode.__data__] || []; }, + function(d) { return [d.id, d.index]; } + ); oneways.enter() .append('path') @@ -97,10 +115,10 @@ iD.svg.Lines = function(projection) { .attr('marker-mid', 'url(#oneway-marker)'); oneways - .order() .attr('d', function(d) { return d.d; }); oneways.exit() .remove(); + }; }; diff --git a/js/id/svg/surface.js b/js/id/svg/surface.js index 3b11461dd..c489ea0df 100644 --- a/js/id/svg/surface.js +++ b/js/id/svg/surface.js @@ -1,7 +1,7 @@ iD.svg.Surface = function() { return function (selection) { var layers = selection.selectAll('.layer') - .data(['fill', 'shadow', 'casing', 'stroke', 'oneway', 'hit', 'halo', 'label']); + .data(['areas', 'lines', 'hit', 'halo', 'label']); layers.enter().append('g') .attr('class', function(d) { return 'layer layer-' + d; }); diff --git a/test/spec/core/way.js b/test/spec/core/way.js index 5ff68c431..799b6a224 100644 --- a/test/spec/core/way.js +++ b/test/spec/core/way.js @@ -148,6 +148,68 @@ describe('iD.Way', function() { }); }); + describe('#layer', function() { + it('returns 0 when the way has no tags', function() { + expect(iD.Way().layer()).to.equal(0); + }); + + it('returns the layer when the way has an explicit layer tag', function() { + expect(iD.Way({tags: { layer: '2' }}).layer()).to.equal(2); + expect(iD.Way({tags: { layer: '-5' }}).layer()).to.equal(-5); + }); + + it('clamps the layer to within -10, 10', function() { + expect(iD.Way({tags: { layer: '12' }}).layer()).to.equal(10); + expect(iD.Way({tags: { layer: '-15' }}).layer()).to.equal(-10); + }); + + it('returns 1 for location=overground', function() { + expect(iD.Way({tags: { location: 'overground' }}).layer()).to.equal(1); + }); + + it('returns -1 for location=underground', function() { + expect(iD.Way({tags: { location: 'underground' }}).layer()).to.equal(-1); + }); + + it('returns -10 for location=underwater', function() { + expect(iD.Way({tags: { location: 'underwater' }}).layer()).to.equal(-10); + }); + + it('returns 10 for power lines', function() { + expect(iD.Way({tags: { power: 'line' }}).layer()).to.equal(10); + expect(iD.Way({tags: { power: 'minor_line' }}).layer()).to.equal(10); + }); + + it('returns 10 for aerialways', function() { + expect(iD.Way({tags: { aerialway: 'cable_car' }}).layer()).to.equal(10); + }); + + it('returns 1 for bridges', function() { + expect(iD.Way({tags: { bridge: 'yes' }}).layer()).to.equal(1); + }); + + it('returns -1 for cuttings', function() { + expect(iD.Way({tags: { cutting: 'yes' }}).layer()).to.equal(-1); + }); + + it('returns -1 for tunnels', function() { + expect(iD.Way({tags: { tunnel: 'yes' }}).layer()).to.equal(-1); + }); + + it('returns -1 for waterways', function() { + expect(iD.Way({tags: { waterway: 'stream' }}).layer()).to.equal(-1); + }); + + it('returns -10 for pipelines', function() { + expect(iD.Way({tags: { man_made: 'pipeline' }}).layer()).to.equal(-10); + }); + + it('returns -10 for boundaries', function() { + expect(iD.Way({tags: { boundary: 'administrative' }}).layer()).to.equal(-10); + }); + + }); + describe('#isOneWay', function() { it('returns false when the way has no tags', function() { expect(iD.Way().isOneWay()).to.be.false; diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index b0d95c59f..fcbcb1792 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -40,19 +40,6 @@ describe("iD.svg.Areas", function () { expect(surface.select('.area')).to.be.classed('tag-building-yes'); }); - it("preserves non-area paths", function () { - var area = iD.Way({tags: {area: 'yes'}}), - graph = iD.Graph([area]); - - surface.select('.layer-fill') - .append('path') - .attr('class', 'other'); - - surface.call(iD.svg.Areas(projection), graph, [area], none); - - expect(surface.selectAll('.other')[0].length).to.equal(1); - }); - it("handles deletion of a way and a member vertex (#1903)", function () { var graph = iD.Graph([ iD.Node({id: 'a', loc: [0, 0]}), diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index 21ba6981d..b94575a60 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -2,20 +2,21 @@ describe("iD.svg.Lines", function () { var surface, projection = d3.geo.projection(function(x, y) { return [x, y]; }) .clipExtent([[0, 0], [Infinity, Infinity]]), - filter = d3.functor(true); + all = d3.functor(true), + none = d3.functor(false); beforeEach(function () { surface = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg')) .call(iD.svg.Surface(iD())); }); - it("adds way and area classes", function () { + it("adds way and line classes", function () { var a = iD.Node({loc: [0, 0]}), b = iD.Node({loc: [1, 1]}), line = iD.Way({nodes: [a.id, b.id]}), graph = iD.Graph([a, b, line]); - surface.call(iD.svg.Lines(projection), graph, [line], filter); + surface.call(iD.svg.Lines(projection), graph, [line], all); expect(surface.select('path.way')).to.be.classed('way'); expect(surface.select('path.line')).to.be.classed('line'); @@ -27,7 +28,7 @@ describe("iD.svg.Lines", function () { line = iD.Way({nodes: [a.id, b.id], tags: {highway: 'residential'}}), graph = iD.Graph([a, b, line]); - surface.call(iD.svg.Lines(projection), graph, [line], filter); + surface.call(iD.svg.Lines(projection), graph, [line], all); expect(surface.select('.line')).to.be.classed('tag-highway'); expect(surface.select('.line')).to.be.classed('tag-highway-residential'); @@ -40,7 +41,7 @@ describe("iD.svg.Lines", function () { relation = iD.Relation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}), graph = iD.Graph([a, b, line, relation]); - surface.call(iD.svg.Lines(projection), graph, [line], filter); + surface.call(iD.svg.Lines(projection), graph, [line], all); expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); }); @@ -53,7 +54,7 @@ describe("iD.svg.Lines", function () { r = iD.Relation({members: [{id: w.id}], tags: {type: 'multipolygon'}}), graph = iD.Graph([a, b, c, w, r]); - surface.call(iD.svg.Lines(projection), graph, [w], filter); + surface.call(iD.svg.Lines(projection), graph, [w], all); expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); }); @@ -67,21 +68,54 @@ describe("iD.svg.Lines", function () { r = iD.Relation({members: [{id: o.id, role: 'outer'}, {id: i.id, role: 'inner'}], tags: {type: 'multipolygon'}}), graph = iD.Graph([a, b, c, o, i, r]); - surface.call(iD.svg.Lines(projection), graph, [i], filter); + surface.call(iD.svg.Lines(projection), graph, [i], all); expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); }); - it("preserves non-line paths", function () { - var line = iD.Way(), - graph = iD.Graph([line]); + describe("z-indexing", function() { + var graph = iD.Graph([ + iD.Node({id: 'a', loc: [0, 0]}), + iD.Node({id: 'b', loc: [1, 1]}), + iD.Node({id: 'c', loc: [0, 0]}), + iD.Node({id: 'd', loc: [1, 1]}), + iD.Way({id: 'lo', tags: {highway: 'residential', tunnel: 'yes'}, nodes: ['a', 'b']}), + iD.Way({id: 'hi', tags: {highway: 'residential', bridge: 'yes'}, nodes: ['c', 'd']}) + ]); - surface.select('.layer-fill') - .append('path') - .attr('class', 'other'); + it("stacks higher lines above lower ones in a single render", function () { + surface.call(iD.svg.Lines(projection), graph, [graph.entity('lo'), graph.entity('hi')], none); - surface.call(iD.svg.Lines(projection), graph, [line], filter); + var selection = surface.selectAll('g.line-stroke > path.line'); + expect(selection[0][0].__data__.id).to.eql('lo'); + expect(selection[0][1].__data__.id).to.eql('hi'); + }); - expect(surface.selectAll('.other')[0].length).to.equal(1); + it("stacks higher lines above lower ones in a single render (reverse)", function () { + surface.call(iD.svg.Lines(projection), graph, [graph.entity('hi'), graph.entity('lo')], none); + + var selection = surface.selectAll('g.line-stroke > path.line'); + expect(selection[0][0].__data__.id).to.eql('lo'); + expect(selection[0][1].__data__.id).to.eql('hi'); + }); + + it("stacks higher lines above lower ones in separate renders", function () { + surface.call(iD.svg.Lines(projection), graph, [graph.entity('lo')], none); + surface.call(iD.svg.Lines(projection), graph, [graph.entity('hi')], none); + + var selection = surface.selectAll('g.line-stroke > path.line'); + expect(selection[0][0].__data__.id).to.eql('lo'); + expect(selection[0][1].__data__.id).to.eql('hi'); + }); + + it("stacks higher lines above lower in separate renders (reverse)", function () { + surface.call(iD.svg.Lines(projection), graph, [graph.entity('hi')], none); + surface.call(iD.svg.Lines(projection), graph, [graph.entity('lo')], none); + + var selection = surface.selectAll('g.line-stroke > path.line'); + expect(selection[0][0].__data__.id).to.eql('lo'); + expect(selection[0][1].__data__.id).to.eql('hi'); + }); }); + });