diff --git a/example/generator/bundle.json b/example/generator/bundle.json index a13bbdc8f8..cdb4736957 100644 --- a/example/generator/bundle.json +++ b/example/generator/bundle.json @@ -34,6 +34,10 @@ { "key": "time", "name": "Time" + }, + { + "key": "yesterday", + "name": "Yesterday" } ], "ranges": [ @@ -61,4 +65,4 @@ } ] } -} \ No newline at end of file +} diff --git a/example/generator/src/SinewaveTelemetrySeries.js b/example/generator/src/SinewaveTelemetrySeries.js index 5b7914a867..1e84034766 100644 --- a/example/generator/src/SinewaveTelemetrySeries.js +++ b/example/generator/src/SinewaveTelemetrySeries.js @@ -29,23 +29,25 @@ define( function () { "use strict"; - var firstObservedTime = Math.floor(Date.now() / 1000); + var ONE_DAY = 60 * 60 * 24, + firstObservedTime = Math.floor(Date.now() / 1000) - ONE_DAY; /** * * @constructor */ function SinewaveTelemetrySeries(request) { - var latestObservedTime = Math.floor(Date.now() / 1000), + var timeOffset = (request.domain === 'yesterday') ? ONE_DAY : 0, + latestTime = Math.floor(Date.now() / 1000) - timeOffset, + firstTime = firstObservedTime - timeOffset, endTime = (request.end !== undefined) ? - Math.floor(request.end / 1000) : latestObservedTime, - count = - Math.min(endTime, latestObservedTime) - firstObservedTime, - period = request.period || 30, + Math.floor(request.end / 1000) : latestTime, + count = Math.min(endTime, latestTime) - firstTime, + period = +request.period || 30, generatorData = {}, - offset = (request.start !== undefined) ? - Math.floor(request.start / 1000) - firstObservedTime : - 0; + requestStart = (request.start === undefined) ? firstTime : + Math.max(Math.floor(request.start / 1000), firstTime), + offset = requestStart - firstTime; if (request.size !== undefined) { offset = Math.max(offset, count - request.size); @@ -56,8 +58,8 @@ define( }; generatorData.getDomainValue = function (i, domain) { - return (i + offset) * 1000 + - (domain !== 'delta' ? (firstObservedTime * 1000) : 0); + return (i + offset) * 1000 + firstTime * 1000 - + (domain === 'yesterday' ? ONE_DAY : 0); }; generatorData.getRangeValue = function (i, range) { diff --git a/platform/features/conductor/bundle.json b/platform/features/conductor/bundle.json index 2e4a7c652f..de903cfb93 100644 --- a/platform/features/conductor/bundle.json +++ b/platform/features/conductor/bundle.json @@ -23,7 +23,23 @@ { "key": "conductorService", "implementation": "ConductorService.js", - "depends": [ "now" ] + "depends": [ "now", "TIME_CONDUCTOR_DOMAINS" ] + } + ], + "templates": [ + { + "key": "time-conductor", + "templateUrl": "templates/time-conductor.html" + } + ], + "constants": [ + { + "key": "TIME_CONDUCTOR_DOMAINS", + "value": [ + { "key": "time", "name": "Time" }, + { "key": "yesterday", "name": "Yesterday" } + ], + "comment": "Placeholder; to be replaced by inspection of available domains." } ] } diff --git a/platform/features/conductor/res/templates/time-conductor.html b/platform/features/conductor/res/templates/time-conductor.html new file mode 100644 index 0000000000..4126652d5b --- /dev/null +++ b/platform/features/conductor/res/templates/time-conductor.html @@ -0,0 +1,10 @@ + + + + diff --git a/platform/features/conductor/src/ConductorRepresenter.js b/platform/features/conductor/src/ConductorRepresenter.js index 9f9c6a291f..c6d18c9266 100644 --- a/platform/features/conductor/src/ConductorRepresenter.js +++ b/platform/features/conductor/src/ConductorRepresenter.js @@ -28,7 +28,7 @@ define( var CONDUCTOR_HEIGHT = "100px", TEMPLATE = [ - "", + "", "" ].join(''), THROTTLE_MS = 200, @@ -78,7 +78,8 @@ define( function bounds(start, end) { return { start: conductor.displayStart(), - end: conductor.displayEnd() + end: conductor.displayEnd(), + domain: conductor.domain() }; } @@ -89,12 +90,30 @@ define( } function updateConductorInner() { - conductor.displayStart(conductorScope.conductor.inner.start); - conductor.displayEnd(conductorScope.conductor.inner.end); + var innerBounds = conductorScope.ngModel.conductor.inner; + conductor.displayStart(innerBounds.start); + conductor.displayEnd(innerBounds.end); lastObservedBounds = lastObservedBounds || bounds(); broadcastBounds(); } + function updateDomain(value) { + conductor.domain(value); + repScope.$broadcast('telemetry:display:bounds', bounds( + conductor.displayStart(), + conductor.displayEnd(), + conductor.domain() + )); + } + + // telemetry domain metadata -> option for a select control + function makeOption(domainOption) { + return { + name: domainOption.name, + value: domainOption.key + }; + } + broadcastBounds = this.throttle(function () { var newlyObservedBounds = bounds(); @@ -107,12 +126,19 @@ define( } }, THROTTLE_MS); - conductorScope.conductor = { outer: bounds(), inner: bounds() }; + conductorScope.ngModel = {}; + conductorScope.ngModel.conductor = + { outer: bounds(), inner: bounds() }; + conductorScope.ngModel.options = + conductor.domainOptions().map(makeOption); + conductorScope.ngModel.domain = conductor.domain(); conductorScope - .$watch('conductor.inner.start', updateConductorInner); + .$watch('ngModel.conductor.inner.start', updateConductorInner); conductorScope - .$watch('conductor.inner.end', updateConductorInner); + .$watch('ngModel.conductor.inner.end', updateConductorInner); + conductorScope + .$watch('ngModel.domain', updateDomain); repScope.$on('telemetry:view', updateConductorInner); }; diff --git a/platform/features/conductor/src/ConductorService.js b/platform/features/conductor/src/ConductorService.js index 59cfa95e3c..3e281d2c1d 100644 --- a/platform/features/conductor/src/ConductorService.js +++ b/platform/features/conductor/src/ConductorService.js @@ -39,12 +39,15 @@ define( * @param {Function} now a function which returns the current time * as a UNIX timestamp, in milliseconds */ - function ConductorService(now) { + function ConductorService(now, domains) { var initialEnd = Math.ceil(now() / SIX_HOURS_IN_MS) * SIX_HOURS_IN_MS; - this.conductor = - new TimeConductor(initialEnd - ONE_DAY_IN_MS, initialEnd); + this.conductor = new TimeConductor( + initialEnd - ONE_DAY_IN_MS, + initialEnd, + domains + ); } /** diff --git a/platform/features/conductor/src/ConductorTelemetryDecorator.js b/platform/features/conductor/src/ConductorTelemetryDecorator.js index ed16db7d1b..ab2d958d7e 100644 --- a/platform/features/conductor/src/ConductorTelemetryDecorator.js +++ b/platform/features/conductor/src/ConductorTelemetryDecorator.js @@ -44,12 +44,14 @@ define( ConductorTelemetryDecorator.prototype.amendRequests = function (requests) { var conductor = this.conductorService.getConductor(), start = conductor.displayStart(), - end = conductor.displayEnd(); + end = conductor.displayEnd(), + domain = conductor.domain(); function amendRequest(request) { request = request || {}; request.start = start; request.end = end; + request.domain = domain; return request; } diff --git a/platform/features/conductor/src/TimeConductor.js b/platform/features/conductor/src/TimeConductor.js index 394d9b01bb..0fa0403fd9 100644 --- a/platform/features/conductor/src/TimeConductor.js +++ b/platform/features/conductor/src/TimeConductor.js @@ -40,8 +40,10 @@ define( * @param {number} start the initial start time * @param {number} end the initial end time */ - function TimeConductor(start, end) { + function TimeConductor(start, end, domains) { this.range = { start: start, end: end }; + this.domains = domains; + this.activeDomain = domains[0].key; } /** @@ -68,6 +70,34 @@ define( return this.range.end; }; + /** + * Get available domain options which can be used to bound time + * selection. + * @returns {TelemetryDomain[]} available domains + */ + TimeConductor.prototype.domainOptions = function () { + return this.domains; + }; + + /** + * Get or set (if called with an argument) the active domain. + * @param {string} [key] the key identifying the domain choice + * @returns {TelemetryDomain} the active telemetry domain + */ + TimeConductor.prototype.domain = function (key) { + function matchesKey(domain) { + return domain.key === key; + } + + if (arguments.length > 0) { + if (!this.domains.some(matchesKey)) { + throw new Error("Unknown domain " + key); + } + this.activeDomain = key; + } + return this.activeDomain; + }; + return TimeConductor; } ); diff --git a/platform/features/conductor/test/ConductorRepresenterSpec.js b/platform/features/conductor/test/ConductorRepresenterSpec.js index 59fae1b4ee..5d78c8a720 100644 --- a/platform/features/conductor/test/ConductorRepresenterSpec.js +++ b/platform/features/conductor/test/ConductorRepresenterSpec.js @@ -21,12 +21,9 @@ *****************************************************************************/ /*global define,describe,it,expect,beforeEach,waitsFor,afterEach,jasmine*/ -/** - * EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015. - */ define( - ["../src/ConductorRepresenter"], - function (ConductorRepresenter) { + ["../src/ConductorRepresenter", "./TestTimeConductor"], + function (ConductorRepresenter, TestTimeConductor) { "use strict"; var SCOPE_METHODS = [ @@ -77,10 +74,7 @@ define( testViews = [ { someKey: "some value" } ]; mockScope = jasmine.createSpyObj('scope', SCOPE_METHODS); mockElement = jasmine.createSpyObj('element', ELEMENT_METHODS); - mockConductor = jasmine.createSpyObj( - 'conductor', - [ 'displayStart', 'displayEnd' ] - ); + mockConductor = new TestTimeConductor(); mockCompiledTemplate = jasmine.createSpy('template'); mockNewScope = jasmine.createSpyObj('newScope', SCOPE_METHODS); mockNewElement = jasmine.createSpyObj('newElement', ELEMENT_METHODS); @@ -135,11 +129,12 @@ define( it("exposes conductor state in scope", function () { mockConductor.displayStart.andReturn(1977); mockConductor.displayEnd.andReturn(1984); + mockConductor.domain.andReturn('d'); representer.represent(testViews[0], {}); - expect(mockNewScope.conductor).toEqual({ - inner: { start: 1977, end: 1984 }, - outer: { start: 1977, end: 1984 } + expect(mockNewScope.ngModel.conductor).toEqual({ + inner: { start: 1977, end: 1984, domain: 'd' }, + outer: { start: 1977, end: 1984, domain: 'd' } }); }); @@ -151,17 +146,27 @@ define( representer.represent(testViews[0], {}); - mockNewScope.conductor = testState; + mockNewScope.ngModel.conductor = testState; - fireWatch(mockNewScope, 'conductor.inner.start', testState.inner.start); + fireWatch( + mockNewScope, + 'ngModel.conductor.inner.start', + testState.inner.start + ); expect(mockConductor.displayStart).toHaveBeenCalledWith(42); - fireWatch(mockNewScope, 'conductor.inner.end', testState.inner.end); + fireWatch( + mockNewScope, + 'ngModel.conductor.inner.end', + testState.inner.end + ); expect(mockConductor.displayEnd).toHaveBeenCalledWith(1984); }); describe("when bounds are changing", function () { - var mockThrottledFn = jasmine.createSpy('throttledFn'), + var startWatch = "ngModel.conductor.inner.start", + endWatch = "ngModel.conductor.inner.end", + mockThrottledFn = jasmine.createSpy('throttledFn'), testBounds; function fireThrottledFn() { @@ -172,7 +177,7 @@ define( mockThrottle.andReturn(mockThrottledFn); representer.represent(testViews[0], {}); testBounds = { start: 0, end: 1000 }; - mockNewScope.conductor.inner = testBounds; + mockNewScope.ngModel.conductor.inner = testBounds; mockConductor.displayStart.andCallFake(function () { return testBounds.start; }); @@ -184,14 +189,14 @@ define( it("does not broadcast while bounds are changing", function () { expect(mockScope.$broadcast).not.toHaveBeenCalled(); testBounds.start = 100; - fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start); + fireWatch(mockNewScope, startWatch, testBounds.start); testBounds.end = 500; - fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end); + fireWatch(mockNewScope, endWatch, testBounds.end); fireThrottledFn(); testBounds.start = 200; - fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start); + fireWatch(mockNewScope, startWatch, testBounds.start); testBounds.end = 400; - fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end); + fireWatch(mockNewScope, endWatch, testBounds.end); fireThrottledFn(); expect(mockScope.$broadcast).not.toHaveBeenCalled(); }); @@ -199,17 +204,56 @@ define( it("does broadcast when bounds have stabilized", function () { expect(mockScope.$broadcast).not.toHaveBeenCalled(); testBounds.start = 100; - fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start); + fireWatch(mockNewScope, startWatch, testBounds.start); testBounds.end = 500; - fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end); + fireWatch(mockNewScope, endWatch, testBounds.end); fireThrottledFn(); - fireWatch(mockNewScope, 'conductor.inner.start', testBounds.start); - fireWatch(mockNewScope, 'conductor.inner.end', testBounds.end); + fireWatch(mockNewScope, startWatch, testBounds.start); + fireWatch(mockNewScope, endWatch, testBounds.end); fireThrottledFn(); expect(mockScope.$broadcast).toHaveBeenCalled(); }); }); + it("exposes domain selection in scope", function () { + representer.represent(testViews[0], null); + + expect(mockNewScope.ngModel.domain) + .toEqual(mockConductor.domain()); + }); + + it("exposes domain options in scope", function () { + representer.represent(testViews[0], null); + + mockConductor.domainOptions().forEach(function (option, i) { + expect(mockNewScope.ngModel.options[i].value) + .toEqual(option.key); + expect(mockNewScope.ngModel.options[i].name) + .toEqual(option.name); + }); + }); + + it("updates domain selection from scope", function () { + var choice; + representer.represent(testViews[0], null); + + // Choose a domain that isn't currently selected + mockNewScope.ngModel.options.forEach(function (option) { + if (option.value !== mockNewScope.ngModel.domain) { + choice = option.value; + } + }); + + expect(mockConductor.domain) + .not.toHaveBeenCalledWith(choice); + + mockNewScope.ngModel.domain = choice; + fireWatch(mockNewScope, "ngModel.domain", choice); + + expect(mockConductor.domain) + .toHaveBeenCalledWith(choice); + }); + }); } ); diff --git a/platform/features/conductor/test/ConductorServiceSpec.js b/platform/features/conductor/test/ConductorServiceSpec.js index 640212540c..08080658a2 100644 --- a/platform/features/conductor/test/ConductorServiceSpec.js +++ b/platform/features/conductor/test/ConductorServiceSpec.js @@ -21,9 +21,6 @@ *****************************************************************************/ /*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ -/** - * EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015. - */ define( ["../src/ConductorService"], function (ConductorService) { @@ -38,7 +35,10 @@ define( beforeEach(function () { mockNow = jasmine.createSpy('now'); mockNow.andReturn(TEST_NOW); - conductorService = new ConductorService(mockNow); + conductorService = new ConductorService(mockNow, [ + { key: "d1", name: "Domain #1" }, + { key: "d2", name: "Domain #2" } + ]); }); it("initializes a time conductor around the current time", function () { diff --git a/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js b/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js index 9a5efc2448..6e768419c1 100644 --- a/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js +++ b/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js @@ -23,8 +23,8 @@ define( - ["../src/ConductorTelemetryDecorator"], - function (ConductorTelemetryDecorator) { + ["../src/ConductorTelemetryDecorator", "./TestTimeConductor"], + function (ConductorTelemetryDecorator, TestTimeConductor) { "use strict"; describe("ConductorTelemetryDecorator", function () { @@ -54,10 +54,7 @@ define( 'conductorService', ['getConductor'] ); - mockConductor = jasmine.createSpyObj( - 'conductor', - [ 'queryStart', 'queryEnd', 'displayStart', 'displayEnd' ] - ); + mockConductor = new TestTimeConductor(); mockPromise = jasmine.createSpyObj( 'promise', ['then'] @@ -78,10 +75,9 @@ define( return j * j * j; }); - mockConductor.queryStart.andReturn(-12321); - mockConductor.queryEnd.andReturn(-12321); mockConductor.displayStart.andReturn(42); mockConductor.displayEnd.andReturn(1977); + mockConductor.domain.andReturn("testDomain"); decorator = new ConductorTelemetryDecorator( mockConductorService, @@ -89,24 +85,72 @@ define( ); }); - it("adds display start/end times to historical requests", function () { + + describe("decorates historical requests", function () { + var request; + + beforeEach(function () { + decorator.requestTelemetry([{ someKey: "some value" }]); + request = mockTelemetryService.requestTelemetry + .mostRecentCall.args[0][0]; + }); + + it("with start times", function () { + expect(request.start).toEqual(mockConductor.displayStart()); + }); + + it("with end times", function () { + expect(request.end).toEqual(mockConductor.displayEnd()); + }); + + it("with domain selection", function () { + expect(request.domain).toEqual(mockConductor.domain()); + }); + }); + + describe("decorates subscription requests", function () { + var request; + + beforeEach(function () { + var mockCallback = jasmine.createSpy('callback'); + decorator.subscribe(mockCallback, [{ someKey: "some value" }]); + request = mockTelemetryService.subscribe + .mostRecentCall.args[1][0]; + }); + + it("with start times", function () { + expect(request.start).toEqual(mockConductor.displayStart()); + }); + + it("with end times", function () { + expect(request.end).toEqual(mockConductor.displayEnd()); + }); + + it("with domain selection", function () { + expect(request.domain).toEqual(mockConductor.domain()); + }); + }); + + it("adds display start/end times & domain selection to historical requests", function () { decorator.requestTelemetry([{ someKey: "some value" }]); expect(mockTelemetryService.requestTelemetry) .toHaveBeenCalledWith([{ someKey: "some value", start: mockConductor.displayStart(), - end: mockConductor.displayEnd() + end: mockConductor.displayEnd(), + domain: jasmine.any(String) }]); }); - it("adds display start/end times to subscription requests", function () { + it("adds display start/end times & domain selection to subscription requests", function () { var mockCallback = jasmine.createSpy('callback'); decorator.subscribe(mockCallback, [{ someKey: "some value" }]); expect(mockTelemetryService.subscribe) .toHaveBeenCalledWith(jasmine.any(Function), [{ someKey: "some value", start: mockConductor.displayStart(), - end: mockConductor.displayEnd() + end: mockConductor.displayEnd(), + domain: jasmine.any(String) }]); }); diff --git a/platform/features/conductor/test/TestTimeConductor.js b/platform/features/conductor/test/TestTimeConductor.js new file mode 100644 index 0000000000..01fed0c8fd --- /dev/null +++ b/platform/features/conductor/test/TestTimeConductor.js @@ -0,0 +1,50 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,spyOn*/ + +define( + ["../src/TimeConductor"], + function (TimeConductor) { + 'use strict'; + + function TestTimeConductor() { + var self = this; + + TimeConductor.apply(this, [ + 402514200000, + 444546000000, + [ + { key: "domain0", name: "Domain #1" }, + { key: "domain1", name: "Domain #2" } + ] + ]); + + Object.keys(TimeConductor.prototype).forEach(function (method) { + spyOn(self, method).andCallThrough(); + }); + } + + TestTimeConductor.prototype = TimeConductor.prototype; + + return TestTimeConductor; + } +); diff --git a/platform/features/conductor/test/TimeConductorSpec.js b/platform/features/conductor/test/TimeConductorSpec.js index 1e3859bf60..c9336a93b0 100644 --- a/platform/features/conductor/test/TimeConductorSpec.js +++ b/platform/features/conductor/test/TimeConductorSpec.js @@ -21,9 +21,6 @@ *****************************************************************************/ /*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ -/** - * EventSpec. Created by vwoeltje on 11/6/14. Modified by shale on 06/23/2015. - */ define( ["../src/TimeConductor"], function (TimeConductor) { @@ -32,12 +29,17 @@ define( describe("TimeConductor", function () { var testStart, testEnd, + testDomains, conductor; beforeEach(function () { testStart = 42; testEnd = 12321; - conductor = new TimeConductor(testStart, testEnd); + testDomains = [ + { key: "d1", name: "Domain #1" }, + { key: "d2", name: "Domain #2" } + ]; + conductor = new TimeConductor(testStart, testEnd, testDomains); }); it("provides accessors for query/display start/end times", function () { @@ -52,6 +54,25 @@ define( expect(conductor.displayEnd()).toEqual(4); }); + it("exposes domain options", function () { + expect(conductor.domainOptions()).toEqual(testDomains); + }); + + it("exposes the current domain choice", function () { + expect(conductor.domain()).toEqual(testDomains[0].key); + }); + + it("allows the domain choice to be changed", function () { + conductor.domain(testDomains[1].key); + expect(conductor.domain()).toEqual(testDomains[1].key); + }); + + it("throws an error on attempts to set an invalid domain", function () { + expect(function () { + conductor.domain("invalid-domain"); + }).toThrow(); + }); + }); } ); diff --git a/platform/features/layout/src/FixedController.js b/platform/features/layout/src/FixedController.js index f608f490f8..77c655e2cb 100644 --- a/platform/features/layout/src/FixedController.js +++ b/platform/features/layout/src/FixedController.js @@ -105,11 +105,13 @@ define( index ); - setDisplayedValue( - telemetryObject, - telemetrySeries.getRangeValue(index), - limit && datum && limit.evaluate(datum) - ); + if (index >= 0) { + setDisplayedValue( + telemetryObject, + telemetrySeries.getRangeValue(index), + limit && datum && limit.evaluate(datum) + ); + } } // Update the displayed value for this object diff --git a/platform/telemetry/src/TelemetryHandle.js b/platform/telemetry/src/TelemetryHandle.js index ae25fd9bfa..ff77d7b9e0 100644 --- a/platform/telemetry/src/TelemetryHandle.js +++ b/platform/telemetry/src/TelemetryHandle.js @@ -110,20 +110,23 @@ define( * Get the latest telemetry datum for this domain object. This * will be from real-time telemetry, unless an index is specified, * in which case it will be pulled from the historical telemetry - * series at the specified index. + * series at the specified index. If there is no latest available + * datum, this will return undefined. * * @param {DomainObject} domainObject the object of interest * @param {number} [index] the index of the data of interest * @returns {TelemetryDatum} the most recent datum */ self.getDatum = function (telemetryObject, index) { + function makeNewDatum(series) { + return series ? + subscription.makeDatum(telemetryObject, series, index) : + undefined; + } + return typeof index !== 'number' ? subscription.getDatum(telemetryObject) : - subscription.makeDatum( - telemetryObject, - this.getSeries(telemetryObject), - index - ); + makeNewDatum(this.getSeries(telemetryObject)); }; return self;