Compare commits

..

63 Commits

Author SHA1 Message Date
Aaron Doubek-Kraft
6cfee100d7 Merge branch 'master' into handle-invalid-time-url 2017-08-02 15:44:18 -07:00
Pete Richards
eeb214204d Merge pull request #1662 from nasa/revert-1659-master
Revert "persist user preference width for MCTSplitPanes issue #1646"
2017-08-02 10:53:07 -07:00
Deep Tailor
b41ceab51e Revert "persist user preference width for MCTSplitPanes issue #1646" 2017-08-02 10:48:57 -07:00
Deep Tailor
10b0f43fc1 Merge pull request #1659 from dtailor90/master
persist user preference width for MCTSplitPanes issue #1646, updated MCTSplitPane to handle logic regarding localStorage
2017-08-02 10:48:51 -07:00
Deep Tailor
593c1adf56 updated mctSplitPane to handle all logic regarding persisting userPreferenceWidth to local storage 2017-08-02 10:46:14 -07:00
Deep Tailor
5a7fdf82ac persist user preference width for MCTSplitPanes 2017-08-01 12:00:21 -07:00
Pete Richards
4571205871 [Time] Handle missing time system gracefully
When loading the application and no time system is set,
the url handler should not cause multiple errors to be
logged.

This adds a test for the case but does not specify the
expected behavior in detail, other than "it should not
crash."

Fixes bugs as reported here.
2017-07-27 15:38:42 -07:00
Pete Richards
34c3763421 Merge pull request #1635 from nasa/historical-imagery
[Historical imagery] [WIP]: Add historical view for image telemetry
2017-07-26 11:36:21 -07:00
Preston Crowe
ed6ae23dc0 [Historical Imagery] JSDoc, code review style changes
Added $element dependency and JSDoc for private methods. Autoscroll is now enabled by default when there is an active clock. Inline comments removed.
2017-07-14 13:05:59 -07:00
Pete Richards
23839b05b0 Merge pull request #1643 from nasa/open1641
[Open 1641] Change warnings to info messages
2017-07-13 17:23:10 -07:00
Pete Richards
6aed3bb0b5 Merge pull request #1638 from nasa/only-time-change-when-changed-1636
[Time API] Only change time when changed
2017-07-13 17:11:59 -07:00
Pete Richards
1ae62cde05 [Browse] Don't klobber params when preventing default
When browse controller is hijacking a browser navigation event,
it calls preventDefault on the route change.  This has the effect
of preventing all changes in the location (including search changes).

This change checks if other changes were made in the route change and
re-applies them after the navigation has completed.

Fixes a bug where navigating via a link that contained additional
search paramterers would have the effect of navigating but not
keeping the parameters in tact.

Discovered in the course of fixing #1636
2017-07-13 16:48:38 -07:00
Pete Richards
4e7e5bb783 [Time] Conductor changes based on click not scope
Update time conductor so that it triggers changes when the user
selects them instead of when the scope is updated.  Prevents spurious
changes from being triggered by the conductor when it updates
in response to a time API change.

Fixes #1636.
2017-07-13 16:48:38 -07:00
Luis-Johannes Schubert
efc46613bb Updated TelemetryCapability.js
removed outdated comment.
2017-07-13 16:47:24 -07:00
Preston Crowe
218ef16160 [Imagery] Implemented historical view for imagery
Implemented auto-scrolling historical imagery view in ImageryController. Imagery domain objects now request historical data on each manual bounds change. Added new specs for ensuring that historical data is requested on bounds change and duplicate bounds / datum are ignored.
2017-07-12 12:13:57 -07:00
Luis Schubert
fb0a577d16 [Open1641] Updated the spec files to check for info messages instead of warning messages 2017-07-10 09:46:51 -07:00
Luis Schubert
19b5e7c781 [Open 1641] Change warnings to info messages 2017-07-06 15:55:29 -07:00
Luis-Johannes Schubert
0794c0edf7 Merge pull request #1640 from nasa/fix-error-on-table-destroy
[Resize] don't trigger callback after being destroyed
2017-07-05 17:24:16 -07:00
Pete Richards
89515bb896 verify that eval isn't called after destroy 2017-07-05 16:48:09 -07:00
Pete Richards
318aecb7bc Merge pull request #1634 from nasa/open1405
[Fixed Position] Add numerical inputs for size and position of elements
2017-07-05 11:45:36 -07:00
Aaron Doubek-Kraft
a4b857a034 [Fixed Position] Change default behavior for old elements
For elements created before this change where useGrid is not defined,
default it to true to ensure consistent display size

Inline constant definitions in unit tests if they are only used once
2017-07-05 10:58:13 -07:00
Pete Richards
d82230dea4 [Resize] don't trigger callback when destroyed
Prevent MCTResize from triggering a callback after it is destroyed.

Fixes https://github.com/nasa/openmct/issues/1509
2017-07-04 17:03:25 -07:00
Pete Richards
cb242d8efb [TimeAPI] check for change before triggering
Update the TimeSettingsURLHandler to check for changes to the search
parameters and only trigger a bounds change when there is a change.

Added integration tests between Time API and settings url handler.

Prevents extraneous bounds events.

Fixes https://github.com/nasa/openmct/issues/1636
2017-07-04 15:09:56 -07:00
Aaron Doubek-Kraft
aa8f780e4e [Fixed Position] Add unit tests 2017-06-29 15:22:39 -07:00
Aaron Doubek-Kraft
3ed0880c6e [Fixed Position] Add unit tests for new code
Refactored ElementProxy and UnitAccessorMutator slightly to improve encasulation. Added unit tests for UnitAccessorMutator
2017-06-29 13:14:38 -07:00
Aaron Doubek-Kraft
40c68e6399 [Fixed Position] Change UI pixel/grid toggle to checkbox
Change the input for grid units/pixels to a simple checkbox toggle from a
dropdown menu.

Add a new specialized AccessorMutator class to support this operation.
2017-06-28 15:19:18 -07:00
Aaron Doubek-Kraft
65500736da [Fixed Postion] Update unit tests for new code 2017-06-28 13:38:35 -07:00
Aaron Doubek-Kraft
b9ab97eb7f [Fixed Position] Add ability to work in pixel space
Fix code style issues per Victor's review

Add toggle to work in pixel space or grid space, per the issue description.
Each element stores a boolean property that determines whether or not it
snaps to grid space or pixel space. Coordinates are converted between spaces
on toggle, preserving the size of the element in pixels.

To complete: change UI element for toggle to a checkbox.
2017-06-28 12:37:14 -07:00
Victor Woeltjen
34ef98e0cd Merge pull request #1595 from dhrubomoy/timezone_dropdown_feature
[CLOCK] Allow clock to set timezone with autocomplete dropdown option.
2017-06-26 16:51:30 -05:00
Aaron Doubek-Kraft
825f50262c [Fixed position] Incorporate numberfield control
Fix style and merge issues
2017-06-26 11:15:09 -07:00
Aaron Doubek-Kraft
a6079936e8 [Fixed position] Incorporate numberfield control 2017-06-26 11:07:20 -07:00
Aaron Doubek-Kraft
542b7a6f20 [Fixed Position] Incorporate numberfield control
Change inputs from textfield to new numberfield input, remove internal type checking in favor of input validation
2017-06-26 10:57:47 -07:00
Aaron Doubek-Kraft
2a8c3977a4 [Fixed Position] Incorporate new numberfield inputs
Changed inputs from textfields to numberfields, and removed internal
type checking for these inputs
2017-06-26 10:52:04 -07:00
Doubek-Kraft
515ea7caf8 [Layout] Code Style Issues 2017-06-26 09:36:31 -07:00
Doubek-Kraft
65993bd77f [Layout] Code style
Fix code style issues
2017-06-26 09:36:31 -07:00
Doubek-Kraft
54e07ccfdd [Layout] Line endpoint coordinate editing
Added appropriate line endpoint coordinate editing input fields
2017-06-26 09:36:31 -07:00
Doubek-Kraft
2e6fcec1c3 [Layout] Consistent input behavior
Inputs now handle invalid input consistently for all fields
2017-06-26 09:36:31 -07:00
Doubek-Kraft
f992fcebe1 [Layout] Consistent input behavior
Inputs now consistently default to 0 when left empty
2017-06-26 09:36:31 -07:00
Doubek-Kraft
280c838735 [Layout] Add numerical inputs for fixed-position layout
Added individual property inputs to the toolbar for height, width, x,
 y, and line endpoint coordinates in fixed/bundle.js.

Added "edit<Property>" AccessorMutators for height and width to each
of the element proxies, so these properties can be exposed only for
elements where it makes sense (e.g. boxes, but not lines, since lines
are better controlled by endpoint coordinates).

Added a method "checkNumeric" to ElementProxy.js , to be used
to restrict inputs to integral values for position and size
 textfield inputs.

Current issues: endpoint coordinate inputs have undexpected behvaior,
handles disappear after using the input fields to modify element
properties.
2017-06-26 09:36:31 -07:00
Pete Richards
445dfb3d91 Merge pull request #1633 from nasa/revert-1632-historical-imagery
Revert "Historical imagery "
2017-06-25 21:51:31 -07:00
Preston Crowe
bc616ecdee Revert "Historical imagery " 2017-06-25 20:46:09 -07:00
Preston Crowe
895c9b12e6 Merge pull request #1632 from nasa/historical-imagery
Historical imagery
2017-06-25 20:45:19 -07:00
Preston Crowe
95e68fce57 Integrated historic and realtime telemetry in imagery timeline view, added sass constast for timeline hover color 2017-06-25 13:02:04 -07:00
Preston Crowe
9f4f771774 Updated stylesheet 2017-06-25 13:02:04 -07:00
Preston Crowe
05290593e9 Added imagery timeline view for telemetry sources and placeholder template for displaying historical imagery 2017-06-25 13:02:04 -07:00
Pete Richards
2aa2d9d4bb Merge pull request #1629 from nasa/number-input
Review and integrate new Number input for mct-control
2017-06-25 12:53:11 -07:00
Dhrubomoy Das Gupta
7b690d0785 Revert "[Autocomplete] Show warning icon if invalid option was typed"
This reverts commit 307320b3ff.
2017-06-25 14:58:28 -04:00
Doubek-Kraft
39fe2fd7b6 [Layout] Code Style Issues 2017-06-23 16:04:34 -07:00
Doubek-Kraft
b661b4737e [Layout] Code style
Fix code style issues
2017-06-23 14:47:01 -07:00
Doubek-Kraft
5f7eeeae30 [Layout] Line endpoint coordinate editing
Added appropriate line endpoint coordinate editing input fields
2017-06-23 13:48:33 -07:00
Doubek-Kraft
537656303a [Layout] Consistent input behavior
Inputs now handle invalid input consistently for all fields
2017-06-23 11:42:52 -07:00
Doubek-Kraft
64bf63c18a [Layout] Consistent input behavior
Inputs now consistently default to 0 when left empty
2017-06-23 10:57:50 -07:00
Doubek-Kraft
ac3f638b35 [Layout] Add numerical inputs for fixed-position layout
Added individual property inputs to the toolbar for height, width, x,
 y, and line endpoint coordinates in fixed/bundle.js.

Added "edit<Property>" AccessorMutators for height and width to each
of the element proxies, so these properties can be exposed only for
elements where it makes sense (e.g. boxes, but not lines, since lines
are better controlled by endpoint coordinates).

