Render oneway markers inline in dashed lines (#10849)

This commit is contained in:
Martin Raifer
2025-03-20 14:01:26 +01:00
committed by GitHub
parent 7d933f3875
commit 8e8c5a8621
9 changed files with 127 additions and 69 deletions
+1
View File
@@ -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
+17 -1
View File
@@ -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;
}
+1
View File
@@ -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;
+5 -1
View File
@@ -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;
+17 -8
View File
@@ -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<geoRawMercator>} Projection
*/
+2
View File
@@ -21,6 +21,8 @@ declare global {
export type AbstractEntity = InstanceType<typeof iD.osmEntity>;
export type OsmEntity = OsmNode | OsmWay | OsmRelation;
export type Projection = import('./geo/raw_mercator').Projection;
}
declare namespace d3 {
+6 -6
View File
@@ -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) {
+73 -46
View File
@@ -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<Boolean>} [shouldReverse]
* @param {Function<Boolean>} [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;
}
+5 -7
View File
@@ -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));
});