Skip to content

Commit c55cb67

Browse files
authored
History replay improvements (#4342)
* vk-NAVIOS-841-history-replay: exposed PushRecoirdHistoryEvent publicly to allow history parser to show this event to users; modified ReplayLocationManager to be able to replay history events in chronological order, including listening to the events; added default history events listener to MapboxNavigationService that will also set route updates from history to router; breaking changes accepted; renamed PushRecordHistoryEvent to UserPushedHistoryEvent
1 parent 8f8d20e commit c55cb67

5 files changed

Lines changed: 155 additions & 32 deletions

File tree

.breakage-allowlist

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
Func CarPlayManagerDelegate.carPlayManagerDidCancelPreview(_:) has been added as a protocol requirement
1+
Func CarPlayManagerDelegate.carPlayManagerDidCancelPreview(_:) has been added as a protocol requirement
2+
Constructor MapboxNavigationService.init(history:customRoutingProvider:credentials:eventsManagerType:routerType:customActivityType:) has been removed

Sources/MapboxCoreNavigation/HistoryEvent.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,21 @@ internal class StatusUpdateHistoryEvent: UnknownHistoryEvent {
4949
}
5050
}
5151

52-
internal class PushRecordHistoryEvent: UnknownHistoryEvent {
53-
let type: String
54-
let properties: String
52+
/// History event being pushed by the user
53+
///
54+
/// Such events are created by calling `HistoryRecording.pushHistoryEvent(type:jsonData:)`.
55+
public struct UserPushedHistoryEvent: HistoryEvent {
56+
public let timestamp: TimeInterval
57+
/// The event type specified for this custom event.
58+
public let type: String
59+
/// The data value that contains a valid JSON attached to the event.
60+
///
61+
/// This value was provided by user with `HistoryRecording.pushHistoryEvent` method's `dictionary` argument.
62+
public let properties: String
5563

5664
init(timestamp: TimeInterval, type: String, properties: String) {
5765
self.type = type
5866
self.properties = properties
59-
60-
super.init(timestamp: timestamp)
67+
self.timestamp = timestamp
6168
}
6269
}

Sources/MapboxCoreNavigation/HistoryReader.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ public struct History {
2121
return ($0 as? LocationUpdateHistoryEvent)?.location
2222
}
2323
}
24+
25+
func rawLocationsShiftedToPresent() -> [CLLocation] {
26+
return self.rawLocations.enumerated().map { CLLocation(coordinate: $0.element.coordinate,
27+
altitude: $0.element.altitude,
28+
horizontalAccuracy: $0.element.horizontalAccuracy,
29+
verticalAccuracy: $0.element.verticalAccuracy,
30+
course: $0.element.course,
31+
speed: $0.element.speed,
32+
timestamp: Date() + TimeInterval($0.offset)) }
33+
}
2434
}
2535

