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

// MARK: Rotate direction tag

func rotateDirectionBegin() {
guard let node = selectedNode,
let tagKey = node.technicalDirectionTagKey,
let bearing = node.direction?.location
else { return }
mapData.beginUndoGrouping()
dragState.didMove = false
directionRotateTagKey = tagKey
directionRotateInitialBearing = bearing
}

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()
owner.didUpdateObject()
}

func rotateDirectionFinish() {
mapData.endUndoGrouping()
directionRotateTagKey = nil
directionRotateInitialBearing = nil
}

func isRotateDirectionMode() -> Bool {
directionRotateTagKey != nil
}

// MARK: Editing

func adjust(_ node: OsmNode, byScreenDistance delta: CGPoint) {
Expand Down Expand Up @@ -672,9 +720,12 @@ extension EditorMapLayer {
actionList += [.STRAIGHTEN, .REVERSE, .DUPLICATE, .CREATE_RELATION]
}
}
} else if selectedNode != nil {
} else if let selectedNode = selectedNode {
// node
actionList += [.DUPLICATE]
if selectedNode.technicalDirectionTagKey != nil {
actionList.append(.ROTATE)
}
} else if let selectedRelation = selectedRelation {
// relation
if selectedRelation.isMultipolygon() {
Expand Down Expand Up @@ -744,7 +795,11 @@ 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)
let canRotateDirection = selectedWay == nil &&
selectedRelation == nil &&
selectedNode?.technicalDirectionTagKey != nil
guard canRotateGeometry || canRotateDirection else {
throw EditError.text(NSLocalizedString("Only ways/multipolygons can be rotated", comment: ""))
}
owner.startObjectRotation()
Expand Down
4 changes: 4 additions & 0 deletions src/Shared/EditorLayer/EditorMapLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ 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?

let objectFilters = EditorFilters()

var whiteText = false {
Expand Down
25 changes: 22 additions & 3 deletions src/Shared/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,20 @@ final class MapView: UIView, UIGestureRecognizerDelegate, UIContextMenuInteracti
// remove previous rotation in case user pressed Rotate button twice
endObjectRotation()

let isDirectionRotate = editorLayer.selectedWay == nil &&
editorLayer.selectedRelation == nil &&
editorLayer.selectedNode?.technicalDirectionTagKey != nil

guard let rotateObjectCenter = editorLayer.selectedNode?.latLon
?? editorLayer.selectedWay?.centerPoint()
?? editorLayer.selectedRelation?.centerPoint()
else {
return
}

if isDirectionRotate {
editorLayer.rotateDirectionBegin()
}
removePin()
let rotateObjectOverlay = CAShapeLayer()
let radiusInner: CGFloat = 70
Expand Down Expand Up @@ -303,6 +311,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 +1059,21 @@ final class MapView: UIView, UIGestureRecognizerDelegate, UIContextMenuInteracti
}
// Rotate object on screen
if rotationGesture.state == .began {
editorLayer.rotateBegin()
if !editorLayer.isRotateDirectionMode() {
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
29 changes: 29 additions & 0 deletions src/iOS/Direction/OsmNode+Direction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,35 @@ 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
}

/// Bearing in degrees clockwise from north for a point direction (`direction` length 0).
var directionPointBearing: Int? {
guard let range = direction, range.length == 0 else { return nil }
return range.location
}

/// 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
Loading