Added a method "checkNumeric" to ElementProxy.js , to be used
to restrict inputs to integral values for position and size
 textfield inputs.

Current issues: endpoint coordinate inputs have undexpected behvaior,
handles disappear after using the input fields to modify element
properties.
2017-06-23 09:28:49 -07:00
Dhrubomoy Das Gupta
307320b3ff [Autocomplete] Show warning icon if invalid option was typed 2017-06-21 22:35:32 -04:00
Dhrubomoy Das Gupta
504b2e1ecf [Autocomplete] Update test 2017-06-21 18:14:14 -04:00
dhrubomoy
f20c8b7d99 Fix code style and add missing semicolons 2017-06-21 15:29:28 -04:00
dhrubomoy
17a067752f [Forms] Remove redundant mctClickElsewhere 2017-06-21 14:31:55 -04:00
Dhrubomoy Das Gupta
e3bd22de8c [Autocomplete] Minor refactoring 2017-06-07 23:01:14 -04:00
Dhrubomoy Das Gupta
3870266131 [Autocomplete] Clicking the arrow will display the entire list of timezones
This change is based on the  following code review:
- Display the entire list of timezones, regardless of what's currently entered.
- As soon as the user typed (or deleted chars in the input) then that filtration would take over the list display
2017-06-06 18:55:16 -04:00
Dhrubomoy Das Gupta
98cc19c637 [Autocomplete] Check if indexed filteredOptions is defined 2017-06-04 17:49:31 -04:00
Dhrubomoy Das Gupta
7cdb8db775 [Autocomplete] Hide options if clicked elsewhere. 2017-05-30 23:14:05 -04:00
dhrubomoy
7f14397262 [CLOCK] Issue #1273 : Use '$ scope.field' to avoid hard-coding. 2017-05-30 16:20:12 -04:00
Dhrubomoy Das Gupta
893e24ff98 [CLOCK] Issue #1273 : Allow clock to set timezone with autocomplete dropdown option. 2017-05-28 20:21:52 -04:00
52 changed files with 1770 additions and 329 deletions

View File

@@ -22,6 +22,7 @@
"eventemitter3": "^1.2.0",
"lodash": "3.10.1",
"almond": "~0.3.2",
"html2canvas": "^0.4.1"
"html2canvas": "^0.4.1",
"moment-timezone": "^0.5.13"
}
}

View File

