diff --git a/LocoKit/Base/Timelines/Items/Path.swift b/LocoKit/Base/Timelines/Items/Path.swift index f7f6a6d8..184ce78d 100644 --- a/LocoKit/Base/Timelines/Items/Path.swift +++ b/LocoKit/Base/Timelines/Items/Path.swift @@ -242,8 +242,8 @@ open class Path: TimelineItem { } override open func samplesChanged() { - _distance = nil super.samplesChanged() + _distance = nil } } diff --git a/LocoKit/Base/Timelines/Merge.swift b/LocoKit/Base/Timelines/Merge.swift index 3d124a5f..2236209a 100644 --- a/LocoKit/Base/Timelines/Merge.swift +++ b/LocoKit/Base/Timelines/Merge.swift @@ -5,7 +5,12 @@ import os.log +public extension NSNotification.Name { + public static let mergedTimelineItems = Notification.Name("mergedTimelineItems") +} + typealias MergeScore = ConsumptionScore +public typealias MergeResult = (kept: TimelineItem, killed: [TimelineItem]) internal class Merge: CustomStringConvertible { @@ -42,14 +47,25 @@ internal class Merge: CustomStringConvertible { if let betweener = betweener { store.release(betweener) } } - @discardableResult func doIt() -> (kept: TimelineItem, killed: [TimelineItem]) { + @discardableResult func doIt() -> MergeResult { + let description = String(describing: self) + os_log("Doing:\n%@", type: .debug, description) + merge(deadman, into: keeper) - + + let results: MergeResult if let betweener = betweener { - return (kept: keeper, killed: [deadman, betweener]) + results = (kept: keeper, killed: [deadman, betweener]) } else { - return (kept: keeper, killed: [deadman]) + results = (kept: keeper, killed: [deadman]) } + + // notify listeners + let note = Notification(name: .mergedTimelineItems, object: self, + userInfo: ["description": description, "results": results]) + NotificationCenter.default.post(note) + + return results } private func merge(_ deadman: TimelineItem, into keeper: TimelineItem) { diff --git a/LocoKit/Base/Timelines/TimelineProcessor.swift b/LocoKit/Base/Timelines/TimelineProcessor.swift index 194afc71..d34d9596 100644 --- a/LocoKit/Base/Timelines/TimelineProcessor.swift +++ b/LocoKit/Base/Timelines/TimelineProcessor.swift @@ -7,15 +7,10 @@ import os.log -public extension NSNotification.Name { - public static let mergedTimelineItems = Notification.Name("mergedTimelineItems") -} - public class TimelineProcessor { public static func process(from fromItem: TimelineItem) { - guard let store = fromItem.store else { return } - store.process { + fromItem.store?.process { var items: [TimelineItem] = [fromItem] // collect items before fromItem, up to two keepers @@ -36,6 +31,21 @@ public class TimelineProcessor { workingItem = next } + // recurse until no remaining possible merges + process(items) { results in + if let kept = results?.kept { + process(from: kept) + } + } + } + } + + public static func process(_ items: [TimelineItem], completion: ((MergeResult?) -> Void)? = nil) { + guard let store = items.first?.store else { return } + store.process { + + /** collate all the potential merges **/ + var merges: [Merge] = [] for workingItem in items { workingItem.sanitiseEdges() @@ -88,7 +98,8 @@ public class TimelineProcessor { } } - // sort the merges by highest to lowest score + /** sort the merges by highest to lowest score **/ + merges = merges.sorted { $0.score.rawValue > $1.score.rawValue } if !merges.isEmpty { @@ -97,21 +108,18 @@ public class TimelineProcessor { os_log("Considering:\n%@", type: .debug, descriptions) } - // do the highest scoring valid merge - if let winningMerge = merges.first, winningMerge.score != .impossible { - let description = String(describing: winningMerge) - os_log("Doing:\n%@", type: .debug, description) + /** find the highest scoring valid merge **/ - let results = winningMerge.doIt() + guard let winningMerge = merges.first, winningMerge.score != .impossible else { + completion?(nil) + return + } - onMain { - let note = Notification(name: .mergedTimelineItems, object: self, userInfo: ["merge": description]) - NotificationCenter.default.post(note) - } + /** do it **/ - // recurse until no valid merges left to do - self.process(from: results.kept) - } + let results = winningMerge.doIt() + + completion?(results) } } diff --git a/LocoKit/Base/Timelines/TimelineRecorder.swift b/LocoKit/Base/Timelines/TimelineRecorder.swift index 716d0243..be77761d 100644 --- a/LocoKit/Base/Timelines/TimelineRecorder.swift +++ b/LocoKit/Base/Timelines/TimelineRecorder.swift @@ -33,11 +33,9 @@ public class TimelineRecorder { public init(store: TimelineStore, classifier: MLCompositeClassifier? = nil) { self.store = store + store.recorder = self self.classifier = classifier - // bootstrap the current item - self.currentItem = store.mostRecentItem - let notes = NotificationCenter.default notes.addObserver(forName: .locomotionSampleUpdated, object: nil, queue: nil) { [weak self] _ in self?.recordSample() @@ -51,6 +49,15 @@ public class TimelineRecorder { notes.addObserver(forName: .recordingStateChanged, object: nil, queue: nil) { [weak self] _ in self?.updateSleepModeAcceptability() } + + // keep currentItem sane after merges + notes.addObserver(forName: .mergedTimelineItems, object: nil, queue: nil) { [weak self] note in + guard let results = note.userInfo?["results"] as? MergeResult else { return } + guard let current = self?.currentItem else { return } + if results.killed.contains(current) { + self?.currentItem = results.kept + } + } } // MARK: - Starting and stopping recording @@ -97,7 +104,17 @@ public class TimelineRecorder { // MARK: - The recording cycle - private(set) public var currentItem: TimelineItem? + private var _currentItem: TimelineItem? + public private(set) var currentItem: TimelineItem? { + get { + if let item = _currentItem { return item } + _currentItem = store.mostRecentItem + return _currentItem + } + set(newValue) { + _currentItem = newValue + } + } public var currentVisit: Visit? { return currentItem as? Visit } diff --git a/LocoKit/LocalStore/PersistentObject.swift b/LocoKit/LocalStore/PersistentObject.swift index a6e28d7f..f5970196 100644 --- a/LocoKit/LocalStore/PersistentObject.swift +++ b/LocoKit/LocalStore/PersistentObject.swift @@ -17,7 +17,8 @@ public protocol PersistentObject: TimelineObject, Persistable { var transactionDate: Date? { get set } var lastSaved: Date? { get set } var unsaved: Bool { get } - var hasChanges: Bool { get } + var hasChanges: Bool { get set } + var needsSave: Bool { get } func save(immediate: Bool) func save(in db: Database) throws @@ -26,8 +27,12 @@ public protocol PersistentObject: TimelineObject, Persistable { public extension PersistentObject { public var unsaved: Bool { return lastSaved == nil } + public var needsSave: Bool { return unsaved || hasChanges } public func save(immediate: Bool = false) { persistentStore.save(self, immediate: immediate) } - public func save(in db: Database) throws { if unsaved { try insert(db) } else if hasChanges { try update(db) } } + public func save(in db: Database) throws { + if unsaved { try insert(db) } else if hasChanges { try update(db) } + hasChanges = false + } } public extension PersistentObject where Self: TimelineItem { diff --git a/LocoKit/LocalStore/PersistentPath.swift b/LocoKit/LocalStore/PersistentPath.swift index a2039160..a2ea3599 100644 --- a/LocoKit/LocalStore/PersistentPath.swift +++ b/LocoKit/LocalStore/PersistentPath.swift @@ -16,7 +16,7 @@ open class PersistentPath: Path, PersistentObject { didSet { if oldValue != deleted { hasChanges = true - save(immediate: true) + save() } } } @@ -114,7 +114,7 @@ open class PersistentPath: Path, PersistentObject { public var transactionDate: Date? public var lastSaved: Date? - public private(set) var hasChanges: Bool = false + public var hasChanges: Bool = false // MARK: Initialisers diff --git a/LocoKit/LocalStore/PersistentSample.swift b/LocoKit/LocalStore/PersistentSample.swift index edbe8a5d..5c88a15a 100644 --- a/LocoKit/LocalStore/PersistentSample.swift +++ b/LocoKit/LocalStore/PersistentSample.swift @@ -93,7 +93,7 @@ open class PersistentSample: LocomotionSample, PersistentObject { public var persistentStore: PersistentTimelineStore { return store as! PersistentTimelineStore } public var transactionDate: Date? public var lastSaved: Date? - public private(set) var hasChanges: Bool = false + public var hasChanges: Bool = false } diff --git a/LocoKit/LocalStore/PersistentTimelineStore.swift b/LocoKit/LocalStore/PersistentTimelineStore.swift index 4ee4d3b6..8ee02353 100644 --- a/LocoKit/LocalStore/PersistentTimelineStore.swift +++ b/LocoKit/LocalStore/PersistentTimelineStore.swift @@ -212,10 +212,10 @@ open class PersistentTimelineStore: TimelineStore { mutex.sync { guard immediate || (itemsToSave.count + samplesToSave.count >= saveBatchSize) else { return } - savingItems = itemsToSave + savingItems = itemsToSave.filter { ($0 as? PersistentItem)?.needsSave == true } itemsToSave.removeAll(keepingCapacity: true) - savingSamples = samplesToSave + savingSamples = samplesToSave.filter { $0.needsSave } samplesToSave.removeAll(keepingCapacity: true) } diff --git a/LocoKit/LocalStore/PersistentVisit.swift b/LocoKit/LocalStore/PersistentVisit.swift index e7823140..4c83f4df 100644 --- a/LocoKit/LocalStore/PersistentVisit.swift +++ b/LocoKit/LocalStore/PersistentVisit.swift @@ -16,7 +16,7 @@ open class PersistentVisit: Visit, PersistentObject { didSet { if oldValue != deleted { hasChanges = true - save(immediate: true) + save() } } } @@ -114,7 +114,7 @@ open class PersistentVisit: Visit, PersistentObject { public var transactionDate: Date? public var lastSaved: Date? - public private(set) var hasChanges: Bool = false + public var hasChanges: Bool = false // MARK: Initialisers diff --git a/LocoKit/LocalStore/TimelineSegment.swift b/LocoKit/LocalStore/TimelineSegment.swift index a4454c32..0f7a758a 100644 --- a/LocoKit/LocalStore/TimelineSegment.swift +++ b/LocoKit/LocalStore/TimelineSegment.swift @@ -5,14 +5,21 @@ // Created by Matt Greenfield on 29/04/18. // +import os.log import GRDB public class TimelineSegment { public let store: PersistentTimelineStore - public private(set) var timelineItems: [TimelineItem]? public var onUpdate: (() -> Void)? + private var _timelineItems: [TimelineItem]? + public var timelineItems: [TimelineItem] { + if let existing = _timelineItems { return existing } + _timelineItems = updatedItems + return _timelineItems! + } + private let query: String private let arguments: StatementArguments? private let queue = DispatchQueue(label: "TimelineSegment") @@ -37,14 +44,16 @@ public class TimelineSegment { self.observer = try FetchedRecordsController(store.pool, sql: fullQuery, arguments: arguments, queue: queue) - observer.trackChanges { [weak self] observer in + self.observer.trackChanges { [weak self] observer in self?.needsUpdate() } + self.observer.trackErrors { observer, error in + os_log("FetchedRecordsController error: %@", type: .error, error.localizedDescription) + } queue.async { do { try self.observer.performFetch() - self.update() } catch { fatalError("OOPS: \(error)") } @@ -58,21 +67,17 @@ public class TimelineSegment { // MARK: - Result updating private func needsUpdate() { + _timelineItems = nil onMain { self.updateTimer?.invalidate() self.updateTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: false) { [weak self] _ in - self?.update() + self?.onUpdate?() } } } - private func update() { - var items: [TimelineItem] = [] - for row in observer.fetchedRecords { - items.append(store.item(for: row.row)) - } - self.timelineItems = items - onUpdate?() + private var updatedItems: [TimelineItem] { + return observer.fetchedRecords.map { store.item(for: $0.row) } } }