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
61 changes: 59 additions & 2 deletions src/Shared/EditorLayer/EditorMapLayer+Edit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,59 @@ extension EditorMapLayer {
mapData.endUndoGrouping()
}

// MARK: Rotate direction tag

func prepareDirectionRotation() {
guard let node = selectedNode,
let tagKey = node.technicalDirectionTagKey,
let bearing = node.direction?.location
else { return }
directionRotateTagKey = tagKey
directionRotateInitialBearing = bearing
}

func rotateDirectionBegin() {
mapData.beginUndoGrouping()
dragState.didMove = false
directionRotateUndoOpen = true
}

func rotateDirectionContinue(delta: CGFloat) {
guard let node = selectedNode,
let tagKey = directionRotateTagKey,
let initialBearing = directionRotateInitialBearing
else { return }

if dragState.didMove {
mapData.endUndoGrouping()
silentUndo = true
mapData.undo()
silentUndo = false
mapData.beginUndoGrouping()
}
dragState.didMove = true

let deltaDegrees = Int(round(Double(-delta) * 180 / .pi))
let bearing = ((initialBearing + deltaDegrees) % 360 + 360) % 360
guard let value = node.directionTagValue(forBearingDegrees: bearing) else { return }
var tags = node.tags
tags[tagKey] = value
mapData.setTags(tags, for: node)
setNeedsLayout()
}

func rotateDirectionFinish() {
if directionRotateUndoOpen {
mapData.endUndoGrouping()
directionRotateUndoOpen = false
}
if dragState.didMove {
owner.didUpdateObject()
}
directionRotateTagKey = nil
directionRotateInitialBearing = nil
}

// MARK: Editing