@@ -32,6 +32,7 @@ requirejs.config({
"html2canvas": "bower_components/html2canvas/build/html2canvas.min",
"moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"moment-timezone": "bower_components/moment-timezone/builds/moment-timezone-with-data",
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",

View File

@@ -25,8 +25,8 @@
* @namespace platform/commonUI/browse
*/
define(
[],
function () {
['lodash'],
function (_) {
/**
* The BrowseController is used to populate the initial scope in Browse
@@ -157,12 +157,28 @@ define(
// (e.g. bookmarks to pages in OpenMCT) and prevent them. Instead,
// navigate to the path ourselves, which results in it being
// properly set.
$scope.$on('$routeChangeStart', function (event, route) {
$scope.$on('$routeChangeStart', function (event, route, oldRoute) {
if (route.$$route === $route.current.$$route) {
if (route.pathParams.ids &&
route.pathParams.ids !== $route.current.pathParams.ids) {
var otherParams = _.omit(route.params, 'ids');
var oldOtherParams = _.omit(oldRoute.params, 'ids');
var deletedParams = _.omit(oldOtherParams, _.keys(otherParams));
event.preventDefault();
navigateToPath(route.pathParams.ids.split('/'));
navigateToPath(route.pathParams.ids.split('/'))
.then(function () {
if (!_.isEqual(otherParams, oldOtherParams)) {
_.forEach(otherParams, function (v, k) {
$location.search(k, v);
});
_.forEach(deletedParams, function (k) {
$location.search(k, null);
});
}
});
} else {
navigateToPath([]);
}

View File

@@ -307,6 +307,40 @@ textarea.lg { position: relative; height: 300px; }
}
}
/******************************************************** AUTOCOMPLETE */
.autocomplete {
input {
width: 226px;
padding: 5px 0px 5px 7px;
}
.icon-arrow-down {
position: absolute;
top: 8px;
left: 210px;
font-size: 10px;
cursor: pointer;
}
.autocompleteOptions {
border: 1px solid $colorFormLines;
border-radius: 5px;
width: 224px;
max-height: 170px;
overflow-y: auto;
overflow-x: hidden;
li {
border: 1px solid $colorFormLines;
padding: 8px 0px 8px 5px;
.optionText {
cursor: pointer;
}
}
.optionPreSelected {
background-color: $colorInspectorSectionHeaderBg;
color: $colorInspectorSectionHeaderFg;
}
}
}
/******************************************************** OBJECT-HEADER */
.object-header {
font-size: 1em;

View File

@@ -83,12 +83,10 @@
.l-image-thumbs-wrapper {
//@include test(green);
direction: rtl;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: $interiorMargin;
white-space: nowrap;
z-index: 70;
}
.l-image-thumb-item {
@@ -114,7 +112,7 @@
width: $imageThumbsD + $imageThumbPad*2;
white-space: normal;
&:hover {
background: rgba(#fff, 0.2);
background: $colorThumbHoverBg;
.l-date,
.l-time {
color: #fff;

View File

@@ -83,13 +83,14 @@ define(
// Callback to fire after each timeout;
// update bounds and schedule another timeout
function onInterval() {
if (!active) {
return;
}
fireEval({
width: element[0].offsetWidth,
height: element[0].offsetHeight
});
if (active) {
$timeout(onInterval, currentInterval(), false);
}
$timeout(onInterval, currentInterval(), false);
}
// Stop running in the background

View File

@@ -102,11 +102,16 @@ define(
// Broadcast a destroy event
mockScope.$on.mostRecentCall.args[1]();
testElement.offsetWidth = 300;
testElement.offsetHeight = 350;
mockScope.$eval.reset();
// Fire the timeout
mockTimeout.mostRecentCall.args[0]();
// Should NOT have scheduled another timeout
expect(mockTimeout.calls.length).toEqual(2);
expect(mockScope.$eval).not.toHaveBeenCalled();
});
it("triggers a digest cycle when size changes", function () {

View File

@@ -191,6 +191,9 @@ $colorItemTreeVCHover: pullForward($colorItemTreeVC, 20%);
$colorItemTreeSelectedVC: $colorItemTreeVC;
$shdwItemTreeIcon: 0.6;
// Images
$colorThumbHoverBg: $colorItemTreeHoverBg;
// Scrollbar
$scrollbarTrackSize: 10px;
$scrollbarTrackShdw: rgba(#000, 0.7) 0 1px 5px;

View File

@@ -191,6 +191,9 @@ $colorItemTreeVCHover: $colorKey;
$colorItemTreeSelectedVC: $colorBodyBg;
$shdwItemTreeIcon: none;
// Images
$colorThumbHoverBg: $colorItemTreeHoverBg;
// Scrollbar
$scrollbarTrackSize: 10px;
$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;

View File

@@ -21,6 +21,7 @@
*****************************************************************************/
define([
"moment-timezone",
"./src/indicators/ClockIndicator",
"./src/services/TickerService",
"./src/controllers/ClockController",
@@ -34,6 +35,7 @@ define([
"text!./res/templates/timer.html",
'legacyRegistry'
], function (
MomentTimezone,
ClockIndicator,
TickerService,
ClockController,
@@ -226,13 +228,20 @@ define([
"cssClass": "l-inline"
}
]
},
{
"key": "timezone",
"name": "Timezone",
"control": "autocomplete",
"options": MomentTimezone.tz.names()
}
],
"model": {
"clockFormat": [
"YYYY/MM/DD hh:mm:ss",
"clock12"
]
],
"timezone": "UTC"
}
},
{

View File

@@ -20,9 +20,14 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['moment'],
function (moment) {
define([
'moment',
'moment-timezone'
],
function (
moment,
momentTimezone
) {
/**
* Controller for views of a Clock domain object.
@@ -37,10 +42,13 @@ define(
var lastTimestamp,
unlisten,
timeFormat,
zoneName,
self = this;
function update() {
var m = moment.utc(lastTimestamp);
var m = zoneName ?
moment.utc(lastTimestamp).tz(zoneName) : moment.utc(lastTimestamp);
self.zoneAbbr = m.zoneAbbr();
self.textValue = timeFormat && m.format(timeFormat);
self.ampmValue = m.format("A"); // Just the AM or PM part
}
@@ -50,21 +58,23 @@ define(
update();
}
function updateFormat(clockFormat) {
function updateModel(model) {
var baseFormat;
if (model !== undefined) {
baseFormat = model.clockFormat[0];
if (clockFormat !== undefined) {
baseFormat = clockFormat[0];
self.use24 = clockFormat[1] === 'clock24';
self.use24 = model.clockFormat[1] === 'clock24';
timeFormat = self.use24 ?
baseFormat.replace('hh', "HH") : baseFormat;
// If wrong timezone is provided, the UTC will be used
zoneName = momentTimezone.tz.names().includes(model.timezone) ?
model.timezone : "UTC";
update();
}
}
// Pull in the clock format from the domain object model
$scope.$watch('model.clockFormat', updateFormat);
// Pull in the model (clockFormat and timezone) from the domain object model
$scope.$watch('model', updateModel);
// Listen for clock ticks ... and stop listening on destroy
unlisten = tickerService.listen(tick);
@@ -76,7 +86,7 @@ define(
* @returns {string}
*/
ClockController.prototype.zone = function () {
return "UTC";
return this.zoneAbbr;
};
/**

View File

@@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2009-2016, United States Government
* Open MCT, Copyright (c) 2009-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@@ -43,9 +43,9 @@ define(
controller = new ClockController(mockScope, mockTicker);
});
it("watches for clock format from the domain object model", function () {
it("watches for model (clockFormat and timezone) from the domain object model", function () {
expect(mockScope.$watch).toHaveBeenCalledWith(
"model.clockFormat",
"model",
jasmine.any(Function)
);
});
@@ -67,29 +67,35 @@ define(
it("formats using the format string from the model", function () {
mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
mockScope.$watch.mostRecentCall.args[1]([
"YYYY-DDD hh:mm:ss",
"clock24"
]);
mockScope.$watch.mostRecentCall.args[1]({
"clockFormat": [
"YYYY-DDD hh:mm:ss",
"clock24"
],
"timezone": "Canada/Eastern"
});
expect(controller.zone()).toEqual("UTC");
expect(controller.text()).toEqual("2015-154 17:56:14");
expect(controller.zone()).toEqual("EDT");
expect(controller.text()).toEqual("2015-154 13:56:14");
expect(controller.ampm()).toEqual("");
});
it("formats 12-hour time", function () {
mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
mockScope.$watch.mostRecentCall.args[1]([
"YYYY-DDD hh:mm:ss",
"clock12"
]);
mockScope.$watch.mostRecentCall.args[1]({
"clockFormat": [
"YYYY-DDD hh:mm:ss",
"clock12"
],
"timezone": ""
});
expect(controller.zone()).toEqual("UTC");
expect(controller.text()).toEqual("2015-154 05:56:14");
expect(controller.ampm()).toEqual("PM");
});
it("does not throw exceptions when clockFormat is undefined", function () {
it("does not throw exceptions when model is undefined", function () {
mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP);
expect(function () {
mockScope.$watch.mostRecentCall.args[1](undefined);

View File

@@ -23,7 +23,7 @@
<div class="pane left menu-items">
<ul>
<li ng-repeat="metadata in ngModel.options"
ng-click="ngModel.selected = metadata">
ng-click="ngModel.select(metadata)">
<a ng-mouseover="ngModel.activeMetadata = metadata"
ng-mouseleave="ngModel.activeMetadata = undefined"
class="menu-item-a {{metadata.cssClass}}">

View File

@@ -111,7 +111,8 @@ define(
var options = this.optionsFromConfig(config);
this.menu = {
selected: undefined,
options: options
options: options,
select: this.selectMenuOption
};
//Set the initial state of the UI from the conductor state
@@ -132,8 +133,6 @@ define(
this.setViewFromBounds(bounds);
}
this.$scope.$watch("tcController.menu.selected", this.selectMenuOption);
this.conductorViewService.on('pan', this.onPan);
this.conductorViewService.on('pan-stop', this.onPanStop);
@@ -164,26 +163,28 @@ define(
*
* @private
* @param newOption
* @param oldOption
*/
TimeConductorController.prototype.selectMenuOption = function (newOption, oldOption) {
if (newOption !== oldOption) {
var config = this.getConfig(this.timeAPI.timeSystem(), newOption.clock);
if (!config) {
// Clock does not support this timeSystem, fallback to first
// option provided for clock.
config = this.config.menuOptions.filter(function (menuOption) {
return menuOption.clock === (newOption.clock && newOption.clock.key);
})[0];
}
TimeConductorController.prototype.selectMenuOption = function (newOption) {
if (this.menu.selected.key === newOption.key) {
return;
}
this.menu.selected = newOption;
if (config.clock) {
this.timeAPI.clock(config.clock, config.clockOffsets);
this.timeAPI.timeSystem(config.timeSystem);
} else {
this.timeAPI.stopClock();
this.timeAPI.timeSystem(config.timeSystem, config.bounds);
}
var config = this.getConfig(this.timeAPI.timeSystem(), newOption.clock);
if (!config) {
// Clock does not support this timeSystem, fallback to first
// option provided for clock.
config = this.config.menuOptions.filter(function (menuOption) {
return menuOption.clock === (newOption.clock && newOption.clock.key);
})[0];
}
if (config.clock) {
this.timeAPI.clock(config.clock, config.clockOffsets);
this.timeAPI.timeSystem(config.timeSystem);
} else {
this.timeAPI.stopClock();
this.timeAPI.timeSystem(config.timeSystem, config.bounds);
}
};

View File

@@ -142,7 +142,90 @@ define([
"cssClass": "l-input-lg",
"required": true
}
}
]
},
{
"items": [
{
"property": "editX",
"text": "X",
"name": "X",
"cssClass": "l-input-sm",
"control": "numberfield",
"min": "0"
},
{
"property": "editY",
"text": "Y",
"name": "Y",
"cssClass": "l-input-sm",
"control": "numberfield",
"min": "0"
},
{
"property": "editX1",
"text": "X1",
"name": "X1",
"cssClass": "l-input-sm",
"control" : "numberfield",
"min": "0"
},
{
"property": "editY1",
"text": "Y1",
"name": "Y1",
"cssClass": "l-input-sm",
"control" : "numberfield",
"min": "0"
},
{
"property": "editX2",
"text": "X2",
"name": "X2",
"cssClass": "l-input-sm",
"control" : "numberfield",
"min": "0"
},
{
"property": "editY2",
"text": "Y2",
"name": "Y2",
"cssClass": "l-input-sm",
"control" : "numberfield",
"min": "0"
},
{
"property": "editHeight",
"text": "H",
"name": "H",
"cssClass": "l-input-sm",
"control": "numberfield",
"description": "Resize object height",
"min": "1"
},
{
"property": "editWidth",
"text": "W",
"name": "W",
"cssClass": "l-input-sm",
"control": "numberfield",
"description": "Resize object width",
"min": "1"
}
]
},
{
"items": [
{
"property": "useGrid",
"name": "Snap to Grid",
"control": "checkbox"
}
]
},
{
"items": [
{
"property": "text",
"cssClass": "icon-gear",

View File

@@ -25,12 +25,14 @@ define([
"./src/controllers/ImageryController",
"./src/directives/MCTBackgroundImage",
"text!./res/templates/imagery.html",
"text!./res/templates/imageryTimeline.html",
'legacyRegistry'
], function (
ImageryViewPolicy,
ImageryController,
MCTBackgroundImage,
imageryTemplate,
imageryTimelineTemplate,
legacyRegistry
) {
@@ -48,6 +50,17 @@ define([
"telemetry"
],
"editable": false
},
{
"name": "Historical Imagery",
"key": "historical-imagery",
"cssClass": "icon-image",
"template": imageryTimelineTemplate,
"priority": "preferred",
"needs": [
"telemetry"
],
"editable": false
}
],
"policies": [
@@ -65,6 +78,8 @@ define([
"implementation": ImageryController,
"depends": [
"$scope",
"$window",
"$element",
"openmct"
]
}

View File

@@ -0,0 +1,8 @@
<div class="l-image-thumbs-wrapper" ng-controller="ImageryController as imagery">
<div class="l-image-thumb-item" ng-repeat="image in imageHistory track by $index">
<img class="l-thumb" ng-init="imagery.scrollToRight()"
ng-src={{imagery.getImageUrl(image)}} >
<div class="l-time">{{imagery.getTime(image)}}</div>
</div>
</div>

View File

@@ -24,9 +24,13 @@
* This bundle implements views of image telemetry.
* @namespace platform/features/imagery
*/
define(
['moment'],
function (moment) {
[
'zepto',
'lodash'
],
function ($, _) {
/**
* Controller for the "Imagery" view of a domain object which
@@ -34,14 +38,20 @@ define(
* @constructor
* @memberof platform/features/imagery
*/
function ImageryController($scope, openmct) {
function ImageryController($scope, $window, element, openmct) {
this.$scope = $scope;
this.$window = $window;
this.openmct = openmct;
this.date = "";
this.time = "";
this.zone = "";
this.imageUrl = "";
this.requestCount = 0;
this.scrollable = $(element[0]);
this.autoScroll = openmct.time.clock() ? true : false;
this.$scope.imageHistory = [];
this.$scope.filters = {
brightness: 100,
contrast: 100
@@ -50,12 +60,15 @@ define(
this.subscribe = this.subscribe.bind(this);
this.stopListening = this.stopListening.bind(this);
this.updateValues = this.updateValues.bind(this);
this.updateHistory = this.updateHistory.bind(this);
this.onBoundsChange = this.onBoundsChange.bind(this);
this.onScroll = this.onScroll.bind(this);
// Subscribe to telemetry when a domain object becomes available
this.subscribe(this.$scope.domainObject);
// Unsubscribe when the plot is destroyed
this.$scope.$on("$destroy", this.stopListening);
this.$scope.$on('$destroy', this.stopListening);
this.openmct.time.on('bounds', this.onBoundsChange);
this.scrollable.on('scroll', this.onScroll);
}
ImageryController.prototype.subscribe = function (domainObject) {
@@ -75,50 +88,152 @@ define(
.telemetry
.getValueFormatter(metadata.valuesForHints(['image'])[0]);
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, this.updateValues);
this.openmct.telemetry
.request(this.domainObject, {
strategy: 'latest',
size: 1
})
.then(function (values) {
this.updateValues(values[0]);
.subscribe(this.domainObject, function (datum) {
this.updateHistory(datum);
this.updateValues(datum);
}.bind(this));
this.requestLad(false);
this.requestHistory(this.openmct.time.bounds());
}.bind(this));
};
ImageryController.prototype.requestHistory = function (bounds) {
this.requestCount++;
this.$scope.imageHistory = [];
var requestId = this.requestCount;
this.openmct.telemetry
.request(this.domainObject, bounds)
.then(function (values) {
if (this.requestCount > requestId) {
return Promise.resolve('Stale request');
}
values.forEach(function (datum) {
this.updateHistory(datum);
}, this);
this.requestLad(true);
}.bind(this));
};
/**
* Makes a request for the most recent datum in the
* telelmetry store. Optional addToHistory argument
* determines whether the requested telemetry should
* be added to history or only used to update the current
* image url and timestamp.
* @private
* @param {boolean} [addToHistory] if true, adds to history
*/
ImageryController.prototype.requestLad = function (addToHistory) {
this.openmct.telemetry
.request(this.domainObject, {
strategy: 'latest',
size: 1
})
.then(function (values) {
this.updateValues(values[0]);
if (addToHistory !== false) {
this.updateHistory(values[0]);
}
}.bind(this));
};
ImageryController.prototype.stopListening = function () {
this.openmct.time.off('bounds', this.onBoundsChange);
this.scrollable.off('scroll', this.onScroll);
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
};
// Update displayable values to reflect latest image telemetry
/**
* Responds to bound change event be requesting new
* historical data if the bound change was manual.
* @private
* @param {object} [newBounds] new bounds object
* @param {boolean} [tick] true when change is automatic
*/
ImageryController.prototype.onBoundsChange = function (newBounds, tick) {
if (this.domainObject && !tick) {
this.requestHistory(newBounds);
}
};
/**
* Updates displayable values to match those of the most
* recently recieved datum.
* @param {object} [datum] the datum
* @private
*/
ImageryController.prototype.updateValues = function (datum) {
if (this.isPaused) {
this.nextDatum = datum;
return;
}
this.time = this.timeFormat.format(datum);
this.imageUrl = this.imageFormat.format(datum);
};
/**
* Appends given imagery datum to running history.
* @private
* @param {object} [datum] target telemetry datum
* @returns {boolean} falsy when a duplicate datum is given
*/
ImageryController.prototype.updateHistory = function (datum) {
if (this.$scope.imageHistory.length === 0 ||
!_.isEqual(this.$scope.imageHistory.slice(-1)[0], datum)) {
var index = _.sortedIndex(this.$scope.imageHistory, datum, 'utc');
this.$scope.imageHistory.splice(index, 0, datum);
return true;
}
return false;
};
ImageryController.prototype.onScroll = function (event) {
this.$window.requestAnimationFrame(function () {
if (this.scrollable[0].scrollLeft <
(this.scrollable[0].scrollWidth - this.scrollable[0].clientWidth) - 20) {
this.autoScroll = false;
} else {
this.autoScroll = true;
}
}.bind(this));
};
ImageryController.prototype.scrollToRight = function () {
if (this.autoScroll) {
this.scrollable[0].scrollLeft = this.scrollable[0].scrollWidth;
}
};
/**
* Get the time portion (hours, minutes, seconds) of the
* timestamp associated with the incoming image telemetry.
* timestamp associated with the incoming image telemetry
* if no parameter is given, or of a provided datum.
* @param {object} [datum] target telemetry datum
* @returns {string} the time
*/
ImageryController.prototype.getTime = function () {
return this.time;
ImageryController.prototype.getTime = function (datum) {
return datum ?
this.timeFormat.format(datum) :
this.time;
};
/**
* Get the URL of the image telemetry to display.
* Get the URL of the most recent image telemetry if no
* parameter is given, or of a provided datum.
* @param {object} [datum] target telemetry datum
* @returns {string} URL for telemetry image
*/
ImageryController.prototype.getImageUrl = function () {
return this.imageUrl;
ImageryController.prototype.getImageUrl = function (datum) {
return datum ?
this.imageFormat.format(datum) :
this.imageUrl;
};
/**
@@ -128,15 +243,15 @@ define(
* @returns {boolean} the current state
*/
ImageryController.prototype.paused = function (state) {
if (arguments.length > 0 && state !== this.isPaused) {
this.isPaused = state;
if (this.nextDatum) {
this.updateValues(this.nextDatum);
delete this.nextDatum;
if (arguments.length > 0 && state !== this.isPaused) {
this.isPaused = state;
if (this.nextDatum) {
this.updateValues(this.nextDatum);
delete this.nextDatum;
}
}
}
return this.isPaused;
};
return this.isPaused;
};
return ImageryController;
}

View File

@@ -47,7 +47,7 @@ define([
};
ImageryViewPolicy.prototype.allow = function (view, domainObject) {
if (view.key === 'imagery') {
if (view.key === 'imagery' || view.key === 'historical-imagery') {
return this.hasImageTelemetry(domainObject);
}

View File

@@ -21,8 +21,14 @@
*****************************************************************************/
define(
["../../src/controllers/ImageryController"],
function (ImageryController) {
[
"zepto",
"../../src/controllers/ImageryController"
],
function ($, ImageryController) {
var MOCK_ELEMENT_TEMPLATE =
'<div class="l-image-thumbs-wrapper"></div>';
describe("The Imagery controller", function () {
var $scope,
@@ -33,7 +39,9 @@ define(
metadata,
prefix,
controller,
hasLoaded;
hasLoaded,
mockWindow,
mockElement;
beforeEach(function () {
$scope = jasmine.createSpyObj('$scope', ['$on', '$watch']);
@@ -42,14 +50,16 @@ define(
['getId']
);
newDomainObject = { name: 'foo' };
oldDomainObject.getId.andReturn('testID');
openmct = {
objects: jasmine.createSpyObj('objectAPI', [
'get'
]),
time: jasmine.createSpyObj('timeAPI', [
'timeSystem'
'timeSystem',
'clock',
'on',
'off'
]),
telemetry: jasmine.createSpyObj('telemetryAPI', [
'subscribe',
@@ -92,13 +102,24 @@ define(
});
metadata.value.andReturn("timestamp");
metadata.valuesForHints.andReturn(["value"]);
mockElement = $(MOCK_ELEMENT_TEMPLATE);
mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']);
mockWindow.requestAnimationFrame.andCallFake(function (f) {
return f();
});
controller = new ImageryController($scope, openmct);
controller = new ImageryController(
$scope,
mockWindow,
mockElement,
openmct
);
});
describe("when loaded", function () {
var callback;
var callback,
boundsListener;
beforeEach(function () {
waitsFor(function () {
return hasLoaded;
@@ -106,12 +127,16 @@ define(
runs(function () {
openmct.time.on.calls.forEach(function (call) {
if (call.args[0] === "bounds") {
boundsListener = call.args[1];
}
});
callback =
openmct.telemetry.subscribe.mostRecentCall.args[1];
});
});
it("uses LAD telemetry", function () {
expect(openmct.telemetry.request).toHaveBeenCalledWith(
newDomainObject,
@@ -165,7 +190,14 @@ define(
);
});
it("unsubscribes when scope is destroyed", function () {
it("requests telemetry", function () {
expect(openmct.telemetry.request).toHaveBeenCalledWith(
newDomainObject,
jasmine.any(Object)
);
});
it("unsubscribes and unlistens when scope is destroyed", function () {
expect(unsubscribe).not.toHaveBeenCalled();
$scope.$on.calls.forEach(function (call) {
@@ -174,6 +206,25 @@ define(
}
});
expect(unsubscribe).toHaveBeenCalled();
expect(openmct.time.off)
.toHaveBeenCalledWith('bounds', jasmine.any(Function));
});
it("listens for bounds event and responds to tick and manual change", function () {
var mockBounds = {start: 1434600000000, end: 1434600500000};
expect(openmct.time.on).toHaveBeenCalled();
openmct.telemetry.request.reset();
boundsListener(mockBounds, true);
expect(openmct.telemetry.request).not.toHaveBeenCalled();
boundsListener(mockBounds, false);
expect(openmct.telemetry.request).toHaveBeenCalledWith(newDomainObject, mockBounds);
});
it ("doesnt append duplicate datum", function () {
var mockDatum = {url: 'image/url', utc: 1434600000000};
expect(controller.updateHistory(mockDatum)).toBe(true);
expect(controller.updateHistory(mockDatum)).toBe(false);
expect(controller.updateHistory(mockDatum)).toBe(false);
});
});

View File

@@ -19,12 +19,12 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<svg ng-attr-width="{{parameters.gridSize[0] * ngModel.width()}}"
ng-attr-height="{{parameters.gridSize[1] * ngModel.height()}}">
<line ng-attr-x1="{{parameters.gridSize[0] * ngModel.x1() + 1}}"
ng-attr-y1="{{parameters.gridSize[1] * ngModel.y1() + 1}}"
ng-attr-x2="{{parameters.gridSize[0] * ngModel.x2() + 1}}"
ng-attr-y2="{{parameters.gridSize[1] * ngModel.y2() + 1}}"
<svg ng-attr-width="{{ngModel.getGridSize()[0] * ngModel.width()}}"
ng-attr-height="{{ngModel.getGridSize()[1] * ngModel.height()}}">
<line ng-attr-x1="{{ngModel.getGridSize()[0] * ngModel.x1() + 1}}"
ng-attr-y1="{{ngModel.getGridSize()[1] * ngModel.y1() + 1}}"
ng-attr-x2="{{ngModel.getGridSize()[0] * ngModel.x2() + 1}}"
ng-attr-y2="{{ngModel.getGridSize()[1] * ngModel.y2() + 1}}"
ng-attr-stroke="{{ngModel.stroke()}}"
stroke-width="2">
</line>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -75,7 +75,7 @@ define(
// Convert from element x/y/width/height to an
// appropriate ng-style argument, to position elements.
function convertPosition(elementProxy) {
var gridSize = self.gridSize;
var gridSize = elementProxy.getGridSize();
// Multiply position/dimensions by grid size
return {
left: (gridSize[0] * elementProxy.x()) + 'px',
@@ -114,6 +114,7 @@ define(
self.gridSize = layoutGrid;
self.elementProxies.forEach(function (elementProxy) {
elementProxy.setGridSize(self.gridSize);
elementProxy.style = convertPosition(elementProxy);
});
}
@@ -121,7 +122,7 @@ define(
// Decorate an element for display
function makeProxyElement(element, index, elements) {
var ElementProxy = ElementProxies[element.type],
e = ElementProxy && new ElementProxy(element, index, elements);
e = ElementProxy && new ElementProxy(element, index, elements, self.gridSize);
if (e) {
// Provide a displayable position (convert from grid to px)
@@ -254,7 +255,8 @@ define(
color: "",
titled: true,
width: DEFAULT_DIMENSIONS[0],
height: DEFAULT_DIMENSIONS[1]
height: DEFAULT_DIMENSIONS[1],
useGrid: true
});
//Re-initialize objects, and subscribe to new object
@@ -518,4 +520,3 @@ define(
return FixedController;
}
);

View File

@@ -47,9 +47,10 @@ define(
* @memberof platform/features/layout.FixedDragHandle#
*/
FixedDragHandle.prototype.style = function () {
var gridSize = this.elementHandle.getGridSize();
// Adjust from grid to pixel coordinates
var x = this.elementHandle.x() * this.gridSize[0],
y = this.elementHandle.y() * this.gridSize[1];
var x = this.elementHandle.x() * gridSize[0],
y = this.elementHandle.y() * gridSize[1];
// Convert to a CSS style centered on that point
return {
@@ -78,13 +79,14 @@ define(
* started
*/
FixedDragHandle.prototype.continueDrag = function (delta) {
var gridSize = this.elementHandle.getGridSize();
if (this.dragging) {
// Update x/y positions (snapping to grid)
this.elementHandle.x(
this.dragging.x + Math.round(delta[0] / this.gridSize[0])
this.dragging.x + Math.round(delta[0] / gridSize[0])
);
this.elementHandle.y(
this.dragging.y + Math.round(delta[1] / this.gridSize[1])
this.dragging.y + Math.round(delta[1] / gridSize[1])
);
// Invoke update callback
if (this.update) {

View File

@@ -61,6 +61,7 @@ define(
element.width = element.width || 1;
element.height = element.height || 1;
element.type = type;
element.useGrid = true;
// Finally, add it to the view's configuration
addElementCallback(element);

View File

@@ -37,10 +37,11 @@ define(
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {number[]} gridSize the current layout grid size in [x,y] from
* @param {Array} elements the full array of elements
*/
function BoxProxy(element, index, elements) {
var proxy = new ElementProxy(element, index, elements);
function BoxProxy(element, index, elements, gridSize) {
var proxy = new ElementProxy(element, index, elements, gridSize);
/**
* Get/set this element's fill color. (Omitting the
@@ -52,6 +53,12 @@ define(
*/
proxy.fill = new AccessorMutator(element, 'fill');
//Expose x,y, width and height for editing
proxy.editWidth = new AccessorMutator(element, 'width');
proxy.editHeight = new AccessorMutator(element, 'height');
proxy.editX = new AccessorMutator(element, 'x');
proxy.editY = new AccessorMutator(element, 'y');
return proxy;
}

View File

@@ -21,8 +21,8 @@
*****************************************************************************/
define(
['./AccessorMutator', './ResizeHandle'],
function (AccessorMutator, ResizeHandle) {
['./AccessorMutator', './ResizeHandle', './UnitAccessorMutator'],
function (AccessorMutator, ResizeHandle, UnitAccessorMutator) {
// Index deltas for changes in order
var ORDERS = {
@@ -32,6 +32,10 @@ define(
bottom: Number.NEGATIVE_INFINITY
};
// Mininmum pixel height and width for objects
var MIN_WIDTH = 10;
var MIN_HEIGHT = 10;
// Ensure a value is non-negative (for x/y setters)
function clamp(value) {
return Math.max(value, 0);
@@ -51,17 +55,29 @@ define(
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {number[]} gridSize the current layout grid size in [x,y] from
* @param {Array} elements the full array of elements
*/
function ElementProxy(element, index, elements) {
this.resizeHandles = [new ResizeHandle(element, 1, 1)];
function ElementProxy(element, index, elements, gridSize) {
/**
* The element as stored in the view configuration.
* @memberof platform/features/layout.ElementProxy#
*/
this.element = element;
/**
* The current grid size of the layout.
* @memberof platform/features/layout.ElementProxy#
*/
this.gridSize = gridSize || [1,1]; //Ensure a reasonable default
this.resizeHandles = [new ResizeHandle(
this.element,
this.getMinWidth(),
this.getMinHeight(),
this.getGridSize()
)];
/**
* Get and/or set the x position of this element.
* Units are in fixed position grid space.
@@ -106,6 +122,8 @@ define(
*/
this.height = new AccessorMutator(element, 'height');
this.useGrid = new UnitAccessorMutator(this);
this.index = index;
this.elements = elements;
}
@@ -156,6 +174,51 @@ define(
return this.resizeHandles;
};
/**
* Returns which grid size the element is currently using.
* @return {number[]} The current grid size in [x,y] form if the element
* is currently using the grid, [1,1] if it is using
* pixels.
*/
ElementProxy.prototype.getGridSize = function () {
var gridSize;
// Default to using the grid if useGrid was not defined
if (typeof this.element.useGrid === 'undefined') {
this.element.useGrid = true;
}
if (this.element.useGrid) {
gridSize = this.gridSize;
} else {
gridSize = [1,1];
}
return gridSize;
};
/**
* Set the current grid size stored by this element proxy
* @param {number[]} gridSize The current layout grid size in [x,y] form
*/
ElementProxy.prototype.setGridSize = function (gridSize) {
this.gridSize = gridSize;
};
/**
* Get the current minimum element width in grid units
* @return {number} The current minimum element width
*/
ElementProxy.prototype.getMinWidth = function () {
return Math.ceil(MIN_WIDTH / this.getGridSize()[0]);
};
/**
* Get the current minimum element height in grid units
* @return {number} The current minimum element height
*/
ElementProxy.prototype.getMinHeight = function () {
return Math.ceil(MIN_HEIGHT / this.getGridSize()[1]);
};
return ElementProxy;
}
);

View File

@@ -36,10 +36,11 @@ define(
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
* @param {number[]} gridSize the current layout grid size in [x,y] from
* @augments {platform/features/layout.ElementProxy}
*/
function ImageProxy(element, index, elements) {
var proxy = new ElementProxy(element, index, elements);
function ImageProxy(element, index, elements, gridSize) {
var proxy = new ElementProxy(element, index, elements, gridSize);
/**
* Get and/or set the displayed text of this element.
@@ -49,6 +50,12 @@ define(
*/
proxy.url = new AccessorMutator(element, 'url');
//Expose x,y, width and height properties for editing
proxy.editWidth = new AccessorMutator(element, 'width');
proxy.editHeight = new AccessorMutator(element, 'height');
proxy.editX = new AccessorMutator(element, 'x');
proxy.editY = new AccessorMutator(element, 'y');
return proxy;
}

View File

@@ -35,14 +35,16 @@ define(
* @param {string} yProperty field which stores x position
* @param {string} xOther field which stores x of other end
* @param {string} yOther field which stores y of other end
* @param {number[]} gridSize the current layout grid size in [x,y] from
* @implements {platform/features/layout.ElementHandle}
*/
function LineHandle(element, xProperty, yProperty, xOther, yOther) {
function LineHandle(element, xProperty, yProperty, xOther, yOther, gridSize) {
this.element = element;
this.xProperty = xProperty;
this.yProperty = yProperty;
this.xOther = xOther;
this.yOther = yOther;
this.gridSize = gridSize;
}
LineHandle.prototype.x = function (value) {
@@ -83,6 +85,10 @@ define(
return element[yProperty];
};
LineHandle.prototype.getGridSize = function () {
return this.gridSize;
};
return LineHandle;
}

View File

@@ -21,8 +21,8 @@
*****************************************************************************/
define(
['./ElementProxy', './LineHandle'],
function (ElementProxy, LineHandle) {
['./ElementProxy', './LineHandle', './AccessorMutator'],
function (ElementProxy, LineHandle, AccessorMutator) {
/**
* Selection/diplay proxy for line elements of a fixed position
@@ -33,13 +33,14 @@ define(
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
* @param {number[]} gridSize the current layout grid size in [x,y] from
* @augments {platform/features/layout.ElementProxy}
*/
function LineProxy(element, index, elements) {
var proxy = new ElementProxy(element, index, elements),
function LineProxy(element, index, elements, gridSize) {
var proxy = new ElementProxy(element, index, elements, gridSize),
handles = [
new LineHandle(element, 'x', 'y', 'x2', 'y2'),
new LineHandle(element, 'x2', 'y2', 'x', 'y')
new LineHandle(element, 'x', 'y', 'x2', 'y2', proxy.getGridSize()),
new LineHandle(element, 'x2', 'y2', 'x', 'y', proxy.getGridSize())
];
/**
@@ -148,6 +149,12 @@ define(
return handles;
};
// Expose endpoint coordinates for editing
proxy.editX1 = new AccessorMutator(element, 'x');
proxy.editY1 = new AccessorMutator(element, 'y');
proxy.editX2 = new AccessorMutator(element, 'x2');
proxy.editY2 = new AccessorMutator(element, 'y2');
return proxy;
}

View File

@@ -35,12 +35,14 @@ define(
* @memberof platform/features/layout
* @constructor
*/
function ResizeHandle(element, minWidth, minHeight) {
function ResizeHandle(element, minWidth, minHeight, gridSize) {
this.element = element;
// Ensure reasonable defaults
this.minWidth = minWidth || 0;
this.minHeight = minHeight || 0;
this.gridSize = gridSize;
}
ResizeHandle.prototype.x = function (value) {
@@ -65,6 +67,10 @@ define(
return element.y + element.height;
};
ResizeHandle.prototype.getGridSize = function () {
return this.gridSize;
};
return ResizeHandle;
}

View File

@@ -39,10 +39,11 @@ define(
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
* @param {number[]} gridSize the current layout grid size in [x,y] form
* @augments {platform/features/layout.ElementProxy}
*/
function TelemetryProxy(element, index, elements) {
var proxy = new TextProxy(element, index, elements);
function TelemetryProxy(element, index, elements, gridSize) {
var proxy = new TextProxy(element, index, elements, gridSize);
// Toggle the visibility of the title
function toggle() {

View File

@@ -36,10 +36,11 @@ define(
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
* @param {number[]} gridSize the current layout grid size in [x,y] from
* @augments {platform/features/layout.ElementProxy}
*/
function TextProxy(element, index, elements) {
var proxy = new BoxProxy(element, index, elements);
function TextProxy(element, index, elements, gridSize) {
var proxy = new BoxProxy(element, index, elements, gridSize);
/**
* Get and/or set the text color of this element.

View File

@@ -0,0 +1,92 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
define(
[],
function () {
/**
* Variant of AccessorMutator to handle the specific case of updating
* useGrid, in order update the positions appropriately from within
* the scope of UnitAccessorMutator
*
* @memberof platform/features/layout
* @constructor
* @param {ElementProxy} proxy ElementProxy object to perform the update
* upon
*/
function UnitAccessorMutator(elementProxy) {
var self = this;
this.elementProxy = elementProxy;
return function (useGrid) {
var current = elementProxy.element.useGrid;
if (arguments.length > 0) {
elementProxy.element.useGrid = useGrid;
if (useGrid && !current) {
self.convertCoordsTo('grid');
} else if (!useGrid && current) {
self.convertCoordsTo('px');
}
}
return elementProxy.element.useGrid;
};
}
/**
* For the elementProxy object called upon, convert its element's
* coordinates and size from pixels to grid units, or vice-versa.
* @param {string} unit When called with 'px', converts grid units to
* pixels; when called with 'grid', snaps element
* to grid units
*/
UnitAccessorMutator.prototype.convertCoordsTo = function (unit) {
var proxy = this.elementProxy,
gridSize = proxy.gridSize,
element = proxy.element,
minWidth = proxy.getMinWidth(),
minHeight = proxy.getMinHeight();
if (unit === 'px') {
element.x = element.x * gridSize[0];
element.y = element.y * gridSize[1];
element.width = element.width * gridSize[0];
element.height = element.height * gridSize[1];
if (element.x2 && element.y2) {
element.x2 = element.x2 * gridSize[0];
element.y2 = element.y2 * gridSize[1];
}
} else if (unit === 'grid') {
element.x = Math.round(element.x / gridSize[0]);
element.y = Math.round(element.y / gridSize[1]);
element.width = Math.max(Math.round(element.width / gridSize[0]), minWidth);
element.height = Math.max(Math.round(element.height / gridSize[1]), minHeight);
if (element.x2 && element.y2) {
element.x2 = Math.round(element.x2 / gridSize[0]);
element.y2 = Math.round(element.y2 / gridSize[1]);
}
}
};
return UnitAccessorMutator;
}
);

View File

@@ -157,9 +157,9 @@ define(
};
testValues = { a: 10, b: 42, c: 31.42 };
testConfiguration = { elements: [
{ type: "fixed.telemetry", id: 'a', x: 1, y: 1 },
{ type: "fixed.telemetry", id: 'b', x: 1, y: 1 },
{ type: "fixed.telemetry", id: 'c', x: 1, y: 1 }
{ type: "fixed.telemetry", id: 'a', x: 1, y: 1, useGrid: true},
{ type: "fixed.telemetry", id: 'b', x: 1, y: 1, useGrid: true},
{ type: "fixed.telemetry", id: 'c', x: 1, y: 1, useGrid: true}
]};
mockChildren = testModel.composition.map(makeMockDomainObject);

View File

@@ -35,13 +35,14 @@ define(
beforeEach(function () {
mockElementHandle = jasmine.createSpyObj(
'elementHandle',
['x', 'y']
['x', 'y','getGridSize']
);
mockUpdate = jasmine.createSpy('update');
mockCommit = jasmine.createSpy('commit');
mockElementHandle.x.andReturn(6);
mockElementHandle.y.andReturn(8);
mockElementHandle.getGridSize.andReturn(TEST_GRID_SIZE);
handle = new FixedDragHandle(
mockElementHandle,

View File

@@ -61,7 +61,8 @@ define(
x: 0,
y: 0,
width: 1,
height: 1
height: 1,
useGrid: true
});
});

View File

@@ -35,13 +35,15 @@ define(
y: 2,
stroke: '#717171',
width: 42,
height: 24
height: 24,
useGrid: true
};
testElements = [{}, {}, testElement, {}];
proxy = new ElementProxy(
testElement,
testElements.indexOf(testElement),
testElements
testElements,
[13,21]
);
});
@@ -73,6 +75,29 @@ define(
expect(proxy.x()).toEqual(0);
expect(proxy.y()).toEqual(0);
});
it("allows modifying the current grid size", function () {
proxy.setGridSize([112,420]);
expect(proxy.gridSize).toEqual([112,420]);
});
it("returns the current grid size only if the element snaps to grid", function () {
expect(proxy.getGridSize()).toEqual([13,21]);
proxy.useGrid(false);
expect(proxy.getGridSize()).toEqual([1,1]);
});
it("returns the mininum height and width of an element currently used units", function () {
// Assumes mininum height and width are 10, in pixels
expect(proxy.getMinWidth()).toEqual(1);
expect(proxy.getMinHeight()).toEqual(1);
proxy.setGridSize([7,4]);
expect(proxy.getMinWidth()).toEqual(2);
expect(proxy.getMinHeight()).toEqual(3);
proxy.useGrid(false);
expect(proxy.getMinWidth()).toEqual(10);
expect(proxy.getMinHeight()).toEqual(10);
});
});
}
);

View File

@@ -33,10 +33,11 @@ define(
x: 3,
y: 42,
x2: 8,
y2: 11
y2: 11,
useGrid: true
};
handle = new LineHandle(testElement, 'x', 'y', 'x2', 'y2');
handle = new LineHandle(testElement, 'x', 'y', 'x2', 'y2', [45,21]);
});
it("provides x/y grid coordinates for its corner", function () {
@@ -67,6 +68,9 @@ define(
expect(testElement.y).not.toEqual(testElement.y2);
});
it("returns the correct grid size", function () {
expect(handle.getGridSize()).toEqual([45,21]);
});
});
}

View File

@@ -28,10 +28,10 @@ define(
var vertical, horizontal, diagonal, reversed;
beforeEach(function () {
vertical = { x: 1, y: 4, x2: 1, y2: 8 };
horizontal = { x: 3, y: 3, x2: 12, y2: 3 };
diagonal = { x: 3, y: 8, x2: 5, y2: 11 };
reversed = { x2: 3, y2: 8, x: 5, y: 11 };
vertical = { x: 1, y: 4, x2: 1, y2: 8};
horizontal = { x: 3, y: 3, x2: 12, y2: 3};
diagonal = { x: 3, y: 8, x2: 5, y2: 11};
reversed = { x2: 3, y2: 8, x: 5, y: 11};
});
it("ensures visible width for vertical lines", function () {
@@ -63,13 +63,13 @@ define(
it("adjusts both ends when mutating x", function () {
var proxy = new LineProxy(diagonal);
proxy.x(6);
expect(diagonal).toEqual({ x: 6, y: 8, x2: 8, y2: 11 });
expect(diagonal).toEqual({ x: 6, y: 8, x2: 8, y2: 11, useGrid: true });
});
it("adjusts both ends when mutating y", function () {
var proxy = new LineProxy(diagonal);
proxy.y(6);
expect(diagonal).toEqual({ x: 3, y: 6, x2: 5, y2: 9 });
expect(diagonal).toEqual({ x: 3, y: 6, x2: 5, y2: 9, useGrid: true });
});
it("provides internal positions for SVG lines", function () {

View File

@@ -24,7 +24,8 @@ define(
['../../src/elements/ResizeHandle'],
function (ResizeHandle) {
var TEST_MIN_WIDTH = 4, TEST_MIN_HEIGHT = 2;
var TEST_MIN_WIDTH = 4,
TEST_MIN_HEIGHT = 2;
describe("A fixed position drag handle", function () {
var testElement,
@@ -35,13 +36,15 @@ define(
x: 3,
y: 42,
width: 30,
height: 36
height: 36,
useGrid: true
};
handle = new ResizeHandle(
testElement,
TEST_MIN_WIDTH,
TEST_MIN_HEIGHT
TEST_MIN_HEIGHT,
[34,81]
);
});
@@ -73,6 +76,10 @@ define(
expect(testElement.height).toEqual(TEST_MIN_HEIGHT);
});
it("returns the correct grid size", function () {
expect(handle.getGridSize()).toEqual([34,81]);
});
});
}
);

View File

@@ -0,0 +1,157 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
define(
['../../src/elements/UnitAccessorMutator'],
function (UnitAccessorMutator) {
var GRID_SIZE = [13,17];
describe("An elementProxy.gridSize accessor-mutator", function () {
var mockElementProxy,
testElement,
mockLineProxy,
testLine,
uAM,
uAMLine;
beforeEach(function () {
testElement = {
x: 2,
y: 3,
width: 4,
height: 5,
useGrid: true
};
mockElementProxy = {
element: testElement,
gridSize: GRID_SIZE,
getMinHeight: jasmine.createSpy('minHeight'),
getMinWidth: jasmine.createSpy('minWidth')
};
testLine = {
x: 7,
y: 8,
x2: 9,
y2: 10,
width: 11,
height: 12,
useGrid: true
};
mockLineProxy = {
element: testLine,
gridSize: GRID_SIZE,
getMinHeight: jasmine.createSpy('minHeight'),
getMinWidth: jasmine.createSpy('minWidth')
};
uAM = new UnitAccessorMutator(mockElementProxy);
uAMLine = new UnitAccessorMutator(mockLineProxy);
mockElementProxy.getMinWidth.andReturn(1);
mockElementProxy.getMinHeight.andReturn(1);
mockLineProxy.getMinWidth.andReturn(1);
mockLineProxy.getMinHeight.andReturn(1);
});
it("allows access to useGrid", function () {
expect(uAM()).toEqual(mockElementProxy.element.useGrid);
});
it("allows mutation of useGrid", function () {
uAM(false);
expect(mockElementProxy.element.useGrid).toEqual(false);
});
it("converts coordinates appropriately for a box", function () {
uAM(false);
expect(mockElementProxy.element.x).toEqual(26);
expect(mockElementProxy.element.y).toEqual(51);
expect(mockElementProxy.element.width).toEqual(52);
expect(mockElementProxy.element.height).toEqual(85);
uAM(true);
expect(mockElementProxy.element.x).toEqual(2);
expect(mockElementProxy.element.y).toEqual(3);
expect(mockElementProxy.element.width).toEqual(4);
expect(mockElementProxy.element.height).toEqual(5);
});
it("converts coordinates appropriately for a line", function () {
uAMLine(false);
expect(mockLineProxy.element.x).toEqual(91);
expect(mockLineProxy.element.y).toEqual(136);
expect(mockLineProxy.element.x2).toEqual(117);
expect(mockLineProxy.element.y2).toEqual(170);
expect(mockLineProxy.element.width).toEqual(143);
expect(mockLineProxy.element.height).toEqual(204);
uAMLine(true);
expect(mockLineProxy.element.x).toEqual(7);
expect(mockLineProxy.element.y).toEqual(8);
expect(mockLineProxy.element.x2).toEqual(9);
expect(mockLineProxy.element.y2).toEqual(10);
expect(mockLineProxy.element.width).toEqual(11);
expect(mockLineProxy.element.height).toEqual(12);
});
it("doesn't covert coordinates unecessarily", function () {
uAM(false);
expect(mockElementProxy.element.x).toEqual(26);
expect(mockElementProxy.element.y).toEqual(51);
expect(mockElementProxy.element.width).toEqual(52);
expect(mockElementProxy.element.height).toEqual(85);
uAM(false);
expect(mockElementProxy.element.x).toEqual(26);
expect(mockElementProxy.element.y).toEqual(51);
expect(mockElementProxy.element.width).toEqual(52);
expect(mockElementProxy.element.height).toEqual(85);
});
it("snaps coordinates onto the grid", function () {
uAM(false);
mockElementProxy.element.x += 11;
mockElementProxy.element.y -= 27;
mockElementProxy.element.width -= 14;
mockElementProxy.element.height += 4;
uAM(true);
expect(mockElementProxy.element.x).toEqual(3);
expect(mockElementProxy.element.y).toEqual(1);
expect(mockElementProxy.element.width).toEqual(3);
expect(mockElementProxy.element.height).toEqual(5);
});
it("enforces a minimum height and width", function () {
uAM(false);
mockElementProxy.element.width = 4;
mockElementProxy.element.height = 4;
uAM(true);
expect(mockElementProxy.element.width).toEqual(1);
expect(mockElementProxy.element.height).toEqual(1);
});
});
}
);

View File

@@ -24,10 +24,12 @@ define([
"./src/MCTForm",
"./src/MCTToolbar",
"./src/MCTControl",
"./src/controllers/AutocompleteController",
"./src/controllers/DateTimeController",
"./src/controllers/CompositeController",
"./src/controllers/ColorController",
"./src/controllers/DialogButtonController",
"text!./res/templates/controls/autocomplete.html",
"text!./res/templates/controls/checkbox.html",
"text!./res/templates/controls/datetime.html",
"text!./res/templates/controls/select.html",
@@ -45,10 +47,12 @@ define([
MCTForm,
MCTToolbar,
MCTControl,
AutocompleteController,
DateTimeController,
CompositeController,
ColorController,
DialogButtonController,
autocompleteTemplate,
checkboxTemplate,
datetimeTemplate,
selectTemplate,
@@ -87,6 +91,10 @@ define([
}
],
"controls": [
{
"key": "autocomplete",
"template": autocompleteTemplate
},
{
"key": "checkbox",
"template": checkboxTemplate
@@ -137,6 +145,14 @@ define([
}
],
"controllers": [
{
"key": "AutocompleteController",
"implementation": AutocompleteController,
"depends": [
"$scope",
"$element"
]
},
{
"key": "DateTimeController",
"implementation": DateTimeController,

View File

@@ -0,0 +1,46 @@
<!--
Open MCT, Copyright (c) 2014-2017, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<div ng-controller="AutocompleteController"
class='form-control autocomplete'>
<input class="autocompleteInput"
type="text"
ng-model="ngModel[field]"
ng-change="filterOptions(ngModel[field])"
ng-click="inputClicked()"
ng-keydown="keyDown($event)"/>
<span class="icon-arrow-down"
ng-click="arrowClicked()"></span>
<div class="autocompleteOptions"
ng-init="hideOptions = true"
ng-hide="hideOptions"
mct-click-elsewhere="hideOptions = true">
<ul>
<li ng-repeat="opt in filteredOptions"
ng-click="fillInput(opt.name)"
ng-mouseover="optionMouseover(opt.optionId)"
ng-class="optionIndex === opt.optionId ? 'optionPreSelected' : ''">
<span class="optionText">{{opt.name}}</span>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,138 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
define(
[],
function () {
/**
* Controller for the `autocomplete` form control.
*
* @memberof platform/forms
* @constructor
*/
function AutocompleteController($scope, $element) {
var key = {
down: 40,
up: 38,
enter: 13
},
autocompleteInputElement = $element[0].getElementsByClassName('autocompleteInput')[0];
if ($scope.options[0].name) {
// If "options" include name, value pair
$scope.optionNames = $scope.options.map(function (opt) {
return opt.name;
});
} else {
// If options is only an array of string.
$scope.optionNames = $scope.options;
}
function fillInputWithIndexedOption() {
if ($scope.filteredOptions[$scope.optionIndex]) {
$scope.ngModel[$scope.field] = $scope.filteredOptions[$scope.optionIndex].name;
}
}
function decrementOptionIndex() {
if ($scope.optionIndex === 0) {
$scope.optionIndex = $scope.filteredOptions.length;
}
$scope.optionIndex--;
fillInputWithIndexedOption();
}
function incrementOptionIndex() {
if ($scope.optionIndex === $scope.filteredOptions.length - 1) {
$scope.optionIndex = -1;
}
$scope.optionIndex++;
fillInputWithIndexedOption();
}
function fillInputWithString(string) {
$scope.hideOptions = true;
$scope.ngModel[$scope.field] = string;
}
function showOptions(string) {
$scope.hideOptions = false;
$scope.filterOptions(string);
$scope.optionIndex = 0;
}
$scope.keyDown = function ($event) {
if ($scope.filteredOptions) {
var keyCode = $event.keyCode;
switch (keyCode) {
case key.down:
incrementOptionIndex();
break;
case key.up:
$event.preventDefault(); // Prevents cursor jumping back and forth
decrementOptionIndex();
break;
case key.enter:
if ($scope.filteredOptions[$scope.optionIndex]) {
fillInputWithString($scope.filteredOptions[$scope.optionIndex].name);
}
}
}
};
$scope.filterOptions = function (string) {
$scope.hideOptions = false;
$scope.filteredOptions = $scope.optionNames.filter(function (option) {
return option.toLowerCase().indexOf(string.toLowerCase()) >= 0;
}).map(function (option, index) {
return {
optionId: index,
name: option
};
});
};
$scope.inputClicked = function () {
autocompleteInputElement.select();
showOptions(autocompleteInputElement.value);
};
$scope.arrowClicked = function () {
autocompleteInputElement.select();
showOptions('');
};
$scope.fillInput = function (string) {
fillInputWithString(string);
};
$scope.optionMouseover = function (optionId) {
$scope.optionIndex = optionId;
};
}
return AutocompleteController;
}
);

View File

@@ -0,0 +1,69 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2017, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
define([
"../../src/controllers/AutocompleteController",
"angular"
], function (
AutocompleteController,
angular
) {
describe("The autocomplete controller", function () {
var mockScope,
mockElement,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", ["$watch"]);
mockScope.options = ['Asia/Dhaka', 'UTC', 'Toronto', 'Asia/Shanghai', 'Hotel California'];
mockScope.ngModel = [null, null, null, null, null];
mockScope.field = 4;
mockElement = angular.element("<div></div>");
controller = new AutocompleteController(mockScope, mockElement);
});
it("makes optionNames array equal to options if options is an array of string", function () {
expect(mockScope.optionNames).toEqual(mockScope.options);
});
it("filters options by returning array containing optionId and name", function () {
mockScope.filterOptions('Asia');
var filteredOptions = [{ optionId : 0, name : 'Asia/Dhaka' },
{ optionId : 1, name : 'Asia/Shanghai' }];
expect(mockScope.filteredOptions).toEqual(filteredOptions);
});
it("fills input with given string", function () {
var str = "UTC";
mockScope.fillInput(str);
expect(mockScope.hideOptions).toEqual(true);
expect(mockScope.ngModel[mockScope.field]).toEqual(str);
});
it("sets a new optionIndex on mouse hover", function () {
mockScope.optionMouseover(1);
expect(mockScope.optionIndex).toEqual(1);
});
});
});

View File

@@ -75,6 +75,21 @@ define(
].join(""));
}
//Log an info: defaults to "no service provide by"
function info(extension, category, message) {
var msg = message || "No service provided by";
$log.info([
msg,
" ",
category,
" ",
extension.key,
" from bundle ",
(extension.bundle || { path: "unknown bundle" }).path,
"; skipping."
].join(""));
}
// Echo arguments; used to represent groups of non-built-in
// extensions as a single dependency.
function echoMany() {
@@ -161,13 +176,13 @@ define(
name = makeName("aggregator", service, index);
if (!service) {
return warn(aggregator, "aggregator");
return info(aggregator, "aggregator");
}
// Aggregators need other services to aggregate, otherwise they
// do nothing.
if (!latest[service]) {
return warn(
return info(
aggregator,
"aggregator",
"No services to aggregate for"

View File

@@ -195,7 +195,8 @@ define(
expect(mockApp.service).not.toHaveBeenCalled();
// Should have gotten one warning for each skipped component
expect(mockLog.warn.calls.length).toEqual(3);
expect(mockLog.warn.calls.length).toEqual(2);
expect(mockLog.info.calls.length).toEqual(1);
});
it("warns about and skips aggregators with zero providers", function () {
@@ -217,7 +218,7 @@ define(
expect(mockApp.service).not.toHaveBeenCalled();
// Should have gotten a warning
expect(mockLog.warn).toHaveBeenCalled();
expect(mockLog.info).toHaveBeenCalled();
});
it("warns about and skips decorators with nothing to decorate", function () {

View File

@@ -115,9 +115,7 @@ define(
return (this.telemetryService =
$injector.get("telemetryService"));
} catch (e) {
// $injector should throw if telemetryService
// is unavailable or unsatisfiable.
$log.warn("Telemetry service unavailable");
$log.info("Telemetry service unavailable");
return (this.telemetryService = null);
}
};
@@ -314,4 +312,3 @@ define(
return TelemetryCapability;
}
);

View File

@@ -199,7 +199,7 @@ define(
telemetry.requestData();
expect(mockLog.warn).toHaveBeenCalled();
expect(mockLog.info).toHaveBeenCalled();
});
it("if a new style telemetry source is available, use it", function () {

View File

@@ -20,7 +20,11 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
define([
'lodash'
], function (
_
) {
// Parameter names in query string
var SEARCH = {
MODE: 'tc.mode',
@@ -73,43 +77,72 @@ define([], function () {
this.$location.search(SEARCH.END_DELTA, deltas.end);
};
TimeSettingsURLHandler.prototype.parseQueryParams = function () {
var searchParams = _.pick(this.$location.search(), _.values(SEARCH));
var parsedParams = {
clock: searchParams[SEARCH.MODE],
timeSystem: searchParams[SEARCH.TIME_SYSTEM]
};
if (!isNaN(parseInt(searchParams[SEARCH.START_DELTA], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_DELTA], 0xA))) {
parsedParams.clockOffsets = {
start: -searchParams[SEARCH.START_DELTA],
end: +searchParams[SEARCH.END_DELTA]
};
}
if (!isNaN(parseInt(searchParams[SEARCH.START_BOUND], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_BOUND], 0xA))) {
parsedParams.bounds = {
start: +searchParams[SEARCH.START_BOUND],
end: +searchParams[SEARCH.END_BOUND]
};
}
return parsedParams;
};
TimeSettingsURLHandler.prototype.updateTime = function () {
var searchParams = this.$location.search();
var mode = searchParams[SEARCH.MODE];
var timeSystem = searchParams[SEARCH.TIME_SYSTEM];
var clockOffsets = {
start: -searchParams[SEARCH.START_DELTA],
end: +searchParams[SEARCH.END_DELTA]
};
var bounds = {
start: +searchParams[SEARCH.START_BOUND],
end: +searchParams[SEARCH.END_BOUND]
};
var fixed = (mode === 'fixed');
var clock = fixed ? undefined : mode;
var hasDeltas =
!isNaN(parseInt(searchParams[SEARCH.START_DELTA], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_DELTA], 0xA));
var hasBounds =
!isNaN(parseInt(searchParams[SEARCH.START_BOUND], 0xA)) &&
!isNaN(parseInt(searchParams[SEARCH.END_BOUND], 0xA));
if (fixed && timeSystem && hasBounds) {
this.time.timeSystem(timeSystem, bounds);
this.time.stopClock();
var params = this.parseQueryParams();
if (_.isEqual(params, this.last)) {
return; // Do nothing;
}
this.last = params;
if (!fixed && clock && hasDeltas) {
this.time.clock(clock, clockOffsets);
this.time.timeSystem(timeSystem);
}
if (!params.timeSystem) {
this.updateQueryParams();
} else if (params.clock === 'fixed' && params.bounds) {
if (!this.time.timeSystem() ||
this.time.timeSystem().key !== params.timeSystem) {
if (hasDeltas && !fixed) {
this.time.clockOffsets(clockOffsets);
}
this.time.timeSystem(
params.timeSystem,
params.bounds
);
} else if (!_.isEqual(this.time.bounds(), params.bounds)) {
this.time.bounds(params.bounds);
}
if (this.time.clock()) {
this.time.stopClock();
}
} else if (params.clockOffsets) {
if (params.clock === 'fixed') {
this.time.stopClock();
return;
}
if (!this.time.clock() ||
this.time.clock().key !== params.clock) {
if (hasBounds && fixed) {
this.time.bounds(bounds);
this.time.clock(params.clock, params.clockOffsets);
} else if (!_.isEqual(this.time.clockOffsets(), params.clockOffsets)) {
this.time.clockOffsets(params.clockOffsets);
}
if (!this.time.timeSystem() ||
this.time.timeSystem().key !== params.timeSystem) {
this.time.timeSystem(params.timeSystem);
}
} else {
// Neither found, update from timeSystem.
this.updateQueryParams();
}
};

View File

@@ -20,23 +20,78 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./TimeSettingsURLHandler'], function (TimeSettingsURLHandler) {
define([
'./TimeSettingsURLHandler',
'../../api/time/TimeAPI'
], function (
TimeSettingsURLHandler,
TimeAPI
) {
describe("TimeSettingsURLHandler", function () {
var time;
var $location;
var $rootScope;
var search;
var handler;
var clockA;
var clockB;
var timeSystemA;
var timeSystemB;
var boundsA;
var boundsB;
var offsetsA;
var offsetsB;
var initialize;
var triggerLocationChange;
beforeEach(function () {
time = jasmine.createSpyObj('time', [
clockA = jasmine.createSpyObj('clockA', ['on', 'off']);
clockA.key = 'clockA';
clockA.currentValue = function () {
return 1000;
};
clockB = jasmine.createSpyObj('clockB', ['on', 'off']);
clockB.key = 'clockB';
clockB.currentValue = function () {
return 2000;
};
timeSystemA = {key: 'timeSystemA'};
timeSystemB = {key: 'timeSystemB'};
boundsA = {
start: 10,
end: 20
};
boundsB = {
start: 120,
end: 360
};
offsetsA = {
start: -100,
end: 0
};
offsetsB = {
start: -50,
end: 50
};
time = new TimeAPI();
[
'on',
'bounds',
'clockOffsets',
'timeSystem',
'clock',
'stopClock'
]);
].forEach(function (method) {
spyOn(time, method).andCallThrough();
});
time.addTimeSystem(timeSystemA);
time.addTimeSystem(timeSystemB);
time.addClock(clockA);
time.addClock(clockB);
$location = jasmine.createSpyObj('$location', [
'search'
]);
@@ -44,8 +99,6 @@ define(['./TimeSettingsURLHandler'], function (TimeSettingsURLHandler) {
'$on'
]);
time.timeSystem.andReturn({ key: 'test-time-system' });
search = {};
$location.search.andCallFake(function (key, value) {
if (arguments.length === 0) {
@@ -59,142 +112,465 @@ define(['./TimeSettingsURLHandler'], function (TimeSettingsURLHandler) {
return this;
});
handler = new TimeSettingsURLHandler(
time,
$location,
$rootScope
expect(time.timeSystem()).toBeUndefined();
expect(time.bounds()).toEqual({});
expect(time.clockOffsets()).toBeUndefined();
expect(time.clock()).toBeUndefined();
initialize = function () {
handler = new TimeSettingsURLHandler(
time,
$location,
$rootScope
);
expect($rootScope.$on).toHaveBeenCalledWith(
'$locationChangeSuccess',
jasmine.any(Function)
);
triggerLocationChange = $rootScope.$on.mostRecentCall.args[1];
};
});
it("initializes with missing time system", function () {
// This handles an odd transitory case where a url does not include
// a timeSystem. It's generally only experienced by those who
// based their code on the tutorial before it specified a time
// system.
search['tc.mode'] = 'clockA';
search['tc.timeSystem'] = undefined;
search['tc.startDelta'] = '123';
search['tc.endDelta'] = '456';
// We don't specify behavior right now other than "don't break."
expect(initialize).not.toThrow();
});
it("can initalize fixed mode from location", function () {
search['tc.mode'] = 'fixed';
search['tc.timeSystem'] = 'timeSystemA';
search['tc.startBound'] = '123';
search['tc.endBound'] = '456';
initialize();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemA',
{
start: 123,
end: 456
}
);
});
['bounds', 'timeSystem', 'clock', 'clockOffsets'].forEach(function (event) {
it("listens for " + event + " time events", function () {
expect(time.on)
.toHaveBeenCalledWith(event, jasmine.any(Function));
});
it("can initialize clock mode from location", function () {
search['tc.mode'] = 'clockA';
search['tc.timeSystem'] = 'timeSystemA';
search['tc.startDelta'] = '123';
search['tc.endDelta'] = '456';
describe("when " + event + " time event occurs with no clock", function () {
var expected;
initialize();
beforeEach(function () {
expected = {
'tc.mode': 'fixed',
'tc.timeSystem': 'test-time-system',
'tc.startBound': '123',
'tc.endBound': '456'
};
time.clock.andReturn(undefined);
time.bounds.andReturn({ start: 123, end: 456 });
time.on.calls.forEach(function (call) {
if (call.args[0] === event) {
call.args[1]();
}
});
});
it("updates query parameters for fixed mode", function () {
expect(search).toEqual(expected);
});
});
describe("when " + event + " time event occurs with no time system", function () {
beforeEach(function () {
time.timeSystem.andReturn(undefined);
time.on.calls.forEach(function (call) {
if (call.args[0] === event) {
call.args[1]();
}
});
});
it("clears the time system from the URL", function () {
expect(search['tc.timeSystem']).toBeUndefined();
});
});
describe("when " + event + " time event occurs with a clock", function () {
var expected;
beforeEach(function () {
expected = {
'tc.mode': 'clocky',
'tc.timeSystem': 'test-time-system',
'tc.startDelta': '123',
'tc.endDelta': '456'
};
time.clock.andReturn({ key: 'clocky' });
time.clockOffsets.andReturn({ start: -123, end: 456 });
time.on.calls.forEach(function (call) {
if (call.args[0] === event) {
call.args[1]();
}
});
});
it("updates query parameters for realtime mode", function () {
expect(search).toEqual(expected);
});
});
});
it("listens for location changes", function () {
expect($rootScope.$on)
.toHaveBeenCalledWith('$locationChangeSuccess', jasmine.any(Function));
});
[false, true].forEach(function (fixed) {
var name = fixed ? "fixed-time" : "real-time";
var suffix = fixed ? 'Bound' : 'Delta';
describe("when " + name + " location changes occur", function () {
beforeEach(function () {
search['tc.mode'] = fixed ? 'fixed' : 'clocky';
search['tc.timeSystem'] = 'some-time-system';
search['tc.start' + suffix] = '12321';
search['tc.end' + suffix] = '32123';
$rootScope.$on.mostRecentCall.args[1]();
});
if (fixed) {
var bounds = { start: 12321, end: 32123 };
it("stops the clock", function () {
expect(time.stopClock).toHaveBeenCalled();
});
it("sets the bounds", function () {
expect(time.bounds).toHaveBeenCalledWith(bounds);
});
it("sets the time system with bounds", function () {
expect(time.timeSystem).toHaveBeenCalledWith(
search['tc.timeSystem'],
bounds
);
});
} else {
var clockOffsets = { start: -12321, end: 32123 };
it("sets the clock", function () {
expect(time.stopClock).not.toHaveBeenCalled();
expect(time.clock).toHaveBeenCalledWith(
search['tc.mode'],
clockOffsets
);
});
it("sets clock offsets", function () {
expect(time.clockOffsets)
.toHaveBeenCalledWith(clockOffsets);
});
it("sets the time system without bounds", function () {
expect(time.timeSystem).toHaveBeenCalledWith(
search['tc.timeSystem']
);
});
expect(time.clock).toHaveBeenCalledWith(
'clockA',
{
start: -123,
end: 456
}
);
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemA'
);
});
it("can initialize fixed mode from time API", function () {
time.timeSystem(timeSystemA.key, boundsA);
initialize();
expect($location.search)
.toHaveBeenCalledWith('tc.mode', 'fixed');
expect($location.search)
.toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA');
expect($location.search)
.toHaveBeenCalledWith('tc.startBound', 10);
expect($location.search)
.toHaveBeenCalledWith('tc.endBound', 20);
expect($location.search)
.toHaveBeenCalledWith('tc.startDelta', null);
expect($location.search)
.toHaveBeenCalledWith('tc.endDelta', null);
});
it("can initialize clock mode from time API", function () {
time.clock(clockA.key, offsetsA);
time.timeSystem(timeSystemA.key);
initialize();
expect($location.search)
.toHaveBeenCalledWith('tc.mode', 'clockA');
expect($location.search)
.toHaveBeenCalledWith('tc.timeSystem', 'timeSystemA');
expect($location.search)
.toHaveBeenCalledWith('tc.startBound', null);
expect($location.search)
.toHaveBeenCalledWith('tc.endBound', null);
expect($location.search)
.toHaveBeenCalledWith('tc.startDelta', 100);
expect($location.search)
.toHaveBeenCalledWith('tc.endDelta', 0);
});
describe('location changes in fixed mode', function () {
beforeEach(function () {
time.timeSystem(timeSystemA.key, boundsA);
initialize();
time.timeSystem.reset();
time.bounds.reset();
time.clock.reset();
time.stopClock.reset();
});
it("does not change on spurious location change", function () {
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
'timeSystemA',
jasmine.any(Object)
);
expect(time.bounds).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.stopClock).not.toHaveBeenCalled();
});
it("updates timeSystem changes", function () {
search['tc.timeSystem'] = 'timeSystemB';
triggerLocationChange();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB',
{
start: 10,
end: 20
}
);
});
it("updates bounds changes", function () {
search['tc.startBound'] = '100';
search['tc.endBound'] = '200';
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.any(), jasmine.any()
);
expect(time.bounds).toHaveBeenCalledWith({
start: 100,
end: 200
});
search['tc.endBound'] = '300';
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.any(), jasmine.any()
);
expect(time.bounds).toHaveBeenCalledWith({
start: 100,
end: 300
});
});
it("updates clock mode w/o timeSystem change", function () {
search['tc.mode'] = 'clockA';
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
delete search['tc.endBound'];
delete search['tc.startBound'];
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockA',
{
start: -50,
end: 50
}
);
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.any(), jasmine.any()
);
});
it("updates clock mode and timeSystem", function () {
search['tc.mode'] = 'clockA';
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
search['tc.timeSystem'] = 'timeSystemB';
delete search['tc.endBound'];
delete search['tc.startBound'];
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockA',
{
start: -50,
end: 50
}
);
expect(time.timeSystem).toHaveBeenCalledWith('timeSystemB');
});
});
describe('location changes in clock mode', function () {
beforeEach(function () {
time.clock(clockA.key, offsetsA);
time.timeSystem(timeSystemA.key);
initialize();
time.timeSystem.reset();
time.bounds.reset();
time.clock.reset();
time.clockOffsets.reset();
time.stopClock.reset();
});
it("does not change on spurious location change", function () {
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
'timeSystemA',
jasmine.any(Object)
);
expect(time.clockOffsets).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.clock).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.bounds).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("changes time system", function () {
search['tc.timeSystem'] = 'timeSystemB';
triggerLocationChange();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB'
);
expect(time.clockOffsets).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.clock).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
expect(time.stopClock).not.toHaveBeenCalled();
expect(time.bounds).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("changes offsets", function () {
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
triggerLocationChange();
expect(time.timeSystem).not.toHaveBeenCalledWith(
'timeSystemA',
jasmine.any(Object)
);
expect(time.clockOffsets).toHaveBeenCalledWith(
{
start: -50,
end: 50
}
);
expect(time.clock).not.toHaveBeenCalledWith(
jasmine.any(Object)
);
});
it("updates to fixed w/o timeSystem change", function () {
search['tc.mode'] = 'fixed';
search['tc.startBound'] = '234';
search['tc.endBound'] = '567';
delete search['tc.endDelta'];
delete search['tc.startDelta'];
triggerLocationChange();
expect(time.stopClock).toHaveBeenCalled();
expect(time.bounds).toHaveBeenCalledWith({
start: 234,
end: 567
});
expect(time.timeSystem).not.toHaveBeenCalledWith(
jasmine.any(), jasmine.any()
);
});
it("updates fixed and timeSystem", function () {
search['tc.mode'] = 'fixed';
search['tc.startBound'] = '234';
search['tc.endBound'] = '567';
search['tc.timeSystem'] = 'timeSystemB';
delete search['tc.endDelta'];
delete search['tc.startDelta'];
triggerLocationChange();
expect(time.stopClock).toHaveBeenCalled();
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB',
{
start: 234,
end: 567
}
);
});
it("updates clock", function () {
search['tc.mode'] = 'clockB';
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockB',
{
start: -100,
end: 0
}
);
expect(time.timeSystem).not.toHaveBeenCalledWith(jasmine.any());
});
it("updates clock and timeSystem", function () {
search['tc.mode'] = 'clockB';
search['tc.timeSystem'] = 'timeSystemB';
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockB',
{
start: -100,
end: 0
}
);
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB'
);
});
it("updates clock and timeSystem and offsets", function () {
search['tc.mode'] = 'clockB';
search['tc.timeSystem'] = 'timeSystemB';
search['tc.startDelta'] = '50';
search['tc.endDelta'] = '50';
triggerLocationChange();
expect(time.clock).toHaveBeenCalledWith(
'clockB',
{
start: -50,
end: 50
}
);
expect(time.timeSystem).toHaveBeenCalledWith(
'timeSystemB'
);
});
it("stops the clock", function () {
// this is a robustness test, unsure if desired, requires
// user to be manually editing location strings.
search['tc.mode'] = 'fixed';
triggerLocationChange();
expect(time.stopClock).toHaveBeenCalled();
});
});
describe("location updates from time API in fixed", function () {
beforeEach(function () {
time.timeSystem(timeSystemA.key, boundsA);
initialize();
});
it("updates on bounds change", function () {
time.bounds(boundsB);
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '120',
'tc.endBound': '360',
'tc.timeSystem': 'timeSystemA'
});
});
it("updates on timeSystem change", function () {
time.timeSystem(timeSystemB, boundsA);
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '10',
'tc.endBound': '20',
'tc.timeSystem': 'timeSystemB'
});
time.timeSystem(timeSystemA, boundsB);
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '120',
'tc.endBound': '360',
'tc.timeSystem': 'timeSystemA'
});
});
it("Updates to clock", function () {
time.clock(clockA, offsetsA);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '100',
'tc.endDelta': '0',
'tc.timeSystem': 'timeSystemA'
});
});
});
describe("location updates from time API in fixed", function () {
beforeEach(function () {
time.clock(clockA.key, offsetsA);
time.timeSystem(timeSystemA.key);
initialize();
});
it("updates offsets", function () {
time.clockOffsets(offsetsB);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '50',
'tc.endDelta': '50',
'tc.timeSystem': 'timeSystemA'
});
});
it("updates clocks", function () {
time.clock(clockB, offsetsA);
expect(search).toEqual({
'tc.mode': 'clockB',
'tc.startDelta': '100',
'tc.endDelta': '0',
'tc.timeSystem': 'timeSystemA'
});
time.clock(clockA, offsetsB);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '50',
'tc.endDelta': '50',
'tc.timeSystem': 'timeSystemA'
});
});
it("updates timesystems", function () {
time.timeSystem(timeSystemB);
expect(search).toEqual({
'tc.mode': 'clockA',
'tc.startDelta': '100',
'tc.endDelta': '0',
'tc.timeSystem': 'timeSystemB'
});
});
it("stops the clock", function () {
time.stopClock();
expect(search).toEqual({
'tc.mode': 'fixed',
'tc.startBound': '900',
'tc.endBound': '1000',
'tc.timeSystem': 'timeSystemA'
});
});
});
});
});

View File

@@ -58,6 +58,7 @@ requirejs.config({
"html2canvas": "bower_components/html2canvas/build/html2canvas.min",
"moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"moment-timezone": "bower_components/moment-timezone/builds/moment-timezone-with-data",
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",