Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 82 additions & 31 deletions src/Shared/EditorLayer/EditorMapLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ final class EditorMapLayer: CALayer {
alongScreenClippedWay way: OsmWay,
offset initialOffset: Double,
interval: Double,
block: @escaping (_ pt: OSMPoint, _ direction: OSMPoint) -> Void)
block: @escaping (_ pt: OSMPoint, _ direction: OSMPoint, _ offsetAlongSegment: Double, _ segmentLength: Double) -> Void)
{
var offset = initialOffset
invoke(alongScreenClippedWay: way, block: { p1, p2, isEntry, _ in
Expand All @@ -624,14 +624,65 @@ final class EditorMapLayer: CALayer {
// found it
let pos = OSMPoint(x: p1.x + offset * dx, y: p1.y + offset * dy)
let dir = OSMPoint(x: dx, y: dy)
block(pos, dir)
block(pos, dir, offset, len)
offset += interval
}
offset -= len
return true
})
}

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 static func canPlaceBicycleContraflowArrow(
behindAlongWay: Double,
offsetAlongSegment: Double,
segmentLength: Double
) -> Bool {
if behindAlongWay < 0 {
return offsetAlongSegment >= bicycleContraflowArrowLongitudinalOffset
}
return (segmentLength - offsetAlongSegment) >= bicycleContraflowArrowLongitudinalOffset
}

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<CGFloat>?) -> CGPath? {
var path: CGMutablePath?
Expand Down Expand Up @@ -1409,36 +1460,36 @@ 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)
invoke(alongScreenClippedWay: way, offset: 50, interval: 100, block: { loc, dir, offsetAlongSegment, segmentLength in
let motorLen = way.isOneWay == .BACKWARD
? -Self.oneWayArrowChevronLength
: Self.oneWayArrowChevronLength
if showBicycleContraflow {
let behindAlongWay = way.isOneWay == .FORWARD
? -Self.bicycleContraflowArrowLongitudinalOffset
: Self.bicycleContraflowArrowLongitudinalOffset
if Self.canPlaceBicycleContraflowArrow(
behindAlongWay: behindAlongWay,
offsetAlongSegment: offsetAlongSegment,
segmentLength: segmentLength)
{
layers.append(self.makeOneWayArrowLayer(at: loc,
direction: dir,
chevronLength: -motorLen,
alongWayOffset: behindAlongWay,
fillColor: UIColor.systemBlue,
zPosition: arrowZ))
}
}
layers.append(self.makeOneWayArrowLayer(at: loc,
direction: dir,
chevronLength: motorLen,
alongWayOffset: 0,
fillColor: UIColor.black,
zPosition: arrowZ))
})
}

Expand Down
9 changes: 9 additions & 0 deletions src/Shared/OSMModels/OsmWay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions src/iOS/Go Map!!.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@
64348CFE225E867800ADE7FB /* MeasureDirectionViewModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64348CFA225E867800ADE7FB /* MeasureDirectionViewModelTestCase.swift */; };
64348D01225E8D3F00ADE7FB /* OsmNode+Direction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64348D00225E8D3F00ADE7FB /* OsmNode+Direction.swift */; };
64348D03225E8E4300ADE7FB /* OsmNode_DirectionTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64348D02225E8E4300ADE7FB /* OsmNode_DirectionTestCase.swift */; };
64348D15225E8E4300ADE7FB /* OsmWay_BicycleContraflowTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64348D14225E8E4300ADE7FB /* OsmWay_BicycleContraflowTestCase.swift */; };
64348D13225EAA5D00ADE7FB /* MapViewUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64348D12225EAA5D00ADE7FB /* MapViewUITestCase.swift */; };
6442666722540EDF00C0D545 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442666422540EDF00C0D545 /* Lock.swift */; };
6442666822540EDF00C0D545 /* Disposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442666522540EDF00C0D545 /* Disposable.swift */; };
Expand Down Expand Up @@ -591,6 +592,7 @@
64348CFA225E867800ADE7FB /* MeasureDirectionViewModelTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeasureDirectionViewModelTestCase.swift; sourceTree = "<group>"; };
64348D00225E8D3F00ADE7FB /* OsmNode+Direction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OsmNode+Direction.swift"; sourceTree = "<group>"; };
64348D02225E8E4300ADE7FB /* OsmNode_DirectionTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsmNode_DirectionTestCase.swift; sourceTree = "<group>"; };
64348D14225E8E4300ADE7FB /* OsmWay_BicycleContraflowTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsmWay_BicycleContraflowTestCase.swift; sourceTree = "<group>"; };
64348D08225EA24E00ADE7FB /* GoMapUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GoMapUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
64348D0C225EA24E00ADE7FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
64348D12225EAA5D00ADE7FB /* MapViewUITestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewUITestCase.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1188,6 +1190,7 @@
64348CFA225E867800ADE7FB /* MeasureDirectionViewModelTestCase.swift */,
64348CF6225E867800ADE7FB /* Mocks */,
64348D02225E8E4300ADE7FB /* OsmNode_DirectionTestCase.swift */,
64348D14225E8E4300ADE7FB /* OsmWay_BicycleContraflowTestCase.swift */,
64348CEC225E7CD900ADE7FB /* GoMapTests.swift */,
64C072FC22622B9C00598078 /* Vendor */,
64348CEE225E7CD900ADE7FB /* Info.plist */,
Expand Down Expand Up @@ -1815,6 +1818,7 @@
64E21EB822651F2D004605D7 /* XCTestCase+UserDefaults.swift in Sources */,
64348CFD225E867800ADE7FB /* CLHeadingMock.swift in Sources */,
64348D03225E8E4300ADE7FB /* OsmNode_DirectionTestCase.swift in Sources */,
64348D15225E8E4300ADE7FB /* OsmWay_BicycleContraflowTestCase.swift in Sources */,
64E21EB522651C06004605D7 /* OSMMapDataTestCase.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
39 changes: 39 additions & 0 deletions src/iOS/GoMapTests/OsmWay_BicycleContraflowTestCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// OsmWay_BicycleContraflowTestCase.swift
// GoMapTests
//

@testable import Go_Map__
import XCTest

class OsmWay_BicycleContraflowTestCase: XCTestCase {
func testAllowsBicycleContraflowWhenOneWayAndOnewayBicycleNo() {
let way = OsmWay(asUserCreated: "")
way.constructTag("oneway", value: "yes")
way.constructTag("oneway:bicycle", value: "no")

XCTAssertTrue(way.allowsBicycleContraflow())
}

func testAllowsBicycleContraflowWhenOneWayBackwardAndOnewayBicycleNo() {
let way = OsmWay(asUserCreated: "")
way.constructTag("oneway", value: "-1")
way.constructTag("oneway:bicycle", value: "no")

XCTAssertTrue(way.allowsBicycleContraflow())
}

func testDoesNotAllowBicycleContraflowWithoutOnewayBicycleNo() {
let way = OsmWay(asUserCreated: "")
way.constructTag("oneway", value: "yes")

XCTAssertFalse(way.allowsBicycleContraflow())
}

func testDoesNotAllowBicycleContraflowWhenNotOneWay() {
let way = OsmWay(asUserCreated: "")
way.constructTag("oneway:bicycle", value: "no")

XCTAssertFalse(way.allowsBicycleContraflow())
}
}