diff --git a/example/generator/src/SinewaveTelemetrySeries.js b/example/generator/src/SinewaveTelemetrySeries.js index fa47f8f59a..8643b0429b 100644 --- a/example/generator/src/SinewaveTelemetrySeries.js +++ b/example/generator/src/SinewaveTelemetrySeries.js @@ -62,7 +62,7 @@ define( // so it's not checked for here, just formatted for display // differently. return (i + offset) * 1000 + firstTime * 1000 - - (domain === 'yesterday' ? ONE_DAY : 0); + (domain === 'yesterday' ? (ONE_DAY * 1000) : 0); }; generatorData.getRangeValue = function (i, range) { diff --git a/platform/core/bundle.json b/platform/core/bundle.json index 3e1fcbd4d3..911678d5f4 100644 --- a/platform/core/bundle.json +++ b/platform/core/bundle.json @@ -97,7 +97,7 @@ "provides": "actionService", "type": "provider", "implementation": "actions/ActionProvider.js", - "depends": [ "actions[]" ] + "depends": [ "actions[]", "$log" ] }, { "provides": "actionService", diff --git a/platform/core/src/actions/ActionProvider.js b/platform/core/src/actions/ActionProvider.js index dcb17eb6ce..b16db2e52a 100644 --- a/platform/core/src/actions/ActionProvider.js +++ b/platform/core/src/actions/ActionProvider.js @@ -39,9 +39,11 @@ define( * @imeplements {ActionService} * @constructor */ - function ActionProvider(actions) { + function ActionProvider(actions, $log) { var self = this; + this.$log = $log; + // Build up look-up tables this.actions = actions; this.actionsByKey = {}; @@ -74,6 +76,7 @@ define( var context = (actionContext || {}), category = context.category, key = context.key, + $log = this.$log, candidates; // Instantiate an action; invokes the constructor and @@ -103,12 +106,31 @@ define( // appliesTo method of given actions (if defined), and // instantiate those applicable actions. function createIfApplicable(actions, context) { - return (actions || []).filter(function (Action) { - return Action.appliesTo ? - Action.appliesTo(context) : true; - }).map(function (Action) { - return instantiateAction(Action, context); - }); + function isApplicable(Action) { + return Action.appliesTo ? Action.appliesTo(context) : true; + } + + function instantiate(Action) { + try { + return instantiateAction(Action, context); + } catch (e) { + $log.error([ + "Could not instantiate action", + Action.key, + "due to:", + e.message + ].join(" ")); + return undefined; + } + } + + function isDefined(action) { + return action !== undefined; + } + + return (actions || []).filter(isApplicable) + .map(instantiate) + .filter(isDefined); } // Match actions to the provided context by comparing "key" diff --git a/platform/core/src/types/TypeImpl.js b/platform/core/src/types/TypeImpl.js index 1a9854ea74..333f37663f 100644 --- a/platform/core/src/types/TypeImpl.js +++ b/platform/core/src/types/TypeImpl.js @@ -156,6 +156,13 @@ define( }); }; + /** + * Returns the default model for an object of this type. Note that + * this method returns a clone of the original model, so if using this + * method heavily, consider caching the result to optimize performance. + * + * @return {object} The default model for an object of this type. + */ TypeImpl.prototype.getInitialModel = function () { return JSON.parse(JSON.stringify(this.typeDef.model || {})); }; diff --git a/platform/core/test/actions/ActionProviderSpec.js b/platform/core/test/actions/ActionProviderSpec.js index f059052139..14c719f0c2 100644 --- a/platform/core/test/actions/ActionProviderSpec.js +++ b/platform/core/test/actions/ActionProviderSpec.js @@ -30,7 +30,8 @@ define( "use strict"; describe("The action provider", function () { - var actions, + var mockLog, + actions, actionProvider; function SimpleAction() { @@ -62,6 +63,10 @@ define( MetadataAction.key = "metadata"; beforeEach(function () { + mockLog = jasmine.createSpyObj( + '$log', + ['error', 'warn', 'info', 'debug'] + ); actions = [ SimpleAction, CategorizedAction, @@ -137,6 +142,42 @@ define( expect(provided[0].getMetadata()).toEqual("custom metadata"); }); + describe("when actions throw errors during instantiation", function () { + var errorText, + provided; + + beforeEach(function () { + errorText = "some error text"; + + function BadAction() { + throw new Error(errorText); + } + + provided = new ActionProvider( + [ SimpleAction, BadAction ], + mockLog + ).getActions(); + }); + + it("logs an error", function () { + expect(mockLog.error) + .toHaveBeenCalledWith(jasmine.any(String)); + }); + + it("reports the error's message", function () { + expect( + mockLog.error.mostRecentCall.args[0].indexOf(errorText) + ).not.toEqual(-1); + }); + + it("still provides valid actions", function () { + expect(provided.length).toEqual(1); + expect(provided[0].perform()).toEqual("simple"); + }); + + }); + + }); } -); \ No newline at end of file +); diff --git a/platform/core/test/types/TypeImplSpec.js b/platform/core/test/types/TypeImplSpec.js index f0a89875b3..0203058934 100644 --- a/platform/core/test/types/TypeImplSpec.js +++ b/platform/core/test/types/TypeImplSpec.js @@ -102,6 +102,8 @@ define( }); it("provides a fresh initial model each time", function () { + var model = type.getInitialModel(); + model.someKey = "some other value"; expect(type.getInitialModel().someKey).toEqual("some value"); }); diff --git a/platform/entanglement/src/actions/AbstractComposeAction.js b/platform/entanglement/src/actions/AbstractComposeAction.js index f68391adc9..ef56c952b3 100644 --- a/platform/entanglement/src/actions/AbstractComposeAction.js +++ b/platform/entanglement/src/actions/AbstractComposeAction.js @@ -122,6 +122,14 @@ define( }); }; + AbstractComposeAction.appliesTo = function (context) { + var applicableObject = + context.selectedObject || context.domainObject; + + return !!(applicableObject && + applicableObject.hasCapability('context')); + }; + return AbstractComposeAction; } ); diff --git a/platform/entanglement/src/actions/CopyAction.js b/platform/entanglement/src/actions/CopyAction.js index cdcefeb935..161db1ce27 100644 --- a/platform/entanglement/src/actions/CopyAction.js +++ b/platform/entanglement/src/actions/CopyAction.js @@ -34,7 +34,7 @@ define( * @constructor * @memberof platform/entanglement */ - function CopyAction($log, locationService, copyService, dialogService, + function CopyAction($log, locationService, copyService, dialogService, notificationService, context) { this.dialog = undefined; this.notification = undefined; @@ -42,7 +42,7 @@ define( this.notificationService = notificationService; this.$log = $log; //Extend the behaviour of the Abstract Compose Action - AbstractComposeAction.call(this, locationService, copyService, + AbstractComposeAction.call(this, locationService, copyService, context, "Duplicate", "to a location"); } @@ -87,8 +87,8 @@ define( }; /** - * Executes the CopyAction. The CopyAction uses the default behaviour of - * the AbstractComposeAction, but extends it to support notification + * Executes the CopyAction. The CopyAction uses the default behaviour of + * the AbstractComposeAction, but extends it to support notification * updates of progress on copy. */ CopyAction.prototype.perform = function() { @@ -131,6 +131,9 @@ define( return AbstractComposeAction.prototype.perform.call(this) .then(success, error, notification); }; + + CopyAction.appliesTo = AbstractComposeAction.appliesTo; + return CopyAction; } ); diff --git a/platform/entanglement/src/actions/LinkAction.js b/platform/entanglement/src/actions/LinkAction.js index c791310886..f34e91f156 100644 --- a/platform/entanglement/src/actions/LinkAction.js +++ b/platform/entanglement/src/actions/LinkAction.js @@ -35,14 +35,15 @@ define( * @memberof platform/entanglement */ function LinkAction(locationService, linkService, context) { - return new AbstractComposeAction( - locationService, - linkService, - context, - "Link" + AbstractComposeAction.apply( + this, + [locationService, linkService, context, "Link"] ); } + LinkAction.prototype = Object.create(AbstractComposeAction.prototype); + LinkAction.appliesTo = AbstractComposeAction.appliesTo; + return LinkAction; } ); diff --git a/platform/entanglement/src/actions/MoveAction.js b/platform/entanglement/src/actions/MoveAction.js index 4fdd4b59df..1d090ce313 100644 --- a/platform/entanglement/src/actions/MoveAction.js +++ b/platform/entanglement/src/actions/MoveAction.js @@ -35,14 +35,16 @@ define( * @memberof platform/entanglement */ function MoveAction(locationService, moveService, context) { - return new AbstractComposeAction( - locationService, - moveService, - context, - "Move" + AbstractComposeAction.apply( + this, + [locationService, moveService, context, "Move"] ); + } + MoveAction.prototype = Object.create(AbstractComposeAction.prototype); + MoveAction.appliesTo = AbstractComposeAction.appliesTo; + return MoveAction; } ); diff --git a/platform/entanglement/test/actions/AbstractComposeActionSpec.js b/platform/entanglement/test/actions/AbstractComposeActionSpec.js index 5be0604ec3..6a0ddd7ebd 100644 --- a/platform/entanglement/test/actions/AbstractComposeActionSpec.js +++ b/platform/entanglement/test/actions/AbstractComposeActionSpec.js @@ -94,6 +94,28 @@ define( composeService = new MockCopyService(); }); + it("are only applicable to domain objects with a context", function () { + var noContextObject = domainObjectFactory({ + name: 'selectedObject', + model: { name: 'selectedObject' }, + capabilities: {} + }); + + expect(AbstractComposeAction.appliesTo({ + selectedObject: selectedObject + })).toBe(true); + expect(AbstractComposeAction.appliesTo({ + domainObject: selectedObject + })).toBe(true); + + expect(AbstractComposeAction.appliesTo({ + selectedObject: noContextObject + })).toBe(false); + expect(AbstractComposeAction.appliesTo({ + domainObject: noContextObject + })).toBe(false); + }); + describe("with context from context-action", function () { beforeEach(function () { diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js index cf6bfcf581..4789f4c682 100644 --- a/platform/features/plot/src/PlotController.js +++ b/platform/features/plot/src/PlotController.js @@ -78,6 +78,8 @@ define( cachedObjects = [], updater, lastBounds, + lastRange, + lastDomain, handle; // Populate the scope with axis information (specifically, options @@ -120,16 +122,16 @@ define( // Reinstantiate the plot updater (e.g. because we have a // new subscription.) This will clear the plot. function recreateUpdater() { - updater = new PlotUpdater( - handle, - ($scope.axes[0].active || {}).key, - ($scope.axes[1].active || {}).key, - PLOT_FIXED_DURATION - ); - self.limitTracker = new PlotLimitTracker( - handle, - ($scope.axes[1].active || {}).key - ); + var domain = $scope.axes[0].active.key, + range = $scope.axes[1].active.key, + duration = PLOT_FIXED_DURATION; + + updater = new PlotUpdater(handle, domain, range, duration); + lastDomain = domain; + lastRange = range; + + self.limitTracker = new PlotLimitTracker(handle, range); + // Keep any externally-provided bounds if (lastBounds) { setBasePanZoom(lastBounds); @@ -201,22 +203,39 @@ define( } } + function requery() { + self.pending = true; + releaseSubscription(); + subscribe($scope.domainObject); + } + + function updateDomainFormat() { + var domainAxis = $scope.axes[0]; + plotTelemetryFormatter + .setDomainFormat(domainAxis.active.format); + } + + function domainRequery(newDomain) { + if (newDomain !== lastDomain) { + updateDomainFormat(); + requery(); + } + } + + function rangeRequery(newRange) { + if (newRange !== lastRange) { + requery(); + } + } + // Respond to a display bounds change (requery for data) function changeDisplayBounds(event, bounds) { var domainAxis = $scope.axes[0]; domainAxis.chooseOption(bounds.domain); - plotTelemetryFormatter - .setDomainFormat(domainAxis.active.format); - - self.pending = true; - releaseSubscription(); - subscribe($scope.domainObject); + updateDomainFormat(); setBasePanZoom(bounds); - } - - function updateDomainFormat(format) { - plotTelemetryFormatter.setDomainFormat(format); + requery(); } this.modeOptions = new PlotModeOptions([], subPlotFactory); @@ -237,6 +256,10 @@ define( new PlotAxis("ranges", [], AXIS_DEFAULTS[1]) ]; + // Watch for changes to the selected axis + $scope.$watch("axes[0].active.key", domainRequery); + $scope.$watch("axes[1].active.key", rangeRequery); + // Subscribe to telemetry when a domain object becomes available $scope.$watch('domainObject', subscribe); diff --git a/platform/features/plot/test/PlotControllerSpec.js b/platform/features/plot/test/PlotControllerSpec.js index addbdf5032..8edab2fc6d 100644 --- a/platform/features/plot/test/PlotControllerSpec.js +++ b/platform/features/plot/test/PlotControllerSpec.js @@ -53,6 +53,14 @@ define( }); } + function fireWatch(expr, value) { + mockScope.$watch.calls.forEach(function (call) { + if (call.args[0] === expr) { + call.args[1].apply(null, [value]); + } + }); + } + beforeEach(function () { mockScope = jasmine.createSpyObj( @@ -263,6 +271,20 @@ define( ]); expect(mockHandle.request.calls.length).toEqual(2); }); + + it("requeries when user changes domain selection", function () { + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + expect(mockHandle.request.calls.length).toEqual(1); + fireWatch("axes[0].active.key", 'someNewKey'); + expect(mockHandle.request.calls.length).toEqual(2); + }); + + it("requeries when user changes range selection", function () { + mockScope.$watch.mostRecentCall.args[1](mockDomainObject); + expect(mockHandle.request.calls.length).toEqual(1); + fireWatch("axes[1].active.key", 'someNewKey'); + expect(mockHandle.request.calls.length).toEqual(2); + }); }); } ); diff --git a/platform/telemetry/src/TelemetrySubscription.js b/platform/telemetry/src/TelemetrySubscription.js index 5dcab54b94..3de00e4c37 100644 --- a/platform/telemetry/src/TelemetrySubscription.js +++ b/platform/telemetry/src/TelemetrySubscription.js @@ -285,11 +285,17 @@ define( * domain objects returned by `getTelemetryObjects()`. * * @param {DomainObject} domainObject the object of interest + * @param {string} [key] the symbolic identifier of the domain + * to look up; if omitted, the value for this object's + * default domain will be used * @returns the most recent domain value observed */ - TelemetrySubscription.prototype.getDomainValue = function (domainObject) { - var id = domainObject.getId(); - return (this.latestValues[id] || {}).domain; + TelemetrySubscription.prototype.getDomainValue = function (domainObject, key) { + var id = domainObject.getId(), + latestValue = this.latestValues[id]; + return latestValue && (key ? + latestValue.datum[key] : + latestValue.domain); }; /** @@ -302,11 +308,17 @@ define( * domain objects returned by `getTelemetryObjects()`. * * @param {DomainObject} domainObject the object of interest + * @param {string} [key] the symbolic identifier of the range + * to look up; if omitted, the value for this object's + * default range will be used * @returns the most recent range value observed */ - TelemetrySubscription.prototype.getRangeValue = function (domainObject) { - var id = domainObject.getId(); - return (this.latestValues[id] || {}).range; + TelemetrySubscription.prototype.getRangeValue = function (domainObject, key) { + var id = domainObject.getId(), + latestValue = this.latestValues[id]; + return latestValue && (key ? + latestValue.datum[key] : + latestValue.range); }; /**