diff --git a/XIONControlPanel.xcodeproj/project.pbxproj b/XIONControlPanel.xcodeproj/project.pbxproj index cfff33a..1da4f33 100644 --- a/XIONControlPanel.xcodeproj/project.pbxproj +++ b/XIONControlPanel.xcodeproj/project.pbxproj @@ -12,12 +12,14 @@ 0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */; }; 0C3CAEAF1C3350C800B856AD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEAE1C3350C800B856AD /* Assets.xcassets */; }; 0C3CAEB21C3350C800B856AD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C3CAEB01C3350C800B856AD /* LaunchScreen.storyboard */; }; - 0C41EF4A1C34B51500AF847F /* ArcadeMachinesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF491C34B51500AF847F /* ArcadeMachinesViewController.swift */; }; + 0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF491C34B51500AF847F /* SwitchesViewController.swift */; }; 0C41EF4E1C34B81F00AF847F /* KSIRingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF4D1C34B81F00AF847F /* KSIRingView.m */; }; 0C41EF501C34B97700AF847F /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF4F1C34B97700AF847F /* HeaderView.swift */; }; 0C41EF521C34BA4E00AF847F /* Orbitron-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0C41EF511C34BA4E00AF847F /* Orbitron-Medium.ttf */; }; 0C41EF541C34BC9800AF847F /* XIONLogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF531C34BC9800AF847F /* XIONLogoView.swift */; }; 0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EF551C35ABCD00AF847F /* ConnectionStatusView.swift */; }; + 0C4315001C35B1E700164559 /* WemoDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4314FF1C35B1E700164559 /* WemoDevice.swift */; }; + 0C4315061C35E67600164559 /* WemoDeviceCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4315051C35E67600164559 /* WemoDeviceCellView.swift */; }; 0C63A8731C337381007E4B52 /* CubletsTechnique.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0C63A8721C337381007E4B52 /* CubletsTechnique.plist */; }; 0C63A8761C3373D1007E4B52 /* HorizontalBlur.vsh in Resources */ = {isa = PBXBuildFile; fileRef = 0C63A8751C3373D1007E4B52 /* HorizontalBlur.vsh */; }; 0C63A8781C3373D9007E4B52 /* HorizontalBlur.fsh in Resources */ = {isa = PBXBuildFile; fileRef = 0C63A8771C3373D9007E4B52 /* HorizontalBlur.fsh */; }; @@ -28,6 +30,7 @@ 0C63A8851C33DC29007E4B52 /* VerticalBlur.vsh in Resources */ = {isa = PBXBuildFile; fileRef = 0C63A8831C33DC29007E4B52 /* VerticalBlur.vsh */; }; 0C63A8881C33EB3B007E4B52 /* FinalRender.fsh in Resources */ = {isa = PBXBuildFile; fileRef = 0C63A8861C33EB3B007E4B52 /* FinalRender.fsh */; }; 0C63A8891C33EB3B007E4B52 /* FinalRender.vsh in Resources */ = {isa = PBXBuildFile; fileRef = 0C63A8871C33EB3B007E4B52 /* FinalRender.vsh */; }; + 0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -38,13 +41,15 @@ 0C3CAEAE1C3350C800B856AD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 0C3CAEB11C3350C800B856AD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 0C3CAEB31C3350C800B856AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 0C41EF491C34B51500AF847F /* ArcadeMachinesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArcadeMachinesViewController.swift; sourceTree = ""; }; + 0C41EF491C34B51500AF847F /* SwitchesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchesViewController.swift; sourceTree = ""; }; 0C41EF4C1C34B81F00AF847F /* KSIRingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KSIRingView.h; sourceTree = ""; }; 0C41EF4D1C34B81F00AF847F /* KSIRingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSIRingView.m; sourceTree = ""; }; 0C41EF4F1C34B97700AF847F /* HeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; 0C41EF511C34BA4E00AF847F /* Orbitron-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Orbitron-Medium.ttf"; sourceTree = ""; }; 0C41EF531C34BC9800AF847F /* XIONLogoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XIONLogoView.swift; sourceTree = ""; }; 0C41EF551C35ABCD00AF847F /* ConnectionStatusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionStatusView.swift; sourceTree = ""; }; + 0C4314FF1C35B1E700164559 /* WemoDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WemoDevice.swift; sourceTree = ""; }; + 0C4315051C35E67600164559 /* WemoDeviceCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WemoDeviceCellView.swift; sourceTree = ""; }; 0C63A8721C337381007E4B52 /* CubletsTechnique.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = CubletsTechnique.plist; sourceTree = ""; }; 0C63A8751C3373D1007E4B52 /* HorizontalBlur.vsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = HorizontalBlur.vsh; sourceTree = ""; }; 0C63A8771C3373D9007E4B52 /* HorizontalBlur.fsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = HorizontalBlur.fsh; sourceTree = ""; }; @@ -57,6 +62,7 @@ 0C63A8831C33DC29007E4B52 /* VerticalBlur.vsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = VerticalBlur.vsh; sourceTree = ""; }; 0C63A8861C33EB3B007E4B52 /* FinalRender.fsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = FinalRender.fsh; sourceTree = ""; }; 0C63A8871C33EB3B007E4B52 /* FinalRender.vsh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.glsl; path = FinalRender.vsh; sourceTree = ""; }; + 0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchIndicatorView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -93,7 +99,7 @@ 0C20E2B61C33514100BA789B /* Controllers */ = { isa = PBXGroup; children = ( - 0C41EF491C34B51500AF847F /* ArcadeMachinesViewController.swift */, + 0C41EF491C34B51500AF847F /* SwitchesViewController.swift */, 0C20E2BA1C33529700BA789B /* MainViewController.swift */, 0C20E2BC1C33534F00BA789B /* VisualizationViewController.swift */, ); @@ -103,6 +109,7 @@ 0C20E2B71C33514100BA789B /* Models */ = { isa = PBXGroup; children = ( + 0C4314FF1C35B1E700164559 /* WemoDevice.swift */, ); path = Models; sourceTree = ""; @@ -112,7 +119,9 @@ children = ( 0C41EF551C35ABCD00AF847F /* ConnectionStatusView.swift */, 0C41EF4F1C34B97700AF847F /* HeaderView.swift */, + 0CF9A48D1C35E9C800276F13 /* SwitchIndicatorView.swift */, 0C41EF531C34BC9800AF847F /* XIONLogoView.swift */, + 0C4315051C35E67600164559 /* WemoDeviceCellView.swift */, ); path = Views; sourceTree = ""; @@ -276,11 +285,14 @@ 0C41EF561C35ABCD00AF847F /* ConnectionStatusView.swift in Sources */, 0C63A87D1C33C655007E4B52 /* NSValue+XIONAdditions.m in Sources */, 0C20E2BD1C33534F00BA789B /* VisualizationViewController.swift in Sources */, - 0C41EF4A1C34B51500AF847F /* ArcadeMachinesViewController.swift in Sources */, + 0C41EF4A1C34B51500AF847F /* SwitchesViewController.swift in Sources */, 0C20E2B91C33514100BA789B /* AppDelegate.swift in Sources */, 0C20E2BB1C33529700BA789B /* MainViewController.swift in Sources */, 0C41EF501C34B97700AF847F /* HeaderView.swift in Sources */, 0C41EF541C34BC9800AF847F /* XIONLogoView.swift in Sources */, + 0C4315061C35E67600164559 /* WemoDeviceCellView.swift in Sources */, + 0CF9A48E1C35E9C800276F13 /* SwitchIndicatorView.swift in Sources */, + 0C4315001C35B1E700164559 /* WemoDevice.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/XIONControlPanel/Controllers/ArcadeMachinesViewController.swift b/XIONControlPanel/Controllers/ArcadeMachinesViewController.swift deleted file mode 100644 index ddca4cc..0000000 --- a/XIONControlPanel/Controllers/ArcadeMachinesViewController.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ArcadeMachinesViewController.swift -// XIONControlPanel -// -// Created by Charles Magahern on 12/30/15. -// Copyright © 2015 XION. All rights reserved. -// - -import UIKit - -class ArcadeMachinesViewController: UIViewController { - override func viewDidLoad() - { - super.viewDidLoad() - } -} diff --git a/XIONControlPanel/Controllers/MainViewController.swift b/XIONControlPanel/Controllers/MainViewController.swift index fd62f9f..fa5bb4a 100644 --- a/XIONControlPanel/Controllers/MainViewController.swift +++ b/XIONControlPanel/Controllers/MainViewController.swift @@ -10,7 +10,7 @@ import UIKit class MainViewController: UIViewController { private var _visualizationController: VisualizationViewController = VisualizationViewController() - private var _machinesController: ArcadeMachinesViewController = ArcadeMachinesViewController() + private var _switchesController: SwitchesViewController = SwitchesViewController() private var _headerView: HeaderView = HeaderView() // MARK: Overrides @@ -24,8 +24,8 @@ class MainViewController: UIViewController { self.addChildViewController(_visualizationController) self.view.addSubview(_visualizationController.view) - self.addChildViewController(_machinesController) - self.view.addSubview(_machinesController.view) + self.addChildViewController(_switchesController) + self.view.addSubview(_switchesController.view) self.view.addSubview(_headerView) } @@ -53,18 +53,18 @@ class MainViewController: UIViewController { let visualizationFrame = CGRect( x: bodyBounds.origin.x, y: bodyBounds.origin.y, - width: rint((5.0 / 8.0) * bodyBounds.size.width), + width: rint(0.6 * bodyBounds.size.width), height: bodyBounds.size.height ) _visualizationController.view.frame = visualizationFrame - let machinesControllerFrame = CGRect( + let switchesControllerFrame = CGRect( x: CGRectGetMaxX(visualizationFrame), y: bodyBounds.origin.y, width: bodyBounds.size.width - visualizationFrame.size.width, height: bodyBounds.size.height ) - _machinesController.view.frame = machinesControllerFrame + _switchesController.view.frame = switchesControllerFrame } override func viewDidAppear(animated: Bool) diff --git a/XIONControlPanel/Controllers/SwitchesViewController.swift b/XIONControlPanel/Controllers/SwitchesViewController.swift new file mode 100644 index 0000000..5dc1370 --- /dev/null +++ b/XIONControlPanel/Controllers/SwitchesViewController.swift @@ -0,0 +1,74 @@ +// +// SwitchesViewController.swift +// XIONControlPanel +// +// Created by Charles Magahern on 12/30/15. +// Copyright © 2015 XION. All rights reserved. +// + +import Darwin +import Foundation +import UIKit + +class SwitchesViewController: UIViewController, + UICollectionViewDataSource, + UICollectionViewDelegateFlowLayout +{ + private var _collectionView: UICollectionView = UICollectionView(frame: CGRectZero, + collectionViewLayout: UICollectionViewFlowLayout()) + + static private let collectionViewCellReuseIdentifier = "SwitchesCollectionViewReuseID" + static private let collectionViewCellsSpacing: CGFloat = 5.0 + static private let collectionViewCellsPerRow = 3 + + override func viewDidLoad() + { + super.viewDidLoad() + + let reuseID = SwitchesViewController.collectionViewCellReuseIdentifier + let layout = _collectionView.collectionViewLayout as! UICollectionViewFlowLayout + layout.scrollDirection = .Vertical + layout.minimumLineSpacing = SwitchesViewController.collectionViewCellsSpacing + layout.minimumInteritemSpacing = SwitchesViewController.collectionViewCellsSpacing + + _collectionView.backgroundColor = UIColor.blackColor() + _collectionView.delegate = self + _collectionView.dataSource = self + _collectionView.registerClass(WemoDeviceCellView.self, forCellWithReuseIdentifier: reuseID) + self.view.addSubview(_collectionView) + } + + override func viewDidLayoutSubviews() + { + super.viewDidLayoutSubviews() + + let bounds = self.view.bounds + _collectionView.frame = bounds + } + + // MARK: UICollectionView + + func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int + { + return 10 + } + + func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell + { + let reuseID = SwitchesViewController.collectionViewCellReuseIdentifier + let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseID, forIndexPath: indexPath) as! WemoDeviceCellView + cell.ordinal = indexPath.row + 1 + + return cell + } + + func collectionView(collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize + { + let spacing = SwitchesViewController.collectionViewCellsSpacing + let cellsPerRow = CGFloat(SwitchesViewController.collectionViewCellsPerRow) + let dimensions = rint(collectionView.bounds.size.width / cellsPerRow) - spacing + return CGSize(width: dimensions, height: dimensions) + } +} diff --git a/XIONControlPanel/Models/WemoDevice.swift b/XIONControlPanel/Models/WemoDevice.swift new file mode 100644 index 0000000..8a5239f --- /dev/null +++ b/XIONControlPanel/Models/WemoDevice.swift @@ -0,0 +1,62 @@ +// +// WemoDevice.swift +// XIONControlPanel +// +// Created by Charles Magahern on 12/31/15. +// Copyright © 2015 XION. All rights reserved. +// + +import Foundation + +struct WemoDevice { + enum State { + case Off + case On + } + + enum Type { + case Switch + } + + var name: String = "" + var host: String = "" + var model: String = "" + var state: State = .Off + var type: Type = .Switch + var serial: String = "" + + init() + {} + + init(_ responseData: NSData) + { + if let dict = (try? NSJSONSerialization.JSONObjectWithData(responseData, options: NSJSONReadingOptions())) as? NSDictionary { + if let name = dict["name"] as? NSString { + self.name = String(name) + } + + if let host = dict["host"] as? NSString { + self.host = String(host) + } + + if let model = dict["model"] as? NSString { + self.model = String(model) + } + + if let state = dict["state"] as? NSNumber { + switch (state.integerValue) { + case 0: + self.state = .Off + case 1: + self.state = .On + default: + self.state = .Off + } + } + + if let serial = dict["serialnumber"] as? NSString { + self.serial = String(serial) + } + } + } +} diff --git a/XIONControlPanel/Views/SwitchIndicatorView.swift b/XIONControlPanel/Views/SwitchIndicatorView.swift new file mode 100644 index 0000000..dbc3eee --- /dev/null +++ b/XIONControlPanel/Views/SwitchIndicatorView.swift @@ -0,0 +1,105 @@ +// +// SwitchIndicatorView.swift +// XIONControlPanel +// +// Created by Charles Magahern on 12/31/15. +// Copyright © 2015 XION. All rights reserved. +// + +import UIKit + +class SwitchIndicatorView: UIView { + private var _status: Bool = false + private var _foregroundColor: UIColor = UIColor.whiteColor() + private var _outerCircleLayer: CAShapeLayer = CAShapeLayer() + private var _offSymbolLayer: CAShapeLayer = CAShapeLayer() + private var _onSymbolLayer: CAShapeLayer = CAShapeLayer() + + static private var lineWidth: CGFloat = 2.0 + + override init(frame: CGRect) + { + super.init(frame: frame) + + _outerCircleLayer.fillColor = UIColor.clearColor().CGColor + _outerCircleLayer.lineWidth = SwitchIndicatorView.lineWidth + + _offSymbolLayer.fillColor = UIColor.clearColor().CGColor + _offSymbolLayer.lineWidth = SwitchIndicatorView.lineWidth + + self.layer.addSublayer(_outerCircleLayer) + self.layer.addSublayer(_offSymbolLayer) + self.layer.addSublayer(_onSymbolLayer) + + self.status = false + self.foregroundColor = UIColor.whiteColor() + } + + required init?(coder aDecoder: NSCoder) + { + fatalError("unsupported") + } + + override func layoutSubviews() + { + super.layoutSubviews() + + let bounds = self.bounds + let lineWidth = SwitchIndicatorView.lineWidth + + _outerCircleLayer.frame = bounds + _offSymbolLayer.frame = bounds + _onSymbolLayer.frame = bounds + + _outerCircleLayer.path = CGPathCreateWithEllipseInRect(bounds, nil) + + let innerSize = CGSize(width: rint(bounds.size.width / 2.0), height: rint(bounds.size.height / 2.0)) + let innerBounds = CGRect( + x: rint(bounds.size.width / 2.0 - innerSize.width / 2.0), + y: rint(bounds.size.height / 2.0 - innerSize.height / 2.0), + width: innerSize.width, + height: innerSize.height + ) + _offSymbolLayer.path = CGPathCreateWithEllipseInRect(innerBounds, nil) + + let onSymbolSize = CGSize(width: lineWidth, height: innerSize.height) + let onSymbolRect = CGRect( + x: rint(bounds.size.width / 2.0 - onSymbolSize.width / 2.0), + y: rint(bounds.size.height / 2.0 - onSymbolSize.height / 2.0), + width: onSymbolSize.width, + height: onSymbolSize.height + ) + _onSymbolLayer.path = CGPathCreateWithRect(onSymbolRect, nil) + } + + // MARK: API + + var status: Bool { + get + { + return _status + } + + set(status) + { + _status = status + _offSymbolLayer.hidden = _status + _onSymbolLayer.hidden = !_status + } + } + + var foregroundColor: UIColor { + get + { + return _foregroundColor + } + + set(color) + { + _foregroundColor = color + _outerCircleLayer.strokeColor = _foregroundColor.CGColor + _offSymbolLayer.strokeColor = _foregroundColor.CGColor + _onSymbolLayer.fillColor = _foregroundColor.CGColor + } + } +} diff --git a/XIONControlPanel/Views/WemoDeviceCellView.swift b/XIONControlPanel/Views/WemoDeviceCellView.swift new file mode 100644 index 0000000..0de5d34 --- /dev/null +++ b/XIONControlPanel/Views/WemoDeviceCellView.swift @@ -0,0 +1,105 @@ +// +// WemoDeviceCellView.swift +// XIONControlPanel +// +// Created by Charles Magahern on 12/31/15. +// Copyright © 2015 XION. All rights reserved. +// + +import UIKit + +class WemoDeviceCellView: UICollectionViewCell { + private var _device: WemoDevice? + private var _ordinal: Int = 0 + private var _nameLabel: UILabel = UILabel() + private var _ordinalLabel: UILabel = UILabel() + private var _indicator: SwitchIndicatorView = SwitchIndicatorView() + + override init(frame: CGRect) + { + super.init(frame: frame) + + let annotationsColor = UIColor.darkGrayColor() + self.backgroundColor = UIColor(white: 0.2, alpha: 1.0) + + _nameLabel.font = UIFont(name: "Orbitron-Medium", size: 16.0) + _nameLabel.numberOfLines = 3 + _nameLabel.textColor = UIColor.whiteColor() + _nameLabel.text = "beatmania IIDX".uppercaseString + self.addSubview(_nameLabel) + + _ordinalLabel.font = UIFont(name: "Orbitron-Medium", size: 21.0) + _ordinalLabel.textColor = annotationsColor + _ordinalLabel.text = "00" + self.addSubview(_ordinalLabel) + + _indicator.foregroundColor = annotationsColor + self.addSubview(_indicator) + } + + required init?(coder aDecoder: NSCoder) + { + fatalError("unsupported") + } + + // MARK: Overrides + + override func layoutSubviews() + { + super.layoutSubviews() + + let bounds = self.bounds + let padding: CGFloat = 10.0 + + _indicator.frame = CGRect( + x: padding, + y: padding, + width: 20.0, + height: 20.0 + ) + + let nameLabelSize = _nameLabel.sizeThatFits(CGSize(width: bounds.size.width - padding * 2.0, height: bounds.size.height - padding)) + _nameLabel.frame = CGRect( + x: padding, + y: bounds.size.height - nameLabelSize.height - padding, + width: nameLabelSize.width, + height: nameLabelSize.height + ) + + let ordinalLabelSize = _ordinalLabel.sizeThatFits(bounds.size) + _ordinalLabel.frame = CGRect( + x: bounds.size.width - ordinalLabelSize.width - padding, + y: padding, + width: ordinalLabelSize.width, + height: ordinalLabelSize.height + ) + } + + // MARK: API + + var device: WemoDevice? { + get + { + return _device + } + + set(device) + { + _device = device + _nameLabel.text = _device?.name + } + } + + var ordinal: Int { + get + { + return _ordinal + } + + set(ordinal) + { + _ordinal = ordinal + _ordinalLabel.text = String(format: "%.2d", _ordinal) + } + } +}