ECMWF.define('dashboard_widgets/basic_chart', ['basic_chart/parameter_select','basic_chart/time_navigator','basic_chart/model/product', 'dashboard_widgets/widget', 'basic_chart/basic_chart', 'container', 'chart/plugins/title_overlay', 'css!dashboard_widgets/basic_chart'], function($, ParameterSelect, TimeNavigator, BasicInstance){ var chart_options = { priority: 'normal', editable: false, show_latlong: false, show_title_overlay: true}; var product_select_handler; var projection_select_handler; $.widget('ui.basicChartDashboardWidget', $.ui.dashboardWidget, DashboardWidget.register('basicChartDashboardWidget', { factory: { title : 'New basic chart widget', kind : 'basic_chart', is_private: true, data : { product: 'public_plots' } }, options: { data: { }, has_help: false, collapsible: false, has_external: true, fixed_size: false, editable: false, messageBus: Bus() }, _init: function() { var self = this, o = this.options; $.ui.dashboardWidget.prototype._init.call(this); self.element.addClass('basicChartDashboardWidget'); // TODO this is widget bus - used? or do we always want the content bus? var bus = self.chartBus = self.element.messageBus(o.messageBus); var chart_bus = self.content.messageBus(); var save = false; self.old_basetime_requested = false; if(!o.data.projection){ o.data.projection = $.getUserSetting('dashboard-chart-projection', 'europe'); save = true; } var width = self.element.width() - 5; o.data.product.viewport = {width: width, thumbnail: true}; o.data.product.in_dashboard = true; // TODO can remove? self._product = BasicInstance.get_instance(o.data.product); self.content .basicChart($.extend(chart_options, { instance: self._product, messageBus: bus })) .thumbnailTools({selector: '.chart-clip'}) .thumbnailTools('add', [ ['ui-icon-play', function(){ bus.send('tool-animation-start'); }, 'Animate'], ['ui-icon-pause', function(){ bus.send('tool-animation-pause'); }, 'Pause animation'], ['ui-icon-stop', function(){ bus.send('tool-animation-stop'); }, 'Stop animation'], ['ui-icon-carat-1-w', function(){ bus.send('prev-step'); }, 'Previous time step'], ['ui-icon-carat-1-e', function(){ bus.send('next-step'); }, 'Next time step'], ['ui-icon-seek-prev', function(){ bus.send('prev-day'); }, 'Previous day'], ['ui-icon-seek-next', function(){ bus.send('next-day'); }, 'Next day'], ['ui-icon-arrowthickstop-1-w', function(){ bus.send('first-step'); }, 'First time step'], ['ui-icon-arrowthickstop-1-e', function(){ bus.send('last-step'); }, 'Last time step'], ['ui-icon-arrowthick-1-s', function(){ self.old_basetime_requested = true; bus.send('prev-base-time'); }, 'Previous base time'], ['ui-icon-arrowthick-1-n', function(){ bus.send('next-base-time'); }, 'Next base time'] ]); bus .listen('chart-content-update', function(_, msg) { if(msg.changed.get('all')){ //console.log('chart.save: ', msg.product.save()); if(this._false){ o.data.product = msg.product.save(); if(!self.editing){ self._emit_changed(); } } } }) .listen('chart-product-change', function(){ self._notify_content_changed(); }) .listen('chart-product-availability', function(_, avail) { //console.log("BEST BASE TIME ", avail.best_base_time, " OLD CYCLE ", avail.old_cycle); //if(o.active) self.content.chart("notifyNotLatest", !avail.best_base_time); if (avail.old_cycle && !self.old_basetime_requested && $.getUserPreference('dashboard_autoupdate')) { console.log('dashboardChartWidget.on_availability: auto_update enabled. updating...'); self.bring_up_to_date(); } else { self.out_of_date(avail.old_cycle); } //TODO clicking on disabled buttons is propogated to the chart in MSIE //TODO setting button state stops the tooltips from working (mouseover event no longer received) // (it looks like the button is destroyed and recreated? though the tooltips are not destroyed). // Tooltip is re-applied as a temporary workaround. /* self.content.find('.ui-icon-carat-1-w' ).parent() .modeTooltip('destroy') .button(avail.current.previous(avail.valid_time.values) ? 'enable' : 'disable') .modeTooltip({content: 'Previous time step'}); self.content.find('.ui-icon-carat-1-e' ).parent() .modeTooltip('destroy') .button(avail.current.next(avail.valid_time.values) ? 'enable' : 'disable') .modeTooltip({content: 'Next time step'}); self.content.find('.ui-icon-arrowthickstop-1-w').parent() .modeTooltip('destroy') .button(avail.current.previous(avail.valid_time.values) ? 'enable' : 'disable') .modeTooltip({content: 'First time step'}); self.content.find('.ui-icon-arrowthickstop-1-e').parent() .modeTooltip('destroy') .button(avail.current.next(avail.valid_time.values) ? 'enable' : 'disable') .modeTooltip({content: 'Last time step'}); self.content.find('.ui-icon-arrowthick-1-n' ).parent() .modeTooltip('destroy') .button(avail.old_cycle ? 'enable' : 'disable') .modeTooltip({content: 'Next base time'}); self.content.find('.ui-icon-arrowthick-1-s' ).parent() .modeTooltip('destroy') .button(avail.current.previousBaseTime(avail.base_time.values) ? 'enable' : 'disable') .modeTooltip({content: 'Previous base time'}); */ }) .listen('start-animation', function() { if(o.active) self.content.chart('animator').start(); }) .listen('stop-animation', function() { if(o.active) self.content.chart('animator').stop(); }); chart_bus .listen_once('chart-content-update', function(_, msg) { self._product.events.bind('parameters-changed', function(changed) { $.each(changed, function(i) { var v = self._product.variables[i]; v && v.prev && self._emit_changed(); }); }); }) .listen('size-changed', function(_, size) { self._update_height(); }); if(save){ setTimeout(function() { self._emit_changed(); },500); } }, bring_up_to_date: function() { var self = this; self.content.chart('showLatestCycle'); }, activate: function() { var self = this; $.ui.dashboardWidget.prototype.activate.call(this); console.log('dashboard.basicChart.activate'); self.content.data('uiChart') && self.content.basicChart('checkAvailability', true).basicChart('redraw'); self.content.css({height: ''}); // TODO this allows the charts to be visible when switching tabs but may not be the best way. setTimeout(function(){ self._update_height(); }, 500); }, deactivate: function() { var self = this, o = this.options, chart; o.active = false; console.log('dashboard.basicChart.deactivate'); if((chart = self.content.data('uiChart'))){ chart.animator && chart.animator().stop(); chart.checkAvailability(false); } }, destroy: function() { var self = this; /* enabling this increases memory usage in MSIE self.chartBus .forget('chart-content-update') .forget('chart-product-availability') .forget('start-animation') .forget('stop-animation'); */ this.element.removeClass('chartDashboardWidget'); this.content.chart('destroy'); self.chartBus.destroy(); //FIXME destroyed busses do not get removed. self.chartBus = null; $.ui.dashboardWidget.prototype.destroy.call(this); /* self.element.removeData('chartDashboardWidget'); var d = self.element.data(); for(var i in d){ console.log('* ' + i); } */ }, edit: function(enable, form) { var self = this; console.log('chart.edit', (enable !== false)); $.ui.dashboardWidget.prototype.edit.call(this, enable, form); self.content.chart('animator').stop; self.content.thumbnailTools('enable', enable === false) }, _on_preview_open: function(el, height, width) { var self = this, o = this.options; //var bus = Bus(el); o.messageBus.send('tool-animation-stop'); // copying the product is not really what we want but is quick. // The real desired behaviour is to scale the existing image to the new // size pending the new plot result. It requires the backend to pass // back the default image size when producing the thumbnail. /* var product = self._product .copy() .set_size({width: -1}); product.data.img.url = null; */ // create a derived Preview product. var controls = el.div('.controls'); self._chart_preview = { bus: el.messageBus(), // TODO change? //bus: controls.messageBus(), // temp chart: el.div() .basicChart({ instance: product, plugins: [ {mixin: ParameterSelect, options: {controls: controls}}, //{mixin: self.Tools, options: {parent: basic.controls}}, //{mixin: self.Canvas, options: {}}, {mixin: TimeNavigator, options: {element: el.div({id: 'time-navigator'})}} ] }) }; /* (self._chart_preview.nav = el.div()).timeNavigator({ messageBus: bus, ready: function(){ self._chart_preview.nav.find('.time-nav').trigger('layout', {width: self._chart_preview.chart.width() - 2}); } }); */ }, _on_preview_close: function () { var self = this; self._product.set_size({width: self.element.width() - 5, thumbnail: true}); console.log('widgets.basicChart.on_preview_close: ', self._chart_preview); self._chart_preview.chart.data('uiChart') && self._chart_preview.chart.chart('destroy'); //self._chart_preview.nav.remove(); var bus = self._chart_preview.bus; if(bus.listeners.listeners.length){ console.warn('widgets.basicChart._on_preview_close: bus still has listeners. applying workaround...'); bus.forgetAll(); } }, /* _get_form: function() { var self = this; function on_update(updated_item, selection){ var r = {}; r[updated_item._item.name] = selection; self.update_from_form(r); } product_select_handler = product_select_handler || $.newFormItemHandler(ECMWF.ObjectListHandler); product_select_handler.on_update = on_update; projection_select_handler = projection_select_handler || $.newFormItemHandler(ECMWF.ObjectListHandler); projection_select_handler.on_update = on_update; return [ { type: 'catalogue', subtype: 'product', title: 'Product', name: 'product', handler: product_select_handler}, { type: 'catalogue', subtype: 'projection', title: 'Projection', name: 'projection', handler: projection_select_handler} ]; }, */ open: function(){ var product = this.options.data.product.product; window.location.href = 'http://www.ecmwf.int/en/forecasts/charts/' + product.package + '/' + product.name; return this; }, /* _get_help_content: function () { var self = this, o = this.options; if(!self.content) return; var product = self.content.data('uiChart').product; return $.Deferred(function(dfd){ var layers = []; $.each(product.layerList(new Layer().is_visible).reverse(), function(){ var item = {}; item.promise = new Layer(this).get_help_content(item, product); layers.push(item); }); $.when.apply($, $.pluck(layers, 'promise')).then(function(){ var markup = []; $.each(layers, function(i, item) { markup.push("**" + item.title + " layer**:"); markup.push(item.help); markup.push(''); if(item.legend) { markup.push("![legend](" + item.legend + ")"); markup.push(''); } }); dfd.resolve({title: o.title, content: markup.join('\n')}); }); }).promise(); }, update_from_form: function(form){ var self = this, o = this.options; if(form){ if(form.projection){ console.log('dashboard.chart.update_from_form: projection=', form.projection); self.content.chart('setProjection', o.data.projection = form.projection); $.setUserSetting('dashboard-chart-projection', o.data.projection); } if(form.product){ console.log('dashboard.chart.update_from_form: product=', form.product); self.content.chart('setProduct', o.data.product = form.product); $.setUserSetting('dashboard-chart-product', o.data.projection); } } }, */ serialize: function() { return $.extend(true, $.ui.dashboardWidget.prototype.serialize.call(this), {data: {product: {values: this._product.as_json().product.values}}} ); }, get_state: function(state) { var self = this; return self.content.chart('getState', true); }, set_state: function(state) { var self = this; console.log('chart.STATE', state); self.content.chart('setState', state); }, get_bus: function() { return this.chartBus; }, share: function () { var o = this.options; Bus().send('product-send', {type: 'chart', product: o.data.product.name}); return this; }, on_show: function() { var self = this; self.content.resize(); return this; }, _update_height: function() { var self = this, o = this.options; var width = self.element.width(); var new_height = self.content.height(); if(width){ console.log('dashboard.basicChart._update_height: height=' + new_height); if(new_height !== self._height){ self.content.css({height: self._height = new_height}); self.content.triggerHandler('resize', {size: {height: new_height}}); this().send('height-changed', this.widget_height()); } } return self; }, // the height may be not be set. must subscribe to height-changed to get the real height. widget_height: function () { var self = this; var border_height = 2; var header_height = 24; if(!self._height) console.log('widgets.basicChart: _height not set'); return (self._height || self.content.height()) + border_height + header_height; }, /* widget_height_async: function () { var self = this; var border_height = 2; var header_height = 24; return $.Deferred(function(dfd){ setTimeout(function(){ var height = (self._height || self.content.height()) + border_height + header_height; dfd.resolve(height); }, 2000); }).promise(); }, */ content_height: function() { var self = this; return self.content.height(); } })); });