diff --git a/viewer/js/config/app.js b/viewer/js/config/app.js index dc36e5e1c..486259e58 100644 --- a/viewer/js/config/app.js +++ b/viewer/js/config/app.js @@ -31,9 +31,9 @@ 'viewer/_ConfigMixin', // manage the Configuration 'viewer/_LayoutMixin', // build and manage the Page Layout and User Interface 'viewer/_MapMixin', // build and manage the Map - 'viewer/_WidgetsMixin', // build and manage the Widgets + 'viewer/_WidgetsMixin' // build and manage the Widgets - 'viewer/_WebMapMixin' // for WebMaps + // 'viewer/_WebMapMixin' // for WebMaps //'config/_customMixin' ], function ( @@ -43,21 +43,34 @@ _ConfigMixin, _LayoutMixin, _MapMixin, - _WidgetsMixin, + _WidgetsMixin - _WebMapMixin + // _WebMapMixin //_MyCustomMixin ) { - var controller = new (declare([ - _ControllerBase, - _ConfigMixin, + var App = declare([ + + // add custom mixins here...note order may be important and + // overriding certain methods incorrectly may break the app + // First on the list are last called last, for instance the startup + // method on _ControllerBase is called FIRST, and _LayoutMixin is called LAST + // for the most part they are interchangeable, except _ConfigMixin + // and _ControllerBase + // _LayoutMixin, - _MapMixin, _WidgetsMixin, + // _WebMapMixin, + _MapMixin, + + // configMixin should be right before _ControllerBase so it is + // called first to initialize the config object + _ConfigMixin, - _WebMapMixin - ]))(); - controller.startup(); + // controller base needs to be last + _ControllerBase + ]); + var app = new App(); + app.startup(); }); })(); diff --git a/viewer/js/viewer/_ConfigMixin.js b/viewer/js/viewer/_ConfigMixin.js index 430a22f8f..10a7929fe 100644 --- a/viewer/js/viewer/_ConfigMixin.js +++ b/viewer/js/viewer/_ConfigMixin.js @@ -9,6 +9,42 @@ define([ ) { return declare(null, { + loadConfig: function (wait) { + + // this will be used to make any inherited methods 'wait' + var waitDeferred; + + if (wait) { + waitDeferred = new Deferred(); + + // if we need to wait for a previous deferred + // wait for it, + wait.then(lang.hitch(this, function () { + + // load the config + this.initConfigAsync().then(lang.hitch(this, function () { + + // do some stuff + this.initConfigSuccess(arguments); + + // resolve + waitDeferred.resolve(); + }), + lang.hitch(this, 'initConfigError') + ); + + })); + } else { + + waitDeferred = this.initConfigAsync(); + waitDeferred.then( + lang.hitch(this, 'initConfigSuccess'), + lang.hitch(this, 'initConfigError') + ); + } + // call any inherited methods or return a deferred + return this.inherited(arguments, [waitDeferred]) || waitDeferred; + }, initConfigAsync: function () { var returnDeferred = new Deferred(); @@ -30,10 +66,6 @@ define([ initConfigSuccess: function (config) { this.config = config; - - // in _WidgetsMixin - this.createWidgets(['loading']); - if (config.isDebug) { window.app = this; //dev only } @@ -43,18 +75,6 @@ define([ current: config.defaultMapClickMode, defaultMode: config.defaultMapClickMode }; - - // in _LayoutMixin - this.initLayout(); - - // in _WidgetsMixin - this.createWidgets(['layout']); - - // in _MapMixin - this.initMapAsync().then( - lang.hitch(this, 'initMapComplete'), - lang.hitch(this, 'initMapError') - ); }, initConfigError: function (err) { @@ -64,4 +84,4 @@ define([ }); } }); -}); \ No newline at end of file +}); diff --git a/viewer/js/viewer/_ControllerBase.js b/viewer/js/viewer/_ControllerBase.js index 3863c9ff2..20958b0d8 100644 --- a/viewer/js/viewer/_ControllerBase.js +++ b/viewer/js/viewer/_ControllerBase.js @@ -1,27 +1,86 @@ /*eslint no-console: 0*/ define([ 'dojo/_base/declare', - 'dojo/_base/lang' + 'dojo/_base/lang', + 'dojo/Deferred' ], function ( - declare, - lang + declare, lang, Deferred ) { return declare(null, { + /** + * A method run before anything else, can be inherited by mixins to + * load and process the config sync or async + * @return {undefined | Deferred} If the operation is async it should return + * a deferred, otherwise it should return the value of `this.inherited(arguments)` + */ + loadConfig: function () { + return this.inherited(arguments); + }, + /** + * A method run after the config is loaded but before startup is called + * on mixins + * @return {undefined | Deferred} If the operation is async it should return + * a deferred, otherwise it should return the value of `this.inherited(arguments)` + */ + postConfig: function () { + return this.inherited(arguments); + }, + /** + * Start the application mixin chain, once the + * startupDeferred is resolved + * @return {undefined} + */ startup: function () { - this.inherited(arguments); - // in _ConfigMixin - this.initConfigAsync().then( - lang.hitch(this, 'initConfigSuccess'), - lang.hitch(this, 'initConfigError') - ); + // cache the inherited + var inherited = this.getInherited(arguments); + + // load config and process it + this.startupDeferred = this.executeSync([ + this.loadConfig, + this.postConfig + ]); + + // wait for any loading to complete + this.startupDeferred.then(lang.hitch(this, function () { + + // start up the mixin chain + inherited.apply(this); + })); + }, + /** + * executes an array of asynchronous methods synchronously + * @param {Array} methods The array of functions to execute + * @param {Deferred} deferred A deferred created inside the method and resolved once all methods are complete + * @return {Deferred} A deferred resolved once all methods are executed + */ + executeSync: function (methods, deferred) { + deferred = deferred || new Deferred(); + + // if our list is empty, resolve the deferred and quit + if (!methods || !methods.length) { + deferred.resolve(); + return deferred; + } + + // execute and remove the method from the list + var result = lang.hitch(this, methods.splice(0, 1)[0])(); + + // execute our next function once this one completes + if (result) { + result.then(lang.hitch(this, 'executeSync', methods, deferred)); + } else { + this.executeSync(methods, deferred); + } + return deferred; + }, //centralized error handler handleError: function (options) { if (this.config.isDebug) { - if (typeof (console) === 'object') { + if (typeof(console) === 'object') { for (var option in options) { if (options.hasOwnProperty(option)) { console.log(option, options[option]); @@ -54,4 +113,4 @@ define([ return dest; } }); -}); \ No newline at end of file +}); diff --git a/viewer/js/viewer/_LayoutMixin.js b/viewer/js/viewer/_LayoutMixin.js index 7560e78e9..4940098d1 100644 --- a/viewer/js/viewer/_LayoutMixin.js +++ b/viewer/js/viewer/_LayoutMixin.js @@ -11,6 +11,7 @@ define([ 'dojo/dom-class', 'dojo/dom-geometry', 'dojo/sniff', + 'dojo/Deferred', 'put-selector', @@ -33,6 +34,7 @@ define([ domClass, domGeom, has, + Deferred, put, @@ -61,14 +63,24 @@ define([ } }, collapseButtons: {}, + postConfig: function () { + this.layoutDeferred = new Deferred(); + return this.inherited(arguments); + }, - initLayout: function () { + startup: function () { this.config.layout = this.config.layout || {}; this.addTopics(); this.addTitles(); this.detectTouchDevices(); this.initPanes(); + + this.mapDeferred.then(lang.hitch(this, 'createPanes')); + + // resolve the layout deferred + this.layoutDeferred.resolve(); + this.inherited(arguments); }, // add topics for subscribing and publishing @@ -171,7 +183,7 @@ define([ panes[key] = lang.mixin(this.defaultPanes[key], panes[key]); } } - // where to place the buttons + // where to place the buttons // either the center map pane or the outer pane? this.collapseButtonsPane = this.config.collapseButtonsPane || 'outer'; @@ -251,7 +263,10 @@ define([ } if (!suppressEvent) { - topic.publish('viewer/onTogglePane', {pane: id, show: show}); + topic.publish('viewer/onTogglePane', { + pane: id, + show: show + }); } } } diff --git a/viewer/js/viewer/_MapMixin.js b/viewer/js/viewer/_MapMixin.js index 48ebcc95c..71e658d0a 100644 --- a/viewer/js/viewer/_MapMixin.js +++ b/viewer/js/viewer/_MapMixin.js @@ -23,29 +23,51 @@ define([ return declare(null, { + postConfig: function () { + this.mapDeferred = new Deferred(); + return this.inherited(arguments); + }, + + startup: function () { + this.inherited(arguments); + this.layoutDeferred.then(lang.hitch(this, 'initMapAsync')); + }, + initMapAsync: function () { var returnDeferred = new Deferred(); var returnWarnings = []; - this._createMap(returnWarnings).then( + this.createMap(returnWarnings).then( lang.hitch(this, '_createMapResult', returnDeferred, returnWarnings) ); + returnDeferred.then(lang.hitch(this, 'initMapComplete')); return returnDeferred; }, - _createMap: function (returnWarnings) { + createMap: function (returnWarnings) { + + // mixins override the default createMap method and return a deferred + var result = this.inherited(arguments); + if (result) { + return result; + } + + // otherwise we can create the map var mapDeferred = new Deferred(), container = dom.byId(this.config.layout.map) || 'mapCenter'; - if (this.config.webMapId) { - if (this._initWebMap) { - mapDeferred = this._initWebMap(this.config.webMapId, container, this.config.webMapOptions); - } else { - returnWarnings.push('The "_WebMapMixin" Controller Mixin is required to use a webmap'); + this.map = new Map(container, this.config.mapOptions); + + // let some other mixins modify or add map items async + var wait = this.inherited(arguments); + if (wait) { + wait.then(function (warnings) { + if (warnings) { + returnWarnings = returnWarnings.concat(warnings); + } mapDeferred.resolve(returnWarnings); - } + }); } else { - this.map = new Map(container, this.config.mapOptions); mapDeferred.resolve(returnWarnings); } return mapDeferred; @@ -194,8 +216,6 @@ define([ } if (this.map) { - // in _WidgetsMixin - this.createWidgets(['map', 'layer']); this.map.on('resize', function (evt) { var pnt = evt.target.extent.getCenter(); @@ -204,11 +224,8 @@ define([ }, 100); }); - // in _LayoutsMixin - this.createPanes(); - - // in _WidgetsMixin - this.createWidgets(); + // resolve the map deferred + this.mapDeferred.resolve(this.map); } }, @@ -234,4 +251,4 @@ define([ } } }); -}); \ No newline at end of file +}); diff --git a/viewer/js/viewer/_WebMapMixin.js b/viewer/js/viewer/_WebMapMixin.js index b24e4d0d4..9a4fcd1d4 100644 --- a/viewer/js/viewer/_WebMapMixin.js +++ b/viewer/js/viewer/_WebMapMixin.js @@ -2,6 +2,7 @@ define([ 'dojo/_base/declare', 'dojo/_base/lang', 'dojo/_base/array', + 'dojo/dom', 'esri/arcgis/utils', 'esri/units', @@ -12,6 +13,7 @@ define([ declare, lang, array, + dom, arcgisUtils, units, @@ -19,14 +21,19 @@ define([ i18n ) { return declare(null, { + startup: function () { + this.inherited(arguments); + // this.mapDeferred.then(lang.hitch(this, '_initWebMap')); + }, - _initWebMap: function (webMapId, container, webMapOptions) { - webMapOptions = webMapOptions || {}; + createMap: function () { + var webMapOptions = this.config.webMapOptions || {}; if (!webMapOptions.mapOptions && this.config.mapOptions) { webMapOptions.mapOptions = this.config.mapOptions; } + var container = dom.byId(this.config.layout.map) || 'mapCenter'; - var mapDeferred = arcgisUtils.createMap(webMapId, container, webMapOptions); + var mapDeferred = arcgisUtils.createMap(this.config.webMapId, container, webMapOptions); mapDeferred.then(lang.hitch(this, function (response) { this.webMap = { clickEventHandle: response.clickEventHandle, @@ -227,4 +234,4 @@ define([ } } }); -}); \ No newline at end of file +}); diff --git a/viewer/js/viewer/_WidgetsMixin.js b/viewer/js/viewer/_WidgetsMixin.js index 9d300b577..8ab99d897 100644 --- a/viewer/js/viewer/_WidgetsMixin.js +++ b/viewer/js/viewer/_WidgetsMixin.js @@ -2,6 +2,8 @@ define([ 'dojo/_base/declare', 'dojo/_base/array', 'dojo/_base/lang', + 'dojo/promise/all', + 'dojo/Deferred', 'put-selector', @@ -15,6 +17,8 @@ define([ declare, array, lang, + promiseAll, + Deferred, put, @@ -34,6 +38,35 @@ define([ widgets: {}, widgetTypes: ['titlePane', 'contentPane', 'floating', 'domNode', 'invisible', 'map', 'layer', 'layout', 'loading'], + postConfig: function (wait) { + + var waitDeferred; + if (wait) { + waitDeferred = new Deferred(); + + wait.then(lang.hitch(this, function () { + // load loading widgets + promiseAll(this.createWidgets(['loading'])).then(waitDeferred.resolve); + })); + } else { + var deferreds = this.createWidgets(['loading']); + if (deferreds && deferreds.length) { + waitDeferred = promiseAll(deferreds); + } + } + + return this.inherited(arguments) || waitDeferred; + }, + startup: function () { + this.inherited(arguments); + if (this.mapDeferred) { + this.mapDeferred.then(lang.hitch(this, 'createWidgets', ['map', 'layer'])); + } + if (this.layoutDeferred) { + promiseAll([this.mapDeferred, this.layoutDeferred]) + .then(lang.hitch(this, 'createWidgets', null)); + } + }, createWidgets: function (widgetTypes) { var widgets = [], @@ -45,7 +78,7 @@ define([ var widget = lang.clone(this.config.widgets[key]); widget.widgetKey = widget.widgetKey || widget.id || key; if (widget.include && (!this.widgets[widget.widgetKey]) && (array.indexOf(widgetTypes, widget.type) >= 0)) { - widget.position = (typeof (widget.position) !== 'undefined') ? widget.position : 10000; + widget.position = (typeof(widget.position) !== 'undefined') ? widget.position : 10000; if ((widget.type === 'titlePane' || widget.type === 'contentPane') && !widget.placeAt) { widget.placeAt = 'left'; } @@ -82,10 +115,14 @@ define([ paneWidgets.sort(function (a, b) { return a.position - b.position; }); - + var deferreds = []; array.forEach(paneWidgets, function (paneWidget, i) { - this.widgetLoader(paneWidget, i); + var def = this.widgetLoader(paneWidget, i); + if (def) { + deferreds.push(def); + } }, this); + return deferreds; }, widgetLoader: function (widgetConfig, position) { @@ -100,7 +137,7 @@ define([ source: 'Controller', error: 'Widget type "' + widgetConfig.type + '" (' + widgetConfig.title + ') at position ' + position + ' is not supported.' }); - return; + return null; } if (position) { @@ -122,11 +159,19 @@ define([ } // 2 ways to use require to accommodate widgets that may have an optional separate configuration file - if (typeof (widgetConfig.options) === 'string') { - require([widgetConfig.options, widgetConfig.path], lang.hitch(this, 'createWidget', widgetConfig)); + var deferred = new Deferred(); + if (typeof(widgetConfig.options) === 'string') { + require([widgetConfig.options, widgetConfig.path], lang.hitch(this, function (options, WidgetClass) { + deferred.resolve(); + this.createWidget(widgetConfig, options, WidgetClass); + })); } else { - require([widgetConfig.path], lang.hitch(this, 'createWidget', widgetConfig, widgetConfig.options)); + require([widgetConfig.path], lang.hitch(this, function (WidgetClass) { + deferred.resolve(); + this.createWidget(widgetConfig, widgetConfig.options, WidgetClass); + })); } + return deferred; }, createWidget: function (widgetConfig, options, WidgetClass) { @@ -221,7 +266,7 @@ define([ options.id = parentId; } var placeAt = widgetConfig.placeAt; - if (typeof (placeAt) === 'string') { + if (typeof(placeAt) === 'string') { placeAt = this.panes[placeAt]; } if (!placeAt) { @@ -262,7 +307,7 @@ define([ var placeAt = widgetConfig.placeAt; if (!placeAt) { placeAt = this.panes.left; - } else if (typeof (placeAt) === 'string') { + } else if (typeof(placeAt) === 'string') { placeAt = this.panes[placeAt]; } if (placeAt) {