func adjust(_ node: OsmNode, byScreenDistance delta: CGPoint) {
Expand Down Expand Up @@ -672,9 +725,12 @@ extension EditorMapLayer {
actionList += [.STRAIGHTEN, .REVERSE, .DUPLICATE, .CREATE_RELATION]
}
}
} else if selectedNode != nil {
} else if let selectedNode = selectedNode {
// node
actionList += [.DUPLICATE]
if canRotateSelectedNodeDirection() {
actionList.append(.ROTATE)
}
} else if let selectedRelation = selectedRelation {
// relation
if selectedRelation.isMultipolygon() {
Expand Down Expand Up @@ -744,7 +800,8 @@ extension EditorMapLayer {
selectedRelation = newObject.isRelation()
owner.placePushpinForSelection(at: nil)
case .ROTATE:
guard selectedWay != nil || (selectedRelation?.isMultipolygon() ?? false) else {
let canRotateGeometry = selectedWay != nil || (selectedRelation?.isMultipolygon() ?? false)
guard canRotateGeometry || canRotateSelectedNodeDirection() else {
throw EditError.text(NSLocalizedString("Only ways/multipolygons can be rotated", comment: ""))
}
owner.startObjectRotation()
Expand Down
13 changes: 13 additions & 0 deletions src/Shared/EditorLayer/EditorMapLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,19 @@ final class EditorMapLayer: CALayer {

var dragState = DragState(startPoint: .zero, didMove: false, confirmDrag: false)

/// Active while rotating a node's `direction` / `camera:direction` tag (not geometry).
var directionRotateTagKey: String?
var directionRotateInitialBearing: Int?
var directionRotateUndoOpen = false

var isRotateDirectionMode: Bool { directionRotateTagKey != nil }

func canRotateSelectedNodeDirection() -> Bool {
selectedWay == nil &&
selectedRelation == nil &&
selectedNode?.technicalDirectionTagKey != nil
}

let objectFilters = EditorFilters()

var whiteText = false {
Expand Down
23 changes: 20 additions & 3 deletions src/Shared/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ final class MapView: UIView, UIGestureRecognizerDelegate, UIContextMenuInteracti
else {
return
}

if editorLayer.canRotateSelectedNodeDirection() {
editorLayer.prepareDirectionRotation()
}
removePin()
let rotateObjectOverlay = CAShapeLayer()
let radiusInner: CGFloat = 70
Expand Down Expand Up @@ -303,6 +307,9 @@ final class MapView: UIView, UIGestureRecognizerDelegate, UIContextMenuInteracti

func endObjectRotation() {
isRotateObjectMode?.rotateObjectOverlay.removeFromSuperlayer()
if editorLayer.isRotateDirectionMode {
editorLayer.rotateDirectionFinish()
}
placePushpinForSelection()
editorLayer.dragState.confirmDrag = false
isRotateObjectMode = nil
Expand Down Expand Up @@ -1048,13 +1055,23 @@ final class MapView: UIView, UIGestureRecognizerDelegate, UIContextMenuInteracti
}
// Rotate object on screen
if rotationGesture.state == .began {
editorLayer.rotateBegin()
if editorLayer.isRotateDirectionMode {
editorLayer.rotateDirectionBegin()
} else {
editorLayer.rotateBegin()
}
} else if rotationGesture.state == .changed {
editorLayer.rotateContinue(delta: rotationGesture.rotation, rotate: rotate)
if editorLayer.isRotateDirectionMode {
editorLayer.rotateDirectionContinue(delta: rotationGesture.rotation)
} else {
editorLayer.rotateContinue(delta: rotationGesture.rotation, rotate: rotate)
}
} else {
// ended
if !editorLayer.isRotateDirectionMode {
editorLayer.rotateFinish()
}
endObjectRotation()
editorLayer.rotateFinish()
}
}

Expand Down
23 changes: 23 additions & 0 deletions src/iOS/Direction/OsmNode+Direction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ extension OsmNode {
return nil
}

/// Tag key (`direction` or `camera:direction`) whose value parses as a technical bearing, if any.
var technicalDirectionTagKey: String? {
for key in ["direction", "camera:direction"] {
if let value = tags[key],
OsmNode.directionFromString(value) != nil
{
return key
}
}
return nil
}

/// OSM tag value for a bearing, preserving arc span when the current direction is a range.
func directionTagValue(forBearingDegrees bearing: Int) -> String? {
guard let range = direction else { return nil }
let normalized = ((bearing % 360) + 360) % 360
if range.length == 0 {
return "\(normalized)"
}
let end = (normalized + range.length) % 360
return "\(normalized)-\(end)"
}

private static func directionFromString(_ string: String) -> NSRange? {
if let direction = Float(string) ?? cardinalDictionary[string] {
return NSMakeRange(Int(direction), 0)
Expand Down
38 changes: 38 additions & 0 deletions src/iOS/GoMapTests/OsmNode_DirectionTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,44 @@ class OsmNode_DirectionTestCase: XCTestCase {
XCTAssertEqual(node.direction?.lowerBound, direction)
}

func testTechnicalDirectionTagKeyPrefersDirectionOverCameraDirection() {
let node = OsmNode(asUserCreated: "")
node.constructTag("direction", value: "90")
node.constructTag("camera:direction", value: "180")

XCTAssertEqual(node.technicalDirectionTagKey, "direction")
}

func testTechnicalDirectionTagKeyUsesCameraDirectionWhenDirectionAbsent() {
let node = OsmNode(asUserCreated: "")
node.constructTag("camera:direction", value: "45")

XCTAssertEqual(node.technicalDirectionTagKey, "camera:direction")
}

func testTechnicalDirectionTagKeyIsNilForHighwayForwardBackward() {
let node = OsmNode(asUserCreated: "")
node.constructTag("highway", value: "stop")
node.constructTag("direction", value: "forward")

XCTAssertNil(node.technicalDirectionTagKey)
XCTAssertNil(node.direction)
}

func testDirectionTagValueFormatsPointBearing() {
let node = OsmNode(asUserCreated: "")
node.constructTag("direction", value: "10")

XCTAssertEqual(node.directionTagValue(forBearingDegrees: 95), "95")
}

func testDirectionTagValuePreservesRangeSpan() {
let node = OsmNode(asUserCreated: "")
node.constructTag("direction", value: "90-120")

XCTAssertEqual(node.directionTagValue(forBearingDegrees: 0), "0-30")
}

func testDirectionShouldParseCardinalDirectionToLowerBound() {
let key = "camera:direction"

Expand Down