diff --git a/CHANGELOG.md b/CHANGELOG.md index 93d8e98a9..5a9533122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :sparkles: Usability & Accessibility * Allow searching for coordinates in localized number format in search box ([#10805]) +* Improve visibility of oneway arrows for dashed line styles (such as railway lines, foot paths, etc.): they are now rendered such that the arrows seemlessly integrate into the line dashes ([#10849]) #### :scissors: Operations * Fix unexpected behavior of squaring operation on individual vertices ([#10401]) #### :camera: Street-Level diff --git a/css/30_highways.css b/css/30_highways.css index 3a9ab7648..a997d63e7 100644 --- a/css/30_highways.css +++ b/css/30_highways.css @@ -405,9 +405,14 @@ path.line.stroke.tag-highway-pedestrian, path.line.stroke.tag-pedestrian { stroke: #fff; stroke-width: 3.5; - stroke-dasharray: 8, 8; + stroke-dasharray: 6, 6; stroke-linecap: butt; } +path.line.stroke.tag-highway-pedestrian.tag-oneway, +path.line.stroke.tag-pedestrian.tag-oneway { + stroke-dasharray: 6, 6, 6, 18; + stroke-dashoffset: 9; +} .low-zoom path.line.stroke.tag-highway-pedestrian, .low-zoom path.line.stroke.tag-pedestrian { stroke-width: 2; @@ -484,6 +489,13 @@ path.line.stroke.tag-highway-bridleway { stroke-linecap: butt; stroke-dasharray: 6, 6; } +path.line.stroke.tag-highway-path.tag-oneway, +path.line.stroke.tag-highway-footway.tag-oneway, +path.line.stroke.tag-highway-cycleway.tag-oneway, +path.line.stroke.tag-highway-bridleway.tag-oneway { + stroke-dasharray: 6, 6, 6, 18; + stroke-dashoffset: 9; +} .low-zoom path.line.stroke.tag-highway-path, .low-zoom path.line.stroke.tag-highway-footway, .low-zoom path.line.stroke.tag-highway-cycleway, @@ -633,6 +645,10 @@ path.line.stroke.tag-highway-ladder, path.line.stroke.tag-highway.tag-crossing-unmarked { stroke-dasharray: 6, 4; } +path.line.stroke.tag-highway.tag-crossing-unmarked.tag-oneway { + stroke-dasharray: 6, 4, 6, 20; + stroke-dashoffset: 8; +} .low-zoom path.line.stroke.tag-highway.tag-crossing-unmarked { stroke-dasharray: 3, 2; } diff --git a/css/35_aeroways.css b/css/35_aeroways.css index d301a6a6c..11c5d770e 100644 --- a/css/35_aeroways.css +++ b/css/35_aeroways.css @@ -60,6 +60,7 @@ path.line.stroke.tag-aeroway-runway { stroke-width: 2; stroke-linecap: butt; stroke-dasharray: 24, 48; + stroke-dashoffset: -7; } .low-zoom path.line.shadow.tag-aeroway-runway { stroke-width: 16; diff --git a/css/40_railways.css b/css/40_railways.css index 161266e5e..8f934ab16 100644 --- a/css/40_railways.css +++ b/css/40_railways.css @@ -20,7 +20,11 @@ path.line.casing.tag-railway { path.line.stroke.tag-railway { stroke-width: 2; stroke-linecap: butt; - stroke-dasharray: 12, 12; + stroke-dasharray: 10,8; +} +path.line.stroke.tag-railway.tag-oneway { + stroke-dasharray: 10, 26; + stroke-dashoffset: 5; } .low-zoom path.line.shadow.tag-railway { stroke-width: 12; diff --git a/modules/geo/raw_mercator.js b/modules/geo/raw_mercator.js index 26f98a04e..11543d610 100644 --- a/modules/geo/raw_mercator.js +++ b/modules/geo/raw_mercator.js @@ -15,19 +15,25 @@ import { * Resampling */ export function geoRawMercator() { - var project = d3_geoMercatorRaw; - var k = 512 / Math.PI; // scale - var x = 0; - var y = 0; // translate - var clipExtent = [[0, 0], [0, 0]]; - + const project = d3_geoMercatorRaw; + let k = 512 / Math.PI; // scale + let x = 0; + let y = 0; // translate + let clipExtent = [[0, 0], [0, 0]]; + /** + * @param {[number, number]} point + * @returns {[number, number]} + */ function projection(point) { point = project(point[0] * Math.PI / 180, point[1] * Math.PI / 180); return [point[0] * k + x, y - point[1] * k]; } - + /** + * @param {[number, number]} point + * @returns {[number, number]} + */ projection.invert = function(point) { point = project.invert((point[0] - x) / k, (y - point[1]) / k); return point && [point[0] * 180 / Math.PI, point[1] * 180 / Math.PI]; @@ -67,7 +73,7 @@ export function geoRawMercator() { projection.stream = d3_geoTransform({ point: function(x, y) { - var vec = projection([x, y]); + const vec = projection([x, y]); this.stream.point(vec[0], vec[1]); } }).stream; @@ -75,3 +81,6 @@ export function geoRawMercator() { return projection; } +/** + * @typedef {ReturnType} Projection + */ diff --git a/modules/globals.d.ts b/modules/globals.d.ts index cc1d6e642..bd90b618c 100644 --- a/modules/globals.d.ts +++ b/modules/globals.d.ts @@ -21,6 +21,8 @@ declare global { export type AbstractEntity = InstanceType; export type OsmEntity = OsmNode | OsmWay | OsmRelation; + + export type Projection = import('./geo/raw_mercator').Projection; } declare namespace d3 { diff --git a/modules/svg/defs.js b/modules/svg/defs.js index 340170887..b4ebe8275 100644 --- a/modules/svg/defs.js +++ b/modules/svg/defs.js @@ -28,12 +28,12 @@ export function svgDefs(context) { // positioning for different tags) /** @param {string} name @param {string} colour */ - function addOnewayMarker(name, colour) { + function addOnewayMarker(name, colour, opacity) { _defsSelection .append('marker') .attr('id', `ideditor-oneway-marker-${name}`) .attr('viewBox', '0 0 10 5') - .attr('refX', 2.5) + .attr('refX', 4) .attr('refY', 2.5) .attr('markerWidth', 2) .attr('markerHeight', 2) @@ -41,14 +41,14 @@ export function svgDefs(context) { .attr('orient', 'auto') .append('path') .attr('class', 'oneway-marker-path') - .attr('d', 'M 5,3 L 0,3 L 0,2 L 5,2 L 5,0 L 10,2.5 L 5,5 z') + .attr('d', 'M 6,3 L 0,3 L 0,2 L 6,2 L 5,0 L 10,2.5 L 5,5 z') .attr('stroke', 'none') .attr('fill', colour) - .attr('opacity', '0.75'); + .attr('opacity', '1'); } - addOnewayMarker('black', '#000'); // default + addOnewayMarker('black', '#333'); // default addOnewayMarker('white', '#fff'); // for dark lines (bridges under construction, railways, etc.) - addOnewayMarker('pink', '#eaf'); // for dark lines where white arrows don't work + addOnewayMarker('gray', '#eee'); // for railway lines function addSidedMarker(name, color, offset) { diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 8c946f6fd..60ab246d5 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -59,20 +59,31 @@ export function svgPassiveVertex(node, graph, activeID) { } -export function svgMarkerSegments(projection, graph, dt, - shouldReverse, - bothDirections) { +/** + * + * @param {iD.Projection} projection + * @param {iD.Graph} graph + * @param {Number} dt spacing between segments + * @param {Function} [shouldReverse] + * @param {Function} [bothDirections] + */ +export function svgMarkerSegments(projection, graph, dt, shouldReverse = () => false, bothDirections = () => false) { + /** + * @param {iD.OsmWay} entity + * @returns {[{id: String, d: String}]} list of svg path segments corres + */ return function(entity) { - var i = 0; - var offset = dt; - var segments = []; - var clip = d3_geoIdentity().clipExtent(projection.clipExtent()).stream; - var coordinates = graph.childNodes(entity).map(function(n) { return n.loc; }); - var a, b; + let i = 0; + let offset = dt / 2; + const segments = []; - if (shouldReverse(entity)) { - coordinates.reverse(); - } + const clip = paddedClipExtent(projection); + + const coordinates = graph.childNodes(entity).map(function(n) { return n.loc; }); + let a, b; + + const _shouldReverse = shouldReverse(entity); + const _bothDirections = bothDirections(entity); d3_geoStream({ type: 'LineString', @@ -84,19 +95,19 @@ export function svgMarkerSegments(projection, graph, dt, b = [x, y]; if (a) { - var span = geoVecLength(a, b) - offset; + let span = geoVecLength(a, b) - offset; if (span >= 0) { - var heading = geoVecAngle(a, b); - var dx = dt * Math.cos(heading); - var dy = dt * Math.sin(heading); - var p = [ + const heading = geoVecAngle(a, b); + const dx = dt * Math.cos(heading); + const dy = dt * Math.sin(heading); + let p = [ a[0] + offset * Math.cos(heading), a[1] + offset * Math.sin(heading) ]; // gather coordinates - var coord = [a, p]; + const coord = [a, p]; for (span -= dt; span >= 0; span -= dt) { p = geoVecAdd(p, [dx, dy]); coord.push(p); @@ -104,17 +115,18 @@ export function svgMarkerSegments(projection, graph, dt, coord.push(b); // generate svg paths - var segment = ''; - var j; + let segment = ''; - for (j = 0; j < coord.length; j++) { - segment += (j === 0 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1]; + if (!_shouldReverse || _bothDirections) { + for (let j = 0; j < coord.length; j++) { + segment += (j === 0 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1]; + } + segments.push({ id: entity.id, index: i++, d: segment }); } - segments.push({ id: entity.id, index: i++, d: segment }); - if (bothDirections(entity)) { + if (_shouldReverse || _bothDirections) { segment = ''; - for (j = coord.length - 1; j >= 0; j--) { + for (let j = coord.length - 1; j >= 0; j--) { segment += (j === coord.length - 1 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1]; } segments.push({ id: entity.id, index: i++, d: segment }); @@ -133,30 +145,19 @@ export function svgMarkerSegments(projection, graph, dt, } +/** + * @param {iD.Projection} projection + * @param {iD.Graph} graph + * @param {Boolean} isArea + */ export function svgPath(projection, graph, isArea) { - - // Explanation of magic numbers: - // "padding" here allows space for strokes to extend beyond the viewport, - // so that the stroke isn't drawn along the edge of the viewport when - // the shape is clipped. - // - // When drawing lines, pad viewport by 5px. - // When drawing areas, pad viewport by 65px in each direction to allow - // for 60px area fill stroke (see ".fill-partial path.fill" css rule) - - var cache = {}; - var padding = isArea ? 65 : 5; - var viewport = projection.clipExtent(); - var paddedExtent = [ - [viewport[0][0] - padding, viewport[0][1] - padding], - [viewport[1][0] + padding, viewport[1][1] + padding] - ]; - var clip = d3_geoIdentity().clipExtent(paddedExtent).stream; - var project = projection.stream; - var path = d3_geoPath() + const cache = {}; + const project = projection.stream; + const clip = paddedClipExtent(projection, isArea); + const path = d3_geoPath() .projection({stream: function(output) { return project(clip(output)); }}); - var svgpath = function(entity) { + const svgpath = function(entity) { if (entity.id in cache) { return cache[entity.id]; } else { @@ -283,3 +284,29 @@ export function svgSegmentWay(way, graph, activeID) { } } } + + +/** + * Returns a d3 projection stream that clips the given geometries to an + * extent that is slightly padded. + * + * Explanation of magic numbers: + * "padding" here allows space for strokes to extend beyond the viewport, + * so that the stroke isn't drawn along the edge of the viewport when + * the shape is clipped. + * When drawing lines, pad viewport by 5px. + * When drawing areas, pad viewport by 65px in each direction to allow + * for 60px area fill stroke (see ".fill-partial path.fill" css rule) + * + * @param {import('../geo/raw_mercator').Projection} projection + * @param {Boolean} isArea + */ +function paddedClipExtent(projection, isArea = false) { + var padding = isArea ? 65 : 5; + var viewport = projection.clipExtent(); + var paddedExtent = [ + [viewport[0][0] - padding, viewport[0][1] - padding], + [viewport[1][0] + padding, viewport[1][1] + padding] + ]; + return d3_geoIdentity().clipExtent(paddedExtent).stream; +} diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 80f54950f..c21c73f42 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -14,9 +14,9 @@ import { utilDetect } from '../util/detect'; function onewayArrowColour(tags) { // the return value must be defined in ./defs.js if (tags.highway === 'construction' && tags.bridge) return 'white'; - if (tags.highway === 'pedestrian' && tags.bridge) return 'pink'; - if (tags.railway) return 'black'; // TODO: use a better colour - if (tags.aeroway === 'runway') return 'pink'; + if (tags.highway === 'pedestrian') return 'gray'; + if (tags.railway) return 'gray'; + if (tags.aeroway === 'runway') return 'white'; return 'black'; } @@ -264,7 +264,7 @@ export function svgLines(projection, context) { var v = pathdata[k]; var onewayArr = v.filter(function(d) { return d.isOneWay(); }); var onewaySegments = svgMarkerSegments( - projection, graph, 35, + projection, graph, 36, entity => entity.isOneWayBackwards(), entity => entity.isBiDirectional(), ); @@ -272,9 +272,7 @@ export function svgLines(projection, context) { var sidedArr = v.filter(function(d) { return d.isSided(); }); var sidedSegments = svgMarkerSegments( - projection, graph, 30, - function shouldReverse() { return false; }, - function bothDirections() { return false; } + projection, graph, 30 ); sideddata[k] = utilArrayFlatten(sidedArr.map(sidedSegments)); });