// // Measurements.swift // USB Meter // // Created by Bogdan Timofte on 07/05/2020. // Copyright © 2020 Bogdan Timofte. All rights reserved. // import Foundation import CoreGraphics class Measurements : ObservableObject { class Measurement : ObservableObject { struct Point : Identifiable , Hashable { enum Kind: Hashable { case sample case discontinuity } var id : Int var timestamp: Date var value: Double var kind: Kind = .sample var isSample: Bool { kind == .sample } var isDiscontinuity: Bool { kind == .discontinuity } func point() -> CGPoint { return CGPoint(x: timestamp.timeIntervalSince1970, y: value) } } var points: [Point] = [] var context = ChartContext() var samplePoints: [Point] { points.filter { $0.isSample } } private func rebuildContext() { context.reset() for point in points where point.isSample { context.include(point: point.point()) } } private func appendPoint(timestamp: Date, value: Double, kind: Point.Kind) { let newPoint = Measurement.Point(id: points.count, timestamp: timestamp, value: value, kind: kind) points.append(newPoint) if newPoint.isSample { context.include(point: newPoint.point()) } self.objectWillChange.send() } func removeValue(index: Int) { points.remove(at: index) for index in points.indices { points[index].id = index } rebuildContext() self.objectWillChange.send() } func addPoint(timestamp: Date, value: Double) { appendPoint(timestamp: timestamp, value: value, kind: .sample) } func addDiscontinuity(timestamp: Date) { guard !points.isEmpty else { return } guard points.last?.isDiscontinuity == false else { return } appendPoint(timestamp: timestamp, value: points.last?.value ?? 0, kind: .discontinuity) } func resetSeries() { points.removeAll() context.reset() self.objectWillChange.send() } func trim(before cutoff: Date) { points = points .filter { $0.timestamp >= cutoff } .enumerated() .map { index, point in Measurement.Point(id: index, timestamp: point.timestamp, value: point.value, kind: point.kind) } rebuildContext() self.objectWillChange.send() } } @Published var power = Measurement() @Published var voltage = Measurement() @Published var current = Measurement() private var pendingBucketSecond: Int? private var pendingBucketTimestamp: Date? private var itemsInSum: Double = 0 private var powerSum: Double = 0 private var voltageSum: Double = 0 private var currentSum: Double = 0 private func resetPendingAggregation() { pendingBucketSecond = nil pendingBucketTimestamp = nil itemsInSum = 0 powerSum = 0 voltageSum = 0 currentSum = 0 } private func flushPendingValues() { guard let pendingBucketTimestamp, itemsInSum > 0 else { return } self.power.addPoint(timestamp: pendingBucketTimestamp, value: powerSum / itemsInSum) self.voltage.addPoint(timestamp: pendingBucketTimestamp, value: voltageSum / itemsInSum) self.current.addPoint(timestamp: pendingBucketTimestamp, value: currentSum / itemsInSum) resetPendingAggregation() self.objectWillChange.send() } func resetSeries() { power.resetSeries() voltage.resetSeries() current.resetSeries() resetPendingAggregation() self.objectWillChange.send() } func reset() { resetSeries() } func remove(at idx: Int) { power.removeValue(index: idx) voltage.removeValue(index: idx) current.removeValue(index: idx) self.objectWillChange.send() } func trim(before cutoff: Date) { flushPendingValues() power.trim(before: cutoff) voltage.trim(before: cutoff) current.trim(before: cutoff) self.objectWillChange.send() } func addValues(timestamp: Date, power: Double, voltage: Double, current: Double) { let valuesTimestamp = timestamp.timeIntervalSinceReferenceDate.intValue if pendingBucketSecond == valuesTimestamp { pendingBucketTimestamp = timestamp itemsInSum += 1 powerSum += power voltageSum += voltage currentSum += current return } flushPendingValues() pendingBucketSecond = valuesTimestamp pendingBucketTimestamp = timestamp itemsInSum = 1 powerSum = power voltageSum = voltage currentSum = current } func markDiscontinuity(at timestamp: Date) { flushPendingValues() power.addDiscontinuity(timestamp: timestamp) voltage.addDiscontinuity(timestamp: timestamp) current.addDiscontinuity(timestamp: timestamp) self.objectWillChange.send() } }