diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..5f2fb18d84 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,298 @@ +# Contributing to Open MCT Web + +This document describes the process of contributing to Open MCT Web as well +as the standards that will be applied when evaluating contributions. + +Please be aware that additional agreements will be necessary before we can +accept changes from external contributors. + +## Summary + +The short version: + +1. Write your contribution. +2. Make sure your contribution meets code, test, and commit message + standards as described below. +3. Submit a pull request from a topic branch back to `master`. Include a check + list, as described below. (Optionally, assign this to a specific member + for review.) +4. Respond to any discussion. When the reviewer decides it's ready, they + will merge back `master` and fill out their own check list. + +## Contribution Process + +Open MCT Web uses git for software version control, and for branching and +merging. The central repository is at +https://github.com/nasa/openmctweb.git. + +### Roles + +References to roles are made throughout this document. These are not intended +to reflect titles or long-term job assignments; rather, these are used as +descriptors to refer to members of the development team performing tasks in +the check-in process. These roles are: + +* _Author_: The individual who has made changes to files in the software + repository, and wishes to check these in. +* _Reviewer_: The individual who reviews changes to files before they are + checked in. +* _Integrator_: The individual who performs the task of merging these files. + Usually the reviewer. + +### Branching + +Three basic types of branches may be included in the above repository: + +1. Master branch. +2. Topic branches. +3. Developer branches. + +Branches which do not fit into the above categories may be created and used +during the course of development for various reasons, such as large-scale +refactoring of code or implementation of complex features which may cause +instability. In these exceptional cases it is the responsibility of the +developer who initiates the task which motivated this branching to +communicate to the team the role of these branches and any associated +procedures for the duration of their use. + +#### Master Branch + +The role of the `master` branches is to represent the latest +"ready for test" version of the software. Source code on the master +branch has undergone peer review, and will undergo regular automated +testing with notification on failure. Master branches may be unstable +(particularly for recent features), but the intent is for the stability of +any features on master branches to be non-decreasing. It is the shared +responsibility of authors, reviewers, and integrators to ensure this. + +#### Topic Branches + +Topic branches are used by developers to perform and record work on issues. + +Topic branches need not necessarily be stable, even when pushed to the +central repository; in fact, the practice of making incremental commits +while working on an issue and pushing these to the central repository is +encouraged, to avoid lost work and to share work-in-progress. (Small commits +also help isolate changes, which can help in identifying which change +introduced a defect, particularly when that defect went unnoticed for some +time, e.g. using `git bisect`.) + +Topic branches should be named according to their corresponding issue +identifiers, all lower case, without hyphens. (e.g. branch mct9 would refer +to issue #9.) + +In some cases, work on an issue may warrant the use of multiple divergent +branches; for instance, when a developer wants to try more than one solution +and compare them, or when a "dead end" is reached and an initial approach to +resolving an issue needs to be abandoned. In these cases, a short suffix +should be added to the additional branches; this may be simply a single +character (e.g. wtd481b) or, where useful, a descriptive term for what +distinguishes the branches (e.g. wtd481verbose). It is the responsibility of +the author to communicate which branch is intended to be merged to both the +reviewer and the integrator. + +#### Developer Branches + +Developer branches are any branches used for purposes outside of the scope +of the above; e.g. to try things out, or maintain a "my latest stuff" +branch that is not delayed by the review and integration process. These +may be pushed to the central repository, and may follow any naming convention +desired so long as the owner of the branch is identifiable, and so long as +the name chosen could not be mistaken for a topic or master branch. + +### Merging + +When development is complete on an issue, the first step toward merging it +back into the master branch is to file a Pull Request. The contributions +should meet code, test, and commit message standards as described below, +and the pull request should include a completed author checklist, also +as described below. Pull requests may be assigned to specific team +members when appropriate (e.g. to draw to a specific person's attention.) + +Code review should take place using discussion features within the pull +request. When the reviewer is satisfied, they should add a comment to +the pull request containing the reviewer checklist (from below) and complete +the merge back to the master branch. + +## Standards + +Contributions to Open MCT Web are expected to meet the following standards. +In addition, reviewers should use general discretion before accepting +changes. + +### Code Standards + +JavaScript sources in Open MCT Web must satisfy JSLint under its default +settings. This is verified by the command line build. + +#### Code Guidelines + +JavaScript sources in Open MCT Web should: + +* Use four spaces for indentation. Tabs should not be used. +* Include JSDoc for any exposed API (e.g. public methods, constructors.) +* Include non-JSDoc comments as-needed for explaining private variables, + methods, or algorithms when they are non-obvious. +* Define one public class per script, expressed as a constructor function + returned from an AMD-style module. +* Follow “Java-like” naming conventions. These includes: + * Classes should use camel case, first letter capitalized + (e.g. SomeClassName.) + * Methods, variables, fields, and function names should use camel case, + first letter lower-case (e.g. someVariableName.) Constants + (variables or fields which are meant to be declared and initialized + statically, and never changed) should use only capital letters, with + underscores between words (e.g. SOME_CONSTANT.) + * File name should be the name of the exported class, plus a .js extension + (e.g. SomeClassName.js) +* Avoid anonymous functions, except when functions are short (a few lines) + and/or their inclusion makes sense within the flow of the code + (e.g. as arguments to a forEach call.) +* Avoid deep nesting (especially of functions), except where necessary + (e.g. due to closure scope.) +* End with a single new-line character. +* Expose public methods by declaring them on the class's prototype. +* Within a given function's scope, do not mix declarations and imperative + code, and present these in the following order: + * First, variable declarations and initialization. + * Second, function declarations. + * Third, imperative statements. + * Finally, the returned value. + +Deviations from Open MCT Web code style guidelines require two-party agreement, +typically from the author of the change and its reviewer. + +#### Code Example + +```js +/*global define*/ + +/** + * Bundles should declare themselves as namespaces in whichever source + * file is most like the "main point of entry" to the bundle. + * @namespace some/bundle + */ +define( + ['./OtherClass'], + function (OtherClass) { + "use strict"; + + /** + * A summary of how to use this class goes here. + * + * @constructor + * @memberof some/bundle + */ + function ExampleClass() { + } + + // Methods which are not intended for external use should + // not have JSDoc (or should be marked @private) + ExampleClass.prototype.privateMethod = function () { + }; + + /** + * A summary of this method goes here. + * @param {number} n a parameter + * @returns {number} a return value + */ + ExampleClass.prototype.publicMethod = function (n) { + return n * 2; + } + + return ExampleClass; + } +); +``` + +### Test Standards + +Automated testing shall occur whenever changes are merged into the main +development branch and must be confirmed alongside any pull request. + +Automated tests are typically unit tests which exercise individual software +components. Tests are subject to code review along with the actual +implementation, to ensure that tests are applicable and useful. + +Examples of useful tests: +* Tests which replicate bugs (or their root causes) to verify their + resolution. +* Tests which reflect details from software specifications. +* Tests which exercise edge or corner cases among inputs. +* Tests which verify expected interactions with other components in the + system. + +During automated testing, code coverage metrics will be reported. Line +coverage must remain at or above 80%. + +### Commit Message Standards + +Commit messages should: + +* Contain a one-line subject, followed by one line of white space, + followed by one or more descriptive paragraphs, each separated by one + line of white space. +* Contain a short (usually one word) reference to the feature or subsystem + the commit effects, in square brackets, at the start of the subject line + (e.g. `[Documentation] Draft of check-in process`) +* Contain a reference to a relevant issue number in the body of the commit. + * This is important for traceability; while branch names also provide this, + you cannot tell from looking at a commit what branch it was authored on. +* Describe the change that was made, and any useful rationale therefore. + * Comments in code should explain what things do, commit messages describe + how they came to be done that way. +* Provide sufficient information for a reviewer to understand the changes + made and their relationship to previous code. + +Commit messages should not: + +* Exceed 54 characters in length on the subject line. +* Exceed 72 characters in length in the body of the commit. + * Except where necessary to maintain the structure of machine-readable or + machine-generated text (e.g. error messages) + +See [Contributing to a Project](http://git-scm.com/book/ch5-2.html) from +Pro Git by Shawn Chacon and Ben Straub for a bit of the rationale behind +these standards. + +## Issue Reporting + +Issues are tracked at https://github.com/nasa/openmctweb/issues + +Issues should include: + +* A short description of the issue encountered. +* A longer-form description of the issue encountered. When possible, steps to + reproduce the issue. +* When possible, a description of the impact of the issue. What use case does + it impede? +* An assessment of the severity of the issue. + +Issue severity is categorized as follows (in ascending order): + +* _Trivial_: Minimal impact on the usefulness and functionality of the + software; a "nice-to-have." +* _(Unspecified)_: Major loss of functionality or impairment of use. +* _Critical_: Large-scale loss of functionality or impairment of use, + such that remaining utility becomes marginal. +* _Blocker_: Harmful or otherwise unacceptable behavior. Must fix. + +## Check Lists + +The following check lists should be completed and attached to pull requests +when they are filed (author checklist) and when they are merged (reviewer +checklist.) + +### Author Checklist + +1. Changes address original issue? +2. Unit tests included and/or updated with changes? +3. Command line build passes? +4. Expect to pass code review? + +### Reviewer Checklist + +1. Changes appear to address issue? +2. Appropriate unit tests included? +3. Code style and in-line documentation are appropriate? +4. Commit messages meet standards? diff --git a/jsdoc.json b/jsdoc.json index ed0fddcf31..f913b650d1 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -1,10 +1,12 @@ { "source": { "include": [ - "example/", "platform/" ], - "includePattern": "(example|platform)/.+\\.js$", + "includePattern": "platform/.+\\.js$", "excludePattern": ".+\\Spec\\.js$|lib/.+" - } -} \ No newline at end of file + }, + "plugins": [ + "plugins/markdown" + ] +} diff --git a/platform/commonUI/about/src/AboutController.js b/platform/commonUI/about/src/AboutController.js index 7fc61c8cc2..dffd9b9471 100644 --- a/platform/commonUI/about/src/AboutController.js +++ b/platform/commonUI/about/src/AboutController.js @@ -21,6 +21,11 @@ *****************************************************************************/ /*global define*/ + +/** + * Implements Open MCT Web's About dialog. + * @namespace platform/commonUI/about + */ define( [], function () { @@ -29,35 +34,36 @@ define( /** * The AboutController provides information to populate the * About dialog. + * @memberof platform/commonUI/about * @constructor * @param {object[]} versions an array of version extensions; * injected from `versions[]` * @param $window Angular-injected window object */ function AboutController(versions, $window) { - return { - /** - * Get version info. This is given as an array of - * objects, where each object is intended to appear - * as a line-item in the version information listing. - * @memberof AboutController# - * @returns {object[]} version information - */ - versions: function () { - return versions; - }, - /** - * Open a new window (or tab, depending on browser - * configuration) containing open source licenses. - * @memberof AboutController# - */ - openLicenses: function () { - // Open a new browser window at the licenses route - $window.open("#/licenses"); - } - }; + this.versionDefinitions = versions; + this.$window = $window; } + /** + * Get version info. This is given as an array of + * objects, where each object is intended to appear + * as a line-item in the version information listing. + * @returns {object[]} version information + */ + AboutController.prototype.versions = function () { + return this.versionDefinitions; + }; + + /** + * Open a new window (or tab, depending on browser + * configuration) containing open source licenses. + */ + AboutController.prototype.openLicenses = function () { + // Open a new browser window at the licenses route + this.$window.open("#/licenses"); + }; + return AboutController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/about/src/LicenseController.js b/platform/commonUI/about/src/LicenseController.js index 1d996596aa..740124641f 100644 --- a/platform/commonUI/about/src/LicenseController.js +++ b/platform/commonUI/about/src/LicenseController.js @@ -29,20 +29,22 @@ define( /** * Provides extension-introduced licenses information to the * licenses route. + * @memberof platform/commonUI/about * @constructor */ function LicenseController(licenses) { - return { - /** - * Get license information. - * @returns {Array} license extensions - */ - licenses: function () { - return licenses; - } - }; + this.licenseDefinitions = licenses; } + /** + * Get license information. + * @returns {Array} license extensions + * @memberof platform/commonUI/about.LicenseController# + */ + LicenseController.prototype.licenses = function () { + return this.licenseDefinitions; + }; + return LicenseController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/about/src/LogoController.js b/platform/commonUI/about/src/LogoController.js index a688e96acf..85909a0552 100644 --- a/platform/commonUI/about/src/LogoController.js +++ b/platform/commonUI/about/src/LogoController.js @@ -29,21 +29,23 @@ define( /** * The LogoController provides functionality to the application * logo in the bottom-right of the user interface. + * @memberof platform/commonUI/about * @constructor * @param {OverlayService} overlayService the overlay service */ function LogoController(overlayService) { - return { - /** - * Display the About dialog. - * @memberof LogoController# - */ - showAboutDialog: function () { - overlayService.createOverlay("overlay-about"); - } - }; + this.overlayService = overlayService; } + /** + * Display the About dialog. + * @memberof LogoController# + * @memberof platform/commonUI/about.LogoController# + */ + LogoController.prototype.showAboutDialog = function () { + this.overlayService.createOverlay("overlay-about"); + }; + return LogoController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/BrowseController.js b/platform/commonUI/browse/src/BrowseController.js index b24eb1dba1..543763aaea 100644 --- a/platform/commonUI/browse/src/BrowseController.js +++ b/platform/commonUI/browse/src/BrowseController.js @@ -22,7 +22,8 @@ /*global define,Promise*/ /** - * Module defining BrowseController. Created by vwoeltje on 11/7/14. + * This bundle implements Browse mode. + * @namespace platform/commonUI/browse */ define( [], @@ -39,6 +40,7 @@ define( * which Angular templates first have access to the domain object * hierarchy. * + * @memberof platform/commonUI/browse * @constructor */ function BrowseController($scope, $route, $location, objectService, navigationService, urlService) { @@ -162,3 +164,4 @@ define( return BrowseController; } ); + diff --git a/platform/commonUI/browse/src/BrowseObjectController.js b/platform/commonUI/browse/src/BrowseObjectController.js index 3f450e9352..8e48cdb905 100644 --- a/platform/commonUI/browse/src/BrowseObjectController.js +++ b/platform/commonUI/browse/src/BrowseObjectController.js @@ -29,6 +29,7 @@ define( /** * Controller for the `browse-object` representation of a domain * object (the right-hand side of Browse mode.) + * @memberof platform/commonUI/browse * @constructor */ function BrowseObjectController($scope, $location, $route, $window) { @@ -81,3 +82,4 @@ define( return BrowseObjectController; } ); + diff --git a/platform/commonUI/browse/src/MenuArrowController.js b/platform/commonUI/browse/src/MenuArrowController.js index 86cad25c0e..5c4916e099 100644 --- a/platform/commonUI/browse/src/MenuArrowController.js +++ b/platform/commonUI/browse/src/MenuArrowController.js @@ -33,19 +33,29 @@ define( * A left-click on the menu arrow should display a * context menu. This controller launches the context * menu. + * @memberof platform/commonUI/browse * @constructor */ function MenuArrowController($scope) { - function showMenu(event) { - var actionContext = {key: 'menu', domainObject: $scope.domainObject, event: event}; - $scope.domainObject.getCapability('action').perform(actionContext); - } - - return { - showMenu: showMenu - }; + this.$scope = $scope; } + /** + * Show a context menu for the domain object in this scope. + * + * @param event the browser event which caused this (used to + * position the menu) + */ + MenuArrowController.prototype.showMenu = function (event) { + var actionContext = { + key: 'menu', + domainObject: this.$scope.domainObject, + event: event + }; + + this.$scope.domainObject.getCapability('action').perform(actionContext); + }; + return MenuArrowController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/creation/CreateAction.js b/platform/commonUI/browse/src/creation/CreateAction.js index ce0d6d5cd4..984b26cfe5 100644 --- a/platform/commonUI/browse/src/creation/CreateAction.js +++ b/platform/commonUI/browse/src/creation/CreateAction.js @@ -34,7 +34,10 @@ define( * domain objects of a specific type. This is the action that * is performed when a user uses the Create menu. * + * @memberof platform/commonUI/browse + * @implements {Action} * @constructor + * * @param {Type} type the type of domain object to create * @param {DomainObject} parent the domain object that should * act as a container for the newly-created object @@ -49,78 +52,84 @@ define( * of the newly-created domain object */ function CreateAction(type, parent, context, dialogService, creationService, policyService) { + this.metadata = { + key: 'create', + glyph: type.getGlyph(), + name: type.getName(), + type: type.getKey(), + description: type.getDescription(), + context: context + }; + + this.type = type; + this.parent = parent; + this.policyService = policyService; + this.dialogService = dialogService; + this.creationService = creationService; + } + + /** + * Create a new object of the given type. + * This will prompt for user input first. + */ + CreateAction.prototype.perform = function () { /* Overview of steps in object creation: 1. Show dialog - a. Prepare dialog contents - b. Invoke dialogService + a. Prepare dialog contents + b. Invoke dialogService 2. Create new object in persistence service - a. Generate UUID - b. Store model + a. Generate UUID + b. Store model 3. Mutate destination container - a. Get mutation capability - b. Add new id to composition + a. Get mutation capability + b. Add new id to composition 4. Persist destination container - a. ...use persistence capability. + a. ...use persistence capability. */ - function perform() { - // The wizard will handle creating the form model based - // on the type... - var wizard = new CreateWizard(type, parent, policyService); + // The wizard will handle creating the form model based + // on the type... + var wizard = + new CreateWizard(this.type, this.parent, this.policyService), + self = this; - // Create and persist the new object, based on user - // input. - function persistResult(formValue) { - var parent = wizard.getLocation(formValue), - newModel = wizard.createModel(formValue); - return creationService.createObject(newModel, parent); - } - - function doNothing() { - // Create cancelled, do nothing - return false; - } - - return dialogService.getUserInput( - wizard.getFormStructure(), - wizard.getInitialFormValue() - ).then(persistResult, doNothing); + // Create and persist the new object, based on user + // input. + function persistResult(formValue) { + var parent = wizard.getLocation(formValue), + newModel = wizard.createModel(formValue); + return self.creationService.createObject(newModel, parent); } - return { - /** - * Create a new object of the given type. - * This will prompt for user input first. - * @method - * @memberof CreateAction - */ - perform: perform, + function doNothing() { + // Create cancelled, do nothing + return false; + } - /** - * Get metadata about this action. This includes fields: - * * `name`: Human-readable name - * * `key`: Machine-readable identifier ("create") - * * `glyph`: Glyph to use as an icon for this action - * * `description`: Human-readable description - * * `context`: The context in which this action will be performed. - * - * @return {object} metadata about the create action - */ - getMetadata: function () { - return { - key: 'create', - glyph: type.getGlyph(), - name: type.getName(), - type: type.getKey(), - description: type.getDescription(), - context: context - }; - } - }; - } + return this.dialogService.getUserInput( + wizard.getFormStructure(), + wizard.getInitialFormValue() + ).then(persistResult, doNothing); + }; + + + /** + * Metadata associated with a Create action. + * @typedef {ActionMetadata} CreateActionMetadata + * @property {string} type the key for the type of domain object + * to be created + */ + + /** + * Get metadata about this action. + * @returns {CreateActionMetadata} metadata about this action + */ + CreateAction.prototype.getMetadata = function () { + return this.metadata; + }; return CreateAction; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/creation/CreateActionProvider.js b/platform/commonUI/browse/src/creation/CreateActionProvider.js index dcc98b8e95..4ca2bce59f 100644 --- a/platform/commonUI/browse/src/creation/CreateActionProvider.js +++ b/platform/commonUI/browse/src/creation/CreateActionProvider.js @@ -33,7 +33,10 @@ define( * The CreateActionProvider is an ActionProvider which introduces * a Create action for each creatable domain object type. * + * @memberof platform/commonUI/browse * @constructor + * @implements {ActionService} + * * @param {TypeService} typeService the type service, used to discover * available types * @param {DialogService} dialogService the dialog service, used by @@ -44,44 +47,41 @@ define( * object creation. */ function CreateActionProvider(typeService, dialogService, creationService, policyService) { - return { - /** - * Get all Create actions which are applicable in the provided - * context. - * @memberof CreateActionProvider - * @method - * @returns {CreateAction[]} - */ - getActions: function (actionContext) { - var context = actionContext || {}, - key = context.key, - destination = context.domainObject; - - // We only provide Create actions, and we need a - // domain object to serve as the container for the - // newly-created object (although the user may later - // make a different selection) - if (key !== 'create' || !destination) { - return []; - } - - // Introduce one create action per type - return typeService.listTypes().filter(function (type) { - return type.hasFeature("creation"); - }).map(function (type) { - return new CreateAction( - type, - destination, - context, - dialogService, - creationService, - policyService - ); - }); - } - }; + this.typeService = typeService; + this.dialogService = dialogService; + this.creationService = creationService; + this.policyService = policyService; } + CreateActionProvider.prototype.getActions = function (actionContext) { + var context = actionContext || {}, + key = context.key, + destination = context.domainObject, + self = this; + + // We only provide Create actions, and we need a + // domain object to serve as the container for the + // newly-created object (although the user may later + // make a different selection) + if (key !== 'create' || !destination) { + return []; + } + + // Introduce one create action per type + return this.typeService.listTypes().filter(function (type) { + return type.hasFeature("creation"); + }).map(function (type) { + return new CreateAction( + type, + destination, + context, + self.dialogService, + self.creationService, + self.policyService + ); + }); + }; + return CreateActionProvider; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/creation/CreateMenuController.js b/platform/commonUI/browse/src/creation/CreateMenuController.js index 2dace415df..624764c2e4 100644 --- a/platform/commonUI/browse/src/creation/CreateMenuController.js +++ b/platform/commonUI/browse/src/creation/CreateMenuController.js @@ -34,6 +34,7 @@ define( * set of Create actions based on the currently-selected * domain object. * + * @memberof platform/commonUI/browse * @constructor */ function CreateMenuController($scope) { @@ -55,4 +56,4 @@ define( return CreateMenuController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/creation/CreateWizard.js b/platform/commonUI/browse/src/creation/CreateWizard.js index 29fe953e18..4073ed7e90 100644 --- a/platform/commonUI/browse/src/creation/CreateWizard.js +++ b/platform/commonUI/browse/src/creation/CreateWizard.js @@ -21,12 +21,6 @@ *****************************************************************************/ /*global define*/ -/** - * Defines the CreateWizard, used by the CreateAction to - * populate the form shown in dialog based on the created type. - * - * @module core/action/create-wizard - */ define( function () { 'use strict'; @@ -37,113 +31,118 @@ define( * @param {TypeImpl} type the type of domain object to be created * @param {DomainObject} parent the domain object to serve as * the initial parent for the created object, in the dialog + * @memberof platform/commonUI/browse * @constructor - * @memberof module:core/action/create-wizard */ function CreateWizard(type, parent, policyService) { - var model = type.getInitialModel(), - properties = type.getProperties(); + this.type = type; + this.model = type.getInitialModel(); + this.properties = type.getProperties(); + this.parent = parent; + this.policyService = policyService; + } + + /** + * Get the form model for this wizard; this is a description + * that will be rendered to an HTML form. See the + * platform/forms bundle + * + * @return {FormModel} formModel the form model to + * show in the create dialog + */ + CreateWizard.prototype.getFormStructure = function () { + var sections = [], + type = this.type, + policyService = this.policyService; function validateLocation(locatingObject) { var locatingType = locatingObject && - locatingObject.getCapability('type'); + locatingObject.getCapability('type'); return locatingType && policyService.allow( - "composition", - locatingType, - type - ); + "composition", + locatingType, + type + ); } + sections.push({ + name: "Properties", + rows: this.properties.map(function (property, index) { + // Property definition is same as form row definition + var row = Object.create(property.getDefinition()); + + // Use index as the key into the formValue; + // this correlates to the indexing provided by + // getInitialFormValue + row.key = index; + + return row; + }) + }); + + // Ensure there is always a "save in" section + sections.push({ name: 'Location', rows: [{ + name: "Save In", + control: "locator", + validate: validateLocation, + key: "createParent" + }]}); + return { - /** - * Get the form model for this wizard; this is a description - * that will be rendered to an HTML form. See the - * platform/forms bundle - * - * @return {FormModel} formModel the form model to - * show in the create dialog - */ - getFormStructure: function () { - var sections = []; - - sections.push({ - name: "Properties", - rows: properties.map(function (property, index) { - // Property definition is same as form row definition - var row = Object.create(property.getDefinition()); - - // Use index as the key into the formValue; - // this correlates to the indexing provided by - // getInitialFormValue - row.key = index; - - return row; - }) - }); - - // Ensure there is always a "save in" section - sections.push({ name: 'Location', rows: [{ - name: "Save In", - control: "locator", - validate: validateLocation, - key: "createParent" - }]}); - - return { - sections: sections, - name: "Create a New " + type.getName() - }; - }, - /** - * Get the initial value for the form being described. - * This will include the values for all properties described - * in the structure. - * - * @returns {object} the initial value of the form - */ - getInitialFormValue: function () { - // Start with initial values for properties - var formValue = properties.map(function (property) { - return property.getValue(model); - }); - - // Include the createParent - formValue.createParent = parent; - - return formValue; - }, - /** - * Based on a populated form, get the domain object which - * should be used as a parent for the newly-created object. - * @return {DomainObject} - */ - getLocation: function (formValue) { - return formValue.createParent || parent; - }, - /** - * Create the domain object model for a newly-created object, - * based on user input read from a formModel. - * @return {object} the domain object' model - */ - createModel: function (formValue) { - // Clone - var newModel = JSON.parse(JSON.stringify(model)); - - // Always use the type from the type definition - newModel.type = type.getKey(); - - // Update all properties - properties.forEach(function (property, index) { - property.setValue(newModel, formValue[index]); - }); - - return newModel; - } + sections: sections, + name: "Create a New " + this.type.getName() }; + }; + /** + * Get the initial value for the form being described. + * This will include the values for all properties described + * in the structure. + * + * @returns {object} the initial value of the form + */ + CreateWizard.prototype.getInitialFormValue = function () { + // Start with initial values for properties + var model = this.model, + formValue = this.properties.map(function (property) { + return property.getValue(model); + }); - } + // Include the createParent + formValue.createParent = this.parent; + + return formValue; + }; + + /** + * Based on a populated form, get the domain object which + * should be used as a parent for the newly-created object. + * @return {DomainObject} + */ + CreateWizard.prototype.getLocation = function (formValue) { + return formValue.createParent || this.parent; + }; + + /** + * Create the domain object model for a newly-created object, + * based on user input read from a formModel. + * @return {object} the domain object model + */ + CreateWizard.prototype.createModel = function (formValue) { + // Clone + var newModel = JSON.parse(JSON.stringify(this.model)); + + // Always use the type from the type definition + newModel.type = this.type.getKey(); + + // Update all properties + this.properties.forEach(function (property, index) { + property.setValue(newModel, formValue[index]); + }); + + return newModel; + }; return CreateWizard; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/creation/CreationService.js b/platform/commonUI/browse/src/creation/CreationService.js index 17cc5ce6b3..110a49c216 100644 --- a/platform/commonUI/browse/src/creation/CreationService.js +++ b/platform/commonUI/browse/src/creation/CreationService.js @@ -39,15 +39,45 @@ define( * persisting new domain objects. Handles all actual object * mutation and persistence associated with domain object * creation. + * @memberof platform/commonUI/browse * @constructor */ function CreationService(persistenceService, $q, $log) { + this.persistenceService = persistenceService; + this.$q = $q; + this.$log = $log; + } + + /** + * Create a new domain object with the provided model, as + * a member of the provided parent domain object's composition. + * This parent will additionally determine which persistence + * space an object is created within (as it is possible to + * have multiple persistence spaces attached.) + * + * @param {object} model the model for the newly-created + * domain object + * @param {DomainObject} parent the domain object which + * should contain the newly-created domain object + * in its composition + * @return {Promise} a promise that will resolve when the domain + * object has been created + */ + CreationService.prototype.createObject = function (model, parent) { + var persistence = parent.getCapability("persistence"), + self = this; + + // Store the location of an object relative to it's parent. + function addLocationToModel(modelId, model, parent) { + model.location = parent.getId(); + return model; + } // Persist the new domain object's model; it will be fully // constituted as a domain object when loaded back, as all // domain object models are. function doPersist(space, id, model) { - return persistenceService.createObject( + return self.persistenceService.createObject( space, id, model @@ -66,14 +96,14 @@ define( } } else { // This is abnormal; composition should be an array - $log.warn(NO_COMPOSITION_WARNING + parent.getId()); + self.$log.warn(NO_COMPOSITION_WARNING + parent.getId()); return false; // Cancel mutation } }); - return $q.when(mutatationResult).then(function (result) { + return self.$q.when(mutatationResult).then(function (result) { if (!result) { - $log.error("Could not mutate " + parent.getId()); + self.$log.error("Could not mutate " + parent.getId()); return undefined; } @@ -93,56 +123,28 @@ define( }); } - // Store the location of an object relative to it's parent. - function addLocationToModel(modelId, model, parent) { - model.location = parent.getId(); - return model; + // We need the parent's persistence capability to determine + // what space to create the new object's model in. + if (!persistence) { + self.$log.warn(NON_PERSISTENT_WARNING); + return self.$q.reject(new Error(NON_PERSISTENT_WARNING)); } - // Create a new domain object with the provided model as a - // member of the specified parent's composition - function createObject(model, parent) { - var persistence = parent.getCapability("persistence"); + // We create a new domain object in three sequential steps: + // 1. Get a new UUID for the object + // 2. Create a model with that ID in the persistence space + // 3. Add that ID to + return self.$q.when(uuid()).then(function (id) { + model = addLocationToModel(id, model, parent); + return doPersist(persistence.getSpace(), id, model); + }).then(function (id) { + return addToComposition(id, parent, persistence); + }); + }; - // We need the parent's persistence capability to determine - // what space to create the new object's model in. - if (!persistence) { - $log.warn(NON_PERSISTENT_WARNING); - return $q.reject(new Error(NON_PERSISTENT_WARNING)); - } - // We create a new domain object in three sequential steps: - // 1. Get a new UUID for the object - // 2. Create a model with that ID in the persistence space - // 3. Add that ID to - return $q.when( - uuid() - ).then(function (id) { - model = addLocationToModel(id, model, parent); - return doPersist(persistence.getSpace(), id, model); - }).then(function (id) { - return addToComposition(id, parent, persistence); - }); - } - - return { - /** - * Create a new domain object with the provided model, as - * a member of the provided parent domain object's composition. - * This parent will additionally determine which persistence - * space an object is created within (as it is possible to - * have multiple persistence spaces attached.) - * - * @param {object} model the model for the newly-created - * domain object - * @param {DomainObject} parent the domain object which - * should contain the newly-created domain object - * in its composition - */ - createObject: createObject - }; - } return CreationService; } ); + diff --git a/platform/commonUI/browse/src/creation/LocatorController.js b/platform/commonUI/browse/src/creation/LocatorController.js index c7104956ea..d6335f9bd1 100644 --- a/platform/commonUI/browse/src/creation/LocatorController.js +++ b/platform/commonUI/browse/src/creation/LocatorController.js @@ -30,6 +30,7 @@ define( * Controller for the "locator" control, which provides the * user with the ability to select a domain object as the * destination for a newly-created object in the Create menu. + * @memberof platform/commonUI/browse * @constructor */ function LocatorController($scope) { @@ -79,3 +80,4 @@ define( return LocatorController; } ); + diff --git a/platform/commonUI/browse/src/navigation/NavigateAction.js b/platform/commonUI/browse/src/navigation/NavigateAction.js index 779a83044a..7b258afafe 100644 --- a/platform/commonUI/browse/src/navigation/NavigateAction.js +++ b/platform/commonUI/browse/src/navigation/NavigateAction.js @@ -31,32 +31,34 @@ define( /** * The navigate action navigates to a specific domain object. + * @memberof platform/commonUI/browse * @constructor + * @implements {Action} */ function NavigateAction(navigationService, $q, context) { - var domainObject = context.domainObject; - - function perform() { - // Set navigation, and wrap like a promise - return $q.when(navigationService.setNavigation(domainObject)); - } - - return { - /** - * Navigate to the object described in the context. - * @returns {Promise} a promise that is resolved once the - * navigation has been updated - */ - perform: perform - }; + this.domainObject = context.domainObject; + this.$q = $q; + this.navigationService = navigationService; } + /** + * Navigate to the object described in the context. + * @returns {Promise} a promise that is resolved once the + * navigation has been updated + */ + NavigateAction.prototype.perform = function () { + // Set navigation, and wrap like a promise + return this.$q.when( + this.navigationService.setNavigation(this.domainObject) + ); + }; + /** * Navigate as an action is only applicable when a domain object * is described in the action context. * @param {ActionContext} context the context in which the action * will be performed - * @returns true if applicable + * @returns {boolean} true if applicable */ NavigateAction.appliesTo = function (context) { return context.domainObject !== undefined; @@ -64,4 +66,4 @@ define( return NavigateAction; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/navigation/NavigationService.js b/platform/commonUI/browse/src/navigation/NavigationService.js index c8c76857b8..87e5582ef7 100644 --- a/platform/commonUI/browse/src/navigation/NavigationService.js +++ b/platform/commonUI/browse/src/navigation/NavigationService.js @@ -32,68 +32,58 @@ define( /** * The navigation service maintains the application's current * navigation state, and allows listening for changes thereto. + * @memberof platform/commonUI/browse * @constructor */ function NavigationService() { - var navigated, - callbacks = []; + this.navigated = undefined; + this.callbacks = []; + } - // Getter for current navigation - function getNavigation() { - return navigated; - } + /** + * Get the current navigation state. + * @returns {DomainObject} the object that is navigated-to + */ + NavigationService.prototype.getNavigation = function () { + return this.navigated; + }; - // Setter for navigation; invokes callbacks - function setNavigation(value) { - if (navigated !== value) { - navigated = value; - callbacks.forEach(function (callback) { - callback(value); - }); - } - } - - // Adds a callback - function addListener(callback) { - callbacks.push(callback); - } - - // Filters out a callback - function removeListener(callback) { - callbacks = callbacks.filter(function (cb) { - return cb !== callback; + /** + * Set the current navigation state. This will invoke listeners. + * @param {DomainObject} domainObject the domain object to navigate to + */ + NavigationService.prototype.setNavigation = function (value) { + if (this.navigated !== value) { + this.navigated = value; + this.callbacks.forEach(function (callback) { + callback(value); }); } + }; - return { - /** - * Get the current navigation state. - */ - getNavigation: getNavigation, - /** - * Set the current navigation state. Thiswill invoke listeners. - * @param {DomainObject} value the domain object to navigate - * to - */ - setNavigation: setNavigation, - /** - * Listen for changes in navigation. The passed callback will - * be invoked with the new domain object of navigation when - * this changes. - * @param {function} callback the callback to invoke when - * navigation state changes - */ - addListener: addListener, - /** - * Stop listening for changes in navigation state. - * @param {function} callback the callback which should - * no longer be invoked when navigation state - * changes - */ - removeListener: removeListener - }; - } + /** + * Listen for changes in navigation. The passed callback will + * be invoked with the new domain object of navigation when + * this changes. + * @param {function} callback the callback to invoke when + * navigation state changes + */ + NavigationService.prototype.addListener = function (callback) { + this.callbacks.push(callback); + }; + + /** + * Stop listening for changes in navigation state. + * @param {function} callback the callback which should + * no longer be invoked when navigation state + * changes + */ + NavigationService.prototype.removeListener = function (callback) { + this.callbacks = this.callbacks.filter(function (cb) { + return cb !== callback; + }); + }; return NavigationService; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/windowing/FullscreenAction.js b/platform/commonUI/browse/src/windowing/FullscreenAction.js index efba99520b..82d92fbd40 100644 --- a/platform/commonUI/browse/src/windowing/FullscreenAction.js +++ b/platform/commonUI/browse/src/windowing/FullscreenAction.js @@ -35,36 +35,32 @@ define( /** * The fullscreen action toggles between fullscreen display * and regular in-window display. + * @memberof platform/commonUI/browse * @constructor + * @implements {Action} */ function FullscreenAction(context) { - return { - /** - * Toggle full screen state - */ - perform: function () { - screenfull.toggle(); - }, - /** - * Get metadata about this action, including the - * applicable glyph to display. - */ - getMetadata: function () { - // We override getMetadata, because the glyph and - // description need to be determined at run-time - // based on whether or not we are currently - // full screen. - var metadata = Object.create(FullscreenAction); - metadata.glyph = screenfull.isFullscreen ? "_" : "z"; - metadata.description = screenfull.isFullscreen ? - EXIT_FULLSCREEN : ENTER_FULLSCREEN; - metadata.group = "windowing"; - metadata.context = context; - return metadata; - } - }; + this.context = context; } + FullscreenAction.prototype.perform = function () { + screenfull.toggle(); + }; + + FullscreenAction.prototype.getMetadata = function () { + // We override getMetadata, because the glyph and + // description need to be determined at run-time + // based on whether or not we are currently + // full screen. + var metadata = Object.create(FullscreenAction); + metadata.glyph = screenfull.isFullscreen ? "_" : "z"; + metadata.description = screenfull.isFullscreen ? + EXIT_FULLSCREEN : ENTER_FULLSCREEN; + metadata.group = "windowing"; + metadata.context = this.context; + return metadata; + }; + return FullscreenAction; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/windowing/NewTabAction.js b/platform/commonUI/browse/src/windowing/NewTabAction.js index 8616a89711..301c204bbd 100644 --- a/platform/commonUI/browse/src/windowing/NewTabAction.js +++ b/platform/commonUI/browse/src/windowing/NewTabAction.js @@ -33,35 +33,29 @@ define( /** * The new tab action allows a domain object to be opened * into a new browser tab. + * @memberof platform/commonUI/browse * @constructor + * @implements {Action} */ function NewTabAction(urlService, $window, context) { - // Returns the selected domain object - // when using the context menu or the top right button - // based on the context and the existance of the object - // It is set to object an returned - function getSelectedObject() { - var object; - if (context.selectedObject) { - object = context.selectedObject; - } else { - object = context.domainObject; - } - return object; - } - - return { - // Performs the open in new tab function - // By calling the url service, the mode needed - // (browse) and the domainObject is passed in and - // the path is returned and opened in a new tab - perform: function () { - $window.open(urlService.urlForNewTab("browse", getSelectedObject()), - "_blank"); - } + context = context || {}; + + this.urlService = urlService; + this.open = function () { + $window.open.apply($window, arguments); }; + + // Choose the object to be opened into a new tab + this.domainObject = context.selectedObject || context.domainObject; } + NewTabAction.prototype.perform = function () { + this.open( + this.urlService.urlForNewTab("browse", this.domainObject), + "_blank" + ); + }; + return NewTabAction; } -); \ No newline at end of file +); diff --git a/platform/commonUI/browse/src/windowing/WindowTitler.js b/platform/commonUI/browse/src/windowing/WindowTitler.js index 9db65e5b0e..4ce448cb1e 100644 --- a/platform/commonUI/browse/src/windowing/WindowTitler.js +++ b/platform/commonUI/browse/src/windowing/WindowTitler.js @@ -29,6 +29,7 @@ define( /** * Updates the title of the current window to reflect the name * of the currently navigated-to domain object. + * @memberof platform/commonUI/browse * @constructor */ function WindowTitler(navigationService, $rootScope, $document) { @@ -49,4 +50,4 @@ define( return WindowTitler; } -); \ No newline at end of file +); diff --git a/platform/commonUI/dialog/src/DialogService.js b/platform/commonUI/dialog/src/DialogService.js index d0c2c83f42..25e8943c06 100644 --- a/platform/commonUI/dialog/src/DialogService.js +++ b/platform/commonUI/dialog/src/DialogService.js @@ -22,7 +22,9 @@ /*global define*/ /** - * Module defining DialogService. Created by vwoeltje on 11/10/14. + * This bundle implements the dialog service, which can be used to + * launch dialogs for user input & notifications. + * @namespace platform/commonUI/dialog */ define( [], @@ -32,128 +34,130 @@ define( * The dialog service is responsible for handling window-modal * communication with the user, such as displaying forms for user * input. + * @memberof platform/commonUI/dialog * @constructor */ function DialogService(overlayService, $q, $log) { - var overlay, - dialogVisible = false; - - // Stop showing whatever overlay is currently active - // (e.g. because the user hit cancel) - function dismiss() { - if (overlay) { - overlay.dismiss(); - } - dialogVisible = false; - } - - function getDialogResponse(key, model, resultGetter) { - // We will return this result as a promise, because user - // input is asynchronous. - var deferred = $q.defer(), - overlayModel; - - // Confirm function; this will be passed in to the - // overlay-dialog template and associated with a - // OK button click - function confirm(value) { - // Pass along the result - deferred.resolve(resultGetter ? resultGetter() : value); - - // Stop showing the dialog - dismiss(); - } - - // Cancel function; this will be passed in to the - // overlay-dialog template and associated with a - // Cancel or X button click - function cancel() { - deferred.reject(); - dismiss(); - } - - // Add confirm/cancel callbacks - model.confirm = confirm; - model.cancel = cancel; - - if (dialogVisible) { - // Only one dialog should be shown at a time. - // The application design should be such that - // we never even try to do this. - $log.warn([ - "Dialog already showing; ", - "unable to show ", - model.name - ].join("")); - deferred.reject(); - } else { - // Add the overlay using the OverlayService, which - // will handle actual insertion into the DOM - overlay = overlayService.createOverlay( - key, - model - ); - - // Track that a dialog is already visible, to - // avoid spawning multiple dialogs at once. - dialogVisible = true; - } - - return deferred.promise; - } - - function getUserInput(formModel, value) { - var overlayModel = { - title: formModel.name, - message: formModel.message, - structure: formModel, - value: value - }; - - // Provide result from the model - function resultGetter() { - return overlayModel.value; - } - - // Show the overlay-dialog - return getDialogResponse( - "overlay-dialog", - overlayModel, - resultGetter - ); - } - - function getUserChoice(dialogModel) { - // Show the overlay-options dialog - return getDialogResponse( - "overlay-options", - { dialog: dialogModel } - ); - } - - return { - /** - * Request user input via a window-modal dialog. - * - * @param {FormModel} formModel a description of the form - * to be shown (see platform/forms) - * @param {object} value the initial state of the form - * @returns {Promise} a promsie for the form value that the - * user has supplied; this may be rejected if - * user input cannot be obtained (for instance, - * because the user cancelled the dialog) - */ - getUserInput: getUserInput, - /** - * Request that the user chooses from a set of options, - * which will be shown as buttons. - * - * @param dialogModel a description of the dialog to show - */ - getUserChoice: getUserChoice - }; + this.overlayService = overlayService; + this.$q = $q; + this.$log = $log; + this.overlay = undefined; + this.dialogVisible = false; } + // Stop showing whatever overlay is currently active + // (e.g. because the user hit cancel) + DialogService.prototype.dismiss = function () { + var overlay = this.overlay; + if (overlay) { + overlay.dismiss(); + } + this.dialogVisible = false; + }; + + DialogService.prototype.getDialogResponse = function (key, model, resultGetter) { + // We will return this result as a promise, because user + // input is asynchronous. + var deferred = this.$q.defer(), + self = this; + + // Confirm function; this will be passed in to the + // overlay-dialog template and associated with a + // OK button click + function confirm(value) { + // Pass along the result + deferred.resolve(resultGetter ? resultGetter() : value); + + // Stop showing the dialog + self.dismiss(); + } + + // Cancel function; this will be passed in to the + // overlay-dialog template and associated with a + // Cancel or X button click + function cancel() { + deferred.reject(); + self.dismiss(); + } + + // Add confirm/cancel callbacks + model.confirm = confirm; + model.cancel = cancel; + + if (this.dialogVisible) { + // Only one dialog should be shown at a time. + // The application design should be such that + // we never even try to do this. + this.$log.warn([ + "Dialog already showing; ", + "unable to show ", + model.name + ].join("")); + deferred.reject(); + } else { + // Add the overlay using the OverlayService, which + // will handle actual insertion into the DOM + this.overlay = this.overlayService.createOverlay( + key, + model + ); + + // Track that a dialog is already visible, to + // avoid spawning multiple dialogs at once. + this.dialogVisible = true; + } + + return deferred.promise; + }; + + /** + * Request user input via a window-modal dialog. + * + * @param {FormModel} formModel a description of the form + * to be shown (see platform/forms) + * @param {object} value the initial state of the form + * @returns {Promise} a promise for the form value that the + * user has supplied; this may be rejected if + * user input cannot be obtained (for instance, + * because the user cancelled the dialog) + */ + DialogService.prototype.getUserInput = function (formModel, value) { + var overlayModel = { + title: formModel.name, + message: formModel.message, + structure: formModel, + value: value + }; + + // Provide result from the model + function resultGetter() { + return overlayModel.value; + } + + // Show the overlay-dialog + return this.getDialogResponse( + "overlay-dialog", + overlayModel, + resultGetter + ); + }; + + /** + * Request that the user chooses from a set of options, + * which will be shown as buttons. + * + * @param dialogModel a description of the dialog to show + * @return {Promise} a promise for the user's choice + */ + DialogService.prototype.getUserChoice = function (dialogModel) { + // Show the overlay-options dialog + return this.getDialogResponse( + "overlay-options", + { dialog: dialogModel } + ); + }; + + return DialogService; } -); \ No newline at end of file +); diff --git a/platform/commonUI/dialog/src/OverlayService.js b/platform/commonUI/dialog/src/OverlayService.js index b66bffa7dc..5faba5dcf6 100644 --- a/platform/commonUI/dialog/src/OverlayService.js +++ b/platform/commonUI/dialog/src/OverlayService.js @@ -43,57 +43,63 @@ define( * particularly where a multiple-overlay effect is not specifically * desired). * + * @memberof platform/commonUI/dialog * @constructor */ function OverlayService($document, $compile, $rootScope) { - function createOverlay(key, overlayModel) { - // Create a new scope for this overlay - var scope = $rootScope.$new(), - element; + this.$compile = $compile; - // Stop showing the overlay; additionally, release the scope - // that it uses. - function dismiss() { - scope.$destroy(); - element.remove(); - } - - // If no model is supplied, just fill in a default "cancel" - overlayModel = overlayModel || { cancel: dismiss }; - - // Populate the scope; will be passed directly to the template - scope.overlay = overlayModel; - scope.key = key; - - // Create the overlay element and add it to the document's body - element = $compile(TEMPLATE)(scope); - $document.find('body').prepend(element); - - - - return { - dismiss: dismiss - }; - } - - return { - /** - * Add a new overlay to the document. This will be - * prepended to the document body; the overlay's - * template (as pointed to by the `key` argument) is - * responsible for having a useful z-order, and for - * blocking user interactions if appropriate. - * - * @param {string} key the symbolic key which identifies - * the template of the overlay to be shown - * @param {object} overlayModel the model to pass to the - * included overlay template (this will be passed - * in via ng-model) - */ - createOverlay: createOverlay + // Don't include $document and $rootScope directly; + // avoids https://docs.angularjs.org/error/ng/cpws + this.findBody = function () { + return $document.find('body'); + }; + this.newScope = function () { + return $rootScope.$new(); }; } + /** + * Add a new overlay to the document. This will be + * prepended to the document body; the overlay's + * template (as pointed to by the `key` argument) is + * responsible for having a useful z-order, and for + * blocking user interactions if appropriate. + * + * @param {string} key the symbolic key which identifies + * the template of the overlay to be shown + * @param {object} overlayModel the model to pass to the + * included overlay template (this will be passed + * in via ng-model) + */ + OverlayService.prototype.createOverlay = function (key, overlayModel) { + // Create a new scope for this overlay + var scope = this.newScope(), + element; + + // Stop showing the overlay; additionally, release the scope + // that it uses. + function dismiss() { + scope.$destroy(); + element.remove(); + } + + // If no model is supplied, just fill in a default "cancel" + overlayModel = overlayModel || { cancel: dismiss }; + + // Populate the scope; will be passed directly to the template + scope.overlay = overlayModel; + scope.key = key; + + // Create the overlay element and add it to the document's body + element = this.$compile(TEMPLATE)(scope); + this.findBody().prepend(element); + + return { + dismiss: dismiss + }; + }; + return OverlayService; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/actions/CancelAction.js b/platform/commonUI/edit/src/actions/CancelAction.js index 725f6ee6a7..a9e6effe9f 100644 --- a/platform/commonUI/edit/src/actions/CancelAction.js +++ b/platform/commonUI/edit/src/actions/CancelAction.js @@ -29,9 +29,26 @@ define( * The "Cancel" action; the action triggered by clicking Cancel from * Edit Mode. Exits the editing user interface and invokes object * capabilities to persist the changes that have been made. + * @constructor + * @memberof platform/commonUI/edit + * @implements {Action} */ function CancelAction($location, urlService, context) { - var domainObject = context.domainObject; + this.domainObject = context.domainObject; + this.$location = $location; + this.urlService = urlService; + } + + /** + * Cancel editing. + * + * @returns {Promise} a promise that will be fulfilled when + * cancellation has completed + */ + CancelAction.prototype.perform = function () { + var domainObject = this.domainObject, + $location = this.$location, + urlService = this.urlService; // Look up the object's "editor.completion" capability; // this is introduced by EditableDomainObject which is @@ -56,25 +73,15 @@ define( ))); } - return { - /** - * Cancel editing. - * - * @returns {Promise} a promise that will be fulfilled when - * cancellation has completed - */ - perform: function () { - return doCancel(getEditorCapability()) - .then(returnToBrowse); - } - }; - } + return doCancel(getEditorCapability()) + .then(returnToBrowse); + }; /** * Check if this action is applicable in a given context. * This will ensure that a domain object is present in the context, * and that this domain object is in Edit mode. - * @returns true if applicable + * @returns {boolean} true if applicable */ CancelAction.appliesTo = function (context) { var domainObject = (context || {}).domainObject; @@ -84,4 +91,4 @@ define( return CancelAction; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/actions/EditAction.js b/platform/commonUI/edit/src/actions/EditAction.js index 38260469ae..86a8a75540 100644 --- a/platform/commonUI/edit/src/actions/EditAction.js +++ b/platform/commonUI/edit/src/actions/EditAction.js @@ -42,7 +42,9 @@ define( * mode (typically triggered by the Edit button.) This will * show the user interface for editing (by way of a change in * route) + * @memberof platform/commonUI/edit * @constructor + * @implements {Action} */ function EditAction($location, navigationService, $log, context) { var domainObject = (context || {}).domainObject; @@ -60,17 +62,19 @@ define( return NULL_ACTION; } - return { - /** - * Enter edit mode. - */ - perform: function () { - navigationService.setNavigation(domainObject); - $location.path("/edit"); - } - }; + this.domainObject = domainObject; + this.$location = $location; + this.navigationService = navigationService; } + /** + * Enter edit mode. + */ + EditAction.prototype.perform = function () { + this.navigationService.setNavigation(this.domainObject); + this.$location.path("/edit"); + }; + /** * Check for applicability; verify that a domain object is present * for this action to be performed upon. @@ -87,4 +91,4 @@ define( return EditAction; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/actions/LinkAction.js b/platform/commonUI/edit/src/actions/LinkAction.js index f7d0087646..74abd2a93c 100644 --- a/platform/commonUI/edit/src/actions/LinkAction.js +++ b/platform/commonUI/edit/src/actions/LinkAction.js @@ -29,42 +29,43 @@ define( /** * Add one domain object to another's composition. + * @constructor + * @memberof platform/commonUI/edit + * @implements {Action} */ function LinkAction(context) { - var domainObject = (context || {}).domainObject, - selectedObject = (context || {}).selectedObject, - selectedId = selectedObject && selectedObject.getId(); + this.domainObject = (context || {}).domainObject; + this.selectedObject = (context || {}).selectedObject; + this.selectedId = this.selectedObject && this.selectedObject.getId(); + } + + LinkAction.prototype.perform = function () { + var self = this; // Add this domain object's identifier function addId(model) { if (Array.isArray(model.composition) && - model.composition.indexOf(selectedId) < 0) { - model.composition.push(selectedId); + model.composition.indexOf(self.selectedId) < 0) { + model.composition.push(self.selectedId); } } // Persist changes to the domain object function doPersist() { - var persistence = domainObject.getCapability('persistence'); + var persistence = + self.domainObject.getCapability('persistence'); return persistence.persist(); } // Link these objects function doLink() { - return domainObject.useCapability("mutation", addId) + return self.domainObject.useCapability("mutation", addId) .then(doPersist); } - return { - /** - * Perform this action. - */ - perform: function () { - return selectedId && doLink(); - } - }; - } + return this.selectedId && doLink(); + }; return LinkAction; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/actions/PropertiesAction.js b/platform/commonUI/edit/src/actions/PropertiesAction.js index bb37727c0e..1134c23190 100644 --- a/platform/commonUI/edit/src/actions/PropertiesAction.js +++ b/platform/commonUI/edit/src/actions/PropertiesAction.js @@ -32,58 +32,58 @@ define( 'use strict'; /** - * Construct an action which will allow an object's metadata to be - * edited. + * Implements the "Edit Properties" action, which prompts the user + * to modify a domain object's properties. * * @param {DialogService} dialogService a service which will show the dialog * @param {DomainObject} object the object to be edited * @param {ActionContext} context the context in which this action is performed + * @memberof platform/commonUI/edit + * @implements {Action} * @constructor */ function PropertiesAction(dialogService, context) { - var object = context.domainObject; + this.domainObject = (context || {}).domainObject; + this.dialogService = dialogService; + } + + PropertiesAction.prototype.perform = function () { + var type = this.domainObject.getCapability('type'), + domainObject = this.domainObject, + dialogService = this.dialogService; // Persist modifications to this domain object function doPersist() { - var persistence = object.getCapability('persistence'); + var persistence = domainObject.getCapability('persistence'); return persistence && persistence.persist(); } // Update the domain object model based on user input function updateModel(userInput, dialog) { - return object.useCapability('mutation', function (model) { + return domainObject.useCapability('mutation', function (model) { dialog.updateModel(model, userInput); }); } function showDialog(type) { // Create a dialog object to generate the form structure, etc. - var dialog = new PropertiesDialog(type, object.getModel()); + var dialog = + new PropertiesDialog(type, domainObject.getModel()); // Show the dialog return dialogService.getUserInput( dialog.getFormStructure(), dialog.getInitialFormValue() ).then(function (userInput) { - // Update the model, if user input was provided - return userInput && updateModel(userInput, dialog); - }).then(function (result) { - return result && doPersist(); - }); + // Update the model, if user input was provided + return userInput && updateModel(userInput, dialog); + }).then(function (result) { + return result && doPersist(); + }); } - return { - /** - * Perform this action. - * @return {Promise} a promise which will be - * fulfilled when the action has completed. - */ - perform: function () { - var type = object.getCapability('type'); - return type && showDialog(type); - } - }; - } + return type && showDialog(type); + }; /** * Filter this action for applicability against a given context. @@ -106,3 +106,4 @@ define( ); + diff --git a/platform/commonUI/edit/src/actions/PropertiesDialog.js b/platform/commonUI/edit/src/actions/PropertiesDialog.js index 8319efc315..97ee1f5c0a 100644 --- a/platform/commonUI/edit/src/actions/PropertiesDialog.js +++ b/platform/commonUI/edit/src/actions/PropertiesDialog.js @@ -21,12 +21,6 @@ *****************************************************************************/ /*global define*/ -/** - * Defines the PropertiesDialog, used by the PropertiesAction to - * populate the form shown in dialog based on the created type. - * - * @module common/actions/properties-dialog - */ define( function () { 'use strict'; @@ -37,58 +31,60 @@ define( * @param {TypeImpl} type the type of domain object for which properties * will be specified * @param {DomainObject} the object for which properties will be set + * @memberof platform/commonUI/edit * @constructor - * @memberof module:common/actions/properties-dialog */ function PropertiesDialog(type, model) { - var properties = type.getProperties(); - - return { - /** - * Get sections provided by this dialog. - * @return {FormStructure} the structure of this form - */ - getFormStructure: function () { - return { - name: "Edit " + model.name, - sections: [{ - name: "Properties", - rows: properties.map(function (property, index) { - // Property definition is same as form row definition - var row = Object.create(property.getDefinition()); - row.key = index; - return row; - }) - }] - }; - }, - /** - * Get the initial state of the form shown by this dialog - * (based on the object model) - * @returns {object} initial state of the form - */ - getInitialFormValue: function () { - // Start with initial values for properties - // Note that index needs to correlate to row.key - // from getFormStructure - return properties.map(function (property) { - return property.getValue(model); - }); - }, - /** - * Update a domain object model based on the value of a form. - */ - updateModel: function (model, formValue) { - // Update all properties - properties.forEach(function (property, index) { - property.setValue(model, formValue[index]); - }); - } - }; - - + this.type = type; + this.model = model; + this.properties = type.getProperties(); } + /** + * Get sections provided by this dialog. + * @return {FormStructure} the structure of this form + */ + PropertiesDialog.prototype.getFormStructure = function () { + return { + name: "Edit " + this.model.name, + sections: [{ + name: "Properties", + rows: this.properties.map(function (property, index) { + // Property definition is same as form row definition + var row = Object.create(property.getDefinition()); + row.key = index; + return row; + }) + }] + }; + }; + + /** + * Get the initial state of the form shown by this dialog + * (based on the object model) + * @returns {object} initial state of the form + */ + PropertiesDialog.prototype.getInitialFormValue = function () { + var model = this.model; + + // Start with initial values for properties + // Note that index needs to correlate to row.key + // from getFormStructure + return this.properties.map(function (property) { + return property.getValue(model); + }); + }; + + /** + * Update a domain object model based on the value of a form. + */ + PropertiesDialog.prototype.updateModel = function (model, formValue) { + // Update all properties + this.properties.forEach(function (property, index) { + property.setValue(model, formValue[index]); + }); + }; + return PropertiesDialog; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/actions/RemoveAction.js b/platform/commonUI/edit/src/actions/RemoveAction.js index fbf47d22d9..da1c81b486 100644 --- a/platform/commonUI/edit/src/actions/RemoveAction.js +++ b/platform/commonUI/edit/src/actions/RemoveAction.js @@ -37,22 +37,34 @@ define( * * @param {DomainObject} object the object to be removed * @param {ActionContext} context the context in which this action is performed + * @memberof platform/commonUI/edit * @constructor - * @memberof module:editor/actions/remove-action + * @implements {Action} */ function RemoveAction($q, context) { - var object = (context || {}).domainObject; + this.domainObject = (context || {}).domainObject; + this.$q = $q; + } - /** + /** + * Perform this action. + * @return {Promise} a promise which will be + * fulfilled when the action has completed. + */ + RemoveAction.prototype.perform = function () { + var $q = this.$q, + domainObject = this.domainObject; + + /* * Check whether an object ID matches the ID of the object being * removed (used to filter a parent's composition to handle the * removal.) */ function isNotObject(otherObjectId) { - return otherObjectId !== object.getId(); + return otherObjectId !== domainObject.getId(); } - /** + /* * Mutate a parent object such that it no longer contains the object * which is being removed. */ @@ -60,7 +72,7 @@ define( model.composition = model.composition.filter(isNotObject); } - /** + /* * Invoke persistence on a domain object. This will be called upon * the removed object's parent (as its composition will have changed.) */ @@ -69,33 +81,22 @@ define( return persistence && persistence.persist(); } - /** + /* * Remove the object from its parent, as identified by its context * capability. - * @param {ContextCapability} contextCapability the "context" capability - * of the domain object being removed. */ function removeFromContext(contextCapability) { var parent = contextCapability.getParent(); - $q.when( - parent.useCapability('mutation', doMutate) - ).then(function () { - return doPersist(parent); - }); + return $q.when( + parent.useCapability('mutation', doMutate) + ).then(function () { + return doPersist(parent); + }); } - return { - /** - * Perform this action. - * @return {module:core/promises.Promise} a promise which will be - * fulfilled when the action has completed. - */ - perform: function () { - return $q.when(object.getCapability('context')) - .then(removeFromContext); - } - }; - } + return $q.when(this.domainObject.getCapability('context')) + .then(removeFromContext); + }; // Object needs to have a parent for Remove to be applicable RemoveAction.appliesTo = function (context) { @@ -113,4 +114,4 @@ define( return RemoveAction; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/actions/SaveAction.js b/platform/commonUI/edit/src/actions/SaveAction.js index d5db593742..fa276bba4b 100644 --- a/platform/commonUI/edit/src/actions/SaveAction.js +++ b/platform/commonUI/edit/src/actions/SaveAction.js @@ -30,9 +30,27 @@ define( * The "Save" action; the action triggered by clicking Save from * Edit Mode. Exits the editing user interface and invokes object * capabilities to persist the changes that have been made. + * @constructor + * @implements {Action} + * @memberof platform/commonUI/edit */ function SaveAction($location, urlService, context) { - var domainObject = context.domainObject; + this.domainObject = (context || {}).domainObject; + this.$location = $location; + this.urlService = urlService; + } + + /** + * Save changes and conclude editing. + * + * @returns {Promise} a promise that will be fulfilled when + * cancellation has completed + * @memberof platform/commonUI/edit.SaveAction# + */ + SaveAction.prototype.perform = function () { + var domainObject = this.domainObject, + $location = this.$location, + urlService = this.urlService; // Invoke any save behavior introduced by the editor capability; // this is introduced by EditableDomainObject which is @@ -51,18 +69,8 @@ define( )); } - return { - /** - * Save changes and conclude editing. - * - * @returns {Promise} a promise that will be fulfilled when - * cancellation has completed - */ - perform: function () { - return doSave().then(returnToBrowse); - } - }; - } + return doSave().then(returnToBrowse); + }; /** * Check if this action is applicable in a given context. @@ -78,4 +86,4 @@ define( return SaveAction; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/capabilities/EditableCompositionCapability.js b/platform/commonUI/edit/src/capabilities/EditableCompositionCapability.js index 02276639d2..17dff58c0d 100644 --- a/platform/commonUI/edit/src/capabilities/EditableCompositionCapability.js +++ b/platform/commonUI/edit/src/capabilities/EditableCompositionCapability.js @@ -35,6 +35,9 @@ define( * Meant specifically for use by EditableDomainObject and the * associated cache; the constructor signature is particular * to a pattern used there and may contain unused arguments. + * @constructor + * @memberof platform/commonUI/edit + * @implements {CompositionCapability} */ return function EditableCompositionCapability( contextCapability, @@ -54,4 +57,4 @@ define( ); }; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/capabilities/EditableContextCapability.js b/platform/commonUI/edit/src/capabilities/EditableContextCapability.js index 34ea3ee465..d0df90afc4 100644 --- a/platform/commonUI/edit/src/capabilities/EditableContextCapability.js +++ b/platform/commonUI/edit/src/capabilities/EditableContextCapability.js @@ -35,6 +35,9 @@ define( * Meant specifically for use by EditableDomainObject and the * associated cache; the constructor signature is particular * to a pattern used there and may contain unused arguments. + * @constructor + * @memberof platform/commonUI/edit + * @implements {ContextCapability} */ return function EditableContextCapability( contextCapability, @@ -72,4 +75,4 @@ define( return capability; }; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/capabilities/EditableLookupCapability.js b/platform/commonUI/edit/src/capabilities/EditableLookupCapability.js index 5571072b25..c92495dc3f 100644 --- a/platform/commonUI/edit/src/capabilities/EditableLookupCapability.js +++ b/platform/commonUI/edit/src/capabilities/EditableLookupCapability.js @@ -35,6 +35,8 @@ define( * Meant specifically for use by EditableDomainObject and the * associated cache; the constructor signature is particular * to a pattern used there and may contain unused arguments. + * @constructor + * @memberof platform/commonUI/edit */ return function EditableLookupCapability( contextCapability, @@ -76,7 +78,7 @@ define( // Wrap a returned value (see above); if it's a promise, wrap // the resolved value. function wrapResult(result) { - return result.then ? // promise-like + return (result && result.then) ? // promise-like result.then(makeEditable) : makeEditable(result); } @@ -105,8 +107,10 @@ define( // Wrap a method of this capability function wrapMethod(fn) { - capability[fn] = - (idempotent ? oneTimeFunction : wrapFunction)(fn); + if (typeof capability[fn] === 'function') { + capability[fn] = + (idempotent ? oneTimeFunction : wrapFunction)(fn); + } } // Wrap all methods; return only editable domain objects. @@ -115,4 +119,4 @@ define( return capability; }; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/capabilities/EditablePersistenceCapability.js b/platform/commonUI/edit/src/capabilities/EditablePersistenceCapability.js index 5252ef1d44..42b08c72b1 100644 --- a/platform/commonUI/edit/src/capabilities/EditablePersistenceCapability.js +++ b/platform/commonUI/edit/src/capabilities/EditablePersistenceCapability.js @@ -35,6 +35,9 @@ define( * Meant specifically for use by EditableDomainObject and the * associated cache; the constructor signature is particular * to a pattern used there and may contain unused arguments. + * @constructor + * @memberof platform/commonUI/edit + * @implements {PersistenceCapability} */ function EditablePersistenceCapability( persistenceCapability, @@ -62,4 +65,4 @@ define( return EditablePersistenceCapability; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/capabilities/EditableRelationshipCapability.js b/platform/commonUI/edit/src/capabilities/EditableRelationshipCapability.js index f61a54176c..3034301502 100644 --- a/platform/commonUI/edit/src/capabilities/EditableRelationshipCapability.js +++ b/platform/commonUI/edit/src/capabilities/EditableRelationshipCapability.js @@ -35,6 +35,9 @@ define( * Meant specifically for use by EditableDomainObject and the * associated cache; the constructor signature is particular * to a pattern used there and may contain unused arguments. + * @constructor + * @memberof platform/commonUI/edit + * @implements {RelationshipCapability} */ return function EditableRelationshipCapability( relationshipCapability, @@ -54,4 +57,4 @@ define( ); }; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/capabilities/EditorCapability.js b/platform/commonUI/edit/src/capabilities/EditorCapability.js index e59fbcae8c..7094d1142c 100644 --- a/platform/commonUI/edit/src/capabilities/EditorCapability.js +++ b/platform/commonUI/edit/src/capabilities/EditorCapability.js @@ -39,27 +39,48 @@ define( * Meant specifically for use by EditableDomainObject and the * associated cache; the constructor signature is particular * to a pattern used there and may contain unused arguments. + * @constructor + * @memberof platform/commonUI/edit */ - return function EditorCapability( + function EditorCapability( persistenceCapability, editableObject, domainObject, cache ) { + this.editableObject = editableObject; + this.domainObject = domainObject; + this.cache = cache; + } - // Simulate Promise.resolve (or $q.when); the former - // causes a delayed reaction from Angular (since it - // does not trigger a digest) and the latter is not - // readily accessible, since we're a few classes - // removed from the layer which gets dependency - // injection. - function resolvePromise(value) { - return (value && value.then) ? value : { - then: function (callback) { - return resolvePromise(callback(value)); - } - }; - } + // Simulate Promise.resolve (or $q.when); the former + // causes a delayed reaction from Angular (since it + // does not trigger a digest) and the latter is not + // readily accessible, since we're a few classes + // removed from the layer which gets dependency + // injection. + function resolvePromise(value) { + return (value && value.then) ? value : { + then: function (callback) { + return resolvePromise(callback(value)); + } + }; + } + + /** + * Save any changes that have been made to this domain object + * (as well as to others that might have been retrieved and + * modified during the editing session) + * @param {boolean} nonrecursive if true, save only this + * object (and not other objects with associated changes) + * @returns {Promise} a promise that will be fulfilled after + * persistence has completed. + * @memberof platform/commonUI/edit.EditorCapability# + */ + EditorCapability.prototype.save = function (nonrecursive) { + var domainObject = this.domainObject, + editableObject = this.editableObject, + cache = this.cache; // Update the underlying, "real" domain object's model // with changes made to the copy used for editing. @@ -74,39 +95,32 @@ define( return domainObject.getCapability('persistence').persist(); } - return { - /** - * Save any changes that have been made to this domain object - * (as well as to others that might have been retrieved and - * modified during the editing session) - * @param {boolean} nonrecursive if true, save only this - * object (and not other objects with associated changes) - * @returns {Promise} a promise that will be fulfilled after - * persistence has completed. - */ - save: function (nonrecursive) { - return nonrecursive ? - resolvePromise(doMutate()).then(doPersist) : - resolvePromise(cache.saveAll()); - }, - /** - * Cancel editing; Discard any changes that have been made to - * this domain object (as well as to others that might have - * been retrieved and modified during the editing session) - * @returns {Promise} a promise that will be fulfilled after - * cancellation has completed. - */ - cancel: function () { - return resolvePromise(undefined); - }, - /** - * Check if there are any unsaved changes. - * @returns {boolean} true if there are unsaved changes - */ - dirty: function () { - return cache.dirty(); - } - }; + return nonrecursive ? + resolvePromise(doMutate()).then(doPersist) : + resolvePromise(cache.saveAll()); }; + + /** + * Cancel editing; Discard any changes that have been made to + * this domain object (as well as to others that might have + * been retrieved and modified during the editing session) + * @returns {Promise} a promise that will be fulfilled after + * cancellation has completed. + * @memberof platform/commonUI/edit.EditorCapability# + */ + EditorCapability.prototype.cancel = function () { + return resolvePromise(undefined); + }; + + /** + * Check if there are any unsaved changes. + * @returns {boolean} true if there are unsaved changes + * @memberof platform/commonUI/edit.EditorCapability# + */ + EditorCapability.prototype.dirty = function () { + return this.cache.dirty(); + }; + + return EditorCapability; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/controllers/EditActionController.js b/platform/commonUI/edit/src/controllers/EditActionController.js index 43c173b098..4ea38f9bb5 100644 --- a/platform/commonUI/edit/src/controllers/EditActionController.js +++ b/platform/commonUI/edit/src/controllers/EditActionController.js @@ -33,6 +33,7 @@ define( /** * Controller which supplies action instances for Save/Cancel. + * @memberof platform/commonUI/edit * @constructor */ function EditActionController($scope) { @@ -51,4 +52,4 @@ define( return EditActionController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/controllers/EditController.js b/platform/commonUI/edit/src/controllers/EditController.js index 34cabd0b0b..eaffe02186 100644 --- a/platform/commonUI/edit/src/controllers/EditController.js +++ b/platform/commonUI/edit/src/controllers/EditController.js @@ -22,7 +22,8 @@ /*global define,Promise*/ /** - * Module defining EditController. Created by vwoeltje on 11/14/14. + * This bundle implements Edit mode. + * @namespace platform/commonUI/edit */ define( ["../objects/EditableDomainObject"], @@ -33,15 +34,16 @@ define( * Controller which is responsible for populating the scope for * Edit mode; introduces an editable version of the currently * navigated domain object into the scope. + * @memberof platform/commonUI/edit * @constructor */ function EditController($scope, $q, navigationService) { - var navigatedObject; + var self = this; function setNavigation(domainObject) { // Wrap the domain object such that all mutation is // confined to edit mode (until Save) - navigatedObject = + self.navigatedDomainObject = domainObject && new EditableDomainObject(domainObject, $q); } @@ -50,33 +52,33 @@ define( $scope.$on("$destroy", function () { navigationService.removeListener(setNavigation); }); - - return { - /** - * Get the domain object which is navigated-to. - * @returns {DomainObject} the domain object that is navigated-to - */ - navigatedObject: function () { - return navigatedObject; - }, - /** - * Get the warning to show if the user attempts to navigate - * away from Edit mode while unsaved changes are present. - * @returns {string} the warning to show, or undefined if - * there are no unsaved changes - */ - getUnloadWarning: function () { - var editorCapability = navigatedObject && - navigatedObject.getCapability("editor"), - hasChanges = editorCapability && editorCapability.dirty(); - - return hasChanges ? - "Unsaved changes will be lost if you leave this page." : - undefined; - } - }; } + /** + * Get the domain object which is navigated-to. + * @returns {DomainObject} the domain object that is navigated-to + */ + EditController.prototype.navigatedObject = function () { + return this.navigatedDomainObject; + }; + + /** + * Get the warning to show if the user attempts to navigate + * away from Edit mode while unsaved changes are present. + * @returns {string} the warning to show, or undefined if + * there are no unsaved changes + */ + EditController.prototype.getUnloadWarning = function () { + var navigatedObject = this.navigatedDomainObject, + editorCapability = navigatedObject && + navigatedObject.getCapability("editor"), + hasChanges = editorCapability && editorCapability.dirty(); + + return hasChanges ? + "Unsaved changes will be lost if you leave this page." : + undefined; + }; + return EditController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/controllers/EditPanesController.js b/platform/commonUI/edit/src/controllers/EditPanesController.js index 4bfc91002c..7dedc251ec 100644 --- a/platform/commonUI/edit/src/controllers/EditPanesController.js +++ b/platform/commonUI/edit/src/controllers/EditPanesController.js @@ -28,15 +28,17 @@ define( /** * Supports the Library and Elements panes in Edit mode. + * @memberof platform/commonUI/edit * @constructor */ function EditPanesController($scope) { - var root; + var self = this; // Update root object based on represented object function updateRoot(domainObject) { - var context = domainObject && - domainObject.getCapability('context'), + var root = self.rootDomainObject, + context = domainObject && + domainObject.getCapability('context'), newRoot = context && context.getTrueRoot(), oldId = root && root.getId(), newId = newRoot && newRoot.getId(); @@ -44,25 +46,22 @@ define( // Only update if this has actually changed, // to avoid excessive refreshing. if (oldId !== newId) { - root = newRoot; + self.rootDomainObject = newRoot; } } // Update root when represented object changes $scope.$watch('domainObject', updateRoot); - - return { - /** - * Get the root-level domain object, as reported by the - * represented domain object. - * @returns {DomainObject} the root object - */ - getRoot: function () { - return root; - } - }; } + /** + * Get the root-level domain object, as reported by the + * represented domain object. + * @returns {DomainObject} the root object + */ + EditPanesController.prototype.getRoot = function () { + return this.rootDomainObject; + }; return EditPanesController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/directives/MCTBeforeUnload.js b/platform/commonUI/edit/src/directives/MCTBeforeUnload.js index 60e825d1b0..3e7501c788 100644 --- a/platform/commonUI/edit/src/directives/MCTBeforeUnload.js +++ b/platform/commonUI/edit/src/directives/MCTBeforeUnload.js @@ -31,6 +31,7 @@ define( * to this attribute will be evaluated during page navigation events * and, if it returns a truthy value, will be used to populate a * prompt to the user to confirm this navigation. + * @memberof platform/commonUI/edit * @constructor * @param $window the window */ @@ -102,4 +103,4 @@ define( return MCTBeforeUnload; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/objects/EditableDomainObject.js b/platform/commonUI/edit/src/objects/EditableDomainObject.js index dce39bec87..bbbc0ae512 100644 --- a/platform/commonUI/edit/src/objects/EditableDomainObject.js +++ b/platform/commonUI/edit/src/objects/EditableDomainObject.js @@ -68,6 +68,9 @@ define( * which need to behave differently in edit mode, * and provides a "working copy" of the object's * model to allow changes to be easily cancelled. + * @constructor + * @memberof platform/commonUI/edit + * @implements {DomainObject} */ function EditableDomainObject(domainObject, $q) { // The cache will hold all domain objects reached from @@ -92,10 +95,10 @@ define( this, delegateArguments ), - factory = capabilityFactories[name]; + Factory = capabilityFactories[name]; - return (factory && capability) ? - factory(capability, editableObject, domainObject, cache) : + return (Factory && capability) ? + new Factory(capability, editableObject, domainObject, cache) : capability; }; @@ -109,4 +112,4 @@ define( return EditableDomainObject; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/objects/EditableDomainObjectCache.js b/platform/commonUI/edit/src/objects/EditableDomainObjectCache.js index a13a3e2360..88a154d79b 100644 --- a/platform/commonUI/edit/src/objects/EditableDomainObjectCache.js +++ b/platform/commonUI/edit/src/objects/EditableDomainObjectCache.js @@ -22,7 +22,7 @@ /*global define*/ -/** +/* * An editable domain object cache stores domain objects that have been * made editable, in a group that can be saved all-at-once. This supports * Edit mode, which is launched for a specific object but may contain @@ -32,8 +32,6 @@ * to ensure that changes made while in edit mode do not propagate up * to the objects used in browse mode (or to persistence) until the user * initiates a Save. - * - * @module editor/object/editable-domain-object-cache */ define( ["./EditableModelCache"], @@ -46,107 +44,118 @@ define( * of objects retrieved via composition or context capabilities as * editable domain objects. * - * @param {Constructor} EditableDomainObject a + * @param {Constructor} EditableDomainObject a * constructor function which takes a regular domain object as * an argument, and returns an editable domain object as its * result. * @param $q Angular's $q, for promise handling + * @memberof platform/commonUI/edit * @constructor - * @memberof module:editor/object/editable-domain-object-cache */ function EditableDomainObjectCache(EditableDomainObject, $q) { - var cache = new EditableModelCache(), - dirty = {}, - root; - - return { - /** - * Wrap this domain object in an editable form, or pull such - * an object from the cache if one already exists. - * - * @param {DomainObject} domainObject the regular domain object - * @returns {DomainObject} the domain object in an editable form - */ - getEditableObject: function (domainObject) { - var type = domainObject.getCapability('type'); - - // Track the top-level domain object; this will have - // some special behavior for its context capability. - root = root || domainObject; - - // Avoid double-wrapping (WTD-1017) - if (domainObject.hasCapability('editor')) { - return domainObject; - } - - // Don't bother wrapping non-editable objects - if (!type || !type.hasFeature('creation')) { - return domainObject; - } - - // Provide an editable form of the object - return new EditableDomainObject( - domainObject, - cache.getCachedModel(domainObject) - ); - }, - /** - * Check if a domain object is (effectively) the top-level - * object in this editable subgraph. - * @returns {boolean} true if it is the root - */ - isRoot: function (domainObject) { - return domainObject === root; - }, - /** - * Mark an editable domain object (presumably already cached) - * as having received modifications during editing; it should be - * included in the bulk save invoked when editing completes. - * - * @param {DomainObject} domainObject the domain object - */ - markDirty: function (domainObject) { - dirty[domainObject.getId()] = domainObject; - }, - /** - * Mark an object (presumably already cached) as having had its - * changes saved (and thus no longer needing to be subject to a - * save operation.) - * - * @param {DomainObject} domainObject the domain object - */ - markClean: function (domainObject) { - delete dirty[domainObject.getId()]; - }, - /** - * Initiate a save on all objects that have been cached. - */ - saveAll: function () { - // Get a list of all dirty objects - var objects = Object.keys(dirty).map(function (k) { - return dirty[k]; - }); - - // Clear dirty set, since we're about to save. - dirty = {}; - - // Most save logic is handled by the "editor.completion" - // capability, so that is delegated here. - return $q.all(objects.map(function (object) { - // Save; pass a nonrecursive flag to avoid looping - return object.getCapability('editor').save(true); - })); - }, - /** - * Check if any objects have been marked dirty in this cache. - * @returns {boolean} true if objects are dirty - */ - dirty: function () { - return Object.keys(dirty).length > 0; - } - }; + this.cache = new EditableModelCache(); + this.dirtyObjects = {}; + this.root = undefined; + this.$q = $q; + this.EditableDomainObject = EditableDomainObject; } + /** + * Wrap this domain object in an editable form, or pull such + * an object from the cache if one already exists. + * + * @param {DomainObject} domainObject the regular domain object + * @returns {DomainObject} the domain object in an editable form + */ + EditableDomainObjectCache.prototype.getEditableObject = function (domainObject) { + var type = domainObject.getCapability('type'), + EditableDomainObject = this.EditableDomainObject; + + // Track the top-level domain object; this will have + // some special behavior for its context capability. + this.root = this.root || domainObject; + + // Avoid double-wrapping (WTD-1017) + if (domainObject.hasCapability('editor')) { + return domainObject; + } + + // Don't bother wrapping non-editable objects + if (!type || !type.hasFeature('creation')) { + return domainObject; + } + + // Provide an editable form of the object + return new EditableDomainObject( + domainObject, + this.cache.getCachedModel(domainObject) + ); + }; + + /** + * Check if a domain object is (effectively) the top-level + * object in this editable subgraph. + * @returns {boolean} true if it is the root + */ + EditableDomainObjectCache.prototype.isRoot = function (domainObject) { + return domainObject === this.root; + }; + + /** + * Mark an editable domain object (presumably already cached) + * as having received modifications during editing; it should be + * included in the bulk save invoked when editing completes. + * + * @param {DomainObject} domainObject the domain object + * @memberof platform/commonUI/edit.EditableDomainObjectCache# + */ + EditableDomainObjectCache.prototype.markDirty = function (domainObject) { + this.dirtyObjects[domainObject.getId()] = domainObject; + }; + + /** + * Mark an object (presumably already cached) as having had its + * changes saved (and thus no longer needing to be subject to a + * save operation.) + * + * @param {DomainObject} domainObject the domain object + */ + EditableDomainObjectCache.prototype.markClean = function (domainObject) { + delete this.dirtyObjects[domainObject.getId()]; + }; + + /** + * Initiate a save on all objects that have been cached. + * @return {Promise} A promise which will resolve when all objects are + * persisted. + */ + EditableDomainObjectCache.prototype.saveAll = function () { + // Get a list of all dirty objects + var dirty = this.dirtyObjects, + objects = Object.keys(dirty).map(function (k) { + return dirty[k]; + }); + + // Clear dirty set, since we're about to save. + this.dirtyObjects = {}; + + // Most save logic is handled by the "editor.completion" + // capability, so that is delegated here. + return this.$q.all(objects.map(function (object) { + // Save; pass a nonrecursive flag to avoid looping + return object.getCapability('editor').save(true); + })); + }; + + /** + * Check if any objects have been marked dirty in this cache. + * @returns {boolean} true if objects are dirty + */ + EditableDomainObjectCache.prototype.dirty = function () { + return Object.keys(this.dirtyObjects).length > 0; + }; + return EditableDomainObjectCache; } ); + diff --git a/platform/commonUI/edit/src/objects/EditableModelCache.js b/platform/commonUI/edit/src/objects/EditableModelCache.js index 3652f679f7..30ca3d774a 100644 --- a/platform/commonUI/edit/src/objects/EditableModelCache.js +++ b/platform/commonUI/edit/src/objects/EditableModelCache.js @@ -31,33 +31,32 @@ define( * made editable, to support a group that can be saved all-at-once. * This is useful in Edit mode, which is launched for a specific * object but may contain changes across many objects. + * @memberof platform/commonUI/edit * @constructor */ function EditableModelCache() { - var cache = {}; - - // Deep-copy a model. Models are JSONifiable, so this can be - // done by stringification then destringification - function clone(model) { - return JSON.parse(JSON.stringify(model)); - } - - return { - /** - * Get this domain object's model from the cache (or - * place it in the cache if it isn't in the cache yet) - * @returns a clone of the domain object's model - */ - getCachedModel: function (domainObject) { - var id = domainObject.getId(); - - return (cache[id] = - cache[id] || clone(domainObject.getModel())); - } - }; - + this.cache = {}; } + // Deep-copy a model. Models are JSONifiable, so this can be + // done by stringification then destringification + function clone(model) { + return JSON.parse(JSON.stringify(model)); + } + + /** + * Get this domain object's model from the cache (or + * place it in the cache if it isn't in the cache yet) + * @returns a clone of the domain object's model + */ + EditableModelCache.prototype.getCachedModel = function (domainObject) { + var id = domainObject.getId(), + cache = this.cache; + + return (cache[id] = + cache[id] || clone(domainObject.getModel())); + }; + return EditableModelCache; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/policies/EditActionPolicy.js b/platform/commonUI/edit/src/policies/EditActionPolicy.js index 3c47af43b8..bec2fc423d 100644 --- a/platform/commonUI/edit/src/policies/EditActionPolicy.js +++ b/platform/commonUI/edit/src/policies/EditActionPolicy.js @@ -30,53 +30,47 @@ define( * Policy controlling when the `edit` and/or `properties` actions * can appear as applicable actions of the `view-control` category * (shown as buttons in the top-right of browse mode.) + * @memberof platform/commonUI/edit * @constructor + * @implements {Policy.} */ function EditActionPolicy() { - // Get a count of views which are not flagged as non-editable. - function countEditableViews(context) { - var domainObject = (context || {}).domainObject, - views = domainObject && domainObject.useCapability('view'), - count = 0; + } - // A view is editable unless explicitly flagged as not - (views || []).forEach(function (view) { - count += (view.editable !== false) ? 1 : 0; - }); + // Get a count of views which are not flagged as non-editable. + function countEditableViews(context) { + var domainObject = (context || {}).domainObject, + views = domainObject && domainObject.useCapability('view'), + count = 0; - return count; + // A view is editable unless explicitly flagged as not + (views || []).forEach(function (view) { + count += (view.editable !== false) ? 1 : 0; + }); + + return count; + } + + EditActionPolicy.prototype.allow = function (action, context) { + var key = action.getMetadata().key, + category = (context || {}).category; + + // Only worry about actions in the view-control category + if (category === 'view-control') { + // Restrict 'edit' to cases where there are editable + // views (similarly, restrict 'properties' to when + // the converse is true) + if (key === 'edit') { + return countEditableViews(context) > 0; + } else if (key === 'properties') { + return countEditableViews(context) < 1; + } } - return { - /** - * Check whether or not a given action is allowed by this - * policy. - * @param {Action} action the action - * @param context the context - * @returns {boolean} true if not disallowed - */ - allow: function (action, context) { - var key = action.getMetadata().key, - category = (context || {}).category; - - // Only worry about actions in the view-control category - if (category === 'view-control') { - // Restrict 'edit' to cases where there are editable - // views (similarly, restrict 'properties' to when - // the converse is true) - if (key === 'edit') { - return countEditableViews(context) > 0; - } else if (key === 'properties') { - return countEditableViews(context) < 1; - } - } - - // Like all policies, allow by default. - return true; - } - }; - } + // Like all policies, allow by default. + return true; + }; return EditActionPolicy; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/policies/EditableViewPolicy.js b/platform/commonUI/edit/src/policies/EditableViewPolicy.js index c93072e861..17194064b0 100644 --- a/platform/commonUI/edit/src/policies/EditableViewPolicy.js +++ b/platform/commonUI/edit/src/policies/EditableViewPolicy.js @@ -28,30 +28,24 @@ define( /** * Policy controlling which views should be visible in Edit mode. + * @memberof platform/commonUI/edit * @constructor + * @implements {Policy.} */ function EditableViewPolicy() { - return { - /** - * Check whether or not a given action is allowed by this - * policy. - * @param {Action} action the action - * @param domainObject the domain object which will be viewed - * @returns {boolean} true if not disallowed - */ - allow: function (view, domainObject) { - // If a view is flagged as non-editable, only allow it - // while we're not in Edit mode. - if ((view || {}).editable === false) { - return !domainObject.hasCapability('editor'); - } - - // Like all policies, allow by default. - return true; - } - }; } + EditableViewPolicy.prototype.allow = function (view, domainObject) { + // If a view is flagged as non-editable, only allow it + // while we're not in Edit mode. + if ((view || {}).editable === false) { + return !domainObject.hasCapability('editor'); + } + + // Like all policies, allow by default. + return true; + }; + return EditableViewPolicy; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/representers/EditRepresenter.js b/platform/commonUI/edit/src/representers/EditRepresenter.js index 96f6da332b..17a0f634b2 100644 --- a/platform/commonUI/edit/src/representers/EditRepresenter.js +++ b/platform/commonUI/edit/src/representers/EditRepresenter.js @@ -41,14 +41,17 @@ define( * and may be reused for different domain objects and/or * representations resulting from changes there. * + * @memberof platform/commonUI/edit + * @implements {Representer} * @constructor */ function EditRepresenter($q, $log, scope) { - var domainObject, - key; + var self = this; // Mutate and persist a new version of a domain object's model. function doPersist(model) { + var domainObject = self.domainObject; + // First, mutate; then, persist. return $q.when(domainObject.useCapability("mutation", function () { return model; @@ -64,7 +67,8 @@ define( // Look up from scope; these will have been populated by // mct-representation. var model = scope.model, - configuration = scope.configuration; + configuration = scope.configuration, + domainObject = self.domainObject; // Log the commit message $log.debug([ @@ -78,50 +82,33 @@ define( if (domainObject && domainObject.hasCapability("persistence")) { // Configurations for specific views are stored by // key in the "configuration" field of the model. - if (key && configuration) { + if (self.key && configuration) { model.configuration = model.configuration || {}; - model.configuration[key] = configuration; + model.configuration[self.key] = configuration; } doPersist(model); } } - // Respond to the destruction of the current representation. - function destroy() { - // Nothing to clean up - } - - // Handle a specific representation of a specific domain object - function represent(representation, representedObject) { - // Track the key, to know which view configuration to save to. - key = (representation || {}).key; - // Track the represented object - domainObject = representedObject; - // Ensure existing watches are released - destroy(); - } - // Place the "commit" method in the scope scope.commit = commit; - - return { - /** - * Set the current representation in use, and the domain - * object being represented. - * - * @param {RepresentationDefinition} representation the - * definition of the representation in use - * @param {DomainObject} domainObject the domain object - * being represented - */ - represent: represent, - /** - * Release any resources associated with this representer. - */ - destroy: destroy - }; } + // Handle a specific representation of a specific domain object + EditRepresenter.prototype.represent = function represent(representation, representedObject) { + // Track the key, to know which view configuration to save to. + this.key = (representation || {}).key; + // Track the represented object + this.domainObject = representedObject; + // Ensure existing watches are released + this.destroy(); + }; + + // Respond to the destruction of the current representation. + EditRepresenter.prototype.destroy = function destroy() { + // Nothing to clean up + }; + return EditRepresenter; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/representers/EditToolbar.js b/platform/commonUI/edit/src/representers/EditToolbar.js index 58dab7497e..367eaf1705 100644 --- a/platform/commonUI/edit/src/representers/EditToolbar.js +++ b/platform/commonUI/edit/src/representers/EditToolbar.js @@ -38,125 +38,23 @@ define( * * @param structure toolbar structure, as provided by view definition * @param {Function} commit callback to invoke after changes + * @memberof platform/commonUI/edit * @constructor */ function EditToolbar(structure, commit) { - var toolbarStructure = Object.create(structure || {}), - toolbarState, - selection, - properties = []; + var self = this; // Generate a new key for an item's property function addKey(property) { - properties.push(property); - return properties.length - 1; // Return index of property - } - - // Update value for this property in all elements of the - // selection which have this property. - function updateProperties(property, value) { - var changed = false; - - // Update property in a selected element - function updateProperty(selected) { - // Ignore selected elements which don't have this property - if (selected[property] !== undefined) { - // Check if this is a setter, or just assignable - if (typeof selected[property] === 'function') { - changed = - changed || (selected[property]() !== value); - selected[property](value); - } else { - changed = - changed || (selected[property] !== value); - selected[property] = value; - } - } - } - - // Update property in all selected elements - selection.forEach(updateProperty); - - // Return whether or not anything changed - return changed; - } - - // Look up the current value associated with a property - // in selection i - function lookupState(property, selected) { - var value = selected[property]; - return (typeof value === 'function') ? value() : value; - } - - // Get initial value for a given property - function initializeState(property) { - var result; - // Look through all selections for this property; - // values should all match by the time we perform - // this lookup anyway. - selection.forEach(function (selected) { - result = (selected[property] !== undefined) ? - lookupState(property, selected) : - result; - }); - return result; - } - - // Check if all elements of the selection which have this - // property have the same value for this property. - function isConsistent(property) { - var consistent = true, - observed = false, - state; - - // Check if a given element of the selection is consistent - // with previously-observed elements for this property. - function checkConsistency(selected) { - var next; - // Ignore selections which don't have this property - if (selected[property] !== undefined) { - // Look up state of this element in the selection - next = lookupState(property, selected); - // Detect inconsistency - if (observed) { - consistent = consistent && (next === state); - } - // Track state for next iteration - state = next; - observed = true; - } - } - - // Iterate through selections - selection.forEach(checkConsistency); - - return consistent; - } - - // Used to filter out items which are applicable (or not) - // to the current selection. - function isApplicable(item) { - var property = (item || {}).property, - method = (item || {}).method, - exclusive = !!(item || {}).exclusive; - - // Check if a selected item defines this property - function hasProperty(selected) { - return (property && (selected[property] !== undefined)) || - (method && (typeof selected[method] === 'function')); - } - - return selection.map(hasProperty).reduce( - exclusive ? and : or, - exclusive - ) && isConsistent(property); + self.properties.push(property); + return self.properties.length - 1; // Return index of property } // Invoke all functions in selections with the given name function invoke(method, value) { if (method) { // Make the change in the selection - selection.forEach(function (selected) { + self.selection.forEach(function (selected) { if (typeof selected[method] === 'function') { selected[method](value); } @@ -189,73 +87,172 @@ define( return converted; } + this.toolbarState = []; + this.selection = undefined; + this.properties = []; + this.toolbarStructure = Object.create(structure || {}); + this.toolbarStructure.sections = + ((structure || {}).sections || []).map(convertSection); + } + + // Check if all elements of the selection which have this + // property have the same value for this property. + EditToolbar.prototype.isConsistent = function (property) { + var self = this, + consistent = true, + observed = false, + state; + + // Check if a given element of the selection is consistent + // with previously-observed elements for this property. + function checkConsistency(selected) { + var next; + // Ignore selections which don't have this property + if (selected[property] !== undefined) { + // Look up state of this element in the selection + next = self.lookupState(property, selected); + // Detect inconsistency + if (observed) { + consistent = consistent && (next === state); + } + // Track state for next iteration + state = next; + observed = true; + } + } + + // Iterate through selections + self.selection.forEach(checkConsistency); + + return consistent; + }; + + // Used to filter out items which are applicable (or not) + // to the current selection. + EditToolbar.prototype.isApplicable = function (item) { + var property = (item || {}).property, + method = (item || {}).method, + exclusive = !!(item || {}).exclusive; + + // Check if a selected item defines this property + function hasProperty(selected) { + return (property && (selected[property] !== undefined)) || + (method && (typeof selected[method] === 'function')); + } + + return this.selection.map(hasProperty).reduce( + exclusive ? and : or, + exclusive + ) && this.isConsistent(property); + }; + + + // Look up the current value associated with a property + EditToolbar.prototype.lookupState = function (property, selected) { + var value = selected[property]; + return (typeof value === 'function') ? value() : value; + }; + + /** + * Set the current selection. Visibility of sections + * and items in the toolbar will be updated to match this. + * @param {Array} s the new selection + */ + EditToolbar.prototype.setSelection = function (s) { + var self = this; + // Show/hide controls in this section per applicability function refreshSectionApplicability(section) { var count = 0; // Show/hide each item (section.items || []).forEach(function (item) { - item.hidden = !isApplicable(item); + item.hidden = !self.isApplicable(item); count += item.hidden ? 0 : 1; }); // Hide this section if there are no applicable items section.hidden = !count; } - // Show/hide controls if they are applicable - function refreshApplicability() { - toolbarStructure.sections.forEach(refreshSectionApplicability); + // Get initial value for a given property + function initializeState(property) { + var result; + // Look through all selections for this property; + // values should all match by the time we perform + // this lookup anyway. + self.selection.forEach(function (selected) { + result = (selected[property] !== undefined) ? + self.lookupState(property, selected) : + result; + }); + return result; } - // Refresh toolbar state to match selection - function refreshState() { - toolbarState = properties.map(initializeState); - } + this.selection = s; + this.toolbarStructure.sections.forEach(refreshSectionApplicability); + this.toolbarState = this.properties.map(initializeState); + }; - toolbarStructure.sections = - ((structure || {}).sections || []).map(convertSection); + /** + * Get the structure of the toolbar, as appropriate to + * pass to `mct-toolbar`. + * @returns the toolbar structure + */ + EditToolbar.prototype.getStructure = function () { + return this.toolbarStructure; + }; - toolbarState = []; + /** + * Get the current state of the toolbar, as appropriate + * to two-way bind to the state handled by `mct-toolbar`. + * @returns {Array} state of the toolbar + */ + EditToolbar.prototype.getState = function () { + return this.toolbarState; + }; - return { - /** - * Set the current selection. Visisbility of sections - * and items in the toolbar will be updated to match this. - * @param {Array} s the new selection - */ - setSelection: function (s) { - selection = s; - refreshApplicability(); - refreshState(); - }, - /** - * Get the structure of the toolbar, as appropriate to - * pass to `mct-toolbar`. - * @returns the toolbar structure - */ - getStructure: function () { - return toolbarStructure; - }, - /** - * Get the current state of the toolbar, as appropriate - * to two-way bind to the state handled by `mct-toolbar`. - * @returns {Array} state of the toolbar - */ - getState: function () { - return toolbarState; - }, - /** - * Update state within the current selection. - * @param {number} index the index of the corresponding - * element in the state array - * @param value the new value to convey to the selection - */ - updateState: function (index, value) { - return updateProperties(properties[index], value); + /** + * Update state within the current selection. + * @param {number} index the index of the corresponding + * element in the state array + * @param value the new value to convey to the selection + */ + EditToolbar.prototype.updateState = function (index, value) { + var self = this; + + // Update value for this property in all elements of the + // selection which have this property. + function updateProperties(property, value) { + var changed = false; + + // Update property in a selected element + function updateProperty(selected) { + // Ignore selected elements which don't have this property + if (selected[property] !== undefined) { + // Check if this is a setter, or just assignable + if (typeof selected[property] === 'function') { + changed = + changed || (selected[property]() !== value); + selected[property](value); + } else { + changed = + changed || (selected[property] !== value); + selected[property] = value; + } + } } - }; - } + + // Update property in all selected elements + self.selection.forEach(updateProperty); + + // Return whether or not anything changed + return changed; + } + + return updateProperties(this.properties[index], value); + }; return EditToolbar; } ); + diff --git a/platform/commonUI/edit/src/representers/EditToolbarRepresenter.js b/platform/commonUI/edit/src/representers/EditToolbarRepresenter.js index f94ddcd7a8..daf3645b69 100644 --- a/platform/commonUI/edit/src/representers/EditToolbarRepresenter.js +++ b/platform/commonUI/edit/src/representers/EditToolbarRepresenter.js @@ -27,17 +27,21 @@ define( "use strict"; // No operation - function noop() {} + var NOOP_REPRESENTER = { + represent: function () {}, + destroy: function () {} + }; /** * The EditToolbarRepresenter populates the toolbar in Edit mode * based on a view's definition. * @param {Scope} scope the Angular scope of the representation + * @memberof platform/commonUI/edit * @constructor + * @implements {Representer} */ function EditToolbarRepresenter(scope, element, attrs) { - var toolbar, - toolbarObject = {}; + var self = this; // Mark changes as ready to persist function commit(message) { @@ -49,31 +53,33 @@ define( // Handle changes to the current selection function updateSelection(selection) { // Only update if there is a toolbar to update - if (toolbar) { + if (self.toolbar) { // Make sure selection is array-like selection = Array.isArray(selection) ? selection : (selection ? [selection] : []); // Update the toolbar's selection - toolbar.setSelection(selection); + self.toolbar.setSelection(selection); // ...and expose its structure/state - toolbarObject.structure = toolbar.getStructure(); - toolbarObject.state = toolbar.getState(); + self.toolbarObject.structure = + self.toolbar.getStructure(); + self.toolbarObject.state = + self.toolbar.getState(); } } // Get state (to watch it) function getState() { - return toolbarObject.state; + return self.toolbarObject.state; } // Update selection models to match changed toolbar state function updateState(state) { // Update underlying state based on toolbar changes var changed = (state || []).map(function (value, index) { - return toolbar.updateState(index, value); + return self.toolbar.updateState(index, value); }).reduce(function (a, b) { return a || b; }, false); @@ -85,66 +91,73 @@ define( } } - // Initialize toolbar (expose object to parent scope) - function initialize(definition) { - // If we have been asked to expose toolbar state... - if (attrs.toolbar) { - // Initialize toolbar object - toolbar = new EditToolbar(definition, commit); - // Ensure toolbar state is exposed - scope.$parent[attrs.toolbar] = toolbarObject; - } - } - - // Represent a domain object using this definition - function represent(representation) { - // Get the newest toolbar definition from the view - var definition = (representation || {}).toolbar || {}; - // Expose the toolbar object to the parent scope - initialize(definition); - // Create a selection scope - scope.selection = new EditToolbarSelection(); - // Initialize toolbar to an empty selection - updateSelection([]); - } - - // Destroy; remove toolbar object from parent scope - function destroy() { + // Avoid attaching scope to this; + // http://errors.angularjs.org/1.2.26/ng/cpws + this.setSelection = function (s) { + scope.selection = s; + }; + this.clearExposedToolbar = function () { // Clear exposed toolbar state (if any) if (attrs.toolbar) { delete scope.$parent[attrs.toolbar]; } - } + }; + this.exposeToolbar = function () { + scope.$parent[self.attrs.toolbar] = self.toolbarObject; + }; + + this.commit = commit; + this.attrs = attrs; + this.updateSelection = updateSelection; + this.toolbar = undefined; + this.toolbarObject = {}; // If this representation exposes a toolbar, set up watches // to synchronize with it. - if (attrs.toolbar) { + if (attrs && attrs.toolbar) { // Detect and handle changes to state from the toolbar scope.$watchCollection(getState, updateState); // Watch for changes in the current selection state scope.$watchCollection("selection.all()", updateSelection); // Expose toolbar state under that name - scope.$parent[attrs.toolbar] = toolbarObject; + scope.$parent[attrs.toolbar] = this.toolbarObject; + } else { + // No toolbar declared, so do nothing. + return NOOP_REPRESENTER; } - return { - /** - * Set the current representation in use, and the domain - * object being represented. - * - * @param {RepresentationDefinition} representation the - * definition of the representation in use - * @param {DomainObject} domainObject the domain object - * being represented - */ - represent: (attrs || {}).toolbar ? represent : noop, - /** - * Release any resources associated with this representer. - */ - destroy: (attrs || {}).toolbar ? destroy : noop - }; } + // Represent a domain object using this definition + EditToolbarRepresenter.prototype.represent = function (representation) { + // Get the newest toolbar definition from the view + var definition = (representation || {}).toolbar || {}, + self = this; + + // Initialize toolbar (expose object to parent scope) + function initialize(definition) { + // If we have been asked to expose toolbar state... + if (self.attrs.toolbar) { + // Initialize toolbar object + self.toolbar = new EditToolbar(definition, self.commit); + // Ensure toolbar state is exposed + self.exposeToolbar(); + } + } + + // Expose the toolbar object to the parent scope + initialize(definition); + // Create a selection scope + this.setSelection(new EditToolbarSelection()); + // Initialize toolbar to an empty selection + this.updateSelection([]); + }; + + // Destroy; remove toolbar object from parent scope + EditToolbarRepresenter.prototype.destroy = function () { + this.clearExposedToolbar(); + }; + return EditToolbarRepresenter; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/src/representers/EditToolbarSelection.js b/platform/commonUI/edit/src/representers/EditToolbarSelection.js index d88ad0da1a..318ae935b5 100644 --- a/platform/commonUI/edit/src/representers/EditToolbarSelection.js +++ b/platform/commonUI/edit/src/representers/EditToolbarSelection.js @@ -37,110 +37,96 @@ define( * * The selection, for single selected elements within the * view. * + * @memberof platform/commonUI/edit * @constructor */ function EditToolbarSelection() { - var selection = [ {} ], - selecting = false, - selected; + this.selection = [{}]; + this.selecting = false; + this.selectedObj = undefined; + } - // Remove the currently-selected object - function deselect() { - // Nothing to do if we don't have a selected object - if (selecting) { - // Clear state tracking - selecting = false; - selected = undefined; + /** + * Check if an object is currently selected. + * @param {*} obj the object to check for selection + * @returns {boolean} true if selected, otherwise false + */ + EditToolbarSelection.prototype.selected = function (obj) { + return (obj === this.selectedObj) || (obj === this.selection[0]); + }; - // Remove the selection - selection.pop(); - - return true; - } + /** + * Select an object. + * @param obj the object to select + * @returns {boolean} true if selection changed + */ + EditToolbarSelection.prototype.select = function (obj) { + // Proxy is always selected + if (obj === this.selection[0]) { return false; } - // Select an object - function select(obj) { - // Proxy is always selected - if (obj === selection[0]) { - return false; - } + // Clear any existing selection + this.deselect(); - // Clear any existing selection - deselect(); + // Note the current selection state + this.selectedObj = obj; + this.selecting = true; - // Note the current selection state - selected = obj; - selecting = true; + // Add the selection + this.selection.push(obj); + }; - // Add the selection - selection.push(obj); + /** + * Clear the current selection. + * @returns {boolean} true if selection changed + */ + EditToolbarSelection.prototype.deselect = function () { + // Nothing to do if we don't have a selected object + if (this.selecting) { + // Clear state tracking + this.selecting = false; + this.selectedObj = undefined; + + // Remove the selection + this.selection.pop(); + + return true; } + return false; + }; + /** + * Get the currently-selected object. + * @returns the currently selected object + */ + EditToolbarSelection.prototype.get = function () { + return this.selectedObj; + }; - // Check if an object is selected - function isSelected(obj) { - return (obj === selected) || (obj === selection[0]); + /** + * Get/set the view proxy (for toolbar actions taken upon + * the view itself.) + * @param [proxy] the view proxy (if setting) + * @returns the current view proxy + */ + EditToolbarSelection.prototype.proxy = function (p) { + if (arguments.length > 0) { + this.selection[0] = p; } + return this.selection[0]; + }; - // Getter for current selection - function get() { - return selected; - } - - // Getter/setter for view proxy - function proxy(p) { - if (arguments.length > 0) { - selection[0] = p; - } - return selection[0]; - } - - // Getter for the full array of selected objects (incl. view proxy) - function all() { - return selection; - } - - return { - /** - * Check if an object is currently selected. - * @returns true if selected, otherwise false - */ - selected: isSelected, - /** - * Select an object. - * @param obj the object to select - * @returns {boolean} true if selection changed - */ - select: select, - /** - * Clear the current selection. - * @returns {boolean} true if selection changed - */ - deselect: deselect, - /** - * Get the currently-selected object. - * @returns the currently selected object - */ - get: get, - /** - * Get/set the view proxy (for toolbar actions taken upon - * the view itself.) - * @param [proxy] the view proxy (if setting) - * @returns the current view proxy - */ - proxy: proxy, - /** - * Get an array containing all selections, including the - * selection proxy. It is generally not advisable to - * mutate this array directly. - * @returns {Array} all selections - */ - all: all - }; - } + /** + * Get an array containing all selections, including the + * selection proxy. It is generally not advisable to + * mutate this array directly. + * @returns {Array} all selections + */ + EditToolbarSelection.prototype.all = function () { + return this.selection; + }; return EditToolbarSelection; } -); \ No newline at end of file +); diff --git a/platform/commonUI/edit/test/objects/EditableDomainObjectCacheSpec.js b/platform/commonUI/edit/test/objects/EditableDomainObjectCacheSpec.js index 39cac56d9f..4eea727e26 100644 --- a/platform/commonUI/edit/test/objects/EditableDomainObjectCacheSpec.js +++ b/platform/commonUI/edit/test/objects/EditableDomainObjectCacheSpec.js @@ -112,7 +112,9 @@ define( }); it("saves objects that have been marked dirty", function () { - var objects = ['a', 'b', 'c'].map(TestObject).map(cache.getEditableObject); + var objects = ['a', 'b', 'c'].map(TestObject).map(function (domainObject) { + return cache.getEditableObject(domainObject); + }); cache.markDirty(objects[0]); cache.markDirty(objects[2]); @@ -123,7 +125,9 @@ define( }); it("does not save objects that have been marked clean", function () { - var objects = ['a', 'b', 'c'].map(TestObject).map(cache.getEditableObject); + var objects = ['a', 'b', 'c'].map(TestObject).map(function (domainObject) { + return cache.getEditableObject(domainObject); + }); cache.markDirty(objects[0]); cache.markDirty(objects[2]); diff --git a/platform/commonUI/general/res/css/theme-espresso.css b/platform/commonUI/general/res/css/theme-espresso.css index db8b8ef47f..6edf471f13 100644 --- a/platform/commonUI/general/res/css/theme-espresso.css +++ b/platform/commonUI/general/res/css/theme-espresso.css @@ -1237,29 +1237,32 @@ table { table .tr .th:first-child { border-left: none; } /* line 85, ../sass/lists/_tabular.scss */ - .tabular tr th.sort .icon-sorting:before, .tabular tr .th.sort .icon-sorting:before, .tabular .tr th.sort .icon-sorting:before, .tabular .tr .th.sort .icon-sorting:before, - table tr th.sort .icon-sorting:before, - table tr .th.sort .icon-sorting:before, - table .tr th.sort .icon-sorting:before, - table .tr .th.sort .icon-sorting:before { - display: inline-block; + .tabular tr th.sort.sort:after, .tabular tr .th.sort.sort:after, .tabular .tr th.sort.sort:after, .tabular .tr .th.sort.sort:after, + table tr th.sort.sort:after, + table tr .th.sort.sort:after, + table .tr th.sort.sort:after, + table .tr .th.sort.sort:after { + color: #49dedb; font-family: symbolsfont; - margin-left: 5px; } - /* line 90, ../sass/lists/_tabular.scss */ - .tabular tr th.sort.asc .icon-sorting:before, .tabular tr .th.sort.asc .icon-sorting:before, .tabular .tr th.sort.asc .icon-sorting:before, .tabular .tr .th.sort.asc .icon-sorting:before, - table tr th.sort.asc .icon-sorting:before, - table tr .th.sort.asc .icon-sorting:before, - table .tr th.sort.asc .icon-sorting:before, - table .tr .th.sort.asc .icon-sorting:before { - content: '0'; } + font-size: 8px; + content: "\ed"; + display: inline-block; + margin-left: 3px; } /* line 93, ../sass/lists/_tabular.scss */ - .tabular tr th.sort.desc .icon-sorting:before, .tabular tr .th.sort.desc .icon-sorting:before, .tabular .tr th.sort.desc .icon-sorting:before, .tabular .tr .th.sort.desc .icon-sorting:before, - table tr th.sort.desc .icon-sorting:before, - table tr .th.sort.desc .icon-sorting:before, - table .tr th.sort.desc .icon-sorting:before, - table .tr .th.sort.desc .icon-sorting:before { - content: '1'; } - /* line 98, ../sass/lists/_tabular.scss */ + .tabular tr th.sort.sort.desc:after, .tabular tr .th.sort.sort.desc:after, .tabular .tr th.sort.sort.desc:after, .tabular .tr .th.sort.sort.desc:after, + table tr th.sort.sort.desc:after, + table tr .th.sort.sort.desc:after, + table .tr th.sort.sort.desc:after, + table .tr .th.sort.sort.desc:after { + content: "\ec"; } + /* line 97, ../sass/lists/_tabular.scss */ + .tabular tr th.sortable, .tabular tr .th.sortable, .tabular .tr th.sortable, .tabular .tr .th.sortable, + table tr th.sortable, + table tr .th.sortable, + table .tr th.sortable, + table .tr .th.sortable { + cursor: pointer; } + /* line 101, ../sass/lists/_tabular.scss */ .tabular tr td, .tabular tr .td, .tabular .tr td, .tabular .tr .td, table tr td, table tr .td, @@ -1271,21 +1274,21 @@ table { padding: 3px 5px; word-wrap: break-word; vertical-align: top; } - /* line 105, ../sass/lists/_tabular.scss */ + /* line 108, ../sass/lists/_tabular.scss */ .tabular tr td.numeric, .tabular tr .td.numeric, .tabular .tr td.numeric, .tabular .tr .td.numeric, table tr td.numeric, table tr .td.numeric, table .tr td.numeric, table .tr .td.numeric { text-align: right; } - /* line 108, ../sass/lists/_tabular.scss */ + /* line 111, ../sass/lists/_tabular.scss */ .tabular tr td.s-cell-type-value, .tabular tr .td.s-cell-type-value, .tabular .tr td.s-cell-type-value, .tabular .tr .td.s-cell-type-value, table tr td.s-cell-type-value, table tr .td.s-cell-type-value, table .tr td.s-cell-type-value, table .tr .td.s-cell-type-value { text-align: right; } - /* line 110, ../sass/lists/_tabular.scss */ + /* line 113, ../sass/lists/_tabular.scss */ .tabular tr td.s-cell-type-value .l-cell-contents, .tabular tr .td.s-cell-type-value .l-cell-contents, .tabular .tr td.s-cell-type-value .l-cell-contents, .tabular .tr .td.s-cell-type-value .l-cell-contents, table tr td.s-cell-type-value .l-cell-contents, table tr .td.s-cell-type-value .l-cell-contents, @@ -1296,23 +1299,23 @@ table { border-radius: 2px; padding-left: 5px; padding-right: 5px; } - /* line 126, ../sass/lists/_tabular.scss */ + /* line 129, ../sass/lists/_tabular.scss */ .tabular.filterable tbody, .tabular.filterable .tbody, table.filterable tbody, table.filterable .tbody { top: 44px; } - /* line 129, ../sass/lists/_tabular.scss */ + /* line 132, ../sass/lists/_tabular.scss */ .tabular.filterable input[type="text"], table.filterable input[type="text"] { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; width: 100%; } - /* line 135, ../sass/lists/_tabular.scss */ + /* line 138, ../sass/lists/_tabular.scss */ .tabular.fixed-header, table.fixed-header { height: 100%; } - /* line 137, ../sass/lists/_tabular.scss */ + /* line 140, ../sass/lists/_tabular.scss */ .tabular.fixed-header thead, .tabular.fixed-header .thead, .tabular.fixed-header tbody tr, .tabular.fixed-header .tbody .tr, table.fixed-header thead, @@ -1321,12 +1324,12 @@ table { table.fixed-header .tbody .tr { display: table; table-layout: fixed; } - /* line 142, ../sass/lists/_tabular.scss */ + /* line 145, ../sass/lists/_tabular.scss */ .tabular.fixed-header thead, .tabular.fixed-header .thead, table.fixed-header thead, table.fixed-header .thead { width: calc(100% - 10px); } - /* line 144, ../sass/lists/_tabular.scss */ + /* line 147, ../sass/lists/_tabular.scss */ .tabular.fixed-header thead:before, .tabular.fixed-header .thead:before, table.fixed-header thead:before, table.fixed-header .thead:before { @@ -1337,7 +1340,7 @@ table { width: 100%; height: 22px; background: rgba(255, 255, 255, 0.15); } - /* line 154, ../sass/lists/_tabular.scss */ + /* line 157, ../sass/lists/_tabular.scss */ .tabular.fixed-header tbody, .tabular.fixed-header .tbody, table.fixed-header tbody, table.fixed-header .tbody { @@ -1352,7 +1355,7 @@ table { top: 22px; display: block; overflow-y: scroll; } - /* line 162, ../sass/lists/_tabular.scss */ + /* line 165, ../sass/lists/_tabular.scss */ .tabular.t-event-messages td, .tabular.t-event-messages .td, table.t-event-messages td, table.t-event-messages .td { @@ -4429,26 +4432,26 @@ input[type="text"] { .l-infobubble-wrapper .l-infobubble table tr td { padding: 2px 0; vertical-align: top; } - /* line 57, ../sass/helpers/_bubbles.scss */ + /* line 53, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper .l-infobubble table tr td.label { padding-right: 10px; white-space: nowrap; } - /* line 61, ../sass/helpers/_bubbles.scss */ + /* line 57, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper .l-infobubble table tr td.value { - white-space: nowrap; } - /* line 65, ../sass/helpers/_bubbles.scss */ + word-break: break-all; } + /* line 61, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper .l-infobubble table tr td.align-wrap { white-space: normal; } - /* line 71, ../sass/helpers/_bubbles.scss */ + /* line 67, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper .l-infobubble .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 5px; } - /* line 78, ../sass/helpers/_bubbles.scss */ + /* line 74, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper.arw-left { margin-left: 20px; } - /* line 80, ../sass/helpers/_bubbles.scss */ + /* line 76, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper.arw-left .l-infobubble::before { right: 100%; width: 0; @@ -4456,10 +4459,10 @@ input[type="text"] { border-top: 6.66667px solid transparent; border-bottom: 6.66667px solid transparent; border-right: 10px solid #ddd; } - /* line 86, ../sass/helpers/_bubbles.scss */ + /* line 82, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper.arw-right { margin-right: 20px; } - /* line 88, ../sass/helpers/_bubbles.scss */ + /* line 84, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper.arw-right .l-infobubble::before { left: 100%; width: 0; @@ -4467,16 +4470,16 @@ input[type="text"] { border-top: 6.66667px solid transparent; border-bottom: 6.66667px solid transparent; border-left: 10px solid #ddd; } - /* line 95, ../sass/helpers/_bubbles.scss */ + /* line 91, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper.arw-top .l-infobubble::before { top: 20px; } - /* line 101, ../sass/helpers/_bubbles.scss */ + /* line 97, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper.arw-btm .l-infobubble::before { bottom: 20px; } - /* line 106, ../sass/helpers/_bubbles.scss */ + /* line 102, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper.arw-down { margin-bottom: 10px; } - /* line 108, ../sass/helpers/_bubbles.scss */ + /* line 104, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper.arw-down .l-infobubble::before { left: 50%; top: 100%; @@ -4484,21 +4487,21 @@ input[type="text"] { border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 7.5px solid #ddd; } - /* line 117, ../sass/helpers/_bubbles.scss */ + /* line 113, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper .arw { z-index: 2; } - /* line 120, ../sass/helpers/_bubbles.scss */ + /* line 116, ../sass/helpers/_bubbles.scss */ .l-infobubble-wrapper.arw-up .arw.arw-down, .l-infobubble-wrapper.arw-down .arw.arw-up { display: none; } -/* line 127, ../sass/helpers/_bubbles.scss */ +/* line 125, ../sass/helpers/_bubbles.scss */ .l-thumbsbubble-wrapper .arw-up { width: 0; height: 0; border-left: 6.66667px solid transparent; border-right: 6.66667px solid transparent; border-bottom: 10px solid #4d4d4d; } -/* line 130, ../sass/helpers/_bubbles.scss */ +/* line 128, ../sass/helpers/_bubbles.scss */ .l-thumbsbubble-wrapper .arw-down { width: 0; height: 0; @@ -4506,7 +4509,7 @@ input[type="text"] { border-right: 6.66667px solid transparent; border-top: 10px solid #4d4d4d; } -/* line 134, ../sass/helpers/_bubbles.scss */ +/* line 133, ../sass/helpers/_bubbles.scss */ .s-infobubble { -moz-border-radius: 2px; -webkit-border-radius: 2px; @@ -4517,22 +4520,29 @@ input[type="text"] { background: #ddd; color: #666; font-size: 0.8rem; } - /* line 141, ../sass/helpers/_bubbles.scss */ + /* line 140, ../sass/helpers/_bubbles.scss */ .s-infobubble .title { color: #333333; font-weight: bold; } /* line 146, ../sass/helpers/_bubbles.scss */ - .s-infobubble tr td { - border-top: 1px solid #c4c4c4; + .s-infobubble table tr td { + border: none; + border-top: 1px solid #c4c4c4 !important; font-size: 0.9em; } - /* line 150, ../sass/helpers/_bubbles.scss */ - .s-infobubble tr:first-child td { + /* line 152, ../sass/helpers/_bubbles.scss */ + .s-infobubble table tr:first-child td { + border-top: none !important; } + /* line 157, ../sass/helpers/_bubbles.scss */ + .s-infobubble:first-child td { border-top: none; } - /* line 154, ../sass/helpers/_bubbles.scss */ + /* line 161, ../sass/helpers/_bubbles.scss */ + .s-infobubble .label { + color: gray; } + /* line 165, ../sass/helpers/_bubbles.scss */ .s-infobubble .value { color: #333333; } -/* line 159, ../sass/helpers/_bubbles.scss */ +/* line 171, ../sass/helpers/_bubbles.scss */ .s-thumbsbubble { background: #4d4d4d; color: #b3b3b3; } diff --git a/platform/commonUI/general/res/sass/helpers/_bubbles.scss b/platform/commonUI/general/res/sass/helpers/_bubbles.scss index 5b174ba6da..10deec9645 100644 --- a/platform/commonUI/general/res/sass/helpers/_bubbles.scss +++ b/platform/commonUI/general/res/sass/helpers/_bubbles.scss @@ -48,19 +48,15 @@ width: 100%; tr { td { - //max-width: 150px; padding: 2px 0; vertical-align: top; - //white-space: nowrap; - //overflow: hidden; - //text-overflow: ellipsis; &.label { padding-right: $interiorMargin * 2; white-space: nowrap; } &.value { - white-space: nowrap; - //width: 90%; + //word-wrap: break-word; // Doesn't work in ? + word-break: break-all; } &.align-wrap { white-space: normal; @@ -118,7 +114,9 @@ z-index: 2; } &.arw-up .arw.arw-down, - &.arw-down .arw.arw-up { display: none; } + &.arw-down .arw.arw-up { + display: none; + } } //************************************************* LOOK AND FEEL @@ -131,6 +129,7 @@ @include triangle('down', $bubbleArwSize, 1.5, $colorThumbsBubbleBg); } } + .s-infobubble { $emFg: darken($colorInfoBubbleFg, 20%); @include border-radius($basicCr); @@ -142,18 +141,31 @@ color: $emFg; font-weight: bold; } - tr { - td { - border-top: 1px solid darken($colorInfoBubbleBg, 10%); - font-size: 0.9em; - } - &:first-child td { - border-top: none; + table { + tr { + td { + border: none; + border-top: 1px solid darken($colorInfoBubbleBg, 10%) !important; + font-size: 0.9em; + } + + &:first-child td { + border-top: none !important; + } } } + &:first-child td { + border-top: none; + } + + .label { + color: lighten($emFg, 30%); + } + .value { color: $emFg; } + } .s-thumbsbubble { diff --git a/platform/commonUI/general/res/sass/lists/_tabular.scss b/platform/commonUI/general/res/sass/lists/_tabular.scss index 629cac9d1a..0621cad46b 100644 --- a/platform/commonUI/general/res/sass/lists/_tabular.scss +++ b/platform/commonUI/general/res/sass/lists/_tabular.scss @@ -82,18 +82,21 @@ table { border-left: none; } &.sort { - .icon-sorting:before { - display: inline-block; + &.sort:after { + color: $colorIconLink; font-family: symbolsfont; - margin-left: 5px; + font-size: 8px; + content: "\ed"; + display: inline-block; + margin-left: $interiorMarginSm; } - &.asc .icon-sorting:before { - content: '0'; - } - &.desc .icon-sorting:before { - content: '1'; + &.sort.desc:after { + content: "\ec"; } } + &.sortable { + cursor: pointer; + } } td, .td { border-bottom: 1px solid $tabularColorBorder; diff --git a/platform/commonUI/general/src/StyleSheetLoader.js b/platform/commonUI/general/src/StyleSheetLoader.js index 9775288b28..19c0ffc291 100644 --- a/platform/commonUI/general/src/StyleSheetLoader.js +++ b/platform/commonUI/general/src/StyleSheetLoader.js @@ -21,6 +21,11 @@ *****************************************************************************/ /*global define*/ +/** + * This bundle provides various general-purpose UI elements, including + * platform styling. + * @namespace platform/commonUI/general + */ define( [], function () { @@ -29,6 +34,7 @@ define( /** * The StyleSheetLoader adds links to style sheets exposed from * various bundles as extensions of category `stylesheets`. + * @memberof platform/commonUI/general * @constructor * @param {object[]} stylesheets stylesheet extension definitions * @param $document Angular's jqLite-wrapped document element @@ -62,4 +68,4 @@ define( return StyleSheetLoader; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/ActionGroupController.js b/platform/commonUI/general/src/controllers/ActionGroupController.js index 1675d2e611..0992b5e967 100644 --- a/platform/commonUI/general/src/controllers/ActionGroupController.js +++ b/platform/commonUI/general/src/controllers/ActionGroupController.js @@ -42,6 +42,7 @@ define( * * `ungrouped`: All actions which did not have a defined * group. * + * @memberof platform/commonUI/general * @constructor */ function ActionGroupController($scope) { @@ -102,4 +103,4 @@ define( return ActionGroupController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/BottomBarController.js b/platform/commonUI/general/src/controllers/BottomBarController.js index b33ce0fe7a..d53d76fce6 100644 --- a/platform/commonUI/general/src/controllers/BottomBarController.js +++ b/platform/commonUI/general/src/controllers/BottomBarController.js @@ -29,6 +29,7 @@ define( /** * Controller for the bottombar template. Exposes * available indicators (of extension category "indicators") + * @memberof platform/commonUI/general * @constructor */ function BottomBarController(indicators) { @@ -42,20 +43,19 @@ define( }; } - indicators = indicators.map(present); - - return { - /** - * Get all indicators to display. - * @returns {Indicator[]} all indicators - * to display in the bottom bar. - */ - getIndicators: function () { - return indicators; - } - }; + this.indicators = indicators.map(present); } + /** + * Get all indicators to display. + * @returns {Indicator[]} all indicators + * to display in the bottom bar. + * @memberof platform/commonUI/general.BottomBarController# + */ + BottomBarController.prototype.getIndicators = function () { + return this.indicators; + }; + return BottomBarController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/ClickAwayController.js b/platform/commonUI/general/src/controllers/ClickAwayController.js index 75b1d985da..9c7c6f8091 100644 --- a/platform/commonUI/general/src/controllers/ClickAwayController.js +++ b/platform/commonUI/general/src/controllers/ClickAwayController.js @@ -31,71 +31,69 @@ define( * menus) where clicking elsewhere in the document while the toggle * is in an active state is intended to dismiss the toggle. * + * @memberof platform/commonUI/general * @constructor * @param $scope the scope in which this controller is active * @param $document the document element, injected by Angular */ function ClickAwayController($scope, $document) { - var state = false, - clickaway; + var self = this; - // Track state, but also attach and detach a listener for - // mouseup events on the document. - function deactivate() { - state = false; - $document.off("mouseup", clickaway); - } - - function activate() { - state = true; - $document.on("mouseup", clickaway); - } - - function changeState() { - if (state) { - deactivate(); - } else { - activate(); - } - } + this.state = false; + this.$scope = $scope; + this.$document = $document; // Callback used by the document listener. Deactivates; // note also $scope.$apply is invoked to indicate that // the state of this controller has changed. - clickaway = function () { - deactivate(); + this.clickaway = function () { + self.deactivate(); $scope.$apply(); return false; }; - - return { - /** - * Get the current state of the toggle. - * @return {boolean} true if active - */ - isActive: function () { - return state; - }, - /** - * Set a new state for the toggle. - * @return {boolean} true to activate - */ - setState: function (newState) { - if (state !== newState) { - changeState(); - } - }, - /** - * Toggle the current state; activate if it is inactive, - * deactivate if it is active. - */ - toggle: function () { - changeState(); - } - }; - } + // Track state, but also attach and detach a listener for + // mouseup events on the document. + ClickAwayController.prototype.deactivate = function () { + this.state = false; + this.$document.off("mouseup", this.clickaway); + }; + ClickAwayController.prototype.activate = function () { + this.state = true; + this.$document.on("mouseup", this.clickaway); + }; + + /** + * Get the current state of the toggle. + * @return {boolean} true if active + */ + ClickAwayController.prototype.isActive =function () { + return this.state; + }; + + /** + * Set a new state for the toggle. + * @return {boolean} true to activate + */ + ClickAwayController.prototype.setState = function (newState) { + if (this.state !== newState) { + this.toggle(); + } + }; + + /** + * Toggle the current state; activate if it is inactive, + * deactivate if it is active. + */ + ClickAwayController.prototype.toggle = function () { + if (this.state) { + this.deactivate(); + } else { + this.activate(); + } + }; + return ClickAwayController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/ContextMenuController.js b/platform/commonUI/general/src/controllers/ContextMenuController.js index 4e1bcd3b8f..dece522682 100644 --- a/platform/commonUI/general/src/controllers/ContextMenuController.js +++ b/platform/commonUI/general/src/controllers/ContextMenuController.js @@ -33,6 +33,7 @@ define( * Controller for the context menu. Maintains an up-to-date * list of applicable actions (those from category "contextual") * + * @memberof platform/commonUI/general * @constructor */ function ContextMenuController($scope) { @@ -49,4 +50,4 @@ define( return ContextMenuController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/GetterSetterController.js b/platform/commonUI/general/src/controllers/GetterSetterController.js index 05d7b2f4ef..3d61c00116 100644 --- a/platform/commonUI/general/src/controllers/GetterSetterController.js +++ b/platform/commonUI/general/src/controllers/GetterSetterController.js @@ -54,6 +54,7 @@ define( * parameter it received.) Getter-setter functions are never the * target of a scope assignment and so avoid this problem. * + * @memberof platform/commonUI/general * @constructor * @param {Scope} $scope the controller's scope */ @@ -87,4 +88,4 @@ define( return GetterSetterController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/SelectorController.js b/platform/commonUI/general/src/controllers/SelectorController.js index 5b09f3423a..26fe5f4d62 100644 --- a/platform/commonUI/general/src/controllers/SelectorController.js +++ b/platform/commonUI/general/src/controllers/SelectorController.js @@ -30,6 +30,7 @@ define( /** * Controller for the domain object selector control. + * @memberof platform/commonUI/general * @constructor * @param {ObjectService} objectService service from which to * read domain objects @@ -38,28 +39,17 @@ define( function SelectorController(objectService, $scope) { var treeModel = {}, listModel = {}, - selectedObjects = [], - rootObject, - previousSelected; + previousSelected, + self = this; // For watch; look at the user's selection in the tree function getTreeSelection() { return treeModel.selectedObject; } - // Get the value of the field being edited - function getField() { - return $scope.ngModel[$scope.field] || []; - } - - // Get the value of the field being edited - function setField(value) { - $scope.ngModel[$scope.field] = value; - } - // Store root object for subsequent exposure to template function storeRoot(objects) { - rootObject = objects[ROOT_ID]; + self.rootObject = objects[ROOT_ID]; } // Check that a selection is of the valid type @@ -82,7 +72,8 @@ define( function updateSelectedObjects(objects) { // Look up from the function getObject(id) { return objects[id]; } - selectedObjects = ids.filter(getObject).map(getObject); + self.selectedObjects = + ids.filter(getObject).map(getObject); } // Look up objects by id, then populate right-hand list @@ -93,64 +84,85 @@ define( $scope.$watch(getTreeSelection, validateTreeSelection); // Make sure right-hand list matches underlying model - $scope.$watchCollection(getField, updateList); + $scope.$watchCollection(function () { + return self.getField(); + }, updateList); // Look up root object, then store it objectService.getObjects([ROOT_ID]).then(storeRoot); - return { - /** - * Get the root object to show in the left-hand tree. - * @returns {DomainObject} the root object - */ - root: function () { - return rootObject; - }, - /** - * Add a domain object to the list of selected objects. - * @param {DomainObject} the domain object to select - */ - select: function (domainObject) { - var id = domainObject && domainObject.getId(), - list = getField() || []; - // Only select if we have a valid id, - // and it isn't already selected - if (id && list.indexOf(id) === -1) { - setField(list.concat([id])); - } - }, - /** - * Remove a domain object from the list of selected objects. - * @param {DomainObject} the domain object to select - */ - deselect: function (domainObject) { - var id = domainObject && domainObject.getId(), - list = getField() || []; - // Only change if this was a valid id, - // for an object which was already selected - if (id && list.indexOf(id) !== -1) { - // Filter it out of the current field - setField(list.filter(function (otherId) { - return otherId !== id; - })); - // Clear the current list selection - delete listModel.selectedObject; - } - }, - /** - * Get the currently-selected domain objects. - * @returns {DomainObject[]} the current selection - */ - selected: function () { - return selectedObjects; - }, - // Expose tree/list model for use in template directly - treeModel: treeModel, - listModel: listModel - }; + this.$scope = $scope; + this.selectedObjects = []; + + // Expose tree/list model for use in template directly + this.treeModel = treeModel; + this.listModel = listModel; } + + + // Set the value of the field being edited + SelectorController.prototype.setField = function (value) { + this.$scope.ngModel[this.$scope.field] = value; + }; + + // Get the value of the field being edited + SelectorController.prototype.getField = function () { + return this.$scope.ngModel[this.$scope.field] || []; + }; + + + /** + * Get the root object to show in the left-hand tree. + * @returns {DomainObject} the root object + */ + SelectorController.prototype.root = function () { + return this.rootObject; + }; + + /** + * Add a domain object to the list of selected objects. + * @param {DomainObject} the domain object to select + */ + SelectorController.prototype.select = function (domainObject) { + var id = domainObject && domainObject.getId(), + list = this.getField() || []; + // Only select if we have a valid id, + // and it isn't already selected + if (id && list.indexOf(id) === -1) { + this.setField(list.concat([id])); + } + }; + + /** + * Remove a domain object from the list of selected objects. + * @param {DomainObject} the domain object to select + */ + SelectorController.prototype.deselect = function (domainObject) { + var id = domainObject && domainObject.getId(), + list = this.getField() || []; + // Only change if this was a valid id, + // for an object which was already selected + if (id && list.indexOf(id) !== -1) { + // Filter it out of the current field + this.setField(list.filter(function (otherId) { + return otherId !== id; + })); + // Clear the current list selection + delete this.listModel.selectedObject; + } + }; + + /** + * Get the currently-selected domain objects. + * @returns {DomainObject[]} the current selection + */ + SelectorController.prototype.selected = function () { + return this.selectedObjects; + }; + + return SelectorController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/SplitPaneController.js b/platform/commonUI/general/src/controllers/SplitPaneController.js index c747f2f256..75dfed28d9 100644 --- a/platform/commonUI/general/src/controllers/SplitPaneController.js +++ b/platform/commonUI/general/src/controllers/SplitPaneController.js @@ -32,59 +32,58 @@ define( /** * Controller for the splitter in Browse mode. Current implementation * uses many hard-coded constants; this could be generalized. + * @memberof platform/commonUI/general * @constructor */ function SplitPaneController() { - var current = 200, - start = 200, - assigned = false; - - return { - /** - * Get the current position of the splitter, in pixels - * from the left edge. - * @returns {number} position of the splitter, in pixels - */ - state: function (defaultState) { - // Set the state to the desired default, if we don't have a - // "real" current state yet. - if (arguments.length > 0 && !assigned) { - current = defaultState; - assigned = true; - } - return current; - }, - /** - * Begin moving the splitter; this will note the splitter's - * current position, which is necessary for correct - * interpretation of deltas provided by mct-drag. - */ - startMove: function () { - start = current; - }, - /** - * Move the splitter a number of pixels to the right - * (negative numbers move the splitter to the left.) - * This movement is relative to the position of the - * splitter when startMove was last invoked. - * @param {number} delta number of pixels to move - */ - move: function (delta, minimum, maximum) { - // Ensure defaults for minimum/maximum - maximum = isNaN(maximum) ? DEFAULT_MAXIMUM : maximum; - minimum = isNaN(minimum) ? DEFAULT_MINIMUM : minimum; - - // Update current splitter state - current = Math.min( - maximum, - Math.max(minimum, start + delta) - ); - - //console.log(current + "; minimum: " + minimum + "; max: " + maximum); - } - }; + this.current = 200; + this.start = 200; + this.assigned = false; } + /** + * Get the current position of the splitter, in pixels + * from the left edge. + * @returns {number} position of the splitter, in pixels + */ + SplitPaneController.prototype.state = function (defaultState) { + // Set the state to the desired default, if we don't have a + // "real" current state yet. + if (arguments.length > 0 && !this.assigned) { + this.current = defaultState; + this.assigned = true; + } + return this.current; + }; + + /** + * Begin moving the splitter; this will note the splitter's + * current position, which is necessary for correct + * interpretation of deltas provided by mct-drag. + */ + SplitPaneController.prototype.startMove = function () { + this.start = this.current; + }; + + /** + * Move the splitter a number of pixels to the right + * (negative numbers move the splitter to the left.) + * This movement is relative to the position of the + * splitter when startMove was last invoked. + * @param {number} delta number of pixels to move + */ + SplitPaneController.prototype.move = function (delta, minimum, maximum) { + // Ensure defaults for minimum/maximum + maximum = isNaN(maximum) ? DEFAULT_MAXIMUM : maximum; + minimum = isNaN(minimum) ? DEFAULT_MINIMUM : minimum; + + // Update current splitter state + this.current = Math.min( + maximum, + Math.max(minimum, this.start + delta) + ); + }; + return SplitPaneController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/ToggleController.js b/platform/commonUI/general/src/controllers/ToggleController.js index 0d3bd664ca..9d7d493f15 100644 --- a/platform/commonUI/general/src/controllers/ToggleController.js +++ b/platform/commonUI/general/src/controllers/ToggleController.js @@ -30,37 +30,37 @@ define( * A ToggleController is used to activate/deactivate things. * A common usage is for "twistie" * + * @memberof platform/commonUI/general * @constructor */ function ToggleController() { - var state = false; - - return { - /** - * Get the current state of the toggle. - * @return {boolean} true if active - */ - isActive: function () { - return state; - }, - /** - * Set a new state for the toggle. - * @return {boolean} true to activate - */ - setState: function (newState) { - state = newState; - }, - /** - * Toggle the current state; activate if it is inactive, - * deactivate if it is active. - */ - toggle: function () { - state = !state; - } - }; - + this.state = false; } + /** + * Get the current state of the toggle. + * @return {boolean} true if active + */ + ToggleController.prototype.isActive = function () { + return this.state; + }; + + /** + * Set a new state for the toggle. + * @return {boolean} true to activate + */ + ToggleController.prototype.setState = function (newState) { + this.state = newState; + }; + + /** + * Toggle the current state; activate if it is inactive, + * deactivate if it is active. + */ + ToggleController.prototype.toggle = function () { + this.state = !this.state; + }; + return ToggleController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/TreeNodeController.js b/platform/commonUI/general/src/controllers/TreeNodeController.js index 77124bb6e3..3c3829700b 100644 --- a/platform/commonUI/general/src/controllers/TreeNodeController.js +++ b/platform/commonUI/general/src/controllers/TreeNodeController.js @@ -48,12 +48,12 @@ define( * node expansion when this tree node's _subtree_ will contain * the navigated object (recursively, this becomes an * expand-to-show-navigated-object behavior.) + * @memberof platform/commonUI/general * @constructor */ - function TreeNodeController($scope, $timeout, $rootScope) { - var selectedObject = ($scope.ngModel || {}).selectedObject, - isSelected = false, - hasBeenExpanded = false; + function TreeNodeController($scope, $timeout) { + var self = this, + selectedObject = ($scope.ngModel || {}).selectedObject; // Look up the id for a domain object. A convenience // for mapping; additionally does some undefined-checking. @@ -76,17 +76,6 @@ define( checkPath(nodePath, navPath, index + 1)); } - // Track that a node has been expanded, either by the - // user or automatically to show a selection. - function trackExpansion() { - if (!hasBeenExpanded) { - // Run on a timeout; if a lot of expansion needs to - // occur (e.g. if the selection is several nodes deep) we - // want this to be spread across multiple digest cycles. - $timeout(function () { hasBeenExpanded = true; }, 0); - } - } - // Consider the currently-navigated object and update // parameters which support display. function checkSelection() { @@ -101,7 +90,7 @@ define( // Deselect; we will reselect below, iff we are // exactly at the end of the path. - isSelected = false; + self.isSelectedFlag = false; // Expand if necessary (if the navigated object will // be in this node's subtree) @@ -120,12 +109,12 @@ define( // at the end of the path, highlight; // otherwise, expand. if (nodePath.length === navPath.length) { - isSelected = true; + self.isSelectedFlag = true; } else { // node path is shorter: Expand! if ($scope.toggle) { $scope.toggle.setState(true); } - trackExpansion(); + self.trackExpansion(); } } @@ -138,38 +127,55 @@ define( selectedObject = object; checkSelection(); } - + + this.isSelectedFlag = false; + this.hasBeenExpandedFlag = false; + this.$timeout = $timeout; + // Listen for changes which will effect display parameters $scope.$watch("ngModel.selectedObject", setSelection); $scope.$watch("domainObject", checkSelection); - return { - /** - * This method should be called when a node is expanded - * to record that this has occurred, to support one-time - * lazy loading of the node's subtree. - */ - trackExpansion: trackExpansion, - /** - * Check if this not has ever been expanded. - * @returns true if it has been expanded - */ - hasBeenExpanded: function () { - return hasBeenExpanded; - }, - /** - * Check whether or not the domain object represented by - * this tree node should be highlighted. - * An object will be highlighted if it matches - * ngModel.selectedObject - * @returns true if this should be highlighted - */ - isSelected: function () { - return isSelected; - } - }; + + } + /** + * This method should be called when a node is expanded + * to record that this has occurred, to support one-time + * lazy loading of the node's subtree. + */ + TreeNodeController.prototype.trackExpansion = function () { + var self = this; + if (!self.hasBeenExpanded()) { + // Run on a timeout; if a lot of expansion needs to + // occur (e.g. if the selection is several nodes deep) we + // want this to be spread across multiple digest cycles. + self.$timeout(function () { + self.hasBeenExpandedFlag = true; + }, 0); + } + }; + + /** + * Check if this not has ever been expanded. + * @returns true if it has been expanded + */ + TreeNodeController.prototype.hasBeenExpanded = function () { + return this.hasBeenExpandedFlag; + }; + + /** + * Check whether or not the domain object represented by + * this tree node should be highlighted. + * An object will be highlighted if it matches + * ngModel.selectedObject + * @returns true if this should be highlighted + */ + TreeNodeController.prototype.isSelected = function () { + return this.isSelectedFlag; + }; + return TreeNodeController; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/controllers/ViewSwitcherController.js b/platform/commonUI/general/src/controllers/ViewSwitcherController.js index 69674013d5..a3ab2e7bc4 100644 --- a/platform/commonUI/general/src/controllers/ViewSwitcherController.js +++ b/platform/commonUI/general/src/controllers/ViewSwitcherController.js @@ -32,6 +32,7 @@ define( /** * Controller for the view switcher; populates and maintains a list * of applicable views for a represented domain object. + * @memberof platform/commonUI/general * @constructor */ function ViewSwitcherController($scope, $timeout) { @@ -71,3 +72,4 @@ define( return ViewSwitcherController; } ); + diff --git a/platform/commonUI/general/src/directives/MCTContainer.js b/platform/commonUI/general/src/directives/MCTContainer.js index 00b7b2b21a..f65cf0803d 100644 --- a/platform/commonUI/general/src/directives/MCTContainer.js +++ b/platform/commonUI/general/src/directives/MCTContainer.js @@ -39,6 +39,7 @@ define( * plain string attribute, instead of as an Angular * expression. * + * @memberof platform/commonUI/general * @constructor */ function MCTContainer(containers) { @@ -96,4 +97,4 @@ define( return MCTContainer; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/directives/MCTDrag.js b/platform/commonUI/general/src/directives/MCTDrag.js index f12aae5c5f..7bccccdf28 100644 --- a/platform/commonUI/general/src/directives/MCTDrag.js +++ b/platform/commonUI/general/src/directives/MCTDrag.js @@ -44,6 +44,7 @@ define( * and vertical pixel offset of the current mouse position * relative to the mouse position where dragging began. * + * @memberof platform/commonUI/general * @constructor * */ @@ -157,3 +158,4 @@ define( return MCTDrag; } ); + diff --git a/platform/commonUI/general/src/directives/MCTResize.js b/platform/commonUI/general/src/directives/MCTResize.js index 62ae977271..c78039627a 100644 --- a/platform/commonUI/general/src/directives/MCTResize.js +++ b/platform/commonUI/general/src/directives/MCTResize.js @@ -49,6 +49,7 @@ define( * This is an Angular expression, and it will be re-evaluated after * each interval. * + * @memberof platform/commonUI/general * @constructor * */ @@ -111,4 +112,4 @@ define( return MCTResize; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/directives/MCTScroll.js b/platform/commonUI/general/src/directives/MCTScroll.js index 3df79f23b2..6b9d480c66 100644 --- a/platform/commonUI/general/src/directives/MCTScroll.js +++ b/platform/commonUI/general/src/directives/MCTScroll.js @@ -37,6 +37,7 @@ define( * This is exposed as two directives in `bundle.json`; the difference * is handled purely by parameterization. * + * @memberof platform/commonUI/general * @constructor * @param $parse Angular's $parse * @param {string} property property to manage within the HTML element @@ -80,4 +81,4 @@ define( return MCTScroll; } -); \ No newline at end of file +); diff --git a/platform/commonUI/general/src/directives/MCTSplitPane.js b/platform/commonUI/general/src/directives/MCTSplitPane.js index 8d95ff2b69..688689d79f 100644 --- a/platform/commonUI/general/src/directives/MCTSplitPane.js +++ b/platform/commonUI/general/src/directives/MCTSplitPane.js @@ -91,6 +91,7 @@ define( * etc. can be set on that element to control the splitter's * allowable positions. * + * @memberof platform/commonUI/general * @constructor */ function MCTSplitPane($parse, $log) { @@ -213,3 +214,4 @@ define( } ); + diff --git a/platform/commonUI/general/src/directives/MCTSplitter.js b/platform/commonUI/general/src/directives/MCTSplitter.js index 0494830057..5216c69358 100644 --- a/platform/commonUI/general/src/directives/MCTSplitter.js +++ b/platform/commonUI/general/src/directives/MCTSplitter.js @@ -39,6 +39,7 @@ define( /** * Implements `mct-splitter` directive. + * @memberof platform/commonUI/general * @constructor */ function MCTSplitter() { @@ -88,3 +89,4 @@ define( } ); + diff --git a/platform/commonUI/general/src/services/UrlService.js b/platform/commonUI/general/src/services/UrlService.js index 4562059d0c..5d57b03ca0 100644 --- a/platform/commonUI/general/src/services/UrlService.js +++ b/platform/commonUI/general/src/services/UrlService.js @@ -32,62 +32,56 @@ define( /** * The url service handles calls for url paths * using domain objects. + * @constructor + * @memberof platform/commonUI/general */ function UrlService($location) { - // Returns the url for the mode wanted - // and the domainObject passed in. A path - // is returned. The view is defaulted to - // the current location's (current object's) - // view set. - function urlForLocation(mode, domainObject) { - var context = domainObject && - domainObject.getCapability('context'), - objectPath = context ? context.getPath() : [], - ids = objectPath.map(function (domainObject) { - return domainObject.getId(); - }), - // Parses the path together. Starts with the - // default index.html file, then the mode passed - // into the service, followed by ids in the url - // joined by '/', and lastly the view path from - // the current location - path = mode + "/" + ids.slice(1).join("/"); - return path; - } - - // Uses the Url for the current location - // from the urlForLocation function and - // includes the view and the index path - function urlForNewTab(mode, domainObject) { - var viewPath = "?view=" + $location.search().view, - newTabPath = - "index.html#" + urlForLocation(mode, domainObject) + viewPath; - return newTabPath; - } - - return { - /** - * Returns the Url path for a specific domain object - * without the index.html path and the view path - * @param {value} value of the browse or edit mode - * for the path - * @param {DomainObject} value of the domain object - * to get the path of - */ - urlForNewTab: urlForNewTab, - /** - * Returns the Url path for a specific domain object - * including the index.html path and the view path - * allowing a new tab to hold the correct characteristics - * @param {value} value of the browse or edit mode - * for the path - * @param {DomainObject} value of the domain object - * to get the path of - */ - urlForLocation: urlForLocation - }; + this.$location = $location; } + /** + * Returns the Url path for a specific domain object + * without the index.html path and the view path + * @param {string} mode value of browse or edit mode + * for the path + * @param {DomainObject} value of the domain object + * to get the path of + * @returns {string} URL for the domain object + */ + UrlService.prototype.urlForLocation = function (mode, domainObject) { + var context = domainObject && + domainObject.getCapability('context'), + objectPath = context ? context.getPath() : [], + ids = objectPath.map(function (domainObject) { + return domainObject.getId(); + }); + + // Parses the path together. Starts with the + // default index.html file, then the mode passed + // into the service, followed by ids in the url + // joined by '/', and lastly the view path from + // the current location + return mode + "/" + ids.slice(1).join("/"); + }; + + /** + * Returns the Url path for a specific domain object + * including the index.html path and the view path + * allowing a new tab to hold the correct characteristics + * @param {string} mode value of browse or edit mode + * for the path + * @param {DomainObject} value of the domain object + * to get the path of + * @returns {string} URL for the domain object + */ + UrlService.prototype.urlForNewTab = function (mode, domainObject) { + var viewPath = "?view=" + this.$location.search().view, + newTabPath = + "index.html#" + this.urlForLocation(mode, domainObject) + + viewPath; + return newTabPath; + }; + return UrlService; } -); \ No newline at end of file +); diff --git a/platform/commonUI/inspect/src/InfoConstants.js b/platform/commonUI/inspect/src/InfoConstants.js index 5e43a1b618..4927de870f 100644 --- a/platform/commonUI/inspect/src/InfoConstants.js +++ b/platform/commonUI/inspect/src/InfoConstants.js @@ -20,6 +20,13 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ /*global define*/ + +/** + * This bundle provides support for object inspection (specifically, metadata + * show in bubbles on hover.) + * @namespace platform/commonUI/inspect + */ + define({ BUBBLE_TEMPLATE: " (winDim[0] - bubbleSpaceLR), - goUp = position[1] > (winDim[1] / 2), - bubble; + /** + * Display an info bubble at the specified location. + * @param {string} templateKey template to place in bubble + * @param {string} title title for the bubble + * @param {*} content content to pass to the template, via + * `ng-model` + * @param {number[]} x,y position of the info bubble, in + * pixel coordinates. + * @returns {Function} a function that may be invoked to + * dismiss the info bubble + */ + InfoService.prototype.display = function (templateKey, title, content, position) { + var $compile = this.$compile, + $document = this.$document, + $window = this.$window, + $rootScope = this.$rootScope, + body = $document.find('body'), + scope = $rootScope.$new(), + winDim = [$window.innerWidth, $window.innerHeight], + bubbleSpaceLR = InfoConstants.BUBBLE_MARGIN_LR + InfoConstants.BUBBLE_MAX_WIDTH, + goLeft = position[0] > (winDim[0] - bubbleSpaceLR), + goUp = position[1] > (winDim[1] / 2), + bubble; - // Pass model & container parameters into the scope - scope.bubbleModel = content; - scope.bubbleTemplate = templateKey; - scope.bubbleLayout = (goUp ? 'arw-btm' : 'arw-top') + ' ' + - (goLeft ? 'arw-right' : 'arw-left'); - scope.bubbleTitle = title; + // Pass model & container parameters into the scope + scope.bubbleModel = content; + scope.bubbleTemplate = templateKey; + scope.bubbleLayout = (goUp ? 'arw-btm' : 'arw-top') + ' ' + + (goLeft ? 'arw-right' : 'arw-left'); + scope.bubbleTitle = title; - // Create the context menu - bubble = $compile(BUBBLE_TEMPLATE)(scope); + // Create the context menu + bubble = $compile(BUBBLE_TEMPLATE)(scope); - // Position the bubble - bubble.css('position', 'absolute'); - if (goLeft) { - bubble.css('right', (winDim[0] - position[0] + OFFSET[0]) + 'px'); - } else { - bubble.css('left', position[0] + OFFSET[0] + 'px'); - } - if (goUp) { - bubble.css('bottom', (winDim[1] - position[1] + OFFSET[1]) + 'px'); - } else { - bubble.css('top', position[1] + OFFSET[1] + 'px'); - } - - // Add the menu to the body - body.append(bubble); - - // Return a function to dismiss the bubble - return function () { bubble.remove(); }; + // Position the bubble + bubble.css('position', 'absolute'); + if (goLeft) { + bubble.css('right', (winDim[0] - position[0] + OFFSET[0]) + 'px'); + } else { + bubble.css('left', position[0] + OFFSET[0] + 'px'); + } + if (goUp) { + bubble.css('bottom', (winDim[1] - position[1] + OFFSET[1]) + 'px'); + } else { + bubble.css('top', position[1] + OFFSET[1] + 'px'); } - return { - /** - * Display an info bubble at the specified location. - * @param {string} templateKey template to place in bubble - * @param {string} title title for the bubble - * @param {*} content content to pass to the template, via - * `ng-model` - * @param {number[]} x,y position of the info bubble, in - * pixel coordinates. - * @returns {Function} a function that may be invoked to - * dismiss the info bubble - */ - display: display - }; - } + // Add the menu to the body + body.append(bubble); + + // Return a function to dismiss the bubble + return function () { bubble.remove(); }; + }; return InfoService; } ); + diff --git a/platform/containment/src/CapabilityTable.js b/platform/containment/src/CapabilityTable.js index db14c0f20f..db89f9e68f 100644 --- a/platform/containment/src/CapabilityTable.js +++ b/platform/containment/src/CapabilityTable.js @@ -32,9 +32,11 @@ define( * which capabilities. This supports composition policy (rules * for which objects can contain which other objects) which * sometimes is determined based on the presence of capabilities. + * @constructor + * @memberof platform/containment */ function CapabilityTable(typeService, capabilityService) { - var table = {}; + var self = this; // Build an initial model for a type function buildModel(type) { @@ -52,25 +54,26 @@ define( function addToTable(type) { var typeKey = type.getKey(); Object.keys(getCapabilities(type)).forEach(function (key) { - table[key] = table[key] || {}; - table[key][typeKey] = true; + self.table[key] = self.table[key] || {}; + self.table[key][typeKey] = true; }); } // Build the table + this.table = {}; (typeService.listTypes() || []).forEach(addToTable); - - return { - /** - * Check if a type is expected to expose a specific - * capability. - */ - hasCapability: function (typeKey, capabilityKey) { - return (table[capabilityKey] || {})[typeKey]; - } - }; } + /** + * Check if a type is expected to expose a specific capability. + * @param {string} typeKey the type identifier + * @param {string} capabilityKey the capability identifier + * @returns {boolean} true if expected to be exposed + */ + CapabilityTable.prototype.hasCapability = function (typeKey, capabilityKey) { + return (this.table[capabilityKey] || {})[typeKey]; + }; + return CapabilityTable; } -); \ No newline at end of file +); diff --git a/platform/containment/src/ComposeActionPolicy.js b/platform/containment/src/ComposeActionPolicy.js index 6d3952b763..3468cd3107 100644 --- a/platform/containment/src/ComposeActionPolicy.js +++ b/platform/containment/src/ComposeActionPolicy.js @@ -34,47 +34,51 @@ define( * since it's delegated to a different policy category. * To avoid a circular dependency, the service is obtained via * Angular's `$injector`. + * @constructor + * @memberof platform/containment + * @implements {Policy.} */ function ComposeActionPolicy($injector) { - var policyService; - - function allowComposition(containerObject, selectedObject) { - // Get the object types involved in the compose action - var containerType = containerObject && - containerObject.getCapability('type'), - selectedType = selectedObject && - selectedObject.getCapability('type'); - - // Get a reference to the policy service if needed... - policyService = policyService || $injector.get('policyService'); - - // ...and delegate to the composition policy - return policyService.allow( - 'composition', - containerType, - selectedType - ); - } - - return { - /** - * Check whether or not a compose action should be allowed - * in this context. - * @returns {boolean} true if it may be allowed - */ - allow: function (candidate, context) { - if (candidate.getMetadata().key === 'compose') { - return allowComposition( - (context || {}).domainObject, - (context || {}).selectedObject - ); - } - return true; - } + this.getPolicyService = function () { + return $injector.get('policyService'); }; } + ComposeActionPolicy.prototype.allowComposition = function (containerObject, selectedObject) { + // Get the object types involved in the compose action + var containerType = containerObject && + containerObject.getCapability('type'), + selectedType = selectedObject && + selectedObject.getCapability('type'); + + // Get a reference to the policy service if needed... + this.policyService = this.policyService || this.getPolicyService(); + + // ...and delegate to the composition policy + return this.policyService.allow( + 'composition', + containerType, + selectedType + ); + }; + + /** + * Check whether or not a compose action should be allowed + * in this context. + * @returns {boolean} true if it may be allowed + * @memberof platform/containment.ComposeActionPolicy# + */ + ComposeActionPolicy.prototype.allow = function (candidate, context) { + if (candidate.getMetadata().key === 'compose') { + return this.allowComposition( + (context || {}).domainObject, + (context || {}).selectedObject + ); + } + return true; + }; + return ComposeActionPolicy; } -); \ No newline at end of file +); diff --git a/platform/containment/src/CompositionModelPolicy.js b/platform/containment/src/CompositionModelPolicy.js index 74f1200530..d5e5cb5f72 100644 --- a/platform/containment/src/CompositionModelPolicy.js +++ b/platform/containment/src/CompositionModelPolicy.js @@ -8,21 +8,19 @@ define( /** * Policy allowing composition only for domain object types which * have a composition property. + * @constructor + * @memberof platform/containment + * @implements {Policy.} */ function CompositionModelPolicy() { - return { - /** - * Is the type identified by the candidate allowed to - * contain the type described by the context? - */ - allow: function (candidate, context) { - return Array.isArray( - (candidate.getInitialModel() || {}).composition - ); - } - }; } + CompositionModelPolicy.prototype.allow = function (candidate, context) { + return Array.isArray( + (candidate.getInitialModel() || {}).composition + ); + }; + return CompositionModelPolicy; } -); \ No newline at end of file +); diff --git a/platform/containment/src/CompositionMutabilityPolicy.js b/platform/containment/src/CompositionMutabilityPolicy.js index 9b3e12eb95..8c5ef6a765 100644 --- a/platform/containment/src/CompositionMutabilityPolicy.js +++ b/platform/containment/src/CompositionMutabilityPolicy.js @@ -28,24 +28,20 @@ define( /** * Disallow composition changes to objects which are not mutable. + * @memberof platform/containment * @constructor + * @implements {Policy.} */ function CompositionMutabilityPolicy() { - return { - /** - * Is the type identified by the candidate allowed to - * contain the type described by the context? - * @param {Type} candidate the type of domain object - */ - allow: function (candidate) { - // Equate creatability with mutability; that is, users - // can only modify objects of types they can create, and - // vice versa. - return candidate.hasFeature('creation'); - } - }; } + CompositionMutabilityPolicy.prototype.allow = function (candidate) { + // Equate creatability with mutability; that is, users + // can only modify objects of types they can create, and + // vice versa. + return candidate.hasFeature('creation'); + }; + return CompositionMutabilityPolicy; } -); \ No newline at end of file +); diff --git a/platform/containment/src/CompositionPolicy.js b/platform/containment/src/CompositionPolicy.js index 992fced49f..1f5239ec59 100644 --- a/platform/containment/src/CompositionPolicy.js +++ b/platform/containment/src/CompositionPolicy.js @@ -21,6 +21,11 @@ *****************************************************************************/ /*global define*/ +/** + * This bundle implements "containment" rules, which determine which objects + * can be contained within which other objects. + * @namespace platform/containment + */ define( ['./ContainmentTable'], function (ContainmentTable) { @@ -28,30 +33,27 @@ define( /** * Defines composition policy as driven by type metadata. + * @constructor + * @memberof platform/containment + * @implements {Policy.} */ function CompositionPolicy($injector) { // We're really just wrapping the containment table and rephrasing // it as a policy decision. var table; - function getTable() { + this.getTable = function () { return (table = table || new ContainmentTable( $injector.get('typeService'), $injector.get('capabilityService') )); - } - - return { - /** - * Is the type identified by the candidate allowed to - * contain the type described by the context? - */ - allow: function (candidate, context) { - return getTable().canContain(candidate, context); - } }; } + CompositionPolicy.prototype.allow = function (candidate, context) { + return this.getTable().canContain(candidate, context); + }; + return CompositionPolicy; } -); \ No newline at end of file +); diff --git a/platform/containment/src/ContainmentTable.js b/platform/containment/src/ContainmentTable.js index a3d7ab6ed4..823c782faf 100644 --- a/platform/containment/src/ContainmentTable.js +++ b/platform/containment/src/ContainmentTable.js @@ -37,15 +37,13 @@ define( * start time (plug-in support means this cannot be determined * prior to that, but we don't want to redo these calculations * every time policy is checked.) + * @constructor + * @memberof platform/containment */ function ContainmentTable(typeService, capabilityService) { - var types = typeService.listTypes(), - capabilityTable = new CapabilityTable(typeService, capabilityService), - table = {}; - - // Check if one type can contain another - function canContain(containerType, containedType) { - } + var self = this, + types = typeService.listTypes(), + capabilityTable = new CapabilityTable(typeService, capabilityService); // Add types which have all these capabilities to the set // of allowed types @@ -82,38 +80,39 @@ define( // Check for defined containment restrictions if (contains === undefined) { // If not, accept anything - table[key] = ANY; + self.table[key] = ANY; } else { // Start with an empty set... - table[key] = {}; + self.table[key] = {}; // ...cast accepted types to array if necessary... contains = Array.isArray(contains) ? contains : [contains]; // ...and add all containment rules to that set contains.forEach(function (c) { - addToSet(table[key], c); + addToSet(self.table[key], c); }); } } // Build the table + this.table = {}; types.forEach(addToTable); - - return { - /** - * Check if domain objects of one type can contain domain - * objects of another type. - * @returns {boolean} true if allowable - */ - canContain: function (containerType, containedType) { - var set = table[containerType.getKey()] || {}; - // Recognize either the symbolic value for "can contain - // anything", or lookup the specific type from the set. - return (set === ANY) || set[containedType.getKey()]; - } - }; - } + /** + * Check if domain objects of one type can contain domain + * objects of another type. + * @param {Type} containerType type of the containing domain object + * @param {Type} containedType type of the domain object + * to be contained + * @returns {boolean} true if allowable + */ + ContainmentTable.prototype.canContain = function (containerType, containedType) { + var set = this.table[containerType.getKey()] || {}; + // Recognize either the symbolic value for "can contain + // anything", or lookup the specific type from the set. + return (set === ANY) || set[containedType.getKey()]; + }; + return ContainmentTable; } -); \ No newline at end of file +); diff --git a/platform/core/src/actions/ActionAggregator.js b/platform/core/src/actions/ActionAggregator.js index 4a9288a612..3056ab04e8 100644 --- a/platform/core/src/actions/ActionAggregator.js +++ b/platform/core/src/actions/ActionAggregator.js @@ -25,51 +25,102 @@ define( function () { "use strict"; + /** + * Actions are reusable processes/behaviors performed by users within + * the system, typically upon domain objects. Actions are commonly + * exposed to users as menu items or buttons. + * + * Actions are usually registered via the `actions` extension + * category, or (in advanced cases) via an `actionService` + * implementation. + * + * @interface Action + */ + + /** + * Perform the behavior associated with this action. The return type + * may vary depending on which action has been performed; in general, + * no return value should be expected. + * + * @method Action#perform + */ + + /** + * Get metadata associated with this action. + * + * @method Action#getMetadata + * @returns {ActionMetadata} + */ + + /** + * Metadata associated with an Action. Actions of specific types may + * extend this with additional properties. + * + * @typedef {Object} ActionMetadata + * @property {string} key machine-readable identifier for this action + * @property {string} name human-readable name for this action + * @property {string} description human-readable description + * @property {string} glyph character to display as icon + * @property {ActionContext} context the context in which the action + * will be performed. + */ + + /** + * Provides actions that can be performed within specific contexts. + * + * @interface ActionService + */ + + /** + * Get actions which can be performed within a certain context. + * + * @method ActionService#getActions + * @param {ActionContext} context the context in which the action will + * be performed + * @return {Action[]} relevant actions + */ + + /** + * A description of the context in which an action may occur. + * + * @typedef ActionContext + * @property {DomainObject} [domainObject] the domain object being + * acted upon. + * @property {DomainObject} [selectedObject] the selection at the + * time of action (e.g. the dragged object in a + * drag-and-drop operation.) + * @property {string} [key] the machine-readable identifier of + * the relevant action + * @property {string} [category] a string identifying the category + * of action being performed + */ + /** * The ActionAggregator makes several actionService * instances act as those they were one. When requesting * actions for a given context, results from all * services will be assembled and concatenated. * + * @memberof platform/core * @constructor - * @param {ActionProvider[]} actionProviders an array + * @implements {ActionService} + * @param {ActionService[]} actionProviders an array * of action services */ function ActionAggregator(actionProviders) { - - function getActions(context) { - // Get all actions from all providers, reduce down - // to one array by concatenation - return actionProviders.map(function (provider) { - return provider.getActions(context); - }).reduce(function (a, b) { - return a.concat(b); - }, []); - } - - return { - /** - * Get a list of actions which are valid in a given - * context. - * - * @param {ActionContext} the context in which - * the action will occur; this is a - * JavaScript object containing key-value - * pairs. Typically, this will contain a - * field "domainObject" which refers to - * the domain object that will be acted - * upon, but may contain arbitrary information - * recognized by specific providers. - * @return {Action[]} an array of actions which - * may be performed in the provided context. - * - * @method - * @memberof ActionAggregator - */ - getActions: getActions - }; + this.actionProviders = actionProviders; } + ActionAggregator.prototype.getActions = function (context) { + // Get all actions from all providers, reduce down + // to one array by concatenation + return this.actionProviders.map(function (provider) { + return provider.getActions(context); + }).reduce(function (a, b) { + return a.concat(b); + }, []); + }; + return ActionAggregator; } -); \ No newline at end of file +); diff --git a/platform/core/src/actions/ActionCapability.js b/platform/core/src/actions/ActionCapability.js index 94b0706a0d..2164969a05 100644 --- a/platform/core/src/actions/ActionCapability.js +++ b/platform/core/src/actions/ActionCapability.js @@ -45,73 +45,74 @@ define( * which the action will be performed (also, the * action which exposes the capability.) * + * @memberof platform/core * @constructor */ function ActionCapability($q, actionService, domainObject) { + this.$q = $q; + this.actionService = actionService; + this.domainObject = domainObject; + } + /** + * Perform an action. This will find and perform the + * first matching action available for the specified + * context or key. + * + * @param {ActionContext|string} context the context in which + * to perform the action; this is passed along to + * the action service to match against available + * actions. The "domainObject" field will automatically + * be populated with the domain object that exposed + * this capability. If given as a string, this will + * be taken as the "key" field to match against + * specific actions. + * @returns {Promise} the result of the action that was + * performed, or undefined if no matching action + * was found. + * @memberof platform/core.ActionCapability# + */ + ActionCapability.prototype.getActions = function (context) { // Get all actions which are valid in this context; // this simply redirects to the action service, // but additionally adds a domainObject field. - function getActions(context) { - var baseContext = typeof context === 'string' ? - { key: context } : - (context || {}), - actionContext = Object.create(baseContext); + var baseContext = typeof context === 'string' ? + { key: context } : (context || {}), + actionContext = Object.create(baseContext); - actionContext.domainObject = domainObject; + actionContext.domainObject = this.domainObject; - return actionService.getActions(actionContext); - } + return this.actionService.getActions(actionContext); + }; + /** + * Get actions which are available for this domain object, + * in this context. + * + * @param {ActionContext|string} context the context in which + * to perform the action; this is passed along to + * the action service to match against available + * actions. The "domainObject" field will automatically + * be populated with the domain object that exposed + * this capability. If given as a string, this will + * be taken as the "key" field to match against + * specific actions. + * @returns {Action[]} an array of matching actions + * @memberof platform/core.ActionCapability# + */ + ActionCapability.prototype.perform = function (context) { // Alias to getActions(context)[0].perform, with a // check for empty arrays. - function performAction(context) { - var actions = getActions(context); + var actions = this.getActions(context); - return $q.when( - (actions && actions.length > 0) ? - actions[0].perform() : - undefined - ); - } + return this.$q.when( + (actions && actions.length > 0) ? + actions[0].perform() : + undefined + ); + }; - return { - /** - * Perform an action. This will find and perform the - * first matching action available for the specified - * context or key. - * - * @param {ActionContext|string} context the context in which - * to perform the action; this is passed along to - * the action service to match against available - * actions. The "domainObject" field will automatically - * be populated with the domain object that exposed - * this capability. If given as a string, this will - * be taken as the "key" field to match against - * specific actions. - * @returns {Promise} the result of the action that was - * performed, or undefined if no matching action - * was found. - */ - perform: performAction, - /** - * Get actions which are available for this domain object, - * in this context. - * - * @param {ActionContext|string} context the context in which - * to perform the action; this is passed along to - * the action service to match against available - * actions. The "domainObject" field will automatically - * be populated with the domain object that exposed - * this capability. If given as a string, this will - * be taken as the "key" field to match against - * specific actions. - * @returns {Action[]} an array of matching actions - */ - getActions: getActions - }; - } return ActionCapability; } -); \ No newline at end of file +); diff --git a/platform/core/src/actions/ActionProvider.js b/platform/core/src/actions/ActionProvider.js index 5937f00fc2..dcb17eb6ce 100644 --- a/platform/core/src/actions/ActionProvider.js +++ b/platform/core/src/actions/ActionProvider.js @@ -35,11 +35,46 @@ define( * of actions exposed via extension (specifically, the "actions" * category of extension.) * + * @memberof platform/core + * @imeplements {ActionService} * @constructor */ function ActionProvider(actions) { - var actionsByKey = {}, - actionsByCategory = {}; + var self = this; + + // Build up look-up tables + this.actions = actions; + this.actionsByKey = {}; + this.actionsByCategory = {}; + actions.forEach(function (Action) { + // Get an action's category or categories + var categories = Action.category || []; + + // Convert to an array if necessary + categories = Array.isArray(categories) ? + categories : [categories]; + + // Store action under all relevant categories + categories.forEach(function (category) { + self.actionsByCategory[category] = + self.actionsByCategory[category] || []; + self.actionsByCategory[category].push(Action); + }); + + // Store action by ekey as well + if (Action.key) { + self.actionsByKey[Action.key] = + self.actionsByKey[Action.key] || []; + self.actionsByKey[Action.key].push(Action); + } + }); + } + + ActionProvider.prototype.getActions = function (actionContext) { + var context = (actionContext || {}), + category = context.category, + key = context.key, + candidates; // Instantiate an action; invokes the constructor and // additionally fills in the action's getMetadata method @@ -70,86 +105,32 @@ define( function createIfApplicable(actions, context) { return (actions || []).filter(function (Action) { return Action.appliesTo ? - Action.appliesTo(context) : true; + Action.appliesTo(context) : true; }).map(function (Action) { return instantiateAction(Action, context); }); } - // Get an array of actions that are valid in the supplied context. - function getActions(actionContext) { - var context = (actionContext || {}), - category = context.category, - key = context.key, - candidates; - - // Match actions to the provided context by comparing "key" - // and/or "category" parameters, if specified. - candidates = actions; - if (key) { - candidates = actionsByKey[key]; - if (category) { - candidates = candidates.filter(function (Action) { - return Action.category === category; - }); - } - } else if (category) { - candidates = actionsByCategory[category]; + // Match actions to the provided context by comparing "key" + // and/or "category" parameters, if specified. + candidates = this.actions; + if (key) { + candidates = this.actionsByKey[key]; + if (category) { + candidates = candidates.filter(function (Action) { + return Action.category === category; + }); } - - // Instantiate those remaining actions, with additional - // filtering per any appliesTo methods defined on those - // actions. - return createIfApplicable(candidates, context); + } else if (category) { + candidates = this.actionsByCategory[category]; } - // Build up look-up tables - actions.forEach(function (Action) { - // Get an action's category or categories - var categories = Action.category || []; - - // Convert to an array if necessary - categories = Array.isArray(categories) ? - categories : [categories]; - - // Store action under all relevant categories - categories.forEach(function (category) { - actionsByCategory[category] = - actionsByCategory[category] || []; - actionsByCategory[category].push(Action); - }); - - // Store action by ekey as well - if (Action.key) { - actionsByKey[Action.key] = - actionsByKey[Action.key] || []; - actionsByKey[Action.key].push(Action); - } - }); - - return { - /** - * Get a list of actions which are valid in a given - * context. - * - * @param {ActionContext} the context in which - * the action will occur; this is a - * JavaScript object containing key-value - * pairs. Typically, this will contain a - * field "domainObject" which refers to - * the domain object that will be acted - * upon, but may contain arbitrary information - * recognized by specific providers. - * @return {Action[]} an array of actions which - * may be performed in the provided context. - * - * @method - * @memberof ActionProvider - */ - getActions: getActions - }; - } + // Instantiate those remaining actions, with additional + // filtering per any appliesTo methods defined on those + // actions. + return createIfApplicable(candidates, context); + }; return ActionProvider; } -); \ No newline at end of file +); diff --git a/platform/core/src/actions/LoggingActionDecorator.js b/platform/core/src/actions/LoggingActionDecorator.js index 3e9652d229..0d6f170261 100644 --- a/platform/core/src/actions/LoggingActionDecorator.js +++ b/platform/core/src/actions/LoggingActionDecorator.js @@ -34,9 +34,21 @@ define( * the actions it exposes always emit a log message when they are * performed. * + * @memberof platform/core * @constructor + * @implements {ActionService} + * @param $log Angular's logging service + * @param {ActionService} actionService the decorated action service */ function LoggingActionDecorator($log, actionService) { + this.$log = $log; + this.actionService = actionService; + } + + LoggingActionDecorator.prototype.getActions = function () { + var actionService = this.actionService, + $log = this.$log; + // Decorate the perform method of the specified action, such that // it emits a log message whenever performed. function addLogging(action) { @@ -58,35 +70,12 @@ define( return logAction; } - return { - /** - * Get a list of actions which are valid in a given - * context. These actions will additionally log - * themselves when performed. - * - * @param {ActionContext} the context in which - * the action will occur; this is a - * JavaScript object containing key-value - * pairs. Typically, this will contain a - * field "domainObject" which refers to - * the domain object that will be acted - * upon, but may contain arbitrary information - * recognized by specific providers. - * @return {Action[]} an array of actions which - * may be performed in the provided context. - * - * @method - * @memberof LoggingActionDecorator - */ - getActions: function () { - return actionService.getActions.apply( - actionService, - arguments - ).map(addLogging); - } - }; - } + return actionService.getActions.apply( + actionService, + arguments + ).map(addLogging); + }; return LoggingActionDecorator; } -); \ No newline at end of file +); diff --git a/platform/core/src/capabilities/CompositionCapability.js b/platform/core/src/capabilities/CompositionCapability.js index 8049d5b3c4..f1b2532040 100644 --- a/platform/core/src/capabilities/CompositionCapability.js +++ b/platform/core/src/capabilities/CompositionCapability.js @@ -37,68 +37,61 @@ define( * require consulting the object service (e.g. to trigger a database * query to retrieve the nested object models.) * + * @memberof platform/core * @constructor + * @implements {Capability} */ function CompositionCapability($injector, domainObject) { - var objectService, - lastPromise, - lastModified; - // Get a reference to the object service from $injector - function injectObjectService() { - objectService = $injector.get("objectService"); - return objectService; - } - - // Get a reference to the object service (either cached or - // from the injector) - function getObjectService() { - return objectService || injectObjectService(); - } - - // Promise this domain object's composition (an array of domain - // object instances corresponding to ids in its model.) - function promiseComposition() { - var model = domainObject.getModel(), - ids; - - // Then filter out non-existent objects, - // and wrap others (such that they expose a - // "context" capability) - function contextualize(objects) { - return ids.filter(function (id) { - return objects[id]; - }).map(function (id) { - return new ContextualDomainObject( - objects[id], - domainObject - ); - }); - } - - // Make a new request if we haven't made one, or if the - // object has been modified. - if (!lastPromise || lastModified !== model.modified) { - ids = model.composition || []; - lastModified = model.modified; - // Load from the underlying object service - lastPromise = getObjectService().getObjects(ids) - .then(contextualize); - } - - return lastPromise; - } - - return { - /** - * Request the composition of this object. - * @returns {Promise.} a list of all domain - * objects which compose this domain object. - */ - invoke: promiseComposition + this.injectObjectService = function () { + this.objectService = $injector.get("objectService"); }; + + this.domainObject = domainObject; } + /** + * Request the composition of this object. + * @returns {Promise.} a list of all domain + * objects which compose this domain object. + */ + CompositionCapability.prototype.invoke = function () { + var domainObject = this.domainObject, + model = domainObject.getModel(), + ids; + + // Then filter out non-existent objects, + // and wrap others (such that they expose a + // "context" capability) + function contextualize(objects) { + return ids.filter(function (id) { + return objects[id]; + }).map(function (id) { + return new ContextualDomainObject( + objects[id], + domainObject + ); + }); + } + + // Lazily acquire object service (avoids cyclical dependency) + if (!this.objectService) { + this.injectObjectService(); + } + + // Make a new request if we haven't made one, or if the + // object has been modified. + if (!this.lastPromise || this.lastModified !== model.modified) { + ids = model.composition || []; + this.lastModified = model.modified; + // Load from the underlying object service + this.lastPromise = this.objectService.getObjects(ids) + .then(contextualize); + } + + return this.lastPromise; + }; + /** * Test to determine whether or not this capability should be exposed * by a domain object based on its model. Checks for the presence of @@ -112,4 +105,4 @@ define( return CompositionCapability; } -); \ No newline at end of file +); diff --git a/platform/core/src/capabilities/ContextCapability.js b/platform/core/src/capabilities/ContextCapability.js index 9eeb00823b..9ffaf4a5bb 100644 --- a/platform/core/src/capabilities/ContextCapability.js +++ b/platform/core/src/capabilities/ContextCapability.js @@ -36,77 +36,78 @@ define( * those whose `composition` capability was used to access this * object.) * + * @memberof platform/core * @constructor + * @implements {Capability} */ function ContextCapability(parentObject, domainObject) { - return { - /** - * Get the immediate parent of a domain object. - * - * A domain object may be contained in multiple places; its - * parent (as exposed by this capability) is the domain - * object from which this object was accessed, usually - * by way of a `composition` capability. - * - * @returns {DomainObject} the immediate parent of this - * domain object. - */ - getParent: function () { - return parentObject; - }, - /** - * Get an array containing the complete direct ancestry - * of this domain object, including the domain object - * itself. - * - * A domain object may be contained in multiple places; its - * parent and all ancestors (as exposed by this capability) - * serve as a record of how this specific domain object - * instance was reached. - * - * The first element in the returned array is the deepest - * ancestor; subsequent elements are progressively more - * recent ancestors, with the domain object which exposed - * the capability occupying the last element of the array. - * - * @returns {DomainObject[]} the full composition ancestry - * of the domain object which exposed this - * capability. - */ - getPath: function () { - var parentPath = [], - parentContext; - - if (parentObject) { - parentContext = parentObject.getCapability("context"); - parentPath = parentContext ? - parentContext.getPath() : - [parentObject]; - } - - return parentPath.concat([domainObject]); - }, - /** - * Get the deepest ancestor available for this domain object; - * equivalent to `getPath()[0]`. - * - * See notes on `getPath()` for how ancestry is defined in - * the context of this capability. - * - * @returns {DomainObject} the deepest ancestor of the domain - * object which exposed this capability. - */ - getRoot: function () { - var parentContext = parentObject && - parentObject.getCapability('context'); - - return parentContext ? - parentContext.getRoot() : - (parentObject || domainObject); - } - }; + this.parentObject = parentObject; + this.domainObject = domainObject; } + /** + * Get the immediate parent of a domain object. + * + * A domain object may be contained in multiple places; its + * parent (as exposed by this capability) is the domain + * object from which this object was accessed, usually + * by way of a `composition` capability. + * + * @returns {DomainObject} the immediate parent of this + * domain object. + */ + ContextCapability.prototype.getParent = function () { + return this.parentObject; + }; + + /** + * Get an array containing the complete direct ancestry + * of this domain object, including the domain object + * itself. + * + * A domain object may be contained in multiple places; its + * parent and all ancestors (as exposed by this capability) + * serve as a record of how this specific domain object + * instance was reached. + * + * The first element in the returned array is the deepest + * ancestor; subsequent elements are progressively more + * recent ancestors, with the domain object which exposed + * the capability occupying the last element of the array. + * + * @returns {DomainObject[]} the full composition ancestry + * of the domain object which exposed this + * capability. + */ + ContextCapability.prototype.getPath = function () { + var parentObject = this.parentObject, + parentContext = + parentObject && parentObject.getCapability('context'), + parentPath = parentContext ? + parentContext.getPath() : [ this.parentObject ]; + + return parentPath.concat([this.domainObject]); + }; + + /** + * Get the deepest ancestor available for this domain object; + * equivalent to `getPath()[0]`. + * + * See notes on `getPath()` for how ancestry is defined in + * the context of this capability. + * + * @returns {DomainObject} the deepest ancestor of the domain + * object which exposed this capability. + */ + ContextCapability.prototype.getRoot = function () { + var parentContext = this.parentObject && + this.parentObject.getCapability('context'); + + return parentContext ? + parentContext.getRoot() : + (this.parentObject || this.domainObject); + }; + return ContextCapability; } -); \ No newline at end of file +); diff --git a/platform/core/src/capabilities/ContextualDomainObject.js b/platform/core/src/capabilities/ContextualDomainObject.js index 0d042923a0..2955515ead 100644 --- a/platform/core/src/capabilities/ContextualDomainObject.js +++ b/platform/core/src/capabilities/ContextualDomainObject.js @@ -42,7 +42,9 @@ define( * @param {DomainObject} parentObject the domain object from which * the wrapped object was retrieved * + * @memberof platform/core * @constructor + * @implements {DomainObject} */ function ContextualDomainObject(domainObject, parentObject) { // Prototypally inherit from the domain object, and @@ -63,4 +65,4 @@ define( return ContextualDomainObject; } -); \ No newline at end of file +); diff --git a/platform/core/src/capabilities/CoreCapabilityProvider.js b/platform/core/src/capabilities/CoreCapabilityProvider.js index 89660b72ee..7b1ba070d7 100644 --- a/platform/core/src/capabilities/CoreCapabilityProvider.js +++ b/platform/core/src/capabilities/CoreCapabilityProvider.js @@ -29,6 +29,19 @@ define( function () { "use strict"; + /** + * A capability provides an interface with dealing with some + * dynamic behavior associated with a domain object. + * @interface Capability + */ + + /** + * Optional; if present, will be used by `DomainObject#useCapability` + * to simplify interaction with a specific capability. Parameters + * and return values vary depending on capability type. + * @method Capability#invoke + */ + /** * Provides capabilities based on extension definitions, * matched to domain object models. @@ -37,6 +50,7 @@ define( * of constructor functions for capabilities, as * exposed by extensions defined at the bundle level. * + * @memberof platform/core * @constructor */ function CoreCapabilityProvider(capabilities, $log) { @@ -84,6 +98,7 @@ define( * @returns {Object.} all * capabilities known to be valid for this model, as * key-value pairs + * @memberof platform/core.CoreCapabilityProvider# */ getCapabilities: getCapabilities }; @@ -92,3 +107,4 @@ define( return CoreCapabilityProvider; } ); + diff --git a/platform/core/src/capabilities/DelegationCapability.js b/platform/core/src/capabilities/DelegationCapability.js index 452b842452..0c62c05f00 100644 --- a/platform/core/src/capabilities/DelegationCapability.js +++ b/platform/core/src/capabilities/DelegationCapability.js @@ -45,12 +45,40 @@ define( * in the type's definition, which contains an array of names of * capabilities to be delegated. * - * @param domainObject + * @param $q Angular's $q, for promises + * @param {DomainObject} domainObject the delegating domain object + * @memberof platform/core * @constructor + * @implements {Capability} */ function DelegationCapability($q, domainObject) { - var delegateCapabilities = {}, - type = domainObject.getCapability("type"); + var type = domainObject.getCapability("type"), + self = this; + + this.$q = $q; + this.delegateCapabilities = {}; + this.domainObject = domainObject; + + // Generate set for easy lookup of capability delegation + if (type && type.getDefinition) { + (type.getDefinition().delegates || []).forEach(function (key) { + self.delegateCapabilities[key] = true; + }); + } + } + + + /** + * Get the domain objects which are intended to be delegated + * responsibility for some specific capability. + * + * @param {string} key the name of the delegated capability + * @returns {DomainObject[]} the domain objects to which + * responsibility for this capability is delegated. + * @memberof platform/core.DelegationCapability# + */ + DelegationCapability.prototype.getDelegates = function (key) { + var domainObject = this.domainObject; function filterObjectsWithCapability(capability) { return function (objects) { @@ -64,55 +92,42 @@ define( return domainObject.useCapability('composition'); } - function doesDelegate(key) { - return delegateCapabilities[key] || false; - } + return this.doesDelegateCapability(key) ? + promiseChildren().then( + filterObjectsWithCapability(key) + ) : + this.$q.when([]); + }; - function getDelegates(capability) { - return doesDelegate(capability) ? - promiseChildren().then( - filterObjectsWithCapability(capability) - ) : - $q.when([]); - } - - // Generate set for easy lookup of capability delegation - if (type && type.getDefinition) { - (type.getDefinition().delegates || []).forEach(function (key) { - delegateCapabilities[key] = true; - }); - } - - return { - /** - * Invoke this capability; alias of `getDelegates`, used to - * simplify usage, e.g.: - * - * `domainObject.useCapability("delegation", "telemetry")` - * - * ...will retrieve all members of a domain object's - * composition which have a "telemetry" capability. - * - * @param {string} the name of the delegated capability - * @returns {DomainObject[]} the domain objects to which - * responsibility for this capability is delegated. - */ - invoke: getDelegates, - /** - * Get the domain objects which are intended to be delegated - * responsibility for some specific capability. - * - * @param {string} the name of the delegated capability - * @returns {DomainObject[]} the domain objects to which - * responsibility for this capability is delegated. - */ - getDelegates: getDelegates, - doesDelegateCapability: doesDelegate - }; - } + /** + * Check if the domain object which exposed this capability + * wishes to delegate another capability. + * + * @param {string} key the capability to check for + * @returns {boolean} true if the capability is delegated + */ + DelegationCapability.prototype.doesDelegateCapability = function (key) { + return !!(this.delegateCapabilities[key]); + }; + /** + * Invoke this capability; alias of `getDelegates`, used to + * simplify usage, e.g.: + * + * `domainObject.useCapability("delegation", "telemetry")` + * + * ...will retrieve all members of a domain object's + * composition which have a "telemetry" capability. + * + * @param {string} the name of the delegated capability + * @returns {DomainObject[]} the domain objects to which + * responsibility for this capability is delegated. + * @memberof platform/core.DelegationCapability# + */ + DelegationCapability.prototype.invoke = + DelegationCapability.prototype.getDelegates; return DelegationCapability; } -); \ No newline at end of file +); diff --git a/platform/core/src/capabilities/MetadataCapability.js b/platform/core/src/capabilities/MetadataCapability.js index 677dab0008..242b35b6dc 100644 --- a/platform/core/src/capabilities/MetadataCapability.js +++ b/platform/core/src/capabilities/MetadataCapability.js @@ -25,10 +25,23 @@ define( * `value` properties describing that domain object (suitable for * display.) * + * @param {DomainObject} domainObject the domain object whose + * metadata is to be exposed + * @implements {Capability} * @constructor + * @memberof platform/core */ function MetadataCapability(domainObject) { - var model = domainObject.getModel(); + this.domainObject = domainObject; + } + + /** + * Get metadata about this object. + * @returns {MetadataProperty[]} metadata about this object + */ + MetadataCapability.prototype.invoke = function () { + var domainObject = this.domainObject, + model = domainObject.getModel(); function hasDisplayableValue(metadataProperty) { var t = typeof metadataProperty.value; @@ -37,8 +50,8 @@ define( function formatTimestamp(timestamp) { return typeof timestamp === 'number' ? - (moment.utc(timestamp).format(TIME_FORMAT) + " UTC") : - undefined; + (moment.utc(timestamp).format(TIME_FORMAT) + " UTC") : + undefined; } function getProperties() { @@ -73,20 +86,11 @@ define( ]; } - function getMetadata() { - return getProperties().concat(getCommonMetadata()) - .filter(hasDisplayableValue); - } - - return { - /** - * Get metadata about this object. - * @returns {MetadataProperty[]} metadata about this object - */ - invoke: getMetadata - }; - } + return getProperties().concat(getCommonMetadata()) + .filter(hasDisplayableValue); + }; return MetadataCapability; } ); + diff --git a/platform/core/src/capabilities/MutationCapability.js b/platform/core/src/capabilities/MutationCapability.js index 4268f7c323..b9f49ca969 100644 --- a/platform/core/src/capabilities/MutationCapability.js +++ b/platform/core/src/capabilities/MutationCapability.js @@ -69,97 +69,105 @@ define( * }); * ``` * + * @param {Function} topic a service for creating listeners + * @param {Function} now a service to get the current time * @param {DomainObject} domainObject the domain object * which will expose this capability + * @memberof platform/core * @constructor + * @implements {Capability} */ function MutationCapability(topic, now, domainObject) { - var t = topic(TOPIC_PREFIX + domainObject.getId()); + this.mutationTopic = topic(TOPIC_PREFIX + domainObject.getId()); + this.now = now; + this.domainObject = domainObject; + } - function mutate(mutator, timestamp) { - // Get the object's model and clone it, so the - // mutator function has a temporary copy to work with. - var model = domainObject.getModel(), - clone = JSON.parse(JSON.stringify(model)), - useTimestamp = arguments.length > 1; + /** + * Modify the domain object's model, using a provided + * function. This function will receive a copy of the + * domain object's model as an argument; behavior + * varies depending on that function's return value: + * + * * If no value (or undefined) is returned by the mutator, + * the state of the model object delivered as the mutator's + * argument will become the domain object's new model. + * This is useful for writing code that modifies the model + * directly. + * * If a plain object is returned, that object will be used + * as the domain object's new model. + * * If boolean `false` is returned, the mutation will be + * cancelled. + * * If a promise is returned, its resolved value will be + * handled as one of the above. + * + * + * @param {Function} mutator the function which will make + * changes to the domain object's model. + * @param {number} [timestamp] timestamp to record for + * this mutation (otherwise, system time will be + * used) + * @returns {Promise.} a promise for the result + * of the mutation; true if changes were made. + */ + MutationCapability.prototype.mutate = function (mutator, timestamp) { + // Get the object's model and clone it, so the + // mutator function has a temporary copy to work with. + var domainObject = this.domainObject, + now = this.now, + t = this.mutationTopic, + model = domainObject.getModel(), + clone = JSON.parse(JSON.stringify(model)), + useTimestamp = arguments.length > 1; - // Function to handle copying values to the actual - function handleMutation(mutationResult) { - // If mutation result was undefined, just use - // the clone; this allows the mutator to omit return - // values and just change the model directly. - var result = mutationResult || clone; + // Function to handle copying values to the actual + function handleMutation(mutationResult) { + // If mutation result was undefined, just use + // the clone; this allows the mutator to omit return + // values and just change the model directly. + var result = mutationResult || clone; - // Allow mutators to change their mind by - // returning false. - if (mutationResult !== false) { - // Copy values if result was a different object - // (either our clone or some other new thing) - if (model !== result) { - copyValues(model, result); - } - model.modified = useTimestamp ? timestamp : now(); - t.notify(model); + // Allow mutators to change their mind by + // returning false. + if (mutationResult !== false) { + // Copy values if result was a different object + // (either our clone or some other new thing) + if (model !== result) { + copyValues(model, result); } - - // Report the result of the mutation - return mutationResult !== false; + model.modified = useTimestamp ? timestamp : now(); + t.notify(model); } - // Invoke the provided mutator, then make changes to - // the underlying model (if applicable.) - return fastPromise(mutator(clone)).then(handleMutation); + // Report the result of the mutation + return mutationResult !== false; } - function listen(listener) { - return t.listen(listener); - } + // Invoke the provided mutator, then make changes to + // the underlying model (if applicable.) + return fastPromise(mutator(clone)).then(handleMutation); + }; - return { - /** - * Alias of `mutate`, used to support useCapability. - */ - invoke: mutate, - /** - * Modify the domain object's model, using a provided - * function. This function will receive a copy of the - * domain object's model as an argument; behavior - * varies depending on that function's return value: - * - * * If no value (or undefined) is returned by the mutator, - * the state of the model object delivered as the mutator's - * argument will become the domain object's new model. - * This is useful for writing code that modifies the model - * directly. - * * If a plain object is returned, that object will be used - * as the domain object's new model. - * * If boolean `false` is returned, the mutation will be - * cancelled. - * * If a promise is returned, its resolved value will be - * handled as one of the above. - * - * - * @param {function} mutator the function which will make - * changes to the domain object's model. - * @param {number} [timestamp] timestamp to record for - * this mutation (otherwise, system time will be - * used) - * @returns {Promise.} a promise for the result - * of the mutation; true if changes were made. - */ - mutate: mutate, - /** - * Listen for mutations of this domain object's model. - * The provided listener will be invoked with the domain - * object's new model after any changes. To stop listening, - * invoke the function returned by this method. - * @param {Function} listener function to call on mutation - * @returns {Function} a function to stop listening - */ - listen: listen - }; - } + /** + * Listen for mutations of this domain object's model. + * The provided listener will be invoked with the domain + * object's new model after any changes. To stop listening, + * invoke the function returned by this method. + * @param {Function} listener function to call on mutation + * @returns {Function} a function to stop listening + * @memberof platform/core.MutationCapability# + */ + MutationCapability.prototype.listen = function (listener) { + return this.mutationTopic.listen(listener); + }; + + /** + * Alias of `mutate`, used to support useCapability. + */ + MutationCapability.prototype.invoke = + MutationCapability.prototype.mutate; return MutationCapability; } ); + diff --git a/platform/core/src/capabilities/PersistenceCapability.js b/platform/core/src/capabilities/PersistenceCapability.js index 68c3255412..8c7e08e17d 100644 --- a/platform/core/src/capabilities/PersistenceCapability.js +++ b/platform/core/src/capabilities/PersistenceCapability.js @@ -33,18 +33,69 @@ define( * * @param {PersistenceService} persistenceService the underlying * provider of persistence capabilities. - * @param {string} SPACE the name of the persistence space to + * @param {string} space the name of the persistence space to * use (this is an arbitrary string, useful in principle * for distinguishing different persistence stores from * one another.) * @param {DomainObject} the domain object which shall expose * this capability * + * @memberof platform/core * @constructor + * @implements {Capability} */ - function PersistenceCapability(persistenceService, SPACE, domainObject) { + function PersistenceCapability(persistenceService, space, domainObject) { // Cache modified timestamp - var modified = domainObject.getModel().modified; + this.modified = domainObject.getModel().modified; + + this.domainObject = domainObject; + this.space = space; + this.persistenceService = persistenceService; + } + + // Utility function for creating promise-like objects which + // resolve synchronously when possible + function fastPromise(value) { + return (value || {}).then ? value : { + then: function (callback) { + return fastPromise(callback(value)); + } + }; + } + + /** + * Persist any changes which have been made to this + * domain object's model. + * @returns {Promise} a promise which will be resolved + * if persistence is successful, and rejected + * if not. + */ + PersistenceCapability.prototype.persist = function () { + var domainObject = this.domainObject, + modified = domainObject.getModel().modified; + + // Update persistence timestamp... + domainObject.useCapability("mutation", function (model) { + model.persisted = modified; + }, modified); + + // ...and persist + return this.persistenceService.updateObject( + this.getSpace(), + domainObject.getId(), + domainObject.getModel() + ); + }; + + /** + * Update this domain object to match the latest from + * persistence. + * @returns {Promise} a promise which will be resolved + * when the update is complete + */ + PersistenceCapability.prototype.refresh = function () { + var domainObject = this.domainObject, + model = domainObject.getModel(); // Update a domain object's model upon refresh function updateModel(model) { @@ -54,73 +105,29 @@ define( }, modified); } - // For refresh; update a domain object model, only if there - // are no unsaved changes. - function updatePersistenceTimestamp() { - var modified = domainObject.getModel().modified; - domainObject.useCapability("mutation", function (model) { - model.persisted = modified; - }, modified); - } + // Only update if we don't have unsaved changes + return (model.modified === model.persisted) ? + this.persistenceService.readObject( + this.getSpace(), + this.domainObject.getId() + ).then(updateModel) : + fastPromise(false); + }; - // Utility function for creating promise-like objects which - // resolve synchronously when possible - function fastPromise(value) { - return (value || {}).then ? value : { - then: function (callback) { - return fastPromise(callback(value)); - } - }; - } - - return { - /** - * Persist any changes which have been made to this - * domain object's model. - * @returns {Promise} a promise which will be resolved - * if persistence is successful, and rejected - * if not. - */ - persist: function () { - updatePersistenceTimestamp(); - return persistenceService.updateObject( - SPACE, - domainObject.getId(), - domainObject.getModel() - ); - }, - /** - * Update this domain object to match the latest from - * persistence. - * @returns {Promise} a promise which will be resolved - * when the update is complete - */ - refresh: function () { - var model = domainObject.getModel(); - // Only update if we don't have unsaved changes - return (model.modified === model.persisted) ? - persistenceService.readObject( - SPACE, - domainObject.getId() - ).then(updateModel) : - fastPromise(false); - }, - /** - * Get the space in which this domain object is persisted; - * this is useful when, for example, decided which space a - * newly-created domain object should be persisted to (by - * default, this should be the space of its containing - * object.) - * - * @returns {string} the name of the space which should - * be used to persist this object - */ - getSpace: function () { - return SPACE; - } - }; - } + /** + * Get the space in which this domain object is persisted; + * this is useful when, for example, decided which space a + * newly-created domain object should be persisted to (by + * default, this should be the space of its containing + * object.) + * + * @returns {string} the name of the space which should + * be used to persist this object + */ + PersistenceCapability.prototype.getSpace = function () { + return this.space; + }; return PersistenceCapability; } -); \ No newline at end of file +); diff --git a/platform/core/src/capabilities/RelationshipCapability.js b/platform/core/src/capabilities/RelationshipCapability.js index fc8729a9a1..7eb6d01bb9 100644 --- a/platform/core/src/capabilities/RelationshipCapability.js +++ b/platform/core/src/capabilities/RelationshipCapability.js @@ -38,91 +38,82 @@ define( * which are not intended to appear in the tree, but are instead * intended only for special, limited usage. * + * @memberof platform/core * @constructor + * @implements {Capability} */ function RelationshipCapability($injector, domainObject) { - var objectService, - lastPromise = {}, - lastModified; - // Get a reference to the object service from $injector - function injectObjectService() { - objectService = $injector.get("objectService"); - return objectService; - } - - // Get a reference to the object service (either cached or - // from the injector) - function getObjectService() { - return objectService || injectObjectService(); - } - - // Promise this domain object's composition (an array of domain - // object instances corresponding to ids in its model.) - function promiseRelationships(key) { - var model = domainObject.getModel(), - ids; - - // Package objects as an array - function packageObject(objects) { - return ids.map(function (id) { - return objects[id]; - }).filter(function (obj) { - return obj; - }); - } - - // Clear cached promises if modification has occurred - if (lastModified !== model.modified) { - lastPromise = {}; - lastModified = model.modified; - } - - // Make a new request if needed - if (!lastPromise[key]) { - ids = (model.relationships || {})[key] || []; - lastModified = model.modified; - // Load from the underlying object service - lastPromise[key] = getObjectService().getObjects(ids) - .then(packageObject); - } - - return lastPromise[key]; - } - - // List types of relationships which this object has - function listRelationships() { - var relationships = - (domainObject.getModel() || {}).relationships || {}; - - // Check if this key really does expose an array of ids - // (to filter out malformed relationships) - function isArray(key) { - return Array.isArray(relationships[key]); - } - - return Object.keys(relationships).filter(isArray).sort(); - } - - return { - /** - * List all types of relationships exposed by this - * object. - * @returns {string[]} a list of all relationship types - */ - listRelationships: listRelationships, - /** - * Request related objects, with a given relationship type. - * This will typically require asynchronous lookup, so this - * returns a promise. - * @param {string} key the type of relationship - * @returns {Promise.} a promise for related - * domain objects - */ - getRelatedObjects: promiseRelationships + this.injectObjectService = function () { + this.objectService = $injector.get("objectService"); }; + + this.lastPromise = {}; + this.domainObject = domainObject; } + /** + * List all types of relationships exposed by this + * object. + * @returns {string[]} a list of all relationship types + */ + RelationshipCapability.prototype.listRelationships = function listRelationships() { + var relationships = + (this.domainObject.getModel() || {}).relationships || {}; + + // Check if this key really does expose an array of ids + // (to filter out malformed relationships) + function isArray(key) { + return Array.isArray(relationships[key]); + } + + return Object.keys(relationships).filter(isArray).sort(); + }; + + /** + * Request related objects, with a given relationship type. + * This will typically require asynchronous lookup, so this + * returns a promise. + * @param {string} key the type of relationship + * @returns {Promise.} a promise for related + * domain objects + */ + RelationshipCapability.prototype.getRelatedObjects = function (key) { + var model = this.domainObject.getModel(), + ids; + + // Package objects as an array + function packageObject(objects) { + return ids.map(function (id) { + return objects[id]; + }).filter(function (obj) { + return obj; + }); + } + + // Clear cached promises if modification has occurred + if (this.lastModified !== model.modified) { + this.lastPromise = {}; + this.lastModified = model.modified; + } + + // Make a new request if needed + if (!this.lastPromise[key]) { + ids = (model.relationships || {})[key] || []; + this.lastModified = model.modified; + // Lazily initialize object service now that we need it + if (!this.objectService) { + this.injectObjectService(); + } + // Load from the underlying object service + this.lastPromise[key] = this.objectService.getObjects(ids) + .then(packageObject); + } + + return this.lastPromise[key]; + }; + + /** * Test to determine whether or not this capability should be exposed * by a domain object based on its model. Checks for the presence of @@ -136,4 +127,4 @@ define( return RelationshipCapability; } -); \ No newline at end of file +); diff --git a/platform/core/src/models/CachingModelDecorator.js b/platform/core/src/models/CachingModelDecorator.js index d9d4ef6775..a338d6770f 100644 --- a/platform/core/src/models/CachingModelDecorator.js +++ b/platform/core/src/models/CachingModelDecorator.js @@ -30,11 +30,32 @@ define( * The caching model decorator maintains a cache of loaded domain * object models, and ensures that duplicate models for the same * object are not provided. + * @memberof platform/core * @constructor + * @param {ModelService} modelService this service to decorate + * @implements {ModelService} */ function CachingModelDecorator(modelService) { - var cache = {}, - cached = {}; + this.cache = {}; + this.cached = {}; + this.modelService = modelService; + } + + // Fast-resolving promise + function fastPromise(value) { + return (value || {}).then ? value : { + then: function (callback) { + return fastPromise(callback(value)); + } + }; + } + + CachingModelDecorator.prototype.getModels = function (ids) { + var cache = this.cache, + cached = this.cached, + neededIds = ids.filter(function notCached(id) { + return !cached[id]; + }); // Update the cached instance of a model to a new value. // We update in-place to ensure there is only ever one instance @@ -67,30 +88,12 @@ define( return oldModel; } - // Fast-resolving promise - function fastPromise(value) { - return (value || {}).then ? value : { - then: function (callback) { - return fastPromise(callback(value)); - } - }; - } - - // Store this model in the cache - function cacheModel(id, model) { - cache[id] = cached[id] ? updateModel(id, model) : model; - cached[id] = true; - } - - // Check if an id is not in cache, for lookup filtering - function notCached(id) { - return !cached[id]; - } - // Store the provided models in our cache function cacheAll(models) { Object.keys(models).forEach(function (id) { - cacheModel(id, models[id]); + cache[id] = cached[id] ? + updateModel(id, models[id]) : models[id]; + cached[id] = true; }); } @@ -99,38 +102,17 @@ define( return cache; } - return { - /** - * Get models for these specified string identifiers. - * These will be given as an object containing keys - * and values, where keys are object identifiers and - * values are models. - * This result may contain either a subset or a - * superset of the total objects. - * - * @param {Array} ids the string identifiers for - * models of interest. - * @returns {Promise} a promise for an object - * containing key-value pairs, where keys are - * ids and values are models - * @method - */ - getModels: function (ids) { - var neededIds = ids.filter(notCached); + // Look up if we have unknown IDs + if (neededIds.length > 0) { + return this.modelService.getModels(neededIds) + .then(cacheAll) + .then(giveCache); + } - // Look up if we have unknown IDs - if (neededIds.length > 0) { - return modelService.getModels(neededIds) - .then(cacheAll) - .then(giveCache); - } - - // Otherwise, just expose the cache directly - return fastPromise(cache); - } - }; - } + // Otherwise, just expose the cache directly + return fastPromise(cache); + }; return CachingModelDecorator; } -); \ No newline at end of file +); diff --git a/platform/core/src/models/MissingModelDecorator.js b/platform/core/src/models/MissingModelDecorator.js index 3c82bd2726..d3eb8f3159 100644 --- a/platform/core/src/models/MissingModelDecorator.js +++ b/platform/core/src/models/MissingModelDecorator.js @@ -29,31 +29,35 @@ define( /** * Adds placeholder domain object models for any models which * fail to load from the underlying model service. + * @constructor + * @memberof platform/core + * @param {ModelService} modelService this service to decorate * @implements {ModelService} */ function MissingModelDecorator(modelService) { - function missingModel(id) { - return { - type: "unknown", - name: "Missing: " + id - }; - } + this.modelService = modelService; + } + function missingModel(id) { return { - getModels: function (ids) { - function addMissingModels(models) { - var result = {}; - ids.forEach(function (id) { - result[id] = models[id] || missingModel(id); - }); - return result; - } - - return modelService.getModels(ids).then(addMissingModels); - } + type: "unknown", + name: "Missing: " + id }; } + MissingModelDecorator.prototype.getModels = function (ids) { + function addMissingModels(models) { + var result = {}; + ids.forEach(function (id) { + result[id] = models[id] || missingModel(id); + }); + return result; + } + + return this.modelService.getModels(ids).then(addMissingModels); + }; + return MissingModelDecorator; } ); + diff --git a/platform/core/src/models/ModelAggregator.js b/platform/core/src/models/ModelAggregator.js index 30b645ff86..d5f4060415 100644 --- a/platform/core/src/models/ModelAggregator.js +++ b/platform/core/src/models/ModelAggregator.js @@ -29,65 +29,71 @@ define( function () { "use strict"; + /** + * Allow domain object models to be looked up by their identifiers. + * + * @interface ModelService + */ + + /** + * Get domain object models. + * + * This may provide either a superset or a subset of the models + * requested. Absence of a model means it does not exist within + * this service instance. + * + * @method ModelService#getModels + * @param {string[]} ids identifiers for models desired. + * @returns {Promise.} a promise for an object mapping + * string identifiers to domain object models. + */ + /** * Allows multiple services which provide models for domain objects * to be treated as one. * + * @memberof platform/core * @constructor - * @param {ModelProvider[]} providers the model providers to be + * @implements {ModelService} + * @param $q Angular's $q, for promises + * @param {ModelService[]} providers the model providers to be * aggregated */ function ModelAggregator($q, providers) { - - // Pick a domain object model to use, favoring the one - // with the most recent timestamp - function pick(a, b) { - var aModified = (a || {}).modified || Number.NEGATIVE_INFINITY, - bModified = (b || {}).modified || Number.NEGATIVE_INFINITY; - return (aModified > bModified) ? a : (b || a); - } - - // Merge results from multiple providers into one - // large result object. - function mergeModels(provided, ids) { - var result = {}; - ids.forEach(function (id) { - provided.forEach(function (models) { - if (models[id]) { - result[id] = pick(result[id], models[id]); - } - }); - }); - return result; - } - - return { - /** - * Get models with the specified identifiers. - * - * This will invoke the `getModels()` method of all providers - * given at constructor-time, and aggregate the result into - * one object. - * - * Note that the returned object may contain a subset or a - * superset of the models requested. - * - * @param {string[]} ids an array of domain object identifiers - * @returns {Promise.} a promise for an object - * containing key-value pairs, - * where keys are object identifiers and values - * are object models. - */ - getModels: function (ids) { - return $q.all(providers.map(function (provider) { - return provider.getModels(ids); - })).then(function (provided) { - return mergeModels(provided, ids); - }); - } - }; + this.providers = providers; + this.$q = $q; } + // Pick a domain object model to use, favoring the one + // with the most recent timestamp + function pick(a, b) { + var aModified = (a || {}).modified || Number.NEGATIVE_INFINITY, + bModified = (b || {}).modified || Number.NEGATIVE_INFINITY; + return (aModified > bModified) ? a : (b || a); + } + + // Merge results from multiple providers into one + // large result object. + function mergeModels(provided, ids) { + var result = {}; + ids.forEach(function (id) { + provided.forEach(function (models) { + if (models[id]) { + result[id] = pick(result[id], models[id]); + } + }); + }); + return result; + } + + ModelAggregator.prototype.getModels = function (ids) { + return this.$q.all(this.providers.map(function (provider) { + return provider.getModels(ids); + })).then(function (provided) { + return mergeModels(provided, ids); + }); + }; + return ModelAggregator; } -); \ No newline at end of file +); diff --git a/platform/core/src/models/PersistedModelProvider.js b/platform/core/src/models/PersistedModelProvider.js index add01ba654..59ab020b14 100644 --- a/platform/core/src/models/PersistedModelProvider.js +++ b/platform/core/src/models/PersistedModelProvider.js @@ -33,62 +33,50 @@ define( * A model service which reads domain object models from an external * persistence service. * + * @memberof platform/core * @constructor + * @implements {ModelService} * @param {PersistenceService} persistenceService the service in which * domain object models are persisted. * @param $q Angular's $q service, for working with promises * @param {string} SPACE the name of the persistence space from which * models should be retrieved. */ - function PersistedModelProvider(persistenceService, $q, SPACE) { - // Load a single object model from persistence - function loadModel(id) { - return persistenceService.readObject(SPACE, id); - } - - // Promise all persisted models (in id->model form) - function promiseModels(ids) { - // Package the result as id->model - function packageResult(models) { - var result = {}; - ids.forEach(function (id, index) { - result[id] = models[index]; - }); - return result; - } - - // Filter out "namespaced" identifiers; these are - // not expected to be found in database. See WTD-659. - ids = ids.filter(function (id) { - return id.indexOf(":") === -1; - }); - - // Give a promise for all persistence lookups... - return $q.all(ids.map(loadModel)).then(packageResult); - } - - return { - /** - * Get models with the specified identifiers. - * - * This will invoke the underlying persistence service to - * retrieve object models which match the provided - * identifiers. - * - * Note that the returned object may contain a subset or a - * superset of the models requested. - * - * @param {string[]} ids an array of domain object identifiers - * @returns {Promise.} a promise for an object - * containing key-value pairs, - * where keys are object identifiers and values - * are object models. - */ - getModels: promiseModels - }; + function PersistedModelProvider(persistenceService, $q, space) { + this.persistenceService = persistenceService; + this.$q = $q; + this.space = space; } + PersistedModelProvider.prototype.getModels = function (ids) { + var persistenceService = this.persistenceService, + $q = this.$q, + space = this.space; + + // Load a single object model from persistence + function loadModel(id) { + return persistenceService.readObject(space, id); + } + + // Package the result as id->model + function packageResult(models) { + var result = {}; + ids.forEach(function (id, index) { + result[id] = models[index]; + }); + return result; + } + + // Filter out "namespaced" identifiers; these are + // not expected to be found in database. See WTD-659. + ids = ids.filter(function (id) { + return id.indexOf(":") === -1; + }); + + // Give a promise for all persistence lookups... + return $q.all(ids.map(loadModel)).then(packageResult); + }; return PersistedModelProvider; } -); \ No newline at end of file +); diff --git a/platform/core/src/models/RootModelProvider.js b/platform/core/src/models/RootModelProvider.js index f60d359af8..7c582449d5 100644 --- a/platform/core/src/models/RootModelProvider.js +++ b/platform/core/src/models/RootModelProvider.js @@ -39,46 +39,41 @@ define( * exposes them all as composition of the root object ROOT, * whose model is also provided by this service. * + * @memberof platform/core * @constructor + * @implements {ModelService} + * @param {Array} roots all `roots[]` extensions + * @param $q Angular's $q, for promises + * @param $log Anuglar's $log, for logging */ function RootModelProvider(roots, $q, $log) { - // Pull out identifiers to used as ROOT's, while setting locations. - var ids = roots.map(function (root) { - if (!root.model) { root.model = {}; } - root.model.location = 'ROOT'; - return root.id; - }), - baseProvider = new StaticModelProvider(roots, $q, $log); + // Pull out identifiers to used as ROOT's + var ids = roots.map(function (root) { return root.id; }); - function addRoot(models) { - models.ROOT = { - name: "The root object", - type: "root", - composition: ids - }; - return models; - } - - return { - - /** - * Get models with the specified identifiers. - * - * Note that the returned object may contain a subset or a - * superset of the models requested. - * - * @param {string[]} ids an array of domain object identifiers - * @returns {Promise.} a promise for an object - * containing key-value pairs, - * where keys are object identifiers and values - * are object models. - */ - getModels: function (ids) { - return baseProvider.getModels(ids).then(addRoot); + // Assign an initial location to root models + roots.forEach(function (root) { + if (!root.model) { + root.model = {}; } + root.model.location = 'ROOT'; + }); + + this.baseProvider = new StaticModelProvider(roots, $q, $log); + this.rootModel = { + name: "The root object", + type: "root", + composition: ids }; } + RootModelProvider.prototype.getModels = function (ids) { + var rootModel = this.rootModel; + return this.baseProvider.getModels(ids).then(function (models) { + models.ROOT = rootModel; + return models; + }); + }; + return RootModelProvider; } ); diff --git a/platform/core/src/models/StaticModelProvider.js b/platform/core/src/models/StaticModelProvider.js index eb946d6d4b..ea5846b07a 100644 --- a/platform/core/src/models/StaticModelProvider.js +++ b/platform/core/src/models/StaticModelProvider.js @@ -31,6 +31,7 @@ define( /** * Loads static models, provided as declared extensions of bundles. + * @memberof platform/core * @constructor */ function StaticModelProvider(models, $q, $log) { @@ -40,7 +41,7 @@ define( // Skip models which don't look right if (typeof model !== 'object' || typeof model.id !== 'string' || - typeof model.model !== 'object') { + typeof model.model !== 'object') { $log.warn([ "Skipping malformed domain object model exposed by ", ((model || {}).bundle || {}).path @@ -53,33 +54,19 @@ define( // Prepoulate maps with models to make subsequent lookup faster. models.forEach(addModelToMap); - return { - /** - * Get models for these specified string identifiers. - * These will be given as an object containing keys - * and values, where keys are object identifiers and - * values are models. - * This result may contain either a subset or a - * superset of the total objects. - * - * @param {Array} ids the string identifiers for - * models of interest. - * @returns {Promise} a promise for an object - * containing key-value pairs, where keys are - * ids and values are models - * @method - * @memberof StaticModelProvider# - */ - getModels: function (ids) { - var result = {}; - ids.forEach(function (id) { - result[id] = modelMap[id]; - }); - return $q.when(result); - } - }; + this.modelMap = modelMap; + this.$q = $q; } + StaticModelProvider.prototype.getModels = function (ids) { + var modelMap = this.modelMap, + result = {}; + ids.forEach(function (id) { + result[id] = modelMap[id]; + }); + return this.$q.when(result); + }; + return StaticModelProvider; } -); \ No newline at end of file +); diff --git a/platform/core/src/objects/DomainObject.js b/platform/core/src/objects/DomainObject.js deleted file mode 100644 index e3a10160a5..0000000000 --- a/platform/core/src/objects/DomainObject.js +++ /dev/null @@ -1,128 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT Web includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ -/*global define,Promise*/ - -/** - * Module defining DomainObject. Created by vwoeltje on 11/7/14. - */ -define( - [], - function () { - "use strict"; - - /** - * Construct a new domain object with the specified - * identifier, model, and capabilities. - * - * @param {string} id the object's unique identifier - * @param {object} model the "JSONifiable" state of the object - * @param {Object.|function} capabilities all - * capabilities to be exposed by this object - * @constructor - */ - function DomainObject(id, model, capabilities) { - return { - /** - * Get the unique identifier for this domain object. - * @return {string} the domain object's unique identifier - * @memberof DomainObject# - */ - getId: function () { - return id; - }, - - /** - * Get the domain object's model. This is useful to - * directly look up known properties of an object, but - * direct modification of a returned model is generally - * discouraged and may result in errors. Instead, an - * object's "mutation" capability should be used. - * - * @return {object} the domain object's persistent state - * @memberof DomainObject# - */ - getModel: function () { - return model; - }, - - /** - * Get a capability associated with this object. - * Capabilities are looked up by string identifiers; - * prior knowledge of a capability's interface is - * necessary. - * - * @return {Capability} the named capability, or undefined - * if not present. - * @memberof DomainObject# - */ - getCapability: function (name) { - var capability = capabilities[name]; - return typeof capability === 'function' ? - capability(this) : capability; - }, - - /**g - * Check if this domain object supports a capability - * with the provided name. - * - * @param {string} name the name of the capability to - * check for - * @returns {boolean} true if provided - */ - hasCapability: function hasCapability(name) { - return this.getCapability(name) !== undefined; - }, - - /** - * Use a capability of an object; this is a shorthand - * for: - * - * ``` - * hasCapability(name) ? - * getCapability(name).invoke(args...) : - * undefined - * ``` - * - * That is, it handles both the check-for-existence and - * invocation of the capability, and checks for existence - * before invoking the capability. - * - * @param {string} name the name of the capability to invoke - * @param {...*} [arguments] to pass to the invocation - * @returns {*} - * @memberof DomainObject# - */ - useCapability: function (name) { - // Get tail of args to pass to invoke - var args = Array.prototype.slice.apply(arguments, [1]), - capability = this.getCapability(name); - - return (capability && capability.invoke) ? - capability.invoke.apply(capability, args) : - capability; - } - }; - } - - return DomainObject; - } -); \ No newline at end of file diff --git a/platform/core/src/objects/DomainObjectImpl.js b/platform/core/src/objects/DomainObjectImpl.js new file mode 100644 index 0000000000..5c2c270a23 --- /dev/null +++ b/platform/core/src/objects/DomainObjectImpl.js @@ -0,0 +1,143 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise*/ + +/** + * Module defining DomainObject. Created by vwoeltje on 11/7/14. + */ +define( + [], + function () { + "use strict"; + + /** + * A domain object is an entity of interest to the user. + * + * @interface DomainObject + */ + + /** + * Get the unique identifier for this domain object. + * + * @method DomainObject#getId + * @return {string} the domain object's unique identifier + */ + + /** + * Get the domain object's model. This is useful to + * directly look up known properties of an object, but + * direct modification of a returned model is generally + * discouraged and may result in errors. Instead, an + * object's `mutation` capability should be used. + * + * @method DomainObject#getModel + * @return {object} the domain object's persistent state + */ + + /** + * Get a capability associated with this object. + * Capabilities are looked up by string identifiers; + * prior knowledge of a capability's interface is + * necessary. + * + * @method DomainObject#getCapability + * @param {string} key the identifier for the capability + * @return {Capability} the named capability, or undefined + * if not present. + */ + + /** + * Check if this domain object supports a capability + * with the provided name. + * + * @method DomainObject#hasCapability + * @param {string} key the identifier for the capability + * @return {boolean} true if this domain object has this capability + */ + + /** + * Use a capability of an object; the behavior of this method + * depends on the interface of the capability, and whether + * or not it is present. + * + * * If the capability is not present for this object, + * no operation occurs. + * * If the capability is present and has an `invoke` method, + * that method is called with any additional arguments + * provided, and its return value is returned. + * * If the capability is present but has no `invoke` method, + * this capability itself is returned. + * + * @method DomainObject#useCapability + * @param {string} name the name of the capability to invoke + * @param {...*} [arguments] to pass to the invocation + * @returns {*|Capability} the result of invocation (see description) + */ + + /** + * Construct a new domain object with the specified + * identifier, model, and capabilities. + * + * @param {string} id the object's unique identifier + * @param {object} model the "JSONifiable" state of the object + * @param {Object.|function} capabilities all + * capabilities to be exposed by this object + * @memberof platform/core + * @constructor + */ + function DomainObjectImpl(id, model, capabilities) { + this.id = id; + this.model = model; + this.capabilities = capabilities; + } + + DomainObjectImpl.prototype.getId = function () { + return this.id; + }; + + DomainObjectImpl.prototype.getModel = function () { + return this.model; + }; + + DomainObjectImpl.prototype.getCapability = function (name) { + var capability = this.capabilities[name]; + return typeof capability === 'function' ? + capability(this) : capability; + }; + + DomainObjectImpl.prototype.hasCapability = function (name) { + return this.getCapability(name) !== undefined; + }; + + DomainObjectImpl.prototype.useCapability = function (name) { + // Get tail of args to pass to invoke + var args = Array.prototype.slice.apply(arguments, [1]), + capability = this.getCapability(name); + + return (capability && capability.invoke) ? + capability.invoke.apply(capability, args) : + capability; + }; + + return DomainObjectImpl; + } +); diff --git a/platform/core/src/objects/DomainObjectProvider.js b/platform/core/src/objects/DomainObjectProvider.js index 46e0fbea6a..c846cbf665 100644 --- a/platform/core/src/objects/DomainObjectProvider.js +++ b/platform/core/src/objects/DomainObjectProvider.js @@ -22,13 +22,36 @@ /*global define,Promise*/ /** - * Module defining DomainObjectProvider. Created by vwoeltje on 11/7/14. + * This bundle implements core components of Open MCT Web's service + * infrastructure and information model. + * @namespace platform/core */ define( - ["./DomainObject"], - function (DomainObject) { + ["./DomainObjectImpl"], + function (DomainObjectImpl) { "use strict"; + /** + * Provides instances of domain objects, as retrieved by their + * identifiers. + * + * @interface ObjectService + */ + + /** + * Get a set of objects associated with a list of identifiers. + * The provided result may contain a subset or a superset of + * the total number of objects. + * + * @method ObjectService#getObjects + * @param {string[]} ids the identifiers for domain objects + * of interest. + * @return {Promise>} a promise + * for an object containing key-value pairs, where keys + * are string identifiers for domain objects, and + * values are the corresponding domain objects themselves. + */ + /** * Construct a new provider for domain objects. * @@ -38,9 +61,20 @@ define( * which provides capabilities (dynamic behavior) * for domain objects. * @param $q Angular's $q, for promise consolidation + * @memberof platform/core * @constructor */ function DomainObjectProvider(modelService, capabilityService, $q) { + this.modelService = modelService; + this.capabilityService = capabilityService; + this.$q = $q; + } + + DomainObjectProvider.prototype.getObjects = function getObjects(ids) { + var modelService = this.modelService, + capabilityService = this.capabilityService, + $q = this.$q; + // Given a models object (containing key-value id-model pairs) // create a function that will look up from the capability // service based on id; for handy mapping below. @@ -48,8 +82,8 @@ define( return function (id) { var model = models[id]; return model ? - capabilityService.getCapabilities(model) : - undefined; + capabilityService.getCapabilities(model) : + undefined; }; } @@ -62,7 +96,7 @@ define( ids.forEach(function (id, index) { if (models[id]) { // Create the domain object - result[id] = new DomainObject( + result[id] = new DomainObjectImpl( id, models[id], capabilities[index] @@ -72,36 +106,15 @@ define( return result; } - // Get object instances; this is the useful API exposed by the - // domain object provider. - function getObjects(ids) { - return modelService.getModels(ids).then(function (models) { - return $q.all( - ids.map(capabilityResolver(models)) - ).then(function (capabilities) { + return modelService.getModels(ids).then(function (models) { + return $q.all( + ids.map(capabilityResolver(models)) + ).then(function (capabilities) { return assembleResult(ids, models, capabilities); }); - }); - } - - return { - /** - * Get a set of objects associated with a list of identifiers. - * The provided result may contain a subset or a superset of - * the total number of objects. - * - * @param {Array} ids the identifiers for domain objects - * of interest. - * @return {Promise>} a promise - * for an object containing key-value pairs, where keys - * are string identifiers for domain objects, and - * values are the corresponding domain objects themselves. - * @memberof module:core/object/object-provider.ObjectProvider# - */ - getObjects: getObjects - }; - } + }); + }; return DomainObjectProvider; } -); \ No newline at end of file +); diff --git a/platform/core/src/services/Now.js b/platform/core/src/services/Now.js index ea78cbd57a..e1a639fe11 100644 --- a/platform/core/src/services/Now.js +++ b/platform/core/src/services/Now.js @@ -31,12 +31,14 @@ define( * `Date.now()` which can be injected to support testability. * * @returns {Function} a function which returns current system time + * @memberof platform/core */ function Now() { /** * Get the current time. * @returns {number} current time, in milliseconds since * 1970-01-01 00:00:00Z + * @memberof platform/core.Now# */ return function () { return Date.now(); @@ -45,4 +47,4 @@ define( return Now; } -); \ No newline at end of file +); diff --git a/platform/core/src/services/Throttle.js b/platform/core/src/services/Throttle.js index c0493a733a..3d68988d6b 100644 --- a/platform/core/src/services/Throttle.js +++ b/platform/core/src/services/Throttle.js @@ -42,6 +42,7 @@ define( * resolve to the returned value of `fn` whenever that is invoked. * * @returns {Function} + * @memberof platform/core */ function Throttle($timeout) { /** @@ -52,6 +53,7 @@ define( * @param {boolean} apply true if a `$apply` call should be * invoked after this function executes; defaults to * `false`. + * @memberof platform/core.Throttle# */ return function (fn, delay, apply) { var activeTimeout; @@ -82,3 +84,4 @@ define( return Throttle; } ); + diff --git a/platform/core/src/services/Topic.js b/platform/core/src/services/Topic.js index f1afafa843..ca38dfcde7 100644 --- a/platform/core/src/services/Topic.js +++ b/platform/core/src/services/Topic.js @@ -44,6 +44,7 @@ define( * arguments) are private; each call returns a new instance. * * @returns {Function} + * @memberof platform/core */ function Topic() { var topics = {}; @@ -71,6 +72,7 @@ define( /** * Use and (if necessary) create a new topic. * @param {string} [key] name of the topic to use + * @memberof platform/core.Topic# */ return function (key) { if (arguments.length < 1) { @@ -85,3 +87,4 @@ define( return Topic; } ); + diff --git a/platform/core/src/types/MergeModels.js b/platform/core/src/types/MergeModels.js index 1b5639b823..b3df6d65ae 100644 --- a/platform/core/src/types/MergeModels.js +++ b/platform/core/src/types/MergeModels.js @@ -64,6 +64,8 @@ define( * @param b the second object to be merged * @param merger the merger, as described above * @returns {*} the result of merging `a` and `b` + * @constructor + * @memberof platform/core */ function mergeModels(a, b, merger) { var mergeFunction; @@ -102,4 +104,4 @@ define( return mergeModels; } -); \ No newline at end of file +); diff --git a/platform/core/src/types/TypeCapability.js b/platform/core/src/types/TypeCapability.js index d14cff5caf..883c8dc7ab 100644 --- a/platform/core/src/types/TypeCapability.js +++ b/platform/core/src/types/TypeCapability.js @@ -34,7 +34,10 @@ define( * type directly available when working with that object, by way * of a `domainObject.getCapability('type')` invocation. * + * @memberof platform/core * @constructor + * @augments {Type} + * @implements {Capability} * @param {TypeService} typeService the service which * provides type information * @param {DomainObject} domainObject the domain object @@ -51,4 +54,4 @@ define( return TypeCapability; } -); \ No newline at end of file +); diff --git a/platform/core/src/types/TypeImpl.js b/platform/core/src/types/TypeImpl.js index a6ddd8e8aa..deb23873e5 100644 --- a/platform/core/src/types/TypeImpl.js +++ b/platform/core/src/types/TypeImpl.js @@ -21,27 +21,105 @@ *****************************************************************************/ /*global define*/ -/** - * Type implementation. Defines a type object which wraps a - * type definition and exposes useful methods for inspecting - * that type and understanding its relationship to other - * types. - * - * @module core/type/type-impl - */ define( ['./TypeProperty'], function (TypeProperty) { "use strict"; + /** + * Describes a type of domain object. + * + * @interface Type + */ + + /** + * Get the string key which identifies this type. + * This is the type's machine-readable name/identifier, + * and will correspond to the "type" field of the models + * of domain objects of this type. + * + * @returns {string} the key which identifies this type + * @method Type#getKey + */ + /** + * Get the human-readable name for this type, as should + * be displayed in the user interface when referencing + * this type. + * + * @returns {string} the human-readable name of this type + * @method Type#getName + */ + /** + * Get the human-readable description for this type, as should + * be displayed in the user interface when describing + * this type. + * + * @returns {string} the human-readable description of this type + * @method Type#getDescription + */ + /** + * Get the glyph associated with this type. Glyphs are + * single-character strings which will appear as icons (when + * displayed in an appropriate font) which visually + * distinguish types from one another. + * + * @returns {string} the glyph to be displayed + * @method Type#getGlyph + */ + /** + * Get an array of properties associated with objects of + * this type, as might be shown in a Create wizard or + * an Edit Properties view. + * + * @return {TypeProperty[]} properties associated with + * objects of this type + * @method Type#getPropertiees + */ + /** + * Get the initial state of a model for domain objects of + * this type. + * + * @return {object} initial domain object model + * @method Type#getInitialModel + */ + /** + * Get the raw type definition for this type. This is an + * object containing key-value pairs of type metadata; + * this allows the retrieval and use of custom type + * properties which are not recognized within this interface. + * + * @returns {object} the raw definition for this type + * @method Type#getDefinition + */ + /** + * Check if this type is or inherits from some other type. + * + * @param {string|Type} key either + * a string key for a type, or an instance of a type + * object, which this + * @returns {boolean} true + * @method Type#instanceOf + */ + /** + * Check if a type should support a given feature. This simply + * checks for the presence or absence of the feature key in + * the type definition's "feature" field. + * @param {string} feature a string identifying the feature + * @returns {boolean} true if the feature is supported + * @method Type#hasFeature + */ + + /** * Construct a new type. Types describe categories of * domain objects. * + * @implements {Type} * @param {TypeDefinition} typeDef an object containing * key-value pairs describing a type and its * relationship to other types. - * @memberof module:core/type/type-impl + * @constructor + * @memberof platform/core */ function TypeImpl(typeDef) { var inheritList = typeDef.inherits || [], @@ -51,124 +129,62 @@ define( featureSet[feature] = true; }); - return { - /** - * Get the string key which identifies this type. - * This is the type's machine-readable name/identifier, - * and will correspond to the "type" field of the models - * of domain objects of this type. - * - * @returns {string} the key which identifies this type - * @memberof module:core/type/type-impl.TypeImpl# - */ - getKey: function () { - return typeDef.key; - }, - /** - * Get the human-readable name for this type, as should - * be displayed in the user interface when referencing - * this type. - * - * @returns {string} the human-readable name of this type - * @memberof module:core/type/type-impl.TypeImpl# - */ - getName: function () { - return typeDef.name; - }, - /** - * Get the human-readable description for this type, as should - * be displayed in the user interface when describing - * this type. - * - * @returns {string} the human-readable description of this type - * @memberof module:core/type/type-impl.TypeImpl# - */ - getDescription: function () { - return typeDef.description; - }, - /** - * Get the glyph associated with this type. Glyphs are - * single-character strings which will appear as icons (when - * displayed in an appropriate font) which visually - * distinguish types from one another. - * - * @returns {string} the glyph to be displayed - * @memberof module:core/type/type-impl.TypeImpl# - */ - getGlyph: function () { - return typeDef.glyph; - }, - /** - * Get an array of properties associated with objects of - * this type, as might be shown in a Create wizard or - * an Edit Properties view. - * - * @return {Array} properties associated with - * objects of this type - */ - getProperties: function () { - return (typeDef.properties || []).map(TypeProperty); - }, - /** - * Get the initial state of a model for domain objects of - * this type. - * - * @return {object} initial domain object model - */ - getInitialModel: function () { - return typeDef.model || {}; - }, - /** - * Get the raw type definition for this type. This is an - * object containing key-value pairs of type metadata; - * this allows the retrieval and use of custom type - * properties which are not recognized within this interface. - * - * @returns {object} the raw definition for this type - * @memberof module:core/type/type-impl.TypeImpl# - */ - getDefinition: function () { - return typeDef; - }, - /** - * Check if this type is or inherits from some other type. - * - * TODO: Rename, "instanceOf" is a misnomer (since there is - * no "instance", so to speak.) - * - * @param {string|module:core/type/type-implTypeImpl} key either - * a string key for a type, or an instance of a type - * object, which this - * @returns {boolean} true - * @memberof module:core/type/type-impl.TypeImpl# - */ - instanceOf: function instanceOf(key) { - - if (key === typeDef.key) { - return true; - } else if (inheritList.indexOf(key) > -1) { - return true; - } else if (!key) { - return true; - } else if (key !== null && typeof key === 'object') { - return key.getKey ? instanceOf(key.getKey()) : false; - } else { - return false; - } - }, - /** - * Check if a type should support a given feature. This simply - * checks for the presence or absence of the feature key in - * the type definition's "feature" field. - * @param {string} feature a string identifying the feature - * @returns {boolean} true if the feature is supported - */ - hasFeature: function (feature) { - return featureSet[feature] || false; - } - }; + this.typeDef = typeDef; + this.featureSet = featureSet; + this.inheritList = inheritList; } + TypeImpl.prototype.getKey = function () { + return this.typeDef.key; + }; + + TypeImpl.prototype.getName = function () { + return this.typeDef.name; + }; + + TypeImpl.prototype.getDescription = function () { + return this.typeDef.description; + }; + + TypeImpl.prototype.getGlyph = function () { + return this.typeDef.glyph; + }; + + TypeImpl.prototype.getProperties = function () { + return (this.typeDef.properties || []).map(function (propertyDef) { + return new TypeProperty(propertyDef); + }); + }; + + TypeImpl.prototype.getInitialModel = function () { + return this.typeDef.model || {}; + }; + + TypeImpl.prototype.getDefinition = function () { + return this.typeDef; + }; + + TypeImpl.prototype.instanceOf = function instanceOf(key) { + var typeDef = this.typeDef, + inheritList = this.inheritList; + + if (key === typeDef.key) { + return true; + } else if (inheritList.indexOf(key) > -1) { + return true; + } else if (!key) { + return true; + } else if (key !== null && typeof key === 'object') { + return key.getKey ? this.instanceOf(key.getKey()) : false; + } else { + return false; + } + }; + + TypeImpl.prototype.hasFeature = function (feature) { + return this.featureSet[feature] || false; + }; + return TypeImpl; } -); \ No newline at end of file +); diff --git a/platform/core/src/types/TypeProperty.js b/platform/core/src/types/TypeProperty.js index 50bd1621a1..70aaf8fbf8 100644 --- a/platform/core/src/types/TypeProperty.js +++ b/platform/core/src/types/TypeProperty.js @@ -21,12 +21,6 @@ *****************************************************************************/ /*global define*/ -/** - * Type property. Defines a mutable or displayable property - * associated with objects of a given type. - * - * @module core/type/type-property - */ define( ['./TypePropertyConversion'], function (TypePropertyConversion) { @@ -36,130 +30,135 @@ define( * Instantiate a property associated with domain objects of a * given type. This provides an interface by which * + * @memberof platform/core * @constructor - * @memberof module:core/type/type-property */ function TypeProperty(propertyDefinition) { // Load an appropriate conversion - var conversion = new TypePropertyConversion( + this.conversion = new TypePropertyConversion( propertyDefinition.conversion || "identity" ); + this.propertyDefinition = propertyDefinition; + } - // Check if a value is defined; used to check if initial array - // values have been populated. - function isUnpopulatedArray(value) { - var i; + // Check if a value is defined; used to check if initial array + // values have been populated. + function isUnpopulatedArray(value) { + var i; - if (!Array.isArray(value) || value.length === 0) { - return false; - } - - for (i = 0; i < value.length; i += 1) { - if (value[i] !== undefined) { - return false; - } - } - - return true; + if (!Array.isArray(value) || value.length === 0) { + return false; } - // Perform a lookup for a value from an object, - // which may recursively look at contained objects - // based on the path provided. - function lookupValue(object, propertyPath) { - var value; - - // Can't look up from a non-object - if (!object) { - return undefined; + for (i = 0; i < value.length; i += 1) { + if (value[i] !== undefined) { + return false; } + } - // If path is not an array, just look up the property - if (!Array.isArray(propertyPath)) { - return object[propertyPath]; - } + return true; + } - // Otherwise, look up in the sequence defined in the array - if (propertyPath.length > 0) { - value = object[propertyPath[0]]; - return propertyPath.length > 1 ? - lookupValue(value, propertyPath.slice(1)) : - value; - } + // Specify a field deeply within an object + function specifyValue(object, propertyPath, value) { + // If path is not an array, just set the property + if (!Array.isArray(propertyPath)) { + object[propertyPath] = value; + } else if (propertyPath.length > 1) { + // Otherwise, look up in defined sequence + object[propertyPath[0]] = object[propertyPath[0]] || {}; + specifyValue( + object[propertyPath[0]], + propertyPath.slice(1), + value + ); + } else if (propertyPath.length === 1) { + object[propertyPath[0]] = value; + } + } - // Fallback; property path was empty + // Perform a lookup for a value from an object, + // which may recursively look at contained objects + // based on the path provided. + function lookupValue(object, propertyPath) { + var value; + + // Can't look up from a non-object + if (!object) { return undefined; } - function specifyValue(object, propertyPath, value) { - - // If path is not an array, just set the property - if (!Array.isArray(propertyPath)) { - object[propertyPath] = value; - } else if (propertyPath.length > 1) { - // Otherwise, look up in defined sequence - object[propertyPath[0]] = object[propertyPath[0]] || {}; - specifyValue( - object[propertyPath[0]], - propertyPath.slice(1), - value - ); - } else if (propertyPath.length === 1) { - object[propertyPath[0]] = value; - } - + // If path is not an array, just look up the property + if (!Array.isArray(propertyPath)) { + return object[propertyPath]; } - return { - /** - * Retrieve the value associated with this property - * from a given model. - */ - getValue: function (model) { - var property = propertyDefinition.property || - propertyDefinition.key, - initialValue = - property && lookupValue(model, property); + // Otherwise, look up in the sequence defined in the array + if (propertyPath.length > 0) { + value = object[propertyPath[0]]; + return propertyPath.length > 1 ? + lookupValue(value, propertyPath.slice(1)) : + value; + } - // Provide an empty array if this is a multi-item - // property. - if (Array.isArray(propertyDefinition.items)) { - initialValue = initialValue || - new Array(propertyDefinition.items.length); - } - - return conversion.toFormValue(initialValue); - }, - /** - * Set a value associated with this property in - * an object's model. - */ - setValue: function setValue(model, value) { - var property = propertyDefinition.property || - propertyDefinition.key; - - // If an array contains all undefined values, treat it - // as undefined, to filter back out arrays for input - // that never got entered. - value = isUnpopulatedArray(value) ? undefined : value; - - // Convert to a value suitable for storage in the - // domain object's model - value = conversion.toModelValue(value); - - return property ? - specifyValue(model, property, value) : - undefined; - }, - /** - * Get the raw definition for this property. - */ - getDefinition: function () { - return propertyDefinition; - } - }; + // Fallback; property path was empty + return undefined; } + /** + * Retrieve the value associated with this property + * from a given model. + * @param {object} model a domain object model to read from + * @returns {*} the value for this property, as read from the model + */ + TypeProperty.prototype.getValue = function (model) { + var property = this.propertyDefinition.property || + this.propertyDefinition.key, + initialValue = + property && lookupValue(model, property); + + // Provide an empty array if this is a multi-item + // property. + if (Array.isArray(this.propertyDefinition.items)) { + initialValue = initialValue || + new Array(this.propertyDefinition.items.length); + } + + return this.conversion.toFormValue(initialValue); + }; + + /** + * Set a value associated with this property in + * an object's model. + * @param {object} model a domain object model to update + * @param {*} value the new value to set for this property + */ + TypeProperty.prototype.setValue = function (model, value) { + var property = this.propertyDefinition.property || + this.propertyDefinition.key; + + // If an array contains all undefined values, treat it + // as undefined, to filter back out arrays for input + // that never got entered. + value = isUnpopulatedArray(value) ? undefined : value; + + // Convert to a value suitable for storage in the + // domain object's model + value = this.conversion.toModelValue(value); + + return property ? + specifyValue(model, property, value) : + undefined; + }; + + /** + * Get the raw definition for this property. + * @returns {TypePropertyDefinition} + */ + TypeProperty.prototype.getDefinition = function () { + return this.propertyDefinition; + }; + return TypeProperty; } -); \ No newline at end of file +); diff --git a/platform/core/src/types/TypePropertyConversion.js b/platform/core/src/types/TypePropertyConversion.js index b00df71c72..6f8344e3d7 100644 --- a/platform/core/src/types/TypePropertyConversion.js +++ b/platform/core/src/types/TypePropertyConversion.js @@ -21,12 +21,6 @@ *****************************************************************************/ /*global define*/ -/** - * Defines type property conversions, used to convert values from - * a domain object model to values displayable in a form, and - * vice versa. - * @module core/type/type-property-conversion - */ define( function () { 'use strict'; @@ -62,6 +56,8 @@ define( /** * Look up an appropriate conversion between form values and model * values, e.g. to numeric values. + * @constructor + * @memberof platform/core */ function TypePropertyConversion(name) { if (name && @@ -80,6 +76,23 @@ define( } } + /** + * Convert a value from its format as read from a form, to a + * format appropriate to store in a model. + * @method platform/core.TypePropertyConversion#toModelValue + * @param {*} formValue value as read from a form + * @returns {*} value to store in a model + */ + + /** + * Convert a value from its format as stored in a model, to a + * format appropriate to display in a form. + * @method platform/core.TypePropertyConversion#toFormValue + * @param {*} modelValue value as stored in a model + * @returns {*} value to display within a form + */ + + return TypePropertyConversion; } -); \ No newline at end of file +); diff --git a/platform/core/src/types/TypeProvider.js b/platform/core/src/types/TypeProvider.js index 79d8c8f800..d8b6475d58 100644 --- a/platform/core/src/types/TypeProvider.js +++ b/platform/core/src/types/TypeProvider.js @@ -26,6 +26,27 @@ define( function (TypeImpl, mergeModels) { 'use strict'; + /** + * Provides domain object types that are available/recognized within + * the system. + * + * @interface TypeService + */ + /** + * Get a specific type by name. + * + * @method TypeService#getType + * @param {string} key the key (machine-readable identifier) + * for the type of interest + * @returns {Type} the type identified by this key + */ + /** + * List all known types. + * + * @method TypeService#listTypes + * @returns {Type[]} all known types + */ + var TO_CONCAT = ['inherits', 'capabilities', 'properties', 'features'], TO_MERGE = ['model']; @@ -49,14 +70,47 @@ define( }) : array; } + // Reduce an array of type definitions to a single type definiton, + // which has merged all properties in order. + function collapse(typeDefs) { + var collapsed = typeDefs.reduce(function (a, b) { + var result = {}; + copyKeys(result, a); + copyKeys(result, b); + + // Special case: Do a merge, e.g. on "model" + TO_MERGE.forEach(function (k) { + if (a[k] && b[k]) { + result[k] = mergeModels(a[k], b[k]); + } + }); + + // Special case: Concatenate certain arrays + TO_CONCAT.forEach(function (k) { + if (a[k] || b[k]) { + result[k] = (a[k] || []).concat(b[k] || []); + } + }); + return result; + }, {}); + + // Remove any duplicates from the collapsed array + TO_CONCAT.forEach(function (k) { + if (collapsed[k]) { + collapsed[k] = removeDuplicates(collapsed[k]); + } + }); + return collapsed; + } + /** * A type provider provides information about types of domain objects * within the running Open MCT Web instance. * - * @param {Array} options.definitions the raw type + * @param {Array} types the raw type * definitions for this type. + * @memberof platform/core * @constructor - * @memberof module:core/type/type-provider */ function TypeProvider(types) { var rawTypeDefinitions = types, @@ -69,46 +123,34 @@ define( } }); return result; - }(rawTypeDefinitions)), - typeMap = {}, - undefinedType; + }(rawTypeDefinitions)); - // Reduce an array of type definitions to a single type definiton, - // which has merged all properties in order. - function collapse(typeDefs) { - var collapsed = typeDefs.reduce(function (a, b) { - var result = {}; - copyKeys(result, a); - copyKeys(result, b); - // Special case: Do a merge, e.g. on "model" - TO_MERGE.forEach(function (k) { - if (a[k] && b[k]) { - result[k] = mergeModels(a[k], b[k]); - } - }); + this.typeMap = {}; + this.typeDefinitions = typeDefinitions; + this.rawTypeDefinitions = types; + } - // Special case: Concatenate certain arrays - TO_CONCAT.forEach(function (k) { - if (a[k] || b[k]) { - result[k] = (a[k] || []).concat(b[k] || []); - } - }); - return result; - }, {}); + TypeProvider.prototype.listTypes = function () { + var self = this; + return removeDuplicates( + this.rawTypeDefinitions.filter(function (def) { + return def.key; + }).map(function (def) { + return def.key; + }).map(function (key) { + return self.getType(key); + }) + ); + }; - // Remove any duplicates from the collapsed array - TO_CONCAT.forEach(function (k) { - if (collapsed[k]) { - collapsed[k] = removeDuplicates(collapsed[k]); - } - }); - return collapsed; - } + TypeProvider.prototype.getType = function (key) { + var typeDefinitions = this.typeDefinitions, + self = this; function getUndefinedType() { - return (undefinedType = undefinedType || collapse( - rawTypeDefinitions.filter(function (typeDef) { + return (self.undefinedType = self.undefinedType || collapse( + self.rawTypeDefinitions.filter(function (typeDef) { return !typeDef.key; }) )); @@ -134,61 +176,20 @@ define( // Always provide a default name def.model = def.model || {}; - def.model.name = def.model.name || ( - "Unnamed " + (def.name || "Object") - ); + def.model.name = def.model.name || + ("Unnamed " + (def.name || "Object")); return def; - } - - return (typeMap[typeKey] = typeMap[typeKey] || buildTypeDef(typeKey)); + + return (self.typeMap[typeKey] = + self.typeMap[typeKey] || buildTypeDef(typeKey)); } - - return { - /** - * Get a list of all types defined by this service. - * - * @returns {Promise>} a - * promise for an array of all type instances defined - * by this service. - * @memberof module:core/type/type-provider.TypeProvider# - */ - listTypes: function () { - var self = this; - return removeDuplicates( - rawTypeDefinitions.filter(function (def) { - return def.key; - }).map(function (def) { - return def.key; - }).map(function (key) { - return self.getType(key); - }) - ); - }, - - /** - * Get a specific type by name. - * - * @param {string} [key] the key (machine-readable identifier) - * for the type of interest - * @returns {Promise} a - * promise for a type object identified by this key. - * @memberof module:core/type/type-provider.TypeProvider# - */ - getType: function (key) { - return new TypeImpl(lookupTypeDef(key)); - } - }; - } - - // Services framework is designed to expect factories - TypeProvider.instantiate = TypeProvider; + return new TypeImpl(lookupTypeDef(key)); + }; return TypeProvider; - - } -); \ No newline at end of file +); diff --git a/platform/core/src/views/ViewCapability.js b/platform/core/src/views/ViewCapability.js index 3653cf5ac5..38862cf2fc 100644 --- a/platform/core/src/views/ViewCapability.js +++ b/platform/core/src/views/ViewCapability.js @@ -35,22 +35,26 @@ define( * thereabout) which are applicable to a specific domain * object. * + * @memberof platform/core + * @implements {Capability} * @constructor */ function ViewCapability(viewService, domainObject) { - return { - /** - * Get all view definitions which are applicable to - * this object. - * @returns {View[]} an array of view definitions - * which are applicable to this object. - */ - invoke: function () { - return viewService.getViews(domainObject); - } - }; + this.viewService = viewService; + this.domainObject = domainObject; } + /** + * Get all view definitions which are applicable to + * this object. + * @returns {View[]} an array of view definitions + * which are applicable to this object. + * @memberof platform/core.ViewCapability# + */ + ViewCapability.prototype.invoke = function () { + return this.viewService.getViews(this.domainObject); + }; + return ViewCapability; } -); \ No newline at end of file +); diff --git a/platform/core/src/views/ViewProvider.js b/platform/core/src/views/ViewProvider.js index 2b82f0f67d..e2034c3d6b 100644 --- a/platform/core/src/views/ViewProvider.js +++ b/platform/core/src/views/ViewProvider.js @@ -29,6 +29,22 @@ define( function () { "use strict"; + /** + * Provides definitions for views that are available for specific + * domain objects. + * + * @interface ViewService + */ + + /** + * Get all views which are applicable to this domain object. + * + * @method ViewService#getViews + * @param {DomainObject} domainObject the domain object to view + * @returns {View[]} all views which can be used to visualize + * this domain object. + */ + /** * A view provider allows view definitions (defined as extensions) * to be read, and takes responsibility for filtering these down @@ -55,8 +71,11 @@ define( * The role of a view provider and of a view capability is to * describe what views are available, not how to instantiate them. * + * @memberof platform/core * @constructor * @param {View[]} an array of view definitions + * @param $log Angular's logging service + * @implements {ViewService} */ function ViewProvider(views, $log) { @@ -78,6 +97,13 @@ define( return key; } + // Filter out any key-less views + this.views = views.filter(validate); + } + + ViewProvider.prototype.getViews = function (domainObject) { + var type = domainObject.useCapability("type"); + // Check if an object has all capabilities designated as `needs` // for a view. Exposing a capability via delegation is taken to // satisfy this filter if `allowDelegation` is true. @@ -121,35 +147,17 @@ define( return matches; } - function getViews(domainObject) { - var type = domainObject.useCapability("type"); - - // First, filter views by type (matched to domain object type.) - // Second, filter by matching capabilities. - return views.filter(function (view) { - return viewMatchesType(view, type) && capabilitiesMatch( + // First, filter views by type (matched to domain object type.) + // Second, filter by matching capabilities. + return this.views.filter(function (view) { + return viewMatchesType(view, type) && capabilitiesMatch( domainObject, view.needs || [], view.delegation || false ); - }); - } - - // Filter out any key-less views - views = views.filter(validate); - - return { - /** - * Get all views which are applicable to this domain object. - * - * @param {DomainObject} domainObject the domain object to view - * @returns {View[]} all views which can be used to visualize - * this domain object. - */ - getViews: getViews - }; - } + }); + }; return ViewProvider; } -); \ No newline at end of file +); diff --git a/platform/core/test/models/MissingModelDecoratorSpec.js b/platform/core/test/models/MissingModelDecoratorSpec.js new file mode 100644 index 0000000000..da9d4fc54c --- /dev/null +++ b/platform/core/test/models/MissingModelDecoratorSpec.js @@ -0,0 +1,84 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../../src/models/MissingModelDecorator"], + function (MissingModelDecorator) { + "use strict"; + + describe("The missing model decorator", function () { + var mockModelService, + testModels, + decorator; + + function asPromise(value) { + return (value || {}).then ? value : { + then: function (callback) { + return asPromise(callback(value)); + } + }; + } + + beforeEach(function () { + mockModelService = jasmine.createSpyObj( + "modelService", + [ "getModels" ] + ); + + testModels = { + testId: { someKey: "some value" } + }; + + mockModelService.getModels.andReturn(asPromise(testModels)); + + decorator = new MissingModelDecorator(mockModelService); + }); + + it("delegates to the wrapped model service", function () { + decorator.getModels(['a', 'b', 'c']); + expect(mockModelService.getModels) + .toHaveBeenCalledWith(['a', 'b', 'c']); + }); + + it("provides models for any IDs which are missing", function () { + var models; + decorator.getModels(['testId', 'otherId']) + .then(function (m) { models = m; }); + expect(models.otherId).toBeDefined(); + }); + + it("does not overwrite existing models", function () { + var models; + decorator.getModels(['testId', 'otherId']) + .then(function (m) { models = m; }); + expect(models.testId).toEqual({ someKey: "some value" }); + }); + + it("does not modify the wrapped service's response", function () { + decorator.getModels(['testId', 'otherId']); + expect(testModels.otherId).toBeUndefined(); + }); + }); + + } +); diff --git a/platform/core/test/objects/DomainObjectSpec.js b/platform/core/test/objects/DomainObjectSpec.js index 29862461b3..13e8968e0d 100644 --- a/platform/core/test/objects/DomainObjectSpec.js +++ b/platform/core/test/objects/DomainObjectSpec.js @@ -25,7 +25,7 @@ * DomainObjectSpec. Created by vwoeltje on 11/6/14. */ define( - ["../../src/objects/DomainObject"], + ["../../src/objects/DomainObjectImpl"], function (DomainObject) { "use strict"; diff --git a/platform/core/test/suite.json b/platform/core/test/suite.json index acc7391d02..e2a7d8f57a 100644 --- a/platform/core/test/suite.json +++ b/platform/core/test/suite.json @@ -15,6 +15,7 @@ "capabilities/RelationshipCapability", "models/ModelAggregator", + "models/MissingModelDecorator", "models/PersistedModelProvider", "models/RootModelProvider", "models/StaticModelProvider", diff --git a/platform/core/test/types/TypeImplSpec.js b/platform/core/test/types/TypeImplSpec.js index c11075870c..d29c4f2712 100644 --- a/platform/core/test/types/TypeImplSpec.js +++ b/platform/core/test/types/TypeImplSpec.js @@ -23,7 +23,7 @@ define( ['../../src/types/TypeImpl'], - function (typeImpl) { + function (TypeImpl) { "use strict"; describe("Type definition wrapper", function () { @@ -41,7 +41,7 @@ define( properties: [ {} ], model: {someKey: "some value"} }; - type = typeImpl(testTypeDef); + type = new TypeImpl(testTypeDef); }); it("exposes key from definition", function () { diff --git a/platform/core/test/types/TypeProviderSpec.js b/platform/core/test/types/TypeProviderSpec.js index 0da1f5910e..24f0a77c73 100644 --- a/platform/core/test/types/TypeProviderSpec.js +++ b/platform/core/test/types/TypeProviderSpec.js @@ -128,7 +128,7 @@ define( }); it("includes capabilities from undefined type in all types", function () { - captured.type = TypeProvider.instantiate( + captured.type = new TypeProvider( testTypeDefinitions.concat([ { capabilities: ['a', 'b', 'c'] }, { capabilities: ['x', 'y', 'z'] } diff --git a/platform/entanglement/src/actions/AbstractComposeAction.js b/platform/entanglement/src/actions/AbstractComposeAction.js new file mode 100644 index 0000000000..cc78eb1888 --- /dev/null +++ b/platform/entanglement/src/actions/AbstractComposeAction.js @@ -0,0 +1,126 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define */ +define( + function () { + "use strict"; + + /** + * Common interface exposed by services which support move, copy, + * and link actions. + * @interface platform/entanglement.AbstractComposeService + * @private + */ + /** + * Change the composition of the specified objects. + * + * @param {DomainObject} domainObject the domain object to + * move, copy, or link. + * @param {DomainObject} parent the domain object whose composition + * will be changed to contain the domainObject (or its duplicate) + * @returns {Promise} A promise that is fulfilled when the + * duplicate operation has completed. + * @method platform/entanglement.AbstractComposeService#perform + */ + /** + * Check if one object can be composed into another. + * @param {DomainObject} domainObject the domain object to + * move, copy, or link. + * @param {DomainObject} parent the domain object whose composition + * will be changed to contain the domainObject (or its duplicate) + * @returns {boolean} true if this composition change is allowed + * @method platform/entanglement.AbstractComposeService#validate + */ + + + /** + * Template class for Move, Copy, and Link actions. + * + * @implements {Action} + * @constructor + * @private + * @memberof platform/entanglement + * @param {platform/entanglement.LocationService} locationService a + * service to request destinations from the user + * @param {platform/entanglement.AbstractComposeService} composeService + * a service which will handle actual changes to composition + * @param {ActionContext} the context in which the action will be performed + * @param {string} verb the verb to display for the action (e.g. "Move") + * @param {string} [suffix] a string to display in the dialog title; + * default is "to a new location" + */ + function AbstractComposeAction(locationService, composeService, context, verb, suffix) { + if (context.selectedObject) { + this.newParent = context.domainObject; + this.object = context.selectedObject; + } else { + this.object = context.domainObject; + } + + this.currentParent = this.object + .getCapability('context') + .getParent(); + + this.locationService = locationService; + this.composeService = composeService; + this.verb = verb || "Compose"; + this.suffix = suffix || "to a new location"; + } + + AbstractComposeAction.prototype.perform = function () { + var dialogTitle, + label, + validateLocation, + locationService = this.locationService, + composeService = this.composeService, + currentParent = this.currentParent, + newParent = this.newParent, + object = this.object; + + if (newParent) { + return composeService.perform(object, newParent); + } + + dialogTitle = [this.verb, object.getModel().name, this.suffix] + .join(" "); + + label = this.verb + " To"; + + validateLocation = function (newParent) { + return composeService.validate(object, newParent); + }; + + return locationService.getLocationFromUser( + dialogTitle, + label, + validateLocation, + currentParent + ).then(function (newParent) { + return composeService.perform(object, newParent); + }); + }; + + return AbstractComposeAction; + } +); + diff --git a/platform/entanglement/src/actions/CopyAction.js b/platform/entanglement/src/actions/CopyAction.js index aff8b94fb5..3411fdba85 100644 --- a/platform/entanglement/src/actions/CopyAction.js +++ b/platform/entanglement/src/actions/CopyAction.js @@ -22,71 +22,29 @@ /*global define */ define( - function () { + ['./AbstractComposeAction'], + function (AbstractComposeAction) { "use strict"; - /** * The CopyAction is available from context menus and allows a user to * deep copy an object to another location of their choosing. * - * @implements Action + * @implements {Action} + * @constructor + * @memberof platform/entanglement */ function CopyAction(locationService, copyService, context) { - - var object, - newParent, - currentParent; - - if (context.selectedObject) { - newParent = context.domainObject; - object = context.selectedObject; - } else { - object = context.domainObject; - } - - currentParent = object - .getCapability('context') - .getParent(); - - return { - perform: function () { - - if (newParent) { - return copyService - .perform(object, newParent); - } - - var dialogTitle, - label, - validateLocation; - - dialogTitle = [ - "Duplicate ", - object.getModel().name, - " to a location" - ].join(""); - - label = "Duplicate To"; - - validateLocation = function (newParent) { - return copyService - .validate(object, newParent); - }; - - return locationService.getLocationFromUser( - dialogTitle, - label, - validateLocation, - currentParent - ).then(function (newParent) { - return copyService - .perform(object, newParent); - }); - } - }; + return new AbstractComposeAction( + locationService, + copyService, + context, + "Duplicate", + "to a location" + ); } return CopyAction; } ); + diff --git a/platform/entanglement/src/actions/LinkAction.js b/platform/entanglement/src/actions/LinkAction.js index c6b4be100f..c791310886 100644 --- a/platform/entanglement/src/actions/LinkAction.js +++ b/platform/entanglement/src/actions/LinkAction.js @@ -22,68 +22,28 @@ /*global define */ define( - function () { + ['./AbstractComposeAction'], + function (AbstractComposeAction) { "use strict"; /** * The LinkAction is available from context menus and allows a user to * link an object to another location of their choosing. * - * @implements Action + * @implements {Action} + * @constructor + * @memberof platform/entanglement */ function LinkAction(locationService, linkService, context) { - - var object, - newParent, - currentParent; - - if (context.selectedObject) { - newParent = context.domainObject; - object = context.selectedObject; - } else { - object = context.domainObject; - } - - currentParent = object - .getCapability('context') - .getParent(); - - return { - perform: function () { - if (newParent) { - return linkService - .perform(object, newParent); - } - var dialogTitle, - label, - validateLocation; - - dialogTitle = [ - "Link ", - object.getModel().name, - " to a new location" - ].join(""); - - label = "Link To"; - - validateLocation = function (newParent) { - return linkService - .validate(object, newParent); - }; - - return locationService.getLocationFromUser( - dialogTitle, - label, - validateLocation, - currentParent - ).then(function (newParent) { - return linkService - .perform(object, newParent); - }); - } - }; + return new AbstractComposeAction( + locationService, + linkService, + context, + "Link" + ); } return LinkAction; } ); + diff --git a/platform/entanglement/src/actions/MoveAction.js b/platform/entanglement/src/actions/MoveAction.js index 63f1517c56..4fdd4b59df 100644 --- a/platform/entanglement/src/actions/MoveAction.js +++ b/platform/entanglement/src/actions/MoveAction.js @@ -22,69 +22,28 @@ /*global define */ define( - function () { + ['./AbstractComposeAction'], + function (AbstractComposeAction) { "use strict"; /** * The MoveAction is available from context menus and allows a user to * move an object to another location of their choosing. * - * @implements Action + * @implements {Action} + * @constructor + * @memberof platform/entanglement */ function MoveAction(locationService, moveService, context) { - - var object, - newParent, - currentParent; - - if (context.selectedObject) { - newParent = context.domainObject; - object = context.selectedObject; - } else { - object = context.domainObject; - } - - currentParent = object - .getCapability('context') - .getParent(); - - return { - perform: function () { - if (newParent) { - return moveService - .perform(object, newParent); - } - - var dialogTitle, - label, - validateLocation; - - dialogTitle = [ - "Move ", - object.getModel().name, - " to a new location" - ].join(""); - - label = "Move To"; - - validateLocation = function (newParent) { - return moveService - .validate(object, newParent); - }; - - return locationService.getLocationFromUser( - dialogTitle, - label, - validateLocation, - currentParent - ).then(function (newParent) { - return moveService - .perform(object, newParent); - }); - } - }; + return new AbstractComposeAction( + locationService, + moveService, + context, + "Move" + ); } return MoveAction; } ); + diff --git a/platform/entanglement/src/services/CopyService.js b/platform/entanglement/src/services/CopyService.js index 487568c475..d62eb2a0ed 100644 --- a/platform/entanglement/src/services/CopyService.js +++ b/platform/entanglement/src/services/CopyService.js @@ -30,77 +30,66 @@ define( * CopyService provides an interface for deep copying objects from one * location to another. It also provides a method for determining if * an object can be copied to a specific location. + * @constructor + * @memberof platform/entanglement + * @implements {platform/entanglement.AbstractComposeService} */ function CopyService($q, creationService, policyService) { + this.$q = $q; + this.creationService = creationService; + this.policyService = policyService; + } - /** - * duplicateObject duplicates a `domainObject` into the composition - * of `parent`, and then duplicates the composition of - * `domainObject` into the new object. - * - * This function is a recursive deep copy. - * - * @param {DomainObject} domainObject - the domain object to - * duplicate. - * @param {DomainObject} parent - the parent domain object to - * create the duplicate in. - * @returns {Promise} A promise that is fulfilled when the - * duplicate operation has completed. - */ + CopyService.prototype.validate = function (object, parentCandidate) { + if (!parentCandidate || !parentCandidate.getId) { + return false; + } + if (parentCandidate.getId() === object.getId()) { + return false; + } + return this.policyService.allow( + "composition", + parentCandidate.getCapability('type'), + object.getCapability('type') + ); + }; + + CopyService.prototype.perform = function (domainObject, parent) { + var model = JSON.parse(JSON.stringify(domainObject.getModel())), + $q = this.$q, + self = this; + + // Wrapper for the recursive step function duplicateObject(domainObject, parent) { - var model = JSON.parse(JSON.stringify(domainObject.getModel())); - if (domainObject.hasCapability('composition')) { - model.composition = []; - } - - return creationService - .createObject(model, parent) - .then(function (newObject) { - if (!domainObject.hasCapability('composition')) { - return; - } - - return domainObject - .useCapability('composition') - .then(function (composees) { - // Duplicate composition serially to prevent - // write conflicts. - return composees.reduce(function (promise, composee) { - return promise.then(function () { - return duplicateObject(composee, newObject); - }); - }, $q.when(undefined)); - }); - }); + return self.perform(domainObject, parent); } - return { - /** - * Returns true if `object` can be copied into - * `parentCandidate`'s composition. - */ - validate: function (object, parentCandidate) { - if (!parentCandidate || !parentCandidate.getId) { - return false; + if (domainObject.hasCapability('composition')) { + model.composition = []; + } + + return this.creationService + .createObject(model, parent) + .then(function (newObject) { + if (!domainObject.hasCapability('composition')) { + return; } - if (parentCandidate.getId() === object.getId()) { - return false; - } - return policyService.allow( - "composition", - parentCandidate.getCapability('type'), - object.getCapability('type') - ); - }, - /** - * Wrapper, @see {@link duplicateObject} for implementation. - */ - perform: function (object, parentObject) { - return duplicateObject(object, parentObject); - } - }; - } + + return domainObject + .useCapability('composition') + .then(function (composees) { + // Duplicate composition serially to prevent + // write conflicts. + return composees.reduce(function (promise, composee) { + return promise.then(function () { + return duplicateObject(composee, newObject); + }); + }, $q.when(undefined)); + }); + }); + }; return CopyService; } ); + diff --git a/platform/entanglement/src/services/LinkService.js b/platform/entanglement/src/services/LinkService.js index da9a2f92da..9fb38dd273 100644 --- a/platform/entanglement/src/services/LinkService.js +++ b/platform/entanglement/src/services/LinkService.js @@ -30,58 +30,55 @@ define( * LinkService provides an interface for linking objects to additional * locations. It also provides a method for determining if an object * can be copied to a specific location. + * @constructor + * @memberof platform/entanglement + * @implements {platform/entanglement.AbstractComposeService} */ function LinkService(policyService) { - return { - /** - * Returns `true` if `object` can be linked into - * `parentCandidate`'s composition. - */ - validate: function (object, parentCandidate) { - if (!parentCandidate || !parentCandidate.getId) { - return false; - } - if (parentCandidate.getId() === object.getId()) { - return false; - } - if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { - return false; - } - return policyService.allow( - "composition", - parentCandidate.getCapability('type'), - object.getCapability('type') - ); - }, - /** - * Link `object` into `parentObject`'s composition. - * - * @returns {Promise} A promise that is fulfilled when the - * linking operation has completed. - */ - perform: function (object, parentObject) { - return parentObject.useCapability('mutation', function (model) { - if (model.composition.indexOf(object.getId()) === -1) { - model.composition.push(object.getId()); - } - }).then(function () { - return parentObject.getCapability('persistence').persist(); - }).then(function getObjectWithNewContext() { - return parentObject - .useCapability('composition') - .then(function (children) { - var i; - for (i = 0; i < children.length; i += 1) { - if (children[i].getId() === object.getId()) { - return children[i]; - } - } - }); - }); - } - }; + this.policyService = policyService; } + LinkService.prototype.validate = function (object, parentCandidate) { + if (!parentCandidate || !parentCandidate.getId) { + return false; + } + if (parentCandidate.getId() === object.getId()) { + return false; + } + if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { + return false; + } + return this.policyService.allow( + "composition", + parentCandidate.getCapability('type'), + object.getCapability('type') + ); + }; + + LinkService.prototype.perform = function (object, parentObject) { + function findChild(children) { + var i; + for (i = 0; i < children.length; i += 1) { + if (children[i].getId() === object.getId()) { + return children[i]; + } + } + } + + return parentObject.useCapability('mutation', function (model) { + if (model.composition.indexOf(object.getId()) === -1) { + model.composition.push(object.getId()); + } + }).then(function () { + return parentObject.getCapability('persistence').persist(); + }).then(function getObjectWithNewContext() { + return parentObject + .useCapability('composition') + .then(findChild); + }); + }; + return LinkService; } ); + diff --git a/platform/entanglement/src/services/LocationService.js b/platform/entanglement/src/services/LocationService.js index 3e71011503..cb0a632b4e 100644 --- a/platform/entanglement/src/services/LocationService.js +++ b/platform/entanglement/src/services/LocationService.js @@ -22,6 +22,11 @@ /*global define */ +/** + * This bundle implements actions which control the location of objects + * (move, copy, link.) + * @namespace platform/entanglement + */ define( function () { "use strict"; @@ -29,6 +34,8 @@ define( /** * The LocationService allows for easily prompting the user for a * location in the root tree. + * @constructor + * @memberof platform/entanglement */ function LocationService(dialogService) { return { @@ -43,6 +50,7 @@ define( * @param {domainObject} initialLocation - tree location to * display at start * @returns {Promise} promise for a domain object. + * @memberof platform/entanglement.LocationService# */ getLocationFromUser: function (title, label, validate, initialLocation) { var formStructure, @@ -81,3 +89,4 @@ define( return LocationService; } ); + diff --git a/platform/entanglement/src/services/MoveService.js b/platform/entanglement/src/services/MoveService.js index e6e1238979..30c341ef22 100644 --- a/platform/entanglement/src/services/MoveService.js +++ b/platform/entanglement/src/services/MoveService.js @@ -29,74 +29,70 @@ define( * MoveService provides an interface for moving objects from one * location to another. It also provides a method for determining if * an object can be copied to a specific location. + * @constructor + * @memberof platform/entanglement + * @implements {platform/entanglement.AbstractComposeService} */ - function MoveService(policyService, linkService, $q) { - - return { - /** - * Returns `true` if `object` can be moved into - * `parentCandidate`'s composition. - */ - validate: function (object, parentCandidate) { - var currentParent = object - .getCapability('context') - .getParent(); - - if (!parentCandidate || !parentCandidate.getId) { - return false; - } - if (parentCandidate.getId() === currentParent.getId()) { - return false; - } - if (parentCandidate.getId() === object.getId()) { - return false; - } - if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { - return false; - } - return policyService.allow( - "composition", - parentCandidate.getCapability('type'), - object.getCapability('type') - ); - }, - /** - * Move `object` into `parentObject`'s composition. - * - * @returns {Promise} A promise that is fulfilled when the - * move operation has completed. - */ - perform: function (object, parentObject) { - return linkService - .perform(object, parentObject) - .then(function (objectInNewContext) { - var newLocationCapability = objectInNewContext - .getCapability('location'), - oldLocationCapability = object - .getCapability('location'); - if (!newLocationCapability || - !oldLocationCapability) { - - return; - } - - - if (oldLocationCapability.isOriginal()) { - return newLocationCapability.setPrimaryLocation( - newLocationCapability - .getContextualLocation() - ); - } - }) - .then(function () { - return object - .getCapability('action') - .perform('remove'); - }); - } - }; + function MoveService(policyService, linkService) { + this.policyService = policyService; + this.linkService = linkService; } + MoveService.prototype.validate = function (object, parentCandidate) { + var currentParent = object + .getCapability('context') + .getParent(); + + if (!parentCandidate || !parentCandidate.getId) { + return false; + } + if (parentCandidate.getId() === currentParent.getId()) { + return false; + } + if (parentCandidate.getId() === object.getId()) { + return false; + } + if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { + return false; + } + return this.policyService.allow( + "composition", + parentCandidate.getCapability('type'), + object.getCapability('type') + ); + }; + + MoveService.prototype.perform = function (object, parentObject) { + function relocate(objectInNewContext) { + var newLocationCapability = objectInNewContext + .getCapability('location'), + oldLocationCapability = object + .getCapability('location'); + + if (!newLocationCapability || + !oldLocationCapability) { + return; + } + + if (oldLocationCapability.isOriginal()) { + return newLocationCapability.setPrimaryLocation( + newLocationCapability + .getContextualLocation() + ); + } + } + + return this.linkService + .perform(object, parentObject) + .then(relocate) + .then(function () { + return object + .getCapability('action') + .perform('remove'); + }); + }; + return MoveService; } ); + diff --git a/platform/entanglement/test/actions/AbstractComposeActionSpec.js b/platform/entanglement/test/actions/AbstractComposeActionSpec.js new file mode 100644 index 0000000000..5be0604ec3 --- /dev/null +++ b/platform/entanglement/test/actions/AbstractComposeActionSpec.js @@ -0,0 +1,176 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/*global define,describe,beforeEach,it,jasmine,expect */ + +define( + [ + '../../src/actions/AbstractComposeAction', + '../services/MockCopyService', + '../DomainObjectFactory' + ], + function (AbstractComposeAction, MockCopyService, domainObjectFactory) { + "use strict"; + + describe("Move/copy/link Actions", function () { + + var action, + locationService, + locationServicePromise, + composeService, + context, + selectedObject, + selectedObjectContextCapability, + currentParent, + newParent; + + beforeEach(function () { + selectedObjectContextCapability = jasmine.createSpyObj( + 'selectedObjectContextCapability', + [ + 'getParent' + ] + ); + + selectedObject = domainObjectFactory({ + name: 'selectedObject', + model: { + name: 'selectedObject' + }, + capabilities: { + context: selectedObjectContextCapability + } + }); + + currentParent = domainObjectFactory({ + name: 'currentParent' + }); + + selectedObjectContextCapability + .getParent + .andReturn(currentParent); + + newParent = domainObjectFactory({ + name: 'newParent' + }); + + locationService = jasmine.createSpyObj( + 'locationService', + [ + 'getLocationFromUser' + ] + ); + + locationServicePromise = jasmine.createSpyObj( + 'locationServicePromise', + [ + 'then' + ] + ); + + locationService + .getLocationFromUser + .andReturn(locationServicePromise); + + composeService = new MockCopyService(); + }); + + + describe("with context from context-action", function () { + beforeEach(function () { + context = { + domainObject: selectedObject + }; + + action = new AbstractComposeAction( + locationService, + composeService, + context, + "Compose" + ); + }); + + it("initializes happily", function () { + expect(action).toBeDefined(); + }); + + describe("when performed it", function () { + beforeEach(function () { + action.perform(); + }); + + it("prompts for location", function () { + expect(locationService.getLocationFromUser) + .toHaveBeenCalledWith( + "Compose selectedObject to a new location", + "Compose To", + jasmine.any(Function), + currentParent + ); + }); + + it("waits for location from user", function () { + expect(locationServicePromise.then) + .toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it("copies object to selected location", function () { + locationServicePromise + .then + .mostRecentCall + .args[0](newParent); + + expect(composeService.perform) + .toHaveBeenCalledWith(selectedObject, newParent); + }); + }); + }); + + describe("with context from drag-drop", function () { + beforeEach(function () { + context = { + selectedObject: selectedObject, + domainObject: newParent + }; + + action = new AbstractComposeAction( + locationService, + composeService, + context, + "Compose" + ); + }); + + it("initializes happily", function () { + expect(action).toBeDefined(); + }); + + + it("performs copy immediately", function () { + action.perform(); + expect(composeService.perform) + .toHaveBeenCalledWith(selectedObject, newParent); + }); + }); + }); + } +); diff --git a/platform/entanglement/test/suite.json b/platform/entanglement/test/suite.json index 5c7f786174..fe3c32cbef 100644 --- a/platform/entanglement/test/suite.json +++ b/platform/entanglement/test/suite.json @@ -1,7 +1,5 @@ [ - "actions/CopyAction", - "actions/LinkAction", - "actions/MoveAction", + "actions/AbstractComposeAction", "services/CopyService", "services/LinkService", "services/MoveService", diff --git a/platform/execution/src/WorkerService.js b/platform/execution/src/WorkerService.js index b8f24ee614..68eb171b0f 100644 --- a/platform/execution/src/WorkerService.js +++ b/platform/execution/src/WorkerService.js @@ -21,6 +21,12 @@ *****************************************************************************/ /*global define*/ + +/** + * This bundle contains services for managing the flow of execution, + * such as support for running web workers on background threads. + * @namespace platform/execution + */ define( [], function () { @@ -28,11 +34,11 @@ define( /** * Handles the execution of WebWorkers. + * @memberof platform/execution * @constructor */ function WorkerService($window, workers) { - var workerUrls = {}, - Worker = $window.Worker; + var workerUrls = {}; function addWorker(worker) { var key = worker.key; @@ -46,23 +52,25 @@ define( } (workers || []).forEach(addWorker); - - return { - /** - * Start running a new web worker. This will run a worker - * that has been registered under the `workers` category - * of extension. - * - * @param {string} key symbolic identifier for the worker - * @returns {Worker} the running Worker - */ - run: function (key) { - var scriptUrl = workerUrls[key]; - return scriptUrl && Worker && new Worker(scriptUrl); - } - }; + this.workerUrls = workerUrls; + this.Worker = $window.Worker; } + /** + * Start running a new web worker. This will run a worker + * that has been registered under the `workers` category + * of extension. + * + * @param {string} key symbolic identifier for the worker + * @returns {Worker} the running Worker + */ + WorkerService.prototype.run = function (key) { + var scriptUrl = this.workerUrls[key], + Worker = this.Worker; + return scriptUrl && Worker && new Worker(scriptUrl); + }; + return WorkerService; } ); + diff --git a/platform/features/events/src/DomainColumn.js b/platform/features/events/src/DomainColumn.js index 95a6222553..5ea23fa804 100644 --- a/platform/features/events/src/DomainColumn.js +++ b/platform/features/events/src/DomainColumn.js @@ -33,7 +33,9 @@ define( * A column which will report telemetry domain values * (typically, timestamps.) Used by the ScrollingListController. * + * @memberof platform/features/events * @constructor + * @implements {platform/features/events.EventsColumn} * @param domainMetadata an object with the machine- and human- * readable names for this domain (in `key` and `name` * fields, respectively.) @@ -41,27 +43,21 @@ define( * formatting service, for making values human-readable. */ function DomainColumn(domainMetadata, telemetryFormatter) { - return { - /** - * Get the title to display in this column's header. - * @returns {string} the title to display - */ - getTitle: function () { - return domainMetadata.name; - }, - /** - * Get the text to display inside a row under this - * column. - * @returns {string} the text to display - */ - getValue: function (domainObject, data, index) { - return telemetryFormatter.formatDomainValue( - data.getDomainValue(index, domainMetadata.key) - ); - } - }; + this.domainMetadata = domainMetadata; + this.telemetryFormatter = telemetryFormatter; } + DomainColumn.prototype.getTitle = function () { + return this.domainMetadata.name; + }; + + DomainColumn.prototype.getValue = function (domainObject, data, index) { + var domainKey = this.domainMetadata.key; + return this.telemetryFormatter.formatDomainValue( + data.getDomainValue(index, domainKey) + ); + }; + return DomainColumn; } -); \ No newline at end of file +); diff --git a/platform/features/events/src/EventListController.js b/platform/features/events/src/EventListController.js index 4b90c91b8e..dbb76712bd 100644 --- a/platform/features/events/src/EventListController.js +++ b/platform/features/events/src/EventListController.js @@ -21,11 +21,16 @@ *****************************************************************************/ /*global define*/ -/** +/* * Module defining EventListController. * Created by chacskaylo on 06/18/2015. * Modified by shale on 06/23/2015. */ + +/** + * This bundle implements the "Events" view of string telemetry. + * @namespace platform/features/events + */ define( ["./DomainColumn", "./RangeColumn", "./EventListPopulator"], function (DomainColumn, RangeColumn, EventListPopulator) { @@ -36,6 +41,7 @@ define( /** * The EventListController is responsible for populating * the contents of the event list view. + * @memberof platform/features/events * @constructor */ function EventListController($scope, formatter) { @@ -129,5 +135,30 @@ define( } return EventListController; + + /** + * A description of how to display a certain column of data in an + * Events view. + * @interface platform/features/events.EventColumn + * @private + */ + /** + * Get the title to display in this column's header. + * @returns {string} the title to display + * @method platform/features/events.EventColumn#getTitle + */ + /** + * Get the text to display inside a row under this + * column. + * @param {DomainObject} domainObject the domain object associated + * with this row + * @param {TelemetrySeries} series the telemetry data associated + * with this row + * @param {number} index the index of the telemetry datum associated + * with this row + * @returns {string} the text to display + * @method platform/features/events.EventColumn#getValue + */ } ); + diff --git a/platform/features/events/src/EventListPopulator.js b/platform/features/events/src/EventListPopulator.js index 3999fb1ebc..fca5bea74d 100644 --- a/platform/features/events/src/EventListPopulator.js +++ b/platform/features/events/src/EventListPopulator.js @@ -31,131 +31,135 @@ define( * values which should appear within columns of a event list * view, based on received telemetry data. * @constructor + * @memberof platform/features/events * @param {Column[]} columns the columns to be populated */ function EventListPopulator(columns) { - /** - * Look up the most recent values from a set of data objects. - * Returns an array of objects in the order in which data - * should be displayed; each element is an object with - * two properties: - * - * * objectIndex: The index of the domain object associated - * with the data point to be displayed in that - * row. - * * pointIndex: The index of the data point itself, within - * its data set. - * - * @param {Array} datas an array of the most recent - * data objects; expected to be in the same order - * as the domain objects provided at constructor - * @param {number} count the number of rows to provide - */ - function getLatestDataValues(datas, count) { - var latest = [], - candidate, - candidateTime, - used = datas.map(function () { return 0; }); + this.columns = columns; + } - // This algorithm is O(nk) for n rows and k telemetry elements; - // one O(k) linear search for a max is made for each of n rows. - // This could be done in O(n lg k + k lg k), using a priority - // queue (where priority is max-finding) containing k initial - // values. For n rows, pop the max from the queue and replenish - // the queue with a value from the data at the same - // objectIndex, if available. - // But k is small, so this might not give an observable - // improvement in performance. + /* + * Look up the most recent values from a set of data objects. + * Returns an array of objects in the order in which data + * should be displayed; each element is an object with + * two properties: + * + * * objectIndex: The index of the domain object associated + * with the data point to be displayed in that + * row. + * * pointIndex: The index of the data point itself, within + * its data set. + * + * @param {Array} datas an array of the most recent + * data objects; expected to be in the same order + * as the domain objects provided at constructor + * @param {number} count the number of rows to provide + */ + function getLatestDataValues(datas, count) { + var latest = [], + candidate, + candidateTime, + used = datas.map(function () { return 0; }); - // Find the most recent unused data point (this will be used - // in a loop to find and the N most recent data points) - function findCandidate(data, i) { - var nextTime, - pointCount = data.getPointCount(), - pointIndex = pointCount - used[i] - 1; - if (data && pointIndex >= 0) { - nextTime = data.getDomainValue(pointIndex); - if (nextTime > candidateTime) { - candidateTime = nextTime; - candidate = { - objectIndex: i, - pointIndex: pointIndex - }; - } + // This algorithm is O(nk) for n rows and k telemetry elements; + // one O(k) linear search for a max is made for each of n rows. + // This could be done in O(n lg k + k lg k), using a priority + // queue (where priority is max-finding) containing k initial + // values. For n rows, pop the max from the queue and replenish + // the queue with a value from the data at the same + // objectIndex, if available. + // But k is small, so this might not give an observable + // improvement in performance. + + // Find the most recent unused data point (this will be used + // in a loop to find and the N most recent data points) + function findCandidate(data, i) { + var nextTime, + pointCount = data.getPointCount(), + pointIndex = pointCount - used[i] - 1; + if (data && pointIndex >= 0) { + nextTime = data.getDomainValue(pointIndex); + if (nextTime > candidateTime) { + candidateTime = nextTime; + candidate = { + objectIndex: i, + pointIndex: pointIndex + }; } } - - // Assemble a list of the most recent data points - while (latest.length < count) { - // Reset variables pre-search - candidateTime = Number.NEGATIVE_INFINITY; - candidate = undefined; - - // Linear search for most recent - datas.forEach(findCandidate); - - if (candidate) { - // Record this data point - it is the most recent - latest.push(candidate); - - // Track the data points used so we can look farther back - // in the data set on the next iteration - used[candidate.objectIndex] = used[candidate.objectIndex] + 1; - } else { - // Ran out of candidates; not enough data points - // available to fill all rows. - break; - } - } - - return latest; } + // Assemble a list of the most recent data points + while (latest.length < count) { + // Reset variables pre-search + candidateTime = Number.NEGATIVE_INFINITY; + candidate = undefined; - return { - /** - * Get the text which should appear in headers for the - * provided columns. - * @returns {string[]} column headers - */ - getHeaders: function () { - return columns.map(function (column) { - return column.getTitle(); - }); - }, - /** - * Get the contents of rows for the event list view. - * @param {TelemetrySeries[]} datas the data sets - * @param {DomainObject[]} objects the domain objects which - * provided the data sets; these should match - * index-to-index with the `datas` argument - * @param {number} count the number of rows to populate - * @returns {string[][]} an array of rows, each of which - * is an array of values which should appear - * in that row - */ - getRows: function (datas, objects, count) { - var values = getLatestDataValues(datas, count); + // Linear search for most recent + datas.forEach(findCandidate); - // Each value will become a row, which will contain - // some value in each column (rendering by the - // column object itself) - // Additionally, we want to display the rows in reverse - // order. (i.e. from the top to the bottom of the page) - return values.map(function (value) { - return columns.map(function (column) { - return column.getValue( - objects[value.objectIndex], - datas[value.objectIndex], - value.pointIndex - ); - }); - }).reverse(); + if (candidate) { + // Record this data point - it is the most recent + latest.push(candidate); + + // Track the data points used so we can look farther back + // in the data set on the next iteration + used[candidate.objectIndex] = used[candidate.objectIndex] + 1; + } else { + // Ran out of candidates; not enough data points + // available to fill all rows. + break; } - }; + } + + return latest; } + /** + * Get the text which should appear in headers for the + * provided columns. + * @memberof platform/features/events.EventListPopulator + * @returns {string[]} column headers + */ + EventListPopulator.prototype.getHeaders = function () { + return this.columns.map(function (column) { + return column.getTitle(); + }); + }; + + /** + * Get the contents of rows for the event list view. + * @param {TelemetrySeries[]} datas the data sets + * @param {DomainObject[]} objects the domain objects which + * provided the data sets; these should match + * index-to-index with the `datas` argument + * @param {number} count the number of rows to populate + * @memberof platform/features/events.EventListPopulator + * @returns {string[][]} an array of rows, each of which + * is an array of values which should appear + * in that row + */ + EventListPopulator.prototype.getRows = function (datas, objects, count) { + var values = getLatestDataValues(datas, count), + columns = this.columns; + + // Each value will become a row, which will contain + // some value in each column (rendering by the + // column object itself) + // Additionally, we want to display the rows in reverse + // order. (i.e. from the top to the bottom of the page) + return values.map(function (value) { + return columns.map(function (column) { + return column.getValue( + objects[value.objectIndex], + datas[value.objectIndex], + value.pointIndex + ); + }); + }).reverse(); + }; + return EventListPopulator; } -); \ No newline at end of file +); diff --git a/platform/features/events/src/RangeColumn.js b/platform/features/events/src/RangeColumn.js index 2b11de43c7..f31e1d9550 100644 --- a/platform/features/events/src/RangeColumn.js +++ b/platform/features/events/src/RangeColumn.js @@ -33,7 +33,9 @@ define( * A column which will report telemetry range values * (typically, measurements.) Used by the ScrollingListController. * + * @memberof platform/features/events * @constructor + * @implements {platform/features/events.EventsColumn} * @param rangeMetadata an object with the machine- and human- * readable names for this range (in `key` and `name` * fields, respectively.) @@ -41,27 +43,20 @@ define( * formatting service, for making values human-readable. */ function RangeColumn(rangeMetadata, telemetryFormatter) { - return { - /** - * Get the title to display in this column's header. - * @returns {string} the title to display - */ - getTitle: function () { - return rangeMetadata.name; - }, - /** - * Get the text to display inside a row under this - * column. - * @returns {string} the text to display - */ - getValue: function (domainObject, data, index) { - return telemetryFormatter.formatRangeValue( - data.getRangeValue(index, rangeMetadata.key) - ); - } - }; + this.rangeMetadata = rangeMetadata; + this.telemetryFormatter = telemetryFormatter; } + RangeColumn.prototype.getTitle = function () { + return this.rangeMetadata.name; + }; + RangeColumn.prototype.getValue = function (domainObject, data, index) { + var rangeKey = this.rangeMetadata.key; + return this.telemetryFormatter.formatRangeValue( + data.getRangeValue(index, rangeKey) + ); + }; + return RangeColumn; } -); \ No newline at end of file +); diff --git a/platform/features/events/src/directives/MCTDataTable.js b/platform/features/events/src/directives/MCTDataTable.js index c4cb9970e6..e830fbe885 100644 --- a/platform/features/events/src/directives/MCTDataTable.js +++ b/platform/features/events/src/directives/MCTDataTable.js @@ -71,4 +71,4 @@ define( return MCTDataTable; } -); \ No newline at end of file +); diff --git a/platform/features/events/src/policies/MessagesViewPolicy.js b/platform/features/events/src/policies/MessagesViewPolicy.js index 4426872b1e..712992c0a2 100644 --- a/platform/features/events/src/policies/MessagesViewPolicy.js +++ b/platform/features/events/src/policies/MessagesViewPolicy.js @@ -31,44 +31,37 @@ define( /** * Policy controlling when the Messages view should be avaliable. + * @memberof platform/features/events * @constructor + * @implements {Policy.} */ - function MessagesViewPolicy() { - - function hasStringTelemetry(domainObject) { - var telemetry = domainObject && - domainObject.getCapability('telemetry'), - metadata = telemetry ? telemetry.getMetadata() : {}, - ranges = metadata.ranges || []; + function MessagesViewPolicy() {} - return ranges.some(function (range) { - return range.format === 'string'; - }); - } - return { - /** - * Check whether or not a given action is allowed by this - * policy. - * @param {Action} action the action - * @param domainObject the domain object which will be viewed - * @returns {boolean} true if not disallowed - */ - allow: function (view, domainObject) { - // This policy only applies for the Messages view - if (view.key === 'messages') { - // The Messages view is allowed only if the domain - // object has string telemetry - if (!hasStringTelemetry(domainObject)) { - return false; - } - } - - // Like all policies, allow by default. - return true; - } - }; + function hasStringTelemetry(domainObject) { + var telemetry = domainObject && + domainObject.getCapability('telemetry'), + metadata = telemetry ? telemetry.getMetadata() : {}, + ranges = metadata.ranges || []; + + return ranges.some(function (range) { + return range.format === 'string'; + }); } + MessagesViewPolicy.prototype.allow = function (view, domainObject) { + // This policy only applies for the Messages view + if (view.key === 'messages') { + // The Messages view is allowed only if the domain + // object has string telemetry + if (!hasStringTelemetry(domainObject)) { + return false; + } + } + + // Like all policies, allow by default. + return true; + }; + return MessagesViewPolicy; } -); \ No newline at end of file +); diff --git a/platform/features/imagery/src/controllers/ImageryController.js b/platform/features/imagery/src/controllers/ImageryController.js index 72f72f39db..5439fc414c 100644 --- a/platform/features/imagery/src/controllers/ImageryController.js +++ b/platform/features/imagery/src/controllers/ImageryController.js @@ -21,6 +21,10 @@ *****************************************************************************/ /*global define*/ +/** + * This bundle implements views of image telemetry. + * @namespace platform/features/imagery + */ define( ['moment'], function (moment) { @@ -32,42 +36,34 @@ define( /** * Controller for the "Imagery" view of a domain object which * provides image telemetry. + * @constructor + * @memberof platform/features/imagery */ function ImageryController($scope, telemetryHandler) { - var date = "", - time = "", - imageUrl = "", - paused = false, - handle; + var self = this; function releaseSubscription() { - if (handle) { - handle.unsubscribe(); - handle = undefined; + if (self.handle) { + self.handle.unsubscribe(); + self.handle = undefined; } } - function updateValues() { - var imageObject = handle && handle.getTelemetryObjects()[0], - m; - if (imageObject && !paused) { - m = moment.utc(handle.getDomainValue(imageObject)); - date = m.format(DATE_FORMAT); - time = m.format(TIME_FORMAT); - imageUrl = handle.getRangeValue(imageObject); - } + function updateValuesCallback() { + return self.updateValues(); } // Create a new subscription; telemetrySubscriber gets // to do the meaningful work here. function subscribe(domainObject) { releaseSubscription(); - date = ""; - time = ""; - imageUrl = ""; - handle = domainObject && telemetryHandler.handle( + self.date = ""; + self.time = ""; + self.zone = ""; + self.imageUrl = ""; + self.handle = domainObject && telemetryHandler.handle( domainObject, - updateValues, + updateValuesCallback, true // Lossless ); } @@ -77,57 +73,78 @@ define( // Unsubscribe when the plot is destroyed $scope.$on("$destroy", releaseSubscription); - - return { - /** - * Get the time portion (hours, minutes, seconds) of the - * timestamp associated with the incoming image telemetry. - * @returns {string} the time - */ - getTime: function () { - return time; - }, - /** - * Get the date portion (month, year) of the - * timestamp associated with the incoming image telemetry. - * @returns {string} the date - */ - getDate: function () { - return date; - }, - /** - * Get the time zone for the displayed time/date corresponding - * to the timestamp associated with the incoming image - * telemetry. - * @returns {string} the time - */ - getZone: function () { - return "UTC"; - }, - /** - * Get the URL of the image telemetry to display. - * @returns {string} URL for telemetry image - */ - getImageUrl: function () { - return imageUrl; - }, - /** - * Getter-setter for paused state of the view (true means - * paused, false means not.) - * @param {boolean} [state] the state to set - * @returns {boolean} the current state - */ - paused: function (state) { - if (arguments.length > 0 && state !== paused) { - paused = state; - // Switch to latest image - updateValues(); - } - return paused; - } - }; } + // Update displayable values to reflect latest image telemetry + ImageryController.prototype.updateValues = function () { + var imageObject = + this.handle && this.handle.getTelemetryObjects()[0], + timestamp, + m; + if (imageObject && !this.isPaused) { + timestamp = this.handle.getDomainValue(imageObject); + m = timestamp !== undefined ? + moment.utc(timestamp) : + undefined; + this.date = m ? m.format(DATE_FORMAT) : ""; + this.time = m ? m.format(TIME_FORMAT) : ""; + this.zone = m ? "UTC" : ""; + this.imageUrl = this.handle.getRangeValue(imageObject); + } + }; + + /** + * Get the time portion (hours, minutes, seconds) of the + * timestamp associated with the incoming image telemetry. + * @returns {string} the time + */ + ImageryController.prototype.getTime = function () { + return this.time; + }; + + /** + * Get the date portion (month, year) of the + * timestamp associated with the incoming image telemetry. + * @returns {string} the date + */ + ImageryController.prototype.getDate = function () { + return this.date; + }; + + /** + * Get the time zone for the displayed time/date corresponding + * to the timestamp associated with the incoming image + * telemetry. + * @returns {string} the time + */ + ImageryController.prototype.getZone = function () { + return this.zone; + }; + + /** + * Get the URL of the image telemetry to display. + * @returns {string} URL for telemetry image + */ + ImageryController.prototype.getImageUrl = function () { + return this.imageUrl; + }; + + /** + * Getter-setter for paused state of the view (true means + * paused, false means not.) + * @param {boolean} [state] the state to set + * @returns {boolean} the current state + */ + ImageryController.prototype.paused = function (state) { + if (arguments.length > 0 && state !== this.isPaused) { + this.isPaused = state; + // Switch to latest image + this.updateValues(); + } + return this.isPaused; + }; + return ImageryController; } ); + diff --git a/platform/features/imagery/src/directives/MCTBackgroundImage.js b/platform/features/imagery/src/directives/MCTBackgroundImage.js index 3218910db5..9100dce4b8 100644 --- a/platform/features/imagery/src/directives/MCTBackgroundImage.js +++ b/platform/features/imagery/src/directives/MCTBackgroundImage.js @@ -34,6 +34,8 @@ define( * * If `src` is falsy, no image will be displayed (immediately.) * + * @constructor + * @memberof platform/features/imagery */ function MCTBackgroundImage($document) { function link(scope, element, attrs) { @@ -87,3 +89,4 @@ define( return MCTBackgroundImage; } ); + diff --git a/platform/features/imagery/src/policies/ImageryViewPolicy.js b/platform/features/imagery/src/policies/ImageryViewPolicy.js index 40b74d96df..f9f2e9c074 100644 --- a/platform/features/imagery/src/policies/ImageryViewPolicy.js +++ b/platform/features/imagery/src/policies/ImageryViewPolicy.js @@ -28,32 +28,33 @@ define( /** * Policy preventing the Imagery view from being made available for * domain objects which do not have associated image telemetry. - * @implements {Policy} + * @implements {Policy.} + * @constructor */ function ImageryViewPolicy() { - function hasImageTelemetry(domainObject) { - var telemetry = domainObject && - domainObject.getCapability('telemetry'), - metadata = telemetry ? telemetry.getMetadata() : {}, - ranges = metadata.ranges || []; + } - return ranges.some(function (range) { - return range.format === 'imageUrl' || - range.format === 'image'; - }); + function hasImageTelemetry(domainObject) { + var telemetry = domainObject && + domainObject.getCapability('telemetry'), + metadata = telemetry ? telemetry.getMetadata() : {}, + ranges = metadata.ranges || []; + + return ranges.some(function (range) { + return range.format === 'imageUrl' || + range.format === 'image'; + }); + } + + ImageryViewPolicy.prototype.allow = function (view, domainObject) { + if (view.key === 'imagery') { + return hasImageTelemetry(domainObject); } - return { - allow: function (view, domainObject) { - if (view.key === 'imagery') { - return hasImageTelemetry(domainObject); - } - - return true; - } - }; - } + return true; + }; return ImageryViewPolicy; } ); + diff --git a/platform/features/imagery/test/controllers/ImageryControllerSpec.js b/platform/features/imagery/test/controllers/ImageryControllerSpec.js index 9bb00d6b20..6ae82b3d86 100644 --- a/platform/features/imagery/test/controllers/ImageryControllerSpec.js +++ b/platform/features/imagery/test/controllers/ImageryControllerSpec.js @@ -146,6 +146,19 @@ define( expect(controller.getImageUrl()).toEqual(testUrl); }); + it("initially shows an empty string for date/time", function () { + // Call the subscription listener while domain/range + // values are still undefined + mockHandle.getDomainValue.andReturn(undefined); + mockHandle.getRangeValue.andReturn(undefined); + mockTelemetryHandler.handle.mostRecentCall.args[1](); + + // Should have empty strings for date/time/zone + expect(controller.getTime()).toEqual(""); + expect(controller.getDate()).toEqual(""); + expect(controller.getZone()).toEqual(""); + expect(controller.getImageUrl()).toBeUndefined(); + }); }); } ); diff --git a/platform/features/layout/src/FixedController.js b/platform/features/layout/src/FixedController.js index 42e6b6dbce..410c0d2f94 100644 --- a/platform/features/layout/src/FixedController.js +++ b/platform/features/layout/src/FixedController.js @@ -34,24 +34,21 @@ define( * Fixed Position view. It arranges frames according to saved * configuration and provides methods for updating these based on * mouse movement. + * @memberof platform/features/layout * @constructor * @param {Scope} $scope the controller's Angular scope */ function FixedController($scope, $q, dialogService, telemetrySubscriber, telemetryFormatter) { - var gridSize = DEFAULT_GRID_SIZE, - dragging, + var self = this, subscription, - elementProxies = [], names = {}, // Cache names by ID values = {}, // Cache values by ID - elementProxiesById = {}, - handles = [], - moveHandle, - selection; + elementProxiesById = {}; // Convert from element x/y/width/height to an - // apropriate ng-style argument, to position elements. + // appropriate ng-style argument, to position elements. function convertPosition(elementProxy) { + var gridSize = self.gridSize; // Multiply position/dimensions by grid size return { left: (gridSize[0] * elementProxy.x()) + 'px', @@ -63,7 +60,7 @@ define( // Update the style for a selected element function updateSelectionStyle() { - var element = selection && selection.get(); + var element = self.selection && self.selection.get(); if (element) { element.style = convertPosition(element); } @@ -73,7 +70,7 @@ define( function generateDragHandle(elementHandle) { return new FixedDragHandle( elementHandle, - gridSize, + self.gridSize, updateSelectionStyle, $scope.commit ); @@ -84,17 +81,6 @@ define( return element.handles().map(generateDragHandle); } - // Select an element - function select(element) { - if (selection) { - // Update selection... - selection.select(element); - // ...as well as move, resize handles - moveHandle = generateDragHandle(element); - handles = generateDragHandles(element); - } - } - // Update the displayed value for this object function updateValue(telemetryObject) { var id = telemetryObject && telemetryObject.getId(), @@ -120,9 +106,9 @@ define( // Update element positions when grid size changes function updateElementPositions(layoutGrid) { // Update grid size from model - gridSize = layoutGrid || DEFAULT_GRID_SIZE; + self.gridSize = layoutGrid || DEFAULT_GRID_SIZE; - elementProxies.forEach(function (elementProxy) { + self.elementProxies.forEach(function (elementProxy) { elementProxy.style = convertPosition(elementProxy); }); } @@ -153,7 +139,7 @@ define( function refreshElements() { // Cache selection; we are instantiating new proxies // so we may want to restore this. - var selected = selection && selection.get(), + var selected = self.selection && self.selection.get(), elements = (($scope.configuration || {}).elements || []), index = -1; // Start with a 'not-found' value @@ -163,20 +149,20 @@ define( } // Create the new proxies... - elementProxies = elements.map(makeProxyElement); + self.elementProxies = elements.map(makeProxyElement); // Clear old selection, and restore if appropriate - if (selection) { - selection.deselect(); + if (self.selection) { + self.selection.deselect(); if (index > -1) { - select(elementProxies[index]); + self.select(self.elementProxies[index]); } } // Finally, rebuild lists of elements by id to // facilitate faster update when new telemetry comes in. elementProxiesById = {}; - elementProxies.forEach(function (elementProxy) { + self.elementProxies.forEach(function (elementProxy) { var id = elementProxy.id; if (elementProxy.element.type === 'fixed.telemetry') { // Provide it a cached name/value to avoid flashing @@ -231,7 +217,9 @@ define( // Refresh displayed elements refreshElements(); // Select the newly-added element - select(elementProxies[elementProxies.length - 1]); + self.select( + self.elementProxies[self.elementProxies.length - 1] + ); // Mark change as persistable if ($scope.commit) { $scope.commit("Dropped an element."); @@ -248,8 +236,8 @@ define( // Store the position of this element. addElement({ type: "fixed.telemetry", - x: Math.floor(position.x / gridSize[0]), - y: Math.floor(position.y / gridSize[1]), + x: Math.floor(position.x / self.gridSize[0]), + y: Math.floor(position.y / self.gridSize[1]), id: id, stroke: "transparent", color: "#cccccc", @@ -259,12 +247,17 @@ define( }); } + this.gridSize = DEFAULT_GRID_SIZE; + this.elementProxies = []; + this.generateDragHandle = generateDragHandle; + this.generateDragHandles = generateDragHandles; + // Track current selection state - selection = $scope.selection; + this.selection = $scope.selection; // Expose the view's selection proxy - if (selection) { - selection.proxy(new FixedProxy(addElement, $q, dialogService)); + if (this.selection) { + this.selection.proxy(new FixedProxy(addElement, $q, dialogService)); } // Refresh list of elements whenever model changes @@ -284,66 +277,81 @@ define( // Position panes where they are dropped $scope.$on("mctDrop", handleDrop); - - return { - /** - * Get the size of the grid, in pixels. The returned array - * is in the form `[x, y]`. - * @returns {number[]} the grid size - */ - getGridSize: function () { - return gridSize; - }, - /** - * Get an array of elements in this panel; these are - * decorated proxies for both selection and display. - * @returns {Array} elements in this panel - */ - getElements: function () { - return elementProxies; - }, - /** - * Check if the element is currently selected, or (if no - * argument is supplied) get the currently selected element. - * @returns {boolean} true if selected - */ - selected: function (element) { - return selection && ((arguments.length > 0) ? - selection.selected(element) : selection.get()); - }, - /** - * Set the active user selection in this view. - * @param element the element to select - */ - select: select, - /** - * Clear the current user selection. - */ - clearSelection: function () { - if (selection) { - selection.deselect(); - handles = []; - moveHandle = undefined; - } - }, - /** - * Get drag handles. - * @returns {Array} drag handles for the current selection - */ - handles: function () { - return handles; - }, - /** - * Get the handle to handle dragging to reposition an element. - * @returns {FixedDragHandle} the drag handle - */ - moveHandle: function () { - return moveHandle; - } - }; - } + /** + * Get the size of the grid, in pixels. The returned array + * is in the form `[x, y]`. + * @returns {number[]} the grid size + * @memberof platform/features/layout.FixedController# + */ + FixedController.prototype.getGridSize = function () { + return this.gridSize; + }; + + /** + * Get an array of elements in this panel; these are + * decorated proxies for both selection and display. + * @returns {Array} elements in this panel + */ + FixedController.prototype.getElements = function () { + return this.elementProxies; + }; + + /** + * Check if the element is currently selected, or (if no + * argument is supplied) get the currently selected element. + * @returns {boolean} true if selected + */ + FixedController.prototype.selected = function (element) { + var selection = this.selection; + return selection && ((arguments.length > 0) ? + selection.selected(element) : selection.get()); + }; + + /** + * Set the active user selection in this view. + * @param element the element to select + */ + FixedController.prototype.select = function select(element) { + if (this.selection) { + // Update selection... + this.selection.select(element); + // ...as well as move, resize handles + this.mvHandle = this.generateDragHandle(element); + this.resizeHandles = this.generateDragHandles(element); + } + }; + + /** + * Clear the current user selection. + */ + FixedController.prototype.clearSelection = function () { + if (this.selection) { + this.selection.deselect(); + this.resizeHandles = []; + this.mvHandle = undefined; + } + }; + + /** + * Get drag handles. + * @returns {platform/features/layout.FixedDragHandle[]} + * drag handles for the current selection + */ + FixedController.prototype.handles = function () { + return this.resizeHandles; + }; + + /** + * Get the handle to handle dragging to reposition an element. + * @returns {platform/features/layout.FixedDragHandle} the drag handle + */ + FixedController.prototype.moveHandle = function () { + return this.mvHandle; + }; + return FixedController; } ); + diff --git a/platform/features/layout/src/FixedDragHandle.js b/platform/features/layout/src/FixedDragHandle.js index 7c26e8320b..afc98eabaf 100644 --- a/platform/features/layout/src/FixedDragHandle.js +++ b/platform/features/layout/src/FixedDragHandle.js @@ -33,85 +33,81 @@ define( /** * Template-displayable drag handle for an element in fixed * position mode. + * @memberof platform/features/layout * @constructor */ function FixedDragHandle(elementHandle, gridSize, update, commit) { - var self = {}, - dragging; - - // Generate ng-style-appropriate style for positioning - function getStyle() { - // Adjust from grid to pixel coordinates - var x = elementHandle.x() * gridSize[0], - y = elementHandle.y() * gridSize[1]; - - // Convert to a CSS style centered on that point - return { - left: (x - DRAG_HANDLE_SIZE[0] / 2) + 'px', - top: (y - DRAG_HANDLE_SIZE[1] / 2) + 'px', - width: DRAG_HANDLE_SIZE[0] + 'px', - height: DRAG_HANDLE_SIZE[1] + 'px' - }; - } - - // Begin a drag gesture - function startDrag() { - // Cache initial x/y positions - dragging = { x: elementHandle.x(), y: elementHandle.y() }; - } - - // Reposition during drag - function continueDrag(delta) { - if (dragging) { - // Update x/y positions (snapping to grid) - elementHandle.x( - dragging.x + Math.round(delta[0] / gridSize[0]) - ); - elementHandle.y( - dragging.y + Math.round(delta[1] / gridSize[1]) - ); - // Invoke update callback - if (update) { - update(); - } - } - } - - // Conclude a drag gesture - function endDrag() { - // Clear cached state - dragging = undefined; - // Mark change as complete - if (commit) { - commit("Dragged handle."); - } - } - - return { - /** - * Get a CSS style to position this drag handle. - * @returns CSS style object (for `ng-style`) - */ - style: getStyle, - /** - * Start a drag gesture. This should be called when a drag - * begins to track initial state. - */ - startDrag: startDrag, - /** - * Continue a drag gesture; update x/y positions. - * @param {number[]} delta x/y pixel difference since drag - * started - */ - continueDrag: continueDrag, - /** - * End a drag gesture. This should be callled when a drag - * concludes to trigger commit of changes. - */ - endDrag: endDrag - }; + this.elementHandle = elementHandle; + this.gridSize = gridSize; + this.update = update; + this.commit = commit; } + /** + * Get a CSS style to position this drag handle. + * @returns CSS style object (for `ng-style`) + * @memberof platform/features/layout.FixedDragHandle# + */ + FixedDragHandle.prototype.style = function () { + // Adjust from grid to pixel coordinates + var x = this.elementHandle.x() * this.gridSize[0], + y = this.elementHandle.y() * this.gridSize[1]; + + // Convert to a CSS style centered on that point + return { + left: (x - DRAG_HANDLE_SIZE[0] / 2) + 'px', + top: (y - DRAG_HANDLE_SIZE[1] / 2) + 'px', + width: DRAG_HANDLE_SIZE[0] + 'px', + height: DRAG_HANDLE_SIZE[1] + 'px' + }; + }; + + /** + * Start a drag gesture. This should be called when a drag + * begins to track initial state. + */ + FixedDragHandle.prototype.startDrag = function startDrag() { + // Cache initial x/y positions + this.dragging = { + x: this.elementHandle.x(), + y: this.elementHandle.y() + }; + }; + + /** + * Continue a drag gesture; update x/y positions. + * @param {number[]} delta x/y pixel difference since drag + * started + */ + FixedDragHandle.prototype.continueDrag = function (delta) { + if (this.dragging) { + // Update x/y positions (snapping to grid) + this.elementHandle.x( + this.dragging.x + Math.round(delta[0] / this.gridSize[0]) + ); + this.elementHandle.y( + this.dragging.y + Math.round(delta[1] / this.gridSize[1]) + ); + // Invoke update callback + if (this.update) { + this.update(); + } + } + }; + + /** + * End a drag gesture. This should be callled when a drag + * concludes to trigger commit of changes. + */ + FixedDragHandle.prototype.endDrag = function () { + // Clear cached state + this.dragging = undefined; + // Mark change as complete + if (this.commit) { + this.commit("Dragged handle."); + } + }; + return FixedDragHandle; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/FixedProxy.js b/platform/features/layout/src/FixedProxy.js index f78b8711e9..bb6c2f16c8 100644 --- a/platform/features/layout/src/FixedProxy.js +++ b/platform/features/layout/src/FixedProxy.js @@ -28,6 +28,7 @@ define( /** * Proxy for configuring a fixed position view via the toolbar. + * @memberof platform/features/layout * @constructor * @param {Function} addElementCallback callback to invoke when * elements are created @@ -36,32 +37,41 @@ define( * when adding a new element will require user input */ function FixedProxy(addElementCallback, $q, dialogService) { - var factory = new ElementFactory(dialogService); - - return { - /** - * Add a new visual element to this view. - */ - add: function (type) { - // Place a configured element into the view configuration - function addElement(element) { - // Configure common properties of the element - element.x = element.x || 0; - element.y = element.y || 0; - element.width = element.width || 1; - element.height = element.height || 1; - element.type = type; - - // Finally, add it to the view's configuration - addElementCallback(element); - } - - // Defer creation to the factory - $q.when(factory.createElement(type)).then(addElement); - } - }; + this.factory = new ElementFactory(dialogService); + this.$q = $q; + this.addElementCallback = addElementCallback; } + /** + * Add a new visual element to this view. Supported types are: + * + * * `fixed.image` + * * `fixed.box` + * * `fixed.text` + * * `fixed.line` + * + * @param {string} type the type of element to add + */ + FixedProxy.prototype.add = function (type) { + var addElementCallback = this.addElementCallback; + + // Place a configured element into the view configuration + function addElement(element) { + // Configure common properties of the element + element.x = element.x || 0; + element.y = element.y || 0; + element.width = element.width || 1; + element.height = element.height || 1; + element.type = type; + + // Finally, add it to the view's configuration + addElementCallback(element); + } + + // Defer creation to the factory + this.$q.when(this.factory.createElement(type)).then(addElement); + }; + return FixedProxy; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/LayoutCompositionPolicy.js b/platform/features/layout/src/LayoutCompositionPolicy.js index 2c46484498..23d5a4f882 100644 --- a/platform/features/layout/src/LayoutCompositionPolicy.js +++ b/platform/features/layout/src/LayoutCompositionPolicy.js @@ -29,24 +29,23 @@ define( /** * Defines composition policy for Display Layout objects. * They cannot contain folders. + * @constructor + * @memberof platform/features/layout + * @implements {Policy.} */ function LayoutCompositionPolicy() { - return { - /** - * Is the type identified by the candidate allowed to - * contain the type described by the context? - */ - allow: function (candidate, context) { - var isFolderInLayout = - candidate && - context && - candidate.instanceOf('layout') && - context.instanceOf('folder'); - return !isFolderInLayout; - } - }; } + LayoutCompositionPolicy.prototype.allow = function (candidate, context) { + var isFolderInLayout = + candidate && + context && + candidate.instanceOf('layout') && + context.instanceOf('folder'); + return !isFolderInLayout; + }; + return LayoutCompositionPolicy; } ); + diff --git a/platform/features/layout/src/LayoutController.js b/platform/features/layout/src/LayoutController.js index 643ed952a3..89364a1bb2 100644 --- a/platform/features/layout/src/LayoutController.js +++ b/platform/features/layout/src/LayoutController.js @@ -21,6 +21,11 @@ *****************************************************************************/ /*global define*/ +/** + * This bundle implements object types and associated views for + * display-building. + * @namespace platform/features/layout + */ define( ['./LayoutDrag'], function (LayoutDrag) { @@ -35,15 +40,12 @@ define( * Layout view. It arranges frames according to saved configuration * and provides methods for updating these based on mouse * movement. + * @memberof platform/features/layout * @constructor * @param {Scope} $scope the controller's Angular scope */ function LayoutController($scope) { - var gridSize = DEFAULT_GRID_SIZE, - activeDrag, - activeDragId, - rawPositions = {}, - positions = {}; + var self = this; // Utility function to copy raw positions from configuration, // without writing directly to configuration (to avoid triggering @@ -56,47 +58,6 @@ define( return copy; } - // Convert from { positions: ..., dimensions: ... } to an - // apropriate ng-style argument, to position frames. - function convertPosition(raw) { - // Multiply position/dimensions by grid size - return { - left: (gridSize[0] * raw.position[0]) + 'px', - top: (gridSize[1] * raw.position[1]) + 'px', - width: (gridSize[0] * raw.dimensions[0]) + 'px', - height: (gridSize[1] * raw.dimensions[1]) + 'px' - }; - } - - // Generate default positions for a new panel - function defaultDimensions() { - return MINIMUM_FRAME_SIZE.map(function (min, i) { - return Math.max( - Math.ceil(min / gridSize[i]), - DEFAULT_DIMENSIONS[i] - ); - }); - } - - // Generate a default position (in its raw format) for a frame. - // Use an index to ensure that default positions are unique. - function defaultPosition(index) { - return { - position: [index, index], - dimensions: defaultDimensions() - }; - } - - // Store a computed position for a contained frame by its - // domain object id. Called in a forEach loop, so arguments - // are as expected there. - function populatePosition(id, index) { - rawPositions[id] = - rawPositions[id] || defaultPosition(index || 0); - positions[id] = - convertPosition(rawPositions[id]); - } - // Compute panel positions based on the layout's object model function lookupPanels(ids) { var configuration = $scope.configuration || {}; @@ -106,27 +67,32 @@ define( ids = ids || []; // Pull panel positions from configuration - rawPositions = shallowCopy(configuration.panels || {}, ids); + self.rawPositions = + shallowCopy(configuration.panels || {}, ids); // Clear prior computed positions - positions = {}; + self.positions = {}; // Update width/height that we are tracking - gridSize = ($scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE; + self.gridSize = + ($scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE; // Compute positions and add defaults where needed - ids.forEach(populatePosition); + ids.forEach(function (id, index) { + self.populatePosition(id, index); + }); } // Update grid size when it changed function updateGridSize(layoutGrid) { - var oldSize = gridSize; + var oldSize = self.gridSize; - gridSize = layoutGrid || DEFAULT_GRID_SIZE; + self.gridSize = layoutGrid || DEFAULT_GRID_SIZE; // Only update panel positions if this actually changed things - if (gridSize[0] !== oldSize[0] || gridSize[1] !== oldSize[1]) { - lookupPanels(Object.keys(positions)); + if (self.gridSize[0] !== oldSize[0] || + self.gridSize[1] !== oldSize[1]) { + lookupPanels(Object.keys(self.positions)); } } @@ -144,8 +110,8 @@ define( // Store the position of this panel. $scope.configuration.panels[id] = { position: [ - Math.floor(position.x / gridSize[0]), - Math.floor(position.y / gridSize[1]) + Math.floor(position.x / self.gridSize[0]), + Math.floor(position.y / self.gridSize[1]) ], dimensions: DEFAULT_DIMENSIONS }; @@ -154,13 +120,39 @@ define( $scope.commit("Dropped a frame."); } // Populate template-facing position for this id - populatePosition(id); + self.populatePosition(id); // Layout may contain embedded views which will // listen for drops, so call preventDefault() so // that they can recognize that this event is handled. e.preventDefault(); } + // End drag; we don't want to put $scope into this + // because it triggers "cpws" (copy window or scope) + // errors in Angular. + this.endDragInScope = function () { + // Write to configuration; this is watched and + // saved by the EditRepresenter. + $scope.configuration = + $scope.configuration || {}; + // Make sure there is a "panels" field in the + // view configuration. + $scope.configuration.panels = + $scope.configuration.panels || {}; + // Store the position of this panel. + $scope.configuration.panels[self.activeDragId] = + self.rawPositions[self.activeDragId]; + // Mark this object as dirty to encourage persistence + if ($scope.commit) { + $scope.commit("Moved frame."); + } + }; + + this.positions = {}; + this.rawPositions = {}; + this.gridSize = DEFAULT_GRID_SIZE; + this.$scope = $scope; + // Watch for changes to the grid size in the model $scope.$watch("model.layoutGrid", updateGridSize); @@ -169,88 +161,117 @@ define( // Position panes where they are dropped $scope.$on("mctDrop", handleDrop); - - return { - /** - * Get a style object for a frame with the specified domain - * object identifier, suitable for use in an `ng-style` - * directive to position a frame as configured for this layout. - * @param {string} id the object identifier - * @returns {Object.} an object with - * appropriate left, width, etc fields for positioning - */ - getFrameStyle: function (id) { - // Called in a loop, so just look up; the "positions" - // object is kept up to date by a watch. - return positions[id]; - }, - /** - * Start a drag gesture to move/resize a frame. - * - * The provided position and dimensions factors will determine - * whether this is a move or a resize, and what type it - * will be. For instance, a position factor of [1, 1] - * will move a frame along with the mouse as the drag - * proceeds, while a dimension factor of [0, 0] will leave - * dimensions unchanged. Combining these in different - * ways results in different handles; a position factor of - * [1, 0] and a dimensions factor of [-1, 0] will implement - * a left-edge resize, as the horizontal position will move - * with the mouse while the horizontal dimensions shrink in - * kind (and vertical properties remain unmodified.) - * - * @param {string} id the identifier of the domain object - * in the frame being manipulated - * @param {number[]} posFactor the position factor - * @param {number[]} dimFactor the dimensions factor - */ - startDrag: function (id, posFactor, dimFactor) { - activeDragId = id; - activeDrag = new LayoutDrag( - rawPositions[id], - posFactor, - dimFactor, - gridSize - ); - }, - /** - * Continue an active drag gesture. - * @param {number[]} delta the offset, in pixels, - * of the current pointer position, relative - * to its position when the drag started - */ - continueDrag: function (delta) { - if (activeDrag) { - rawPositions[activeDragId] = - activeDrag.getAdjustedPosition(delta); - populatePosition(activeDragId); - } - }, - /** - * End the active drag gesture. This will update the - * view configuration. - */ - endDrag: function () { - // Write to configuration; this is watched and - // saved by the EditRepresenter. - $scope.configuration = - $scope.configuration || {}; - // Make sure there is a "panels" field in the - // view configuration. - $scope.configuration.panels = - $scope.configuration.panels || {}; - // Store the position of this panel. - $scope.configuration.panels[activeDragId] = - rawPositions[activeDragId]; - // Mark this object as dirty to encourage persistence - if ($scope.commit) { - $scope.commit("Moved frame."); - } - } - }; - } + // Convert from { positions: ..., dimensions: ... } to an + // apropriate ng-style argument, to position frames. + LayoutController.prototype.convertPosition = function (raw) { + var gridSize = this.gridSize; + // Multiply position/dimensions by grid size + return { + left: (gridSize[0] * raw.position[0]) + 'px', + top: (gridSize[1] * raw.position[1]) + 'px', + width: (gridSize[0] * raw.dimensions[0]) + 'px', + height: (gridSize[1] * raw.dimensions[1]) + 'px' + }; + }; + + // Generate default positions for a new panel + LayoutController.prototype.defaultDimensions = function () { + var gridSize = this.gridSize; + return MINIMUM_FRAME_SIZE.map(function (min, i) { + return Math.max( + Math.ceil(min / gridSize[i]), + DEFAULT_DIMENSIONS[i] + ); + }); + }; + + // Generate a default position (in its raw format) for a frame. + // Use an index to ensure that default positions are unique. + LayoutController.prototype.defaultPosition = function (index) { + return { + position: [index, index], + dimensions: this.defaultDimensions() + }; + }; + + // Store a computed position for a contained frame by its + // domain object id. Called in a forEach loop, so arguments + // are as expected there. + LayoutController.prototype.populatePosition = function (id, index) { + this.rawPositions[id] = + this.rawPositions[id] || this.defaultPosition(index || 0); + this.positions[id] = + this.convertPosition(this.rawPositions[id]); + }; + + /** + * Get a style object for a frame with the specified domain + * object identifier, suitable for use in an `ng-style` + * directive to position a frame as configured for this layout. + * @param {string} id the object identifier + * @returns {Object.} an object with + * appropriate left, width, etc fields for positioning + */ + LayoutController.prototype.getFrameStyle = function (id) { + // Called in a loop, so just look up; the "positions" + // object is kept up to date by a watch. + return this.positions[id]; + }; + + /** + * Start a drag gesture to move/resize a frame. + * + * The provided position and dimensions factors will determine + * whether this is a move or a resize, and what type it + * will be. For instance, a position factor of [1, 1] + * will move a frame along with the mouse as the drag + * proceeds, while a dimension factor of [0, 0] will leave + * dimensions unchanged. Combining these in different + * ways results in different handles; a position factor of + * [1, 0] and a dimensions factor of [-1, 0] will implement + * a left-edge resize, as the horizontal position will move + * with the mouse while the horizontal dimensions shrink in + * kind (and vertical properties remain unmodified.) + * + * @param {string} id the identifier of the domain object + * in the frame being manipulated + * @param {number[]} posFactor the position factor + * @param {number[]} dimFactor the dimensions factor + */ + LayoutController.prototype.startDrag = function (id, posFactor, dimFactor) { + this.activeDragId = id; + this.activeDrag = new LayoutDrag( + this.rawPositions[id], + posFactor, + dimFactor, + this.gridSize + ); + }; + /** + * Continue an active drag gesture. + * @param {number[]} delta the offset, in pixels, + * of the current pointer position, relative + * to its position when the drag started + */ + LayoutController.prototype.continueDrag = function (delta) { + if (this.activeDrag) { + this.rawPositions[this.activeDragId] = + this.activeDrag.getAdjustedPosition(delta); + this.populatePosition(this.activeDragId); + } + }; + + /** + * End the active drag gesture. This will update the + * view configuration. + */ + LayoutController.prototype.endDrag = function () { + this.endDragInScope(); + }; + return LayoutController; } ); + diff --git a/platform/features/layout/src/LayoutDrag.js b/platform/features/layout/src/LayoutDrag.js index cb3b4808be..0c0ef6b1a7 100644 --- a/platform/features/layout/src/LayoutDrag.js +++ b/platform/features/layout/src/LayoutDrag.js @@ -50,65 +50,68 @@ define( * @param {number[]} posFactor the position factor * @param {number[]} dimFactor the dimensions factor * @param {number[]} the size of each grid element, in pixels + * @constructor + * @memberof platform/features/layout */ function LayoutDrag(rawPosition, posFactor, dimFactor, gridSize) { - // Convert a delta from pixel coordinates to grid coordinates, - // rounding to whole-number grid coordinates. - function toGridDelta(pixelDelta) { - return pixelDelta.map(function (v, i) { - return Math.round(v / gridSize[i]); - }); - } - - // Utility function to perform element-by-element multiplication - function multiply(array, factors) { - return array.map(function (v, i) { - return v * factors[i]; - }); - } - - // Utility function to perform element-by-element addition - function add(array, other) { - return array.map(function (v, i) { - return v + other[i]; - }); - } - - // Utility function to perform element-by-element max-choosing - function max(array, other) { - return array.map(function (v, i) { - return Math.max(v, other[i]); - }); - } - - function getAdjustedPosition(pixelDelta) { - var gridDelta = toGridDelta(pixelDelta); - return { - position: max(add( - rawPosition.position, - multiply(gridDelta, posFactor) - ), [0, 0]), - dimensions: max(add( - rawPosition.dimensions, - multiply(gridDelta, dimFactor) - ), [1, 1]) - }; - - } - - return { - /** - * Get a new position object in grid coordinates, with - * position and dimensions both offset appropriately - * according to the factors supplied in the constructor. - * @param {number[]} pixelDelta the offset from the - * original position, in pixels - */ - getAdjustedPosition: getAdjustedPosition - }; + this.rawPosition = rawPosition; + this.posFactor = posFactor; + this.dimFactor = dimFactor; + this.gridSize = gridSize; } + // Convert a delta from pixel coordinates to grid coordinates, + // rounding to whole-number grid coordinates. + function toGridDelta(gridSize, pixelDelta) { + return pixelDelta.map(function (v, i) { + return Math.round(v / gridSize[i]); + }); + } + + // Utility function to perform element-by-element multiplication + function multiply(array, factors) { + return array.map(function (v, i) { + return v * factors[i]; + }); + } + + // Utility function to perform element-by-element addition + function add(array, other) { + return array.map(function (v, i) { + return v + other[i]; + }); + } + + // Utility function to perform element-by-element max-choosing + function max(array, other) { + return array.map(function (v, i) { + return Math.max(v, other[i]); + }); + } + + + /** + * Get a new position object in grid coordinates, with + * position and dimensions both offset appropriately + * according to the factors supplied in the constructor. + * @param {number[]} pixelDelta the offset from the + * original position, in pixels + */ + LayoutDrag.prototype.getAdjustedPosition = function (pixelDelta) { + var gridDelta = toGridDelta(this.gridSize, pixelDelta); + return { + position: max(add( + this.rawPosition.position, + multiply(gridDelta, this.posFactor) + ), [0, 0]), + dimensions: max(add( + this.rawPosition.dimensions, + multiply(gridDelta, this.dimFactor) + ), [1, 1]) + }; + }; + return LayoutDrag; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/AccessorMutator.js b/platform/features/layout/src/elements/AccessorMutator.js index 8b96d9cca9..656c4abbcf 100644 --- a/platform/features/layout/src/elements/AccessorMutator.js +++ b/platform/features/layout/src/elements/AccessorMutator.js @@ -38,6 +38,7 @@ define( * in certain ranges; specifically, to keep x/y positions * non-negative in a fixed position view. * + * @memberof platform/features/layout * @constructor * @param {Object} object the object to get/set values upon * @param {string} key the property to get/set @@ -56,4 +57,4 @@ define( return AccessorMutator; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/BoxProxy.js b/platform/features/layout/src/elements/BoxProxy.js index 724c9e21cd..a4b6bc4c09 100644 --- a/platform/features/layout/src/elements/BoxProxy.js +++ b/platform/features/layout/src/elements/BoxProxy.js @@ -34,6 +34,7 @@ define( * Note that arguments here are meant to match those expected * by `Array.prototype.map` * + * @memberof platform/features/layout * @constructor * @param element the fixed position element, as stored in its * configuration @@ -47,9 +48,9 @@ define( * Get/set this element's fill color. (Omitting the * argument makes this act as a getter.) * @method - * @memberof BoxProxy * @param {string} fill the new fill color * @returns {string} the fill color + * @memberof platform/features/layout.BoxProxy# */ proxy.fill = new AccessorMutator(element, 'fill'); @@ -58,4 +59,4 @@ define( return BoxProxy; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/ElementFactory.js b/platform/features/layout/src/elements/ElementFactory.js index 5bcafd8529..63dbb82757 100644 --- a/platform/features/layout/src/elements/ElementFactory.js +++ b/platform/features/layout/src/elements/ElementFactory.js @@ -85,31 +85,32 @@ define( * The ElementFactory creates new instances of elements for the * fixed position view, prompting for user input where necessary. * @param {DialogService} dialogService service to request user input + * @memberof platform/features/layout * @constructor */ function ElementFactory(dialogService) { - return { - /** - * Create a new element for the fixed position view. - * @param {string} type the type of element to create - * @returns {Promise|object} the created element, or a promise - * for that element - */ - createElement: function (type) { - var initialState = INITIAL_STATES[type] || {}; - - // Clone that state - initialState = JSON.parse(JSON.stringify(initialState)); - - // Show a dialog to configure initial state, if appropriate - return DIALOGS[type] ? dialogService.getUserInput( - DIALOGS[type], - initialState - ) : initialState; - } - }; + this.dialogService = dialogService; } + /** + * Create a new element for the fixed position view. + * @param {string} type the type of element to create + * @returns {Promise|object} the created element, or a promise + * for that element + */ + ElementFactory.prototype.createElement = function (type) { + var initialState = INITIAL_STATES[type] || {}; + + // Clone that state + initialState = JSON.parse(JSON.stringify(initialState)); + + // Show a dialog to configure initial state, if appropriate + return DIALOGS[type] ? this.dialogService.getUserInput( + DIALOGS[type], + initialState + ) : initialState; + }; + return ElementFactory; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/ElementProxies.js b/platform/features/layout/src/elements/ElementProxies.js index 02a69f3888..1443363883 100644 --- a/platform/features/layout/src/elements/ElementProxies.js +++ b/platform/features/layout/src/elements/ElementProxies.js @@ -34,4 +34,4 @@ define( "fixed.text": TextProxy }; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/ElementProxy.js b/platform/features/layout/src/elements/ElementProxy.js index 4591480517..5f3ef17bcb 100644 --- a/platform/features/layout/src/elements/ElementProxy.js +++ b/platform/features/layout/src/elements/ElementProxy.js @@ -48,6 +48,7 @@ define( * Note that arguments here are meant to match those expected * by `Array.prototype.map` * + * @memberof platform/features/layout * @constructor * @param element the fixed position element, as stored in its * configuration @@ -55,87 +56,108 @@ define( * @param {Array} elements the full array of elements */ function ElementProxy(element, index, elements) { - var handles = [ new ResizeHandle(element, 1, 1) ]; + this.resizeHandles = [ new ResizeHandle(element, 1, 1) ]; - return { - /** - * The element as stored in the view configuration. - */ - element: element, - /** - * Get and/or set the x position of this element. - * Units are in fixed position grid space. - * @param {number} [x] the new x position (if setting) - * @returns {number} the x position - */ - x: new AccessorMutator(element, 'x', clamp), - /** - * Get and/or set the y position of this element. - * Units are in fixed position grid space. - * @param {number} [y] the new y position (if setting) - * @returns {number} the y position - */ - y: new AccessorMutator(element, 'y', clamp), - /** - * Get and/or set the stroke color of this element. - * @param {string} [stroke] the new stroke color (if setting) - * @returns {string} the stroke color - */ - stroke: new AccessorMutator(element, 'stroke'), - /** - * Get and/or set the width of this element. - * Units are in fixed position grid space. - * @param {number} [w] the new width (if setting) - * @returns {number} the width - */ - width: new AccessorMutator(element, 'width'), - /** - * Get and/or set the height of this element. - * Units are in fixed position grid space. - * @param {number} [h] the new height (if setting) - * @returns {number} the height - */ - height: new AccessorMutator(element, 'height'), - /** - * Change the display order of this element. - * @param {string} o where to move this element; - * one of "top", "up", "down", or "bottom" - */ - order: function (o) { - var delta = ORDERS[o] || 0, - desired = Math.max( - Math.min(index + delta, elements.length - 1), - 0 - ); - // Move to the desired index, if this is a change - if ((desired !== index) && (elements[index] === element)) { - // Splice out the current element - elements.splice(index, 1); - // Splice it back in at the correct index - elements.splice(desired, 0, element); - // Track change in index (proxy should be recreated - // anyway, but be consistent) - index = desired; - } - }, - /** - * Remove this element from the fixed position view. - */ - remove: function () { - if (elements[index] === element) { - elements.splice(index, 1); - } - }, - /** - * Get handles to control specific features of this element, - * e.g. corner size. - */ - handles: function () { - return handles; - } - }; + /** + * The element as stored in the view configuration. + * @memberof platform/features/layout.ElementProxy# + */ + this.element = element; + + /** + * Get and/or set the x position of this element. + * Units are in fixed position grid space. + * @param {number} [x] the new x position (if setting) + * @returns {number} the x position + * @memberof platform/features/layout.ElementProxy# + */ + this.x = new AccessorMutator(element, 'x', clamp); + + /** + * Get and/or set the y position of this element. + * Units are in fixed position grid space. + * @param {number} [y] the new y position (if setting) + * @returns {number} the y position + * @memberof platform/features/layout.ElementProxy# + */ + this.y = new AccessorMutator(element, 'y', clamp); + + /** + * Get and/or set the stroke color of this element. + * @param {string} [stroke] the new stroke color (if setting) + * @returns {string} the stroke color + * @memberof platform/features/layout.ElementProxy# + */ + this.stroke = new AccessorMutator(element, 'stroke'); + + /** + * Get and/or set the width of this element. + * Units are in fixed position grid space. + * @param {number} [w] the new width (if setting) + * @returns {number} the width + * @memberof platform/features/layout.ElementProxy# + */ + this.width = new AccessorMutator(element, 'width'); + + /** + * Get and/or set the height of this element. + * Units are in fixed position grid space. + * @param {number} [h] the new height (if setting) + * @returns {number} the height + * @memberof platform/features/layout.ElementProxy# + */ + this.height = new AccessorMutator(element, 'height'); + + this.index = index; + this.elements = elements; } + /** + * Change the display order of this element. + * @param {string} o where to move this element; + * one of "top", "up", "down", or "bottom" + */ + ElementProxy.prototype.order = function (o) { + var index = this.index, + elements = this.elements, + element = this.element, + delta = ORDERS[o] || 0, + desired = Math.max( + Math.min(index + delta, elements.length - 1), + 0 + ); + // Move to the desired index, if this is a change + if ((desired !== index) && (elements[index] === element)) { + // Splice out the current element + elements.splice(index, 1); + // Splice it back in at the correct index + elements.splice(desired, 0, element); + // Track change in index (proxy should be recreated + // anyway, but be consistent) + this.index = desired; + } + }; + + /** + * Remove this element from the fixed position view. + */ + ElementProxy.prototype.remove = function () { + var index = this.index; + if (this.elements[index] === this.element) { + this.elements.splice(index, 1); + } + }; + + /** + * Get handles to control specific features of this element, + * e.g. corner size. + * @return {platform/features/layout.ElementHandle[]} handles + * for moving/resizing this element + */ + ElementProxy.prototype.handles = function () { + return this.resizeHandles; + }; + return ElementProxy; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/ImageProxy.js b/platform/features/layout/src/elements/ImageProxy.js index bafe1b9b00..22ef3ef0c3 100644 --- a/platform/features/layout/src/elements/ImageProxy.js +++ b/platform/features/layout/src/elements/ImageProxy.js @@ -32,11 +32,13 @@ define( * Note that arguments here are meant to match those expected * by `Array.prototype.map` * + * @memberof platform/features/layout * @constructor * @param element the fixed position element, as stored in its * configuration * @param index the element's index within its array * @param {Array} elements the full array of elements + * @augments {platform/features/layout.ElementProxy} */ function ImageProxy(element, index, elements) { var proxy = new ElementProxy(element, index, elements); @@ -45,6 +47,7 @@ define( * Get and/or set the displayed text of this element. * @param {string} [text] the new text (if setting) * @returns {string} the text + * @memberof platform/features/layout.ImageProxy# */ proxy.url = new AccessorMutator(element, 'url'); @@ -53,4 +56,4 @@ define( return ImageProxy; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/LineHandle.js b/platform/features/layout/src/elements/LineHandle.js index 6fd4ed84aa..c61d1f3802 100644 --- a/platform/features/layout/src/elements/LineHandle.js +++ b/platform/features/layout/src/elements/LineHandle.js @@ -30,53 +30,62 @@ define( * This is used to support drag handles for line elements * in a fixed position view. Field names for opposite ends * are provided to avoid zero-length lines. + * @memberof platform/features/layout * @constructor * @param element the line element * @param {string} xProperty field which stores x position * @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 + * @implements {platform/features/layout.ElementHandle} */ function LineHandle(element, xProperty, yProperty, xOther, yOther) { - return { - /** - * Get/set the x position of the lower-right corner - * of the handle-controlled element, changing size - * as necessary. - */ - x: function (value) { - if (arguments.length > 0) { - // Ensure we stay in view - value = Math.max(value, 0); - // Make sure end points will still be different - if (element[yOther] !== element[yProperty] || - element[xOther] !== value) { - element[xProperty] = value; - } - } - return element[xProperty]; - }, - /** - * Get/set the y position of the lower-right corner - * of the handle-controlled element, changing size - * as necessary. - */ - y: function (value) { - if (arguments.length > 0) { - // Ensure we stay in view - value = Math.max(value, 0); - // Make sure end points will still be different - if (element[xOther] !== element[xProperty] || - element[yOther] !== value) { - element[yProperty] = value; - } - } - return element[yProperty]; - } - }; + this.element = element; + this.xProperty = xProperty; + this.yProperty = yProperty; + this.xOther = xOther; + this.yOther = yOther; } + LineHandle.prototype.x = function (value) { + var element = this.element, + xProperty = this.xProperty, + yProperty = this.yProperty, + xOther = this.xOther, + yOther = this.yOther; + + if (arguments.length > 0) { + // Ensure we stay in view + value = Math.max(value, 0); + // Make sure end points will still be different + if (element[yOther] !== element[yProperty] || + element[xOther] !== value) { + element[xProperty] = value; + } + } + return element[xProperty]; + }; + + LineHandle.prototype.y = function (value) { + var element = this.element, + xProperty = this.xProperty, + yProperty = this.yProperty, + xOther = this.xOther, + yOther = this.yOther; + + if (arguments.length > 0) { + // Ensure we stay in view + value = Math.max(value, 0); + // Make sure end points will still be different + if (element[xOther] !== element[xProperty] || + element[yOther] !== value) { + element[yProperty] = value; + } + } + return element[yProperty]; + }; + return LineHandle; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/LineProxy.js b/platform/features/layout/src/elements/LineProxy.js index 92ca96bf76..44cf282993 100644 --- a/platform/features/layout/src/elements/LineProxy.js +++ b/platform/features/layout/src/elements/LineProxy.js @@ -29,11 +29,13 @@ define( /** * Selection/diplay proxy for line elements of a fixed position * view. + * @memberof platform/features/layout * @constructor * @param element the fixed position element, as stored in its * configuration * @param index the element's index within its array * @param {Array} elements the full array of elements + * @augments {platform/features/layout.ElementProxy} */ function LineProxy(element, index, elements) { var proxy = new ElementProxy(element, index, elements), @@ -46,6 +48,7 @@ define( * Get the top-left x coordinate, in grid space, of * this line's bounding box. * @returns {number} the x coordinate + * @memberof platform/features/layout.LineProxy# */ proxy.x = function (v) { var x = Math.min(element.x, element.x2), @@ -61,6 +64,7 @@ define( * Get the top-left y coordinate, in grid space, of * this line's bounding box. * @returns {number} the y coordinate + * @memberof platform/features/layout.LineProxy# */ proxy.y = function (v) { var y = Math.min(element.y, element.y2), @@ -76,6 +80,7 @@ define( * Get the width, in grid space, of * this line's bounding box. * @returns {number} the width + * @memberof platform/features/layout.LineProxy# */ proxy.width = function () { return Math.max(Math.abs(element.x - element.x2), 1); @@ -85,6 +90,7 @@ define( * Get the height, in grid space, of * this line's bounding box. * @returns {number} the height + * @memberof platform/features/layout.LineProxy# */ proxy.height = function () { return Math.max(Math.abs(element.y - element.y2), 1); @@ -95,6 +101,7 @@ define( * the top-left corner, of the first point in this line * segment. * @returns {number} the x position of the first point + * @memberof platform/features/layout.LineProxy# */ proxy.x1 = function () { return element.x - proxy.x(); @@ -105,6 +112,7 @@ define( * the top-left corner, of the first point in this line * segment. * @returns {number} the y position of the first point + * @memberof platform/features/layout.LineProxy# */ proxy.y1 = function () { return element.y - proxy.y(); @@ -115,6 +123,7 @@ define( * the top-left corner, of the second point in this line * segment. * @returns {number} the x position of the second point + * @memberof platform/features/layout.LineProxy# */ proxy.x2 = function () { return element.x2 - proxy.x(); @@ -125,6 +134,7 @@ define( * the top-left corner, of the second point in this line * segment. * @returns {number} the y position of the second point + * @memberof platform/features/layout.LineProxy# */ proxy.y2 = function () { return element.y2 - proxy.y(); @@ -134,6 +144,7 @@ define( * Get element handles for changing the position of end * points of this line. * @returns {LineHandle[]} line handles for both end points + * @memberof platform/features/layout.LineProxy# */ proxy.handles = function () { return handles; @@ -144,4 +155,4 @@ define( return LineProxy; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/ResizeHandle.js b/platform/features/layout/src/elements/ResizeHandle.js index e87ef91059..757bb6218e 100644 --- a/platform/features/layout/src/elements/ResizeHandle.js +++ b/platform/features/layout/src/elements/ResizeHandle.js @@ -25,50 +25,49 @@ define( function () { 'use strict'; + /** + * @interface platform/features/layout.ElementHandle + * @private + */ + /** * Handle for changing width/height properties of an element. * This is used to support drag handles for different * element types in a fixed position view. + * @memberof platform/features/layout * @constructor */ function ResizeHandle(element, minWidth, minHeight) { - // Ensure reasonable defaults - minWidth = minWidth || 0; - minHeight = minHeight || 0; + this.element = element; - return { - /** - * Get/set the x position of the lower-right corner - * of the handle-controlled element, changing size - * as necessary. - */ - x: function (value) { - if (arguments.length > 0) { - element.width = Math.max( - minWidth, - value - element.x - ); - } - return element.x + element.width; - }, - /** - * Get/set the y position of the lower-right corner - * of the handle-controlled element, changing size - * as necessary. - */ - y: function (value) { - if (arguments.length > 0) { - element.height = Math.max( - minHeight, - value - element.y - ); - } - return element.y + element.height; - } - }; + // Ensure reasonable defaults + this.minWidth = minWidth || 0; + this.minHeight = minHeight || 0; } + ResizeHandle.prototype.x = function (value) { + var element = this.element; + if (arguments.length > 0) { + element.width = Math.max( + this.minWidth, + value - element.x + ); + } + return element.x + element.width; + }; + + ResizeHandle.prototype.y = function (value) { + var element = this.element; + if (arguments.length > 0) { + element.height = Math.max( + this.minHeight, + value - element.y + ); + } + return element.y + element.height; + }; + return ResizeHandle; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/TelemetryProxy.js b/platform/features/layout/src/elements/TelemetryProxy.js index 7397c0e81a..dbb7044c51 100644 --- a/platform/features/layout/src/elements/TelemetryProxy.js +++ b/platform/features/layout/src/elements/TelemetryProxy.js @@ -35,11 +35,13 @@ define( * Note that arguments here are meant to match those expected * by `Array.prototype.map` * + * @memberof platform/features/layout * @constructor * @param element the fixed position element, as stored in its * configuration * @param index the element's index within its array * @param {Array} elements the full array of elements + * @augments {platform/features/layout.ElementProxy} */ function TelemetryProxy(element, index, elements) { var proxy = new TextProxy(element, index, elements); @@ -70,4 +72,4 @@ define( return TelemetryProxy; } -); \ No newline at end of file +); diff --git a/platform/features/layout/src/elements/TextProxy.js b/platform/features/layout/src/elements/TextProxy.js index 5e7ce4870d..c5b5247c4c 100644 --- a/platform/features/layout/src/elements/TextProxy.js +++ b/platform/features/layout/src/elements/TextProxy.js @@ -32,11 +32,13 @@ define( * Note that arguments here are meant to match those expected * by `Array.prototype.map` * + * @memberof platform/features/layout * @constructor * @param element the fixed position element, as stored in its * configuration * @param index the element's index within its array * @param {Array} elements the full array of elements + * @augments {platform/features/layout.ElementProxy} */ function TextProxy(element, index, elements) { var proxy = new BoxProxy(element, index, elements); @@ -45,6 +47,7 @@ define( * Get and/or set the text color of this element. * @param {string} [color] the new text color (if setting) * @returns {string} the text color + * @memberof platform/features/layout.TextProxy# */ proxy.color = new AccessorMutator(element, 'color'); @@ -52,6 +55,7 @@ define( * Get and/or set the displayed text of this element. * @param {string} [text] the new text (if setting) * @returns {string} the text + * @memberof platform/features/layout.TextProxy# */ proxy.text = new AccessorMutator(element, 'text'); @@ -60,4 +64,4 @@ define( return TextProxy; } -); \ No newline at end of file +); diff --git a/platform/features/pages/src/EmbeddedPageController.js b/platform/features/pages/src/EmbeddedPageController.js index a0e08e6549..0ebe5dfa23 100644 --- a/platform/features/pages/src/EmbeddedPageController.js +++ b/platform/features/pages/src/EmbeddedPageController.js @@ -21,6 +21,11 @@ *****************************************************************************/ /*global define*/ +/** + * This bundle adds the Web Page object type, which can be used to embed + * other web pages with layouts. + * @namespace platform/features/pages + */ define( [], function () { @@ -29,19 +34,24 @@ define( /** * Controller for embedded web pages; serves simply as a * wrapper for `$sce` to mark pages as trusted. + * @constructor + * @memberof platform/features/pages */ function EmbeddedPageController($sce) { - return { - /** - * Alias of `$sce.trustAsResourceUrl`. - */ - trust: function (url) { - return $sce.trustAsResourceUrl(url); - } - }; + this.$sce = $sce; } + /** + * Alias of `$sce.trustAsResourceUrl`. + * @param {string} url the URL to trust + * @returns {string} the trusted URL + */ + EmbeddedPageController.prototype.trust = function (url) { + return this.$sce.trustAsResourceUrl(url); + }; + + return EmbeddedPageController; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/Canvas2DChart.js b/platform/features/plot/src/Canvas2DChart.js index 7587159986..5917207920 100644 --- a/platform/features/plot/src/Canvas2DChart.js +++ b/platform/features/plot/src/Canvas2DChart.js @@ -29,113 +29,91 @@ define( /** * Create a new chart which uses Canvas's 2D API for rendering. * + * @memberof platform/features/plot * @constructor + * @implements {platform/features/plot.Chart} * @param {CanvasElement} canvas the canvas object to render upon * @throws {Error} an error is thrown if Canvas's 2D API is unavailable. */ function Canvas2DChart(canvas) { - var c2d = canvas.getContext('2d'), - width = canvas.width, - height = canvas.height, - dimensions = [ width, height ], - origin = [ 0, 0 ]; + this.canvas = canvas; + this.c2d = canvas.getContext('2d'); + this.width = canvas.width; + this.height = canvas.height; + this.dimensions = [ this.width, this.height ]; + this.origin = [ 0, 0 ]; - // Convert from logical to physical x coordinates - function x(v) { - return ((v - origin[0]) / dimensions[0]) * width; - } - - // Convert from logical to physical y coordinates - function y(v) { - return height - ((v - origin[1]) / dimensions[1]) * height; - } - - // Set the color to be used for drawing operations - function setColor(color) { - var mappedColor = color.map(function (c, i) { - return i < 3 ? Math.floor(c * 255) : (c); - }).join(','); - c2d.strokeStyle = "rgba(" + mappedColor + ")"; - c2d.fillStyle = "rgba(" + mappedColor + ")"; - } - - if (!c2d) { + if (!this.c2d) { throw new Error("Canvas 2d API unavailable."); } - - return { - /** - * Clear the chart. - */ - clear: function () { - width = canvas.width; - height = canvas.height; - c2d.clearRect(0, 0, width, height); - }, - /** - * Set the logical boundaries of the chart. - * @param {number[]} dimensions the horizontal and - * vertical dimensions of the chart - * @param {number[]} origin the horizontal/vertical - * origin of the chart - */ - setDimensions: function (newDimensions, newOrigin) { - dimensions = newDimensions; - origin = newOrigin; - }, - /** - * Draw the supplied buffer as a line strip (a sequence - * of line segments), in the chosen color. - * @param {Float32Array} buf the line strip to draw, - * in alternating x/y positions - * @param {number[]} color the color to use when drawing - * the line, as an RGBA color where each element - * is in the range of 0.0-1.0 - * @param {number} points the number of points to draw - */ - drawLine: function (buf, color, points) { - var i; - - setColor(color); - - // Configure context to draw two-pixel-thick lines - c2d.lineWidth = 2; - - // Start a new path... - if (buf.length > 1) { - c2d.beginPath(); - c2d.moveTo(x(buf[0]), y(buf[1])); - } - - // ...and add points to it... - for (i = 2; i < points * 2; i = i + 2) { - c2d.lineTo(x(buf[i]), y(buf[i + 1])); - } - - // ...before finally drawing it. - c2d.stroke(); - }, - /** - * Draw a rectangle extending from one corner to another, - * in the chosen color. - * @param {number[]} min the first corner of the rectangle - * @param {number[]} max the opposite corner - * @param {number[]} color the color to use when drawing - * the rectangle, as an RGBA color where each element - * is in the range of 0.0-1.0 - */ - drawSquare: function (min, max, color) { - var x1 = x(min[0]), - y1 = y(min[1]), - w = x(max[0]) - x1, - h = y(max[1]) - y1; - - setColor(color); - c2d.fillRect(x1, y1, w, h); - } - }; } + // Convert from logical to physical x coordinates + Canvas2DChart.prototype.x = function (v) { + return ((v - this.origin[0]) / this.dimensions[0]) * this.width; + }; + + // Convert from logical to physical y coordinates + Canvas2DChart.prototype.y = function (v) { + return this.height - + ((v - this.origin[1]) / this.dimensions[1]) * this.height; + }; + + // Set the color to be used for drawing operations + Canvas2DChart.prototype.setColor = function (color) { + var mappedColor = color.map(function (c, i) { + return i < 3 ? Math.floor(c * 255) : (c); + }).join(','); + this.c2d.strokeStyle = "rgba(" + mappedColor + ")"; + this.c2d.fillStyle = "rgba(" + mappedColor + ")"; + }; + + + Canvas2DChart.prototype.clear = function () { + var canvas = this.canvas; + this.width = canvas.width; + this.height = canvas.height; + this.c2d.clearRect(0, 0, this.width, this.height); + }; + + Canvas2DChart.prototype.setDimensions = function (newDimensions, newOrigin) { + this.dimensions = newDimensions; + this.origin = newOrigin; + }; + + Canvas2DChart.prototype.drawLine = function (buf, color, points) { + var i; + + this.setColor(color); + + // Configure context to draw two-pixel-thick lines + this.c2d.lineWidth = 2; + + // Start a new path... + if (buf.length > 1) { + this.c2d.beginPath(); + this.c2d.moveTo(this.x(buf[0]), this.y(buf[1])); + } + + // ...and add points to it... + for (i = 2; i < points * 2; i = i + 2) { + this.c2d.lineTo(this.x(buf[i]), this.y(buf[i + 1])); + } + + // ...before finally drawing it. + this.c2d.stroke(); + }; + + Canvas2DChart.prototype.drawSquare = function (min, max, color) { + var x1 = this.x(min[0]), + y1 = this.y(min[1]), + w = this.x(max[0]) - x1, + h = this.y(max[1]) - y1; + + this.setColor(color); + this.c2d.fillRect(x1, y1, w, h); + }; + return Canvas2DChart; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/GLChart.js b/platform/features/plot/src/GLChart.js index a5e52b6f4d..6dc7934fa5 100644 --- a/platform/features/plot/src/GLChart.js +++ b/platform/features/plot/src/GLChart.js @@ -49,7 +49,9 @@ define( /** * Create a new chart which uses WebGL for rendering. * + * @memberof platform/features/plot * @constructor + * @implements {platform/features/plot.Chart} * @param {CanvasElement} canvas the canvas object to render upon * @throws {Error} an error is thrown if WebGL is unavailable. */ @@ -61,8 +63,7 @@ define( aVertexPosition, uColor, uDimensions, - uOrigin, - buffer; + uOrigin; // Ensure a context was actually available before proceeding if (!gl) { @@ -93,7 +94,7 @@ define( gl.enableVertexAttribArray(aVertexPosition); // Create a buffer to holds points which will be drawn - buffer = gl.createBuffer(); + this.buffer = gl.createBuffer(); // Use a line width of 2.0 for legibility gl.lineWidth(2.0); @@ -102,75 +103,59 @@ define( gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - // Utility function to handle drawing of a buffer; - // drawType will determine whether this is a box, line, etc. - function doDraw(drawType, buf, color, points) { - gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - gl.bufferData(gl.ARRAY_BUFFER, buf, gl.DYNAMIC_DRAW); - gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0); - gl.uniform4fv(uColor, color); - gl.drawArrays(drawType, 0, points); - } - - return { - /** - * Clear the chart. - */ - clear: function () { - // Set the viewport size; note that we use the width/height - // that our WebGL context reports, which may be lower - // resolution than the canvas we requested. - gl.viewport( - 0, - 0, - gl.drawingBufferWidth, - gl.drawingBufferHeight - ); - gl.clear(gl.COLOR_BUFFER_BIT + gl.DEPTH_BUFFER_BIT); - }, - /** - * Set the logical boundaries of the chart. - * @param {number[]} dimensions the horizontal and - * vertical dimensions of the chart - * @param {number[]} origin the horizontal/vertical - * origin of the chart - */ - setDimensions: function (dimensions, origin) { - if (dimensions && dimensions.length > 0 && - origin && origin.length > 0) { - gl.uniform2fv(uDimensions, dimensions); - gl.uniform2fv(uOrigin, origin); - } - }, - /** - * Draw the supplied buffer as a line strip (a sequence - * of line segments), in the chosen color. - * @param {Float32Array} buf the line strip to draw, - * in alternating x/y positions - * @param {number[]} color the color to use when drawing - * the line, as an RGBA color where each element - * is in the range of 0.0-1.0 - * @param {number} points the number of points to draw - */ - drawLine: function (buf, color, points) { - doDraw(gl.LINE_STRIP, buf, color, points); - }, - /** - * Draw a rectangle extending from one corner to another, - * in the chosen color. - * @param {number[]} min the first corner of the rectangle - * @param {number[]} max the opposite corner - * @param {number[]} color the color to use when drawing - * the rectangle, as an RGBA color where each element - * is in the range of 0.0-1.0 - */ - drawSquare: function (min, max, color) { - doDraw(gl.TRIANGLE_FAN, new Float32Array( - min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]]) - ), color, 4); - } - }; + this.gl = gl; + this.aVertexPosition = aVertexPosition; + this.uColor = uColor; + this.uDimensions = uDimensions; + this.uOrigin = uOrigin; } + + // Utility function to handle drawing of a buffer; + // drawType will determine whether this is a box, line, etc. + GLChart.prototype.doDraw = function (drawType, buf, color, points) { + var gl = this.gl; + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bufferData(gl.ARRAY_BUFFER, buf, gl.DYNAMIC_DRAW); + gl.vertexAttribPointer(this.aVertexPosition, 2, gl.FLOAT, false, 0, 0); + gl.uniform4fv(this.uColor, color); + gl.drawArrays(drawType, 0, points); + }; + + GLChart.prototype.clear = function () { + var gl = this.gl; + + // Set the viewport size; note that we use the width/height + // that our WebGL context reports, which may be lower + // resolution than the canvas we requested. + gl.viewport( + 0, + 0, + gl.drawingBufferWidth, + gl.drawingBufferHeight + ); + gl.clear(gl.COLOR_BUFFER_BIT + gl.DEPTH_BUFFER_BIT); + }; + + + GLChart.prototype.setDimensions = function (dimensions, origin) { + var gl = this.gl; + if (dimensions && dimensions.length > 0 && + origin && origin.length > 0) { + gl.uniform2fv(this.uDimensions, dimensions); + gl.uniform2fv(this.uOrigin, origin); + } + }; + + GLChart.prototype.drawLine = function (buf, color, points) { + this.doDraw(this.gl.LINE_STRIP, buf, color, points); + }; + + GLChart.prototype.drawSquare = function (min, max, color) { + this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array( + min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]]) + ), color, 4); + }; + return GLChart; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/MCTChart.js b/platform/features/plot/src/MCTChart.js index 2ca51b2309..e8c9db74e4 100644 --- a/platform/features/plot/src/MCTChart.js +++ b/platform/features/plot/src/MCTChart.js @@ -61,6 +61,7 @@ define( * * `color`: The color of the box, as a four-element RGBA * array, where each element is in the range of 0.0-1.0 * + * @memberof platform/features/plot * @constructor */ function MCTChart($interval, $log) { @@ -205,6 +206,46 @@ define( }; } + /** + * @interface platform/features/plot.Chart + * @private + */ + + /** + * Clear the chart. + * @method platform/features/plot.Chart#clear + */ + /** + * Set the logical boundaries of the chart. + * @param {number[]} dimensions the horizontal and + * vertical dimensions of the chart + * @param {number[]} origin the horizontal/vertical + * origin of the chart + * @memberof platform/features/plot.Chart#setDimensions + */ + /** + * Draw the supplied buffer as a line strip (a sequence + * of line segments), in the chosen color. + * @param {Float32Array} buf the line strip to draw, + * in alternating x/y positions + * @param {number[]} color the color to use when drawing + * the line, as an RGBA color where each element + * is in the range of 0.0-1.0 + * @param {number} points the number of points to draw + * @memberof platform/features/plot.Chart#drawLine + */ + /** + * Draw a rectangle extending from one corner to another, + * in the chosen color. + * @param {number[]} min the first corner of the rectangle + * @param {number[]} max the opposite corner + * @param {number[]} color the color to use when drawing + * the rectangle, as an RGBA color where each element + * is in the range of 0.0-1.0 + * @memberof platform/features/plot.Chart#drawSquare + */ + return MCTChart; } ); + diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js index da1a7a88ab..a54fff83dd 100644 --- a/platform/features/plot/src/PlotController.js +++ b/platform/features/plot/src/PlotController.js @@ -22,7 +22,8 @@ /*global define*/ /** - * Module defining PlotController. Created by vwoeltje on 11/12/14. + * This bundle adds a "Plot" view for numeric telemetry data. + * @namespace platform/features/plot */ define( [ @@ -50,6 +51,7 @@ define( * * Handling user interactions. * * Deciding what needs to be drawn in the chart area. * + * @memberof platform/features/plot * @constructor */ function PlotController( @@ -59,15 +61,11 @@ define( throttle, PLOT_FIXED_DURATION ) { - var subPlotFactory = new SubPlotFactory(telemetryFormatter), - modeOptions = new PlotModeOptions([], subPlotFactory), - subplots = [], + var self = this, + subPlotFactory = new SubPlotFactory(telemetryFormatter), cachedObjects = [], - limitTracker, updater, - handle, - scheduleUpdate, - domainOffset; + handle; // Populate the scope with axis information (specifically, options // available for each axis.) @@ -89,18 +87,13 @@ define( function setupModes(telemetryObjects) { if (cachedObjects !== telemetryObjects) { cachedObjects = telemetryObjects; - modeOptions = new PlotModeOptions( + self.modeOptions = new PlotModeOptions( telemetryObjects || [], subPlotFactory ); } } - // Update all sub-plots - function update() { - scheduleUpdate(); - } - // Reinstantiate the plot updater (e.g. because we have a // new subscription.) This will clear the plot. function recreateUpdater() { @@ -110,7 +103,7 @@ define( ($scope.axes[1].active || {}).key, PLOT_FIXED_DURATION ); - limitTracker = new PlotLimitTracker( + self.limitTracker = new PlotLimitTracker( handle, ($scope.axes[1].active || {}).key ); @@ -123,19 +116,19 @@ define( } if (updater) { updater.update(); - modeOptions.getModeHandler().plotTelemetry(updater); + self.modeOptions.getModeHandler().plotTelemetry(updater); } - if (limitTracker) { - limitTracker.update(); + if (self.limitTracker) { + self.limitTracker.update(); } - update(); + self.update(); } // Display new historical data as it becomes available function addHistoricalData(domainObject, series) { updater.addHistorical(domainObject, series); - modeOptions.getModeHandler().plotTelemetry(updater); - update(); + self.modeOptions.getModeHandler().plotTelemetry(updater); + self.update(); } // Issue a new request for historical telemetry @@ -172,105 +165,120 @@ define( } } + this.modeOptions = new PlotModeOptions([], subPlotFactory); + this.updateValues = updateValues; + + // Create a throttled update function + this.scheduleUpdate = throttle(function () { + self.modeOptions.getModeHandler().getSubPlots() + .forEach(updateSubplot); + }); + // Subscribe to telemetry when a domain object becomes available $scope.$watch('domainObject', subscribe); // Unsubscribe when the plot is destroyed $scope.$on("$destroy", releaseSubscription); - // Create a throttled update function - scheduleUpdate = throttle(function () { - modeOptions.getModeHandler().getSubPlots() - .forEach(updateSubplot); - }); - - return { - /** - * Get the color (as a style-friendly string) to use - * for plotting the trace at the specified index. - * @param {number} index the index of the trace - * @returns {string} the color, in #RRGGBB form - */ - getColor: function (index) { - return PlotPalette.getStringColor(index); - }, - /** - * Check if the plot is zoomed or panned out - * of its default state (to determine whether back/unzoom - * controls should be shown) - * @returns {boolean} true if not in default state - */ - isZoomed: function () { - return modeOptions.getModeHandler().isZoomed(); - }, - /** - * Undo the most recent pan/zoom change and restore - * the prior state. - */ - stepBackPanZoom: function () { - return modeOptions.getModeHandler().stepBackPanZoom(); - }, - /** - * Undo all pan/zoom changes and restore the initial state. - */ - unzoom: function () { - return modeOptions.getModeHandler().unzoom(); - }, - /** - * Get the mode options (Stacked/Overlaid) that are applicable - * for this plot. - */ - getModeOptions: function () { - return modeOptions.getModeOptions(); - }, - /** - * Get the current mode that is applicable to this plot. This - * will include key, name, and glyph fields. - */ - getMode: function () { - return modeOptions.getMode(); - }, - /** - * Set the mode which should be active in this plot. - * @param mode one of the mode options returned from - * getModeOptions() - */ - setMode: function (mode) { - modeOptions.setMode(mode); - updateValues(); - }, - /** - * Get all individual plots contained within this Plot view. - * (Multiple may be contained when in Stacked mode). - * @returns {SubPlot[]} all subplots in this Plot view - */ - getSubPlots: function () { - return modeOptions.getModeHandler().getSubPlots(); - }, - /** - * Get the CSS class to apply to the legend for this domain - * object; this will reflect limit state. - * @returns {string} the CSS class - */ - getLegendClass: function (telemetryObject) { - return limitTracker && - limitTracker.getLegendClass(telemetryObject); - }, - /** - * Explicitly update all plots. - */ - update: update, - /** - * Check if a request is pending (to show the wait spinner) - */ - isRequestPending: function () { - // Placeholder; this should reflect request state - // when requesting historical telemetry - return false; - } - }; } + /** + * Get the color (as a style-friendly string) to use + * for plotting the trace at the specified index. + * @param {number} index the index of the trace + * @returns {string} the color, in #RRGGBB form + */ + PlotController.prototype.getColor = function (index) { + return PlotPalette.getStringColor(index); + }; + + /** + * Check if the plot is zoomed or panned out + * of its default state (to determine whether back/unzoom + * controls should be shown) + * @returns {boolean} true if not in default state + */ + PlotController.prototype.isZoomed = function () { + return this.modeOptions.getModeHandler().isZoomed(); + }; + + /** + * Undo the most recent pan/zoom change and restore + * the prior state. + */ + PlotController.prototype.stepBackPanZoom = function () { + return this.modeOptions.getModeHandler().stepBackPanZoom(); + }; + + /** + * Undo all pan/zoom changes and restore the initial state. + */ + PlotController.prototype.unzoom = function () { + return this.modeOptions.getModeHandler().unzoom(); + }; + + /** + * Get the mode options (Stacked/Overlaid) that are applicable + * for this plot. + */ + PlotController.prototype.getModeOptions = function () { + return this.modeOptions.getModeOptions(); + }; + + /** + * Get the current mode that is applicable to this plot. This + * will include key, name, and glyph fields. + */ + PlotController.prototype.getMode = function () { + return this.modeOptions.getMode(); + }; + + /** + * Set the mode which should be active in this plot. + * @param mode one of the mode options returned from + * getModeOptions() + */ + PlotController.prototype.setMode = function (mode) { + this.modeOptions.setMode(mode); + this.updateValues(); + }; + + /** + * Get all individual plots contained within this Plot view. + * (Multiple may be contained when in Stacked mode). + * @returns {SubPlot[]} all subplots in this Plot view + */ + PlotController.prototype.getSubPlots = function () { + return this.modeOptions.getModeHandler().getSubPlots(); + }; + + /** + * Get the CSS class to apply to the legend for this domain + * object; this will reflect limit state. + * @returns {string} the CSS class + */ + PlotController.prototype.getLegendClass = function (telemetryObject) { + return this.limitTracker && + this.limitTracker.getLegendClass(telemetryObject); + }; + + /** + * Explicitly update all plots. + */ + PlotController.prototype.update = function () { + this.scheduleUpdate(); + }; + + /** + * Check if a request is pending (to show the wait spinner) + */ + PlotController.prototype.isRequestPending = function () { + // Placeholder; this should reflect request state + // when requesting historical telemetry + return false; + }; + return PlotController; } ); + diff --git a/platform/features/plot/src/SubPlot.js b/platform/features/plot/src/SubPlot.js index 7c74751b27..06b7f7bb0f 100644 --- a/platform/features/plot/src/SubPlot.js +++ b/platform/features/plot/src/SubPlot.js @@ -36,6 +36,7 @@ define( * A SubPlot is an individual plot within a Plot View (which * may contain multiple plots, specifically when in Stacked * plot mode.) + * @memberof platform/features/plot * @constructor * @param {DomainObject[]} telemetryObjects the domain objects * which will be plotted in this sub-plot @@ -49,141 +50,278 @@ define( // We are used from a template often, so maintain // state in local variables to allow for fast look-up, // as is normal for controllers. - var draw = {}, - rangeTicks = [], - domainTicks = [], - formatter = telemetryFormatter, - domainOffset, - mousePosition, - marqueeStart, - panStart, - panStartBounds, - subPlotBounds, - hoverCoordinates, - isHovering = false; + this.telemetryObjects = telemetryObjects; + this.domainTicks = []; + this.rangeTicks = []; + this.formatter = telemetryFormatter; + this.draw = {}; + this.hovering = false; + this.panZoomStack = panZoomStack; + + // Start with the right initial drawing bounds, + // tick marks + this.updateDrawingBounds(); + this.updateTicks(); + } + + // Utility function for filtering out empty strings. + function isNonEmpty(v) { + return typeof v === 'string' && v !== ""; + } + + // Converts from pixel coordinates to domain-range, + // to interpret mouse gestures. + SubPlot.prototype.mousePositionToDomainRange = function (mousePosition) { + return new PlotPosition( + mousePosition.x, + mousePosition.y, + mousePosition.width, + mousePosition.height, + this.panZoomStack + ).getPosition(); + }; + + // Utility function to get the mouse position (in x,y + // pixel coordinates in the canvas area) from a mouse + // event object. + SubPlot.prototype.toMousePosition = function ($event) { + var bounds = this.subPlotBounds; + + return { + x: $event.clientX - bounds.left, + y: $event.clientY - bounds.top, + width: bounds.width, + height: bounds.height + }; + }; + + // Convert a domain-range position to a displayable + // position. This will subtract the domain offset, which + // is used to bias domain values to minimize loss-of-precision + // associated with conversion to a 32-bit floating point + // format (which is needed in the chart area itself, by WebGL.) + SubPlot.prototype.toDisplayable = function (position) { + return [ position[0] - this.domainOffset, position[1] ]; + }; + + // Update the current hover coordinates + SubPlot.prototype.updateHoverCoordinates = function () { + var formatter = this.formatter; // Utility, for map/forEach loops. Index 0 is domain, // index 1 is range. function formatValue(v, i) { return (i ? - formatter.formatRangeValue : - formatter.formatDomainValue)(v); + formatter.formatRangeValue : + formatter.formatDomainValue)(v); } - // Utility function for filtering out empty strings. - function isNonEmpty(v) { - return typeof v === 'string' && v !== ""; + this.hoverCoordinates = this.mousePosition && + this.mousePositionToDomainRange(this.mousePosition) + .map(formatValue) + .filter(isNonEmpty) + .join(", "); + }; + + // Update the drawable marquee area to reflect current + // mouse position (or don't show it at all, if no marquee + // zoom is in progress) + SubPlot.prototype.updateMarqueeBox = function () { + // Express this as a box in the draw object, which + // is passed to an mct-chart in the template for rendering. + this.draw.boxes = this.marqueeStart ? + [{ + start: this.toDisplayable( + this.mousePositionToDomainRange(this.marqueeStart) + ), + end: this.toDisplayable( + this.mousePositionToDomainRange(this.mousePosition) + ), + color: [1, 1, 1, 0.5 ] + }] : undefined; + }; + + // Update the bounds (origin and dimensions) of the drawing area. + SubPlot.prototype.updateDrawingBounds = function () { + var panZoom = this.panZoomStack.getPanZoom(); + + // Communicate pan-zoom state from stack to the draw object + // which is passed to mct-chart in the template. + this.draw.dimensions = panZoom.dimensions; + this.draw.origin = [ + panZoom.origin[0] - this.domainOffset, + panZoom.origin[1] + ]; + }; + + // Update tick marks in scope. + SubPlot.prototype.updateTicks = function () { + var tickGenerator = + new PlotTickGenerator(this.panZoomStack, this.formatter); + + this.domainTicks = + tickGenerator.generateDomainTicks(DOMAIN_TICKS); + this.rangeTicks = + tickGenerator.generateRangeTicks(RANGE_TICKS); + }; + + SubPlot.prototype.updatePan = function () { + var start, current, delta, nextOrigin; + + // Clear the previous panning pan-zoom state + this.panZoomStack.popPanZoom(); + + // Calculate what the new resulting pan-zoom should be + start = this.mousePositionToDomainRange( + this.panStart, + this.panZoomStack + ); + current = this.mousePositionToDomainRange( + this.mousePosition, + this.panZoomStack + ); + + delta = [ current[0] - start[0], current[1] - start[1] ]; + nextOrigin = [ + this.panStartBounds.origin[0] - delta[0], + this.panStartBounds.origin[1] - delta[1] + ]; + + // ...and push a new one at the current mouse position + this.panZoomStack + .pushPanZoom(nextOrigin, this.panStartBounds.dimensions); + }; + + /** + * Get the set of domain objects which are being + * represented in this sub-plot. + * @returns {DomainObject[]} the domain objects which + * will have data plotted in this sub-plot + */ + SubPlot.prototype.getTelemetryObjects = function () { + return this.telemetryObjects; + }; + + /** + * Get ticks mark information appropriate for using in the + * template for this sub-plot's domain axis, as prepared + * by the PlotTickGenerator. + * @returns {Array} tick marks for the domain axis + */ + SubPlot.prototype.getDomainTicks = function () { + return this.domainTicks; + }; + + /** + * Get ticks mark information appropriate for using in the + * template for this sub-plot's range axis, as prepared + * by the PlotTickGenerator. + * @returns {Array} tick marks for the range axis + */ + SubPlot.prototype.getRangeTicks = function () { + return this.rangeTicks; + }; + + /** + * Get the drawing object associated with this sub-plot; + * this object will be passed to the mct-chart in which + * this sub-plot's lines will be plotted, as its "draw" + * attribute, and should have the same internal format + * expected by that directive. + * @return {object} the drawing object + */ + SubPlot.prototype.getDrawingObject = function () { + return this.draw; + }; + + /** + * Get the coordinates (as displayable text) for the + * current mouse position. + * @returns {string[]} the displayable domain and range + * coordinates over which the mouse is hovered + */ + SubPlot.prototype.getHoverCoordinates = function () { + return this.hoverCoordinates; + }; + + /** + * Handle mouse movement over the chart area. + * @param $event the mouse event + * @memberof platform/features/plot.SubPlot# + */ + SubPlot.prototype.hover = function ($event) { + this.hovering = true; + this.subPlotBounds = $event.target.getBoundingClientRect(); + this.mousePosition = this.toMousePosition($event); + this.updateHoverCoordinates(); + if (this.marqueeStart) { + this.updateMarqueeBox(); } - - // Converts from pixel coordinates to domain-range, - // to interpret mouse gestures. - function mousePositionToDomainRange(mousePosition) { - return new PlotPosition( - mousePosition.x, - mousePosition.y, - mousePosition.width, - mousePosition.height, - panZoomStack - ).getPosition(); + if (this.panStart) { + this.updatePan(); + this.updateDrawingBounds(); + this.updateTicks(); } + }; - // Utility function to get the mouse position (in x,y - // pixel coordinates in the canvas area) from a mouse - // event object. - function toMousePosition($event) { - var bounds = subPlotBounds; - - return { - x: $event.clientX - bounds.left, - y: $event.clientY - bounds.top, - width: bounds.width, - height: bounds.height - }; + /** + * Continue a previously-start pan or zoom gesture. + * @param $event the mouse event + * @memberof platform/features/plot.SubPlot# + */ + SubPlot.prototype.continueDrag = function ($event) { + this.mousePosition = this.toMousePosition($event); + if (this.marqueeStart) { + this.updateMarqueeBox(); } - - // Convert a domain-range position to a displayable - // position. This will subtract the domain offset, which - // is used to bias domain values to minimize loss-of-precision - // associated with conversion to a 32-bit floating point - // format (which is needed in the chart area itself, by WebGL.) - function toDisplayable(position) { - return [ position[0] - domainOffset, position[1] ]; + if (this.panStart) { + this.updatePan(); + this.updateDrawingBounds(); + this.updateTicks(); } + }; - - // Update the currnet hover coordinates - function updateHoverCoordinates() { - hoverCoordinates = mousePosition && - mousePositionToDomainRange(mousePosition) - .map(formatValue) - .filter(isNonEmpty) - .join(", "); - } - - // Update the drawable marquee area to reflect current - // mouse position (or don't show it at all, if no marquee - // zoom is in progress) - function updateMarqueeBox() { - // Express this as a box in the draw object, which - // is passed to an mct-chart in the template for rendering. - draw.boxes = marqueeStart ? - [{ - start: toDisplayable(mousePositionToDomainRange(marqueeStart)), - end: toDisplayable(mousePositionToDomainRange(mousePosition)), - color: [1, 1, 1, 0.5 ] - }] : undefined; - } - - // Update the bounds (origin and dimensions) of the drawing area. - function updateDrawingBounds() { - var panZoom = panZoomStack.getPanZoom(); - - // Communicate pan-zoom state from stack to the draw object - // which is passed to mct-chart in the template. - draw.dimensions = panZoom.dimensions; - draw.origin = [ - panZoom.origin[0] - domainOffset, - panZoom.origin[1] - ]; - } - - // Update tick marks in scope. - function updateTicks() { - var tickGenerator = new PlotTickGenerator(panZoomStack, formatter); - - domainTicks = - tickGenerator.generateDomainTicks(DOMAIN_TICKS); - rangeTicks = - tickGenerator.generateRangeTicks(RANGE_TICKS); - } - - function updatePan() { - var start, current, delta, nextOrigin; - - // Clear the previous panning pan-zoom state - panZoomStack.popPanZoom(); - - // Calculate what the new resulting pan-zoom should be - start = mousePositionToDomainRange(panStart); - current = mousePositionToDomainRange(mousePosition); - delta = [ current[0] - start[0], current[1] - start[1] ]; - nextOrigin = [ - panStartBounds.origin[0] - delta[0], - panStartBounds.origin[1] - delta[1] - ]; - - // ...and push a new one at the current mouse position - panZoomStack.pushPanZoom(nextOrigin, panStartBounds.dimensions); + /** + * Initiate a marquee zoom action. + * @param $event the mouse event + */ + SubPlot.prototype.startDrag = function ($event) { + this.subPlotBounds = $event.target.getBoundingClientRect(); + this.mousePosition = this.toMousePosition($event); + // Treat any modifier key as a pan + if ($event.altKey || $event.shiftKey || $event.ctrlKey) { + // Start panning + this.panStart = this.mousePosition; + this.panStartBounds = this.panZoomStack.getPanZoom(); + // We're starting a pan, so add this back as a + // state on the stack; it will get replaced + // during the pan. + this.panZoomStack.pushPanZoom( + this.panStartBounds.origin, + this.panStartBounds.dimensions + ); + $event.preventDefault(); + } else { + // Start marquee zooming + this.marqueeStart = this.mousePosition; + this.updateMarqueeBox(); } + }; + /** + * Complete a marquee zoom action. + * @param $event the mouse event + */ + SubPlot.prototype.endDrag = function ($event) { + var self = this; // Perform a marquee zoom. function marqueeZoom(start, end) { // Determine what boundary is described by the marquee, // in domain-range values. Use the minima for origin, so that // it doesn't matter what direction the user marqueed in. - var a = mousePositionToDomainRange(start), - b = mousePositionToDomainRange(end), + var a = self.mousePositionToDomainRange(start), + b = self.mousePositionToDomainRange(end), origin = [ Math.min(a[0], b[0]), Math.min(a[1], b[1]) @@ -196,186 +334,71 @@ define( // Proceed with zoom if zoom dimensions are non zeros if (!(dimensions[0] === 0 && dimensions[1] === 0)) { // Push the new state onto the pan-zoom stack - panZoomStack.pushPanZoom(origin, dimensions); + self.panZoomStack.pushPanZoom(origin, dimensions); // Make sure tick marks reflect new bounds - updateTicks(); + self.updateTicks(); } } - // Start with the right initial drawing bounds, - // tick marks - updateDrawingBounds(); - updateTicks(); + this.mousePosition = this.toMousePosition($event); + this.subPlotBounds = undefined; + if (this.marqueeStart) { + marqueeZoom(this.marqueeStart, this.mousePosition); + this.marqueeStart = undefined; + this.updateMarqueeBox(); + this.updateDrawingBounds(); + this.updateTicks(); + } + if (this.panStart) { + // End panning + this.panStart = undefined; + this.panStartBounds = undefined; + } + }; - return { - /** - * Get the set of domain objects which are being - * represented in this sub-plot. - * @returns {DomainObject[]} the domain objects which - * will have data plotted in this sub-plot - */ - getTelemetryObjects: function () { - return telemetryObjects; - }, - /** - * Get ticks mark information appropriate for using in the - * template for this sub-plot's domain axis, as prepared - * by the PlotTickGenerator. - * @returns {Array} tick marks for the domain axis - */ - getDomainTicks: function () { - return domainTicks; - }, - /** - * Get ticks mark information appropriate for using in the - * template for this sub-plot's range axis, as prepared - * by the PlotTickGenerator. - * @returns {Array} tick marks for the range axis - */ - getRangeTicks: function () { - return rangeTicks; - }, - /** - * Get the drawing object associated with this sub-plot; - * this object will be passed to the mct-chart in which - * this sub-plot's lines will be plotted, as its "draw" - * attribute, and should have the same internal format - * expected by that directive. - * @return {object} the drawing object - */ - getDrawingObject: function () { - return draw; - }, - /** - * Get the coordinates (as displayable text) for the - * current mouse position. - * @returns {string[]} the displayable domain and range - * coordinates over which the mouse is hovered - */ - getHoverCoordinates: function () { - return hoverCoordinates; - }, - /** - * Handle mouse movement over the chart area. - * @param $event the mouse event - */ - hover: function ($event) { - isHovering = true; - subPlotBounds = $event.target.getBoundingClientRect(); - mousePosition = toMousePosition($event); - updateHoverCoordinates(); - if (marqueeStart) { - updateMarqueeBox(); - } - if (panStart) { - updatePan(); - updateDrawingBounds(); - updateTicks(); - } - }, - /** - * Continue a previously-start pan or zoom gesture. - * @param $event the mouse event - */ - continueDrag: function ($event) { - mousePosition = toMousePosition($event); - if (marqueeStart) { - updateMarqueeBox(); - } - if (panStart) { - updatePan(); - updateDrawingBounds(); - updateTicks(); - } - }, - /** - * Initiate a marquee zoom action. - * @param $event the mouse event - */ - startDrag: function ($event) { - subPlotBounds = $event.target.getBoundingClientRect(); - mousePosition = toMousePosition($event); - // Treat any modifier key as a pan - if ($event.altKey || $event.shiftKey || $event.ctrlKey) { - // Start panning - panStart = mousePosition; - panStartBounds = panZoomStack.getPanZoom(); - // We're starting a pan, so add this back as a - // state on the stack; it will get replaced - // during the pan. - panZoomStack.pushPanZoom( - panStartBounds.origin, - panStartBounds.dimensions - ); - $event.preventDefault(); - } else { - // Start marquee zooming - marqueeStart = mousePosition; - updateMarqueeBox(); - } - }, - /** - * Complete a marquee zoom action. - * @param $event the mouse event - */ - endDrag: function ($event) { - mousePosition = toMousePosition($event); - subPlotBounds = undefined; - if (marqueeStart) { - marqueeZoom(marqueeStart, mousePosition); - marqueeStart = undefined; - updateMarqueeBox(); - updateDrawingBounds(); - updateTicks(); - } - if (panStart) { - // End panning - panStart = undefined; - panStartBounds = undefined; - } - }, - /** - * Update the drawing bounds, marquee box, and - * tick marks for this subplot. - */ - update: function () { - updateDrawingBounds(); - updateMarqueeBox(); - updateTicks(); - }, - /** - * Set the domain offset associated with this sub-plot. - * A domain offset is subtracted from all domain - * before lines are drawn to avoid artifacts associated - * with the use of 32-bit floats when domain values - * are often timestamps (due to insufficient precision.) - * A SubPlot will be drawing boxes (for marquee zoom) in - * the same offset coordinate space, so it needs to know - * the value of this to position that marquee box - * correctly. - * @param {number} value the domain offset - */ - setDomainOffset: function (value) { - domainOffset = value; - }, - /** - * When used with no argument, check whether or not the user - * is currently hovering over this subplot. When used with - * an argument, set that state. - * @param {boolean} [state] the new hovering state - * @returns {boolean} the hovering state - */ - isHovering: function (state) { - if (state !== undefined) { - isHovering = state; - } - return isHovering; - } - }; - } + /** + * Update the drawing bounds, marquee box, and + * tick marks for this subplot. + */ + SubPlot.prototype.update = function () { + this.updateDrawingBounds(); + this.updateMarqueeBox(); + this.updateTicks(); + }; + + /** + * Set the domain offset associated with this sub-plot. + * A domain offset is subtracted from all domain + * before lines are drawn to avoid artifacts associated + * with the use of 32-bit floats when domain values + * are often timestamps (due to insufficient precision.) + * A SubPlot will be drawing boxes (for marquee zoom) in + * the same offset coordinate space, so it needs to know + * the value of this to position that marquee box + * correctly. + * @param {number} value the domain offset + */ + SubPlot.prototype.setDomainOffset = function (value) { + this.domainOffset = value; + }; + + /** + * When used with no argument, check whether or not the user + * is currently hovering over this subplot. When used with + * an argument, set that state. + * @param {boolean} [state] the new hovering state + * @returns {boolean} the hovering state + */ + SubPlot.prototype.isHovering = function (state) { + if (state !== undefined) { + this.hovering = state; + } + return this.hovering; + }; return SubPlot; } ); + diff --git a/platform/features/plot/src/SubPlotFactory.js b/platform/features/plot/src/SubPlotFactory.js index c883a488c2..6de318f106 100644 --- a/platform/features/plot/src/SubPlotFactory.js +++ b/platform/features/plot/src/SubPlotFactory.js @@ -31,31 +31,31 @@ define( * in a reference to the telemetryFormatter, which will be * used to represent telemetry values (timestamps or data * values) as human-readable strings. + * @memberof platform/features/plot * @constructor */ function SubPlotFactory(telemetryFormatter) { - return { - /** - * Instantiate a new sub-plot. - * @param {DomainObject[]} telemetryObjects the domain objects - * which will be plotted in this sub-plot - * @param {PlotPanZoomStack} panZoomStack the stack of pan-zoom - * states which is applicable to this sub-plot - * @returns {SubPlot} the instantiated sub-plot - * @method - * @memberof SubPlotFactory - */ - createSubPlot: function (telemetryObjects, panZoomStack) { - return new SubPlot( - telemetryObjects, - panZoomStack, - telemetryFormatter - ); - } - }; + this.telemetryFormatter = telemetryFormatter; } + /** + * Instantiate a new sub-plot. + * @param {DomainObject[]} telemetryObjects the domain objects + * which will be plotted in this sub-plot + * @param {PlotPanZoomStack} panZoomStack the stack of pan-zoom + * states which is applicable to this sub-plot + * @returns {SubPlot} the instantiated sub-plot + * @method + */ + SubPlotFactory.prototype.createSubPlot = function (telemetryObjects, panZoomStack) { + return new SubPlot( + telemetryObjects, + panZoomStack, + this.telemetryFormatter + ); + }; + return SubPlotFactory; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotAxis.js b/platform/features/plot/src/elements/PlotAxis.js index a523a7e352..25795fd347 100644 --- a/platform/features/plot/src/elements/PlotAxis.js +++ b/platform/features/plot/src/elements/PlotAxis.js @@ -31,6 +31,7 @@ define( * for the domain or range axis, sufficient to populate * selectors. * + * @memberof platform/features/plot * @constructor * @param {string} axisType the field in metadatas to * look at for axis options; usually one of @@ -61,27 +62,25 @@ define( (metadatas || []).forEach(buildOptionsForMetadata); - // Plot axis will be used directly from the Angular - // template, so expose properties directly to facilitate - // two-way data binding (for drop-down menus) - return { - /** - * The set of options applicable for this axis; - * an array of objects, where each object contains a - * "key" field and a "name" field (for machine- and - * human-readable names respectively) - */ - options: options, - /** - * The currently chosen option for this axis. An - * initial value is provided; this will be updated - * directly form the plot template. - */ - active: options[0] || defaultValue - }; + /** + * The currently chosen option for this axis. An + * initial value is provided; this will be updated + * directly form the plot template. + * @memberof platform/features/plot.PlotAxis# + */ + this.active = options[0] || defaultValue; + + /** + * The set of options applicable for this axis; + * an array of objects, where each object contains a + * "key" field and a "name" field (for machine- and + * human-readable names respectively) + * @memberof platform/features/plot.PlotAxis# + */ + this.options = options; } return PlotAxis; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotLimitTracker.js b/platform/features/plot/src/elements/PlotLimitTracker.js index 518344a08e..9309c50f32 100644 --- a/platform/features/plot/src/elements/PlotLimitTracker.js +++ b/platform/features/plot/src/elements/PlotLimitTracker.js @@ -21,25 +21,32 @@ *****************************************************************************/ /*global define,Float32Array*/ -/** - * Prepares data to be rendered in a GL Plot. Handles - * the conversion from data API to displayable buffers. - */ define( [], function () { 'use strict'; - var MAX_POINTS = 86400, - INITIAL_SIZE = 675; // 1/128 of MAX_POINTS - /** + * Tracks the limit state of telemetry objects being plotted. + * @memberof platform/features/plot * @constructor - * @param {TelemetryHandle} handle the handle to telemetry access + * @param {platform/telemetry.TelemetryHandle} handle the handle + * to telemetry access * @param {string} range the key to use when looking up range values */ function PlotLimitTracker(handle, range) { - var legendClasses = {}; + this.handle = handle; + this.range = range; + this.legendClasses = {}; + } + + /** + * Update limit states to reflect the latest data. + */ + PlotLimitTracker.prototype.update = function () { + var legendClasses = {}, + range = this.range, + handle = this.handle; function updateLimit(telemetryObject) { var limit = telemetryObject.getCapability('limit'), @@ -51,19 +58,23 @@ define( } } - return { - update: function () { - legendClasses = {}; - handle.getTelemetryObjects().forEach(updateLimit); - }, - getLegendClass: function (domainObject) { - var id = domainObject && domainObject.getId(); - return id && legendClasses[id]; - } - }; - } + handle.getTelemetryObjects().forEach(updateLimit); + + this.legendClasses = legendClasses; + }; + + /** + * Get the CSS class associated with any limit violations for this + * telemetry object. + * @param {DomainObject} domainObject the telemetry object to check + * @returns {string} the CSS class name, if any + */ + PlotLimitTracker.prototype.getLegendClass = function (domainObject) { + var id = domainObject && domainObject.getId(); + return id && this.legendClasses[id]; + }; return PlotLimitTracker; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotLine.js b/platform/features/plot/src/elements/PlotLine.js index df20fffa4c..abb23c8770 100644 --- a/platform/features/plot/src/elements/PlotLine.js +++ b/platform/features/plot/src/elements/PlotLine.js @@ -27,7 +27,53 @@ define( "use strict"; + /** + * Represents a single line or trace of a plot. + * @param {{PlotLineBuffer}} buffer the plot buffer + * @constructor + */ function PlotLine(buffer) { + this.buffer = buffer; + } + + /** + * Add a point to this plot line. + * @param {number} domainValue the domain value + * @param {number} rangeValue the range value + */ + PlotLine.prototype.addPoint = function (domainValue, rangeValue) { + var buffer = this.buffer, + index; + + // Make sure we got real/useful values here... + if (domainValue !== undefined && rangeValue !== undefined) { + index = buffer.findInsertionIndex(domainValue); + + // Already in the buffer? Skip insertion + if (index < 0) { + return; + } + + // Insert the point + if (!buffer.insertPoint(domainValue, rangeValue, index)) { + // If insertion failed, trim from the beginning... + buffer.trim(1); + // ...and try again. + buffer.insertPoint(domainValue, rangeValue, index); + } + } + }; + + /** + * Add a series of telemetry data to this plot line. + * @param {TelemetrySeries} series the data series + * @param {string} [domain] the key indicating which domain + * to use when looking up data from this series + * @param {string} [range] the key indicating which range + * to use when looking up data from this series + */ + PlotLine.prototype.addSeries = function (series, domain, range) { + var buffer = this.buffer; // Insert a time-windowed data series into the buffer function insertSeriesWindow(seriesWindow) { @@ -55,61 +101,20 @@ define( } } - function createWindow(series, domain, range) { - return new PlotSeriesWindow( - series, - domain, - range, - 0, - series.getPointCount() - ); - } - - return { - /** - * Add a point to this plot line. - * @param {number} domainValue the domain value - * @param {number} rangeValue the range value - */ - addPoint: function (domainValue, rangeValue) { - var index; - // Make sure we got real/useful values here... - if (domainValue !== undefined && rangeValue !== undefined) { - index = buffer.findInsertionIndex(domainValue); - - // Already in the buffer? Skip insertion - if (index < 0) { - return; - } - - // Insert the point - if (!buffer.insertPoint(domainValue, rangeValue, index)) { - // If insertion failed, trim from the beginning... - buffer.trim(1); - // ...and try again. - buffer.insertPoint(domainValue, rangeValue, index); - } - } - }, - /** - * Add a series of telemetry data to this plot line. - * @param {TelemetrySeries} series the data series - * @param {string} [domain] the key indicating which domain - * to use when looking up data from this series - * @param {string} [range] the key indicating which range - * to use when looking up data from this series - */ - addSeries: function (series, domain, range) { - // Should try to add via insertion if a - // clear insertion point is available; - // if not, should split and add each half. - // Insertion operation also needs to factor out - // redundant timestamps, for overlapping data - insertSeriesWindow(createWindow(series, domain, range)); - } - }; - } + // Should try to add via insertion if a + // clear insertion point is available; + // if not, should split and add each half. + // Insertion operation also needs to factor out + // redundant timestamps, for overlapping data + insertSeriesWindow(new PlotSeriesWindow( + series, + domain, + range, + 0, + series.getPointCount() + )); + }; return PlotLine; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotLineBuffer.js b/platform/features/plot/src/elements/PlotLineBuffer.js index e51e6e8a61..af0bbe553b 100644 --- a/platform/features/plot/src/elements/PlotLineBuffer.js +++ b/platform/features/plot/src/elements/PlotLineBuffer.js @@ -31,231 +31,240 @@ define( * @param {number} domainOffset number to subtract from domain values * @param {number} initialSize initial buffer size * @param {number} maxSize maximum buffer size + * @memberof platform/features/plot * @constructor */ function PlotLineBuffer(domainOffset, initialSize, maxSize) { - var buffer = new Float32Array(initialSize * 2), - rangeExtrema = [ Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY ], - length = 0; - - // Binary search for an insertion index - function binSearch(value, min, max) { - var mid = Math.floor((min + max) / 2), - found = buffer[mid * 2]; - - // On collisions, insert at same index - if (found === value) { - return mid; - } - - // Otherwise, if we're down to a single index, - // we've found our insertion point - if (min >= max) { - // Compare the found timestamp with the search - // value to decide if we'll insert after or before. - return min + ((found < value) ? 1 : 0); - } - - // Finally, do the recursive step - if (found < value) { - return binSearch(value, mid + 1, max); - } else { - return binSearch(value, min, mid - 1); - } - } - - // Increase the size of the buffer - function doubleBufferSize() { - var sz = Math.min(maxSize * 2, buffer.length * 2), - canDouble = sz > buffer.length, - doubled = canDouble && new Float32Array(sz); - - if (canDouble) { - doubled.set(buffer); // Copy contents of original - buffer = doubled; - } - - return canDouble; - } - - // Decrease the size of the buffer - function halveBufferSize() { - var sz = Math.max(initialSize * 2, buffer.length / 2), - canHalve = sz < buffer.length; - - if (canHalve) { - buffer = new Float32Array(buffer.subarray(0, sz)); - } - - return canHalve; - } - - // Set a value in the buffer - function setValue(index, domainValue, rangeValue) { - buffer[index * 2] = domainValue - domainOffset; - buffer[index * 2 + 1] = rangeValue; - // Track min/max of range values (min/max for - // domain values can be read directly from buffer) - rangeExtrema[0] = Math.min(rangeExtrema[0], rangeValue); - rangeExtrema[1] = Math.max(rangeExtrema[1], rangeValue); - } - - return { - /** - * Get the WebGL-displayable buffer of points to plot. - * @returns {Float32Array} displayable buffer for this line - */ - getBuffer: function () { - return buffer; - }, - /** - * Get the number of points stored in this buffer. - * @returns {number} the number of points stored - */ - getLength: function () { - return length; - }, - /** - * Get the min/max range values that are currently in this - * buffer. Unlike range extrema, these will change as the - * buffer gets trimmed. - * @returns {number[]} min, max domain values - */ - getDomainExtrema: function () { - // Since these are ordered in the buffer, assume - // these are the values at the first and last index - return [ - buffer[0] + domainOffset, - buffer[length * 2 - 2] + domainOffset - ]; - }, - /** - * Get the min/max range values that have been observed for this - * buffer. Note that these values may have been trimmed out at - * some point. - * @returns {number[]} min, max range values - */ - getRangeExtrema: function () { - return rangeExtrema; - }, - /** - * Remove values from this buffer. - * Normally, values are removed from the start - * of the buffer; a truthy value in the second argument - * will cause values to be removed from the end. - * @param {number} count number of values to remove - * @param {boolean} [fromEnd] true if the most recent - * values should be removed - */ - trim: function (count, fromEnd) { - // If we're removing values from the start... - if (!fromEnd) { - // ...do so by shifting buffer contents over - buffer.set(buffer.subarray(2 * count)); - } - // Reduce used buffer size accordingly - length -= count; - // Finally, if less than half of the buffer is being - // used, free up some memory. - if (length < buffer.length / 4) { - halveBufferSize(); - } - }, - /** - * Insert data from the provided series at the specified - * index. If this would exceed the buffer's maximum capacity, - * this operation fails and the buffer is unchanged. - * @param {TelemetrySeries} series the series to insert - * @param {number} index the index at which to insert this - * series - * @returns {boolean} true if insertion succeeded; otherwise - * false - */ - insert: function (series, index) { - var sz = series.getPointCount(), - i; - - // Don't allow append after the end; that doesn't make sense - index = Math.min(index, length); - - // Resize if necessary - while (sz > ((buffer.length / 2) - length)) { - if (!doubleBufferSize()) { - // Can't make room for this, insertion fails - return false; - } - } - - // Shift data over if necessary - if (index < length) { - buffer.set( - buffer.subarray(index * 2, length * 2), - (index + sz) * 2 - ); - } - - // Insert data into the set - for (i = 0; i < sz; i += 1) { - setValue( - i + index, - series.getDomainValue(i), - series.getRangeValue(i) - ); - } - - // Increase the length - length += sz; - - // Indicate that insertion was successful - return true; - }, - /** - * Append a single data point. - */ - insertPoint: function (domainValue, rangeValue, index) { - // Don't allow - index = Math.min(length, index); - - // Ensure there is space for this point - if (length >= (buffer.length / 2)) { - if (!doubleBufferSize()) { - return false; - } - } - - // Put the data in the buffer - setValue(length, domainValue, rangeValue); - - // Update length - length += 1; - - // Indicate that this was successful - return true; - }, - /** - * Find an index for inserting data with this - * timestamp. The second argument indicates whether - * we are searching for insert-before or insert-after - * positions. - * Timestamps are meant to be unique, so if a collision - * occurs, this will return -1. - * @param {number} timestamp timestamp to insert - * @returns {number} the index for insertion (or -1) - */ - findInsertionIndex: function (timestamp) { - var value = timestamp - domainOffset; - - // Handle empty buffer case and check for an - // append opportunity (which is most common case for - // real-time data so is optimized-for) before falling - // back to a binary search for the insertion point. - return (length < 1) ? 0 : - (value > buffer[length * 2 - 2]) ? length : - binSearch(value, 0, length - 1); - } - }; + this.buffer = new Float32Array(initialSize * 2); + this.rangeExtrema = + [ Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY ]; + this.length = 0; + this.domainOffset = domainOffset; + this.initialSize = initialSize; + this.maxSize = maxSize; } + // Binary search for an insertion index + PlotLineBuffer.prototype.binSearch = function (value, min, max) { + var mid = Math.floor((min + max) / 2), + found = this.buffer[mid * 2]; + + // On collisions, insert at same index + if (found === value) { + return mid; + } + + // Otherwise, if we're down to a single index, + // we've found our insertion point + if (min >= max) { + // Compare the found timestamp with the search + // value to decide if we'll insert after or before. + return min + ((found < value) ? 1 : 0); + } + + // Finally, do the recursive step + if (found < value) { + return this.binSearch(value, mid + 1, max); + } else { + return this.binSearch(value, min, mid - 1); + } + }; + + // Increase the size of the buffer + PlotLineBuffer.prototype.doubleBufferSize = function () { + var sz = Math.min(this.maxSize * 2, this.buffer.length * 2), + canDouble = sz > this.buffer.length, + doubled = canDouble && new Float32Array(sz); + + if (canDouble) { + doubled.set(this.buffer); // Copy contents of original + this.buffer = doubled; + } + + return canDouble; + }; + + // Decrease the size of the buffer + PlotLineBuffer.prototype.halveBufferSize = function () { + var sz = Math.max(this.initialSize * 2, this.buffer.length / 2), + canHalve = sz < this.buffer.length; + + if (canHalve) { + this.buffer = new Float32Array(this.buffer.subarray(0, sz)); + } + + return canHalve; + }; + + // Set a value in the buffer + PlotLineBuffer.prototype.setValue = function (index, domainValue, rangeValue) { + this.buffer[index * 2] = domainValue - this.domainOffset; + this.buffer[index * 2 + 1] = rangeValue; + // Track min/max of range values (min/max for + // domain values can be read directly from buffer) + this.rangeExtrema[0] = Math.min(this.rangeExtrema[0], rangeValue); + this.rangeExtrema[1] = Math.max(this.rangeExtrema[1], rangeValue); + }; + + /** + * Get the WebGL-displayable buffer of points to plot. + * @returns {Float32Array} displayable buffer for this line + */ + PlotLineBuffer.prototype.getBuffer = function () { + return this.buffer; + }; + + /** + * Get the number of points stored in this buffer. + * @returns {number} the number of points stored + */ + PlotLineBuffer.prototype.getLength = function () { + return this.length; + }; + + /** + * Get the min/max range values that are currently in this + * buffer. Unlike range extrema, these will change as the + * buffer gets trimmed. + * @returns {number[]} min, max domain values + */ + PlotLineBuffer.prototype.getDomainExtrema = function () { + // Since these are ordered in the buffer, assume + // these are the values at the first and last index + return [ + this.buffer[0] + this.domainOffset, + this.buffer[this.length * 2 - 2] + this.domainOffset + ]; + }; + + /** + * Get the min/max range values that have been observed for this + * buffer. Note that these values may have been trimmed out at + * some point. + * @returns {number[]} min, max range values + */ + PlotLineBuffer.prototype.getRangeExtrema = function () { + return this.rangeExtrema; + }; + + /** + * Remove values from this buffer. + * Normally, values are removed from the start + * of the buffer; a truthy value in the second argument + * will cause values to be removed from the end. + * @param {number} count number of values to remove + * @param {boolean} [fromEnd] true if the most recent + * values should be removed + */ + PlotLineBuffer.prototype.trim = function (count, fromEnd) { + // If we're removing values from the start... + if (!fromEnd) { + // ...do so by shifting buffer contents over + this.buffer.set(this.buffer.subarray(2 * count)); + } + // Reduce used buffer size accordingly + this.length -= count; + // Finally, if less than half of the buffer is being + // used, free up some memory. + if (this.length < this.buffer.length / 4) { + this.halveBufferSize(); + } + }; + + /** + * Insert data from the provided series at the specified + * index. If this would exceed the buffer's maximum capacity, + * this operation fails and the buffer is unchanged. + * @param {TelemetrySeries} series the series to insert + * @param {number} index the index at which to insert this + * series + * @returns {boolean} true if insertion succeeded; otherwise + * false + */ + PlotLineBuffer.prototype.insert = function (series, index) { + var sz = series.getPointCount(), + i; + + // Don't allow append after the end; that doesn't make sense + index = Math.min(index, this.length); + + // Resize if necessary + while (sz > ((this.buffer.length / 2) - this.length)) { + if (!this.doubleBufferSize()) { + // Can't make room for this, insertion fails + return false; + } + } + + // Shift data over if necessary + if (index < this.length) { + this.buffer.set( + this.buffer.subarray(index * 2, this.length * 2), + (index + sz) * 2 + ); + } + + // Insert data into the set + for (i = 0; i < sz; i += 1) { + this.setValue( + i + index, + series.getDomainValue(i), + series.getRangeValue(i) + ); + } + + // Increase the length + this.length += sz; + + // Indicate that insertion was successful + return true; + }; + + /** + * Append a single data point. + * @memberof platform/features/plot.PlotLineBuffer# + */ + PlotLineBuffer.prototype.insertPoint = function (domainValue, rangeValue) { + // Ensure there is space for this point + if (this.length >= (this.buffer.length / 2)) { + if (!this.doubleBufferSize()) { + return false; + } + } + + // Put the data in the buffer + this.setValue(this.length, domainValue, rangeValue); + + // Update length + this.length += 1; + + // Indicate that this was successful + return true; + }; + + /** + * Find an index for inserting data with this + * timestamp. The second argument indicates whether + * we are searching for insert-before or insert-after + * positions. + * Timestamps are meant to be unique, so if a collision + * occurs, this will return -1. + * @param {number} timestamp timestamp to insert + * @returns {number} the index for insertion (or -1) + */ + PlotLineBuffer.prototype.findInsertionIndex = function (timestamp) { + var value = timestamp - this.domainOffset; + + // Handle empty buffer case and check for an + // append opportunity (which is most common case for + // real-time data so is optimized-for) before falling + // back to a binary search for the insertion point. + return (this.length < 1) ? 0 : + (value > this.buffer[this.length * 2 - 2]) ? this.length : + this.binSearch(value, 0, this.length - 1); + }; + return PlotLineBuffer; } ); + diff --git a/platform/features/plot/src/elements/PlotPalette.js b/platform/features/plot/src/elements/PlotPalette.js index 8e61fe2f6e..f003317fec 100644 --- a/platform/features/plot/src/elements/PlotPalette.js +++ b/platform/features/plot/src/elements/PlotPalette.js @@ -78,6 +78,7 @@ define( * by index, in various color formats. All PlotPalette methods are * static, so there is no need for a constructor call; using * this will simply return PlotPalette itself. + * @memberof platform/features/plot * @constructor */ function PlotPalette() { @@ -131,4 +132,4 @@ define( return PlotPalette; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotPanZoomStack.js b/platform/features/plot/src/elements/PlotPanZoomStack.js index 32f3efd27a..1afb903056 100644 --- a/platform/features/plot/src/elements/PlotPanZoomStack.js +++ b/platform/features/plot/src/elements/PlotPanZoomStack.js @@ -37,122 +37,107 @@ define( * along the domain axis, and the second element describes the same * along the range axis. * + * @memberof platform/features/plot * @constructor * @param {number[]} origin the plot's origin, initially * @param {number[]} dimensions the plot's dimensions, initially */ function PlotPanZoomStack(origin, dimensions) { // Use constructor parameters as the stack's initial state - var stack = [{ origin: origin, dimensions: dimensions }]; - - // Various functions which follow are simply wrappers for - // normal stack-like array methods, with the exception that - // they prevent undesired modification and enforce that this - // stack must remain non-empty. - // See JSDoc for specific methods below for more detail. - function getDepth() { - return stack.length; - } - - function pushPanZoom(origin, dimensions) { - stack.push({ origin: origin, dimensions: dimensions }); - } - - function popPanZoom() { - if (stack.length > 1) { - stack.pop(); - } - } - - function clearPanZoom() { - stack = [stack[0]]; - } - - function setBasePanZoom(origin, dimensions) { - stack[0] = { origin: origin, dimensions: dimensions }; - } - - function getPanZoom() { - return stack[stack.length - 1]; - } - - function getOrigin() { - return getPanZoom().origin; - } - - function getDimensions() { - return getPanZoom().dimensions; - } - - return { - /** - * Get the current stack depth; that is, the number - * of items on the stack. A depth of one means that no - * panning or zooming relative to the base value has - * been applied. - * @returns {number} the depth of the stack - */ - getDepth: getDepth, - - /** - * Push a new pan-zoom state onto the stack; this will - * become the active pan-zoom state. - * @param {number[]} origin the new origin - * @param {number[]} dimensions the new dimensions - */ - pushPanZoom: pushPanZoom, - - /** - * Pop a pan-zoom state from the stack. Whatever pan-zoom - * state was previously present will become current. - * If called when there is only one pan-zoom state on the - * stack, this acts as a no-op (that is, the lowest - * pan-zoom state on the stack cannot be popped, to ensure - * that some pan-zoom state is always available.) - */ - popPanZoom: popPanZoom, - - /** - * Set the base pan-zoom state; that is, the state at the - * bottom of the stack. This allows the "unzoomed" state of - * a plot to be updated (e.g. as new data comes in) without - * interfering with the user's chosen zoom level. - * @param {number[]} origin the base origin - * @param {number[]} dimensions the base dimensions - */ - setBasePanZoom: setBasePanZoom, - - /** - * Clear the pan-zoom stack down to its bottom element; - * in effect, pop all elements but the last, e.g. to remove - * any temporary user modifications to pan-zoom state. - */ - clearPanZoom: clearPanZoom, - - /** - * Get the current pan-zoom state (the state at the top - * of the stack), expressed as an object with "origin" and - * "dimensions" fields. - * @returns {object} the current pan-zoom state - */ - getPanZoom: getPanZoom, - - /** - * Get the current origin, as represented on the top of the - * stack. - * @returns {number[]} the current plot origin - */ - getOrigin: getOrigin, - - /** - * Get the current dimensions, as represented on the top of - * the stack. - * @returns {number[]} the current plot dimensions - */ - getDimensions: getDimensions - }; + this.stack = [{ origin: origin, dimensions: dimensions }]; } + // Various functions which follow are simply wrappers for + // normal stack-like array methods, with the exception that + // they prevent undesired modification and enforce that this + // stack must remain non-empty. + // See JSDoc for specific methods below for more detail. + + /** + * Get the current stack depth; that is, the number + * of items on the stack. A depth of one means that no + * panning or zooming relative to the base value has + * been applied. + * @returns {number} the depth of the stack + */ + PlotPanZoomStack.prototype.getDepth = function getDepth() { + return this.stack.length; + }; + + /** + * Push a new pan-zoom state onto the stack; this will + * become the active pan-zoom state. + * @param {number[]} origin the new origin + * @param {number[]} dimensions the new dimensions + */ + PlotPanZoomStack.prototype.pushPanZoom = function (origin, dimensions) { + this.stack.push({ origin: origin, dimensions: dimensions }); + }; + + /** + * Pop a pan-zoom state from the stack. Whatever pan-zoom + * state was previously present will become current. + * If called when there is only one pan-zoom state on the + * stack, this acts as a no-op (that is, the lowest + * pan-zoom state on the stack cannot be popped, to ensure + * that some pan-zoom state is always available.) + */ + PlotPanZoomStack.prototype.popPanZoom = function popPanZoom() { + if (this.stack.length > 1) { + this.stack.pop(); + } + }; + + /** + * Set the base pan-zoom state; that is, the state at the + * bottom of the stack. This allows the "unzoomed" state of + * a plot to be updated (e.g. as new data comes in) without + * interfering with the user's chosen zoom level. + * @param {number[]} origin the base origin + * @param {number[]} dimensions the base dimensions + * @memberof platform/features/plot.PlotPanZoomStack# + */ + PlotPanZoomStack.prototype.setBasePanZoom = function (origin, dimensions) { + this.stack[0] = { origin: origin, dimensions: dimensions }; + }; + + /** + * Clear the pan-zoom stack down to its bottom element; + * in effect, pop all elements but the last, e.g. to remove + * any temporary user modifications to pan-zoom state. + */ + PlotPanZoomStack.prototype.clearPanZoom = function clearPanZoom() { + this.stack = [this.stack[0]]; + }; + + /** + * Get the current pan-zoom state (the state at the top + * of the stack), expressed as an object with "origin" and + * "dimensions" fields. + * @returns {object} the current pan-zoom state + */ + PlotPanZoomStack.prototype.getPanZoom = function getPanZoom() { + return this.stack[this.stack.length - 1]; + }; + + /** + * Get the current origin, as represented on the top of the + * stack. + * @returns {number[]} the current plot origin + */ + PlotPanZoomStack.prototype.getOrigin = function getOrigin() { + return this.getPanZoom().origin; + }; + + /** + * Get the current dimensions, as represented on the top of + * the stack. + * @returns {number[]} the current plot dimensions + */ + PlotPanZoomStack.prototype.getDimensions = function getDimensions() { + return this.getPanZoom().dimensions; + }; + return PlotPanZoomStack; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotPanZoomStackGroup.js b/platform/features/plot/src/elements/PlotPanZoomStackGroup.js index 6280c42def..3746958ecb 100644 --- a/platform/features/plot/src/elements/PlotPanZoomStackGroup.js +++ b/platform/features/plot/src/elements/PlotPanZoomStackGroup.js @@ -32,20 +32,19 @@ define( * remain independent upon the range axis. This supports panning * and zooming in stacked-plot mode (and, importantly, * stepping back through those states.) + * @memberof platform/features/plot * @constructor * @param {number} count the number of stacks to include in this * group */ function PlotPanZoomStackGroup(count) { - var stacks = [], - decoratedStacks = [], - i; + var self = this; // Push a pan-zoom state; the index argument identifies // which stack originated the request (all other stacks // will ignore the range part of the change.) function pushPanZoom(origin, dimensions, index) { - stacks.forEach(function (stack, i) { + self.stacks.forEach(function (stack, i) { if (i === index) { // Do a normal push for the specified stack stack.pushPanZoom(origin, dimensions); @@ -60,26 +59,6 @@ define( }); } - // Pop one pan-zoom state from all stacks - function popPanZoom() { - stacks.forEach(function (stack) { - stack.popPanZoom(); - }); - } - - // Set the base pan-zoom state for all stacks - function setBasePanZoom(origin, dimensions) { - stacks.forEach(function (stack) { - stack.setBasePanZoom(origin, dimensions); - }); - } - - // Clear the pan-zoom state of all stacks - function clearPanZoom() { - stacks.forEach(function (stack) { - stack.clearPanZoom(); - }); - } // Decorate a pan-zoom stack; returns an object with // the same interface, but whose stack-mutation methods @@ -91,83 +70,101 @@ define( result.pushPanZoom = function (origin, dimensions) { pushPanZoom(origin, dimensions, index); }; - result.setBasePanZoom = setBasePanZoom; - result.popPanZoom = popPanZoom; - result.clearPanZoom = clearPanZoom; + result.setBasePanZoom = function () { + self.setBasePanZoom.apply(self, arguments); + }; + result.popPanZoom = function () { + self.popPanZoom.apply(self, arguments); + }; + result.clearPanZoom = function () { + self.clearPanZoom.apply(self, arguments); + }; return result; } // Create the stacks in this group ... - while (stacks.length < count) { - stacks.push(new PlotPanZoomStack([], [])); + this.stacks = []; + while (this.stacks.length < count) { + this.stacks.push(new PlotPanZoomStack([], [])); } // ... and their decorated-to-synchronize versions. - decoratedStacks = stacks.map(decorateStack); - - return { - /** - * Pop a pan-zoom state from all stacks in the group. - * If called when there is only one pan-zoom state on each - * stack, this acts as a no-op (that is, the lowest - * pan-zoom state on the stack cannot be popped, to ensure - * that some pan-zoom state is always available.) - */ - popPanZoom: popPanZoom, - - /** - * Set the base pan-zoom state for all stacks in this group. - * This changes the state at the bottom of each stack. - * This allows the "unzoomed" state of plots to be updated - * (e.g. as new data comes in) without - * interfering with the user's chosen pan/zoom states. - * @param {number[]} origin the base origin - * @param {number[]} dimensions the base dimensions - */ - setBasePanZoom: setBasePanZoom, - - /** - * Clear all pan-zoom stacks in this group down to - * their bottom element; in effect, pop all elements - * but the last, e.g. to remove any temporary user - * modifications to pan-zoom state. - */ - clearPanZoom: clearPanZoom, - /** - * Get the current stack depth; that is, the number - * of items on each stack in the group. - * A depth of one means that no - * panning or zooming relative to the base value has - * been applied. - * @returns {number} the depth of the stacks in this group - */ - getDepth: function () { - // All stacks are kept in sync, so look up depth - // from the first one. - return stacks.length > 0 ? - stacks[0].getDepth() : 0; - }, - /** - * Get a specific pan-zoom stack in this group. - * Stacks are specified by index; this index must be less - * than the count provided at construction time, and must - * not be less than zero. - * The stack returned by this function will be synchronized - * to other stacks in this group; that is, mutating that - * stack directly will result in other stacks in this group - * undergoing similar updates to ensure that domain bounds - * remain the same. - * @param {number} index the index of the stack to get - * @returns {PlotPanZoomStack} the pan-zoom stack in the - * group identified by that index - */ - getPanZoomStack: function (index) { - return decoratedStacks[index]; - } - }; - + this.decoratedStacks = this.stacks.map(decorateStack); } + /** + * Pop a pan-zoom state from all stacks in the group. + * If called when there is only one pan-zoom state on each + * stack, this acts as a no-op (that is, the lowest + * pan-zoom state on the stack cannot be popped, to ensure + * that some pan-zoom state is always available.) + */ + PlotPanZoomStackGroup.prototype.popPanZoom = function () { + this.stacks.forEach(function (stack) { + stack.popPanZoom(); + }); + }; + + /** + * Set the base pan-zoom state for all stacks in this group. + * This changes the state at the bottom of each stack. + * This allows the "unzoomed" state of plots to be updated + * (e.g. as new data comes in) without + * interfering with the user's chosen pan/zoom states. + * @param {number[]} origin the base origin + * @param {number[]} dimensions the base dimensions + */ + PlotPanZoomStackGroup.prototype.setBasePanZoom = function (origin, dimensions) { + this.stacks.forEach(function (stack) { + stack.setBasePanZoom(origin, dimensions); + }); + }; + + /** + * Clear all pan-zoom stacks in this group down to + * their bottom element; in effect, pop all elements + * but the last, e.g. to remove any temporary user + * modifications to pan-zoom state. + */ + PlotPanZoomStackGroup.prototype.clearPanZoom = function () { + this.stacks.forEach(function (stack) { + stack.clearPanZoom(); + }); + }; + + /** + * Get the current stack depth; that is, the number + * of items on each stack in the group. + * A depth of one means that no + * panning or zooming relative to the base value has + * been applied. + * @returns {number} the depth of the stacks in this group + */ + PlotPanZoomStackGroup.prototype.getDepth = function () { + // All stacks are kept in sync, so look up depth + // from the first one. + return this.stacks.length > 0 ? + this.stacks[0].getDepth() : 0; + }; + + /** + * Get a specific pan-zoom stack in this group. + * Stacks are specified by index; this index must be less + * than the count provided at construction time, and must + * not be less than zero. + * The stack returned by this function will be synchronized + * to other stacks in this group; that is, mutating that + * stack directly will result in other stacks in this group + * undergoing similar updates to ensure that domain bounds + * remain the same. + * @param {number} index the index of the stack to get + * @returns {PlotPanZoomStack} the pan-zoom stack in the + * group identified by that index + */ + PlotPanZoomStackGroup.prototype.getPanZoomStack = function (index) { + return this.decoratedStacks[index]; + }; + return PlotPanZoomStackGroup; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotPosition.js b/platform/features/plot/src/elements/PlotPosition.js index 462e0882fd..d8369faf33 100644 --- a/platform/features/plot/src/elements/PlotPosition.js +++ b/platform/features/plot/src/elements/PlotPosition.js @@ -36,6 +36,7 @@ define( * PlotPosition was instantiated. Care should be taken when retaining * PlotPosition objects across changes to the pan-zoom stack. * + * @memberof platform/features/plot * @constructor * @param {number} x the horizontal pixel position in the plot area * @param {number} y the vertical pixel position in the plot area @@ -47,8 +48,7 @@ define( function PlotPosition(x, y, width, height, panZoomStack) { var panZoom = panZoomStack.getPanZoom(), origin = panZoom.origin, - dimensions = panZoom.dimensions, - position; + dimensions = panZoom.dimensions; function convert(v, i) { return v * dimensions[i] + origin[i]; @@ -56,42 +56,42 @@ define( if (!dimensions || !origin) { // We need both dimensions and origin to compute a position - position = []; + this.position = []; } else { // Convert from pixel to domain-range space. // Note that range is reversed from the y-axis in pixel space //(positive range points up, positive pixel-y points down) - position = [ x / width, (height - y) / height ].map(convert); + this.position = + [ x / width, (height - y) / height ].map(convert); } - - return { - /** - * Get the domain value corresponding to this pixel position. - * @returns {number} the domain value - */ - getDomain: function () { - return position[0]; - }, - /** - * Get the range value corresponding to this pixel position. - * @returns {number} the range value - */ - getRange: function () { - return position[1]; - }, - /** - * Get the domain and values corresponding to this - * pixel position. - * @returns {number[]} an array containing the domain and - * the range value, in that order - */ - getPosition: function () { - return position; - } - }; - } + /** + * Get the domain value corresponding to this pixel position. + * @returns {number} the domain value + */ + PlotPosition.prototype.getDomain = function () { + return this.position[0]; + }; + + /** + * Get the range value corresponding to this pixel position. + * @returns {number} the range value + */ + PlotPosition.prototype.getRange = function () { + return this.position[1]; + }; + + /** + * Get the domain and values corresponding to this + * pixel position. + * @returns {number[]} an array containing the domain and + * the range value, in that order + */ + PlotPosition.prototype.getPosition = function () { + return this.position; + }; + return PlotPosition; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotPreparer.js b/platform/features/plot/src/elements/PlotPreparer.js index e301b693c0..a1ca0cc5b5 100644 --- a/platform/features/plot/src/elements/PlotPreparer.js +++ b/platform/features/plot/src/elements/PlotPreparer.js @@ -36,6 +36,7 @@ define( * preparing them to be rendered. It creates a WebGL-plottable * Float32Array for each trace, and tracks the boundaries of the * data sets (since this is convenient to do during the same pass). + * @memberof platform/features/plot * @constructor * @param {Telemetry[]} datas telemetry data objects * @param {string} domain the key to use when looking up domain values @@ -48,8 +49,7 @@ define( min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], x, y, - domainOffset = Number.POSITIVE_INFINITY, - buffers; + domainOffset = Number.POSITIVE_INFINITY; // Remove any undefined data sets datas = (datas || []).filter(identity); @@ -84,62 +84,70 @@ define( } // Convert to Float32Array - buffers = vertices.map(function (v) { return new Float32Array(v); }); + this.buffers = vertices.map(function (v) { + return new Float32Array(v); + }); - return { - /** - * Get the dimensions which bound all data in the provided - * data sets. This is given as a two-element array where the - * first element is domain, and second is range. - * @returns {number[]} the dimensions which bound this data set - */ - getDimensions: function () { - return [max[0] - min[0], max[1] - min[1]]; - }, - /** - * Get the origin of this data set's boundary. - * This is given as a two-element array where the - * first element is domain, and second is range. - * The domain value here is not adjusted by the domain offset. - * @returns {number[]} the origin of this data set's boundary - */ - getOrigin: function () { - return min; - }, - /** - * Get the domain offset; this offset will have been subtracted - * from all domain values in all buffers returned by this - * preparer, in order to minimize loss-of-precision due to - * conversion to the 32-bit float format needed by WebGL. - * @returns {number} the domain offset - */ - getDomainOffset: function () { - return domainOffset; - }, - /** - * Get all renderable buffers for this data set. This will - * be returned as an array which can be correlated back to - * the provided telemetry data objects (from the constructor - * call) by index. - * - * Internally, these are flattened; each buffer contains a - * sequence of alternating domain and range values. - * - * All domain values in all buffers will have been adjusted - * from their original values by subtraction of the domain - * offset; this minimizes loss-of-precision resulting from - * the conversion to 32-bit floats, which may otherwise - * cause aliasing artifacts (particularly for timestamps) - * - * @returns {Float32Array[]} the buffers for these traces - */ - getBuffers: function () { - return buffers; - } - }; + this.min = min; + this.max = max; + this.domainOffset = domainOffset; } + /** + * Get the dimensions which bound all data in the provided + * data sets. This is given as a two-element array where the + * first element is domain, and second is range. + * @returns {number[]} the dimensions which bound this data set + */ + PlotPreparer.prototype.getDimensions = function () { + var max = this.max, min = this.min; + return [max[0] - min[0], max[1] - min[1]]; + }; + + /** + * Get the origin of this data set's boundary. + * This is given as a two-element array where the + * first element is domain, and second is range. + * The domain value here is not adjusted by the domain offset. + * @returns {number[]} the origin of this data set's boundary + */ + PlotPreparer.prototype.getOrigin = function () { + return this.min; + }; + + /** + * Get the domain offset; this offset will have been subtracted + * from all domain values in all buffers returned by this + * preparer, in order to minimize loss-of-precision due to + * conversion to the 32-bit float format needed by WebGL. + * @returns {number} the domain offset + */ + PlotPreparer.prototype.getDomainOffset = function () { + return this.domainOffset; + }; + + /** + * Get all renderable buffers for this data set. This will + * be returned as an array which can be correlated back to + * the provided telemetry data objects (from the constructor + * call) by index. + * + * Internally, these are flattened; each buffer contains a + * sequence of alternating domain and range values. + * + * All domain values in all buffers will have been adjusted + * from their original values by subtraction of the domain + * offset; this minimizes loss-of-precision resulting from + * the conversion to 32-bit floats, which may otherwise + * cause aliasing artifacts (particularly for timestamps) + * + * @returns {Float32Array[]} the buffers for these traces + */ + PlotPreparer.prototype.getBuffers = function () { + return this.buffers; + }; + return PlotPreparer; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotSeriesWindow.js b/platform/features/plot/src/elements/PlotSeriesWindow.js index 7cfb89601a..4bf880a239 100644 --- a/platform/features/plot/src/elements/PlotSeriesWindow.js +++ b/platform/features/plot/src/elements/PlotSeriesWindow.js @@ -28,41 +28,55 @@ define( /** * Provides a window on a telemetry data series, to support * insertion into a plot line. + * @constructor + * @memberof platform/features/plot + * @implements {TelemetrySeries} */ function PlotSeriesWindow(series, domain, range, start, end) { - return { - getPointCount: function () { - return end - start; - }, - getDomainValue: function (index) { - return series.getDomainValue(index + start, domain); - }, - getRangeValue: function (index) { - return series.getRangeValue(index + start, range); - }, - split: function () { - var mid = Math.floor((end + start) / 2); - return ((end - start) > 1) ? - [ - new PlotSeriesWindow( - series, - domain, - range, - start, - mid - ), - new PlotSeriesWindow( - series, - domain, - range, - mid, - end - ) - ] : []; - } - }; + this.series = series; + this.domain = domain; + this.range = range; + this.start = start; + this.end = end; } + PlotSeriesWindow.prototype.getPointCount = function () { + return this.end - this.start; + }; + + PlotSeriesWindow.prototype.getDomainValue = function (index) { + return this.series.getDomainValue(index + this.start, this.domain); + }; + + PlotSeriesWindow.prototype.getRangeValue = function (index) { + return this.series.getRangeValue(index + this.start, this.range); + }; + + /** + * Split this series into two series of equal (or nearly-equal) size. + * @returns {PlotSeriesWindow[]} two series + */ + PlotSeriesWindow.prototype.split = function () { + var mid = Math.floor((this.end + this.start) / 2); + return ((this.end - this.start) > 1) ? + [ + new PlotSeriesWindow( + this.series, + this.domain, + this.range, + this.start, + mid + ), + new PlotSeriesWindow( + this.series, + this.domain, + this.range, + mid, + this.end + ) + ] : []; + }; + return PlotSeriesWindow; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotTickGenerator.js b/platform/features/plot/src/elements/PlotTickGenerator.js index 714588b31f..af18050955 100644 --- a/platform/features/plot/src/elements/PlotTickGenerator.js +++ b/platform/features/plot/src/elements/PlotTickGenerator.js @@ -31,6 +31,7 @@ define( * domain and range axes of the plot, to support the plot * template. * + * @memberof platform/features/plot * @constructor * @param {PlotPanZoomStack} panZoomStack the pan-zoom stack for * this plot, used to determine plot boundaries @@ -38,59 +39,57 @@ define( * domain and range values. */ function PlotTickGenerator(panZoomStack, formatter) { + this.panZoomStack = panZoomStack; + this.formatter = formatter; + } - // Generate ticks; interpolate from start up to - // start + span in count steps, using the provided - // formatter to represent each value. - function generateTicks(start, span, count, format) { - var step = span / (count - 1), - result = [], - i; + // Generate ticks; interpolate from start up to + // start + span in count steps, using the provided + // formatter to represent each value. + PlotTickGenerator.prototype.generateTicks = function (start, span, count, format) { + var step = span / (count - 1), + result = [], + i; - for (i = 0; i < count; i += 1) { - result.push({ - label: format(i * step + start) - }); - } - - return result; + for (i = 0; i < count; i += 1) { + result.push({ + label: format(i * step + start) + }); } + return result; + }; - return { - /** - * Generate tick marks for the domain axis. - * @param {number} count the number of ticks - * @returns {string[]} labels for those ticks - */ - generateDomainTicks: function (count) { - var panZoom = panZoomStack.getPanZoom(); - return generateTicks( - panZoom.origin[0], - panZoom.dimensions[0], - count, - formatter.formatDomainValue - ); - }, + /** + * Generate tick marks for the domain axis. + * @param {number} count the number of ticks + * @returns {string[]} labels for those ticks + */ + PlotTickGenerator.prototype.generateDomainTicks = function (count) { + var panZoom = this.panZoomStack.getPanZoom(); + return this.generateTicks( + panZoom.origin[0], + panZoom.dimensions[0], + count, + this.formatter.formatDomainValue + ); + }; - /** - * Generate tick marks for the range axis. - * @param {number} count the number of ticks - * @returns {string[]} labels for those ticks - */ - generateRangeTicks: function (count) { - var panZoom = panZoomStack.getPanZoom(); - return generateTicks( - panZoom.origin[1], - panZoom.dimensions[1], - count, - formatter.formatRangeValue - ); - } - }; - - } + /** + * Generate tick marks for the range axis. + * @param {number} count the number of ticks + * @returns {string[]} labels for those ticks + */ + PlotTickGenerator.prototype.generateRangeTicks = function (count) { + var panZoom = this.panZoomStack.getPanZoom(); + return this.generateTicks( + panZoom.origin[1], + panZoom.dimensions[1], + count, + this.formatter.formatRangeValue + ); + }; return PlotTickGenerator; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index caf3abf3a2..851fa56096 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -21,10 +21,6 @@ *****************************************************************************/ /*global define,Float32Array*/ -/** - * Prepares data to be rendered in a GL Plot. Handles - * the conversion from data API to displayable buffers. - */ define( ['./PlotLine', './PlotLineBuffer'], function (PlotLine, PlotLineBuffer) { @@ -39,301 +35,289 @@ define( * preparing them to be rendered. It creates a WebGL-plottable * Float32Array for each trace, and tracks the boundaries of the * data sets (since this is convenient to do during the same pass). + * @memberof platform/features/plot * @constructor * @param {TelemetryHandle} handle the handle to telemetry access * @param {string} domain the key to use when looking up domain values * @param {string} range the key to use when looking up range values - * @param {number} maxDuration maximum plot duration to display + * @param {number} fixedDuration maximum plot duration to display * @param {number} maxPoints maximum number of points to display */ function PlotUpdater(handle, domain, range, fixedDuration, maxPoints) { - var ids = [], - lines = {}, - dimensions = [0, 0], - origin = [0, 0], - domainExtrema, - rangeExtrema, - buffers = {}, - bufferArray = [], - domainOffset; + this.handle = handle; + this.domain = domain; + this.range = range; + this.fixedDuration = fixedDuration; + this.maxPoints = maxPoints; - // Look up a domain object's id (for mapping, below) - function getId(domainObject) { - return domainObject.getId(); - } - - // Check if this set of ids matches the current set of ids - // (used to detect if line preparation can be skipped) - function idsMatch(nextIds) { - return ids.length === nextIds.length && - nextIds.every(function (id, index) { - return ids[index] === id; - }); - } - - // Prepare plot lines for this group of telemetry objects - function prepareLines(telemetryObjects) { - var nextIds = telemetryObjects.map(getId), - next = {}; - - // Detect if we already have everything we need prepared - if (idsMatch(nextIds)) { - // Nothing to prepare, move on - return; - } - - // Built up a set of ids. Note that we can only - // create plot lines after our domain offset has - // been determined. - if (domainOffset !== undefined) { - // Update list of ids in use - ids = nextIds; - - // Create buffers for these objects - bufferArray = ids.map(function (id) { - buffers[id] = buffers[id] || new PlotLineBuffer( - domainOffset, - INITIAL_SIZE, - maxPoints - ); - next[id] = lines[id] || new PlotLine(buffers[id]); - return buffers[id]; - }); - } - - // If there are no more lines, clear the domain offset - if (Object.keys(next).length < 1) { - domainOffset = undefined; - } - - // Update to the current set of lines - lines = next; - } - - - // Initialize the domain offset, based on these observed values - function initializeDomainOffset(values) { - domainOffset = - ((domainOffset === undefined) && (values.length > 0)) ? - (values.reduce(function (a, b) { - return (a || 0) + (b || 0); - }, 0) / values.length) : - domainOffset; - } - - // Used in the reduce step of updateExtrema - function reduceExtrema(a, b) { - return [ Math.min(a[0], b[0]), Math.max(a[1], b[1]) ]; - } - - // Convert a domain/range extrema to plot dimensions - function dimensionsOf(extrema) { - return extrema[1] - extrema[0]; - } - - // Convert a domain/range extrema to a plot origin - function originOf(extrema) { - return extrema[0]; - } - - // Expand range slightly so points near edges are visible - function expandRange() { - var padding = PADDING_RATIO * dimensions[1], - top; - padding = Math.max(padding, 1.0); - top = Math.ceil(origin[1] + dimensions[1] + padding / 2); - origin[1] = Math.floor(origin[1] - padding / 2); - dimensions[1] = top - origin[1]; - } - - // Update dimensions and origin based on extrema of plots - function updateBounds() { - if (bufferArray.length > 0) { - domainExtrema = bufferArray.map(function (lineBuffer) { - return lineBuffer.getDomainExtrema(); - }).reduce(reduceExtrema); - - rangeExtrema = bufferArray.map(function (lineBuffer) { - return lineBuffer.getRangeExtrema(); - }).reduce(reduceExtrema); - - // Calculate best-fit dimensions - dimensions = - [dimensionsOf(domainExtrema), dimensionsOf(rangeExtrema)]; - origin = [originOf(domainExtrema), originOf(rangeExtrema)]; - - // Enforce some minimum visible area - expandRange(); - - // ...then enforce a fixed duration if needed - if (fixedDuration !== undefined) { - origin[0] = origin[0] + dimensions[0] - fixedDuration; - dimensions[0] = fixedDuration; - } - } - } - - // Enforce maximum duration on all plot lines; not that - // domain extrema must be up-to-date for this to behave correctly. - function enforceDuration() { - var cutoff; - - function enforceDurationForBuffer(plotLineBuffer) { - var index = plotLineBuffer.findInsertionIndex(cutoff); - if (index > 0) { - // Leave one point untrimmed, such that line will - // continue off left edge of visible plot area. - plotLineBuffer.trim(index - 1); - } - } - - if (fixedDuration !== undefined && - domainExtrema !== undefined && - (domainExtrema[1] - domainExtrema[0] > fixedDuration)) { - cutoff = domainExtrema[1] - fixedDuration; - bufferArray.forEach(enforceDurationForBuffer); - updateBounds(); // Extrema may have changed now - } - } - - // Add latest data for this domain object - function addPointFor(domainObject) { - var line = lines[domainObject.getId()]; - if (line) { - line.addPoint( - handle.getDomainValue(domainObject, domain), - handle.getRangeValue(domainObject, range) - ); - } - } - - // Handle new telemetry data - function update() { - var objects = handle.getTelemetryObjects(); - - // Initialize domain offset if necessary - if (domainOffset === undefined) { - initializeDomainOffset(objects.map(function (obj) { - return handle.getDomainValue(obj, domain); - }).filter(function (value) { - return typeof value === 'number'; - })); - } - - // Make sure lines are available - prepareLines(objects); - - // Add new data - objects.forEach(addPointFor); - - // Then, update extrema - updateBounds(); - } - - // Add historical data for this domain object - function setHistorical(domainObject, series) { - var count = series ? series.getPointCount() : 0, - line; - - // Nothing to do if it's an empty series - if (count < 1) { - return; - } - - // Initialize domain offset if necessary - if (domainOffset === undefined) { - initializeDomainOffset([ - series.getDomainValue(0, domain), - series.getDomainValue(count - 1, domain) - ]); - } - - // Make sure lines are available - prepareLines(handle.getTelemetryObjects()); - - // Look up the line for this domain object - line = lines[domainObject.getId()]; - - // ...and put the data into it. - if (line) { - line.addSeries(series, domain, range); - } - - // Update extrema - updateBounds(); - } + this.ids = []; + this.lines = {}; + this.buffers = {}; + this.bufferArray = []; // Use a default MAX_POINTS if none is provided - maxPoints = maxPoints !== undefined ? maxPoints : MAX_POINTS; + this.maxPoints = maxPoints !== undefined ? maxPoints : MAX_POINTS; + this.dimensions = [0, 0]; + this.origin = [0, 0]; // Initially prepare state for these objects. // Note that this may be an empty array at this time, // so we also need to check during update cycles. - update(); - - return { - /** - * Get the dimensions which bound all data in the provided - * data sets. This is given as a two-element array where the - * first element is domain, and second is range. - * @returns {number[]} the dimensions which bound this data set - */ - getDimensions: function () { - return dimensions; - }, - /** - * Get the origin of this data set's boundary. - * This is given as a two-element array where the - * first element is domain, and second is range. - * The domain value here is not adjusted by the domain offset. - * @returns {number[]} the origin of this data set's boundary - */ - getOrigin: function () { - // Pad range if necessary - return origin; - }, - /** - * Get the domain offset; this offset will have been subtracted - * from all domain values in all buffers returned by this - * preparer, in order to minimize loss-of-precision due to - * conversion to the 32-bit float format needed by WebGL. - * @returns {number} the domain offset - */ - getDomainOffset: function () { - return domainOffset; - }, - /** - * Get all renderable buffers for this data set. This will - * be returned as an array which can be correlated back to - * the provided telemetry data objects (from the constructor - * call) by index. - * - * Internally, these are flattened; each buffer contains a - * sequence of alternating domain and range values. - * - * All domain values in all buffers will have been adjusted - * from their original values by subtraction of the domain - * offset; this minimizes loss-of-precision resulting from - * the conversion to 32-bit floats, which may otherwise - * cause aliasing artifacts (particularly for timestamps) - * - * @returns {Float32Array[]} the buffers for these traces - */ - getLineBuffers: function () { - return bufferArray; - }, - /** - * Update with latest data. - */ - update: update, - /** - * Fill in historical data. - */ - addHistorical: setHistorical - }; + this.update(); } + // Look up a domain object's id (for mapping, below) + function getId(domainObject) { + return domainObject.getId(); + } + + // Used in the reduce step of updateExtrema + function reduceExtrema(a, b) { + return [ Math.min(a[0], b[0]), Math.max(a[1], b[1]) ]; + } + + // Convert a domain/range extrema to plot dimensions + function dimensionsOf(extrema) { + return extrema[1] - extrema[0]; + } + + // Convert a domain/range extrema to a plot origin + function originOf(extrema) { + return extrema[0]; + } + + // Check if this set of ids matches the current set of ids + // (used to detect if line preparation can be skipped) + PlotUpdater.prototype.idsMatch = function (nextIds) { + var ids = this.ids; + return ids.length === nextIds.length && + nextIds.every(function (id, index) { + return ids[index] === id; + }); + }; + + // Prepare plot lines for this group of telemetry objects + PlotUpdater.prototype.prepareLines = function (telemetryObjects) { + var nextIds = telemetryObjects.map(getId), + next = {}, + self = this; + + // Detect if we already have everything we need prepared + if (this.idsMatch(nextIds)) { + // Nothing to prepare, move on + return; + } + + // Built up a set of ids. Note that we can only + // create plot lines after our domain offset has + // been determined. + if (this.domainOffset !== undefined) { + // Update list of ids in use + this.ids = nextIds; + + // Create buffers for these objects + this.bufferArray = this.ids.map(function (id) { + self.buffers[id] = self.buffers[id] || new PlotLineBuffer( + self.domainOffset, + INITIAL_SIZE, + self.maxPoints + ); + next[id] = + self.lines[id] || new PlotLine(self.buffers[id]); + return self.buffers[id]; + }); + } + + // If there are no more lines, clear the domain offset + if (Object.keys(next).length < 1) { + this.domainOffset = undefined; + } + + // Update to the current set of lines + this.lines = next; + }; + + // Initialize the domain offset, based on these observed values + PlotUpdater.prototype.initializeDomainOffset = function (values) { + this.domainOffset = + ((this.domainOffset === undefined) && (values.length > 0)) ? + (values.reduce(function (a, b) { + return (a || 0) + (b || 0); + }, 0) / values.length) : + this.domainOffset; + }; + + // Expand range slightly so points near edges are visible + PlotUpdater.prototype.expandRange = function () { + var padding = PADDING_RATIO * this.dimensions[1], + top; + padding = Math.max(padding, 1.0); + top = Math.ceil(this.origin[1] + this.dimensions[1] + padding / 2); + this.origin[1] = Math.floor(this.origin[1] - padding / 2); + this.dimensions[1] = top - this.origin[1]; + }; + + // Update dimensions and origin based on extrema of plots + PlotUpdater.prototype.updateBounds = function () { + var bufferArray = this.bufferArray; + if (bufferArray.length > 0) { + this.domainExtrema = bufferArray.map(function (lineBuffer) { + return lineBuffer.getDomainExtrema(); + }).reduce(reduceExtrema); + + this.rangeExtrema = bufferArray.map(function (lineBuffer) { + return lineBuffer.getRangeExtrema(); + }).reduce(reduceExtrema); + + // Calculate best-fit dimensions + this.dimensions = [ this.domainExtrema, this.rangeExtrema ] + .map(dimensionsOf); + this.origin = [ this.domainExtrema, this.rangeExtrema ] + .map(originOf); + + // Enforce some minimum visible area + this.expandRange(); + + // ...then enforce a fixed duration if needed + if (this.fixedDuration !== undefined) { + this.origin[0] = this.origin[0] + this.dimensions[0] - + this.fixedDuration; + this.dimensions[0] = this.fixedDuration; + } + } + }; + + // Add latest data for this domain object + PlotUpdater.prototype.addPointFor = function (domainObject) { + var line = this.lines[domainObject.getId()]; + if (line) { + line.addPoint( + this.handle.getDomainValue(domainObject, this.domain), + this.handle.getRangeValue(domainObject, this.range) + ); + } + }; + + /** + * Update with latest data. + */ + PlotUpdater.prototype.update = function update() { + var objects = this.handle.getTelemetryObjects(), + self = this; + + // Initialize domain offset if necessary + if (this.domainOffset === undefined) { + this.initializeDomainOffset(objects.map(function (obj) { + return self.handle.getDomainValue(obj, self.domain); + }).filter(function (value) { + return typeof value === 'number'; + })); + } + + // Make sure lines are available + this.prepareLines(objects); + + // Add new data + objects.forEach(function (domainObject, index) { + self.addPointFor(domainObject, index); + }); + + // Then, update extrema + this.updateBounds(); + }; + + /** + * Get the dimensions which bound all data in the provided + * data sets. This is given as a two-element array where the + * first element is domain, and second is range. + * @returns {number[]} the dimensions which bound this data set + */ + PlotUpdater.prototype.getDimensions = function () { + return this.dimensions; + }; + + /** + * Get the origin of this data set's boundary. + * This is given as a two-element array where the + * first element is domain, and second is range. + * The domain value here is not adjusted by the domain offset. + * @returns {number[]} the origin of this data set's boundary + */ + PlotUpdater.prototype.getOrigin = function () { + return this.origin; + }; + + /** + * Get the domain offset; this offset will have been subtracted + * from all domain values in all buffers returned by this + * preparer, in order to minimize loss-of-precision due to + * conversion to the 32-bit float format needed by WebGL. + * @returns {number} the domain offset + * @memberof platform/features/plot.PlotUpdater# + */ + PlotUpdater.prototype.getDomainOffset = function () { + return this.domainOffset; + }; + + /** + * Get all renderable buffers for this data set. This will + * be returned as an array which can be correlated back to + * the provided telemetry data objects (from the constructor + * call) by index. + * + * Internally, these are flattened; each buffer contains a + * sequence of alternating domain and range values. + * + * All domain values in all buffers will have been adjusted + * from their original values by subtraction of the domain + * offset; this minimizes loss-of-precision resulting from + * the conversion to 32-bit floats, which may otherwise + * cause aliasing artifacts (particularly for timestamps) + * + * @returns {Float32Array[]} the buffers for these traces + * @memberof platform/features/plot.PlotUpdater# + */ + PlotUpdater.prototype.getLineBuffers = function () { + return this.bufferArray; + }; + + /** + * Fill in historical data. + */ + PlotUpdater.prototype.addHistorical = function (domainObject, series) { + var count = series ? series.getPointCount() : 0, + line; + + // Nothing to do if it's an empty series + if (count < 1) { + return; + } + + // Initialize domain offset if necessary + if (this.domainOffset === undefined) { + this.initializeDomainOffset([ + series.getDomainValue(0, this.domain), + series.getDomainValue(count - 1, this.domain) + ]); + } + + // Make sure lines are available + this.prepareLines(this.handle.getTelemetryObjects()); + + // Look up the line for this domain object + line = this.lines[domainObject.getId()]; + + // ...and put the data into it. + if (line) { + line.addSeries(series, this.domain, this.range); + } + + // Update extrema + this.updateBounds(); + }; + return PlotUpdater; } ); + diff --git a/platform/features/plot/src/modes/PlotModeOptions.js b/platform/features/plot/src/modes/PlotModeOptions.js index 355f553228..bd03129698 100644 --- a/platform/features/plot/src/modes/PlotModeOptions.js +++ b/platform/features/plot/src/modes/PlotModeOptions.js @@ -30,84 +30,128 @@ define( key: "stacked", name: "Stacked", glyph: "m", - factory: PlotStackMode + Constructor: PlotStackMode }, OVERLAID = { key: "overlaid", name: "Overlaid", glyph: "6", - factory: PlotOverlayMode + Constructor: PlotOverlayMode }; + /** + * Handles distinct behavior associated with different + * plot modes. + * + * @interface platform/features/plot.PlotModeHandler + * @private + */ + + /** + * Plot telemetry to the sub-plot(s) managed by this mode. + * @param {platform/features/plot.PlotUpdater} updater a source + * of data that is ready to plot + * @method platform/features/plot.PlotModeHandler#plotTelemetry + */ + /** + * Get all sub-plots to be displayed in this mode; used + * to populate the plot template. + * @return {platform/features/plot.SubPlot[]} all sub-plots to + * display in this mode + * @method platform/features/plot.PlotModeHandler#getSubPlots + */ + /** + * Check if we are not in our base pan-zoom state (that is, + * there are some temporary user modifications to the + * current pan-zoom state.) + * @returns {boolean} true if not in the base pan-zoom state + * @method platform/features/plot.PlotModeHandler#isZoomed + */ + /** + * Undo the most recent pan/zoom change and restore + * the prior state. + * @method platform/features/plot.PlotModeHandler#stepBackPanZoom + */ + /** + * Undo all pan/zoom change and restore the base state. + * @method platform/features/plot.PlotModeHandler#unzoom + */ + /** * Determines which plotting modes (stacked/overlaid) * are applicable in a given plot view, maintains current * selection state thereof, and provides handlers for the * different behaviors associated with these modes. + * @memberof platform/features/plot * @constructor - * @param {DomainObject[]} the telemetry objects being + * @param {DomainObject[]} telemetryObjects the telemetry objects being * represented in this plot view + * @param {platform/features/plot.SubPlotFactory} subPlotFactory a + * factory for creating sub-plots */ function PlotModeOptions(telemetryObjects, subPlotFactory) { - var options = telemetryObjects.length > 1 ? - [ OVERLAID, STACKED ] : [ OVERLAID ], - mode = options[0], // Initial selection (overlaid) - modeHandler; - - return { - /** - * Get a handler for the current mode. This will handle - * plotting telemetry, providing subplots for the template, - * and view-level interactions with pan-zoom state. - * @returns {PlotOverlayMode|PlotStackMode} a handler - * for the current mode - */ - getModeHandler: function () { - // Lazily initialize - if (!modeHandler) { - modeHandler = mode.factory( - telemetryObjects, - subPlotFactory - ); - } - return modeHandler; - }, - /** - * Get all mode options available for each plot. Each - * mode contains a `name` and `glyph` field suitable - * for display in a template. - * @return {Array} the available modes - */ - getModeOptions: function () { - return options; - }, - /** - * Get the plotting mode option currently in use. - * This will be one of the elements returned from - * `getModeOptions`. - * @return {object} the current mode - */ - getMode: function () { - return mode; - }, - /** - * Set the plotting mode option to use. - * The passed argument must be one of the options - * returned by `getModeOptions`. - * @param {object} option one of the plot mode options - * from `getModeOptions` - */ - setMode: function (option) { - if (mode !== option) { - mode = option; - // Clear the existing mode handler, so it - // can be instantiated next time it's needed. - modeHandler = undefined; - } - } - }; + this.options = telemetryObjects.length > 1 ? + [ OVERLAID, STACKED ] : [ OVERLAID ]; + this.mode = this.options[0]; // Initial selection (overlaid) + this.telemetryObjects = telemetryObjects; + this.subPlotFactory = subPlotFactory; } + /** + * Get a handler for the current mode. This will handle + * plotting telemetry, providing subplots for the template, + * and view-level interactions with pan-zoom state. + * @returns {PlotOverlayMode|PlotStackMode} a handler + * for the current mode + */ + PlotModeOptions.prototype.getModeHandler = function () { + // Lazily initialize + if (!this.modeHandler) { + this.modeHandler = new this.mode.Constructor( + this.telemetryObjects, + this.subPlotFactory + ); + } + return this.modeHandler; + }; + + /** + * Get all mode options available for each plot. Each + * mode contains a `name` and `glyph` field suitable + * for display in a template. + * @return {Array} the available modes + */ + PlotModeOptions.prototype.getModeOptions = function () { + return this.options; + }; + + /** + * Get the plotting mode option currently in use. + * This will be one of the elements returned from + * `getModeOptions`. + * @return {*} the current mode + */ + PlotModeOptions.prototype.getMode = function () { + return this.mode; + }; + + /** + * Set the plotting mode option to use. + * The passed argument must be one of the options + * returned by `getModeOptions`. + * @param {object} option one of the plot mode options + * from `getModeOptions` + */ + PlotModeOptions.prototype.setMode = function (option) { + if (this.mode !== option) { + this.mode = option; + // Clear the existing mode handler, so it + // can be instantiated next time it's needed. + this.modeHandler = undefined; + } + }; + + return PlotModeOptions; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/modes/PlotOverlayMode.js b/platform/features/plot/src/modes/PlotOverlayMode.js index 501d4b0e78..809d800ef2 100644 --- a/platform/features/plot/src/modes/PlotOverlayMode.js +++ b/platform/features/plot/src/modes/PlotOverlayMode.js @@ -29,79 +29,62 @@ define( /** * Handles plotting in Overlaid mode. In overlaid mode, there * is one sub-plot which contains all plotted objects. + * @memberof platform/features/plot * @constructor + * @implements {platform/features/plot.PlotModeHandler} * @param {DomainObject[]} the domain objects to be plotted */ function PlotOverlayMode(telemetryObjects, subPlotFactory) { - var domainOffset, - panZoomStack = new PlotPanZoomStack([], []), - subplot = subPlotFactory.createSubPlot( - telemetryObjects, - panZoomStack - ), - subplots = [ subplot ]; + this.panZoomStack = new PlotPanZoomStack([], []); + this.subplot = subPlotFactory.createSubPlot( + telemetryObjects, + this.panZoomStack + ); + this.subplots = [ this.subplot ]; + } - function plotTelemetry(prepared) { - // Fit to the boundaries of the data, but don't - // override any user-initiated pan-zoom changes. - panZoomStack.setBasePanZoom( - prepared.getOrigin(), - prepared.getDimensions() - ); + PlotOverlayMode.prototype.plotTelemetry = function (updater) { + // Fit to the boundaries of the data, but don't + // override any user-initiated pan-zoom changes. + this.panZoomStack.setBasePanZoom( + updater.getOrigin(), + updater.getDimensions() + ); - // Track the domain offset, used to bias domain values - // to minimize loss of precision when converted to 32-bit - // floating point values for display. - subplot.setDomainOffset(prepared.getDomainOffset()); + // Track the domain offset, used to bias domain values + // to minimize loss of precision when converted to 32-bit + // floating point values for display. + this.subplot.setDomainOffset(updater.getDomainOffset()); - // Draw the buffers. Select color by index. - subplot.getDrawingObject().lines = prepared.getLineBuffers().map(function (buf, i) { + // Draw the buffers. Select color by index. + this.subplot.getDrawingObject().lines = + updater.getLineBuffers().map(function (buf, i) { return { buffer: buf.getBuffer(), color: PlotPalette.getFloatColor(i), points: buf.getLength() }; }); - } + }; - return { - /** - * Plot telemetry to the sub-plot(s) managed by this mode. - * @param {PlotPreparer} prepared the prepared data to plot - */ - plotTelemetry: plotTelemetry, - /** - * Get all sub-plots to be displayed in this mode; used - * to populate the plot template. - * @return {SubPlot[]} all sub-plots to display in this mode - */ - getSubPlots: function () { - return subplots; - }, - /** - * Check if we are not in our base pan-zoom state (that is, - * there are some temporary user modifications to the - * current pan-zoom state.) - * @returns {boolean} true if not in the base pan-zoom state - */ - isZoomed: function () { - return panZoomStack.getDepth() > 1; - }, - /** - * Undo the most recent pan/zoom change and restore - * the prior state. - */ - stepBackPanZoom: function () { - panZoomStack.popPanZoom(); - subplot.update(); - }, - unzoom: function () { - panZoomStack.clearPanZoom(); - subplot.update(); - } - }; - } + PlotOverlayMode.prototype.getSubPlots = function () { + return this.subplots; + }; + + PlotOverlayMode.prototype.isZoomed = function () { + return this.panZoomStack.getDepth() > 1; + }; + + PlotOverlayMode.prototype.stepBackPanZoom = function () { + this.panZoomStack.popPanZoom(); + this.subplot.update(); + }; + + PlotOverlayMode.prototype.unzoom = function () { + this.panZoomStack.clearPanZoom(); + this.subplot.update(); + }; return PlotOverlayMode; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/modes/PlotStackMode.js b/platform/features/plot/src/modes/PlotStackMode.js index 5d54b461f1..b20e9b9e32 100644 --- a/platform/features/plot/src/modes/PlotStackMode.js +++ b/platform/features/plot/src/modes/PlotStackMode.js @@ -29,95 +29,78 @@ define( /** * Handles plotting in Stacked mode. In stacked mode, there * is one sub-plot for each plotted object. + * @memberof platform/features/plot * @constructor + * @implements {platform/features/plot.PlotModeHandler} * @param {DomainObject[]} the domain objects to be plotted */ function PlotStackMode(telemetryObjects, subPlotFactory) { - var domainOffset, - panZoomStackGroup = - new PlotPanZoomStackGroup(telemetryObjects.length), - subplots = telemetryObjects.map(function (telemetryObject, i) { + var self = this; + + this.panZoomStackGroup = + new PlotPanZoomStackGroup(telemetryObjects.length); + + this.subplots = telemetryObjects.map(function (telemetryObject, i) { return subPlotFactory.createSubPlot( [telemetryObject], - panZoomStackGroup.getPanZoomStack(i) + self.panZoomStackGroup.getPanZoomStack(i) ); }); - - function plotTelemetryTo(subplot, prepared, index) { - var buffer = prepared.getLineBuffers()[index]; - - // Track the domain offset, used to bias domain values - // to minimize loss of precision when converted to 32-bit - // floating point values for display. - subplot.setDomainOffset(prepared.getDomainOffset()); - - // Draw the buffers. Always use the 0th color, because there - // is one line per plot. - subplot.getDrawingObject().lines = [{ - buffer: buffer.getBuffer(), - color: PlotPalette.getFloatColor(0), - points: buffer.getLength() - }]; - } - - function plotTelemetry(prepared) { - // Fit to the boundaries of the data, but don't - // override any user-initiated pan-zoom changes. - panZoomStackGroup.setBasePanZoom( - prepared.getOrigin(), - prepared.getDimensions() - ); - - subplots.forEach(function (subplot, index) { - plotTelemetryTo(subplot, prepared, index); - }); - } - - return { - /** - * Plot telemetry to the sub-plot(s) managed by this mode. - * @param {PlotPreparer} prepared the prepared data to plot - */ - plotTelemetry: plotTelemetry, - /** - * Get all sub-plots to be displayed in this mode; used - * to populate the plot template. - * @return {SubPlot[]} all sub-plots to display in this mode - */ - getSubPlots: function () { - return subplots; - }, - /** - * Check if we are not in our base pan-zoom state (that is, - * there are some temporary user modifications to the - * current pan-zoom state.) - * @returns {boolean} true if not in the base pan-zoom state - */ - isZoomed: function () { - return panZoomStackGroup.getDepth() > 1; - }, - /** - * Undo the most recent pan/zoom change and restore - * the prior state. - */ - stepBackPanZoom: function () { - panZoomStackGroup.popPanZoom(); - subplots.forEach(function (subplot) { - subplot.update(); - }); - }, - /** - * Undo all pan/zoom changes and restore the initial state. - */ - unzoom: function () { - panZoomStackGroup.clearPanZoom(); - subplots.forEach(function (subplot) { - subplot.update(); - }); - } - }; } + PlotStackMode.prototype.plotTelemetryTo = function (subplot, prepared, index) { + var buffer = prepared.getLineBuffers()[index]; + + // Track the domain offset, used to bias domain values + // to minimize loss of precision when converted to 32-bit + // floating point values for display. + subplot.setDomainOffset(prepared.getDomainOffset()); + + // Draw the buffers. Always use the 0th color, because there + // is one line per plot. + subplot.getDrawingObject().lines = [{ + buffer: buffer.getBuffer(), + color: PlotPalette.getFloatColor(0), + points: buffer.getLength() + }]; + }; + + PlotStackMode.prototype.plotTelemetry = function (prepared) { + var self = this; + // Fit to the boundaries of the data, but don't + // override any user-initiated pan-zoom changes. + this.panZoomStackGroup.setBasePanZoom( + prepared.getOrigin(), + prepared.getDimensions() + ); + + this.subplots.forEach(function (subplot, index) { + self.plotTelemetryTo(subplot, prepared, index); + }); + }; + + PlotStackMode.prototype.getSubPlots = function () { + return this.subplots; + }; + + PlotStackMode.prototype.isZoomed = function () { + return this.panZoomStackGroup.getDepth() > 1; + }; + + PlotStackMode.prototype.stepBackPanZoom = function () { + this.panZoomStackGroup.popPanZoom(); + this.subplots.forEach(function (subplot) { + subplot.update(); + }); + }; + + PlotStackMode.prototype.unzoom = function () { + this.panZoomStackGroup.clearPanZoom(); + this.subplots.forEach(function (subplot) { + subplot.update(); + }); + }; + return PlotStackMode; } -); \ No newline at end of file +); diff --git a/platform/features/plot/src/policies/PlotViewPolicy.js b/platform/features/plot/src/policies/PlotViewPolicy.js index 78df8c3187..1a2793aaa7 100644 --- a/platform/features/plot/src/policies/PlotViewPolicy.js +++ b/platform/features/plot/src/policies/PlotViewPolicy.js @@ -28,38 +28,40 @@ define( /** * Policy preventing the Plot view from being made available for * domain objects which have non-numeric telemetry. - * @implements {Policy} + * @implements {Policy.} + * @constructor + * @memberof platform/features/plot */ function PlotViewPolicy() { - function hasImageTelemetry(domainObject) { - var telemetry = domainObject && - domainObject.getCapability('telemetry'), - metadata = telemetry ? telemetry.getMetadata() : {}, - ranges = metadata.ranges || []; + } - // Generally, we want to allow Plot for telemetry-providing - // objects (most telemetry is plottable.) We only want to - // suppress this for telemetry which only has explicitly - // non-numeric values. - return ranges.length === 0 || ranges.some(function (range) { + function hasNumericTelemetry(domainObject) { + var telemetry = domainObject && + domainObject.getCapability('telemetry'), + metadata = telemetry ? telemetry.getMetadata() : {}, + ranges = metadata.ranges || []; + + // Generally, we want to allow Plot for telemetry-providing + // objects (most telemetry is plottable.) We only want to + // suppress this for telemetry which only has explicitly + // non-numeric values. + return ranges.length === 0 || ranges.some(function (range) { // Assume format is numeric if it is undefined // (numeric telemetry is the common case) return range.format === undefined || - range.format === 'number'; + range.format === 'number'; }); + } + + PlotViewPolicy.prototype.allow = function (view, domainObject) { + if (view.key === 'plot') { + return hasNumericTelemetry(domainObject); } - return { - allow: function (view, domainObject) { - if (view.key === 'plot') { - return hasImageTelemetry(domainObject); - } - - return true; - } - }; - } + return true; + }; return PlotViewPolicy; } ); + diff --git a/platform/features/plot/test/PlotControllerSpec.js b/platform/features/plot/test/PlotControllerSpec.js index 32529b0f3d..e6c79b4e54 100644 --- a/platform/features/plot/test/PlotControllerSpec.js +++ b/platform/features/plot/test/PlotControllerSpec.js @@ -39,6 +39,12 @@ define( mockSeries, controller; + function bind(method, thisObj) { + return function () { + return method.apply(thisObj, arguments); + }; + } + beforeEach(function () { mockScope = jasmine.createSpyObj( @@ -196,13 +202,13 @@ define( }); it("allows plots to be updated", function () { - expect(controller.update).not.toThrow(); + expect(bind(controller.update, controller)).not.toThrow(); }); it("allows changing pan-zoom state", function () { - expect(controller.isZoomed).not.toThrow(); - expect(controller.stepBackPanZoom).not.toThrow(); - expect(controller.unzoom).not.toThrow(); + expect(bind(controller.isZoomed, controller)).not.toThrow(); + expect(bind(controller.stepBackPanZoom, controller)).not.toThrow(); + expect(bind(controller.unzoom, controller)).not.toThrow(); }); it("indicates if a request is pending", function () { diff --git a/platform/features/rtevents/src/DomainColumn.js b/platform/features/rtevents/src/DomainColumn.js index 43279a42d7..ea2e039634 100644 --- a/platform/features/rtevents/src/DomainColumn.js +++ b/platform/features/rtevents/src/DomainColumn.js @@ -33,6 +33,7 @@ define( * A column which will report telemetry domain values * (typically, timestamps.) Used by the ScrollingListController. * + * @memberof platform/features/rtevents * @constructor * @param domainMetadata an object with the machine- and human- * readable names for this domain (in `key` and `name` @@ -45,6 +46,7 @@ define( /** * Get the title to display in this column's header. * @returns {string} the title to display + * @memberof platform/features/rtevents.DomainColumn# */ getTitle: function () { // At the moment there does not appear to be a way to get the @@ -55,6 +57,7 @@ define( * Get the text to display inside a row under this * column. * @returns {string} the text to display + * @memberof platform/features/rtevents.DomainColumn# */ getValue: function (domainObject, handle) { return { @@ -69,3 +72,4 @@ define( return DomainColumn; } ); + diff --git a/platform/features/rtevents/src/RTEventListController.js b/platform/features/rtevents/src/RTEventListController.js index bed335c6cd..93441f2635 100644 --- a/platform/features/rtevents/src/RTEventListController.js +++ b/platform/features/rtevents/src/RTEventListController.js @@ -35,6 +35,7 @@ define( /** * The RTEventListController is responsible for populating * the contents of the messages view. + * @memberof platform/features/rtevents * @constructor */ function RTEventListController($scope, telemetryHandler, telemetryFormatter) { @@ -136,3 +137,4 @@ define( return RTEventListController; } ); + diff --git a/platform/features/rtevents/src/RangeColumn.js b/platform/features/rtevents/src/RangeColumn.js index 68147062b5..1c398ddd44 100644 --- a/platform/features/rtevents/src/RangeColumn.js +++ b/platform/features/rtevents/src/RangeColumn.js @@ -34,6 +34,7 @@ define( * A column which will report telemetry range values * (typically, measurements.) Used by the RTEventListController. * + * @memberof platform/features/rtevents * @constructor * @param rangeMetadata an object with the machine- and human- * readable names for this range (in `key` and `name` @@ -46,6 +47,7 @@ define( /** * Get the title to display in this column's header. * @returns {string} the title to display + * @memberof platform/features/rtevents.RangeColumn# */ getTitle: function () { return "Message"; @@ -54,6 +56,7 @@ define( * Get the text to display inside a row under this * column. * @returns {string} the text to display + * @memberof platform/features/rtevents.RangeColumn# */ getValue: function (domainObject, handle) { return { @@ -66,3 +69,4 @@ define( return RangeColumn; } ); + diff --git a/platform/features/rtevents/src/directives/MCTRTDataTable.js b/platform/features/rtevents/src/directives/MCTRTDataTable.js index 9047d9e7f1..d7337eab4f 100644 --- a/platform/features/rtevents/src/directives/MCTRTDataTable.js +++ b/platform/features/rtevents/src/directives/MCTRTDataTable.js @@ -71,4 +71,4 @@ define( return MCTRTDataTable; } -); \ No newline at end of file +); diff --git a/platform/features/rtevents/src/policies/RTMessagesViewPolicy.js b/platform/features/rtevents/src/policies/RTMessagesViewPolicy.js index e3948e5a5c..a5a2373fb8 100644 --- a/platform/features/rtevents/src/policies/RTMessagesViewPolicy.js +++ b/platform/features/rtevents/src/policies/RTMessagesViewPolicy.js @@ -31,6 +31,7 @@ define( /** * Policy controlling when the real time Messages view should be avaliable. + * @memberof platform/features/rtevents * @constructor */ function RTMessagesViewPolicy() { @@ -52,6 +53,7 @@ define( * @param {Action} action the action * @param domainObject the domain object which will be viewed * @returns {boolean} true if not disallowed + * @memberof platform/features/rtevents.RTMessagesViewPolicy# */ allow: function (view, domainObject) { // This policy only applies for the RT Messages view @@ -71,4 +73,4 @@ define( return RTMessagesViewPolicy; } -); \ No newline at end of file +); diff --git a/platform/features/rtscrolling/src/DomainColumn.js b/platform/features/rtscrolling/src/DomainColumn.js index c4f8a2a143..95f262515d 100644 --- a/platform/features/rtscrolling/src/DomainColumn.js +++ b/platform/features/rtscrolling/src/DomainColumn.js @@ -33,6 +33,7 @@ define( * A column which will report telemetry domain values * (typically, timestamps.) Used by the ScrollingListController. * + * @memberof platform/features/rtscrolling * @constructor * @param domainMetadata an object with the machine- and human- * readable names for this domain (in `key` and `name` @@ -45,6 +46,7 @@ define( /** * Get the title to display in this column's header. * @returns {string} the title to display + * @memberof platform/features/rtscrolling.DomainColumn# */ getTitle: function () { return "Time"; @@ -53,6 +55,7 @@ define( * Get the text to display inside a row under this * column. * @returns {string} the text to display + * @memberof platform/features/rtscrolling.DomainColumn# */ getValue: function (domainObject, handle) { return { @@ -67,3 +70,4 @@ define( return DomainColumn; } ); + diff --git a/platform/features/rtscrolling/src/NameColumn.js b/platform/features/rtscrolling/src/NameColumn.js index eb08ebc7ed..b30891341b 100644 --- a/platform/features/rtscrolling/src/NameColumn.js +++ b/platform/features/rtscrolling/src/NameColumn.js @@ -33,6 +33,7 @@ define( * A column which will report the name of the domain object * which exposed specific telemetry values. * + * @memberof platform/features/rtscrolling * @constructor */ function NameColumn() { @@ -40,6 +41,7 @@ define( /** * Get the title to display in this column's header. * @returns {string} the title to display + * @memberof platform/features/rtscrolling.NameColumn# */ getTitle: function () { return "Name"; @@ -48,6 +50,7 @@ define( * Get the text to display inside a row under this * column. This returns the domain object's name. * @returns {string} the text to display + * @memberof platform/features/rtscrolling.NameColumn# */ getValue: function (domainObject) { return { @@ -60,3 +63,4 @@ define( return NameColumn; } ); + diff --git a/platform/features/rtscrolling/src/RTScrollingListController.js b/platform/features/rtscrolling/src/RTScrollingListController.js index bc9f8a63ed..282ccd145c 100644 --- a/platform/features/rtscrolling/src/RTScrollingListController.js +++ b/platform/features/rtscrolling/src/RTScrollingListController.js @@ -34,6 +34,7 @@ define( /** * The RTScrollingListController is responsible for populating * the contents of the scrolling list view. + * @memberof platform/features/rtscrolling * @constructor */ function RTScrollingListController($scope, telemetryHandler, telemetryFormatter) { @@ -135,3 +136,4 @@ define( return RTScrollingListController; } ); + diff --git a/platform/features/rtscrolling/src/RangeColumn.js b/platform/features/rtscrolling/src/RangeColumn.js index 867cc0798c..79c6cb8c73 100644 --- a/platform/features/rtscrolling/src/RangeColumn.js +++ b/platform/features/rtscrolling/src/RangeColumn.js @@ -33,6 +33,7 @@ define( * A column which will report telemetry range values * (typically, measurements.) Used by the RTScrollingListController. * + * @memberof platform/features/rtscrolling * @constructor * @param rangeMetadata an object with the machine- and human- * readable names for this range (in `key` and `name` @@ -52,6 +53,7 @@ define( /** * Get the title to display in this column's header. * @returns {string} the title to display + * @memberof platform/features/rtscrolling.RangeColumn# */ getTitle: function () { return "Value"; @@ -60,6 +62,7 @@ define( * Get the text to display inside a row under this * column. * @returns {string} the text to display + * @memberof platform/features/rtscrolling.RangeColumn# */ getValue: function (domainObject, handle) { var range = findRange(domainObject), @@ -81,3 +84,4 @@ define( return RangeColumn; } ); + diff --git a/platform/features/scrolling/src/DomainColumn.js b/platform/features/scrolling/src/DomainColumn.js index 33f3f4e020..a55b4001d5 100644 --- a/platform/features/scrolling/src/DomainColumn.js +++ b/platform/features/scrolling/src/DomainColumn.js @@ -33,6 +33,8 @@ define( * A column which will report telemetry domain values * (typically, timestamps.) Used by the ScrollingListController. * + * @memberof platform/features/scrolling + * @implements {platform/features/scrolling.ScrollingColumn} * @constructor * @param domainMetadata an object with the machine- and human- * readable names for this domain (in `key` and `name` @@ -41,29 +43,22 @@ define( * formatting service, for making values human-readable. */ function DomainColumn(domainMetadata, telemetryFormatter) { - return { - /** - * Get the title to display in this column's header. - * @returns {string} the title to display - */ - getTitle: function () { - return domainMetadata.name; - }, - /** - * Get the text to display inside a row under this - * column. - * @returns {string} the text to display - */ - getValue: function (domainObject, datum) { - return { - text: telemetryFormatter.formatDomainValue( - datum[domainMetadata.key] - ) - }; - } - }; + this.domainMetadata = domainMetadata; + this.telemetryFormatter = telemetryFormatter; } + DomainColumn.prototype.getTitle = function () { + return this.domainMetadata.name; + }; + + DomainColumn.prototype.getValue = function (domainObject, datum) { + return { + text: this.telemetryFormatter.formatDomainValue( + datum[this.domainMetadata.key] + ) + }; + }; + return DomainColumn; } -); \ No newline at end of file +); diff --git a/platform/features/scrolling/src/NameColumn.js b/platform/features/scrolling/src/NameColumn.js index 6420c44439..8947b279f0 100644 --- a/platform/features/scrolling/src/NameColumn.js +++ b/platform/features/scrolling/src/NameColumn.js @@ -33,30 +33,23 @@ define( * A column which will report the name of the domain object * which exposed specific telemetry values. * + * @memberof platform/features/scrolling + * @implements {platform/features/scrolling.ScrollingColumn} * @constructor */ function NameColumn() { - return { - /** - * Get the title to display in this column's header. - * @returns {string} the title to display - */ - getTitle: function () { - return "Name"; - }, - /** - * Get the text to display inside a row under this - * column. This returns the domain object's name. - * @returns {string} the text to display - */ - getValue: function (domainObject) { - return { - text: domainObject.getModel().name - }; - } - }; } + NameColumn.prototype.getTitle = function () { + return "Name"; + }; + + NameColumn.prototype.getValue = function (domainObject) { + return { + text: domainObject.getModel().name + }; + }; + return NameColumn; } -); \ No newline at end of file +); diff --git a/platform/features/scrolling/src/RangeColumn.js b/platform/features/scrolling/src/RangeColumn.js index 1e89dfc376..637a68517d 100644 --- a/platform/features/scrolling/src/RangeColumn.js +++ b/platform/features/scrolling/src/RangeColumn.js @@ -33,6 +33,8 @@ define( * A column which will report telemetry range values * (typically, measurements.) Used by the ScrollingListController. * + * @memberof platform/features/scrolling + * @implements {platform/features/scrolling.ScrollingColumn} * @constructor * @param rangeMetadata an object with the machine- and human- * readable names for this range (in `key` and `name` @@ -41,33 +43,26 @@ define( * formatting service, for making values human-readable. */ function RangeColumn(rangeMetadata, telemetryFormatter) { - return { - /** - * Get the title to display in this column's header. - * @returns {string} the title to display - */ - getTitle: function () { - return rangeMetadata.name; - }, - /** - * Get the text to display inside a row under this - * column. - * @returns {string} the text to display - */ - getValue: function (domainObject, datum) { - var range = rangeMetadata.key, - limit = domainObject.getCapability('limit'), - value = datum[range], - alarm = limit.evaluate(datum, range); - - return { - cssClass: alarm && alarm.cssClass, - text: telemetryFormatter.formatRangeValue(value) - }; - } - }; + this.rangeMetadata = rangeMetadata; + this.telemetryFormatter = telemetryFormatter; } + RangeColumn.prototype.getTitle = function () { + return this.rangeMetadata.name; + }; + + RangeColumn.prototype.getValue = function (domainObject, datum) { + var range = this.rangeMetadata.key, + limit = domainObject.getCapability('limit'), + value = datum[range], + alarm = limit.evaluate(datum, range); + + return { + cssClass: alarm && alarm.cssClass, + text: this.telemetryFormatter.formatRangeValue(value) + }; + }; + return RangeColumn; } -); \ No newline at end of file +); diff --git a/platform/features/scrolling/src/ScrollingListController.js b/platform/features/scrolling/src/ScrollingListController.js index f61e178964..7ca6312369 100644 --- a/platform/features/scrolling/src/ScrollingListController.js +++ b/platform/features/scrolling/src/ScrollingListController.js @@ -22,7 +22,8 @@ /*global define,Promise*/ /** - * Module defining ListController. Created by vwoeltje on 11/18/14. + * This bundle implements a "Scrolling List" view of telemetry data. + * @namespace platform/features/scrolling */ define( ["./NameColumn", "./DomainColumn", "./RangeColumn", "./ScrollingListPopulator"], @@ -34,11 +35,11 @@ define( /** * The ScrollingListController is responsible for populating * the contents of the scrolling list view. + * @memberof platform/features/scrolling * @constructor */ function ScrollingListController($scope, formatter) { - var populator; - + var populator = new ScrollingListPopulator([]); // Get a set of populated, ready-to-display rows for the // latest data values. @@ -127,6 +128,32 @@ define( $scope.$watch("telemetry.getMetadata()", setupColumns); } + /** + * A description of how to display a certain column of data in a + * Scrolling List view. + * @interface platform/features/scrolling.ScrollingColumn + * @private + */ + /** + * Get the title to display in this column's header. + * @returns {string} the title to display + * @method platform/features/scrolling.ScrollingColumn#getTitle + */ + /** + * Get the text to display inside a row under this + * column. + * @param {DomainObject} domainObject the domain object associated + * with this row + * @param {TelemetrySeries} series the telemetry data associated + * with this row + * @param {number} index the index of the telemetry datum associated + * with this row + * @returns {string} the text to display + * @method platform/features/scrolling.ScrollingColumn#getValue + */ + + return ScrollingListController; } ); + diff --git a/platform/features/scrolling/src/ScrollingListPopulator.js b/platform/features/scrolling/src/ScrollingListPopulator.js index bbdda2359a..b5995d65ba 100644 --- a/platform/features/scrolling/src/ScrollingListPopulator.js +++ b/platform/features/scrolling/src/ScrollingListPopulator.js @@ -30,87 +30,119 @@ define( * The ScrollingListPopulator is responsible for filling in the * values which should appear within columns of a scrolling list * view, based on received telemetry data. + * @memberof platform/features/scrolling * @constructor * @param {Column[]} columns the columns to be populated */ function ScrollingListPopulator(columns) { - /** - * Look up the most recent values from a set of data objects. - * Returns an array of objects in the order in which data - * should be displayed; each element is an object with - * two properties: - * - * * objectIndex: The index of the domain object associated - * with the data point to be displayed in that - * row. - * * pointIndex: The index of the data point itself, within - * its data set. - * - * @param {Array} datas an array of the most recent - * data objects; expected to be in the same order - * as the domain objects provided at constructor - * @param {number} count the number of rows to provide - */ - function getLatestDataValues(datas, count) { - var latest = [], - candidate, - candidateTime, - used = datas.map(function () { return 0; }); + this.columns = columns; + } - // This algorithm is O(nk) for n rows and k telemetry elements; - // one O(k) linear search for a max is made for each of n rows. - // This could be done in O(n lg k + k lg k), using a priority - // queue (where priority is max-finding) containing k initial - // values. For n rows, pop the max from the queue and replenish - // the queue with a value from the data at the same - // objectIndex, if available. - // But k is small, so this might not give an observable - // improvement in performance. + /** + * Look up the most recent values from a set of data objects. + * Returns an array of objects in the order in which data + * should be displayed; each element is an object with + * two properties: + * + * * objectIndex: The index of the domain object associated + * with the data point to be displayed in that + * row. + * * pointIndex: The index of the data point itself, within + * its data set. + * + * @param {Array} datas an array of the most recent + * data objects; expected to be in the same order + * as the domain objects provided at constructor + * @param {number} count the number of rows to provide + * @returns {Array} latest data values in display order + * @private + */ + function getLatestDataValues(datas, count) { + var latest = [], + candidate, + candidateTime, + used = datas.map(function () { return 0; }); - // Find the most recent unused data point (this will be used - // in a loop to find and the N most recent data points) - function findCandidate(data, i) { - var nextTime, - pointCount = data.getPointCount(), - pointIndex = pointCount - used[i] - 1; - if (data && pointIndex >= 0) { - nextTime = data.getDomainValue(pointIndex); - if (nextTime > candidateTime) { - candidateTime = nextTime; - candidate = { - objectIndex: i, - pointIndex: pointIndex - }; - } + // This algorithm is O(nk) for n rows and k telemetry elements; + // one O(k) linear search for a max is made for each of n rows. + // This could be done in O(n lg k + k lg k), using a priority + // queue (where priority is max-finding) containing k initial + // values. For n rows, pop the max from the queue and replenish + // the queue with a value from the data at the same + // objectIndex, if available. + // But k is small, so this might not give an observable + // improvement in performance. + + // Find the most recent unused data point (this will be used + // in a loop to find and the N most recent data points) + function findCandidate(data, i) { + var nextTime, + pointCount = data.getPointCount(), + pointIndex = pointCount - used[i] - 1; + if (data && pointIndex >= 0) { + nextTime = data.getDomainValue(pointIndex); + if (nextTime > candidateTime) { + candidateTime = nextTime; + candidate = { + objectIndex: i, + pointIndex: pointIndex + }; } } - - // Assemble a list of the most recent data points - while (latest.length < count) { - // Reset variables pre-search - candidateTime = Number.NEGATIVE_INFINITY; - candidate = undefined; - - // Linear search for most recent - datas.forEach(findCandidate); - - if (candidate) { - // Record this data point - it is the most recent - latest.push(candidate); - - // Track the data points used so we can look farther back - // in the data set on the next iteration - used[candidate.objectIndex] = used[candidate.objectIndex] + 1; - } else { - // Ran out of candidates; not enough data points - // available to fill all rows. - break; - } - } - - return latest; } + // Assemble a list of the most recent data points + while (latest.length < count) { + // Reset variables pre-search + candidateTime = Number.NEGATIVE_INFINITY; + candidate = undefined; + + // Linear search for most recent + datas.forEach(findCandidate); + + if (candidate) { + // Record this data point - it is the most recent + latest.push(candidate); + + // Track the data points used so we can look farther back + // in the data set on the next iteration + used[candidate.objectIndex] = used[candidate.objectIndex] + 1; + } else { + // Ran out of candidates; not enough data points + // available to fill all rows. + break; + } + } + + return latest; + } + + /** + * Get the text which should appear in headers for the + * provided columns. + * @returns {string[]} column headers + */ + ScrollingListPopulator.prototype.getHeaders = function () { + return this.columns.map(function (column) { + return column.getTitle(); + }); + }; + + /** + * Get the contents of rows for the scrolling list view. + * @param {TelemetrySeries[]} datas the data sets + * @param {DomainObject[]} objects the domain objects which + * provided the data sets; these should match + * index-to-index with the `datas` argument + * @param {number} count the number of rows to populate + * @returns {string[][]} an array of rows, each of which + * is an array of values which should appear + * in that row + */ + ScrollingListPopulator.prototype.getRows = function (datas, objects, count) { + var values = getLatestDataValues(datas, count), + self = this; + // From a telemetry series, retrieve a single data point // containing all fields for domains/ranges function makeDatum(domainObject, series, index) { @@ -131,53 +163,27 @@ define( return result; } - return { - /** - * Get the text which should appear in headers for the - * provided columns. - * @returns {string[]} column headers - */ - getHeaders: function () { - return columns.map(function (column) { - return column.getTitle(); - }); - }, - /** - * Get the contents of rows for the scrolling list view. - * @param {TelemetrySeries[]} datas the data sets - * @param {DomainObject[]} objects the domain objects which - * provided the data sets; these should match - * index-to-index with the `datas` argument - * @param {number} count the number of rows to populate - * @returns {string[][]} an array of rows, each of which - * is an array of values which should appear - * in that row - */ - getRows: function (datas, objects, count) { - var values = getLatestDataValues(datas, count); + // Each value will become a row, which will contain + // some value in each column (rendering by the + // column object itself) + return values.map(function (value) { + var datum = makeDatum( + objects[value.objectIndex], + datas[value.objectIndex], + value.pointIndex + ); - // Each value will become a row, which will contain - // some value in each column (rendering by the - // column object itself) - return values.map(function (value) { - var datum = makeDatum( - objects[value.objectIndex], - datas[value.objectIndex], - value.pointIndex - ); - - return columns.map(function (column) { - return column.getValue( - objects[value.objectIndex], - datum - ); - }); - }); - } - }; - } + return self.columns.map(function (column) { + return column.getValue( + objects[value.objectIndex], + datum + ); + }); + }); + }; return ScrollingListPopulator; } ); + diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js index 8ec0d5e039..b46ba6e7a1 100644 --- a/platform/forms/src/MCTControl.js +++ b/platform/forms/src/MCTControl.js @@ -33,6 +33,8 @@ define( * `controls`; this allows plug-ins to introduce new form * control types while still making use of the form * generator to ensure an overall consistent form style. + * @constructor + * @memberof platform/forms */ function MCTControl(controls) { var controlMap = {}; @@ -103,4 +105,4 @@ define( return MCTControl; } -); \ No newline at end of file +); diff --git a/platform/forms/src/MCTForm.js b/platform/forms/src/MCTForm.js index a318870198..0629fbfd21 100644 --- a/platform/forms/src/MCTForm.js +++ b/platform/forms/src/MCTForm.js @@ -22,7 +22,9 @@ /*global define,Promise*/ /** - * Module defining MCTForm. Created by vwoeltje on 11/10/14. + * This bundle implements directives for displaying and handling forms for + * user input. + * @namespace platform/forms */ define( ["./controllers/FormController"], @@ -46,6 +48,7 @@ define( * of name, except this will be made available in the * parent scope. * + * @memberof platform/forms * @constructor */ function MCTForm() { @@ -82,4 +85,4 @@ define( return MCTForm; } -); \ No newline at end of file +); diff --git a/platform/forms/src/MCTToolbar.js b/platform/forms/src/MCTToolbar.js index ea0f644cc3..41d2c4c00d 100644 --- a/platform/forms/src/MCTToolbar.js +++ b/platform/forms/src/MCTToolbar.js @@ -46,6 +46,7 @@ define( * of name, except this will be made available in the * parent scope. * + * @memberof platform/forms * @constructor */ function MCTForm() { @@ -82,4 +83,4 @@ define( return MCTForm; } -); \ No newline at end of file +); diff --git a/platform/forms/src/controllers/ColorController.js b/platform/forms/src/controllers/ColorController.js index 420e2ed96d..62640364df 100644 --- a/platform/forms/src/controllers/ColorController.js +++ b/platform/forms/src/controllers/ColorController.js @@ -83,20 +83,23 @@ define( GROUPS.push(group); } - - function ColorController() { if (GROUPS.length === 0) { initializeGroups(); } - - return { - groups: function () { - return GROUPS; - } - }; } + /** + * Get groups of colors to display in a color picker. These are + * given as #-prefixed color strings, in a two-dimensional array. + * Each element of the array is a group of related colors (e.g. + * grayscale colors, web colors, gradients...) + * @returns {string[][]} groups of colors + */ + ColorController.prototype.groups = function () { + return GROUPS; + }; + return ColorController; } -); \ No newline at end of file +); diff --git a/platform/forms/src/controllers/CompositeController.js b/platform/forms/src/controllers/CompositeController.js index e332ed7115..506fbb6f4e 100644 --- a/platform/forms/src/controllers/CompositeController.js +++ b/platform/forms/src/controllers/CompositeController.js @@ -35,35 +35,30 @@ define( * filled in) should be disallowed. This is enforced in the template * by an ng-required directive, but that is supported by the * isNonEmpty check that this controller provides. + * @memberof platform/forms * @constructor */ function CompositeController() { - // Check if an element is defined; the map step of isNonEmpty - function isDefined(element) { - return typeof element !== 'undefined'; - } - - // Boolean or; the reduce step of isNonEmpty - function or(a, b) { - return a || b; - } - - return { - /** - * Check if an array contains anything other than - * undefined elements. - * @param {Array} value the array to check - * @returns {boolean} true if any non-undefined - * element is in the array - */ - isNonEmpty: function (value) { - return Array.isArray(value) && - value.map(isDefined).reduce(or, false); - } - }; } + // Check if an element is defined; the map step of isNonEmpty + function isDefined(element) { + return typeof element !== 'undefined'; + } + + /** + * Check if an array contains anything other than + * undefined elements. + * @param {Array} value the array to check + * @returns {boolean} true if any non-undefined + * element is in the array + * @memberof platform/forms.CompositeController# + */ + CompositeController.prototype.isNonEmpty = function (value) { + return Array.isArray(value) && value.some(isDefined); + }; + return CompositeController; } -); \ No newline at end of file +); diff --git a/platform/forms/src/controllers/DateTimeController.js b/platform/forms/src/controllers/DateTimeController.js index e37e3a8f71..5ef7735ed1 100644 --- a/platform/forms/src/controllers/DateTimeController.js +++ b/platform/forms/src/controllers/DateTimeController.js @@ -34,6 +34,7 @@ define( * input fields but outputs a single timestamp (in * milliseconds since start of 1970) to the ngModel. * + * @memberof platform/forms * @constructor */ function DateTimeController($scope) { @@ -106,3 +107,4 @@ define( } ); + diff --git a/platform/forms/src/controllers/DialogButtonController.js b/platform/forms/src/controllers/DialogButtonController.js index 1298b1a63c..2c440dead1 100644 --- a/platform/forms/src/controllers/DialogButtonController.js +++ b/platform/forms/src/controllers/DialogButtonController.js @@ -29,15 +29,15 @@ define( * Controller for the `dialog-button` control type. Provides * structure for a button (embedded via the template) which * will show a dialog for editing a single property when clicked. + * @memberof platform/forms * @constructor * @param $scope the control's Angular scope * @param {DialogService} dialogService service to use to prompt * for user input */ function DialogButtonController($scope, dialogService) { - var buttonStructure, - buttonForm, - field; + var self = this, + buttonForm; // Store the result of user input to the model function storeResult(result) { @@ -64,11 +64,11 @@ define( row.key = $scope.field; // Prepare the structure for the button itself - buttonStructure = {}; - buttonStructure.glyph = structure.glyph; - buttonStructure.name = structure.name; - buttonStructure.description = structure.description; - buttonStructure.click = showDialog; + self.buttonStructure = {}; + self.buttonStructure.glyph = structure.glyph; + self.buttonStructure.name = structure.name; + self.buttonStructure.description = structure.description; + self.buttonStructure.click = showDialog; // Prepare the form; a single row buttonForm = { @@ -78,20 +78,18 @@ define( } $scope.$watch('structure', refreshStructure); - - return { - /** - * Get the structure for an `mct-control` of type - * `button`; a dialog will be launched when this button - * is clicked. - * @returns dialog structure - */ - getButtonStructure: function () { - return buttonStructure; - } - }; } + /** + * Get the structure for an `mct-control` of type + * `button`; a dialog will be launched when this button + * is clicked. + * @returns dialog structure + */ + DialogButtonController.prototype.getButtonStructure = function () { + return this.buttonStructure; + }; + return DialogButtonController; } -); \ No newline at end of file +); diff --git a/platform/forms/src/controllers/FormController.js b/platform/forms/src/controllers/FormController.js index 12b5261b76..04b8379304 100644 --- a/platform/forms/src/controllers/FormController.js +++ b/platform/forms/src/controllers/FormController.js @@ -31,6 +31,7 @@ define( /** * Controller for mct-form and mct-toolbar directives. + * @memberof platform/forms * @constructor */ function FormController($scope) { @@ -75,4 +76,4 @@ define( return FormController; } -); \ No newline at end of file +); diff --git a/platform/framework/src/Constants.js b/platform/framework/src/Constants.js index 6bfa6ce7b9..3d9fb949b0 100644 --- a/platform/framework/src/Constants.js +++ b/platform/framework/src/Constants.js @@ -47,4 +47,4 @@ define({ "mandatory": Number.POSITIVE_INFINITY }, DEFAULT_PRIORITY: 0 -}); \ No newline at end of file +}); diff --git a/platform/framework/src/FrameworkInitializer.js b/platform/framework/src/FrameworkInitializer.js index 359fd1274f..add6846e9f 100644 --- a/platform/framework/src/FrameworkInitializer.js +++ b/platform/framework/src/FrameworkInitializer.js @@ -38,25 +38,38 @@ define( * * Registering extensions with Angular * * Bootstrapping the Angular application. * + * @memberof platform/framework * @constructor - * @param {BundleLoader} loader - * @param {BundleResolver} resolver - * @param {ExtensionRegistrar} registrar - * @param {ApplicationBootstrapper} bootstrapper + * @param {platform/framework.BundleLoader} loader + * @param {platform/framework.BundleResolver} resolver + * @param {platform/framework.ExtensionRegistrar} registrar + * @param {platform/framework.ApplicationBootstrapper} bootstrapper */ function FrameworkInitializer(loader, resolver, registrar, bootstrapper) { + this.loader = loader; + this.resolver = resolver; + this.registrar = registrar; + this.bootstrapper = bootstrapper; + } - return { - runApplication: function (bundleList) { - return loader.loadBundles(bundleList) - .then(resolver.resolveBundles) - .then(registrar.registerExtensions) - .then(bootstrapper.bootstrap); - } - + function bind(method, thisArg) { + return function () { + return method.apply(thisArg, arguments); }; } + /** + * Run the application defined by this set of bundles. + * @param bundleList + * @returns {*} + */ + FrameworkInitializer.prototype.runApplication = function (bundleList) { + return this.loader.loadBundles(bundleList) + .then(bind(this.resolver.resolveBundles, this.resolver)) + .then(bind(this.registrar.registerExtensions, this.registrar)) + .then(bind(this.bootstrapper.bootstrap, this.bootstrapper)); + }; + return FrameworkInitializer; } -); \ No newline at end of file +); diff --git a/platform/framework/src/LogLevel.js b/platform/framework/src/LogLevel.js index 5734a66f45..973811ca07 100644 --- a/platform/framework/src/LogLevel.js +++ b/platform/framework/src/LogLevel.js @@ -47,12 +47,35 @@ define( * as a default. Only log messages of levels equal to or greater * than the specified level will be passed to console. * + * @memberof platform/framework * @constructor * @param {string} level the logging level */ function LogLevel(level) { // Find the numeric level associated with the string - var index = LOG_LEVELS.indexOf(level); + this.index = LOG_LEVELS.indexOf(level); + + // Default to 'warn' level if unspecified + if (this.index < 0) { + this.index = 1; + } + } + + /** + * Configure logging to suppress log output if it is + * not of an appropriate level. Both the Angular app + * being initialized and a reference to `$log` should be + * passed; the former is used to configure application + * logging, while the latter is needed to apply the + * same configuration during framework initialization + * (since the framework also logs.) + * + * @param app the Angular app to configure + * @param $log Angular's $log (also configured) + * @memberof platform/framework.LogLevel# + */ + LogLevel.prototype.configure = function (app, $log) { + var index = this.index; // Replace logging methods with no-ops, if they are // not of an appropriate level. @@ -66,36 +89,15 @@ define( }); } - // Default to 'warn' level if unspecified - if (index < 0) { - index = 1; - } - - return { - /** - * Configure logging to suppress log output if it is - * not of an appropriate level. Both the Angular app - * being initialized and a reference to `$log` should be - * passed; the former is used to configure application - * logging, while the latter is needed to apply the - * same configuration during framework initialization - * (since the framework also logs.) - * - * @param app the Angular app to configure - * @param $log Angular's $log (also configured) - */ - configure: function (app, $log) { - decorate($log); - app.config(function ($provide) { - $provide.decorator('$log', function ($delegate) { - decorate($delegate); - return $delegate; - }); - }); - } - }; - } + decorate($log); + app.config(function ($provide) { + $provide.decorator('$log', function ($delegate) { + decorate($delegate); + return $delegate; + }); + }); + }; return LogLevel; } -); \ No newline at end of file +); diff --git a/platform/framework/src/Main.js b/platform/framework/src/Main.js index 2faf115a85..cf8f270336 100644 --- a/platform/framework/src/Main.js +++ b/platform/framework/src/Main.js @@ -32,6 +32,11 @@ requirejs.config({ } }); +/** + * Implements the framework layer, which handles the loading of bundles + * and the wiring-together of the extensions they expose. + * @namespace platform/framework + */ define( [ 'require', @@ -129,4 +134,4 @@ define( requirejs.config({ "baseUrl": "" }); injector.invoke(['$http', '$log', initializeApplication]); } -); \ No newline at end of file +); diff --git a/platform/framework/src/bootstrap/ApplicationBootstrapper.js b/platform/framework/src/bootstrap/ApplicationBootstrapper.js index 8649175091..f191bbbdaa 100644 --- a/platform/framework/src/bootstrap/ApplicationBootstrapper.js +++ b/platform/framework/src/bootstrap/ApplicationBootstrapper.js @@ -38,27 +38,31 @@ define( * framework needs to wait until all extensions have been loaded * and registered. * + * @memberof platform/framework * @constructor */ function ApplicationBootstrapper(angular, document, $log) { - return { - /** - * Bootstrap the application. - * - * @method - * @memberof ApplicationBootstrapper# - * @param {angular.Module} app the Angular application to - * bootstrap - */ - bootstrap: function (app) { - $log.info("Bootstrapping application " + (app || {}).name); - angular.element(document).ready(function () { - angular.bootstrap(document, [app.name]); - }); - } - }; + this.angular = angular; + this.document = document; + this.$log = $log; } + /** + * Bootstrap the application. + * + * @param {angular.Module} app the Angular application to + * bootstrap + */ + ApplicationBootstrapper.prototype.bootstrap = function (app) { + var angular = this.angular, + document = this.document, + $log = this.$log; + $log.info("Bootstrapping application " + (app || {}).name); + angular.element(document).ready(function () { + angular.bootstrap(document, [app.name]); + }); + }; + return ApplicationBootstrapper; } -); \ No newline at end of file +); diff --git a/platform/framework/src/load/Bundle.js b/platform/framework/src/load/Bundle.js index 1a4265fe2c..a53d2761fd 100644 --- a/platform/framework/src/load/Bundle.js +++ b/platform/framework/src/load/Bundle.js @@ -38,6 +38,8 @@ define( * contains resource files used by this bundle * @property {Object.} [extensions={}] * all extensions exposed by this bundle + * @constructor + * @memberof platform/framework */ @@ -54,13 +56,7 @@ define( function Bundle(path, bundleDefinition) { // Start with defaults var definition = Object.create(Constants.DEFAULT_BUNDLE), - logName = path, - self; - - // Utility function for resolving paths in this bundle - function resolvePath(elements) { - return [path].concat(elements || []).join(Constants.SEPARATOR); - } + logName = path; // Override defaults with specifics from bundle definition Object.keys(bundleDefinition).forEach(function (k) { @@ -79,126 +75,138 @@ define( logName += ")"; } - self = { - /** - * Get the path to this bundle. - * @memberof Bundle# - * @returns {string} - */ - getPath: function () { - return path; - }, - /** - * Get the path to this bundle's source folder. If an - * argument is provided, the path will be to the source - * file within the bundle's source file. - * - * @memberof Bundle# - * @param {string} [sourceFile] optionally, give a path to - * a specific source file in the bundle. - * @returns {string} - */ - getSourcePath: function (sourceFile) { - var subpath = sourceFile ? - [ definition.sources, sourceFile ] : - [ definition.sources ]; - - return resolvePath(subpath); - }, - /** - * Get the path to this bundle's resource folder. If an - * argument is provided, the path will be to the resource - * file within the bundle's resource file. - * - * @memberof Bundle# - * @param {string} [resourceFile] optionally, give a path to - * a specific resource file in the bundle. - * @returns {string} - */ - getResourcePath: function (resourceFile) { - var subpath = resourceFile ? - [ definition.resources, resourceFile ] : - [ definition.resources ]; - - return resolvePath(subpath); - }, - /** - * Get the path to this bundle's library folder. If an - * argument is provided, the path will be to the library - * file within the bundle's resource file. - * - * @memberof Bundle# - * @param {string} [libraryFile] optionally, give a path to - * a specific library file in the bundle. - * @returns {string} - */ - getLibraryPath: function (libraryFile) { - var subpath = libraryFile ? - [ definition.libraries, libraryFile ] : - [ definition.libraries ]; - - return resolvePath(subpath); - }, - /** - * Get library configuration for this bundle. This is read - * from the bundle's definition; if the bundle is well-formed, - * it will resemble a require.config object. - * @memberof Bundle# - * @returns {object} - */ - getConfiguration: function () { - return definition.configuration || {}; - }, - /** - * Get a log-friendly name for this bundle; this will - * include both the key (machine-readable name for this - * bundle) and the name (human-readable name for this - * bundle.) - * @returns {string} log-friendly name for this bundle - */ - getLogName: function () { - return logName; - }, - /** - * Get all extensions exposed by this bundle of a given - * category. - * - * @param category - * @memberof Bundle# - * @returns {Array} - */ - getExtensions: function (category) { - var extensions = definition.extensions[category] || []; - - return extensions.map(function objectify(extDefinition) { - return new Extension(self, category, extDefinition); - }); - }, - /** - * Get a list of all categories of extension exposed by - * this bundle. - * - * @memberof Bundle# - * @returns {Array} - */ - getExtensionCategories: function () { - return Object.keys(definition.extensions); - }, - /** - * Get the plain definition of this bundle, as read from - * its JSON declaration. - * - * @memberof Bundle# - * @returns {BundleDefinition} the raw definition of this bundle - */ - getDefinition: function () { - return definition; - } - }; - - return self; + this.path = path; + this.definition = definition; + this.logName = logName; } + + // Utility function for resolving paths in this bundle + Bundle.prototype.resolvePath = function (elements) { + var path = this.path; + return [path].concat(elements || []).join(Constants.SEPARATOR); + }; + + + /** + * Get the path to this bundle. + * @returns {string} path to this bundle; + */ + Bundle.prototype.getPath = function () { + return this.path; + }; + + /** + * Get the path to this bundle's source folder. If an + * argument is provided, the path will be to the source + * file within the bundle's source file. + * + * @param {string} [sourceFile] optionally, give a path to + * a specific source file in the bundle. + * @returns {string} path to the source folder (or to the + * source file within it) + */ + Bundle.prototype.getSourcePath = function (sourceFile) { + var subpath = sourceFile ? + [ this.definition.sources, sourceFile ] : + [ this.definition.sources ]; + + return this.resolvePath(subpath); + }; + + /** + * Get the path to this bundle's resource folder. If an + * argument is provided, the path will be to the resource + * file within the bundle's resource file. + * + * @param {string} [resourceFile] optionally, give a path to + * a specific resource file in the bundle. + * @returns {string} path to the resource folder (or to the + * resource file within it) + */ + Bundle.prototype.getResourcePath = function (resourceFile) { + var subpath = resourceFile ? + [ this.definition.resources, resourceFile ] : + [ this.definition.resources ]; + + return this.resolvePath(subpath); + }; + + /** + * Get the path to this bundle's library folder. If an + * argument is provided, the path will be to the library + * file within the bundle's resource file. + * + * @param {string} [libraryFile] optionally, give a path to + * a specific library file in the bundle. + * @returns {string} path to the resource folder (or to the + * resource file within it) + */ + Bundle.prototype.getLibraryPath = function (libraryFile) { + var subpath = libraryFile ? + [ this.definition.libraries, libraryFile ] : + [ this.definition.libraries ]; + + return this.resolvePath(subpath); + }; + + /** + * Get library configuration for this bundle. This is read + * from the bundle's definition; if the bundle is well-formed, + * it will resemble a require.config object. + * @returns {object} library configuration + */ + Bundle.prototype.getConfiguration = function () { + return this.definition.configuration || {}; + }; + + /** + * Get a log-friendly name for this bundle; this will + * include both the key (machine-readable name for this + * bundle) and the name (human-readable name for this + * bundle.) + * @returns {string} log-friendly name for this bundle + */ + Bundle.prototype.getLogName = function () { + return this.logName; + }; + + /** + * Get all extensions exposed by this bundle of a given + * category. + * + * @param {string} category name of the extension category + * @returns {Array} extension definitions of that cataegory + */ + Bundle.prototype.getExtensions = function (category) { + var extensions = this.definition.extensions[category] || [], + self = this; + + return extensions.map(function objectify(extDefinition) { + return new Extension(self, category, extDefinition); + }); + }; + + /** + * Get a list of all extension categories exposed by this bundle. + * + * @returns {string[]} the extension categories + */ + Bundle.prototype.getExtensionCategories = function () { + return Object.keys(this.definition.extensions); + }; + + /** + * Get the plain definition of this bundle, as read from + * its JSON declaration. + * + * @returns {platform/framework.BundleDefinition} the raw + * definition of this bundle + */ + Bundle.prototype.getDefinition = function () { + return this.definition; + }; + return Bundle; } -); \ No newline at end of file +); diff --git a/platform/framework/src/load/BundleLoader.js b/platform/framework/src/load/BundleLoader.js index 0329681630..14b404f195 100644 --- a/platform/framework/src/load/BundleLoader.js +++ b/platform/framework/src/load/BundleLoader.js @@ -39,11 +39,28 @@ define( * useful to the framework. This provides the base information which * will be used by later phases of framework layer initialization. * + * @memberof platform/framework * @constructor - * @param {object} $http Angular's HTTP requester - * @param {object} $log Angular's logging service + * @param $http Angular's HTTP requester + * @param $log Angular's logging service */ function BundleLoader($http, $log) { + this.$http = $http; + this.$log = $log; + + } + + /** + * Load a group of bundles, to be used to constitute the + * application by later framework initialization phases. + * + * @param {string|string[]} an array of bundle names to load, or + * the name of a JSON file containing that array + * @returns {Promise.} a promise for the loaded bundles + */ + BundleLoader.prototype.loadBundles = function (bundles) { + var $http = this.$http, + $log = this.$log; // Utility function; load contents of JSON file using $http function getJSON(file) { @@ -91,7 +108,7 @@ define( var bundlePromises = bundleArray.map(loadBundle); return Promise.all(bundlePromises) - .then(filterBundles); + .then(filterBundles); } // Load all bundles named in the referenced file. The file is @@ -100,31 +117,11 @@ define( return getJSON(listFile).then(loadBundlesFromArray); } - // Load all indicated bundles. If the argument is an array, - // this is taken to be a list of all bundles to load; if it - // is a string, then it is treated as the name of a JSON - // file containing the list of bundles to load. - function loadBundles(bundles) { - return Array.isArray(bundles) ? loadBundlesFromArray(bundles) : - (typeof bundles === 'string') ? loadBundlesFromFile(bundles) : - Promise.reject(new Error(INVALID_ARGUMENT_MESSAGE)); - } - - - return { - /** - * Load a group of bundles, to be used to constitute the - * application by later framework initialization phases. - * - * @memberof BundleLoader# - * @param {string|string[]} an array of bundle names to load, or - * the name of a JSON file containing that array - * @returns {Promise.} - */ - loadBundles: loadBundles - }; - } + return Array.isArray(bundles) ? loadBundlesFromArray(bundles) : + (typeof bundles === 'string') ? loadBundlesFromFile(bundles) : + Promise.reject(new Error(INVALID_ARGUMENT_MESSAGE)); + }; return BundleLoader; } -); \ No newline at end of file +); diff --git a/platform/framework/src/load/Extension.js b/platform/framework/src/load/Extension.js index 3063838a14..d3b19f94fa 100644 --- a/platform/framework/src/load/Extension.js +++ b/platform/framework/src/load/Extension.js @@ -38,6 +38,8 @@ define( * @property {string[]} [depends=[]] the dependencies needed by this * extension; these are strings as shall be passed to * Angular's dependency resolution mechanism. + * @constructor + * @memberof platform/framework */ /** @@ -76,88 +78,94 @@ define( // Attach bundle metadata extensionDefinition.bundle = bundle.getDefinition(); - return { - /** - * Get the machine-readable identifier for this extension. - * - * @returns {string} - */ - getKey: function () { - return definition.key || "undefined"; - }, - /** - * Get the bundle which declared this extension. - * - * @memberof Extension# - * @returns {Bundle} - */ - getBundle: function () { - return bundle; - }, - /** - * Get the category into which this extension falls. - * (e.g. "directives") - * - * @memberof Extension# - * @returns {string} - */ - getCategory: function () { - return category; - }, - /** - * Check whether or not this extension should have an - * associated implementation module which may need to - * be loaded. - * - * @returns {boolean} true if an implementation separate - * from this definition should also be loaded - */ - hasImplementation: function () { - return definition.implementation !== undefined; - }, - /** - * Get the path to the AMD module which implements this - * extension. Will return undefined if there is no - * implementation associated with this extension. - * - * @memberof Extension# - * @returns {string} path to implementation, or undefined - */ - getImplementationPath: function () { - return definition.implementation ? - bundle.getSourcePath(definition.implementation) : - undefined; - }, - /** - * Get a log-friendly name for this extension; this will - * include both the key (machine-readable name for this - * extension) and the name (human-readable name for this - * extension.) - * @returns {string} log-friendly name for this extension - */ - getLogName: function () { - return logName; - }, - /** - * Get the plain definition of the extension. - * - * Note that this definition will have an additional "bundle" - * field which points back to the bundle which defined the - * extension, as a convenience. - * - * @memberof Extension# - * @returns {ExtensionDefinition} the plain definition of - * this extension, as read from the bundle - * declaration. - */ - getDefinition: function () { - return extensionDefinition; - } - - }; + this.logName = logName; + this.bundle = bundle; + this.category = category; + this.definition = definition; + this.extensionDefinition = extensionDefinition; } + /** + * Get the machine-readable identifier for this extension. + * + * @returns {string} the identifier for this extension + */ + Extension.prototype.getKey = function () { + return this.definition.key || "undefined"; + }; + + /** + * Get the bundle which declared this extension. + * + * @returns {Bundle} the declaring bundle + */ + Extension.prototype.getBundle = function () { + return this.bundle; + }; + + /** + * Get the category into which this extension falls. + * (e.g. "directives") + * + * @returns {string} the extension category + */ + Extension.prototype.getCategory = function () { + return this.category; + }; + + /** + * Check whether or not this extension should have an + * associated implementation module which may need to + * be loaded. + * + * @returns {boolean} true if an implementation separate + * from this definition should also be loaded + */ + Extension.prototype.hasImplementation = function () { + return this.definition.implementation !== undefined; + }; + + /** + * Get the path to the AMD module which implements this + * extension. Will return undefined if there is no + * implementation associated with this extension. + * + * @returns {string} path to implementation, or undefined + */ + Extension.prototype.getImplementationPath = function () { + return this.definition.implementation ? + this.bundle.getSourcePath(this.definition.implementation) : + undefined; + }; + + /** + * Get a log-friendly name for this extension; this will + * include both the key (machine-readable name for this + * extension) and the name (human-readable name for this + * extension.) + * + * @returns {string} log-friendly name for this extension + */ + Extension.prototype.getLogName = function () { + return this.logName; + }; + + /** + * Get the plain definition of the extension. + * + * Note that this definition will have an additional "bundle" + * field which points back to the bundle which defined the + * extension, as a convenience. + * + * @returns {ExtensionDefinition} the plain definition of + * this extension, as read from the bundle + * declaration. + */ + Extension.prototype.getDefinition = function () { + return this.extensionDefinition; + }; + return Extension; } -); \ No newline at end of file +); diff --git a/platform/framework/src/register/CustomRegistrars.js b/platform/framework/src/register/CustomRegistrars.js index 5920e4c630..04c3bbce7a 100644 --- a/platform/framework/src/register/CustomRegistrars.js +++ b/platform/framework/src/register/CustomRegistrars.js @@ -33,135 +33,195 @@ define( * Handles registration of a few specific extension types that are * understood natively by Angular. This includes services and * directives. + * @memberof platform/framework * @constructor */ function CustomRegistrars(app, $log) { + this.app = app; + this.$log = $log; + } - // Used to create custom registration functions which map to - // named methods on Angular modules, which follow the normal - // app.method(key, [ deps..., function ]) pattern. - function CustomRegistrar(angularFunction) { - return function (extension, index) { - var key = extension.key, - dependencies = extension.depends || []; - - - if (!key) { - $log.warn([ - "Cannot register ", - angularFunction, - " ", - index, - ", no key specified. ", - JSON.stringify(extension) - ].join("")); - } else { - $log.info([ - "Registering ", - angularFunction, - ": ", - key - ].join("")); - app[angularFunction]( - key, - dependencies.concat([extension]) - ); - } - }; - } - - function registerConstant(extension) { - var key = extension.key, - value = extension.value; - - if (typeof key === "string" && value !== undefined) { - $log.info([ - "Registering constant: ", - key, - " with value ", - value - ].join("")); - app.constant(key, value); - } else { - $log.warn([ - "Cannot register constant ", - key, - " with value ", - value - ].join("")); - } - - } - - // Custom registration function for extensions of category "runs" - function registerRun(extension) { - if (typeof extension === 'function') { - // Prepend dependencies, and schedule to run - app.run((extension.depends || []).concat([extension])); - } else { - // If it's not a function, no implementation was given - $log.warn([ - "Cannot register run extension from ", - (extension.bundle || {}).path, - "; no implementation." - ].join("")); - } - } - - // Custom registration function for extensions of category "route" - function registerRoute(extension) { - var route = Object.create(extension); - - // Adjust path for bundle - if (route.templateUrl) { - route.templateUrl = [ - route.bundle.path, - route.bundle.resources, - route.templateUrl - ].join(Constants.SEPARATOR); - } - - // Log the registration - $log.info("Registering route: " + (route.key || route.when)); - - // Register the route with Angular - app.config(['$routeProvider', function ($routeProvider) { - if (route.when) { - $routeProvider.when(route.when, route); - } else { - $routeProvider.otherwise(route); - } - }]); - } - - // Handle service compositing - function registerComponents(components) { - return new ServiceCompositor(app, $log) - .registerCompositeServices(components); - } - - // Utility; create a function which converts another function - // (which acts on single objects) to one which acts upon arrays. - function mapUpon(func) { - return function (array) { - return array.map(func); - }; - } - - // More like key-value pairs than methods; key is the - // name of the extension category to be handled, and the value - // is the function which handles it. - return { - constants: mapUpon(registerConstant), - routes: mapUpon(registerRoute), - directives: mapUpon(new CustomRegistrar("directive")), - controllers: mapUpon(new CustomRegistrar("controller")), - services: mapUpon(new CustomRegistrar("service")), - runs: mapUpon(registerRun), - components: registerComponents + // Utility; bind a function to a "this" pointer + function bind(fn, thisArg) { + return function () { + return fn.apply(thisArg, arguments); }; } + // Used to create custom registration functions which map to + // named methods on Angular modules, which follow the normal + // app.method(key, [ deps..., function ]) pattern. + function customRegistrar(angularFunction) { + return function (extension, index) { + var app = this.app, + $log = this.$log, + key = extension.key, + dependencies = extension.depends || []; + + if (!key) { + $log.warn([ + "Cannot register ", + angularFunction, + " ", + index, + ", no key specified. ", + JSON.stringify(extension) + ].join("")); + } else { + $log.info([ + "Registering ", + angularFunction, + ": ", + key + ].join("")); + app[angularFunction]( + key, + dependencies.concat([extension]) + ); + } + }; + } + + function registerConstant(extension) { + var app = this.app, + $log = this.$log, + key = extension.key, + value = extension.value; + + if (typeof key === "string" && value !== undefined) { + $log.info([ + "Registering constant: ", + key, + " with value ", + value + ].join("")); + app.constant(key, value); + } else { + $log.warn([ + "Cannot register constant ", + key, + " with value ", + value + ].join("")); + } + + } + + // Custom registration function for extensions of category "runs" + function registerRun(extension) { + var app = this.app, + $log = this.$log; + + if (typeof extension === 'function') { + // Prepend dependencies, and schedule to run + app.run((extension.depends || []).concat([extension])); + } else { + // If it's not a function, no implementation was given + $log.warn([ + "Cannot register run extension from ", + (extension.bundle || {}).path, + "; no implementation." + ].join("")); + } + } + + // Custom registration function for extensions of category "route" + function registerRoute(extension) { + var app = this.app, + $log = this.$log, + route = Object.create(extension); + + // Adjust path for bundle + if (route.templateUrl) { + route.templateUrl = [ + route.bundle.path, + route.bundle.resources, + route.templateUrl + ].join(Constants.SEPARATOR); + } + + // Log the registration + $log.info("Registering route: " + (route.key || route.when)); + + // Register the route with Angular + app.config(['$routeProvider', function ($routeProvider) { + if (route.when) { + $routeProvider.when(route.when, route); + } else { + $routeProvider.otherwise(route); + } + }]); + } + + // Handle service compositing + function registerComponents(components) { + var app = this.app, + $log = this.$log; + return new ServiceCompositor(app, $log) + .registerCompositeServices(components); + } + + // Utility; create a function which converts another function + // (which acts on single objects) to one which acts upon arrays. + function mapUpon(func) { + return function (array) { + return array.map(bind(func, this)); + }; + } + + // More like key-value pairs than methods; key is the + // name of the extension category to be handled, and the value + // is the function which handles it. + + /** + * Register constant values. + * @param {Array} extensions the resolved extensions + */ + CustomRegistrars.prototype.constants = + mapUpon(registerConstant); + + /** + * Register Angular routes. + * @param {Array} extensions the resolved extensions + */ + CustomRegistrars.prototype.routes = + mapUpon(registerRoute); + + /** + * Register Angular directives. + * @param {Array} extensions the resolved extensions + */ + CustomRegistrars.prototype.directives = + mapUpon(customRegistrar("directive")); + + /** + * Register Angular controllers. + * @param {Array} extensions the resolved extensions + */ + CustomRegistrars.prototype.controllers = + mapUpon(customRegistrar("controller")); + + /** + * Register Angular services. + * @param {Array} extensions the resolved extensions + */ + CustomRegistrars.prototype.services = + mapUpon(customRegistrar("service")); + + /** + * Register functions which will run after bootstrapping. + * @param {Array} extensions the resolved extensions + */ + CustomRegistrars.prototype.runs = + mapUpon(registerRun); + + /** + * Register components of composite services. + * @param {Array} extensions the resolved extensions + */ + CustomRegistrars.prototype.components = + registerComponents; + return CustomRegistrars; } -); \ No newline at end of file +); diff --git a/platform/framework/src/register/ExtensionRegistrar.js b/platform/framework/src/register/ExtensionRegistrar.js index cbc44562d0..352d3b9f3f 100644 --- a/platform/framework/src/register/ExtensionRegistrar.js +++ b/platform/framework/src/register/ExtensionRegistrar.js @@ -32,6 +32,7 @@ define( /** * Responsible for registering extensions with Angular. * + * @memberof platform/framework * @constructor * @param {angular.Module} the Angular application with which * extensions should be registered @@ -46,14 +47,41 @@ define( // Track which extension categories have already been registered. // Exceptions will be thrown if the same extension category is // registered twice. - var registeredCategories = {}; + this.registeredCategories = {}; + this.customRegistrars = customRegistrars || {}; + this.app = app; + this.sorter = sorter; + this.$log = $log; + } + + /** + * Register a group of resolved extensions with the Angular + * module managed by this registrar. + * + * For convenient chaining (particularly from the framework + * initializer's perspective), this returns the Angular + * module with which extensions were registered. + * + * @param {Object.} extensionGroup an object + * containing key-value pairs, where keys are extension + * categories and values are arrays of resolved + * extensions + * @returns {angular.Module} the application module with + * which extensions were registered + */ + ExtensionRegistrar.prototype.registerExtensions = function (extensionGroup) { + var registeredCategories = this.registeredCategories, + customRegistrars = this.customRegistrars, + app = this.app, + sorter = this.sorter, + $log = this.$log; // Used to build unique identifiers for individual extensions, // so that these can be registered separately with Angular function identify(category, extension, index) { var name = extension.key ? - ("extension-" + extension.key + "#" + index) : - ("extension#" + index); + ("extension-" + extension.key + "#" + index) : + ("extension#" + index); return category + "[" + name + "]"; } @@ -75,8 +103,8 @@ define( function makeServiceArgument(category, extension) { var dependencies = extension.depends || [], factory = (typeof extension === 'function') ? - new PartialConstructor(extension) : - staticFunction(extension); + new PartialConstructor(extension) : + staticFunction(extension); return dependencies.concat([factory]); } @@ -128,9 +156,9 @@ define( // an extension category (e.g. is suffixed by []) function isExtensionDependency(dependency) { var index = dependency.indexOf( - Constants.EXTENSION_SUFFIX, - dependency.length - Constants.EXTENSION_SUFFIX.length - ); + Constants.EXTENSION_SUFFIX, + dependency.length - Constants.EXTENSION_SUFFIX.length + ); return index !== -1; } @@ -152,8 +180,8 @@ define( (extension.depends || []).filter( isExtensionDependency ).forEach(function (dependency) { - needed[dependency] = true; - }); + needed[dependency] = true; + }); }); // Remove categories which have been provided @@ -173,53 +201,30 @@ define( findEmptyExtensionDependencies( extensionGroup ).forEach(function (name) { - $log.info("Registering empty extension category " + name); - app.factory(name, [staticFunction([])]); - }); + $log.info("Registering empty extension category " + name); + app.factory(name, [staticFunction([])]); + }); } - function registerExtensionGroup(extensionGroup) { - // Announce we're entering a new phase - $log.info("Registering extensions..."); + // Announce we're entering a new phase + $log.info("Registering extensions..."); - // Register all declared extensions by category - Object.keys(extensionGroup).forEach(function (category) { - registerExtensionsForCategory( - category, - sorter.sort(extensionGroup[category]) - ); - }); + // Register all declared extensions by category + Object.keys(extensionGroup).forEach(function (category) { + registerExtensionsForCategory( + category, + sorter.sort(extensionGroup[category]) + ); + }); - // Also handle categories which are needed but not declared - registerEmptyDependencies(extensionGroup); + // Also handle categories which are needed but not declared + registerEmptyDependencies(extensionGroup); - // Return the application to which these extensions - // have been registered - return app; - } - - customRegistrars = customRegistrars || {}; - - return { - /** - * Register a group of resolved extensions with the Angular - * module managed by this registrar. - * - * For convenient chaining (particularly from the framework - * initializer's perspective), this returns the Angular - * module with which extensions were registered. - * - * @param {Object.} extensionGroup an object - * containing key-value pairs, where keys are extension - * categories and values are arrays of resolved - * extensions - * @returns {angular.Module} the application module with - * which extensions were registered - */ - registerExtensions: registerExtensionGroup - }; - } + // Return the application to which these extensions + // have been registered + return app; + }; return ExtensionRegistrar; } -); \ No newline at end of file +); diff --git a/platform/framework/src/register/ExtensionSorter.js b/platform/framework/src/register/ExtensionSorter.js index fb401de351..9bc902da9e 100644 --- a/platform/framework/src/register/ExtensionSorter.js +++ b/platform/framework/src/register/ExtensionSorter.js @@ -34,9 +34,21 @@ define( * specify symbolic properties as strings (instead of numbers), * which will be looked up from the table `Constants.PRIORITY_LEVELS`. * @param $log Angular's logging service + * @memberof platform/framework * @constructor */ function ExtensionSorter($log) { + this.$log = $log; + } + + /** + * Sort extensions according to priority. + * + * @param {object[]} extensions array of resolved extensions + * @returns {object[]} the same extensions, in priority order + */ + ExtensionSorter.prototype.sort = function (extensions) { + var $log = this.$log; // Handle unknown or malformed priorities specified by extensions function unrecognizedPriority(extension) { @@ -67,7 +79,7 @@ define( // Should be a number; otherwise, issue a warning and // fall back to default priority level. return (typeof priority === 'number') ? - priority : unrecognizedPriority(extension); + priority : unrecognizedPriority(extension); } // Attach a numeric priority to an extension; this is done in @@ -97,22 +109,12 @@ define( return (b.priority - a.priority) || (a.index - b.index); } - return { - /** - * Sort extensions according to priority. - * - * @param {object[]} extensions array of resolved extensions - * @returns {object[]} the same extensions, in priority order - */ - sort: function (extensions) { - return (extensions || []) - .map(prioritize) - .sort(compare) - .map(deprioritize); - } - }; - } + return (extensions || []) + .map(prioritize) + .sort(compare) + .map(deprioritize); + }; return ExtensionSorter; } -); \ No newline at end of file +); diff --git a/platform/framework/src/register/PartialConstructor.js b/platform/framework/src/register/PartialConstructor.js index 3778a2519a..46598f6402 100644 --- a/platform/framework/src/register/PartialConstructor.js +++ b/platform/framework/src/register/PartialConstructor.js @@ -43,6 +43,7 @@ define( * instantiate instances of these extensions by passing only * those per-instance arguments. * + * @memberof platform/framework * @constructor */ function PartialConstructor(Constructor) { @@ -76,4 +77,4 @@ define( return PartialConstructor; } -); \ No newline at end of file +); diff --git a/platform/framework/src/register/ServiceCompositor.js b/platform/framework/src/register/ServiceCompositor.js index d44e8cb24b..349e0c92e4 100644 --- a/platform/framework/src/register/ServiceCompositor.js +++ b/platform/framework/src/register/ServiceCompositor.js @@ -33,11 +33,34 @@ define( * Handles service compositing; that is, building up services * from provider, aggregator, and decorator components. * + * @memberof platform/framework * @constructor */ function ServiceCompositor(app, $log) { - var latest = {}, - providerLists = {}; // Track latest services registered + this.latest = {}; + this.providerLists = {}; // Track latest services registered + this.app = app; + this.$log = $log; + } + + /** + * Register composite services with Angular. This will build + * up a dependency hierarchy between providers, aggregators, + * and/or decorators, such that a dependency upon the service + * type they expose shall be satisfied by their fully-wired + * whole. + * + * Note that this method assumes that a complete set of + * components shall be provided. Multiple calls to this + * method may not behave as expected. + * + * @param {Array} components extensions of category component + */ + ServiceCompositor.prototype.registerCompositeServices = function (components) { + var latest = this.latest, + providerLists = this.providerLists, + app = this.app, + $log = this.$log; // Log a warning; defaults to "no service provided by" function warn(extension, category, message) { @@ -199,33 +222,14 @@ define( registerLatest(); } - // Initial point of entry; just separate components by type - function registerCompositeServices(components) { - registerComposites( - components.filter(hasType("provider")), - components.filter(hasType("aggregator")), - components.filter(hasType("decorator")) - ); - } - - return { - /** - * Register composite services with Angular. This will build - * up a dependency hierarchy between providers, aggregators, - * and/or decorators, such that a dependency upon the service - * type they expose shall be satisfied by their fully-wired - * whole. - * - * Note that this method assumes that a complete set of - * components shall be provided. Multiple calls to this - * method may not behave as expected. - * - * @param {Array} components extensions of category component - */ - registerCompositeServices: registerCompositeServices - }; - } + // Initial point of entry; split into three component types. + registerComposites( + components.filter(hasType("provider")), + components.filter(hasType("aggregator")), + components.filter(hasType("decorator")) + ); + }; return ServiceCompositor; } -); \ No newline at end of file +); diff --git a/platform/framework/src/resolve/BundleResolver.js b/platform/framework/src/resolve/BundleResolver.js index 5c8216da94..4360764aee 100644 --- a/platform/framework/src/resolve/BundleResolver.js +++ b/platform/framework/src/resolve/BundleResolver.js @@ -34,11 +34,31 @@ define( * initialization. During this phase, any scripts implementing * extensions provided by bundles are loaded. * + * @memberof platform/framework * @constructor */ function BundleResolver(extensionResolver, requireConfigurator, $log) { + this.extensionResolver = extensionResolver; + this.requireConfigurator = requireConfigurator; + this.$log = $log; + } - /** + /** + * Resolve all extensions exposed by these bundles. + * + * @param {Bundle[]} bundles the bundles to resolve + * @returns {Promise.>} an promise + * for an object containing + * key-value pairs, where keys are extension + * categories and values are arrays of resolved + * extensions belonging to those categories + */ + BundleResolver.prototype.resolveBundles = function (bundles) { + var extensionResolver = this.extensionResolver, + requireConfigurator = this.requireConfigurator, + $log = this.$log; + + /* * Merge resolved bundles (where each is expressed as an * object containing key-value pairs, where keys are extension * categories and values are arrays of resolved extensions) @@ -47,6 +67,7 @@ define( * * @param {Object.|Array} resolvedBundles * @returns {Object.} + * @memberof platform/framework.BundleResolver# */ function mergeResolvedBundles(resolvedBundles) { var result = {}; @@ -97,28 +118,14 @@ define( .then(giveResult); } - return { - /** - * Resolve all extensions exposed by these bundles. - * - * @param {Bundle[]} bundles the bundles to resolve - * @returns {Promise.>} an promise - * for an object containing - * key-value pairs, where keys are extension - * categories and values are arrays of resolved - * extensions belonging to those categories - */ - resolveBundles: function (bundles) { - // First, make sure Require is suitably configured - requireConfigurator.configure(bundles); + // First, make sure Require is suitably configured + requireConfigurator.configure(bundles); - // Then, resolve all extension implementations. - return Promise.all(bundles.map(resolveBundle)) - .then(mergeResolvedBundles); - } - }; - } + // Then, resolve all extension implementations. + return Promise.all(bundles.map(resolveBundle)) + .then(mergeResolvedBundles); + }; return BundleResolver; } -); \ No newline at end of file +); diff --git a/platform/framework/src/resolve/ExtensionResolver.js b/platform/framework/src/resolve/ExtensionResolver.js index 0257664f1f..e4c2710c0f 100644 --- a/platform/framework/src/resolve/ExtensionResolver.js +++ b/platform/framework/src/resolve/ExtensionResolver.js @@ -35,22 +35,51 @@ define( * * @param {ImplementationLoader} loader used to load implementations * @param {*} $log Angular's logging service + * @memberof platform/framework * @constructor */ function ExtensionResolver(loader, $log) { + this.loader = loader; + this.$log = $log; + } + + /** + * Resolve the provided extension; this will give a promise + * for the extension's implementation, if one has been + * specified, or for the plain definition of the extension + * otherwise. The plain definition will also be given + * if the implementation fails to load for some reason. + * + * All key-value pairs from the extension definition + * will additionally be attached to any loaded implementation. + * + * @param {Extension} extension the extension to resolve + * @returns {Promise} a promise for the resolved extension + */ + ExtensionResolver.prototype.resolve = function (extension) { + var loader = this.loader, + $log = this.$log; + function loadImplementation(extension) { var implPath = extension.getImplementationPath(), implPromise = loader.load(implPath), definition = extension.getDefinition(); + // Wrap a constructor function (to avoid modifying the original) + function constructorFor(impl) { + function Constructor() { + return impl.apply(this, arguments); + } + Constructor.prototype = impl.prototype; + return Constructor; + } + // Attach values from the object definition to the // loaded implementation. function attachDefinition(impl) { var result = (typeof impl === 'function') ? - function () { - return impl.apply({}, arguments); - } : - Object.create(impl); + constructorFor(impl) : + Object.create(impl); // Copy over static properties Object.keys(impl).forEach(function (k) { @@ -76,11 +105,11 @@ define( function handleError(err) { // Build up a log message from parts var message = [ - "Could not load implementation for extension ", - extension.getLogName(), - " due to ", - err.message - ].join(""); + "Could not load implementation for extension ", + extension.getLogName(), + " due to ", + err.message + ].join(""); // Log that the extension was not loaded $log.warn(message); @@ -99,33 +128,17 @@ define( return implPromise.then(attachDefinition, handleError); } - return { - /** - * Resolve the provided extension; this will give a promise - * for the extension's implementation, if one has been - * specified, or for the plain definition of the extension - * otherwise. The plain definition will also be given - * if the implementation fails to load for some reason. - * - * All key-value pairs from the extension definition - * will additionally be attached to any loaded implementation. - * - * @param {Extension} extension - */ - resolve: function (extension) { - // Log that loading has begun - $log.info([ - "Resolving extension ", - extension.getLogName() - ].join("")); + // Log that loading has begun + $log.info([ + "Resolving extension ", + extension.getLogName() + ].join("")); - return extension.hasImplementation() ? - loadImplementation(extension) : - Promise.resolve(extension.getDefinition()); - } - }; - } + return extension.hasImplementation() ? + loadImplementation(extension) : + Promise.resolve(extension.getDefinition()); + }; return ExtensionResolver; } -); \ No newline at end of file +); diff --git a/platform/framework/src/resolve/ImplementationLoader.js b/platform/framework/src/resolve/ImplementationLoader.js index ce0abe104c..c9f1ae8bc9 100644 --- a/platform/framework/src/resolve/ImplementationLoader.js +++ b/platform/framework/src/resolve/ImplementationLoader.js @@ -33,35 +33,33 @@ define( * Responsible for loading extension implementations * (AMD modules.) Acts as a wrapper around RequireJS to * provide a promise-like API. + * @memberof platform/framework * @constructor * @param {*} require RequireJS, or an object with similar API * @param {*} $log Angular's logging service */ function ImplementationLoader(require) { - function loadModule(path) { - return new Promise(function (fulfill, reject) { - require([path], fulfill, reject); - }); - } - - return { - /** - * Load an extension's implementation; or, equivalently, - * load an AMD module. This is fundamentally similar - * to a call to RequireJS, except that the result is - * wrapped in a promise. The promise will be fulfilled - * with the loaded module, or rejected with the error - * reported by Require. - * - * @method - * @memberof ImplementationLoader# - * @param {string} path the path to the module to load - * @returns {Promise} a promise for the specified module. - */ - load: loadModule - }; + this.require = require; } + /** + * Load an extension's implementation; or, equivalently, + * load an AMD module. This is fundamentally similar + * to a call to RequireJS, except that the result is + * wrapped in a promise. The promise will be fulfilled + * with the loaded module, or rejected with the error + * reported by Require. + * + * @param {string} path the path to the module to load + * @returns {Promise} a promise for the specified module. + */ + ImplementationLoader.prototype.load = function loadModule(path) { + var require = this.require; + return new Promise(function (fulfill, reject) { + require([path], fulfill, reject); + }); + }; + return ImplementationLoader; } -); \ No newline at end of file +); diff --git a/platform/framework/src/resolve/RequireConfigurator.js b/platform/framework/src/resolve/RequireConfigurator.js index 7900efeed9..f55ac559c1 100644 --- a/platform/framework/src/resolve/RequireConfigurator.js +++ b/platform/framework/src/resolve/RequireConfigurator.js @@ -30,83 +30,85 @@ define( * Handles configuration of RequireJS to expose libraries * from bundles with module names that can be used from other * bundles. + * @memberof platform/framework * @constructor * @param requirejs an instance of RequireJS */ function RequireConfigurator(requirejs) { - // Utility function to clone part of a bundle definition - function clone(obj) { - return JSON.parse(JSON.stringify(obj)); - } - - // Look up module configuration from the bundle definition. - // This will adjust paths to libraries as-needed. - function getConfiguration(bundle) { - var configuration = bundle.getConfiguration(); - - // Adjust paths to point to libraries - if (configuration.paths) { - // Don't modify the actual bundle definition... - configuration = clone(configuration); - // ...replace values in a clone instead. - Object.keys(configuration.paths).forEach(function (path) { - configuration.paths[path] = - bundle.getLibraryPath(configuration.paths[path]); - }); - } - - return configuration; - } - - // Build up paths and shim values from multiple bundles; - // this is sensitive to the value from baseConfiguration - // passed via reduce in buildConfiguration below, insofar - // as it assumes paths and shim will have initial empty values. - function mergeConfigurations(base, next) { - ["paths", "shim"].forEach(function (k) { - Object.keys(next[k] || {}).forEach(function (p) { - base[k][p] = next[k][p]; - }); - }); - return base; - } - - // Build a configuration object, to pass to requirejs.config, - // based on the defined configurations for all bundles. - // The paths and shim properties from all bundles will be - // merged to allow one requirejs.config call. - function buildConfiguration(bundles) { - // Provide an initial requirejs configuration... - var baseConfiguration = { - baseUrl: "", - paths: {}, - shim: {} - }, - // ...and pull out all bundle-specific parts - bundleConfigurations = bundles.map(getConfiguration); - - // Reduce this into one configuration object. - return bundleConfigurations.reduce( - mergeConfigurations, - baseConfiguration - ); - } - - return { - /** - * Configure RequireJS to utilize any path/shim definitions - * provided by these bundles. - * - * @param {Bundle[]} the bundles to include in this - * configuration - */ - configure: function (bundles) { - return requirejs.config(buildConfiguration(bundles)); - } - }; + this.requirejs = requirejs; } + // Utility function to clone part of a bundle definition + function clone(obj) { + return JSON.parse(JSON.stringify(obj)); + } + + // Look up module configuration from the bundle definition. + // This will adjust paths to libraries as-needed. + function getConfiguration(bundle) { + var configuration = bundle.getConfiguration(); + + // Adjust paths to point to libraries + if (configuration.paths) { + // Don't modify the actual bundle definition... + configuration = clone(configuration); + // ...replace values in a clone instead. + Object.keys(configuration.paths).forEach(function (path) { + configuration.paths[path] = + bundle.getLibraryPath(configuration.paths[path]); + }); + } + + return configuration; + } + + // Build up paths and shim values from multiple bundles; + // this is sensitive to the value from baseConfiguration + // passed via reduce in buildConfiguration below, insofar + // as it assumes paths and shim will have initial empty values. + function mergeConfigurations(base, next) { + ["paths", "shim"].forEach(function (k) { + Object.keys(next[k] || {}).forEach(function (p) { + base[k][p] = next[k][p]; + }); + }); + return base; + } + + // Build a configuration object, to pass to requirejs.config, + // based on the defined configurations for all bundles. + // The paths and shim properties from all bundles will be + // merged to allow one requirejs.config call. + function buildConfiguration(bundles) { + // Provide an initial requirejs configuration... + var baseConfiguration = { + baseUrl: "", + paths: {}, + shim: {} + }, + // ...and pull out all bundle-specific parts + bundleConfigurations = bundles.map(getConfiguration); + + // Reduce this into one configuration object. + return bundleConfigurations.reduce( + mergeConfigurations, + baseConfiguration + ); + } + + /** + * Configure RequireJS to utilize any path/shim definitions + * provided by these bundles. + * + * @param {Bundle[]} the bundles to include in this + * configuration + * @memberof platform/framework.RequireConfigurator# + */ + RequireConfigurator.prototype.configure = function (bundles) { + return this.requirejs.config(buildConfiguration(bundles)); + }; + return RequireConfigurator; } -); \ No newline at end of file +); diff --git a/platform/persistence/cache/src/CachingPersistenceDecorator.js b/platform/persistence/cache/src/CachingPersistenceDecorator.js index 5647f3c9e1..3053bc088a 100644 --- a/platform/persistence/cache/src/CachingPersistenceDecorator.js +++ b/platform/persistence/cache/src/CachingPersistenceDecorator.js @@ -21,6 +21,11 @@ *****************************************************************************/ /*global define*/ +/** + * This bundle decorates the persistence service to maintain a local cache + * of persisted documents. + * @namespace platform/persistence/cache + */ define( [], function () { @@ -31,72 +36,19 @@ define( * that have been loaded, and keeps them in sync after writes. This allows * retrievals to occur more quickly after the first load. * + * @memberof platform/persistence/cache * @constructor - * @param {string[]} CACHE_SPACES persistence space names which + * @param {string[]} cacheSpaces persistence space names which * should be cached * @param {PersistenceService} persistenceService the service which * implements object persistence, whose inputs/outputs * should be cached. + * @implements {PersistenceService} */ - function CachingPersistenceDecorator(CACHE_SPACES, persistenceService) { - var spaces = CACHE_SPACES || [], // List of spaces to cache + function CachingPersistenceDecorator(cacheSpaces, persistenceService) { + var spaces = cacheSpaces || [], // List of spaces to cache cache = {}; // Where objects will be stored - // Update the cached instance of an object to a new value - function replaceValue(valueHolder, newValue) { - var v = valueHolder.value; - - // If it's a JS object, we want to replace contents, so that - // everybody gets the same instance. - if (typeof v === 'object' && v !== null) { - // Only update contents if these are different instances - if (v !== newValue) { - // Clear prior contents - Object.keys(v).forEach(function (k) { - delete v[k]; - }); - // Shallow-copy contents - Object.keys(newValue).forEach(function (k) { - v[k] = newValue[k]; - }); - } - } else { - // Otherwise, just store the new value - valueHolder.value = newValue; - } - } - - // Place value in the cache for space, if there is one. - function addToCache(space, key, value) { - if (cache[space]) { - if (cache[space][key]) { - replaceValue(cache[space][key], value); - } else { - cache[space][key] = { value: value }; - } - } - } - - // Create a function for putting value into a cache; - // useful for then-chaining. - function putCache(space, key) { - return function (value) { - addToCache(space, key, value); - return value; - }; - } - - // Wrap as a thenable; used instead of $q.when because that - // will resolve on a future tick, which can cause latency - // issues (which this decorator is intended to address.) - function fastPromise(value) { - return { - then: function (callback) { - return fastPromise(callback(value)); - } - }; - } - // Arrayify list of spaces to cache, if necessary. spaces = Array.isArray(spaces) ? spaces : [ spaces ]; @@ -105,97 +57,107 @@ define( cache[space] = {}; }); - // Provide PersistenceService interface; mostly delegate to the - // decorated service, intervene and cache where appropriate. + this.spaces = spaces; + this.cache = cache; + this.persistenceService = persistenceService; + } + + // Wrap as a thenable; used instead of $q.when because that + // will resolve on a future tick, which can cause latency + // issues (which this decorator is intended to address.) + function fastPromise(value) { return { - /** - * List all persistence spaces that are supported by the - * decorated service. - * @memberof CachingPersistenceDecorator# - * @returns {Promise.} spaces supported - */ - listSpaces: function () { - return persistenceService.listSpaces(); - }, - /** - * List all objects in a specific space. - * @memberof CachingPersistenceDecorator# - * @param {string} space the space in which to list objects - * @returns {Promise.} keys for objects in this space - */ - listObjects: function (space) { - return persistenceService.listObjects(space); - }, - /** - * Create an object in a specific space. This will - * be cached to expedite subsequent retrieval. - * @memberof CachingPersistenceDecorator# - * @param {string} space the space in which to create the object - * @param {string} key the key associate with the object for - * subsequent lookup - * @param {object} value a JSONifiable object to store - * @returns {Promise.} an indicator of the success or - * failure of this request - */ - createObject: function (space, key, value) { - addToCache(space, key, value); - return persistenceService.createObject(space, key, value); - }, - /** - * Read an object from a specific space. This will read from a - * cache if the object is available. - * @memberof CachingPersistenceDecorator# - * @param {string} space the space in which to create the object - * @param {string} key the key which identifies the object - * @returns {Promise.} a promise for the object; may - * resolve to undefined (if the object does not exist - * in this space) - */ - readObject: function (space, key) { - return (cache[space] && cache[space][key]) ? - fastPromise(cache[space][key].value) : - persistenceService.readObject(space, key) - .then(putCache(space, key)); - }, - /** - * Update an object in a specific space. This will - * be cached to expedite subsequent retrieval. - * @memberof CachingPersistenceDecorator# - * @param {string} space the space in which to create the object - * @param {string} key the key associate with the object for - * subsequent lookup - * @param {object} value a JSONifiable object to store - * @returns {Promise.} an indicator of the success or - * failure of this request - */ - updateObject: function (space, key, value) { - return persistenceService.updateObject(space, key, value) - .then(function (result) { - addToCache(space, key, value); - return result; - }); - }, - /** - * Delete an object in a specific space. This will - * additionally be cleared from the cache. - * @memberof CachingPersistenceDecorator# - * @param {string} space the space in which to create the object - * @param {string} key the key associate with the object for - * subsequent lookup - * @param {object} value a JSONifiable object to delete - * @returns {Promise.} an indicator of the success or - * failure of this request - */ - deleteObject: function (space, key, value) { - if (cache[space]) { - delete cache[space][key]; - } - return persistenceService.deleteObject(space, key, value); + then: function (callback) { + return fastPromise(callback(value)); } }; - } + // Update the cached instance of an object to a new value + function replaceValue(valueHolder, newValue) { + var v = valueHolder.value; + + // If it's a JS object, we want to replace contents, so that + // everybody gets the same instance. + if (typeof v === 'object' && v !== null) { + // Only update contents if these are different instances + if (v !== newValue) { + // Clear prior contents + Object.keys(v).forEach(function (k) { + delete v[k]; + }); + // Shallow-copy contents + Object.keys(newValue).forEach(function (k) { + v[k] = newValue[k]; + }); + } + } else { + // Otherwise, just store the new value + valueHolder.value = newValue; + } + } + + // Place value in the cache for space, if there is one. + CachingPersistenceDecorator.prototype.addToCache = function (space, key, value) { + var cache = this.cache; + if (cache[space]) { + if (cache[space][key]) { + replaceValue(cache[space][key], value); + } else { + cache[space][key] = { value: value }; + } + } + }; + + // Create a function for putting value into a cache; + // useful for then-chaining. + CachingPersistenceDecorator.prototype.putCache = function (space, key) { + var self = this; + return function (value) { + self.addToCache(space, key, value); + return value; + }; + }; + + + + CachingPersistenceDecorator.prototype.listSpaces = function () { + return this.persistenceService.listSpaces(); + }; + + CachingPersistenceDecorator.prototype.listObjects = function (space) { + return this.persistenceService.listObjects(space); + }; + + CachingPersistenceDecorator.prototype.createObject = function (space, key, value) { + this.addToCache(space, key, value); + return this.persistenceService.createObject(space, key, value); + }; + + CachingPersistenceDecorator.prototype.readObject = function (space, key) { + var cache = this.cache; + return (cache[space] && cache[space][key]) ? + fastPromise(cache[space][key].value) : + this.persistenceService.readObject(space, key) + .then(this.putCache(space, key)); + }; + + CachingPersistenceDecorator.prototype.updateObject = function (space, key, value) { + var self = this; + return this.persistenceService.updateObject(space, key, value) + .then(function (result) { + self.addToCache(space, key, value); + return result; + }); + }; + + CachingPersistenceDecorator.prototype.deleteObject = function (space, key, value) { + if (this.cache[space]) { + delete this.cache[space][key]; + } + return this.persistenceService.deleteObject(space, key, value); + }; + return CachingPersistenceDecorator; } -); \ No newline at end of file +); diff --git a/platform/persistence/couch/src/CouchDocument.js b/platform/persistence/couch/src/CouchDocument.js index d115f56839..d61641f041 100644 --- a/platform/persistence/couch/src/CouchDocument.js +++ b/platform/persistence/couch/src/CouchDocument.js @@ -33,6 +33,7 @@ define( * metadata field which contains a subset of information found * in the model itself (to support search optimization with * CouchDB views.) + * @memberof platform/persistence/couch * @constructor * @param {string} id the id under which to store this mode * @param {object} model the model to store @@ -59,4 +60,4 @@ define( return CouchDocument; } -); \ No newline at end of file +); diff --git a/platform/persistence/couch/src/CouchIndicator.js b/platform/persistence/couch/src/CouchIndicator.js index f49f6ce2e5..684f2e58c5 100644 --- a/platform/persistence/couch/src/CouchIndicator.js +++ b/platform/persistence/couch/src/CouchIndicator.js @@ -55,70 +55,64 @@ define( * Indicator for the current CouchDB connection. Polls CouchDB * at a regular interval (defined by bundle constants) to ensure * that the database is available. + * @constructor + * @memberof platform/persistence/couch + * @implements {Indicator} + * @param $http Angular's $http service + * @param $interval Angular's $interval service + * @param {string} path the URL to poll to check for couch availability + * @param {number} interval the interval, in milliseconds, to poll at */ - function CouchIndicator($http, $interval, PATH, INTERVAL) { + function CouchIndicator($http, $interval, path, interval) { + var self = this; + // Track the current connection state - var state = PENDING; + this.state = PENDING; + + this.$http = $http; + this.$interval = $interval; + this.path = path; + this.interval = interval; + // Callback if the HTTP request to Couch fails function handleError(err) { - state = DISCONNECTED; + self.state = DISCONNECTED; } // Callback if the HTTP request succeeds. CouchDB may // report an error, so check for that. function handleResponse(response) { var data = response.data; - state = data.error ? SEMICONNECTED : CONNECTED; + self.state = data.error ? SEMICONNECTED : CONNECTED; } // Try to connect to CouchDB, and update the indicator. function updateIndicator() { - $http.get(PATH).then(handleResponse, handleError); + $http.get(path).then(handleResponse, handleError); } // Update the indicator initially, and start polling. updateIndicator(); - $interval(updateIndicator, INTERVAL); - - return { - /** - * Get the glyph (single character used as an icon) - * to display in this indicator. This will return "D", - * which should appear as a database icon. - * @returns {string} the character of the database icon - */ - getGlyph: function () { - return "D"; - }, - /** - * Get the name of the CSS class to apply to the glyph. - * This is used to color the glyph to match its - * state (one of ok, caution or err) - * @returns {string} the CSS class to apply to this glyph - */ - getGlyphClass: function () { - return state.glyphClass; - }, - /** - * Get the text that should appear in the indicator. - * @returns {string} brief summary of connection status - */ - getText: function () { - return state.text; - }, - /** - * Get a longer-form description of the current connection - * space, suitable for display in a tooltip - * @returns {string} longer summary of connection status - */ - getDescription: function () { - return state.description; - } - }; - + $interval(updateIndicator, interval); } + CouchIndicator.prototype.getGlyph = function () { + return "D"; + }; + + CouchIndicator.prototype.getGlyphClass = function () { + return this.state.glyphClass; + }; + + CouchIndicator.prototype.getText = function () { + return this.state.text; + }; + + CouchIndicator.prototype.getDescription = function () { + return this.state.description; + }; + return CouchIndicator; } -); \ No newline at end of file +); diff --git a/platform/persistence/couch/src/CouchPersistenceProvider.js b/platform/persistence/couch/src/CouchPersistenceProvider.js index e9dd13e39b..c50cc86386 100644 --- a/platform/persistence/couch/src/CouchPersistenceProvider.js +++ b/platform/persistence/couch/src/CouchPersistenceProvider.js @@ -21,6 +21,11 @@ *****************************************************************************/ /*global define*/ +/** + * This bundle implements a persistence service which uses CouchDB to + * store documents. + * @namespace platform/persistence/cache + */ define( ["./CouchDocument"], function (CouchDocument) { @@ -35,149 +40,110 @@ define( * The CouchPersistenceProvider reads and writes JSON documents * (more specifically, domain object models) to/from a CouchDB * instance. + * @memberof platform/persistence/couch * @constructor + * @implements {PersistenceService} + * @param $http Angular's $http service + * @param $interval Angular's $interval service + * @param {string} space the name of the persistence space being served + * @param {string} path the path to the CouchDB instance */ - function CouchPersistenceProvider($http, $q, SPACE, PATH) { - var spaces = [ SPACE ], - revs = {}; - - // Convert a subpath to a full path, suitable to pass - // to $http. - function url(subpath) { - return PATH + '/' + subpath; - } - - // Issue a request using $http; get back the plain JS object - // from the expected JSON response - function request(subpath, method, value) { - return $http({ - method: method, - url: url(subpath), - data: value - }).then(function (response) { - return response.data; - }, function () { - return undefined; - }); - } - - // Shorthand methods for GET/PUT methods - function get(subpath) { - return request(subpath, "GET"); - } - function put(subpath, value) { - return request(subpath, "PUT", value); - } - - // Pull out a list of document IDs from CouchDB's - // _all_docs response - function getIdsFromAllDocs(allDocs) { - return allDocs.rows.map(function (r) { return r.id; }); - } - - // Get a domain object model out of CouchDB's response - function getModel(response) { - if (response && response.model) { - revs[response[ID]] = response[REV]; - return response.model; - } else { - return undefined; - } - } - - // Check the response to a create/update/delete request; - // track the rev if it's valid, otherwise return false to - // indicate that the request failed. - function checkResponse(response) { - if (response && response.ok) { - revs[response.id] = response.rev; - return response.ok; - } else { - return false; - } - } - - return { - /** - * List all persistence spaces which this provider - * recognizes. - * - * @returns {Promise.} a promise for a list of - * spaces supported by this provider - */ - listSpaces: function () { - return $q.when(spaces); - }, - /** - * List all objects (by their identifiers) that are stored - * in the given persistence space, per this provider. - * @param {string} space the space to check - * @returns {Promise.} a promise for the list of - * identifiers - */ - listObjects: function (space) { - return get("_all_docs").then(getIdsFromAllDocs); - }, - /** - * Create a new object in the specified persistence space. - * @param {string} space the space in which to store the object - * @param {string} key the identifier for the persisted object - * @param {object} value a JSONifiable object that should be - * stored and associated with the provided identifier - * @returns {Promise.} a promise for an indication - * of the success (true) or failure (false) of this - * operation - */ - createObject: function (space, key, value) { - return put(key, new CouchDocument(key, value)) - .then(checkResponse); - }, - - /** - * Read an existing object back from persistence. - * @param {string} space the space in which to look for - * the object - * @param {string} key the identifier for the persisted object - * @returns {Promise.} a promise for the stored - * object; this will resolve to undefined if no such - * object is found. - */ - readObject: function (space, key) { - return get(key).then(getModel); - }, - /** - * Update an existing object in the specified persistence space. - * @param {string} space the space in which to store the object - * @param {string} key the identifier for the persisted object - * @param {object} value a JSONifiable object that should be - * stored and associated with the provided identifier - * @returns {Promise.} a promise for an indication - * of the success (true) or failure (false) of this - * operation - */ - updateObject: function (space, key, value) { - return put(key, new CouchDocument(key, value, revs[key])) - .then(checkResponse); - }, - /** - * Delete an object in the specified persistence space. - * @param {string} space the space from which to delete this - * object - * @param {string} key the identifier of the persisted object - * @param {object} value a JSONifiable object that should be - * deleted - * @returns {Promise.} a promise for an indication - * of the success (true) or failure (false) of this - * operation - */ - deleteObject: function (space, key, value) { - return put(key, new CouchDocument(key, value, revs[key], true)) - .then(checkResponse); - } - }; - + function CouchPersistenceProvider($http, $q, space, path) { + this.spaces = [ space ]; + this.revs = {}; + this.$q = $q; + this.$http = $http; + this.path = path; } + function bind(fn, thisArg) { + return function () { + return fn.apply(thisArg, arguments); + }; + } + + // Pull out a list of document IDs from CouchDB's + // _all_docs response + function getIdsFromAllDocs(allDocs) { + return allDocs.rows.map(function (r) { return r.id; }); + } + + // Check the response to a create/update/delete request; + // track the rev if it's valid, otherwise return false to + // indicate that the request failed. + function checkResponse(response) { + if (response && response.ok) { + this.revs[response.id] = response.rev; + return response.ok; + } else { + return false; + } + } + + // Get a domain object model out of CouchDB's response + function getModel(response) { + if (response && response.model) { + this.revs[response[ID]] = response[REV]; + return response.model; + } else { + return undefined; + } + } + + // Issue a request using $http; get back the plain JS object + // from the expected JSON response + CouchPersistenceProvider.prototype.request = function (subpath, method, value) { + return this.$http({ + method: method, + url: this.path + '/' + subpath, + data: value + }).then(function (response) { + return response.data; + }, function () { + return undefined; + }); + }; + + // Shorthand methods for GET/PUT methods + CouchPersistenceProvider.prototype.get = function (subpath) { + return this.request(subpath, "GET"); + }; + + CouchPersistenceProvider.prototype.put = function (subpath, value) { + return this.request(subpath, "PUT", value); + }; + + + CouchPersistenceProvider.prototype.listSpaces = function () { + return this.$q.when(this.spaces); + }; + + CouchPersistenceProvider.prototype.listObjects = function (space) { + return this.get("_all_docs").then(bind(getIdsFromAllDocs, this)); + }; + + CouchPersistenceProvider.prototype.createObject = function (space, key, value) { + return this.put(key, new CouchDocument(key, value)) + .then(bind(checkResponse, this)); + }; + + + CouchPersistenceProvider.prototype.readObject = function (space, key) { + return this.get(key).then(bind(getModel, this)); + }; + + CouchPersistenceProvider.prototype.updateObject = function (space, key, value) { + var rev = this.revs[key]; + return this.put(key, new CouchDocument(key, value, rev)) + .then(bind(checkResponse, this)); + }; + + CouchPersistenceProvider.prototype.deleteObject = function (space, key, value) { + var rev = this.revs[key]; + return this.put(key, new CouchDocument(key, value, rev, true)) + .then(bind(checkResponse, this)); + }; + return CouchPersistenceProvider; } -); \ No newline at end of file +); diff --git a/platform/persistence/elastic/bundle.json b/platform/persistence/elastic/bundle.json index e7dfa1ab9d..8b9ba16fd4 100644 --- a/platform/persistence/elastic/bundle.json +++ b/platform/persistence/elastic/bundle.json @@ -1,6 +1,6 @@ { - "name": "Couch Persistence", - "description": "Adapter to read and write objects using a CouchDB instance.", + "name": "ElasticSearch Persistence", + "description": "Adapter to read and write objects using an ElasticSearch instance.", "extensions": { "components": [ { diff --git a/platform/persistence/elastic/src/ElasticIndicator.js b/platform/persistence/elastic/src/ElasticIndicator.js index 82714080a0..78a29605c1 100644 --- a/platform/persistence/elastic/src/ElasticIndicator.js +++ b/platform/persistence/elastic/src/ElasticIndicator.js @@ -46,71 +46,56 @@ define( }; /** - * Indicator for the current CouchDB connection. Polls CouchDB - * at a regular interval (defined by bundle constants) to ensure - * that the database is available. + * Indicator for the current ElasticSearch connection. Polls + * ElasticSearch at a regular interval (defined by bundle constants) + * to ensure that the database is available. + * @constructor + * @memberof platform/persistence/elastic + * @implements {Indicator} + * @param $http Angular's $http service + * @param $interval Angular's $interval service + * @param {string} path the URL to poll for elasticsearch availability + * @param {number} interval the interval, in milliseconds, to poll at */ - function ElasticIndicator($http, $interval, PATH, INTERVAL) { + function ElasticIndicator($http, $interval, path, interval) { // Track the current connection state - var state = PENDING; + var self = this; - // Callback if the HTTP request to Couch fails - function handleError(err) { - state = DISCONNECTED; + this.state = PENDING; + + // Callback if the HTTP request to ElasticSearch fails + function handleError() { + self.state = DISCONNECTED; } // Callback if the HTTP request succeeds. - function handleResponse(response) { - state = CONNECTED; + function handleResponse() { + self.state = CONNECTED; } - // Try to connect to CouchDB, and update the indicator. + // Try to connect to ElasticSearch, and update the indicator. function updateIndicator() { - $http.get(PATH).then(handleResponse, handleError); + $http.get(path).then(handleResponse, handleError); } // Update the indicator initially, and start polling. updateIndicator(); - $interval(updateIndicator, INTERVAL, false); - - return { - /** - * Get the glyph (single character used as an icon) - * to display in this indicator. This will return "D", - * which should appear as a database icon. - * @returns {string} the character of the database icon - */ - getGlyph: function () { - return "D"; - }, - /** - * Get the name of the CSS class to apply to the glyph. - * This is used to color the glyph to match its - * state (one of ok, caution or err) - * @returns {string} the CSS class to apply to this glyph - */ - getGlyphClass: function () { - return state.glyphClass; - }, - /** - * Get the text that should appear in the indicator. - * @returns {string} brief summary of connection status - */ - getText: function () { - return state.text; - }, - /** - * Get a longer-form description of the current connection - * space, suitable for display in a tooltip - * @returns {string} longer summary of connection status - */ - getDescription: function () { - return state.description; - } - }; - + $interval(updateIndicator, interval, false); } + ElasticIndicator.prototype.getGlyph = function () { + return "D"; + }; + ElasticIndicator.prototype.getGlyphClass = function () { + return this.state.glyphClass; + }; + ElasticIndicator.prototype.getText = function () { + return this.state.text; + }; + ElasticIndicator.prototype.getDescription = function () { + return this.state.description; + }; + return ElasticIndicator; } -); \ No newline at end of file +); diff --git a/platform/persistence/elastic/src/ElasticPersistenceProvider.js b/platform/persistence/elastic/src/ElasticPersistenceProvider.js index a9c35af210..c7d21ae81d 100644 --- a/platform/persistence/elastic/src/ElasticPersistenceProvider.js +++ b/platform/persistence/elastic/src/ElasticPersistenceProvider.js @@ -21,6 +21,11 @@ *****************************************************************************/ /*global define*/ +/** + * This bundle implements a persistence service which uses ElasticSearch to + * store documents. + * @namespace platform/persistence/elastic + */ define( [], function () { @@ -37,164 +42,126 @@ define( * The ElasticPersistenceProvider reads and writes JSON documents * (more specifically, domain object models) to/from an ElasticSearch * instance. + * @memberof platform/persistence/elastic * @constructor + * @implements {PersistenceService} + * @param $http Angular's $http service + * @param $interval Angular's $interval service + * @param {string} space the name of the persistence space being served + * @param {string} root the root of the path to ElasticSearch + * @param {stirng} path the path to domain objects within ElasticSearch */ - function ElasticPersistenceProvider($http, $q, SPACE, ROOT, PATH) { - var spaces = [ SPACE ], - revs = {}; + function ElasticPersistenceProvider($http, $q, space, root, path) { + this.spaces = [ space ]; + this.revs = {}; + this.$http = $http; + this.$q = $q; + this.root = root; + this.path = path; + } - // Convert a subpath to a full path, suitable to pass - // to $http. - function url(subpath) { - return ROOT + '/' + PATH + '/' + subpath; - } + function bind(fn, thisArg) { + return function () { + return fn.apply(thisArg, arguments); + }; + } - // Issue a request using $http; get back the plain JS object - // from the expected JSON response - function request(subpath, method, value, params) { - return $http({ - method: method, - url: url(subpath), - params: params, - data: value - }).then(function (response) { - return response.data; - }, function (response) { - return (response || {}).data; + // Issue a request using $http; get back the plain JS object + // from the expected JSON response + ElasticPersistenceProvider.prototype.request = function (subpath, method, value, params) { + return this.$http({ + method: method, + url: this.root + '/' + this.path + '/' + subpath, + params: params, + data: value + }).then(function (response) { + return response.data; + }, function (response) { + return (response || {}).data; + }); + }; + + // Shorthand methods for GET/PUT methods + ElasticPersistenceProvider.prototype.get = function (subpath) { + return this.request(subpath, "GET"); + }; + ElasticPersistenceProvider.prototype.put = function (subpath, value, params) { + return this.request(subpath, "PUT", value, params); + }; + ElasticPersistenceProvider.prototype.del = function (subpath) { + return this.request(subpath, "DELETE"); + }; + + + // Handle an update error + ElasticPersistenceProvider.prototype.handleError = function (response, key) { + var error = new Error("Persistence error."), + $q = this.$q; + if ((response || {}).status === CONFLICT) { + error.key = "revision"; + // Load the updated model, then reject the promise + return this.get(key).then(function (response) { + error.model = response[SRC]; + return $q.reject(error); }); } + // Reject the promise + return this.$q.reject(error); + }; - // Shorthand methods for GET/PUT methods - function get(subpath) { - return request(subpath, "GET"); + // Get a domain object model out of ElasticSearch's response + function getModel(response) { + if (response && response[SRC]) { + this.revs[response[ID]] = response[REV]; + return response[SRC]; + } else { + return undefined; } - function put(subpath, value, params) { - return request(subpath, "PUT", value, params); - } - function del(subpath) { - return request(subpath, "DELETE"); - } - - // Get a domain object model out of CouchDB's response - function getModel(response) { - if (response && response[SRC]) { - revs[response[ID]] = response[REV]; - return response[SRC]; - } else { - return undefined; - } - } - - // Handle an update error - function handleError(response, key) { - var error = new Error("Persistence error."); - if ((response || {}).status === CONFLICT) { - error.key = "revision"; - // Load the updated model, then reject the promise - return get(key).then(function (response) { - error.model = response[SRC]; - return $q.reject(error); - }); - } - // Reject the promise - return $q.reject(error); - } - - // Check the response to a create/update/delete request; - // track the rev if it's valid, otherwise return false to - // indicate that the request failed. - function checkResponse(response, key) { - var error; - if (response && !response.error) { - revs[key] = response[REV]; - return response; - } else { - return handleError(response, key); - } - } - - return { - /** - * List all persistence spaces which this provider - * recognizes. - * - * @returns {Promise.} a promise for a list of - * spaces supported by this provider - */ - listSpaces: function () { - return $q.when(spaces); - }, - /** - * List all objects (by their identifiers) that are stored - * in the given persistence space, per this provider. - * @param {string} space the space to check - * @returns {Promise.} a promise for the list of - * identifiers - */ - listObjects: function (space) { - return $q.when([]); - }, - /** - * Create a new object in the specified persistence space. - * @param {string} space the space in which to store the object - * @param {string} key the identifier for the persisted object - * @param {object} value a JSONifiable object that should be - * stored and associated with the provided identifier - * @returns {Promise.} a promise for an indication - * of the success (true) or failure (false) of this - * operation - */ - createObject: function (space, key, value) { - return put(key, value).then(checkResponse); - }, - - /** - * Read an existing object back from persistence. - * @param {string} space the space in which to look for - * the object - * @param {string} key the identifier for the persisted object - * @returns {Promise.} a promise for the stored - * object; this will resolve to undefined if no such - * object is found. - */ - readObject: function (space, key) { - return get(key).then(getModel); - }, - /** - * Update an existing object in the specified persistence space. - * @param {string} space the space in which to store the object - * @param {string} key the identifier for the persisted object - * @param {object} value a JSONifiable object that should be - * stored and associated with the provided identifier - * @returns {Promise.} a promise for an indication - * of the success (true) or failure (false) of this - * operation - */ - updateObject: function (space, key, value) { - function checkUpdate(response) { - return checkResponse(response, key); - } - return put(key, value, { version: revs[key] }) - .then(checkUpdate); - }, - /** - * Delete an object in the specified persistence space. - * @param {string} space the space from which to delete this - * object - * @param {string} key the identifier of the persisted object - * @param {object} value a JSONifiable object that should be - * deleted - * @returns {Promise.} a promise for an indication - * of the success (true) or failure (false) of this - * operation - */ - deleteObject: function (space, key, value) { - return del(key).then(checkResponse); - } - }; - } + // Check the response to a create/update/delete request; + // track the rev if it's valid, otherwise return false to + // indicate that the request failed. + ElasticPersistenceProvider.prototype.checkResponse = function (response, key) { + if (response && !response.error) { + this.revs[key] = response[REV]; + return response; + } else { + return this.handleError(response, key); + } + }; + + // Public API + ElasticPersistenceProvider.prototype.listSpaces = function () { + return this.$q.when(this.spaces); + }; + + ElasticPersistenceProvider.prototype.listObjects = function () { + // Not yet implemented + return this.$q.when([]); + }; + + + ElasticPersistenceProvider.prototype.createObject = function (space, key, value) { + return this.put(key, value).then(bind(this.checkResponse, this)); + }; + + ElasticPersistenceProvider.prototype.readObject = function (space, key) { + return this.get(key).then(bind(getModel, this)); + }; + + ElasticPersistenceProvider.prototype.updateObject = function (space, key, value) { + function checkUpdate(response) { + return this.checkResponse(response, key); + } + return this.put(key, value, { version: this.revs[key] }) + .then(bind(checkUpdate, this)); + }; + + ElasticPersistenceProvider.prototype.deleteObject = function (space, key, value) { + return this.del(key).then(bind(this.checkResponse, this)); + }; + return ElasticPersistenceProvider; } -); \ No newline at end of file +); diff --git a/platform/persistence/elastic/src/ElasticsearchSearchProvider.js b/platform/persistence/elastic/src/ElasticsearchSearchProvider.js index af13628af9..d7dea9b1f0 100644 --- a/platform/persistence/elastic/src/ElasticsearchSearchProvider.js +++ b/platform/persistence/elastic/src/ElasticsearchSearchProvider.js @@ -44,17 +44,52 @@ define( * @param $http Angular's $http service, for working with urls. * @param {ObjectService} objectService the service from which * domain objects can be gotten. - * @param ROOT the constant ELASTIC_ROOT which allows us to + * @param root the constant `ELASTIC_ROOT` which allows us to * interact with ElasticSearch. */ - function ElasticsearchSearchProvider($http, objectService, ROOT) { - - // Add the fuzziness operator to the search term + function ElasticsearchSearchProvider($http, objectService, root) { + this.$http = $http; + this.objectService = objectService; + this.root = root; + } + + /** + * Searches through the filetree for domain objects using a search + * term. This is done through querying elasticsearch. Returns a + * promise for a result object that has the format + * {hits: searchResult[], total: number, timedOut: boolean} + * where a searchResult has the format + * {id: string, object: domainObject, score: number} + * + * Notes: + * * The order of the results is from highest to lowest score, + * as elsaticsearch determines them to be. + * * Uses the fuzziness operator to get more results. + * * More about this search's behavior at + * https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html + * + * @param searchTerm The text input that is the query. + * @param timestamp The time at which this function was called. + * This timestamp is used as a unique identifier for this + * query and the corresponding results. + * @param maxResults (optional) The maximum number of results + * that this function should return. + * @param timeout (optional) The time after which the search should + * stop calculations and return partial results. Elasticsearch + * does not guarentee that this timeout will be strictly followed. + */ + ElasticsearchSearchProvider.prototype.query = function query(searchTerm, timestamp, maxResults, timeout) { + var $http = this.$http, + objectService = this.objectService, + root = this.root, + esQuery; + + // Add the fuzziness operator to the search term function addFuzziness(searchTerm, editDistance) { if (!editDistance) { editDistance = ''; } - + return searchTerm.split(' ').map(function (s) { // Don't add fuzziness for quoted strings if (s.indexOf('"') !== -1) { @@ -64,11 +99,11 @@ define( } }).join(' '); } - + // Currently specific to elasticsearch function processSearchTerm(searchTerm) { var spaceIndex; - + // Cut out any extra spaces while (searchTerm.substr(0, 1) === ' ') { searchTerm = searchTerm.substring(1, searchTerm.length); @@ -79,18 +114,18 @@ define( spaceIndex = searchTerm.indexOf(' '); while (spaceIndex !== -1) { searchTerm = searchTerm.substring(0, spaceIndex) + - searchTerm.substring(spaceIndex + 1, searchTerm.length); + searchTerm.substring(spaceIndex + 1, searchTerm.length); spaceIndex = searchTerm.indexOf(' '); } - + // Add fuzziness for completeness searchTerm = addFuzziness(searchTerm); - + return searchTerm; } - - // Processes results from the format that elasticsearch returns to - // a list of searchResult objects, then returns a result object + + // Processes results from the format that elasticsearch returns to + // a list of searchResult objects, then returns a result object // (See documentation for query for object descriptions) function processResults(rawResults, timestamp) { var results = rawResults.data.hits.hits, @@ -99,25 +134,25 @@ define( scores = {}, searchResults = [], i; - + // Get the result objects' IDs for (i = 0; i < resultsLength; i += 1) { ids.push(results[i][ID]); } - + // Get the result objects' scores for (i = 0; i < resultsLength; i += 1) { scores[ids[i]] = results[i][SCORE]; } - + // Get the domain objects from their IDs return objectService.getObjects(ids).then(function (objects) { var j, id; - + for (j = 0; j < resultsLength; j += 1) { id = ids[j]; - + // Include items we can get models for if (objects[id].getModel) { // Format the results as searchResult objects @@ -128,7 +163,7 @@ define( }); } } - + return { hits: searchResults, total: rawResults.data.hits.total, @@ -136,76 +171,43 @@ define( }; }); } - - // For documentation, see query below. - function query(searchTerm, timestamp, maxResults, timeout) { - var esQuery; - - // Check to see if the user provided a maximum - // number of results to display - if (!maxResults) { - // Else, we provide a default value. - maxResults = DEFAULT_MAX_RESULTS; - } - - // If the user input is empty, we want to have no search results. - if (searchTerm !== '' && searchTerm !== undefined) { - // Process the search term - searchTerm = processSearchTerm(searchTerm); - // Create the query to elasticsearch - esQuery = ROOT + "/_search/?q=" + searchTerm + - "&size=" + maxResults; - if (timeout) { - esQuery += "&timeout=" + timeout; - } - // Get the data... - return $http({ - method: "GET", - url: esQuery - }).then(function (rawResults) { - // ...then process the data - return processResults(rawResults, timestamp); - }, function (err) { - // In case of error, return nothing. (To prevent - // infinite loading time.) - return {hits: [], total: 0}; - }); - } else { - return {hits: [], total: 0}; - } + // Check to see if the user provided a maximum + // number of results to display + if (!maxResults) { + // Else, we provide a default value. + maxResults = DEFAULT_MAX_RESULTS; } - - return { - /** - * Searches through the filetree for domain objects using a search - * term. This is done through querying elasticsearch. Returns a - * promise for a result object that has the format - * {hits: searchResult[], total: number, timedOut: boolean} - * where a searchResult has the format - * {id: string, object: domainObject, score: number} - * - * Notes: - * * The order of the results is from highest to lowest score, - * as elsaticsearch determines them to be. - * * Uses the fuzziness operator to get more results. - * * More about this search's behavior at - * https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html - * - * @param searchTerm The text input that is the query. - * @param timestamp The time at which this function was called. - * This timestamp is used as a unique identifier for this - * query and the corresponding results. - * @param maxResults (optional) The maximum number of results - * that this function should return. - * @param timeout (optional) The time after which the search should - * stop calculations and return partial results. Elasticsearch - * does not guarentee that this timeout will be strictly followed. - */ - query: query - }; - } + + // If the user input is empty, we want to have no search results. + if (searchTerm !== '' && searchTerm !== undefined) { + // Process the search term + searchTerm = processSearchTerm(searchTerm); + + // Create the query to elasticsearch + esQuery = root + "/_search/?q=" + searchTerm + + "&size=" + maxResults; + if (timeout) { + esQuery += "&timeout=" + timeout; + } + + // Get the data... + return this.$http({ + method: "GET", + url: esQuery + }).then(function (rawResults) { + // ...then process the data + return processResults(rawResults, timestamp); + }, function (err) { + // In case of error, return nothing. (To prevent + // infinite loading time.) + return {hits: [], total: 0}; + }); + } else { + return {hits: [], total: 0}; + } + }; return ElasticsearchSearchProvider; diff --git a/platform/persistence/queue/src/PersistenceFailureConstants.js b/platform/persistence/queue/src/PersistenceFailureConstants.js index ca8aa2db25..8eab3d9f4c 100644 --- a/platform/persistence/queue/src/PersistenceFailureConstants.js +++ b/platform/persistence/queue/src/PersistenceFailureConstants.js @@ -26,4 +26,4 @@ define({ OVERWRITE_KEY: "overwrite", TIMESTAMP_FORMAT: "YYYY-MM-DD HH:mm:ss\\Z", UNKNOWN_USER: "unknown user" -}); \ No newline at end of file +}); diff --git a/platform/persistence/queue/src/PersistenceFailureController.js b/platform/persistence/queue/src/PersistenceFailureController.js index 5d503a0f56..8586854857 100644 --- a/platform/persistence/queue/src/PersistenceFailureController.js +++ b/platform/persistence/queue/src/PersistenceFailureController.js @@ -29,25 +29,29 @@ define( /** * Controller to support the template to be shown in the * dialog shown for persistence failures. + * @constructor + * @memberof platform/persistence/queue */ function PersistenceFailureController() { - return { - /** - * Format a timestamp for display in the dialog. - */ - formatTimestamp: function (timestamp) { - return moment.utc(timestamp) - .format(Constants.TIMESTAMP_FORMAT); - }, - /** - * Format a user name for display in the dialog. - */ - formatUsername: function (username) { - return username || Constants.UNKNOWN_USER; - } - }; } + /** + * Format a timestamp for display in the dialog. + * @memberof platform/persistence/queue.PersistenceFailureController# + */ + PersistenceFailureController.prototype.formatTimestamp = function (timestamp) { + return moment.utc(timestamp) + .format(Constants.TIMESTAMP_FORMAT); + }; + + /** + * Format a user name for display in the dialog. + * @memberof platform/persistence/queue.PersistenceFailureController# + */ + PersistenceFailureController.prototype.formatUsername = function (username) { + return username || Constants.UNKNOWN_USER; + }; + return PersistenceFailureController; } -); \ No newline at end of file +); diff --git a/platform/persistence/queue/src/PersistenceFailureDialog.js b/platform/persistence/queue/src/PersistenceFailureDialog.js index 9f976e479c..7b048e7519 100644 --- a/platform/persistence/queue/src/PersistenceFailureDialog.js +++ b/platform/persistence/queue/src/PersistenceFailureDialog.js @@ -41,6 +41,8 @@ define( /** * Populates a `dialogModel` to pass to `dialogService.getUserChoise` * in order to choose between Overwrite and Cancel. + * @constructor + * @memberof platform/persistence/queue */ function PersistenceFailureDialog(failures) { var revisionErrors = [], @@ -72,4 +74,4 @@ define( return PersistenceFailureDialog; } -); \ No newline at end of file +); diff --git a/platform/persistence/queue/src/PersistenceFailureHandler.js b/platform/persistence/queue/src/PersistenceFailureHandler.js index 32e12efa66..8920ee5543 100644 --- a/platform/persistence/queue/src/PersistenceFailureHandler.js +++ b/platform/persistence/queue/src/PersistenceFailureHandler.js @@ -26,7 +26,32 @@ define( function (PersistenceFailureDialog, PersistenceFailureConstants) { "use strict"; + /** + * Handle failures to persist domain object models. + * @param $q Angular's `$q` + * @param {DialogService} dialogService the dialog service + * @constructor + * @memberof platform/persistence/queue + */ function PersistenceFailureHandler($q, dialogService) { + this.$q = $q; + this.dialogService = dialogService; + } + + /** + * Handle persistence failures by providing the user with a + * dialog summarizing these failures, and giving the option + * to overwrite/cancel as appropriate. + * @param {Array} failures persistence failures, as prepared + * by PersistenceQueueHandler + * @memberof platform/persistence/queue.PersistenceFailureHandler# + */ + PersistenceFailureHandler.prototype.handle = function handleFailures(failures) { + // Prepare dialog for display + var dialogModel = new PersistenceFailureDialog(failures), + revisionErrors = dialogModel.model.revised, + $q = this.$q; + // Refresh revision information for the domain object associated // with this persistence failure function refresh(failure) { @@ -93,39 +118,21 @@ define( return $q.all(failures.map(discard)); } - // Handle failures in persistence - function handleFailures(failures) { - // Prepare dialog for display - var dialogModel = new PersistenceFailureDialog(failures), - revisionErrors = dialogModel.model.revised; - - // Handle user input (did they choose to overwrite?) - function handleChoice(key) { - // If so, try again - if (key === PersistenceFailureConstants.OVERWRITE_KEY) { - return retry(revisionErrors); - } else { - return discardAll(revisionErrors); - } + // Handle user input (did they choose to overwrite?) + function handleChoice(key) { + // If so, try again + if (key === PersistenceFailureConstants.OVERWRITE_KEY) { + return retry(revisionErrors); + } else { + return discardAll(revisionErrors); } - - // Prompt for user input, the overwrite if they said so. - return dialogService.getUserChoice(dialogModel) - .then(handleChoice, handleChoice); } - return { - /** - * Handle persistence failures by providing the user with a - * dialog summarizing these failures, and giving the option - * to overwrite/cancel as appropriate. - * @param {Array} failures persistence failures, as prepared - * by PersistenceQueueHandler - */ - handle: handleFailures - }; - } + // Prompt for user input, the overwrite if they said so. + return this.dialogService.getUserChoice(dialogModel) + .then(handleChoice, handleChoice); + }; return PersistenceFailureHandler; } -); \ No newline at end of file +); diff --git a/platform/persistence/queue/src/PersistenceQueue.js b/platform/persistence/queue/src/PersistenceQueue.js index a54765f6c7..e704dafe71 100644 --- a/platform/persistence/queue/src/PersistenceQueue.js +++ b/platform/persistence/queue/src/PersistenceQueue.js @@ -50,6 +50,8 @@ define( * persistence when the queue is flushed * @param {number} [DELAY] optional; delay in milliseconds between * attempts to flush the queue + * @constructor + * @memberof platform/persistence/queue */ function PersistenceQueue( $q, @@ -74,4 +76,4 @@ define( return PersistenceQueue; } -); \ No newline at end of file +); diff --git a/platform/persistence/queue/src/PersistenceQueueHandler.js b/platform/persistence/queue/src/PersistenceQueueHandler.js index c57133beb8..4d630ce208 100644 --- a/platform/persistence/queue/src/PersistenceQueueHandler.js +++ b/platform/persistence/queue/src/PersistenceQueueHandler.js @@ -34,8 +34,29 @@ define( * @param $q Angular's $q, for promises * @param {PersistenceFailureHandler} handler to invoke in the event * that a persistence attempt fails. + * @constructor + * @memberof platform/persistence/queue */ function PersistenceQueueHandler($q, failureHandler) { + this.$q = $q; + this.failureHandler = failureHandler; + } + + /** + * Invoke the persist method on the provided persistence + * capabilities. + * @param {Object.} persistences + * capabilities to invoke, in id->capability pairs. + * @param {Object.} domainObjects + * associated domain objects, in id->object pairs. + * @param {PersistenceQueue} queue the persistence queue, + * to requeue as necessary + * @memberof platform/persistence/queue.PersistenceQueueHandler# + */ + PersistenceQueueHandler.prototype.persist = function (persistences, domainObjects, queue) { + var ids = Object.keys(persistences), + $q = this.$q, + failureHandler = this.failureHandler; // Handle a group of persistence invocations function persistGroup(ids, persistences, domainObjects, queue) { @@ -79,33 +100,17 @@ define( // Handle any failures from the full operation function handleFailure(value) { return failures.length > 0 ? - failureHandler.handle(failures) : - value; + failureHandler.handle(failures) : + value; } // Try to persist everything, then handle any failures return $q.all(ids.map(tryPersist)).then(handleFailure); } - - return { - /** - * Invoke the persist method on the provided persistence - * capabilities. - * @param {Object.} persistences - * capabilities to invoke, in id->capability pairs. - * @param {Object.} domainObjects - * associated domain objects, in id->object pairs. - * @param {PersistenceQueue} queue the persistence queue, - * to requeue as necessary - */ - persist: function (persistences, domainObjects, queue) { - var ids = Object.keys(persistences); - return persistGroup(ids, persistences, domainObjects, queue); - } - }; - } + return persistGroup(ids, persistences, domainObjects, queue); + }; return PersistenceQueueHandler; } -); \ No newline at end of file +); diff --git a/platform/persistence/queue/src/PersistenceQueueImpl.js b/platform/persistence/queue/src/PersistenceQueueImpl.js index b0e28becd5..fa68ca864c 100644 --- a/platform/persistence/queue/src/PersistenceQueueImpl.js +++ b/platform/persistence/queue/src/PersistenceQueueImpl.js @@ -41,19 +41,35 @@ define( * persistence when the queue is flushed * @param {number} [DELAY] optional; delay in milliseconds between * attempts to flush the queue + * @constructor + * @memberof platform/persistence/queue */ - function PersistenceQueueImpl($q, $timeout, handler, DELAY) { - var self, - persistences = {}, - objects = {}, - lastObservedSize = 0, - pendingTimeout, - flushPromise, - activeDefer = $q.defer(); + function PersistenceQueueImpl($q, $timeout, handler, delay) { + + this.persistences = {}; + this.objects = {}; + this.lastObservedSize = 0; + this.activeDefer = $q.defer(); + + // If no delay is provided, use a default + this.delay = delay || 0; + this.handler = handler; + this.$timeout = $timeout; + this.$q = $q; + } + + // Schedule a flushing of the queue (that is, plan to flush + // all objects in the queue) + PersistenceQueueImpl.prototype.scheduleFlush = function () { + var self = this, + $timeout = this.$timeout, + $q = this.$q, + handler = this.handler; // Check if the queue's size has stopped increasing) function quiescent() { - return Object.keys(persistences).length === lastObservedSize; + return Object.keys(self.persistences).length + === self.lastObservedSize; } // Persist all queued objects @@ -62,74 +78,72 @@ define( // this will be replaced with a promise for the next round // of persistence calls, so we want to make sure we clear // the correct one when this flush completes. - var flushingDefer = activeDefer; + var flushingDefer = self.activeDefer; // Clear the active promise for a queue flush function clearFlushPromise(value) { - flushPromise = undefined; + self.flushPromise = undefined; flushingDefer.resolve(value); return value; } // Persist all queued objects - flushPromise = handler.persist(persistences, objects, self) - .then(clearFlushPromise, clearFlushPromise); + self.flushPromise = handler.persist( + self.persistences, + self.objects, + self + ).then(clearFlushPromise, clearFlushPromise); // Reset queue, etc. - persistences = {}; - objects = {}; - lastObservedSize = 0; - pendingTimeout = undefined; - activeDefer = $q.defer(); + self.persistences = {}; + self.objects = {}; + self.lastObservedSize = 0; + self.pendingTimeout = undefined; + self.activeDefer = $q.defer(); } - // Schedule a flushing of the queue (that is, plan to flush - // all objects in the queue) - function scheduleFlush() { - function maybeFlush() { - // Timeout fired, so clear it - pendingTimeout = undefined; - // Only flush when we've stopped receiving updates - (quiescent() ? flush : scheduleFlush)(); - // Update lastObservedSize to detect quiescence - lastObservedSize = Object.keys(persistences).length; - } - - // If we are already flushing the queue... - if (flushPromise) { - // Wait until that's over before considering a flush - flushPromise.then(maybeFlush); + function maybeFlush() { + // Timeout fired, so clear it + self.pendingTimeout = undefined; + // Only flush when we've stopped receiving updates + if (quiescent()) { + flush(); } else { - // Otherwise, schedule a flush on a timeout (to give - // a window for other updates to get aggregated) - pendingTimeout = pendingTimeout || - $timeout(maybeFlush, DELAY, false); + self.scheduleFlush(); } - - return activeDefer.promise; + // Update lastObservedSize to detect quiescence + self.lastObservedSize = Object.keys(self.persistences).length; } - // If no delay is provided, use a default - DELAY = DELAY || 0; + // If we are already flushing the queue... + if (self.flushPromise) { + // Wait until that's over before considering a flush + self.flushPromise.then(maybeFlush); + } else { + // Otherwise, schedule a flush on a timeout (to give + // a window for other updates to get aggregated) + self.pendingTimeout = self.pendingTimeout || + $timeout(maybeFlush, self.delay, false); + } - self = { - /** - * Queue persistence of a domain object. - * @param {DomainObject} domainObject the domain object - * @param {PersistenceCapability} persistence the object's - * undecorated persistence capability - */ - put: function (domainObject, persistence) { - var id = domainObject.getId(); - persistences[id] = persistence; - objects[id] = domainObject; - return scheduleFlush(); - } - }; + return self.activeDefer.promise; + }; - return self; - } + + /** + * Queue persistence of a domain object. + * @param {DomainObject} domainObject the domain object + * @param {PersistenceCapability} persistence the object's + * undecorated persistence capability + * @returns {Promise} a promise which will resolve upon persistence + */ + PersistenceQueueImpl.prototype.put = function (domainObject, persistence) { + var id = domainObject.getId(); + this.persistences[id] = persistence; + this.objects[id] = domainObject; + return this.scheduleFlush(); + }; return PersistenceQueueImpl; } -); \ No newline at end of file +); diff --git a/platform/persistence/queue/src/QueuingPersistenceCapability.js b/platform/persistence/queue/src/QueuingPersistenceCapability.js index 7eb98ba3a9..8fe006504a 100644 --- a/platform/persistence/queue/src/QueuingPersistenceCapability.js +++ b/platform/persistence/queue/src/QueuingPersistenceCapability.js @@ -34,6 +34,8 @@ define( * capability * @param {DomainObject} domainObject the domain object which exposes * the capability + * @constructor + * @memberof platform/persistence/queue */ function QueuingPersistenceCapability(queue, persistence, domainObject) { var queuingPersistence = Object.create(persistence); @@ -48,4 +50,4 @@ define( return QueuingPersistenceCapability; } -); \ No newline at end of file +); diff --git a/platform/persistence/queue/src/QueuingPersistenceCapabilityDecorator.js b/platform/persistence/queue/src/QueuingPersistenceCapabilityDecorator.js index 78643f2908..a86fe60515 100644 --- a/platform/persistence/queue/src/QueuingPersistenceCapabilityDecorator.js +++ b/platform/persistence/queue/src/QueuingPersistenceCapabilityDecorator.js @@ -22,7 +22,10 @@ /*global define,Promise*/ /** - * Module defining CoreCapabilityProvider. Created by vwoeltje on 11/7/14. + * This bundle decorates the persistence service to handle persistence + * in batches, and to provide notification of persistence errors in batches + * as well. + * @namespace platform/persistence/queue */ define( ['./QueuingPersistenceCapability'], @@ -35,12 +38,23 @@ define( * will be handled in batches (allowing failure notification to * also be presented in batches.) * + * @memberof platform/persistence/queue * @constructor + * @implements {CapabilityService} + * @param {platform/persistence/queue.PersistenceQueue} persistenceQueue + * @param {CapabilityService} the decorated capability service */ function QueuingPersistenceCapabilityDecorator( persistenceQueue, capabilityService ) { + this.persistenceQueue = persistenceQueue; + this.capabilityService = capabilityService; + } + + QueuingPersistenceCapabilityDecorator.prototype.getCapabilities = function (model) { + var capabilityService = this.capabilityService, + persistenceQueue = this.persistenceQueue; function decoratePersistence(capabilities) { var originalPersistence = capabilities.persistence; @@ -49,8 +63,8 @@ define( // Get/instantiate the original var original = (typeof originalPersistence === 'function') ? - originalPersistence(domainObject) : - originalPersistence; + originalPersistence(domainObject) : + originalPersistence; // Provide a decorated version return new QueuingPersistenceCapability( @@ -63,35 +77,11 @@ define( return capabilities; } - function getCapabilities(model) { - return decoratePersistence( - capabilityService.getCapabilities(model) - ); - } - - return { - /** - * Get all capabilities associated with a given domain - * object. - * - * This returns a promise for an object containing key-value - * pairs, where keys are capability names and values are - * either: - * - * * Capability instances - * * Capability constructors (which take a domain object - * as their argument.) - * - * - * @param {*} model the object model - * @returns {Object.} all - * capabilities known to be valid for this model, as - * key-value pairs - */ - getCapabilities: getCapabilities - }; - } + return decoratePersistence( + capabilityService.getCapabilities(model) + ); + }; return QueuingPersistenceCapabilityDecorator; } -); \ No newline at end of file +); diff --git a/platform/policy/src/PolicyActionDecorator.js b/platform/policy/src/PolicyActionDecorator.js index da15828da9..96dbd9498e 100644 --- a/platform/policy/src/PolicyActionDecorator.js +++ b/platform/policy/src/PolicyActionDecorator.js @@ -31,28 +31,27 @@ define( * @param {PolicyService} policyService the service which provides * policy decisions * @param {ActionService} actionService the service to decorate + * @constructor + * @memberof platform/policy + * @implements {ActionService} */ function PolicyActionDecorator(policyService, actionService) { - return { - /** - * Get actions which are applicable in this context. - * These will be filtered to remove any actions which - * are deemed inapplicable by policy. - * @param context the context in which the action will occur - * @returns {Action[]} applicable actions - */ - getActions: function (context) { - // Check if an action is allowed by policy. - function allow(action) { - return policyService.allow('action', action, context); - } - - // Look up actions, filter out the disallowed ones. - return actionService.getActions(context).filter(allow); - } - }; + this.policyService = policyService; + this.actionService = actionService; } + PolicyActionDecorator.prototype.getActions = function (context) { + var policyService = this.policyService; + + // Check if an action is allowed by policy. + function allow(action) { + return policyService.allow('action', action, context); + } + + // Look up actions, filter out the disallowed ones. + return this.actionService.getActions(context).filter(allow); + }; + return PolicyActionDecorator; } -); \ No newline at end of file +); diff --git a/platform/policy/src/PolicyProvider.js b/platform/policy/src/PolicyProvider.js index 50397b7f1f..38858cadcc 100644 --- a/platform/policy/src/PolicyProvider.js +++ b/platform/policy/src/PolicyProvider.js @@ -21,16 +21,69 @@ *****************************************************************************/ /*global define*/ +/** + * This bundle implements the policy service. + * @namespace platform/policy + */ define( [], function () { "use strict"; + /** + * A policy is a participant in decision-making policies. Policies + * are divided into categories (identified symbolically by strings); + * within a given category, every given policy-driven decision will + * occur by consulting all available policies and requiring their + * collective consent (that is, every individual policy has the + * power to reject the decision entirely.) + * + * @interface Policy + * @template C, X + */ + + /** + * Check if this policy allows the described decision. The types + * of the arguments expected here vary depending on policy category. + * + * @method Policy#allow + * @template C, X + * @param {C} candidate the thing to allow or disallow + * @param {X} context the context in which the decision occurs + * @returns {boolean} false if disallowed; otherwise, true + */ + + + /** + * The `policyService` handles decisions about what things + * are and are not allowed in certain contexts. + * @interface PolicyService + */ + + /** + * Check whether or not a certain decision is allowed by + * policy. + * @param {string} category a machine-readable identifier + * for the kind of decision being made + * @param candidate the object about which the decision is + * being made + * @param context the context in which the decision occurs + * @param {Function} [callback] callback to invoke with a + * string message describing the reason a decision + * was disallowed (if its disallowed) + * @returns {boolean} true if the decision is allowed, + * otherwise false. + * @method PolicyService#allow + */ + /** * Provides an implementation of `policyService` which consults * various policy extensions to determine whether or not a specific * decision should be allowed. + * @memberof platform/policy * @constructor + * @implements {PolicyService} + * @param {Policy[]} policies the policies to enforce */ function PolicyProvider(policies) { var policyMap = {}; @@ -59,48 +112,33 @@ define( // Populate the map for subsequent lookup policies.forEach(addToMap); - - return { - /** - * Check whether or not a certain decision is allowed by - * policy. - * @param {string} category a machine-readable identifier - * for the kind of decision being made - * @param candidate the object about which the decision is - * being made - * @param context the context in which the decision occurs - * @param {Function} [callback] callback to invoke with a - * string message describing the reason a decision - * was disallowed (if its disallowed) - * @returns {boolean} true if the decision is allowed, - * otherwise false. - */ - allow: function (category, candidate, context, callback) { - var policyList = policyMap[category] || [], - i; - - // Iterate through policies. We do this instead of map or - // forEach so that we can return immediately if a policy - // chooses to disallow this decision. - for (i = 0; i < policyList.length; i += 1) { - // Consult the policy... - if (!policyList[i].allow(candidate, context)) { - // ...it disallowed, so pass its message to - // the callback (if any) - if (callback) { - callback(policyList[i].message); - } - // And return the failed result. - return false; - } - } - - // No policy disallowed this decision. - return true; - } - }; + this.policyMap = policyMap; } + PolicyProvider.prototype.allow = function (category, candidate, context, callback) { + var policyList = this.policyMap[category] || [], + i; + + // Iterate through policies. We do this instead of map or + // forEach so that we can return immediately if a policy + // chooses to disallow this decision. + for (i = 0; i < policyList.length; i += 1) { + // Consult the policy... + if (!policyList[i].allow(candidate, context)) { + // ...it disallowed, so pass its message to + // the callback (if any) + if (callback) { + callback(policyList[i].message); + } + // And return the failed result. + return false; + } + } + + // No policy disallowed this decision. + return true; + }; + return PolicyProvider; } -); \ No newline at end of file +); diff --git a/platform/policy/src/PolicyViewDecorator.js b/platform/policy/src/PolicyViewDecorator.js index 1fd6503649..c9ac54b173 100644 --- a/platform/policy/src/PolicyViewDecorator.js +++ b/platform/policy/src/PolicyViewDecorator.js @@ -31,28 +31,27 @@ define( * @param {PolicyService} policyService the service which provides * policy decisions * @param {ViewService} viewService the service to decorate + * @constructor + * @memberof platform/policy + * @implements {ViewService} */ - function PolicyActionDecorator(policyService, viewService) { - return { - /** - * Get views which are applicable to this domain object. - * These will be filtered to remove any views which - * are deemed inapplicable by policy. - * @param {DomainObject} the domain object to view - * @returns {View[]} applicable views - */ - getViews: function (domainObject) { - // Check if an action is allowed by policy. - function allow(view) { - return policyService.allow('view', view, domainObject); - } - - // Look up actions, filter out the disallowed ones. - return viewService.getViews(domainObject).filter(allow); - } - }; + function PolicyViewDecorator(policyService, viewService) { + this.policyService = policyService; + this.viewService = viewService; } - return PolicyActionDecorator; + PolicyViewDecorator.prototype.getViews = function (domainObject) { + var policyService = this.policyService; + + // Check if an action is allowed by policy. + function allow(view) { + return policyService.allow('view', view, domainObject); + } + + // Look up actions, filter out the disallowed ones. + return this.viewService.getViews(domainObject).filter(allow); + }; + + return PolicyViewDecorator; } -); \ No newline at end of file +); diff --git a/platform/representation/src/MCTInclude.js b/platform/representation/src/MCTInclude.js index 41a319e259..dc00c8b89d 100644 --- a/platform/representation/src/MCTInclude.js +++ b/platform/representation/src/MCTInclude.js @@ -49,6 +49,7 @@ define( * an output) and `parameters` is meant to be useful for * display parameterization (more like an input.) * + * @memberof platform/representation * @constructor * @param {TemplateDefinition[]} templates an array of * template extensions @@ -92,3 +93,4 @@ define( return MCTInclude; } ); + diff --git a/platform/representation/src/MCTRepresentation.js b/platform/representation/src/MCTRepresentation.js index 97194f12c3..49b2ae0f57 100644 --- a/platform/representation/src/MCTRepresentation.js +++ b/platform/representation/src/MCTRepresentation.js @@ -22,7 +22,9 @@ /*global define,Promise*/ /** - * Module defining MCTRepresentation. Created by vwoeltje on 11/7/14. + * This bundle implements the directives for representing domain objects + * as Angular-managed HTML. + * @namespace platform/representation */ define( [], @@ -47,6 +49,7 @@ define( * * `parameters`, used to communicate display parameters to * the included template (e.g. title.) * + * @memberof platform/representation * @constructor * @param {RepresentationDefinition[]} representations an array of * representation extensions @@ -235,6 +238,26 @@ define( }; } + /** + * A representer participates in the process of instantiating a + * representation of a domain object. + * + * @interface Representer + * @augments {Destroyable} + */ + /** + * Set the current representation in use, and the domain + * object being represented. + * + * @method Representer#represent + * @param {RepresentationDefinition} representation the + * definition of the representation in use + * @param {DomainObject} domainObject the domain object + * being represented + */ + + return MCTRepresentation; } ); + diff --git a/platform/representation/src/actions/ContextMenuAction.js b/platform/representation/src/actions/ContextMenuAction.js index 67b48536c3..60fdd9042f 100644 --- a/platform/representation/src/actions/ContextMenuAction.js +++ b/platform/representation/src/actions/ContextMenuAction.js @@ -39,6 +39,7 @@ define( /** * Launches a custom context menu for the domain object it contains. * + * @memberof platform/representation * @constructor * @param $compile Angular's $compile service * @param $document the current document @@ -46,71 +47,78 @@ define( * @param $rootScope Angular's root scope * @param actionContexr the context in which the action * should be performed + * @implements {Action} */ function ContextMenuAction($compile, $document, $window, $rootScope, actionContext) { + this.$compile = $compile; + this.actionContext = actionContext; + this.getDocument = function () { return $document; }; + this.getWindow = function () { return $window; }; + this.getRootScope = function () { return $rootScope; }; + } - function perform() { - var winDim = [$window.innerWidth, $window.innerHeight], - eventCoors = [actionContext.event.pageX, actionContext.event.pageY], - menuDim = GestureConstants.MCT_MENU_DIMENSIONS, - body = $document.find('body'), - scope = $rootScope.$new(), - goLeft = eventCoors[0] + menuDim[0] > winDim[0], - goUp = eventCoors[1] + menuDim[1] > winDim[1], - menu; + ContextMenuAction.prototype.perform = function () { + var $compile = this.$compile, + $document = this.getDocument(), + $window = this.getWindow(), + $rootScope = this.getRootScope(), + actionContext = this.actionContext, + winDim = [$window.innerWidth, $window.innerHeight], + eventCoors = [actionContext.event.pageX, actionContext.event.pageY], + menuDim = GestureConstants.MCT_MENU_DIMENSIONS, + body = $document.find('body'), + scope = $rootScope.$new(), + goLeft = eventCoors[0] + menuDim[0] > winDim[0], + goUp = eventCoors[1] + menuDim[1] > winDim[1], + menu; - // Remove the context menu - function dismiss() { - menu.remove(); - body.off("mousedown", dismiss); - dismissExistingMenu = undefined; - } - - // Dismiss any menu which was already showing - if (dismissExistingMenu) { - dismissExistingMenu(); - } - - // ...and record the presence of this menu. - dismissExistingMenu = dismiss; - - // Set up the scope, including menu positioning - scope.domainObject = actionContext.domainObject; - scope.menuStyle = {}; - scope.menuStyle[goLeft ? "right" : "left"] = - (goLeft ? (winDim[0] - eventCoors[0]) : eventCoors[0]) + 'px'; - scope.menuStyle[goUp ? "bottom" : "top"] = - (goUp ? (winDim[1] - eventCoors[1]) : eventCoors[1]) + 'px'; - scope.menuClass = { - "go-left": goLeft, - "go-up": goUp, - "context-menu-holder": true - }; - - // Create the context menu - menu = $compile(MENU_TEMPLATE)(scope); - - // Add the menu to the body - body.append(menu); - - // Stop propagation so that clicks on the menu do not close the menu - menu.on('mousedown', function (event) { - event.stopPropagation(); - }); - - // Dismiss the menu when body is clicked elsewhere - // ('mousedown' because 'click' breaks left-click context menus) - body.on('mousedown', dismiss); - menu.on('click', dismiss); - - // Don't launch browser's context menu - actionContext.event.preventDefault(); + // Remove the context menu + function dismiss() { + menu.remove(); + body.off("mousedown", dismiss); + dismissExistingMenu = undefined; } - return { - perform: perform + // Dismiss any menu which was already showing + if (dismissExistingMenu) { + dismissExistingMenu(); + } + + // ...and record the presence of this menu. + dismissExistingMenu = dismiss; + + // Set up the scope, including menu positioning + scope.domainObject = actionContext.domainObject; + scope.menuStyle = {}; + scope.menuStyle[goLeft ? "right" : "left"] = + (goLeft ? (winDim[0] - eventCoors[0]) : eventCoors[0]) + 'px'; + scope.menuStyle[goUp ? "bottom" : "top"] = + (goUp ? (winDim[1] - eventCoors[1]) : eventCoors[1]) + 'px'; + scope.menuClass = { + "go-left": goLeft, + "go-up": goUp, + "context-menu-holder": true }; - } + + // Create the context menu + menu = $compile(MENU_TEMPLATE)(scope); + + // Add the menu to the body + body.append(menu); + + // Stop propagation so that clicks on the menu do not close the menu + menu.on('mousedown', function (event) { + event.stopPropagation(); + }); + + // Dismiss the menu when body is clicked elsewhere + // ('mousedown' because 'click' breaks left-click context menus) + body.on('mousedown', dismiss); + menu.on('click', dismiss); + + // Don't launch browser's context menu + actionContext.event.preventDefault(); + }; return ContextMenuAction; } diff --git a/platform/representation/src/gestures/ContextMenuGesture.js b/platform/representation/src/gestures/ContextMenuGesture.js index cfab655442..5d6d7c325f 100644 --- a/platform/representation/src/gestures/ContextMenuGesture.js +++ b/platform/representation/src/gestures/ContextMenuGesture.js @@ -33,34 +33,34 @@ define( * Add listeners to a representation such that it calls the * context menu action for the domain object it contains. * + * @memberof platform/representation * @constructor * @param element the jqLite-wrapped element which should exhibit - * the context mennu + * the context menu * @param {DomainObject} domainObject the object on which actions * in the context menu will be performed + * @implements {Gesture} */ function ContextMenuGesture(element, domainObject) { - var actionContext, - stop; - + function showMenu(event) { + domainObject.getCapability('action').perform({ + key: 'menu', + domainObject: domainObject, + event: event + }); + } + // When context menu event occurs, show object actions instead - element.on('contextmenu', function (event) { - actionContext = {key: 'menu', domainObject: domainObject, event: event}; - stop = domainObject.getCapability('action').perform(actionContext); - }); - - return { - /** - * Detach any event handlers associated with this gesture. - * @method - * @memberof ContextMenuGesture - */ - destroy: function () { - element.off('contextmenu', stop); - } - }; + element.on('contextmenu', showMenu); + + this.showMenuCallback = showMenu; + this.element = element; } + ContextMenuGesture.prototype.destroy = function () { + this.element.off('contextmenu', this.showMenu); + }; + return ContextMenuGesture; } -); \ No newline at end of file +); diff --git a/platform/representation/src/gestures/DragGesture.js b/platform/representation/src/gestures/DragGesture.js index bb67732d0a..f240aa156e 100644 --- a/platform/representation/src/gestures/DragGesture.js +++ b/platform/representation/src/gestures/DragGesture.js @@ -33,7 +33,9 @@ define( * Add event handlers to a representation such that it may be * dragged as the source for drag-drop composition. * + * @memberof platform/representation * @constructor + * @implements {Gesture} * @param $log Angular's logging service * @param element the jqLite-wrapped element which should become * draggable @@ -103,21 +105,19 @@ define( element.on('dragstart', startDrag); element.on('dragend', endDrag); - return { - /** - * Detach any event handlers associated with this gesture. - * @memberof DragGesture - * @method - */ - destroy: function () { - // Detach listener - element.removeAttr('draggable'); - element.off('dragstart', startDrag); - } - }; + this.element = element; + this.startDragCallback = startDrag; + this.endDragCallback = endDrag; } + DragGesture.prototype.destroy = function () { + // Detach listener + this.element.removeAttr('draggable'); + this.element.off('dragstart', this.startDragCallback); + this.element.off('dragend', this.endDragCallback); + }; + return DragGesture; } -); \ No newline at end of file +); diff --git a/platform/representation/src/gestures/DropGesture.js b/platform/representation/src/gestures/DropGesture.js index 25bee716cf..bfcb85d3bb 100644 --- a/platform/representation/src/gestures/DropGesture.js +++ b/platform/representation/src/gestures/DropGesture.js @@ -32,14 +32,14 @@ define( /** * A DropGesture adds and maintains event handlers upon an element * such that it may act as a drop target for drag-drop composition. - + * + * @memberof platform/representation * @constructor * @param $q Angular's $q, for promise handling * @param element the jqLite-wrapped representation element * @param {DomainObject} domainObject the domain object whose * composition should be modified as a result of the drop. */ - function DropGesture(dndService, $q, element, domainObject) { var actionCapability = domainObject.getCapability('action'), action; // Action for the drop, when it occurs @@ -121,19 +121,17 @@ define( element.on('drop', drop); } - return { - /** - * Detach any event handlers associated with this gesture. - */ - destroy: function () { - element.off('dragover', dragOver); - element.off('drop', drop); - } - }; - + this.element = element; + this.dragOverCallback = dragOver; + this.dropCallback = drop; } + DropGesture.prototype.destroy = function () { + this.element.off('dragover', this.dragOverCallback); + this.element.off('drop', this.dropCallback); + }; + return DropGesture; } -); \ No newline at end of file +); diff --git a/platform/representation/src/gestures/GestureConstants.js b/platform/representation/src/gestures/GestureConstants.js index 2cdba87d0b..f43487424a 100644 --- a/platform/representation/src/gestures/GestureConstants.js +++ b/platform/representation/src/gestures/GestureConstants.js @@ -22,28 +22,33 @@ /*global define,Promise*/ /** - * Module defining GestureConstants. Created by vwoeltje on 11/17/14. + * Constants used by domain object gestures. + * @class platform/representation.GestureConstants */ define({ /** * The string identifier for the data type used for drag-and-drop * composition of domain objects. (e.g. in event.dataTransfer.setData * calls.) + * @memberof platform/representation.GestureConstants */ MCT_DRAG_TYPE: 'mct-domain-object-id', /** * The string identifier for the data type used for drag-and-drop * composition of domain objects, by object instance (passed through * the dndService) + * @memberof platform/representation.GestureConstants */ MCT_EXTENDED_DRAG_TYPE: 'mct-domain-object', /** * An estimate for the dimensions of a context menu, used for * positioning. + * @memberof platform/representation.GestureConstants */ MCT_MENU_DIMENSIONS: [ 170, 200 ], /** * Identifier for drop events. + * @memberof platform/representation.GestureConstants */ MCT_DROP_EVENT: 'mctDrop' -}); \ No newline at end of file +}); diff --git a/platform/representation/src/gestures/GestureProvider.js b/platform/representation/src/gestures/GestureProvider.js index 797a00aa10..30b463505d 100644 --- a/platform/representation/src/gestures/GestureProvider.js +++ b/platform/representation/src/gestures/GestureProvider.js @@ -29,6 +29,29 @@ define( function () { "use strict"; + /** + * Handles the attachment of gestures (responses to DOM events, + * generally) to DOM elements which represent domain objects. + * + * @interface GestureService + */ + /** + * Attach a set of gestures (indicated by key) to a + * DOM element which represents a specific domain object. + * @method GestureService#attachGestures + * @param element the jqLite-wrapped DOM element which the + * user will interact with + * @param {DomainObject} domainObject the domain object which + * is represented by that element + * @param {string[]} gestureKeys an array of keys identifying + * which gestures should apply; these will be matched + * against the keys defined in the gestures' extension + * definitions + * @return {Destroyable} an object with a `destroy` + * method which can (and should) be used when + * gestures should no longer be applied to an element. + */ + /** * The GestureProvider exposes defined gestures. Gestures are used * do describe and handle general-purpose interactions with the DOM @@ -40,6 +63,8 @@ define( * intermediary between these and the `mct-representation` directive * where they are used. * + * @memberof platform/representation + * @implements {GestureService} * @constructor * @param {Gesture[]} gestures an array of all gestures which are * available as extensions @@ -47,19 +72,28 @@ define( function GestureProvider(gestures) { var gestureMap = {}; - function releaseGesture(gesture) { - // Invoke the gesture's "destroy" method (if there is one) - // to release any held resources and detach event handlers. - if (gesture && gesture.destroy) { - gesture.destroy(); - } - } + // Assemble all gestures into a map, for easy look up + gestures.forEach(function (gesture) { + gestureMap[gesture.key] = gesture; + }); - function attachGestures(element, domainObject, gestureKeys) { - // Look up the desired gestures, filter for applicability, - // and instantiate them. Maintain a reference to allow them - // to be destroyed as a group later. - var attachedGestures = gestureKeys.map(function (key) { + this.gestureMap = gestureMap; + } + + function releaseGesture(gesture) { + // Invoke the gesture's "destroy" method (if there is one) + // to release any held resources and detach event handlers. + if (gesture && gesture.destroy) { + gesture.destroy(); + } + } + + GestureProvider.prototype.attachGestures = function attachGestures(element, domainObject, gestureKeys) { + // Look up the desired gestures, filter for applicability, + // and instantiate them. Maintain a reference to allow them + // to be destroyed as a group later. + var gestureMap = this.gestureMap, + attachedGestures = gestureKeys.map(function (key) { return gestureMap[key]; }).filter(function (Gesture) { return Gesture !== undefined && (Gesture.appliesTo ? @@ -69,42 +103,33 @@ define( return new Gesture(element, domainObject); }); - return { - destroy: function () { - // Just call all the individual "destroy" methods - attachedGestures.forEach(releaseGesture); - } - }; - } - - // Assemble all gestures into a map, for easy look up - gestures.forEach(function (gesture) { - gestureMap[gesture.key] = gesture; - }); - - return { - /** - * Attach a set of gestures (indicated by key) to a - * DOM element which represents a specific domain object. - * @method - * @memberof GestureProvider - * @param element the jqLite-wrapped DOM element which the - * user will interact with - * @param {DomainObject} domainObject the domain object which - * is represented by that element - * @param {string[]} gestureKeys an array of keys identifying - * which gestures should apply; these will be matched - * against the keys defined in the gestures' extension - * definitions - * @return {{ destroy: function }} an object with a `destroy` - * method which can (and should) be used when a - * gesture should no longer be applied to an element. - */ - attachGestures: attachGestures + destroy: function () { + // Just call all the individual "destroy" methods + attachedGestures.forEach(releaseGesture); + } }; - } + }; + + /** + * A destroyable object may have resources allocated which require + * explicit release. + * + * @interface Destroyable + */ + /** + * Release any resources associated with this object. + * + * @method Destroyable#destroy + */ + + /** + * A gesture describes manners in which certain representations of + * domain objects may respond to DOM events upon those representations. + * @interface Gesture + * @augments Destroyable + */ return GestureProvider; } -); \ No newline at end of file +); diff --git a/platform/representation/src/gestures/GestureRepresenter.js b/platform/representation/src/gestures/GestureRepresenter.js index 57084c102f..9353722ae8 100644 --- a/platform/representation/src/gestures/GestureRepresenter.js +++ b/platform/representation/src/gestures/GestureRepresenter.js @@ -37,47 +37,34 @@ define( * gestures * @param {Scope} scope the Angular scope for this representation * @param element the JQLite-wrapped mct-representation element + * @constructor + * @implements {Representer} + * @memberof platform/representation */ function GestureRepresenter(gestureService, scope, element) { - var gestureHandle; - - function destroy() { - // Release any resources associated with these gestures - if (gestureHandle) { - gestureHandle.destroy(); - } - } - - function represent(representation, domainObject) { - // Clear out any existing gestures - destroy(); - - // Attach gestures - by way of the service. - gestureHandle = gestureService.attachGestures( - element, - domainObject, - (representation || {}).gestures || [] - ); - } - - return { - /** - * Set the current representation in use, and the domain - * object being represented. - * - * @param {RepresentationDefinition} representation the - * definition of the representation in use - * @param {DomainObject} domainObject the domain object - * being represented - */ - represent: represent, - /** - * Release any resources associated with this representer. - */ - destroy: destroy - }; + this.gestureService = gestureService; + this.element = element; } + GestureRepresenter.prototype.represent = function represent(representation, domainObject) { + // Clear out any existing gestures + this.destroy(); + + // Attach gestures - by way of the service. + this.gestureHandle = this.gestureService.attachGestures( + this.element, + domainObject, + (representation || {}).gestures || [] + ); + }; + + GestureRepresenter.prototype.destroy = function () { + // Release any resources associated with these gestures + if (this.gestureHandle) { + this.gestureHandle.destroy(); + } + }; + return GestureRepresenter; } -); \ No newline at end of file +); diff --git a/platform/representation/src/services/DndService.js b/platform/representation/src/services/DndService.js index 0497c2bbfd..b8c4ae7bfe 100644 --- a/platform/representation/src/services/DndService.js +++ b/platform/representation/src/services/DndService.js @@ -32,6 +32,7 @@ define( * * Storing arbitrary JavaScript objects (not just strings.) * * Allowing inspection of dragged objects during `dragover` events, * etc. (which cannot be done in Chrome for security reasons) + * @memberof platform/representation * @constructor * @param $log Angular's $log service */ @@ -43,6 +44,7 @@ define( * Set drag data associated with a given type. * @param {string} key the type's identiifer * @param {*} value the data being dragged + * @memberof platform/representation.DndService# */ setData: function (key, value) { $log.debug("Setting drag data for " + key); @@ -51,6 +53,7 @@ define( /** * Get drag data associated with a given type. * @returns {*} the data being dragged + * @memberof platform/representation.DndService# */ getData: function (key) { return data[key]; @@ -58,6 +61,7 @@ define( /** * Remove data associated with active drags. * @param {string} key the type to remove + * @memberof platform/representation.DndService# */ removeData: function (key) { $log.debug("Clearing drag data for " + key); @@ -68,4 +72,4 @@ define( return DndService; } -); \ No newline at end of file +); diff --git a/platform/search/src/GenericSearchProvider.js b/platform/search/src/GenericSearchProvider.js index dae2cab9a9..014d8d7fda 100644 --- a/platform/search/src/GenericSearchProvider.js +++ b/platform/search/src/GenericSearchProvider.js @@ -48,10 +48,14 @@ define( * domain objects' IDs. */ function GenericSearchProvider($q, $timeout, objectService, workerService, ROOTS) { - var worker = workerService.run('genericSearchWorker'), - indexed = {}, - pendingQueries = {}; - // pendingQueries is a dictionary with the key value pairs st + var indexed = {}, + pendingQueries = {}, + worker = workerService.run('genericSearchWorker'); + + this.worker = worker; + this.pendingQueries = pendingQueries; + this.$q = $q; + // pendingQueries is a dictionary with the key value pairs st // the key is the timestamp and the value is the promise // Tell the web worker to add a domain object's model to its list of items. @@ -71,20 +75,7 @@ define( } } - // Tell the worker to search for items it has that match this searchInput. - // Takes the searchInput, as well as a max number of results (will return - // less than that if there are fewer matches). - function workerSearch(searchInput, maxResults, timestamp, timeout) { - var message = { - request: 'search', - input: searchInput, - maxNumber: maxResults, - timestamp: timestamp, - timeout: timeout - }; - worker.postMessage(message); - } - + // Handles responses from the web worker. Namely, the results of // a search request. function handleResponse(event) { @@ -120,8 +111,6 @@ define( } } - worker.onmessage = handleResponse; - // Helper function for getItems(). Indexes the tree. function indexItems(nodes) { nodes.forEach(function (node) { @@ -193,75 +182,87 @@ define( indexItems(objects); }); } - - // For documentation, see query below - function query(input, timestamp, maxResults, timeout) { - var terms = [], - searchResults = [], - defer = $q.defer(); - - // If the input is nonempty, do a search - if (input !== '' && input !== undefined) { - - // Allow us to access this promise later to resolve it later - pendingQueries[timestamp] = defer; - - // Check to see if the user provided a maximum - // number of results to display - if (!maxResults) { - // Else, we provide a default value - maxResults = DEFAULT_MAX_RESULTS; - } - // Similarly, check if timeout was provided - if (!timeout) { - timeout = DEFAULT_TIMEOUT; - } - // Send the query to the worker - workerSearch(input, maxResults, timestamp, timeout); + worker.onmessage = handleResponse; - return defer.promise; - } else { - // Otherwise return an empty result - return {hits: [], total: 0}; - } - } - // Index the tree's contents once at the beginning getItems(); - - return { - /** - * Searches through the filetree for domain objects which match - * the search term. This function is to be used as a fallback - * in the case where other search services are not avaliable. - * Returns a promise for a result object that has the format - * {hits: searchResult[], total: number, timedOut: boolean} - * where a searchResult has the format - * {id: string, object: domainObject, score: number} - * - * Notes: - * * The order of the results is not guarenteed. - * * A domain object qualifies as a match for a search input if - * the object's name property contains any of the search terms - * (which are generated by splitting the input at spaces). - * * Scores are higher for matches that have more of the terms - * as substrings. - * - * @param input The text input that is the query. - * @param timestamp The time at which this function was called. - * This timestamp is used as a unique identifier for this - * query and the corresponding results. - * @param maxResults (optional) The maximum number of results - * that this function should return. - * @param timeout (optional) The time after which the search should - * stop calculations and return partial results. - */ - query: query - - }; } + /** + * Searches through the filetree for domain objects which match + * the search term. This function is to be used as a fallback + * in the case where other search services are not avaliable. + * Returns a promise for a result object that has the format + * {hits: searchResult[], total: number, timedOut: boolean} + * where a searchResult has the format + * {id: string, object: domainObject, score: number} + * + * Notes: + * * The order of the results is not guarenteed. + * * A domain object qualifies as a match for a search input if + * the object's name property contains any of the search terms + * (which are generated by splitting the input at spaces). + * * Scores are higher for matches that have more of the terms + * as substrings. + * + * @param input The text input that is the query. + * @param timestamp The time at which this function was called. + * This timestamp is used as a unique identifier for this + * query and the corresponding results. + * @param maxResults (optional) The maximum number of results + * that this function should return. + * @param timeout (optional) The time after which the search should + * stop calculations and return partial results. + */ + GenericSearchProvider.prototype.query = function query(input, timestamp, maxResults, timeout) { + var terms = [], + searchResults = [], + pendingQueries = this.pendingQueries, + worker = this.worker, + defer = this.$q.defer(); + + // Tell the worker to search for items it has that match this searchInput. + // Takes the searchInput, as well as a max number of results (will return + // less than that if there are fewer matches). + function workerSearch(searchInput, maxResults, timestamp, timeout) { + var message = { + request: 'search', + input: searchInput, + maxNumber: maxResults, + timestamp: timestamp, + timeout: timeout + }; + worker.postMessage(message); + } + + // If the input is nonempty, do a search + if (input !== '' && input !== undefined) { + + // Allow us to access this promise later to resolve it later + pendingQueries[timestamp] = defer; + + // Check to see if the user provided a maximum + // number of results to display + if (!maxResults) { + // Else, we provide a default value + maxResults = DEFAULT_MAX_RESULTS; + } + // Similarly, check if timeout was provided + if (!timeout) { + timeout = DEFAULT_TIMEOUT; + } + + // Send the query to the worker + workerSearch(input, maxResults, timestamp, timeout); + + return defer.promise; + } else { + // Otherwise return an empty result + return { hits: [], total: 0 }; + } + }; + return GenericSearchProvider; } diff --git a/platform/search/src/SearchAggregator.js b/platform/search/src/SearchAggregator.js index da267214bf..2324090595 100644 --- a/platform/search/src/SearchAggregator.js +++ b/platform/search/src/SearchAggregator.js @@ -42,33 +42,55 @@ define( * aggregated. */ function SearchAggregator($q, providers) { - - // Remove duplicate objects that have the same ID. Modifies the passed - // array, and returns the number that were removed. + this.$q = $q; + this.providers = providers; + } + + /** + * Sends a query to each of the providers. Returns a promise for + * a result object that has the format + * {hits: searchResult[], total: number, timedOut: boolean} + * where a searchResult has the format + * {id: string, object: domainObject, score: number} + * + * @param inputText The text input that is the query. + * @param maxResults (optional) The maximum number of results + * that this function should return. If not provided, a + * default of 100 will be used. + */ + SearchAggregator.prototype.query = function queryAll(inputText, maxResults) { + var $q = this.$q, + providers = this.providers, + i, + timestamp = Date.now(), + resultPromises = []; + + // Remove duplicate objects that have the same ID. Modifies the passed + // array, and returns the number that were removed. function filterDuplicates(results, total) { var ids = {}, numRemoved = 0, i; - + for (i = 0; i < results.length; i += 1) { if (ids[results[i].id]) { // If this result's ID is already there, remove the object results.splice(i, 1); numRemoved += 1; - - // Reduce loop index because we shortened the array + + // Reduce loop index because we shortened the array i -= 1; } else { - // Otherwise add the ID to the list of the ones we have seen + // Otherwise add the ID to the list of the ones we have seen ids[results[i].id] = true; } } - + return numRemoved; } - + // Order the objects from highest to lowest score in the array. - // Modifies the passed array, as well as returns the modified array. + // Modifies the passed array, as well as returns the modified array. function orderByScore(results) { results.sort(function (a, b) { if (a.score > b.score) { @@ -81,65 +103,42 @@ define( }); return results; } - - // For documentation, see query below. - function queryAll(inputText, maxResults) { - var i, - timestamp = Date.now(), - resultPromises = []; - - if (!maxResults) { - maxResults = DEFAULT_MAX_RESULTS; - } - - // Send the query to all the providers - for (i = 0; i < providers.length; i += 1) { - resultPromises.push( - providers[i].query(inputText, timestamp, maxResults, DEFUALT_TIMEOUT) - ); - } - - // Get promises for results arrays - return $q.all(resultPromises).then(function (resultObjects) { - var results = [], - totalSum = 0, - i; - - // Merge results - for (i = 0; i < resultObjects.length; i += 1) { - results = results.concat(resultObjects[i].hits); - totalSum += resultObjects[i].total; - } - // Order by score first, so that when removing repeats we keep the higher scored ones - orderByScore(results); - totalSum -= filterDuplicates(results, totalSum); - - return { - hits: results, - total: totalSum, - timedOut: resultObjects.some(function (obj) { - return obj.timedOut; - }) - }; - }); + + if (!maxResults) { + maxResults = DEFAULT_MAX_RESULTS; } - - return { - /** - * Sends a query to each of the providers. Returns a promise for - * a result object that has the format - * {hits: searchResult[], total: number, timedOut: boolean} - * where a searchResult has the format - * {id: string, object: domainObject, score: number} - * - * @param inputText The text input that is the query. - * @param maxResults (optional) The maximum number of results - * that this function should return. If not provided, a - * default of 100 will be used. - */ - query: queryAll - }; - } + + // Send the query to all the providers + for (i = 0; i < providers.length; i += 1) { + resultPromises.push( + providers[i].query(inputText, timestamp, maxResults, DEFUALT_TIMEOUT) + ); + } + + // Get promises for results arrays + return $q.all(resultPromises).then(function (resultObjects) { + var results = [], + totalSum = 0, + i; + + // Merge results + for (i = 0; i < resultObjects.length; i += 1) { + results = results.concat(resultObjects[i].hits); + totalSum += resultObjects[i].total; + } + // Order by score first, so that when removing repeats we keep the higher scored ones + orderByScore(results); + totalSum -= filterDuplicates(results, totalSum); + + return { + hits: results, + total: totalSum, + timedOut: resultObjects.some(function (obj) { + return obj.timedOut; + }) + }; + }); + }; return SearchAggregator; } diff --git a/platform/telemetry/src/TelemetryAggregator.js b/platform/telemetry/src/TelemetryAggregator.js index 11c2802e52..fb4cf81fe0 100644 --- a/platform/telemetry/src/TelemetryAggregator.js +++ b/platform/telemetry/src/TelemetryAggregator.js @@ -22,89 +22,93 @@ /*global define*/ /** - * Module defining TelemetryProvider. Created by vwoeltje on 11/12/14. + * This bundle provides infrastructure and utility services for handling + * telemetry data. + * @namespace platform/telemetry */ define( [], function () { "use strict"; + /** + * Request telemetry data. + * @param {TelemetryRequest[]} requests and array of + * requests to be handled + * @returns {Promise} a promise for telemetry data + * which may (or may not, depending on + * availability) satisfy the requests + * @method TelemetryService#requestTelemetry + */ + /** + * Subscribe to streaming updates to telemetry data. + * The provided callback will be invoked as new + * telemetry becomes available; as an argument, it + * will receive an object of key-value pairs, where + * keys are source identifiers and values are objects + * of key-value pairs, where keys are point identifiers + * and values are TelemetrySeries objects containing + * the latest streaming telemetry. + * @param {Function} callback the callback to invoke + * @param {TelemetryRequest[]} requests an array of + * requests to be subscribed upon + * @returns {Function} a function which can be called + * to unsubscribe + * @method TelmetryService#subscribe + */ + /** * A telemetry aggregator makes many telemetry providers * appear as one. * + * @memberof platform/telemetry * @constructor */ function TelemetryAggregator($q, telemetryProviders) { - - // Merge the results from many providers into one - // result object. - function mergeResults(results) { - var merged = {}; - - results.forEach(function (result) { - Object.keys(result).forEach(function (k) { - merged[k] = result[k]; - }); - }); - - return merged; - } - - // Request telemetry from all providers; once they've - // responded, merge the results into one result object. - function requestTelemetry(requests) { - return $q.all(telemetryProviders.map(function (provider) { - return provider.requestTelemetry(requests); - })).then(mergeResults); - } - - // Subscribe to updates from all providers - function subscribe(callback, requests) { - var unsubscribes = telemetryProviders.map(function (provider) { - return provider.subscribe(callback, requests); - }); - - // Return an unsubscribe function that invokes unsubscribe - // for all providers. - return function () { - unsubscribes.forEach(function (unsubscribe) { - if (unsubscribe) { - unsubscribe(); - } - }); - }; - } - - return { - /** - * Request telemetry data. - * @param {TelemetryRequest[]} requests and array of - * requests to be handled - * @returns {Promise} a promise for telemetry data - * which may (or may not, depending on - * availability) satisfy the requests - */ - requestTelemetry: requestTelemetry, - /** - * Subscribe to streaming updates to telemetry data. - * The provided callback will be invoked as new - * telemetry becomes available; as an argument, it - * will receive an object of key-value pairs, where - * keys are source identifiers and values are objects - * of key-value pairs, where keys are point identifiers - * and values are TelemetrySeries objects containing - * the latest streaming telemetry. - * @param {Function} callback the callback to invoke - * @param {TelemetryRequest[]} requests an array of - * requests to be subscribed upon - * @returns {Function} a function which can be called - * to unsubscribe - */ - subscribe: subscribe - }; + this.$q = $q; + this.telemetryProviders = telemetryProviders; } + // Merge the results from many providers into one + // result object. + function mergeResults(results) { + var merged = {}; + + results.forEach(function (result) { + Object.keys(result).forEach(function (k) { + merged[k] = result[k]; + }); + }); + + return merged; + } + + // Request telemetry from all providers; once they've + // responded, merge the results into one result object. + TelemetryAggregator.prototype.requestTelemetry = function (requests) { + return this.$q.all(this.telemetryProviders.map(function (provider) { + return provider.requestTelemetry(requests); + })).then(mergeResults); + }; + + // Subscribe to updates from all providers + TelemetryAggregator.prototype.subscribe = function (callback, requests) { + var unsubscribes = this.telemetryProviders.map(function (provider) { + return provider.subscribe(callback, requests); + }); + + // Return an unsubscribe function that invokes unsubscribe + // for all providers. + return function () { + unsubscribes.forEach(function (unsubscribe) { + if (unsubscribe) { + unsubscribe(); + } + }); + }; + }; + + return TelemetryAggregator; } -); \ No newline at end of file +); diff --git a/platform/telemetry/src/TelemetryCapability.js b/platform/telemetry/src/TelemetryCapability.js index 473aafcdb9..1fbd12a691 100644 --- a/platform/telemetry/src/TelemetryCapability.js +++ b/platform/telemetry/src/TelemetryCapability.js @@ -41,153 +41,151 @@ define( * for a specific object, and for unwrapping the response (to get * at the specific data which is appropriate to the domain object.) * + * @memberof platform/telemetry + * @implements {Capability} * @constructor */ function TelemetryCapability($injector, $q, $log, domainObject) { - var telemetryService, - subscriptions = [], - unsubscribeFunction; - // We could depend on telemetryService directly, but - // there isn't a platform implementation of this; - function getTelemetryService() { - if (telemetryService === undefined) { - try { - telemetryService = - $injector.get("telemetryService"); - } catch (e) { - // $injector should throw if telemetryService - // is unavailable or unsatisfiable. - $log.warn("Telemetry service unavailable"); - telemetryService = null; - } + // there isn't a platform implementation of this. + this.initializeTelemetryService = function () { + try { + return (this.telemetryService = + $injector.get("telemetryService")); + } catch (e) { + // $injector should throw if telemetryService + // is unavailable or unsatisfiable. + $log.warn("Telemetry service unavailable"); + return (this.telemetryService = null); } - return telemetryService; - } - - // Build a request object. This takes the request that was - // passed to the capability, and adds source, id, and key - // fields associated with the object (from its type definition - // and/or its model) - function buildRequest(request) { - // Start with any "telemetry" field in type; use that as a - // basis for the request. - var type = domainObject.getCapability("type"), - typeRequest = (type && type.getDefinition().telemetry) || {}, - modelTelemetry = domainObject.getModel().telemetry, - fullRequest = Object.create(typeRequest); - - // Add properties from the telemetry field of this - // specific domain object. - Object.keys(modelTelemetry).forEach(function (k) { - fullRequest[k] = modelTelemetry[k]; - }); - - // Add properties from this specific requestData call. - Object.keys(request).forEach(function (k) { - fullRequest[k] = request[k]; - }); - - // Ensure an ID and key are present - if (!fullRequest.id) { - fullRequest.id = domainObject.getId(); - } - if (!fullRequest.key) { - fullRequest.key = domainObject.getId(); - } - - return fullRequest; - } - - // Issue a request for telemetry data - function requestTelemetry(request) { - // Bring in any defaults from the object model - var fullRequest = buildRequest(request || {}), - source = fullRequest.source, - key = fullRequest.key; - - // Pull out the relevant field from the larger, - // structured response. - function getRelevantResponse(response) { - return ((response || {})[source] || {})[key] || - EMPTY_SERIES; - } - - // Issue a request to the service - function requestTelemetryFromService() { - return telemetryService.requestTelemetry([fullRequest]); - } - - // If a telemetryService is not available, - // getTelemetryService() should reject, and this should - // bubble through subsequent then calls. - return getTelemetryService() && - requestTelemetryFromService() - .then(getRelevantResponse); - } - - // Listen for real-time and/or streaming updates - function subscribe(callback, request) { - var fullRequest = buildRequest(request || {}); - - // Unpack the relevant telemetry series - function update(telemetries) { - var source = fullRequest.source, - key = fullRequest.key, - result = ((telemetries || {})[source] || {})[key]; - if (result) { - callback(result); - } - } - - return getTelemetryService() && - telemetryService.subscribe(update, [fullRequest]); - } - - return { - /** - * Request telemetry data for this specific domain object. - * @param {TelemetryRequest} [request] parameters for this - * specific request - * @returns {Promise} a promise for the resulting telemetry - * object - */ - requestData: requestTelemetry, - - /** - * Get metadata about this domain object's associated - * telemetry. - */ - getMetadata: function () { - // metadata just looks like a request, - // so use buildRequest to bring in both - // type-level and object-level telemetry - // properties - return buildRequest({}); - }, - - /** - * Subscribe to updates to telemetry data for this domain - * object. - * @param {Function} callback a function to call when new - * data becomes available; the telemetry series - * containing the data will be given as an argument. - * @param {TelemetryRequest} [request] parameters for the - * subscription request - */ - subscribe: subscribe }; + + + this.$q = $q; + this.$log = $log; + this.domainObject = domainObject; } + // Build a request object. This takes the request that was + // passed to the capability, and adds source, id, and key + // fields associated with the object (from its type definition + // and/or its model) + TelemetryCapability.prototype.buildRequest = function (request) { + // Start with any "telemetry" field in type; use that as a + // basis for the request. + var domainObject = this.domainObject, + type = domainObject.getCapability("type"), + typeRequest = (type && type.getDefinition().telemetry) || {}, + modelTelemetry = domainObject.getModel().telemetry, + fullRequest = Object.create(typeRequest); + + // Add properties from the telemetry field of this + // specific domain object. + Object.keys(modelTelemetry).forEach(function (k) { + fullRequest[k] = modelTelemetry[k]; + }); + + // Add properties from this specific requestData call. + Object.keys(request).forEach(function (k) { + fullRequest[k] = request[k]; + }); + + // Ensure an ID and key are present + if (!fullRequest.id) { + fullRequest.id = domainObject.getId(); + } + if (!fullRequest.key) { + fullRequest.key = domainObject.getId(); + } + + return fullRequest; + }; + + + /** + * Request telemetry data for this specific domain object. + * @param {TelemetryRequest} [request] parameters for this + * specific request + * @returns {Promise} a promise for the resulting telemetry + * object + */ + TelemetryCapability.prototype.requestData = function requestTelemetry(request) { + // Bring in any defaults from the object model + var fullRequest = this.buildRequest(request || {}), + source = fullRequest.source, + key = fullRequest.key, + telemetryService = this.telemetryService || + this.initializeTelemetryService(); // Lazy initialization + + // Pull out the relevant field from the larger, + // structured response. + function getRelevantResponse(response) { + return ((response || {})[source] || {})[key] || + EMPTY_SERIES; + } + + // Issue a request to the service + function requestTelemetryFromService() { + return telemetryService.requestTelemetry([fullRequest]); + } + + // If a telemetryService is not available, + // getTelemetryService() should reject, and this should + // bubble through subsequent then calls. + return telemetryService && + requestTelemetryFromService().then(getRelevantResponse); + }; + + /** + * Get metadata about this domain object's associated + * telemetry. + * @returns {TelemetryMetadata} metadata about this object's telemetry + */ + TelemetryCapability.prototype.getMetadata = function () { + // metadata just looks like a request, + // so use buildRequest to bring in both + // type-level and object-level telemetry + // properties + return (this.metadata = this.metadata || this.buildRequest({})); + }; + + /** + * Subscribe to updates to telemetry data for this domain + * object. + * @param {Function} callback a function to call when new + * data becomes available; the telemetry series + * containing the data will be given as an argument. + * @param {TelemetryRequest} [request] parameters for the + * subscription request + */ + TelemetryCapability.prototype.subscribe = function subscribe(callback, request) { + var fullRequest = this.buildRequest(request || {}), + telemetryService = this.telemetryService || + this.initializeTelemetryService(); // Lazy initialization + + // Unpack the relevant telemetry series + function update(telemetries) { + var source = fullRequest.source, + key = fullRequest.key, + result = ((telemetries || {})[source] || {})[key]; + if (result) { + callback(result); + } + } + + return telemetryService && + telemetryService.subscribe(update, [fullRequest]); + }; + /** * The telemetry capability is applicable when a * domain object model has a "telemetry" field. */ TelemetryCapability.appliesTo = function (model) { - return (model && - model.telemetry) ? true : false; + return (model && model.telemetry) ? true : false; }; return TelemetryCapability; } ); + diff --git a/platform/telemetry/src/TelemetryController.js b/platform/telemetry/src/TelemetryController.js index 504ec2ec4a..83279252d5 100644 --- a/platform/telemetry/src/TelemetryController.js +++ b/platform/telemetry/src/TelemetryController.js @@ -34,7 +34,9 @@ define( * which need to issue requests for telemetry data and use the * results * + * @memberof platform/telemetry * @constructor + * @deprecated use platform/telemetry.TelemetryHandler instead */ function TelemetryController($scope, $q, $timeout, $log) { @@ -314,6 +316,7 @@ define( * given index will correspond to the telemetry-providing * domain object at the same index. * @returns {Array} an array of metadata objects + * @memberof platform/telemetry.TelemetryController# */ getMetadata: function () { return self.metadatas; @@ -328,6 +331,7 @@ define( * given index will correspond to the telemetry-providing * domain object at the same index. * @returns {DomainObject[]} an array of metadata objects + * @memberof platform/telemetry.TelemetryController# */ getTelemetryObjects: function () { return self.telemetryObjects; @@ -345,6 +349,7 @@ define( * response at a given index will correspond to the * telemetry-providing domain object at the same index. * @returns {Array} an array of responses + * @memberof platform/telemetry.TelemetryController# */ getResponse: function getResponse(arg) { var id = arg && (typeof arg === 'string' ? @@ -364,6 +369,7 @@ define( * show user feedback, such as a wait spinner. * * @returns {boolean} true if the request is still outstanding + * @memberof platform/telemetry.TelemetryController# */ isRequestPending: function () { return self.pending > 0; @@ -372,6 +378,7 @@ define( * Issue a new data request. This will change the * request parameters that are passed along to all * telemetry capabilities managed by this controller. + * @memberof platform/telemetry.TelemetryController# */ requestData: function (request) { self.request = request || {}; @@ -382,6 +389,7 @@ define( * perform its polling activity. * @param {number} durationMillis the interval at * which to poll, in milliseconds + * @memberof platform/telemetry.TelemetryController# */ setRefreshInterval: function (durationMillis) { self.interval = durationMillis; @@ -392,4 +400,4 @@ define( return TelemetryController; } -); \ No newline at end of file +); diff --git a/platform/telemetry/src/TelemetryDelegator.js b/platform/telemetry/src/TelemetryDelegator.js index 50fee40ad1..37fd1bedbf 100644 --- a/platform/telemetry/src/TelemetryDelegator.js +++ b/platform/telemetry/src/TelemetryDelegator.js @@ -29,38 +29,44 @@ define( /** * Used to handle telemetry delegation associated with a * given domain object. + * @constructor + * @memberof platform/telemetry */ function TelemetryDelegator($q) { - return { - /** - * Promise telemetry-providing objects associated with - * this domain object (either the domain object itself, - * or the objects it delegates) - * @returns {Promise.} domain objects with - * a telemetry capability - */ - promiseTelemetryObjects: function (domainObject) { - // If object has been cleared, there are no relevant - // telemetry-providing domain objects. - if (!domainObject) { - return $q.when([]); - } - - // Otherwise, try delegation first, and attach the - // object itself if it has a telemetry capability. - return $q.when(domainObject.useCapability( - "delegation", - "telemetry" - )).then(function (result) { - var head = domainObject.hasCapability("telemetry") ? - [ domainObject ] : [], - tail = result || []; - return head.concat(tail); - }); - } - }; + this.$q = $q; } + /** + * Promise telemetry-providing objects associated with + * this domain object (either the domain object itself, + * or the objects it delegates) + * @param {DomainObject} the domain object which may have + * or delegate telemetry + * @returns {Promise.} domain objects with + * a telemetry capability + */ + TelemetryDelegator.prototype.promiseTelemetryObjects = function (domainObject) { + var $q = this.$q; + + // If object has been cleared, there are no relevant + // telemetry-providing domain objects. + if (!domainObject) { + return $q.when([]); + } + + // Otherwise, try delegation first, and attach the + // object itself if it has a telemetry capability. + return $q.when(domainObject.useCapability( + "delegation", + "telemetry" + )).then(function (result) { + var head = domainObject.hasCapability("telemetry") ? + [ domainObject ] : [], + tail = result || []; + return head.concat(tail); + }); + }; + return TelemetryDelegator; } -); \ No newline at end of file +); diff --git a/platform/telemetry/src/TelemetryFormatter.js b/platform/telemetry/src/TelemetryFormatter.js index 853db642d7..bbd4cf100c 100644 --- a/platform/telemetry/src/TelemetryFormatter.js +++ b/platform/telemetry/src/TelemetryFormatter.js @@ -35,42 +35,39 @@ define( * The TelemetryFormatter is responsible for formatting (as text * for display) values along either the domain (usually time) or * the range (usually value) of a data series. + * @memberof platform/telemetry * @constructor */ function TelemetryFormatter() { - function formatDomainValue(v, key) { - return isNaN(v) ? "" : moment.utc(v).format(DATE_FORMAT); - } - - function formatRangeValue(v, key) { - return isNaN(v) ? v : v.toFixed(3); - } - - return { - /** - * Format a domain value. - * @param {number} v the domain value; a timestamp - * in milliseconds since start of 1970 - * @param {string} [key] the key which identifies the - * domain; if unspecified or unknown, this will - * be treated as a standard timestamp. - * @returns {string} a textual representation of the - * data and time, suitable for display. - */ - formatDomainValue: formatDomainValue, - /** - * Format a range value. - * @param {number} v the range value; a numeric value - * @param {string} [key] the key which identifies the - * range; if unspecified or unknown, this will - * be treated as a numeric value. - * @returns {string} a textual representation of the - * value, suitable for display. - */ - formatRangeValue: formatRangeValue - }; } + /** + * Format a domain value. + * @param {number} v the domain value; a timestamp + * in milliseconds since start of 1970 + * @param {string} [key] the key which identifies the + * domain; if unspecified or unknown, this will + * be treated as a standard timestamp. + * @returns {string} a textual representation of the + * data and time, suitable for display. + */ + TelemetryFormatter.prototype.formatDomainValue = function (v, key) { + return isNaN(v) ? "" : moment.utc(v).format(DATE_FORMAT); + }; + + /** + * Format a range value. + * @param {number} v the range value; a numeric value + * @param {string} [key] the key which identifies the + * range; if unspecified or unknown, this will + * be treated as a numeric value. + * @returns {string} a textual representation of the + * value, suitable for display. + */ + TelemetryFormatter.prototype.formatRangeValue = function (v, key) { + return isNaN(v) ? String(v) : v.toFixed(VALUE_FORMAT_DIGITS); + }; + return TelemetryFormatter; } -); \ No newline at end of file +); diff --git a/platform/telemetry/src/TelemetryHandle.js b/platform/telemetry/src/TelemetryHandle.js index e93b606aeb..145edfc5d7 100644 --- a/platform/telemetry/src/TelemetryHandle.js +++ b/platform/telemetry/src/TelemetryHandle.js @@ -34,6 +34,8 @@ define( * @param $q Angular's $q, for promises * @param {TelemetrySubscription} subscription a subscription * to supplied telemetry + * @constructor + * @memberof platform/telemetry */ function TelemetryHandle($q, subscription) { var seriesMap = {}, @@ -67,6 +69,7 @@ define( * data associated with it * @return {TelemetrySeries} the most recent telemetry series * (or undefined if there is not one) + * @memberof platform/telemetry.TelemetryHandle# */ self.getSeries = function (domainObject) { var id = domainObject.getId(); @@ -81,6 +84,7 @@ define( * @param {Function} [callback] a callback that will be * invoked as new data becomes available, with the * domain object for which new data is available. + * @memberof platform/telemetry.TelemetryHandle# */ self.request = function (request, callback) { // Issue (and handle) the new request from this object @@ -109,4 +113,4 @@ define( return TelemetryHandle; } -); \ No newline at end of file +); diff --git a/platform/telemetry/src/TelemetryHandler.js b/platform/telemetry/src/TelemetryHandler.js index 9b56bbc41e..cd0df98724 100644 --- a/platform/telemetry/src/TelemetryHandler.js +++ b/platform/telemetry/src/TelemetryHandler.js @@ -31,24 +31,37 @@ define( * A TelemetryRequester provides an easy interface to request * telemetry associated with a set of domain objects. * + * @memberof platform/telemetry * @constructor * @param $q Angular's $q */ function TelemetryHandler($q, telemetrySubscriber) { - return { - handle: function (domainObject, callback, lossless) { - var subscription = telemetrySubscriber.subscribe( - domainObject, - callback, - lossless - ); - - return new TelemetryHandle($q, subscription); - } - }; + this.$q = $q; + this.telemetrySubscriber = telemetrySubscriber; } + /** + * Start receiving telemetry associated with this domain object + * (either directly, or via delegation.) + * @param {DomainObject} domainObject the domain object + * @param {Function} callback callback to invoke when new data is + * available + * @param {boolean} lossless true if the callback should be invoked + * one separate time for each new latest value + * @returns {TelemetryHandle} a handle to telemetry data + * associated with this object + */ + TelemetryHandler.prototype.handle = function (domainObject, callback, lossless) { + var subscription = this.telemetrySubscriber.subscribe( + domainObject, + callback, + lossless + ); + + return new TelemetryHandle(this.$q, subscription); + }; + return TelemetryHandler; } -); \ No newline at end of file +); diff --git a/platform/telemetry/src/TelemetryQueue.js b/platform/telemetry/src/TelemetryQueue.js index cf82d08ff8..d51c42a98f 100644 --- a/platform/telemetry/src/TelemetryQueue.js +++ b/platform/telemetry/src/TelemetryQueue.js @@ -32,7 +32,9 @@ define( * a queued series of large objects, ensuring that no value is * overwritten (but consolidated non-overlapping keys into single * objects.) + * @memberof platform/telemetry * @constructor + * @implements {platform/telemetry.TelemetryPool} */ function TelemetryQueue() { // General approach here: @@ -59,9 +61,37 @@ define( // 0 1 2 3 4 // a * * * * * // b * * * - // c * * * - var queue = [], - counts = {}; + // c * * * + + this.queue = []; + this.counts = {}; + } + + + TelemetryQueue.prototype.isEmpty = function () { + return this.queue.length < 1; + }; + + TelemetryQueue.prototype.poll = function () { + var counts = this.counts; + + // Decrement counts for a specific key + function decrementCount(key) { + if (counts[key] < 2) { + delete counts[key]; + } else { + counts[key] -= 1; + } + } + + // Decrement counts for the object that will be popped + Object.keys(counts).forEach(decrementCount); + return this.queue.shift(); + }; + + TelemetryQueue.prototype.put = function (key, value) { + var queue = this.queue, + counts = this.counts; // Look up an object in the queue that does not have a value // assigned to this key (or, add a new one) @@ -70,7 +100,7 @@ define( // Track the largest free position for this key counts[key] = index + 1; - + // If it's before the end of the queue, add it there if (index < queue.length) { return queue[index]; @@ -83,52 +113,10 @@ define( queue.push(object); return object; } - - // Decrement counts for a specific key - function decrementCount(key) { - if (counts[key] < 2) { - delete counts[key]; - } else { - counts[key] -= 1; - } - } - - // Decrement all counts - function decrementCounts() { - Object.keys(counts).forEach(decrementCount); - } - return { - /** - * Check if any value groups remain in this pool. - * @return {boolean} true if value groups remain - */ - isEmpty: function () { - return queue.length < 1; - }, - /** - * Retrieve the next value group from this pool. - * This gives an object containing key-value pairs, - * where keys and values correspond to the arguments - * given to previous put functions. - * @return {object} key-value pairs - */ - poll: function () { - // Decrement counts for the object that will be popped - decrementCounts(); - return queue.shift(); - }, - /** - * Put a key-value pair into the pool. - * @param {string} key the key to store the value under - * @param {*} value the value to store - */ - put: function (key, value) { - getFreeObject(key)[key] = value; - } - }; - } + getFreeObject(key)[key] = value; + }; return TelemetryQueue; } -); \ No newline at end of file +); diff --git a/platform/telemetry/src/TelemetrySubscriber.js b/platform/telemetry/src/TelemetrySubscriber.js index e106968828..c6e7ec0bf1 100644 --- a/platform/telemetry/src/TelemetrySubscriber.js +++ b/platform/telemetry/src/TelemetrySubscriber.js @@ -38,43 +38,42 @@ define( * (e.g. for telemetry panels) as well as latest-value * extraction. * + * @memberof platform/telemetry * @constructor * @param $q Angular's $q * @param $timeout Angular's $timeout */ function TelemetrySubscriber($q, $timeout) { - return { - /** - * Subscribe to streaming telemetry updates - * associated with this domain object (either - * directly or via capability delegation.) - * - * @param {DomainObject} domainObject the object whose - * associated telemetry data is of interest - * @param {Function} callback a function to invoke - * when new data has become available. - * @param {boolean} lossless flag to indicate whether the - * callback should be notified for all values - * (otherwise, multiple values in quick succession - * will call back with only the latest value.) - * @returns {TelemetrySubscription} the subscription, - * which will provide access to latest values. - * - * @method - * @memberof TelemetrySubscriber - */ - subscribe: function (domainObject, callback, lossless) { - return new TelemetrySubscription( - $q, - $timeout, - domainObject, - callback, - lossless - ); - } - }; + this.$q = $q; + this.$timeout = $timeout; } + /** + * Subscribe to streaming telemetry updates + * associated with this domain object (either + * directly or via capability delegation.) + * + * @param {DomainObject} domainObject the object whose + * associated telemetry data is of interest + * @param {Function} callback a function to invoke + * when new data has become available. + * @param {boolean} lossless flag to indicate whether the + * callback should be notified for all values + * (otherwise, multiple values in quick succession + * will call back with only the latest value.) + * @returns {platform/telemetry.TelemetrySubscription} the + * subscription, which will provide access to latest values. + */ + TelemetrySubscriber.prototype.subscribe = function (domainObject, callback, lossless) { + return new TelemetrySubscription( + this.$q, + this.$timeout, + domainObject, + callback, + lossless + ); + }; + return TelemetrySubscriber; } -); \ No newline at end of file +); diff --git a/platform/telemetry/src/TelemetrySubscription.js b/platform/telemetry/src/TelemetrySubscription.js index 4f7b8379d1..8b4d7d7a9c 100644 --- a/platform/telemetry/src/TelemetrySubscription.js +++ b/platform/telemetry/src/TelemetrySubscription.js @@ -26,6 +26,32 @@ define( function (TelemetryQueue, TelemetryTable, TelemetryDelegator) { "use strict"; + /** + * A pool of telemetry values. + * @interface platform/telemetry.TelemetryPool + * @private + */ + /** + * Check if any value groups remain in this pool. + * @return {boolean} true if value groups remain + * @method platform/telemetry.TelemetryPool#isEmpty + */ + /** + * Retrieve the next value group from this pool. + * This gives an object containing key-value pairs, + * where keys and values correspond to the arguments + * given to previous put functions. + * @return {object} key-value pairs + * @method platform/telemetry.TelemetryPool#poll + */ + /** + * Put a key-value pair into the pool. + * @param {string} key the key to store the value under + * @param {*} value the value to store + * @method platform/telemetry.TelemetryPool#put + */ + + /** * A TelemetrySubscription tracks latest values for streaming * telemetry data and handles notifying interested observers. @@ -38,6 +64,7 @@ define( * (e.g. for telemetry panels) as well as latest-value * extraction. * + * @memberof platform/telemetry * @constructor * @param $q Angular's $q * @param $timeout Angular's $timeout @@ -51,14 +78,9 @@ define( * the callback once, with access to the latest data */ function TelemetrySubscription($q, $timeout, domainObject, callback, lossless) { - var delegator = new TelemetryDelegator($q), - unsubscribePromise, - telemetryObjectPromise, - latestValues = {}, - telemetryObjects = [], + var self = this, + delegator = new TelemetryDelegator($q), pool = lossless ? new TelemetryQueue() : new TelemetryTable(), - metadatas, - unlistenToMutation, updatePending; // Look up domain objects which have telemetry capabilities. @@ -71,7 +93,7 @@ define( function updateValuesFromPool() { var values = pool.poll(); Object.keys(values).forEach(function (k) { - latestValues[k] = values[k]; + self.latestValues[k] = values[k]; }); } @@ -164,8 +186,8 @@ define( // initial subscription chain; this allows `getTelemetryObjects()` // to return a non-Promise to simplify usage elsewhere. function cacheObjectReferences(objects) { - telemetryObjects = objects; - metadatas = objects.map(lookupMetadata); + self.telemetryObjects = objects; + self.metadatas = objects.map(lookupMetadata); // Fire callback, as this will be the first time that // telemetry objects are available, or these objects // will have changed. @@ -175,14 +197,6 @@ define( return objects; } - function unsubscribeAll() { - return unsubscribePromise.then(function (unsubscribes) { - return $q.all(unsubscribes.map(function (unsubscribe) { - return unsubscribe(); - })); - }); - } - function initialize() { // Get a reference to relevant objects (those with telemetry // capabilities) and subscribe to their telemetry updates. @@ -190,23 +204,23 @@ define( // will be unsubscribe functions. (This must be a promise // because delegation is supported, and retrieving delegate // telemetry-capable objects may be an asynchronous operation.) - telemetryObjectPromise = promiseRelevantObjects(domainObject); - unsubscribePromise = telemetryObjectPromise + self.telemetryObjectPromise = promiseRelevantObjects(domainObject); + self.unsubscribePromise = self.telemetryObjectPromise .then(cacheObjectReferences) .then(subscribeAll); } function idsMatch(ids) { - return ids.length === telemetryObjects.length && + return ids.length === self.telemetryObjects.length && ids.every(function (id, index) { - return telemetryObjects[index].getId() === id; + return self.telemetryObjects[index].getId() === id; }); } function modelChange(model) { if (!idsMatch((model || {}).composition || [])) { // Reinitialize if composition has changed - unsubscribeAll().then(initialize); + self.unsubscribeAll().then(initialize); } } @@ -218,116 +232,128 @@ define( } } - initialize(); - unlistenToMutation = addMutationListener(); + this.$q = $q; + this.latestValues = {}; + this.telemetryObjects = []; + this.metadatas = []; - return { - /** - * Terminate all underlying subscriptions associated - * with this object. - * @method - * @memberof TelemetrySubscription - */ - unsubscribe: function () { - if (unlistenToMutation) { - unlistenToMutation(); - } - return unsubscribeAll(); - }, - /** - * Get the most recent domain value that has been observed - * for the specified domain object. This will typically be - * a timestamp. - * - * The domain object passed here should be one that is - * subscribed-to here; that is, it should be one of the - * domain objects returned by `getTelemetryObjects()`. - * - * @param {DomainObject} domainObject the object of interest - * @returns the most recent domain value observed - * @method - * @memberof TelemetrySubscription - */ - getDomainValue: function (domainObject) { - var id = domainObject.getId(); - return (latestValues[id] || {}).domain; - }, - /** - * Get the most recent range value that has been observed - * for the specified domain object. This will typically - * be a numeric measurement. - * - * The domain object passed here should be one that is - * subscribed-to here; that is, it should be one of the - * domain objects returned by `getTelemetryObjects()`. - * - * @param {DomainObject} domainObject the object of interest - * @returns the most recent range value observed - * @method - * @memberof TelemetrySubscription - */ - getRangeValue: function (domainObject) { - var id = domainObject.getId(); - return (latestValues[id] || {}).range; - }, - /** - * Get the latest telemetry datum for this domain object. - * - * @param {DomainObject} domainObject the object of interest - * @returns {TelemetryDatum} the most recent datum - */ - getDatum: function (domainObject) { - var id = domainObject.getId(); - return (latestValues[id] || {}).datum; - }, - /** - * Get all telemetry-providing domain objects which are - * being observed as part of this subscription. - * - * Capability delegation will be taken into account (so, if - * a Telemetry Panel was passed in the constructor, this will - * return its contents.) Capability delegation is resolved - * asynchronously so the return value here may change over - * time; while this resolution is pending, this method will - * return an empty array. - * - * @returns {DomainObject[]} all subscribed-to domain objects - * @method - * @memberof TelemetrySubscription - */ - getTelemetryObjects: function () { - return telemetryObjects; - }, - /** - * Get all telemetry metadata associated with - * telemetry-providing domain objects managed by - * this controller. - * - * This will ordered in the - * same manner as `getTelemetryObjects()` or - * `getResponse()`; that is, the metadata at a - * given index will correspond to the telemetry-providing - * domain object at the same index. - * @returns {Array} an array of metadata objects - */ - getMetadata: function () { - return metadatas; - }, - /** - * Get a promise for all telemetry-providing objects - * associated with this subscription. - * @returns {Promise.} a promise for - * telemetry-providing objects - */ - promiseTelemetryObjects: function () { - // Unsubscribe promise is available after objects - // are loaded. - return telemetryObjectPromise; - } - }; + initialize(); + this.unlistenToMutation = addMutationListener(); } + TelemetrySubscription.prototype.unsubscribeAll = function () { + var $q = this.$q; + return this.unsubscribePromise.then(function (unsubscribes) { + return $q.all(unsubscribes.map(function (unsubscribe) { + return unsubscribe(); + })); + }); + }; + + /** + * Terminate all underlying subscriptions associated + * with this object. + */ + TelemetrySubscription.prototype.unsubscribe = function () { + if (this.unlistenToMutation) { + this.unlistenToMutation(); + } + return this.unsubscribeAll(); + }; + + /** + * Get the most recent domain value that has been observed + * for the specified domain object. This will typically be + * a timestamp. + * + * The domain object passed here should be one that is + * subscribed-to here; that is, it should be one of the + * domain objects returned by `getTelemetryObjects()`. + * + * @param {DomainObject} domainObject the object of interest + * @returns the most recent domain value observed + */ + TelemetrySubscription.prototype.getDomainValue = function (domainObject) { + var id = domainObject.getId(); + return (this.latestValues[id] || {}).domain; + }; + + /** + * Get the most recent range value that has been observed + * for the specified domain object. This will typically + * be a numeric measurement. + * + * The domain object passed here should be one that is + * subscribed-to here; that is, it should be one of the + * domain objects returned by `getTelemetryObjects()`. + * + * @param {DomainObject} domainObject the object of interest + * @returns the most recent range value observed + */ + TelemetrySubscription.prototype.getRangeValue = function (domainObject) { + var id = domainObject.getId(); + return (this.latestValues[id] || {}).range; + }; + + /** + * Get the latest telemetry datum for this domain object. + * + * @param {DomainObject} domainObject the object of interest + * @returns {TelemetryDatum} the most recent datum + */ + TelemetrySubscription.prototype.getDatum = function (domainObject) { + var id = domainObject.getId(); + return (this.latestValues[id] || {}).datum; + }; + + /** + * Get all telemetry-providing domain objects which are + * being observed as part of this subscription. + * + * Capability delegation will be taken into account (so, if + * a Telemetry Panel was passed in the constructor, this will + * return its contents.) Capability delegation is resolved + * asynchronously so the return value here may change over + * time; while this resolution is pending, this method will + * return an empty array. + * + * @returns {DomainObject[]} all subscribed-to domain objects + */ + TelemetrySubscription.prototype.getTelemetryObjects = function () { + return this.telemetryObjects; + }; + + /** + * Get all telemetry metadata associated with + * telemetry-providing domain objects managed by + * this controller. + * + * This will ordered in the + * same manner as `getTelemetryObjects()` or + * `getResponse()`; that is, the metadata at a + * given index will correspond to the telemetry-providing + * domain object at the same index. + * @returns {TelemetryMetadata[]} an array of metadata objects + */ + TelemetrySubscription.prototype.getMetadata = function () { + return this.metadatas; + }; + + /** + * Get a promise for all telemetry-providing objects + * associated with this subscription. + * @returns {Promise.} a promise for + * telemetry-providing objects + * @memberof platform/telemetry.TelemetrySubscription# + */ + TelemetrySubscription.prototype.promiseTelemetryObjects = function () { + // Unsubscribe promise is available after objects + // are loaded. + return this.telemetryObjectPromise; + }; + return TelemetrySubscription; } ); + diff --git a/platform/telemetry/src/TelemetryTable.js b/platform/telemetry/src/TelemetryTable.js index d70febed2f..2cfcf8823d 100644 --- a/platform/telemetry/src/TelemetryTable.js +++ b/platform/telemetry/src/TelemetryTable.js @@ -32,43 +32,28 @@ define( * one large object, overwriting new values as necessary. Stands * in contrast to the TelemetryQueue, which will avoid overwriting * values. + * @memberof platform/telemetry * @constructor + * @implements {platform/telemetry.TelemetryPool} */ function TelemetryTable() { - var table; - - return { - /** - * Check if any value groups remain in this pool. - * @return {boolean} true if value groups remain - */ - isEmpty: function () { - return !table; - }, - /** - * Retrieve the next value group from this pool. - * This gives an object containing key-value pairs, - * where keys and values correspond to the arguments - * given to previous put functions. - * @return {object} key-value pairs - */ - poll: function () { - var t = table; - table = undefined; - return t; - }, - /** - * Put a key-value pair into the pool. - * @param {string} key the key to store the value under - * @param {*} value the value to store - */ - put: function (key, value) { - table = table || {}; - table[key] = value; - } - }; } + TelemetryTable.prototype.isEmpty = function () { + return !this.table; + }; + + TelemetryTable.prototype.poll = function () { + var t = this.table; + this.table = undefined; + return t; + }; + + TelemetryTable.prototype.put = function (key, value) { + this.table = this.table || {}; + this.table[key] = value; + }; + return TelemetryTable; } -); \ No newline at end of file +);