99import CoreLocation
1010import UIKit
1111
12+ enum ViewPortRotationStyle { case none, smallShift, largeShift }
13+
1214// Allows other layers of the map to view changes to the map view
1315protocol MapViewPort : AnyObject {
1416 var mapTransform : MapTransform { get }
@@ -97,22 +99,24 @@ extension MapViewPort {
9799 mapTransform. transform = t
98100 }
99101
100- func rotate( by angle: CGFloat , aroundScreenPoint zoomCenter : CGPoint ) {
102+ private func rotate( by angle: CGFloat , around center : CGPoint ) {
101103 guard angle != 0.0 else {
102104 return
103105 }
104-
105- let offset = mapTransform. mapPoint ( forScreenPoint: OSMPoint ( zoomCenter) , birdsEye: false )
106+ let offset = mapTransform. mapPoint ( forScreenPoint: OSMPoint ( center) , birdsEye: false )
106107 var t = mapTransform. transform
107108 t = t. translatedBy ( dx: offset. x, dy: offset. y)
108109 t = t. rotatedBy ( Double ( angle) )
109110 t = t. translatedBy ( dx: - offset. x, dy: - offset. y)
110111 mapTransform. transform = t
111112 }
112113
113- func animateRotation( by deltaHeading: Double , aroundPoint center: CGPoint ) {
114- var deltaHeading = deltaHeading
114+ func rotate( by deltaHeading: Double ,
115+ around center: CGPoint ,
116+ animation: ViewPortRotationStyle )
117+ {
115118 // don't rotate the long way around
119+ var deltaHeading = deltaHeading
116120 while deltaHeading < - . pi {
117121 deltaHeading += 2 * . pi
118122 }
@@ -124,20 +128,25 @@ extension MapViewPort {
124128 return
125129 }
126130
127- let startTime = CACurrentMediaTime ( )
128-
129- let duration = 0.4
130- var prevHeading : Double = 0
131- DisplayLink . shared. remove ( . compassSmoothHeading)
132- DisplayLink . shared. add ( . rotateScreenSmoothing, block: { [ weak self] in
133- guard let self else { return }
131+ switch animation {
132+ case . none:
133+ self . rotate ( by: deltaHeading, around: center)
134+
135+ case . smallShift:
136+ var currentDelta = 0.0
137+ DisplayLink . shared. add ( . rotateScreenSmoothing, block: { [ weak self] in
138+ guard let self else { return }
139+ var delta = deltaHeading - currentDelta
140+ if abs ( delta) < 0.0001 {
141+ DisplayLink . shared. remove ( . rotateScreenSmoothing)
142+ } else {
143+ delta *= 0.15
144+ }
145+ currentDelta += delta
146+ self . rotate ( by: delta, around: center)
147+ } )
134148
135- var elapsedTime = CACurrentMediaTime ( ) - startTime
136- if elapsedTime > duration {
137- elapsedTime = CFTimeInterval ( duration) // don't want to over-rotate
138- }
139- // Rotate using an ease-in/out curve. This ensures that small changes in direction don't cause jerkiness.
140- // result = interpolated value, t = current time, b = initial value, c = delta value, d = duration
149+ case . largeShift:
141150 func easeInOutQuad( _ t: Double , _ b: Double , _ c: Double , _ d: Double ) -> Double {
142151 var t = t
143152 t /= d / 2
@@ -147,13 +156,27 @@ extension MapViewPort {
147156 t -= 1
148157 return - c / 2 * ( t * ( t - 2 ) - 1 ) + b
149158 }
150- let miniHeading = easeInOutQuad ( elapsedTime, 0 , deltaHeading, duration)
151- self . rotate ( by: CGFloat ( miniHeading - prevHeading) , aroundScreenPoint: center)
152- prevHeading = miniHeading
153- if elapsedTime >= duration {
154- DisplayLink . shared. remove ( . rotateScreenSmoothing)
155- }
156- } )
159+
160+ let duration = 0.4
161+ var prevHeading : Double = 0
162+ let startTime = CACurrentMediaTime ( )
163+ DisplayLink . shared. add ( . rotateScreenSmoothing, block: { [ weak self] in
164+ guard let self else { return }
165+
166+ var elapsedTime = CACurrentMediaTime ( ) - startTime
167+ if elapsedTime > duration {
168+ elapsedTime = CFTimeInterval ( duration) // don't want to over-rotate
169+ }
170+ // Rotate using an ease-in/out curve. This ensures that small changes in direction don't cause jerkiness.
171+ // result = interpolated value, t = current time, b = initial value, c = delta value, d = duration
172+ let miniHeading = easeInOutQuad ( elapsedTime, 0 , deltaHeading, duration)
173+ self . rotate ( by: CGFloat ( miniHeading - prevHeading) , around: center)
174+ prevHeading = miniHeading
175+ if elapsedTime >= duration {
176+ DisplayLink . shared. remove ( . rotateScreenSmoothing)
177+ }
178+ } )
179+ }
157180 }
158181
159182 func rotateBirdsEye( by angle: Double ) {
@@ -190,11 +213,9 @@ extension MapViewPort {
190213 // Rotate to face current compass heading
191214 let center = screenCenterPoint ( )
192215 let screenAngle = mapTransform. rotation ( )
193- animateRotation ( by: - ( screenAngle + heading) , aroundPoint: center)
194- }
195-
196- func rotateToNorth( ) {
197- rotateToHeading ( 0.0 )
216+ rotate ( by: - ( screenAngle + heading) ,
217+ around: center,
218+ animation: . largeShift)
198219 }
199220}
200221
0 commit comments