From 839b341c21e5bbacce572b61f61f8348d86ae20c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 30 May 2026 15:42:30 +0000 Subject: [PATCH] Show blue contraflow arrow for bicycle:oneway=no on one-way ways MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a way is one-way for motor traffic but tagged oneway:bicycle=no, draw the existing black chevron for the general direction plus a second blue chevron in the opposite direction, offset perpendicular to the way so both arrows remain readable. https: //wiki.openstreetmap.org/wiki/Key:oneway:bicycle Increase gap between motor and bicycle chevrons to one icon width Longitudinal offset is now 3× chevron length (motor body + gap + bicycle tip placement) so < > pairs no longer overlap. Co-Authored-By: Tobias --- src/Shared/EditorLayer/EditorMapLayer.swift | 88 ++++++++++++++------- src/Shared/OSMModels/OsmWay.swift | 9 +++ 2 files changed, 70 insertions(+), 27 deletions(-) diff --git a/src/Shared/EditorLayer/EditorMapLayer.swift b/src/Shared/EditorLayer/EditorMapLayer.swift index 3259cecc3..1a8544819 100644 --- a/src/Shared/EditorLayer/EditorMapLayer.swift +++ b/src/Shared/EditorLayer/EditorMapLayer.swift @@ -632,6 +632,47 @@ final class EditorMapLayer: CALayer { }) } + + private static let oneWayArrowChevronLength: Double = 15 + private static let oneWayArrowChevronWidth: Double = 5 + /// Along-way distance from motor chevron tip to bicycle chevron tip (motor icon + gap + bicycle icon). + private static let bicycleContraflowArrowLongitudinalOffset: Double = 3 * oneWayArrowChevronLength + + private func makeOneWayArrowLayer( + at loc: OSMPoint, + direction dir: OSMPoint, + chevronLength len: Double, + alongWayOffset: Double, + fillColor: UIColor, + zPosition: CGFloat + ) -> CAShapeLayerWithProperties { + let position = OSMPoint(x: loc.x + dir.x * alongWayOffset, + y: loc.y + dir.y * alongWayOffset) + let width = Self.oneWayArrowChevronWidth + + let p1 = OSMPoint(x: position.x - dir.x * len + dir.y * width, + y: position.y - dir.y * len - dir.x * width) + let p2 = OSMPoint(x: position.x - dir.x * len - dir.y * width, + y: position.y - dir.y * len + dir.x * width) + + let arrowPath = CGMutablePath() + arrowPath.move(to: CGPoint(x: p1.x, y: p1.y)) + arrowPath.addLine(to: CGPoint(x: position.x, y: position.y)) + arrowPath.addLine(to: CGPoint(x: p2.x, y: p2.y)) + arrowPath + .addLine(to: CGPoint(x: CGFloat(position.x - dir.x * len * 0.5), + y: CGFloat(position.y - dir.y * len * 0.5))) + arrowPath.closeSubpath() + + let arrow = CAShapeLayerWithProperties() + arrow.path = arrowPath + arrow.fillColor = fillColor.cgColor + arrow.strokeColor = UIColor.white.cgColor + arrow.lineWidth = 0.5 + arrow.zPosition = zPosition + return arrow + } + // clip a way to the path inside the viewable rect so we can draw a name on it func pathClipped(toViewRect way: OsmWay, length pLength: UnsafeMutablePointer?) -> CGPath? { var path: CGMutablePath? @@ -1409,36 +1450,29 @@ final class EditorMapLayer: CALayer { } let isHighlight = highlights.contains(way) if way.isOneWay != .NONE || isHighlight { + let showBicycleContraflow = way.allowsBicycleContraflow() + let arrowZ = isHighlight ? self.Z_HIGHLIGHT_ARROW : self.Z_ARROW // arrow heads invoke(alongScreenClippedWay: way, offset: 50, interval: 100, block: { loc, dir in - // draw direction arrow at loc/dir let reversed = way.isOneWay == ONEWAY.BACKWARD - let len: Double = reversed ? -15 : 15 - let width: Double = 5 - - let p1 = OSMPoint(x: loc.x - dir.x * len + dir.y * width, - y: loc.y - dir.y * len - dir.x * width) - let p2 = OSMPoint(x: loc.x - dir.x * len - dir.y * width, - y: loc.y - dir.y * len + dir.x * width) - - let arrowPath = CGMutablePath() - arrowPath.move(to: CGPoint(x: p1.x, y: p1.y)) - arrowPath.addLine(to: CGPoint(x: loc.x, y: loc.y)) - arrowPath.addLine(to: CGPoint(x: p2.x, y: p2.y)) - arrowPath - .addLine(to: CGPoint(x: CGFloat(loc.x - dir.x * len * 0.5), - y: CGFloat(loc.y - dir.y * len * 0.5))) - arrowPath.closeSubpath() - - let arrow = CAShapeLayerWithProperties() - arrow.path = arrowPath - arrow.lineWidth = 1 - arrow.fillColor = UIColor.black.cgColor - arrow.strokeColor = UIColor.white.cgColor - arrow.lineWidth = 0.5 - arrow.zPosition = isHighlight ? self.Z_HIGHLIGHT_ARROW : self.Z_ARROW - - layers.append(arrow) + let motorLen = reversed ? -Self.oneWayArrowChevronLength : Self.oneWayArrowChevronLength + let behindAlongWay = motorLen > 0 + ? -Self.bicycleContraflowArrowLongitudinalOffset + : Self.bicycleContraflowArrowLongitudinalOffset + if showBicycleContraflow { + layers.append(self.makeOneWayArrowLayer(at: loc, + direction: dir, + chevronLength: -motorLen, + alongWayOffset: behindAlongWay, + fillColor: .systemBlue, + zPosition: arrowZ)) + } + layers.append(self.makeOneWayArrowLayer(at: loc, + direction: dir, + chevronLength: motorLen, + alongWayOffset: 0, + fillColor: .black, + zPosition: arrowZ)) }) } diff --git a/src/Shared/OSMModels/OsmWay.swift b/src/Shared/OSMModels/OsmWay.swift index d79e2962b..22c217d09 100644 --- a/src/Shared/OSMModels/OsmWay.swift +++ b/src/Shared/OSMModels/OsmWay.swift @@ -270,6 +270,15 @@ final class OsmWay: OsmBaseObject, NSSecureCoding { return _isOneWay! } + // https://wiki.openstreetmap.org/wiki/Key:oneway:bicycle + /// True when the way is one-way for general traffic but cyclists may use the opposite direction. + func allowsBicycleContraflow() -> Bool { + guard isOneWay != .NONE else { + return false + } + return tags["oneway:bicycle"] == "no" + } + // return the point on the way closest to the supplied point override func latLonOnObject(forLatLon target: LatLon) -> LatLon { switch nodes.count {