287 lines
11 KiB
Swift
287 lines
11 KiB
Swift
//
|
|
// GraphView.swift
|
|
// Docsis Toolkit
|
|
//
|
|
// Created by Kilian Hofmann on 20.06.17.
|
|
// Copyright © 2017 Kilian Hofmann. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
|
|
class FrequencyGraph: NSView {
|
|
let opQueue: OperationQueue = OperationQueue()
|
|
// Constants
|
|
static let graphSizeWithSeparator: Int = 109
|
|
static let graphSize: Int = 108
|
|
static let maxGraphSize: Int = 17300
|
|
let zeroOffset: Double = 37
|
|
// Path related
|
|
var path: NSBezierPath = NSBezierPath.init()
|
|
var threshold: NSBezierPath = NSBezierPath.init()
|
|
var upstream: Bool = false
|
|
var lastTime: Int = 0
|
|
var lastTimeThreshold: Int = 0
|
|
var timePaths: [NSBezierPath] = []
|
|
var lossPaths: [NSBezierPath] = []
|
|
var offset: Int = 0
|
|
// Popover
|
|
let popover: NSPopover = NSPopover()
|
|
// Data
|
|
var data: [String] = []
|
|
var dataLoss: [String] = []
|
|
var frequency: Double = 0.0
|
|
// Cursor
|
|
var cursor: NSBezierPath = NSBezierPath.init()
|
|
// Superview
|
|
var content: FlippedView!
|
|
// Indicator
|
|
var indicator: NSProgressIndicator!
|
|
|
|
init(upstream: Bool, frame: CGRect, superview: FlippedView, indicator: NSProgressIndicator) {
|
|
super.init(frame: frame)
|
|
self.upstream = upstream
|
|
self.indicator = indicator
|
|
|
|
let tracking: NSTrackingArea = NSTrackingArea(rect: self.bounds, options: [.mouseEnteredAndExited, .mouseMoved, .activeInActiveApp, .inVisibleRect], owner: self, userInfo: nil)
|
|
self.addTrackingArea(tracking)
|
|
|
|
popover.contentViewController = GraphDetailsController()
|
|
|
|
self.content = superview
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
}
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
|
|
NSColor.white.setFill()
|
|
__NSRectFill(dirtyRect)
|
|
|
|
threshold.lineWidth = 0.5
|
|
|
|
if upstream {
|
|
NSColor.red.set()
|
|
threshold.stroke()
|
|
} else {
|
|
NSColor.blue.set()
|
|
threshold.stroke()
|
|
}
|
|
|
|
NSColor.black.set()
|
|
path.lineWidth = 1
|
|
path.stroke()
|
|
|
|
NSColor(red: 0, green: 0, blue: 0, alpha: 0.3).set()
|
|
cursor.lineWidth = 2
|
|
cursor.stroke()
|
|
|
|
for index in 0..<timePaths.count {
|
|
timePaths[index].lineWidth = 0.5
|
|
timePaths[index].stroke()
|
|
}
|
|
|
|
for index in 0..<lossPaths.count {
|
|
NSColor(red: 1, green: 0, blue: 0, alpha: 0.2).set()
|
|
lossPaths[index].lineWidth = 2
|
|
lossPaths[index].stroke()
|
|
}
|
|
}
|
|
|
|
override var isFlipped:Bool {
|
|
get {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func initView(operation: GraphLoadOperation) {
|
|
// Setup Indicator
|
|
indicator.doubleValue = 0
|
|
indicator.isHidden = false
|
|
indicator.minValue = 0
|
|
indicator.maxValue = Double(data.count)
|
|
// Zero line
|
|
let zero: NSBezierPath = NSBezierPath()
|
|
zero.move(to: NSPoint(x: 5, y: zeroOffset))
|
|
zero.line(to: NSPoint(x: Int(FrequencyGraph.maxGraphSize-5), y: Int(zeroOffset)))
|
|
self.timePaths.append(zero)
|
|
// Add data points
|
|
var lastWasSkipped: Bool = false
|
|
//
|
|
var lastLossIndex: Int = 0
|
|
for point in 0..<data.count {
|
|
if operation.isCancelled {
|
|
return
|
|
}
|
|
// Calulate offsets if start
|
|
if point == 0 {
|
|
// Get offset
|
|
offset = 0
|
|
let time: String = data[point].components(separatedBy: ";")[0]
|
|
let hour: Int = Int(time[..<time.index(time.startIndex, offsetBy: 2)])!
|
|
let tensSeconds: Int = Int((time as NSString).substring(with: NSMakeRange(6, 1)))!
|
|
offset = ((hour*60 + Int((time as NSString).substring(with: NSMakeRange(3, 2)))!) * 6 + tensSeconds) * 2
|
|
// Set offset
|
|
lastTime = offset
|
|
lastTimeThreshold = offset
|
|
}
|
|
// Get values
|
|
let level: Double? = Double(data[point].components(separatedBy: ";")[1])
|
|
let threshold: Double? = Double(data[point].components(separatedBy: ";")[2])
|
|
// Set path, skip if empty values (-)
|
|
if level == nil || threshold == nil {
|
|
skipPath()
|
|
lastWasSkipped = true
|
|
} else {
|
|
if lastWasSkipped {
|
|
restartPath(level!, threshold_level: threshold!)
|
|
lastWasSkipped = false
|
|
} else {
|
|
addPath(level!, threshold_level: threshold!)
|
|
}
|
|
}
|
|
// Set connection loss zones
|
|
for loss in lastLossIndex..<dataLoss.count {
|
|
let time1: String = data[point].components(separatedBy: ";")[0]
|
|
let hour1: Int = Int(time1[..<time1.index(time1.startIndex, offsetBy: 2)])!
|
|
let minute1: Int = Int((time1 as NSString).substring(with: NSMakeRange(3, 2)))!
|
|
let time2: String = dataLoss[loss]
|
|
let hour2: Int = Int(time2[..<time2.index(time2.startIndex, offsetBy: 2)])!
|
|
let minute2: Int = Int((time2 as NSString).substring(with: NSMakeRange(3, 2)))!
|
|
|
|
if hour1 < hour2 { break }
|
|
else if hour1 == hour2 && minute1 < minute2 { break }
|
|
|
|
if hour1 == hour2 && minute1 == minute2 {
|
|
let lossPath: NSBezierPath = NSBezierPath()
|
|
lossPath.move(to: NSPoint(x: CGFloat(10 + point*2 + offset), y: CGFloat(17)))
|
|
lossPath.line(to: NSPoint(x: CGFloat(10 + point*2 + offset), y: CGFloat(FrequencyGraph.graphSize)))
|
|
lossPaths.append(lossPath)
|
|
lastLossIndex = loss
|
|
}
|
|
}
|
|
// Set time scale
|
|
if (point+offset/2) % 60 == 0 {
|
|
// Time label
|
|
let time: NSTextField = NSTextField(frame: NSMakeRect(CGFloat(10 + point*2 + offset), 0, 57, 17))
|
|
time.drawsBackground = false
|
|
time.isBezeled = false
|
|
time.isBordered = false
|
|
time.isSelectable = false
|
|
time.stringValue = data[point].components(separatedBy: ";")[0]
|
|
addSubview(time)
|
|
// Time line
|
|
let timePath: NSBezierPath = NSBezierPath()
|
|
timePath.move(to: NSPoint(x: CGFloat(10 + point*2 + offset), y: CGFloat(5)))
|
|
timePath.line(to: NSPoint(x: CGFloat(10 + point*2 + offset), y: CGFloat(107)))
|
|
timePaths.append(timePath)
|
|
}
|
|
if (point+offset/2) % 100 == 0 {
|
|
OperationQueue.main.addOperation {
|
|
self.indicator.increment(by: 100)
|
|
}
|
|
}
|
|
}
|
|
// Put view in display
|
|
OperationQueue.main.addOperation {
|
|
|
|
self.indicator.isHidden = true
|
|
self.content.addSubview(self)
|
|
self.setNeedsDisplay(self.bounds)
|
|
}
|
|
OperationQueue.main.waitUntilAllOperationsAreFinished()
|
|
|
|
}
|
|
|
|
// MARK: - Path related
|
|
|
|
func addPath(_ level: Double, threshold_level: Double) {
|
|
if path.elementCount == 0 {
|
|
path.move(to: NSMakePoint(CGFloat(10 + lastTime), CGFloat(level + zeroOffset)))
|
|
} else {
|
|
path.line(to: NSMakePoint(CGFloat(10 + lastTime), CGFloat(level + zeroOffset)))
|
|
}
|
|
|
|
lastTime += 2
|
|
|
|
if threshold.elementCount == 0 {
|
|
threshold.move(to: NSMakePoint(CGFloat(10 + lastTimeThreshold), CGFloat(threshold_level + zeroOffset)))
|
|
} else {
|
|
threshold.line(to: NSMakePoint(CGFloat(10 + lastTimeThreshold), CGFloat(threshold_level + zeroOffset)))
|
|
}
|
|
|
|
lastTimeThreshold += 2
|
|
}
|
|
|
|
func skipPath() {
|
|
path.move(to: NSMakePoint(CGFloat(10 + lastTime), CGFloat(zeroOffset)))
|
|
lastTime += 2
|
|
threshold.move(to: NSMakePoint(CGFloat(10 + lastTimeThreshold), CGFloat(zeroOffset)))
|
|
lastTimeThreshold += 2
|
|
}
|
|
|
|
func restartPath(_ level: Double, threshold_level: Double) {
|
|
path.move(to: NSMakePoint(CGFloat(10 + lastTime), CGFloat(level + 37)))
|
|
lastTime += 2
|
|
threshold.move(to: NSMakePoint(CGFloat(10 + lastTimeThreshold), CGFloat(threshold_level + zeroOffset)))
|
|
lastTimeThreshold += 2
|
|
}
|
|
|
|
// MARK: - Mouse Events
|
|
|
|
override func mouseMoved(with event: NSEvent) {
|
|
cursor.removeAllPoints()
|
|
popover.performClose(self)
|
|
|
|
let x: Int = Int(event.locationInWindow.x - 20 + (self.superview!.superview!.superview! as! NSScrollView).documentVisibleRect.origin.x)
|
|
|
|
cursor.move(to: NSPoint(x: x - x%2, y: 0))
|
|
cursor.line(to: NSPoint(x: x - x%2, y: FrequencyGraph.graphSize))
|
|
|
|
setNeedsDisplay(self.bounds)
|
|
}
|
|
|
|
override func mouseExited(with event: NSEvent) {
|
|
cursor.removeAllPoints()
|
|
popover.performClose(self)
|
|
|
|
setNeedsDisplay(self.bounds)
|
|
}
|
|
|
|
override func mouseDown(with event: NSEvent) {
|
|
let x: Int = Int(event.locationInWindow.x - 20 + (self.superview!.superview!.superview! as! NSScrollView).documentVisibleRect.origin.x)
|
|
if 10 <= x && (x-10)/2 - offset/2 < data.count && (x-10)/2 - offset/2 >= 0 {
|
|
|
|
(popover.contentViewController! as! GraphDetailsController).freq_str = "\(roundToPlaces(1, input: frequency)) MHz"
|
|
|
|
(popover.contentViewController! as! GraphDetailsController).time_str = "\(data[(x-10) / 2 - offset / 2].components(separatedBy: ";")[0])"
|
|
(popover.contentViewController! as! GraphDetailsController).power_str = "\(data[(x-10) / 2 - offset / 2].components(separatedBy: ";")[1]) dBmV"
|
|
|
|
if upstream {
|
|
(popover.contentViewController! as! GraphDetailsController).snr_str = "\(data[(x-10) / 2 - offset / 2].components(separatedBy: ";")[2]) dBmV"
|
|
(popover.contentViewController! as! GraphDetailsController).ranging_str = "\(data[(x-10) / 2 - offset / 2].components(separatedBy: ";")[3])"
|
|
} else {
|
|
(popover.contentViewController! as! GraphDetailsController).snr_str = "\(data[(x-10) / 2 - offset / 2].components(separatedBy: ";")[2]) dB"
|
|
}
|
|
|
|
popover.show(relativeTo: NSMakeRect(CGFloat(x), 0, 2, CGFloat(FrequencyGraph.graphSize)), of: self, preferredEdge: .minY)
|
|
}
|
|
}
|
|
|
|
override func scrollWheel(with event: NSEvent) {
|
|
super.scrollWheel(with: event)
|
|
cursor.removeAllPoints()
|
|
popover.performClose(self)
|
|
|
|
setNeedsDisplay(self.bounds)
|
|
}
|
|
|
|
// MARK: - Helper
|
|
|
|
func roundToPlaces(_ places:Int, input: Double) -> Double {
|
|
let divisor = pow(10.0, Double(places))
|
|
return round(input * divisor) / divisor
|
|
}
|
|
}
|