2636
/// Provides event-by-event access to history files contents.
@@ -78,7 +88,7 @@ public struct HistoryReader: Sequence {
7888
status: event.result)
7989
case .pushHistory:
8090
guard let event = record.pushHistory else { break }
81-
return PushRecordHistoryEvent(timestamp: timestamp,
91+
return UserPushedHistoryEvent(timestamp: timestamp,
8292
type: event.type,
8393
properties: event.properties)
8494
@unknown default:

Sources/MapboxCoreNavigation/NavigationService.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,13 +476,15 @@ public class MapboxNavigationService: NSObject, NavigationService {
476476
Intializes a new `NavigationService` for replaying a session from provided `History`.
477477

478478
- parameter history: `History` object, containing initial route and location trace to be replayed.
479+
- parameter customHistoryEventsListener: Custom `ReplayManagerHistoryEventsListener` which will be used to handle replay events. Default value (`nil`) will also loop route assignment events to update the `Router`.
479480
- parameter customRoutingProvider: Custom `RoutingProvider`, used to create a route during refreshing or rerouting.
480481
- parameter credentials: Credentials to authorize additional data requests throughout the route.
481482
- parameter eventsManagerType: An optional events manager type to use while tracking the route.
482483
- parameter routerType: An optional router type to use for traversing the route.
483484
- returns `nil` if provided `historyFileDump` does not contain valid initial route.
484485
*/
485486
public convenience init?(history: History,
487+
customHistoryEventsListener: ReplayManagerHistoryEventsListener? = nil,
486488
customRoutingProvider: RoutingProvider? = nil,
487489
credentials: Credentials,
488490
eventsManagerType: NavigationEventsManager.Type? = nil,
@@ -491,13 +493,20 @@ public class MapboxNavigationService: NSObject, NavigationService {
491493
guard let routeResponse = history.initialRoute else {
492494
return nil
493495
}
496+
497+
let replayLocationManager = ReplayLocationManager(history: history,
498+
listener: customHistoryEventsListener)
494499
self.init(indexedRouteResponse: routeResponse,
495500
customRoutingProvider: customRoutingProvider,
496501
credentials: credentials,
497-
locationSource: ReplayLocationManager(history: history),
502+
locationSource: replayLocationManager,
498503
eventsManagerType: eventsManagerType,
499504
routerType: routerType,
500505
customActivityType: customActivityType)
506+
507+
if customHistoryEventsListener == nil {
508+
replayLocationManager.eventsListener = self
509+
}
501510
}
502511

503512
init(indexedRouteResponse: IndexedRouteResponse,
@@ -829,6 +838,17 @@ extension MapboxNavigationService {
829838
}
830839
}
831840

841+
extension MapboxNavigationService: ReplayManagerHistoryEventsListener {
842+
public func replyLocationManager(_ manager: ReplayLocationManager, published event: HistoryEvent) {
843+
// handling `RouteAssignmentHistoryEvent` to replay route updates from user/reroutes/etc.
844+
if let setRouteEvent = event as? RouteAssignmentHistoryEvent {
845+
updateRoute(with: setRouteEvent.routeResponse,
846+
routeOptions: nil,
847+
completion: nil)
848+
}
849+
}
850+
}
851+
832852
private extension Double {
833853
var dispatchInterval: DispatchTimeInterval {
834854
let milliseconds = self * 1000.0 //milliseconds per second

Sources/MapboxCoreNavigation/ReplayLocationManager.swift

Lines changed: 109 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@ open class ReplayLocationManager: NavigationLocationManager {
2222
didSet {
2323
currentIndex = 0
2424
verifyParameters()
25+
self.events = locations.map { ReplayEvent(from: $0) }
2526
}
2627
}
2728

29+
private(set) var events: [ReplayEvent]
30+
31+
/**
32+
Events listener that will receive history events if replaying a `History`.
33+
*/
34+
public weak var eventsListener: ReplayManagerHistoryEventsListener? = nil
35+
2836
/**
2937
`simulatesLocation` used to indicate whether the location manager is providing simulated locations.
3038
- seealso: `NavigationMapView.simulatesLocation`
@@ -64,12 +72,19 @@ open class ReplayLocationManager: NavigationLocationManager {
6472

6573
public init(locations: [CLLocation]) {
6674
self.locations = locations.sorted { $0.timestamp < $1.timestamp }
75+
self.events = locations.map { ReplayEvent(from: $0) }
6776
super.init()
6877
verifyParameters()
6978
}
7079

7180
public convenience init(history: History) {
72-
self.init(locations: history.rawLocations)
81+
self.init(locations: history.rawLocationsShiftedToPresent())
82+
self.events = history.events.map { ReplayEvent(from: $0) }
83+
}
84+
85+
public convenience init(history: History, listener: ReplayManagerHistoryEventsListener?) {
86+
self.init(history: history)
87+
self.eventsListener = listener
7388
}
7489

7590
deinit {
@@ -90,11 +105,16 @@ open class ReplayLocationManager: NavigationLocationManager {
90105
@objc internal func tick() {
91106
guard let startDate = startDate else { return }
92107

93-
func sendTick(with location: CLLocation) {
94-
synthesizedLocation = location
95-
delegate?.locationManager?(self, didUpdateLocations: [location])
96-
onTick?(currentIndex, location)
97-
nextTickWorkItem?.cancel()
108+
func sendTick(_ event: ReplayEvent) {
109+
switch event.kind {
110+
case .location(let location):
111+
synthesizedLocation = location
112+
delegate?.locationManager?(self, didUpdateLocations: [location])
113+
onTick?(currentIndex, location)
114+
nextTickWorkItem?.cancel()
115+
case .historyEvent(let historyEvent):
116+
eventsListener?.replyLocationManager(self, published: historyEvent)
117+
}
98118
}
99119

100120
func scheduleNextTick(afterDelay delay: TimeInterval) {
@@ -105,35 +125,36 @@ open class ReplayLocationManager: NavigationLocationManager {
105125
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: nextTickWorkItem)
106126
}
107127

108-
guard locations.count > 1 else {
109-
sendTick(with: locations[0])
128+
guard events.count > 1 else {
129+
sendTick(events[0])
130+
110131
let startFromBeginning = replayCompletionHandler?(self) ?? false
111132
if startFromBeginning {
112-
advanceLocationsForNextLoop()
133+
advanceEventsForNextLoop()
113134
scheduleNextTick(afterDelay: 1 / speedMultiplier)
114135
}
115136
return
116137
}
117138

118-
let location = locations[currentIndex]
119-
sendTick(with: location)
139+
let event = events[currentIndex]
140+
sendTick(event)
120141

121142
var nextIndex = currentIndex + 1
122-
if nextIndex == locations.count {
143+
if nextIndex == events.count {
123144
let startFromBeginning = replayCompletionHandler?(self) ?? false
124145
if startFromBeginning {
125-
advanceLocationsForNextLoop()
146+
advanceEventsForNextLoop()
126147
nextIndex = 0
127148
}
128149
else {
129150
return
130151
}
131152
}
132153

133-
let nextLocation = locations[nextIndex]
134-
let interval = nextLocation.timestamp.timeIntervalSince(location.timestamp) / TimeInterval(speedMultiplier)
154+
let nextEvent = events[nextIndex]
155+
let interval = nextEvent.date.timeIntervalSince(event.date) / TimeInterval(speedMultiplier)
135156
let intervalSinceStart = Date().timeIntervalSince(startDate)+interval
136-
let actualInterval = nextLocation.timestamp.timeIntervalSince(locations.first!.timestamp)
157+
let actualInterval = nextEvent.date.timeIntervalSince(events.first!.date)
137158
let diff = min(max(0, intervalSinceStart-actualInterval), 0.9) // Don't try to resync more than 0.9 seconds per location update
138159
let syncedInterval = interval-diff
139160

@@ -145,19 +166,83 @@ open class ReplayLocationManager: NavigationLocationManager {
145166
precondition(!locations.isEmpty)
146167
}
147168

148-
/// Shift `locations` so that sent locations always have increasing timestamps, taking into account location deltas.
149-
private func advanceLocationsForNextLoop() {
169+
/// Shift `events` and `locations` so that sent locations always have increasing timestamps, taking into account event deltas.
170+
private func advanceEventsForNextLoop() {
150171
/// Previous location that is used to calculate deltas between locations.
151-
var previousOldLocation: CLLocation = locations.last!
172+
var previousOldLocation = events.last!
152173
/// Previous timestamp that is used to advance timestamps.
153-
var previousNewLocationTimestamp = previousOldLocation.timestamp
174+
var previousNewLocationTimestamp = previousOldLocation.date
154175

155-
for (idx, location) in locations.enumerated() {
156-
let delta: TimeInterval = idx == 0 ? 1 : location.timestamp.timeIntervalSince(previousOldLocation.timestamp)
176+
var advancedLocations: [CLLocation] = []
177+
178+
for (idx, event) in events.enumerated() {
179+
let delta: TimeInterval = idx == 0 ? 1 : event.date.timeIntervalSince(previousOldLocation.date)
157180
let newTimestamp = previousNewLocationTimestamp.addingTimeInterval(delta)
158-
previousOldLocation = location
159-
locations[idx] = location.shifted(to: newTimestamp)
181+
previousOldLocation = event
182+
let shiftedEvent = event.shifted(to: newTimestamp)
183+
events[idx] = shiftedEvent
184+
185+
if case let .location(location) = shiftedEvent.kind {
186+
advancedLocations.append(location)
187+
} else if case let .historyEvent(historyEvent) = shiftedEvent.kind,
188+
let locationEvent = historyEvent as? LocationUpdateHistoryEvent {
189+
advancedLocations.append(locationEvent.location)
190+
}
191+
160192
previousNewLocationTimestamp = newTimestamp
161193
}
194+
self.locations = advancedLocations
195+
}
196+
}
197+
198+
/**
199+
`ReplayLocationManager`'s listener that will receive events feed when it is replaying a `History` data.
200+
*/
201+
public protocol ReplayManagerHistoryEventsListener: AnyObject {
202+
func replyLocationManager(_ manager: ReplayLocationManager, published event: HistoryEvent)
203+
}
204+
205+
206+
struct ReplayEvent {
207+
var date: Date
208+
209+
enum Kind {
210+
case location(CLLocation)
211+
case historyEvent(HistoryEvent)
212+
}
213+
var kind: Kind
214+
215+
func shifted(to date: Date) -> ReplayEvent {
216+
switch kind {
217+
case .location(let location):
218+
return ReplayEvent(from: location.shifted(to: date))
219+
case .historyEvent(let historyEvent):
220+
switch historyEvent {
221+
case let event as LocationUpdateHistoryEvent:
222+
return ReplayEvent(from: LocationUpdateHistoryEvent(timestamp: date.timeIntervalSince1970,
223+
location: event.location.shifted(to: date)))
224+
case let event as RouteAssignmentHistoryEvent:
225+
return ReplayEvent(from: RouteAssignmentHistoryEvent(timestamp: date.timeIntervalSince1970,
226+
routeResponse: event.routeResponse))
227+
case let event as UserPushedHistoryEvent:
228+
return ReplayEvent(from: UserPushedHistoryEvent(timestamp: date.timeIntervalSince1970,
229+
type: event.type,
230+
properties: event.properties))
231+
case is UnknownHistoryEvent:
232+
fallthrough
233+
default:
234+
return ReplayEvent(from: UnknownHistoryEvent(timestamp: date.timeIntervalSince1970))
235+
}
236+
}
237+
}
238+
239+
init(from location: CLLocation) {
240+
self.date = location.timestamp
241+
self.kind = .location(location)
242+
}
243+
244+
init(from historyEvent: HistoryEvent) {
245+
self.date = Date(timeIntervalSince1970: historyEvent.timestamp)
246+
self.kind = .historyEvent(historyEvent)
162247
}
163248
}

0 commit comments

Comments
 (0)