diff --git a/platform/features/layout/res/templates/frame.html b/platform/features/layout/res/templates/frame.html index 808603a84a..df4e3390ad 100644 --- a/platform/features/layout/res/templates/frame.html +++ b/platform/features/layout/res/templates/frame.html @@ -35,7 +35,7 @@
+ mct-object="representation.selected.key && domainObject">
\ No newline at end of file diff --git a/platform/features/layout/src/LayoutController.js b/platform/features/layout/src/LayoutController.js index 4e9de17618..fc6c8aafba 100644 --- a/platform/features/layout/src/LayoutController.js +++ b/platform/features/layout/src/LayoutController.js @@ -90,6 +90,10 @@ define( function lookupPanels(ids) { var configuration = $scope.configuration || {}; + // ids is read from model.composition and may be undefined; + // fall back to an array if that occurs + ids = ids || []; + // Pull panel positions from configuration rawPositions = shallowCopy(configuration.panels || {}, ids); diff --git a/platform/features/plot/src/MCTChart.js b/platform/features/plot/src/MCTChart.js index b8350e9171..2ca51b2309 100644 --- a/platform/features/plot/src/MCTChart.js +++ b/platform/features/plot/src/MCTChart.js @@ -155,6 +155,16 @@ define( } } + // Switch from WebGL to plain 2D if context is lost + function fallbackFromWebGL() { + element.html(TEMPLATE); + canvas = element.find("canvas")[0]; + chart = getChart([Canvas2DChart], canvas); + if (chart) { + doDraw(scope.draw); + } + } + // Try to initialize a chart. chart = getChart([GLChart, Canvas2DChart], canvas); @@ -164,6 +174,11 @@ define( return; } + // WebGL is a bit of a special case; it may work, then fail + // later for various reasons, so we need to listen for this + // and fall back to plain canvas drawing when it occurs. + canvas.addEventListener("webglcontextlost", fallbackFromWebGL); + // Check for resize, on a timer activeInterval = $interval(drawIfResized, 1000); @@ -192,4 +207,4 @@ define( return MCTChart; } -); \ No newline at end of file +); diff --git a/platform/features/plot/test/MCTChartSpec.js b/platform/features/plot/test/MCTChartSpec.js index 93be797114..9c60b034a6 100644 --- a/platform/features/plot/test/MCTChartSpec.js +++ b/platform/features/plot/test/MCTChartSpec.js @@ -36,6 +36,7 @@ define( mockElement, mockCanvas, mockGL, + mockC2d, mockPromise, mctChart; @@ -47,13 +48,14 @@ define( mockScope = jasmine.createSpyObj("$scope", ["$watchCollection", "$on"]); mockElement = - jasmine.createSpyObj("element", ["find"]); + jasmine.createSpyObj("element", ["find", "html"]); mockInterval.cancel = jasmine.createSpy("cancelInterval"); mockPromise = jasmine.createSpyObj("promise", ["then"]); // mct-chart uses GLChart, so it needs WebGL API - mockCanvas = jasmine.createSpyObj("canvas", [ "getContext" ]); + mockCanvas = + jasmine.createSpyObj("canvas", [ "getContext", "addEventListener" ]); mockGL = jasmine.createSpyObj( "gl", [ @@ -81,6 +83,7 @@ define( "drawArrays" ] ); + mockC2d = jasmine.createSpyObj('c2d', ['clearRect']); mockGL.ARRAY_BUFFER = "ARRAY_BUFFER"; mockGL.DYNAMIC_DRAW = "DYNAMIC_DRAW"; mockGL.TRIANGLE_FAN = "TRIANGLE_FAN"; @@ -93,7 +96,9 @@ define( }); mockElement.find.andReturn([mockCanvas]); - mockCanvas.getContext.andReturn(mockGL); + mockCanvas.getContext.andCallFake(function (type) { + return { webgl: mockGL, '2d': mockC2d }[type]; + }); mockInterval.andReturn(mockPromise); mctChart = new MCTChart(mockInterval, mockLog); @@ -169,6 +174,15 @@ define( expect(mockLog.warn).toHaveBeenCalled(); }); + it("falls back to Canvas 2d API if WebGL context is lost", function () { + mctChart.link(mockScope, mockElement); + expect(mockCanvas.addEventListener) + .toHaveBeenCalledWith("webglcontextlost", jasmine.any(Function)); + expect(mockCanvas.getContext).not.toHaveBeenCalledWith('2d'); + mockCanvas.addEventListener.mostRecentCall.args[1](); + expect(mockCanvas.getContext).toHaveBeenCalledWith('2d'); + }); + it("logs nothing in nominal situations (WebGL available)", function () { // Complement the previous test mctChart.link(mockScope, mockElement); @@ -197,4 +211,4 @@ define( }); } -); \ No newline at end of file +); diff --git a/platform/representation/src/MCTRepresentation.js b/platform/representation/src/MCTRepresentation.js index 6008e515e3..d93e42b957 100644 --- a/platform/representation/src/MCTRepresentation.js +++ b/platform/representation/src/MCTRepresentation.js @@ -93,15 +93,18 @@ define( function link($scope, element, attrs) { var activeRepresenters = representers.map(function (Representer) { - return new Representer($scope, element, attrs); - }); + return new Representer($scope, element, attrs); + }), + toClear = [], // Properties to clear out of scope on change + counter = 0; // Populate scope with any capabilities indicated by the // representation's extension definition function refreshCapabilities() { var domainObject = $scope.domainObject, representation = lookup($scope.key, domainObject), - uses = ((representation || {}).uses || []); + uses = ((representation || {}).uses || []), + myCounter = counter; if (domainObject) { // Update model @@ -115,10 +118,16 @@ define( " for representation ", $scope.key ].join("")); + $q.when( domainObject.useCapability(used) ).then(function (c) { - $scope[used] = c; + // Avoid clobbering capabilities from + // subsequent representations; + // Angular reuses scopes. + if (counter === myCounter) { + $scope[used] = c; + } }); }); } @@ -130,8 +139,7 @@ define( function refresh() { var domainObject = $scope.domainObject, representation = lookup($scope.key, domainObject), - uses = ((representation || {}).uses || []), - gestureKeys = ((representation || {}).gestures || []); + uses = ((representation || {}).uses || []); // Create an empty object named "representation", for this // representation to store local variables into. @@ -152,9 +160,19 @@ define( $log.warn("No representation found for " + $scope.key); } + // Clear out the scope from the last representation + toClear.forEach(function (property) { + delete $scope[property]; + }); + // Populate scope with fields associated with the current // domain object (if one has been passed in) if (domainObject) { + // Track how many representations we've made in this scope, + // to ensure that the correct representations are matched to + // the correct object/key pairs. + counter += 1; + // Initialize any capabilities refreshCapabilities(); @@ -168,6 +186,10 @@ define( activeRepresenters.forEach(function (representer) { representer.represent(representation, domainObject); }); + + // Track which properties we want to clear from scope + // next change object/key pair changes + toClear = uses.concat(['model']); } } diff --git a/platform/representation/test/MCTRepresentationSpec.js b/platform/representation/test/MCTRepresentationSpec.js index cb5820a065..43c9104a94 100644 --- a/platform/representation/test/MCTRepresentationSpec.js +++ b/platform/representation/test/MCTRepresentationSpec.js @@ -181,6 +181,25 @@ define( // Should have gotten a warning - that's an unknown key expect(mockLog.warn).toHaveBeenCalled(); }); + + it("clears out obsolete peroperties from scope", function () { + mctRepresentation.link(mockScope, mockElement); + + mockScope.key = "def"; + mockScope.domainObject = mockDomainObject; + mockDomainObject.useCapability.andReturn("some value"); + + // Trigger the watch + mockScope.$watch.calls[0].args[1](); + expect(mockScope.testCapability).toBeDefined(); + + // Change the view + mockScope.key = "xyz"; + + // Trigger the watch again; should clear capability from scope + mockScope.$watch.calls[0].args[1](); + expect(mockScope.testCapability).toBeUndefined(); + }); }); } ); \ No newline at end of file