Commit 30cd92df4e9fa7ad0efa02eafd71124b36fc4dba
Exists in
master
and in
112 other branches
Fix merge conflicts
Showing
17 changed files
with
1458 additions
and
94 deletions
Show diff stats
.gitignore
25.3 KB
@@ -0,0 +1,18 @@ | @@ -0,0 +1,18 @@ | ||
1 | +<h1>Interop module</h1> | ||
2 | + | ||
3 | +<h2>SAMP</h2> | ||
4 | + | ||
5 | +<p>The SAMP module</p> | ||
6 | + | ||
7 | +<h2>Remote Data Base</h2> | ||
8 | + | ||
9 | +<p>The Remote Data Base module</p> | ||
10 | + | ||
11 | +<h2>EPN-TAP</h2> | ||
12 | + | ||
13 | +<p>This module aims to access remote data from EPN-TAP services.</p> | ||
14 | + | ||
15 | +<p>Fill the fields on the top of the window to apply a filter and click on <em>Get services</em> button, then select a service in the left panel.</p> | ||
16 | +<p>You can also launch this module from the AMDA tree, by selecting <em>Display EPN-TAP services</em> on the mission contextual menu:</p><br/> | ||
17 | + | ||
18 | +<img src="help/images/epntap_from_tree.png"> |
js/app/AmdaApp.js
@@ -96,6 +96,13 @@ Ext.define('amdaApp.AmdaApp', { | @@ -96,6 +96,13 @@ Ext.define('amdaApp.AmdaApp', { | ||
96 | source : 'amdaDesktop.InteropModule', | 96 | source : 'amdaDesktop.InteropModule', |
97 | useLauncher : true | 97 | useLauncher : true |
98 | }, | 98 | }, |
99 | + epntap : { | ||
100 | + id : 'epntap-win', | ||
101 | + icon : 'icon-interop', | ||
102 | + title : 'EPN-TAP data', | ||
103 | + source : 'amdaDesktop.EpnTapModule', | ||
104 | + useLauncher : false | ||
105 | + }, | ||
99 | info : { | 106 | info : { |
100 | id : 'info-win', | 107 | id : 'info-win', |
101 | icon : 'icon-information', | 108 | icon : 'icon-information', |
@@ -281,7 +288,7 @@ Ext.define('amdaApp.AmdaApp', { | @@ -281,7 +288,7 @@ Ext.define('amdaApp.AmdaApp', { | ||
281 | { name: 'Create/Modify parameter', iconCls: 'edit', module: 'param-win' }, | 288 | { name: 'Create/Modify parameter', iconCls: 'edit', module: 'param-win' }, |
282 | { name: 'Plot data', iconCls: 'plot', module: 'plot-win'}, | 289 | { name: 'Plot data', iconCls: 'plot', module: 'plot-win'}, |
283 | { name: 'Data mining', iconCls: 'search', module: 'search-win'}, | 290 | { name: 'Data mining', iconCls: 'search', module: 'search-win'}, |
284 | - { name: 'Statistics', iconCls: 'statistics', module: 'statistics-win'}, | 291 | + { name: 'Statistics', iconCls: 'statistics', module: 'statistics-win'}, |
285 | { name: 'Download data', iconCls: 'download_manager', module: 'down-win'}, | 292 | { name: 'Download data', iconCls: 'download_manager', module: 'down-win'}, |
286 | { name: 'Upload data', iconCls: 'mydata', module: 'up-win'}, | 293 | { name: 'Upload data', iconCls: 'mydata', module: 'up-win'}, |
287 | { name: 'Manage TimeTables', iconCls: 'timeTable', module: 'timetab-win' }, | 294 | { name: 'Manage TimeTables', iconCls: 'timeTable', module: 'timetab-win' }, |
@@ -0,0 +1,187 @@ | @@ -0,0 +1,187 @@ | ||
1 | +/** | ||
2 | + * Project : AMDA-NG | ||
3 | + * Name : EpnTapModule.js | ||
4 | + * @class amdaDesktop.EpnTapModule | ||
5 | + * @extends amdaDesktop.AmdaModule | ||
6 | + * @brief EpnTap Module controller definition | ||
7 | + * @author Nathanael Jourdane | ||
8 | + */ | ||
9 | + | ||
10 | +Ext.define('amdaDesktop.EpnTapModule', { | ||
11 | + | ||
12 | + extend: 'amdaDesktop.AmdaModule', | ||
13 | + requires: ['amdaUI.EpnTapUI'], | ||
14 | + contentId : 'EpnTapUI', | ||
15 | + | ||
16 | + /** The alias name of the module view. */ | ||
17 | + // uiType: 'panelEpnTap', | ||
18 | + uiType : 'panelEpnTap', | ||
19 | + | ||
20 | + /** The text displayed on the *help button* tooltip. */ | ||
21 | + helpTitle: 'Help on EPN-TAP Module', | ||
22 | + | ||
23 | + /** The name of the documentation file related to the module. */ | ||
24 | + helpFile : 'epnTapHelp', | ||
25 | + | ||
26 | + /** | ||
27 | + Module initialisation. Called the first time that the user open the epnTap module. | ||
28 | + */ | ||
29 | + init: function() { | ||
30 | + this.launcher = { | ||
31 | + text: this.title, | ||
32 | + iconCls: this.icon, | ||
33 | + handler: this.createWindow, | ||
34 | + scope: this | ||
35 | + }; | ||
36 | + }, | ||
37 | + | ||
38 | + /** | ||
39 | + Called each time the epntap module is loaded. | ||
40 | + - `target`: an array of 3 values: [target_name, dataproduct_type]; or null. | ||
41 | + */ | ||
42 | + loadTarget: function(filter) { | ||
43 | + this.aquireElements(); | ||
44 | + this.addListeners(); | ||
45 | + | ||
46 | + this.servicesStore.each(function(record) { | ||
47 | + record.set('nb_results', -1); | ||
48 | + }, this); | ||
49 | + this.granulesStore.removeAll(); | ||
50 | + | ||
51 | + if(filter) { | ||
52 | + this.targetNameCB.setRawValue(filter['targetName']); | ||
53 | + this.productTypeCB.select(filter['productType']); | ||
54 | + this.getServices(); | ||
55 | + } | ||
56 | + }, | ||
57 | + | ||
58 | + aquireElements: function() { | ||
59 | + // UI elements | ||
60 | + this.servicesGrid = Ext.getCmp('epnTapServicesGrid'); | ||
61 | + this.granulesGrid = Ext.getCmp('epnTapGranulesGrid'); | ||
62 | + this.productTypeCB = Ext.getCmp('epnTapProductTypeCB'); | ||
63 | + this.targetNameCB = Ext.getCmp('epnTapTargetNameCB'); | ||
64 | + this.timeSelector = Ext.getCmp('epnTapTimeSelector'); | ||
65 | + this.getBtn = Ext.getCmp('epnTapGetBtn'); | ||
66 | + | ||
67 | + // stores elements | ||
68 | + this.servicesStore = Ext.data.StoreManager.lookup('servicesStore'); | ||
69 | + this.granulesStore = Ext.data.StoreManager.lookup('granulesStore'); | ||
70 | + this.productTypesStore = Ext.data.StoreManager.lookup('productTypesStore'); | ||
71 | + this.targetNamesStore = Ext.data.StoreManager.lookup('targetNamesStore'); | ||
72 | + }, | ||
73 | + | ||
74 | + addListeners: function() { | ||
75 | + this.targetNameCB.on('change', function() { | ||
76 | + this.updateGetBtnStatus(); | ||
77 | + }, this); | ||
78 | + | ||
79 | + this.productTypeCB.on('change', function() { | ||
80 | + this.updateGetBtnStatus(); | ||
81 | + }, this); | ||
82 | + | ||
83 | + this.servicesGrid.on('cellclick', function(grid, td, cellIndex, record) { | ||
84 | + this.onServiceSelected(record); | ||
85 | + }, this); | ||
86 | + | ||
87 | + this.getBtn.on('click', function() { | ||
88 | + this.getServices(); | ||
89 | + }, this); | ||
90 | + }, | ||
91 | + | ||
92 | + /********************** | ||
93 | + *** Utils functions *** | ||
94 | + **********************/ | ||
95 | + | ||
96 | + updateGetBtnStatus: function() { | ||
97 | + var shouldEnabled = this.targetNameCB.rawValue.length > 0 && this.productTypeCB.rawValue.length > 0; | ||
98 | + if(shouldEnabled) { | ||
99 | + this.getBtn.enable(); | ||
100 | + } else { | ||
101 | + this.getBtn.disable(); | ||
102 | + } | ||
103 | + }, | ||
104 | + | ||
105 | + /************* | ||
106 | + *** Events *** | ||
107 | + *************/ | ||
108 | + | ||
109 | + /** | ||
110 | + Trigerred when the 'Get results' button is clicked. | ||
111 | + */ | ||
112 | + getServices: function() { | ||
113 | + this.granulesStore.removeAll(); | ||
114 | + var targetName = this.targetNameCB.rawValue; | ||
115 | + var productTypes = this.productTypeCB.value.join(';'); | ||
116 | + var timeMin = Ext.Date.format(this.timeSelector.getStartTime(), 'd/m/Y H:i:s'); // start time | ||
117 | + var timeMax = Ext.Date.format(this.timeSelector.getStopTime(), 'd/m/Y H:i:s'); // stop time | ||
118 | + | ||
119 | + loadMask.show(); | ||
120 | + this.servicesStore.each(function(record) { | ||
121 | + // TODO: use store.load() method instead and add 'success' and 'enable' columns in the store | ||
122 | + Ext.Ajax.request({ | ||
123 | + url: 'php/epntap.php', | ||
124 | + method: 'GET', | ||
125 | + headers: {'Content-Type': 'application/json'}, | ||
126 | + params: { | ||
127 | + 'action': 'getNbResults', | ||
128 | + 'serviceId': record.data['id'], | ||
129 | + 'url': record.data['access_url'], | ||
130 | + 'tableName': record.data['table_name'], | ||
131 | + 'targetName': targetName, | ||
132 | + 'productTypes': productTypes, | ||
133 | + 'timeMin': timeMin, | ||
134 | + 'timeMax': timeMax | ||
135 | + }, | ||
136 | + // timeout: 3000, | ||
137 | + success: function(response, options) { | ||
138 | + var record = this.servicesStore.getById(options.params['serviceId']); | ||
139 | + var responseObj = Ext.decode(response.responseText); | ||
140 | + this.updateService(record, responseObj['success'] ? responseObj['data'] : -2, responseObj['msg']); | ||
141 | + }, | ||
142 | + failure: function(response, options) { | ||
143 | + var record = this.servicesStore.getById(options.params['serviceId']); | ||
144 | + this.updateService(record, -1, response.statusText); | ||
145 | + }, | ||
146 | + scope: this | ||
147 | + }); | ||
148 | + }, this); | ||
149 | + }, | ||
150 | + | ||
151 | + /** | ||
152 | + Update the nb_result field of the services store (see `EpnTapUI.servicesStore`), according to the field values in `serviceFilterPanel`. | ||
153 | + */ | ||
154 | + updateService: function(record, nbRes, info) { | ||
155 | + record.set('nb_results', nbRes); | ||
156 | + record.set('info', info); | ||
157 | + this.servicesStore.sort(); | ||
158 | + loadMask.hide(); | ||
159 | + }, | ||
160 | + | ||
161 | + /** | ||
162 | + Trigerred when a row is clicked in `servicesGrid` table (see `EpnTapUI.createServicesGrid()`). Among other things, | ||
163 | + send a new query and fill `granulesGrid`. | ||
164 | + */ | ||
165 | + onServiceSelected: function(record) { | ||
166 | + this.servicesStore.each(function(record) { | ||
167 | + record.set('selected', false); | ||
168 | + }, this); | ||
169 | + record.set('selected', true); | ||
170 | + Ext.Ajax.suspendEvent('requestexception'); | ||
171 | + Ext.Ajax.abortAll(); | ||
172 | + var nbRes = record.get('nb_results'); | ||
173 | + | ||
174 | + if(nbRes > 0 && !isNaN(nbRes)) { // TODO replace !isNaN(nbRes) by this.selectedService.get('success') | ||
175 | + this.granulesStore.load({ | ||
176 | + params: {'action': 'getGranules'}, | ||
177 | + callback: function (records, operation, success) { | ||
178 | + Ext.Ajax.resumeEvents('requestexception'); | ||
179 | + // console.log(Ext.decode(operation.response.responseText)); | ||
180 | + }, | ||
181 | + start: 0, | ||
182 | + limit: this.granulesStore.pageSize, | ||
183 | + scope: this | ||
184 | + }); | ||
185 | + } | ||
186 | + } | ||
187 | +}); |
js/app/controllers/InteropModule.js
@@ -7,7 +7,7 @@ | @@ -7,7 +7,7 @@ | ||
7 | * @author Benjamin RENARD | 7 | * @author Benjamin RENARD |
8 | * $Id: InteropModule.js 1870 2013-11-22 13:43:34Z elena $ | 8 | * $Id: InteropModule.js 1870 2013-11-22 13:43:34Z elena $ |
9 | ***************************************************************************** | 9 | ***************************************************************************** |
10 | - * FT Id : Date : Name - Description | 10 | + * FT Id : Date : Name - Description |
11 | ******************************************************************************* | 11 | ******************************************************************************* |
12 | * 23/04/2012: BRE - file creation | 12 | * 23/04/2012: BRE - file creation |
13 | */ | 13 | */ |
@@ -18,7 +18,8 @@ Ext.define('amdaDesktop.InteropModule', { | @@ -18,7 +18,8 @@ Ext.define('amdaDesktop.InteropModule', { | ||
18 | 18 | ||
19 | requires: [ | 19 | requires: [ |
20 | 'amdaUI.InteropUI', | 20 | 'amdaUI.InteropUI', |
21 | - 'amdaDesktop.SampModule' | 21 | + 'amdaDesktop.SampModule', |
22 | + 'amdaDesktop.EpnTapModule' | ||
22 | ], | 23 | ], |
23 | 24 | ||
24 | contentId : 'interopUI', | 25 | contentId : 'interopUI', |
@@ -27,10 +28,11 @@ Ext.define('amdaDesktop.InteropModule', { | @@ -27,10 +28,11 @@ Ext.define('amdaDesktop.InteropModule', { | ||
27 | * @cfg {String} window definitions | 28 | * @cfg {String} window definitions |
28 | * @required | 29 | * @required |
29 | */ | 30 | */ |
30 | - height: 580, | ||
31 | - width: 850, | 31 | + height: 650, |
32 | + width: 1050, | ||
32 | uiType : 'panelInterop', | 33 | uiType : 'panelInterop', |
33 | helpTitle :'Help on Interop Module', | 34 | helpTitle :'Help on Interop Module', |
35 | + helpFile: 'interopHelp.html', | ||
34 | 36 | ||
35 | samp : null, | 37 | samp : null, |
36 | 38 | ||
@@ -237,6 +239,13 @@ Ext.define('amdaDesktop.InteropModule', { | @@ -237,6 +239,13 @@ Ext.define('amdaDesktop.InteropModule', { | ||
237 | this.samp.sendFITS(url,name); | 239 | this.samp.sendFITS(url,name); |
238 | }, | 240 | }, |
239 | 241 | ||
242 | + loadEpnTap: function(filter) { | ||
243 | + if(!this.epntap) { | ||
244 | + this.epntap = Ext.create('amdaDesktop.EpnTapModule'); | ||
245 | + } | ||
246 | + this.epntap.loadTarget(filter); | ||
247 | + }, | ||
248 | + | ||
240 | generateAladinScript : function(urlList, scriptType) | 249 | generateAladinScript : function(urlList, scriptType) |
241 | { | 250 | { |
242 | /*var script="reset;"; //reset all views & all planes | 251 | /*var script="reset;"; //reset all views & all planes |
@@ -313,8 +322,7 @@ Ext.define('amdaDesktop.InteropModule', { | @@ -313,8 +322,7 @@ Ext.define('amdaDesktop.InteropModule', { | ||
313 | 322 | ||
314 | var script = 'reset;'; | 323 | var script = 'reset;'; |
315 | 324 | ||
316 | - for( var i=0; i < urlList.length; i++) | ||
317 | - { | 325 | + for( var i=0; i < urlList.length; i++) { |
318 | var url = urlList[i].url; | 326 | var url = urlList[i].url; |
319 | var name = urlList[i].name; | 327 | var name = urlList[i].name; |
320 | script += 'get File(' + url +','+name+');'; | 328 | script += 'get File(' + url +','+name+');'; |
@@ -421,6 +429,7 @@ Ext.define('amdaDesktop.InteropModule', { | @@ -421,6 +429,7 @@ Ext.define('amdaDesktop.InteropModule', { | ||
421 | } | 429 | } |
422 | var desktop = this.app.getDesktop(); | 430 | var desktop = this.app.getDesktop(); |
423 | var win = desktop.getWindow(this.id); | 431 | var win = desktop.getWindow(this.id); |
432 | + var activeTab = (config && 'activeTab' in config) ? config['activeTab']: 1; | ||
424 | 433 | ||
425 | activeTab = 1; | 434 | activeTab = 1; |
426 | 435 | ||
@@ -430,9 +439,9 @@ Ext.define('amdaDesktop.InteropModule', { | @@ -430,9 +439,9 @@ Ext.define('amdaDesktop.InteropModule', { | ||
430 | id: this.id, | 439 | id: this.id, |
431 | title:this.title, | 440 | title:this.title, |
432 | layout: 'anchor', | 441 | layout: 'anchor', |
433 | - width:600, | ||
434 | - height:550, | ||
435 | - modal: true, | 442 | + width: 800, |
443 | + height: 600, | ||
444 | + minWidth: 650, | ||
436 | minimizable: false, | 445 | minimizable: false, |
437 | iconCls: this.icon, | 446 | iconCls: this.icon, |
438 | animCollapse:false, | 447 | animCollapse:false, |
@@ -441,12 +450,27 @@ Ext.define('amdaDesktop.InteropModule', { | @@ -441,12 +450,27 @@ Ext.define('amdaDesktop.InteropModule', { | ||
441 | stateful : true, | 450 | stateful : true, |
442 | stateId : this.id, | 451 | stateId : this.id, |
443 | stateEvents: ['move','show','resize'], | 452 | stateEvents: ['move','show','resize'], |
453 | + tools: [{ | ||
454 | + type:'help', | ||
455 | + qtip: this.helpTitle, | ||
456 | + scope:this, | ||
457 | + handler: function(event, toolEl, panel) { | ||
458 | + myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.info.id, true, function(module) { | ||
459 | + module.createWindow(me.helpFile, me.helpTitle); | ||
460 | + }); | ||
461 | + } | ||
462 | + }], | ||
444 | items : [ | 463 | items : [ |
445 | { | 464 | { |
446 | xtype: 'panelInterop', | 465 | xtype: 'panelInterop', |
447 | clientsStore : this.sampclientsStore, | 466 | clientsStore : this.sampclientsStore, |
448 | activeTab : activeTab, | 467 | activeTab : activeTab, |
449 | baseId : baseId, | 468 | baseId : baseId, |
469 | + loadTab: function(tab) { | ||
470 | + if(tab['id'] === 'epntapTab') { | ||
471 | + me.loadEpnTap(config && 'epntapFilter' in config ? config['epntapFilter']: false); | ||
472 | + } | ||
473 | + }, | ||
450 | onSwitchConnect : function () | 474 | onSwitchConnect : function () |
451 | { | 475 | { |
452 | me.switchSampConnect(); | 476 | me.switchSampConnect(); |
js/app/models/AmdaNode.js
@@ -135,6 +135,9 @@ Ext.define('amdaModel.AmdaNode', { | @@ -135,6 +135,9 @@ Ext.define('amdaModel.AmdaNode', { | ||
135 | else if (this.get('isParameter')) { | 135 | else if (this.get('isParameter')) { |
136 | itemKind = amdaUI.ExplorerUI.ITEM_KIND_PARA; | 136 | itemKind = amdaUI.ExplorerUI.ITEM_KIND_PARA; |
137 | } | 137 | } |
138 | + else if (this.get('rank')) { | ||
139 | + itemKind = amdaUI.ExplorerUI.ITEM_KIND_MISS; | ||
140 | + } | ||
138 | // other case | 141 | // other case |
139 | else { | 142 | else { |
140 | itemKind = amdaUI.ExplorerUI.ITEM_KIND_DIRE; | 143 | itemKind = amdaUI.ExplorerUI.ITEM_KIND_DIRE; |
js/app/models/LocalParamNode.js
@@ -98,6 +98,10 @@ Ext.define('amdaModel.LocalParamNode', | @@ -98,6 +98,10 @@ Ext.define('amdaModel.LocalParamNode', | ||
98 | text : 'Close All', | 98 | text : 'Close All', |
99 | hidden : true | 99 | hidden : true |
100 | }, { | 100 | }, { |
101 | + fnId : 'miss-collapseAll', | ||
102 | + text : 'Close All', | ||
103 | + hidden : true | ||
104 | + }, { | ||
101 | fnId : 'para-plotParam', | 105 | fnId : 'para-plotParam', |
102 | text : 'Plot Parameter', | 106 | text : 'Plot Parameter', |
103 | hidden : true | 107 | hidden : true |
@@ -121,6 +125,10 @@ Ext.define('amdaModel.LocalParamNode', | @@ -121,6 +125,10 @@ Ext.define('amdaModel.LocalParamNode', | ||
121 | fnId : 'leaf-downParam', | 125 | fnId : 'leaf-downParam', |
122 | text : 'Download Parameter', | 126 | text : 'Download Parameter', |
123 | hidden : true | 127 | hidden : true |
128 | + }, { | ||
129 | + fnId : 'miss-epnTap', | ||
130 | + text : 'Display EPN-TAP services', | ||
131 | + hidden : true | ||
124 | }]; | 132 | }]; |
125 | 133 | ||
126 | return menuItems; | 134 | return menuItems; |
@@ -157,6 +165,9 @@ Ext.define('amdaModel.LocalParamNode', | @@ -157,6 +165,9 @@ Ext.define('amdaModel.LocalParamNode', | ||
157 | myDesktopApp.warningMsg("Sorry! access to this parameter is restricted"); | 165 | myDesktopApp.warningMsg("Sorry! access to this parameter is restricted"); |
158 | 166 | ||
159 | break; | 167 | break; |
168 | + case 'miss-epnTap': | ||
169 | + this.displayEpnTap(); | ||
170 | + break; | ||
160 | default: | 171 | default: |
161 | break; | 172 | break; |
162 | } | 173 | } |
@@ -245,5 +256,24 @@ Ext.define('amdaModel.LocalParamNode', | @@ -245,5 +256,24 @@ Ext.define('amdaModel.LocalParamNode', | ||
245 | isParameter : function() | 256 | isParameter : function() |
246 | { | 257 | { |
247 | return this.get('isParameter'); | 258 | return this.get('isParameter'); |
259 | + }, | ||
260 | + | ||
261 | + displayEpnTap: function() { | ||
262 | + var icons = { | ||
263 | + 'icon-mercury': 'Mercury', | ||
264 | + 'icon-venus': 'Venus', | ||
265 | + 'icon-earth': 'Earth', | ||
266 | + 'icon-mars': 'Mars', | ||
267 | + 'icon-jupiter': 'Jupiter', | ||
268 | + 'icon-saturn': 'Saturn', | ||
269 | + 'icon-comet': 'Comet', | ||
270 | + } | ||
271 | + var filter = {'productType': 'all'}; | ||
272 | + filter['targetName'] = this.get('iconCls') in icons ? icons[this.get('iconCls')] : ''; | ||
273 | + filter['start'] = this.get('globalStart') ? this.get('globalStart') : ''; | ||
274 | + filter['stop'] = this.get('globalStop') ? this.get('globalStop') : ''; | ||
275 | + myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.interop.id, true, function (module) { | ||
276 | + module.createWindow({'activeTab': 2, 'epntapFilter': filter}); | ||
277 | + }); | ||
248 | } | 278 | } |
249 | }); | 279 | }); |
@@ -0,0 +1,638 @@ | @@ -0,0 +1,638 @@ | ||
1 | +/** | ||
2 | + * Project: AMDA-NG | ||
3 | + * Name: EpnTapUI.js | ||
4 | + * @class amdaUI.EpnTapUI | ||
5 | + * @extends Ext.tab.Panel | ||
6 | + * @author Nathanael JOURDANE | ||
7 | + * 24/10/2016: file creation | ||
8 | + */ | ||
9 | + | ||
10 | +/** | ||
11 | +`productTypesStore`: An ExtJS Store containing the list of the different data product types defined on all granules, on | ||
12 | +all available EPN-TAP services (defined in `generic_data/EpnTapData/metadata.json`, updated periodically with a cron | ||
13 | +script). | ||
14 | + | ||
15 | +This list is used to fill the `productTypeCB` combo box, which is initilized in `EpnTapModule` at the panel creation. | ||
16 | + | ||
17 | +- `id`: the data product type IDs, according to the EPN-TAP specification (see | ||
18 | + https://voparis-confluence.obspm.fr/pages/viewpage.action?pageId=1148225); | ||
19 | +- `name`: the data product name, according to the EPN-TAP specification (ibid). | ||
20 | + | ||
21 | +These IDs and names are hard-defined in the JSon file `generic_data/EpnTapData/dataproduct_types.json`. | ||
22 | + | ||
23 | +Notes: | ||
24 | +- if a granule contains a data product type which is not conform to the EPN-TAP definition (ibid), it is not displayed | ||
25 | +in this store and an information message is displayed on the JavaScript console during the panel creation. | ||
26 | +- if a data product type is not present in any of the granules from the EPN-TAP services, it is not present in this | ||
27 | +store. | ||
28 | +*/ | ||
29 | +Ext.create('Ext.data.Store', { | ||
30 | + storeId: 'productTypesStore', | ||
31 | + autoLoad: true, | ||
32 | + fields: ['id', 'name', 'desc'], | ||
33 | + data: [ | ||
34 | + {'id': 'all', 'name': '--All--', 'desc': 'Select all produt types.'}, | ||
35 | + {'id': 'clear', 'name': '--Clear--', 'desc': 'Clear the selection.'}, | ||
36 | + {'id': 'im', 'name': 'Image', 'desc': '2D series of values depending on 2 spatial axes, with measured parameters.'}, | ||
37 | + {'id': 'ma', 'name': 'Map', 'desc': '2D series of values depending on 2 spatial axes, with derived parameters.'}, | ||
38 | + {'id': 'sp', 'name': 'Spectrum', 'desc': '1D series of values depending on a spectral axis (or Frequency, Energy, Mass,...).'}, | ||
39 | + {'id': 'ds', 'name': 'Dynamic spectrum', 'desc': '2D series of values depending on time and on a spectral axis (Frequency, Energy, Mass,...), FoV is homogeneous.'}, | ||
40 | + {'id': 'sc', 'name': 'Spectral cube', 'desc': '3D series of values depending on 2 spatial axes and on a spectral axis (Frequency, Energy, Mass,..).'}, | ||
41 | + {'id': 'pr', 'name': 'Profile', 'desc': '1D series of values depending on a spatial axis.'}, | ||
42 | + {'id': 'vo', 'name': 'Volume', 'desc': '3D series of values depending on 3 spatial axes (spatial coordinates or tabulated values in a volumic grid).'}, | ||
43 | + {'id': 'mo', 'name': 'Movie', 'desc': '3D series of values depending on 2 spatial axes and on time.'}, | ||
44 | + // {'id': 'cu', 'name': 'Cube', 'desc': '.'}, | ||
45 | + {'id': 'ts', 'name': 'Time series', 'desc': '1D series of values depending on time.'}, | ||
46 | + {'id': 'ca', 'name': 'Catalogue', 'desc': '1D list of elements.'}, | ||
47 | + {'id': 'ci', 'name': 'Catalogue item', 'desc': '0D list of elements.'} | ||
48 | + ] | ||
49 | +}); | ||
50 | + | ||
51 | +/** | ||
52 | +`targetNamesStore`: An ExtJS Store containing the list of the different target names defined on all granules, on | ||
53 | +all available EPN-TAP services (defined in `generic_data/EpnTapData/metadata.json`, updated periodically with a cron | ||
54 | +script), which match with the selected data product and target class. | ||
55 | + | ||
56 | +This list is used to fill the `targetNameCB` combo box, which is updated by `EpnTapModule` each time a new target class | ||
57 | +(or, by transitivity, product type) is selected. | ||
58 | + | ||
59 | +- `id`: the target name in lowercase, with the underscore between each word; | ||
60 | +- `name`: the target name, capitalized with spaces between each word (done `EpnTapModule.prettify()`). | ||
61 | +*/ | ||
62 | +Ext.create('Ext.data.Store', { | ||
63 | + storeId: 'targetNamesStore', | ||
64 | + fields: ['id', 'name', 'type', 'parent', 'aliases'], | ||
65 | + proxy: { | ||
66 | + type: 'ajax', | ||
67 | + url: 'php/epntap.php', | ||
68 | + extraParams: { action: 'resolver' } | ||
69 | + // listeners: { | ||
70 | + // exception: function(proxy, response, operation) { | ||
71 | + // console.log('Error ', response); //TODO: Use ExtJs alert instead | ||
72 | + // } | ||
73 | + // } | ||
74 | + } | ||
75 | +}); | ||
76 | + | ||
77 | +/** | ||
78 | +`servicesStore`: An ExtJS Store containing the list of the EPN-TAP services (defined in | ||
79 | +`generic_data/EpnTapData/metadata.json`, updated periodically with a cron script), which contains at least one granule | ||
80 | +matching with the granules filter (the selected data product type, target class and target name). | ||
81 | + | ||
82 | +This list is used to fill the `servicesGrid` table, which is updated by `EpnTapModule` each time a new target name | ||
83 | +(or, by transitivity, target class or product type) is selected. | ||
84 | + | ||
85 | +- `id`: the database name of the service, according to the `table_name` column from the `rr.res_table` in the | ||
86 | + registry database; | ||
87 | +- `nbResults`: the number of granules matching with the granules filter for this service; | ||
88 | +- `shortName`: the service short name, according to the `short_name` column from the `rr.resource` table in the registry | ||
89 | + database; | ||
90 | +- `title`: the service title, according to the `res_title` column from the `rr.resource` table in the registry database; | ||
91 | +- `accessURL`: the service access URL, according to the `access_url` column from the `rr.interface` table in the | ||
92 | + registry database. | ||
93 | +*/ | ||
94 | +Ext.create('Ext.data.Store', { | ||
95 | + storeId: 'servicesStore', | ||
96 | + autoLoad: true, | ||
97 | + fields: [ | ||
98 | + {name: 'id', type: 'string'}, | ||
99 | + {name: 'short_name', type: 'string'}, | ||
100 | + {name: 'res_title', type: 'string'}, | ||
101 | + {name: 'ivoid', type: 'string'}, | ||
102 | + {name: 'access_url', type: 'string'}, | ||
103 | + {name: 'table_name', type: 'string'}, | ||
104 | + {name: 'content_type', type: 'string'}, | ||
105 | + {name: 'creator_seq', type: 'string'}, | ||
106 | + {name: 'content_level', type: 'string'}, | ||
107 | + {name: 'reference_url', type: 'string'}, | ||
108 | + {name: 'created', type: 'date', dateFormat: 'c'}, | ||
109 | + {name: 'updated', type: 'date', dateFormat: 'c'}, | ||
110 | + {name: 'nb_results', type: 'integer'}, | ||
111 | + {name: 'info', type: 'string'}, | ||
112 | + {name: 'selected', type: 'boolean'} | ||
113 | + ], | ||
114 | + proxy: { | ||
115 | + type: 'ajax', | ||
116 | + url: 'php/epntap.php', | ||
117 | + extraParams : {action: 'getServices'} | ||
118 | + }, | ||
119 | + sorters: [ | ||
120 | + {property: 'nb_results', direction: 'DESC'}, | ||
121 | + {property: 'short_name', direction: 'ASC'} | ||
122 | + ], | ||
123 | + listeners: { | ||
124 | + // load: function(record) { console.log(record); } | ||
125 | + } | ||
126 | +}); | ||
127 | + | ||
128 | +/** | ||
129 | +`granulesStore`: An ExtJS Store containing the list of granules of the selected service (on `servicesGrid`), which match | ||
130 | +with the granules filter (the selected data product type, target class and target name). | ||
131 | + | ||
132 | +This list is used to fill the `granulesGrid` table, which is updated by `EpnTapModule` each time a new service is | ||
133 | +selected. | ||
134 | + | ||
135 | +- `num`: the line number, according to the order of the query response and the current page (see `currentPageLb`); | ||
136 | +- `dataproduct_type`: the dataproduct_type EPN-TAP parameter, as defined in | ||
137 | + https://voparis-confluence.obspm.fr/display/VES/EPN-TAP+V2.0+parameters. | ||
138 | +- `target_name`: the target_name EPN-TAP parameter (ibid); | ||
139 | +- `time_min`: the time_min EPN-TAP parameter (ibid); | ||
140 | +- `time_max`: the time_max EPN-TAP parameter (ibid); | ||
141 | +- `access_format`: the access_format EPN-TAP parameter (ibid); | ||
142 | +- `granule_uid`: the granule_uid EPN-TAP parameter (ibid); | ||
143 | +- `access_estsize`: the access_estsize EPN-TAP parameter (ibid); | ||
144 | +- `access_url`: the access_url EPN-TAP parameter (ibid); | ||
145 | +- `thumbnail_url`: the thumbnail_url EPN-TAP parameter (ibid). | ||
146 | +*/ | ||
147 | +Ext.create('Ext.data.Store', { | ||
148 | + storeId: 'granulesStore', | ||
149 | + model: 'granulesModel', // Created dynamically | ||
150 | + buffered: true, | ||
151 | + leadingBufferZone: 50, | ||
152 | + autoload: false, | ||
153 | + pageSize: 50, | ||
154 | + proxy: { | ||
155 | + type: 'ajax', | ||
156 | + url: 'php/epntap.php', | ||
157 | + reader: { type: 'json', root: 'data'}, | ||
158 | + }, listeners: { | ||
159 | + 'beforeprefetch': function(store, operation) { | ||
160 | + var servicesStore = Ext.data.StoreManager.lookup('servicesStore'); | ||
161 | + var service = servicesStore.getAt(servicesStore.findExact('selected', true)).data; | ||
162 | + store.getProxy().extraParams = { | ||
163 | + 'action': 'getGranules', | ||
164 | + 'url': service['access_url'], | ||
165 | + 'tableName': service['table_name'], | ||
166 | + 'targetName': Ext.getCmp('epnTapTargetNameCB').rawValue, | ||
167 | + 'productTypes': Ext.getCmp('epnTapProductTypeCB').value.join(';'), | ||
168 | + 'timeMin': Ext.Date.format(Ext.getCmp('epnTapTimeSelector').getStartTime(), 'd/m/Y H:i:s'), | ||
169 | + 'timeMax': Ext.Date.format(Ext.getCmp('epnTapTimeSelector').getStopTime(), 'd/m/Y H:i:s'), | ||
170 | + 'nbRes': service['nb_results'] | ||
171 | + }; | ||
172 | + }, | ||
173 | + // 'prefetch': function(store, records, successful, operation) { | ||
174 | + // console.log('(prefetch) operation ' + (successful ? 'success' : 'failed') + ': ', operation); | ||
175 | + // }, | ||
176 | + 'metachange': function(store, meta) { | ||
177 | + Ext.getCmp('epnTapGranulesGrid').reconfigure(store, meta.columns); | ||
178 | + } | ||
179 | + } | ||
180 | +}); | ||
181 | + | ||
182 | +/** | ||
183 | +Error are not displayed here, use try/catch each time it's necessary. | ||
184 | +*/ | ||
185 | +Ext.define('App.util.Format', { | ||
186 | + override: 'Ext.util.Format', | ||
187 | + | ||
188 | + // Utils | ||
189 | + | ||
190 | + 'prettify': function(data) { | ||
191 | + return data.charAt(0).toUpperCase() + data.replace(/_/g, ' ').substr(1).toLowerCase(); | ||
192 | + }, | ||
193 | + | ||
194 | + // Services grid | ||
195 | + | ||
196 | + 'serviceTooltip': function(value, data) { | ||
197 | + for (var key in data) { | ||
198 | + if(typeof data[key] == 'string' && data[key] != '') { | ||
199 | + data[key] = data[key].replace(/'/g, ''').replace(/"/g, '"'); | ||
200 | + } | ||
201 | + } | ||
202 | + var infoColor = data.nb_results == -2 ? 'IndianRed' : 'green'; | ||
203 | + var info = data.info.length > 0 ? '<p style="color:' + infoColor + '">' + data.info + '</p>' : ''; | ||
204 | + | ||
205 | + var colums = ['res_title', 'ivoid', 'access_url', 'table_name', 'content_type', 'creator_seq', 'content_level', 'reference_url', 'created', 'updated']; | ||
206 | + var details = ''; | ||
207 | + for (var key in colums) { | ||
208 | + if(data[colums[key]] !== '') { | ||
209 | + var val = colums[key] === 'content_level' ? data[colums[key]].replace(/#/g, ', ') : data[colums[key]]; | ||
210 | + details += '<li><b>' + Ext.util.Format.prettify(colums[key]) + '</b>: ' + val + '</li>'; | ||
211 | + } | ||
212 | + } | ||
213 | + return Ext.String.format("<div data-qtitle='{0}' data-qtip='{1}<ul>{2}</ul>'>{0}</div>", value, info, details); | ||
214 | + }, | ||
215 | + 'service.text': function(data, metadata, record) { | ||
216 | + return Ext.util.Format.serviceTooltip(data, record.data); | ||
217 | + }, | ||
218 | + 'service.number': function(data, metadata, record) { | ||
219 | + value = '' + data; | ||
220 | + if(data < 0) { | ||
221 | + value = '-'; | ||
222 | + } else if(data >= 1000*1000) { | ||
223 | + value = (data/(1000*1000)).toPrecision(3) + 'm'; | ||
224 | + } else if(data >= 1000) { | ||
225 | + value = (data/1000).toPrecision(3) + 'k'; | ||
226 | + } | ||
227 | + return Ext.util.Format.serviceTooltip(value, record.data); | ||
228 | + }, | ||
229 | + | ||
230 | + // Granules grid | ||
231 | + | ||
232 | + 'granuleTooltip': function(value, data) { | ||
233 | + for (var key in data) { | ||
234 | + if(typeof data[key] == 'string' && data[key] != '') { | ||
235 | + data[key] = data[key].replace(/'/g, ''').replace(/"/g, '"'); | ||
236 | + } | ||
237 | + } | ||
238 | + var tooltip = '<img src="' + data.thumbnail_url + '">'; | ||
239 | + return Ext.String.format("<div data-qtitle='' data-qtip='{0}'>{1}</div>", tooltip, value); | ||
240 | + }, | ||
241 | + 'granule.text': function(data, metadata, record) { | ||
242 | + return Ext.util.Format.granuleTooltip('<p style="white-space: normal;">' + data + '</p>', record.data); | ||
243 | + }, | ||
244 | + 'granule.link': function(data, metadata, record) { | ||
245 | + return Ext.util.Format.granuleTooltip('<a style="font-size:150%" target="_blank" href="' + data + '">🗗</a>', record.data); | ||
246 | + }, | ||
247 | + 'granule.img': function(data, metadata, record) { | ||
248 | + return Ext.util.Format.granuleTooltip('<img width="40px height="40px" src="' + data + '">', record.data); | ||
249 | + }, | ||
250 | + 'granule.type': function(data, metadata, record) { | ||
251 | + var productTypeDict = Ext.data.StoreManager.lookup('productTypesStore').data.map; | ||
252 | + return Ext.util.Format.granuleTooltip('<p>' + productTypeDict[data].data.name + '</p>', record.data); | ||
253 | + }, | ||
254 | + 'granule.size': function(data, metadata, record) { | ||
255 | + var size = parseInt(data); | ||
256 | + var txt = ''; | ||
257 | + if (isNaN(size)) { | ||
258 | + } else if (size >= 1024*1024) { | ||
259 | + txt = (size/(1024*1024)).toPrecision(3) + 'Go'; | ||
260 | + } else if (size >= 1024) { | ||
261 | + txt = (size/1024).toPrecision(3) + 'Mo'; | ||
262 | + } else { | ||
263 | + txt = size + 'Ko'; | ||
264 | + } | ||
265 | + return Ext.util.Format.granuleTooltip('<p>' + txt + '</p>', record.data); | ||
266 | + }, | ||
267 | + 'granule.proc_lvl': function(data, metadata, record) { | ||
268 | + var levels = {1: 'Raw', 2: 'Edited', 3: 'Calibrated', 4: 'Resampled', 5: 'Derived', 6: 'Ancillary'}; | ||
269 | + return Ext.util.Format.granuleTooltip((data in levels) ? '<p>' + levels[data] + '</p>' : '<em>' + data + '</em>', record.data); | ||
270 | + }, | ||
271 | + 'granule.date': function(data, metadata, record) { | ||
272 | + if(isNaN(data)) { | ||
273 | + return ''; | ||
274 | + } | ||
275 | + var f = Number(data) + 1401 + Math.floor((Math.floor((4 * Number(data) + 274277) / 146097) * 3) / 4) - 38; | ||
276 | + var e = 4 * f + 3; | ||
277 | + var g = Math.floor((e % 1461) / 4); | ||
278 | + var h = 5 * g + 2; | ||
279 | + var D = Math.floor((h % 153) / 5) + 1; | ||
280 | + var M = ((Math.floor(h / 153) + 2) % 12) + 1; | ||
281 | + var Y = Math.floor(e / 1461) - 4716 + Math.floor((12 + 2 - M) / 12); | ||
282 | + return Ext.util.Format.granuleTooltip('<p>' + Ext.Date.format(new Date(Y, M-1, D), 'Y/m/d') + '</p>', record.data); | ||
283 | + }, | ||
284 | + 'granule.format': function(data, metadata, record) { | ||
285 | + var mimetypeDict = { | ||
286 | + 'application/fits': 'fits', | ||
287 | + 'application/x-pds': 'pds', | ||
288 | + 'image/x-pds': 'pds', | ||
289 | + 'application/gml+xml': 'gml', | ||
290 | + 'application/json': 'json', | ||
291 | + 'application/octet-stream': 'bin, idl, envi or matlab', | ||
292 | + 'application/pdf': 'pdf', | ||
293 | + 'application/postscript': 'ps', | ||
294 | + 'application/vnd.geo+json': 'geojson', | ||
295 | + 'application/vnd.google-earth.kml+xml': 'kml', | ||
296 | + 'application/vnd.google-earth.kmz': 'kmz', | ||
297 | + 'application/vnd.ms-excel': 'xls', | ||
298 | + 'application/x-asdm': 'asdm', | ||
299 | + 'application/x-cdf': 'cdf', | ||
300 | + 'application/x-cdf-istp': 'cdf', | ||
301 | + 'application/x-cdf-pds4': 'cdf', | ||
302 | + 'application/x-cef1': 'cef1', | ||
303 | + 'application/x-cef2': 'cef2', | ||
304 | + 'application/x-directory': 'dir', | ||
305 | + 'application/x-fits-bintable': 'bintable', | ||
306 | + 'application/x-fits-euro3d': 'euro3d', | ||
307 | + 'application/x-fits-mef': 'mef', | ||
308 | + 'application/x-geotiff': 'geotiff', | ||
309 | + 'application/x-hdf': 'hdf', | ||
310 | + 'application/x-netcdf': 'nc', | ||
311 | + 'application/x-netcdf4': 'nc', | ||
312 | + 'application/x-tar': 'tar', | ||
313 | + 'application/x-tar-gzip': 'gtar', | ||
314 | + 'application/x-votable+xml': 'votable', | ||
315 | + 'application/x-votable+xml;content=datalink': 'votable', | ||
316 | + 'application/zip': 'zip', | ||
317 | + 'image/fits': 'fits', | ||
318 | + 'image/gif': 'gif', | ||
319 | + 'image/jpeg': 'jpeg', | ||
320 | + 'image/png': 'png', | ||
321 | + 'image/tiff': 'tiff', | ||
322 | + 'image/x-fits-gzip': 'fits', | ||
323 | + 'image/x-fits-hcompress': 'fits', | ||
324 | + 'text/csv': 'csv', | ||
325 | + 'text/html': 'html', | ||
326 | + 'text/plain': 'txt', | ||
327 | + 'text/tab-separated-values': 'tsv', | ||
328 | + 'text/xml': 'xml', | ||
329 | + 'video/mpeg': 'mpeg', | ||
330 | + 'video/quicktime': 'mov', | ||
331 | + 'video/x-msvideo': 'avi' | ||
332 | + }; | ||
333 | + return Ext.util.Format.granuleTooltip((data in mimetypeDict) ? '<p>' + mimetypeDict[data] + '</p>' : '<em>' + data + '</em>', record.data); | ||
334 | + } | ||
335 | +}); | ||
336 | + | ||
337 | +/** | ||
338 | +`EpnTapUI`: The view of the AMDA EPN-TAP module, allowing the user to query and display granules information from | ||
339 | +EPN-TAP services. | ||
340 | + | ||
341 | +Note: The controller part of this module is defined in `js/app/controller/EpnTapModule`. | ||
342 | +*/ | ||
343 | +Ext.define('amdaUI.EpnTapUI', { | ||
344 | + extend: 'Ext.panel.Panel', | ||
345 | + alias: 'widget.panelEpnTap', | ||
346 | + requires: ['amdaUI.IntervalUI'], | ||
347 | + | ||
348 | + /** | ||
349 | + Method constructor, which basically call the `init()` method to create the EpnTap panel. | ||
350 | + */ | ||
351 | + constructor: function(config) { | ||
352 | + this.init(config); | ||
353 | + this.callParent(arguments); | ||
354 | + }, | ||
355 | + | ||
356 | + /** | ||
357 | + Create all the EpnTapPanel UI elements, and apply the AMDA module `config` (which includes the created items). | ||
358 | + | ||
359 | + When the panel is correctly rendered, the panel triggers `EpnTapModule.onWindowLoaded()`. | ||
360 | + | ||
361 | + Note: All the UI elements creation are defined as functions in this init method and not as methods in order to make | ||
362 | + them private (ie. to avoid `EpnTapUI.createServicesGrid();`, which doesn't make sense). | ||
363 | + */ | ||
364 | + init: function(config) { | ||
365 | + var myConf = { | ||
366 | + id: 'epntapTab', | ||
367 | + title: 'EPN-TAP', | ||
368 | + layout: 'fit', | ||
369 | + items: [{ | ||
370 | + xtype: 'container', | ||
371 | + layout: { type: 'vbox', pack: 'start', align: 'stretch'}, | ||
372 | + items: [ | ||
373 | + this.createServiceFilterPanel(), | ||
374 | + this.createGridsPanel() | ||
375 | + ] | ||
376 | + }] | ||
377 | + }; | ||
378 | + Ext.apply(this, Ext.apply(arguments, myConf)); | ||
379 | + }, | ||
380 | + | ||
381 | + /*************************** | ||
382 | + *** Service filter panel *** | ||
383 | + ***************************/ | ||
384 | + | ||
385 | + /** | ||
386 | + Create `epnTapServiceFilterPanel`, an ExtJS Panel containing two containers: | ||
387 | + - the left container, containing the combo boxes (for product type, target class and target name) | ||
388 | + and the navigation panel; | ||
389 | + - the right container, containing the time selector. | ||
390 | + */ | ||
391 | + createServiceFilterPanel: function() { | ||
392 | + return { | ||
393 | + xtype: 'form', | ||
394 | + id: 'epnTapServiceFilterPanel', | ||
395 | + layout: { type: 'hbox', pack: 'start', align: 'stretch' }, | ||
396 | + region: 'north', | ||
397 | + defaults: { margin: '5 0 5 5'}, | ||
398 | + items: [{ // Left part | ||
399 | + xtype : 'container', | ||
400 | + flex: 1, | ||
401 | + items: [ | ||
402 | + this.createTargetNameCB(), | ||
403 | + this.createProductTypeCB() | ||
404 | + ] | ||
405 | + }, { // Middle part | ||
406 | + xtype : 'container', | ||
407 | + flex: 1, | ||
408 | + items: [ | ||
409 | + this.createTimeSelector() | ||
410 | + ] | ||
411 | + }, { // Right part | ||
412 | + xtype : 'container', | ||
413 | + items: [ | ||
414 | + this.createSendButton() | ||
415 | + ] | ||
416 | + | ||
417 | + }] | ||
418 | + }; | ||
419 | + }, | ||
420 | + | ||
421 | + /** | ||
422 | + Create `epnTapTargetNameCB`, an ExtJS ComboBox, containing a list of target names corresponding to the selected | ||
423 | + target class, as defined in `targetNamesStore`, which is initilized by `EpnTapModule`. | ||
424 | + | ||
425 | + The selection of a target name triggers `EpnTapModule.onTargetNameCBChanged()`, which basically updates | ||
426 | + `granulesGrid`. | ||
427 | + */ | ||
428 | + createTargetNameCB: function() { | ||
429 | + return { | ||
430 | + xtype: 'combobox', | ||
431 | + id: 'epnTapTargetNameCB', | ||
432 | + fieldLabel: 'Target name', | ||
433 | + emptyText: 'Earth, Saturn, 67P, ...', | ||
434 | + store: Ext.data.StoreManager.lookup('targetNamesStore'), | ||
435 | + queryMode: 'remote', | ||
436 | + queryParam: 'input', | ||
437 | + displayField: 'name', | ||
438 | + valueField: 'id', | ||
439 | + margin: '15 0 5 0', | ||
440 | + labelWidth: 71, | ||
441 | + minWidth: 20, | ||
442 | + minChars: 2, | ||
443 | + hideTrigger: true, | ||
444 | + listConfig: { | ||
445 | + getInnerTpl: function() { | ||
446 | + return '<div data-qtitle="{name}" data-qtip="<p>type: {type}</p><p>parent: {parent}</p><p>aliases:</p><ul>{aliases}</ul>">{name}</div>'; | ||
447 | + } | ||
448 | + }, | ||
449 | + listeners: { | ||
450 | + render: function(cb) { | ||
451 | + new Ext.ToolTip({ | ||
452 | + target: cb.getEl(), | ||
453 | + html: 'Start to type a text then select a target name (required).' | ||
454 | + }); | ||
455 | + } | ||
456 | + } | ||
457 | + }; | ||
458 | + }, | ||
459 | + | ||
460 | + /** | ||
461 | + Create `epnTapProductTypeCB`, an ExtJS ComboBox, containing a list of product types as defined in | ||
462 | + `epnTapProductTypesStore`, which is initilized by `EpnTapModule`. | ||
463 | + | ||
464 | + The selection of a produt type triggers `EpnTapModule.onProductTypeCBChanged()`, which basically update | ||
465 | + `epnTapGranulesGrid`. | ||
466 | + */ | ||
467 | + createProductTypeCB: function() { | ||
468 | + return { | ||
469 | + xtype: 'combobox', | ||
470 | + id: 'epnTapProductTypeCB', | ||
471 | + fieldLabel: 'Product type', | ||
472 | + emptyText: 'Image, Time series, ...', | ||
473 | + store: Ext.data.StoreManager.lookup('productTypesStore'), | ||
474 | + queryMode: 'local', | ||
475 | + valueField: 'id', | ||
476 | + multiSelect: true, | ||
477 | + displayField: 'name', | ||
478 | + labelWidth: 71, | ||
479 | + editable: false, | ||
480 | + listConfig: { | ||
481 | + getInnerTpl: function() { | ||
482 | + return '<div data-qtitle="{name}" data-qwidth=200 data-qtip="<p>{desc}</p>">{name}</div>'; | ||
483 | + } | ||
484 | + }, | ||
485 | + listeners: { | ||
486 | + change: function(cb, records) { | ||
487 | + var val = cb.value[cb.value.length - 1]; | ||
488 | + if(val === 'all') { | ||
489 | + cb.select(cb.store.getRange().slice(2)); | ||
490 | + } else if (val === 'clear') { | ||
491 | + cb.reset(); | ||
492 | + } | ||
493 | + }, | ||
494 | + render: function(cb) { | ||
495 | + new Ext.ToolTip({ | ||
496 | + target: cb.getEl(), | ||
497 | + html: 'Select one or several data product types (required).' | ||
498 | + }); | ||
499 | + } | ||
500 | + } | ||
501 | + }; | ||
502 | + }, | ||
503 | + | ||
504 | + /** | ||
505 | + Create `epnTapTimeSelector`, an IntervalUI object, allowing the user to select a time interval (by filling two | ||
506 | + dates and/or a duration). | ||
507 | + | ||
508 | + See `js/app/views/IntervalUI.js` for more information about this component. | ||
509 | + */ | ||
510 | + createTimeSelector: function() { | ||
511 | + return { | ||
512 | + xtype: 'intervalSelector', | ||
513 | + id: 'epnTapTimeSelector', | ||
514 | + durationLimit: 99999 | ||
515 | + }; | ||
516 | + }, | ||
517 | + | ||
518 | + /*********************** | ||
519 | + *** Navigation panel *** | ||
520 | + ***********************/ | ||
521 | + | ||
522 | + /** | ||
523 | + The button used to send the query. | ||
524 | + */ | ||
525 | + createSendButton: function() { | ||
526 | + return { | ||
527 | + xtype: 'button', | ||
528 | + id: 'epnTapGetBtn', | ||
529 | + text: 'Get services', | ||
530 | + disabled: true, | ||
531 | + width: 140, | ||
532 | + height: 50, | ||
533 | + margin: 10 | ||
534 | + } | ||
535 | + }, | ||
536 | + | ||
537 | + /************ | ||
538 | + *** Grids *** | ||
539 | + ************/ | ||
540 | + | ||
541 | + /** | ||
542 | + Create `epnTapGridsPanel`, an ExtJS Panel, containing `epnTapServicesGrid` and `epnTapGranulesGrid`. | ||
543 | + | ||
544 | + After the rendering of the grids, it triggers `epnTapModule.onWindowLoaded()`, which basically fill | ||
545 | + `epnTapServicesGrid` for the first time. | ||
546 | + */ | ||
547 | + createGridsPanel: function() { | ||
548 | + return { | ||
549 | + xtype: 'panel', | ||
550 | + id: 'epnTapGridsPanel', | ||
551 | + layout: 'fit', | ||
552 | + height: 440, | ||
553 | + region: 'center', | ||
554 | + items: [{ | ||
555 | + xtype: 'container', | ||
556 | + layout: { type: 'hbox', pack: 'start', align: 'stretch'}, | ||
557 | + items: [ | ||
558 | + this.createServicesGrid(), | ||
559 | + this.createGranulesGrid() | ||
560 | + ] | ||
561 | + }] | ||
562 | + }; | ||
563 | + }, | ||
564 | + | ||
565 | + /** | ||
566 | + Create `epnTapServicesGrid`, an ExtJS grid containing the EPN-TAP services matching with the filter form | ||
567 | + (`serviceFilterPanel`). | ||
568 | + | ||
569 | + For each service, this grid displays: | ||
570 | + - the service name; | ||
571 | + - the number of granules matching with the filter. | ||
572 | + | ||
573 | + Other informations are available through an ExtJS Tooltip, on each row: | ||
574 | + - short name; | ||
575 | + - title; | ||
576 | + - access URL. | ||
577 | + | ||
578 | + A click on a service triggers `EpnTapModule.onServiceSelected()`, which basically fills `GranulesGrid` by the | ||
579 | + service granules. | ||
580 | + */ | ||
581 | + createServicesGrid: function() { | ||
582 | + return { | ||
583 | + xtype: 'grid', | ||
584 | + id: 'epnTapServicesGrid', | ||
585 | + title: 'Services', | ||
586 | + store: Ext.data.StoreManager.lookup('servicesStore'), | ||
587 | + flex: 1, | ||
588 | + columns: [ | ||
589 | + {text: 'Name', dataIndex: 'short_name', flex: 1, renderer: 'service.text'}, | ||
590 | + {text: 'Nb res.', dataIndex: 'nb_results', width: 50, renderer: 'service.number'} | ||
591 | + ], | ||
592 | + viewConfig: { | ||
593 | + getRowClass: function(record, index) { | ||
594 | + var nb_res = record.get('nb_results'); | ||
595 | + if(nb_res == 0 || nb_res == -1) { | ||
596 | + return 'disabled_row'; | ||
597 | + } else if (nb_res == -2) { | ||
598 | + return 'error_row'; | ||
599 | + } | ||
600 | + } | ||
601 | + } | ||
602 | + }; | ||
603 | + }, | ||
604 | + | ||
605 | + /** | ||
606 | + Create `epnTapGranulesGrid`, an ExtJS grid containing the granules of the selected service in | ||
607 | + `epnTapServicesGrid`. | ||
608 | + | ||
609 | + For each granule, this grid displays: | ||
610 | + - the row number; | ||
611 | + - the dataproduct type; | ||
612 | + - the target name; | ||
613 | + - the min and max times; | ||
614 | + - the format; | ||
615 | + - the UID (granule identifier); | ||
616 | + - the estimated size; | ||
617 | + - the URL; | ||
618 | + - the thumbnail. | ||
619 | + | ||
620 | + Each of these information are displayed in a specific rendering to improve user experience. | ||
621 | + For more information about these parameters, see https://voparis-confluence.obspm.fr/display/VES/EPN-TAP+V2.0+parameters. | ||
622 | + | ||
623 | + Other informations are available through an ExtJS Tooltip on each row: | ||
624 | + - currently only the granule thumbnail, in full size. | ||
625 | + | ||
626 | + A click on a granule triggers `EpnTapModule.onGranuleSelected()`. | ||
627 | + */ | ||
628 | + createGranulesGrid: function() { | ||
629 | + return { | ||
630 | + xtype: 'grid', | ||
631 | + id: 'epnTapGranulesGrid', | ||
632 | + title: 'Granules', | ||
633 | + store: Ext.data.StoreManager.lookup('granulesStore'), | ||
634 | + flex: 4, | ||
635 | + columns: [] | ||
636 | + }; | ||
637 | + } | ||
638 | +}); |
js/app/views/ExplorerUI.js
@@ -100,6 +100,7 @@ Ext.define('amdaUI.ExplorerUI', { | @@ -100,6 +100,7 @@ Ext.define('amdaUI.ExplorerUI', { | ||
100 | ITEM_KIND_LEAF : 'leaf', | 100 | ITEM_KIND_LEAF : 'leaf', |
101 | ITEM_KIND_DIRE : 'dire', | 101 | ITEM_KIND_DIRE : 'dire', |
102 | ITEM_KIND_PARA : 'para', | 102 | ITEM_KIND_PARA : 'para', |
103 | + ITEM_KIND_MISS : 'miss' | ||
103 | }, | 104 | }, |
104 | 105 | ||
105 | initComponent : function (config) { | 106 | initComponent : function (config) { |
js/app/views/InteropUI.js
1 | /** | 1 | /** |
2 | * Project : AMDA-NG | 2 | * Project : AMDA-NG |
3 | - * Name : InteropUI.js | 3 | + * Name : InteropUI.js |
4 | * @class amdaUI.InteropUI | 4 | * @class amdaUI.InteropUI |
5 | * @extends Ext.tab.Panel | 5 | * @extends Ext.tab.Panel |
6 | * @brief Interop Module UI definition (View) | 6 | * @brief Interop Module UI definition (View) |
7 | * @author Benjamin RENARD | 7 | * @author Benjamin RENARD |
8 | * @version $Id: InteropUI.js 1093 2012-10-03 15:54:26Z elena $ | 8 | * @version $Id: InteropUI.js 1093 2012-10-03 15:54:26Z elena $ |
9 | ****************************************************************************** | 9 | ****************************************************************************** |
10 | - * FT Id : Date : Name - Description | 10 | + * FT Id : Date : Name - Description |
11 | ****************************************************************************** | 11 | ****************************************************************************** |
12 | - * : :23/04/2012: BRE - file creation | 12 | + * : :23/04/2012: BRE - file creation |
13 | */ | 13 | */ |
14 | 14 | ||
15 | 15 | ||
@@ -18,7 +18,7 @@ Ext.define('amdaUI.InteropUI', { | @@ -18,7 +18,7 @@ Ext.define('amdaUI.InteropUI', { | ||
18 | alias: 'widget.panelInterop', | 18 | alias: 'widget.panelInterop', |
19 | 19 | ||
20 | requires: [ | 20 | requires: [ |
21 | - 'amdaUI.ParamsMgrUI' | 21 | + 'amdaUI.ParamsMgrUI', 'amdaUI.EpnTapUI' |
22 | ], | 22 | ], |
23 | 23 | ||
24 | constructor: function(config) { | 24 | constructor: function(config) { |
@@ -103,27 +103,30 @@ Ext.define('amdaUI.InteropUI', { | @@ -103,27 +103,30 @@ Ext.define('amdaUI.InteropUI', { | ||
103 | }; | 103 | }; |
104 | }, | 104 | }, |
105 | 105 | ||
106 | - init : function(config) { | ||
107 | - | ||
108 | - var me = this; | ||
109 | - | ||
110 | - this.onSwitchConnect = config.onSwitchConnect; | ||
111 | - var activeTab = config.activeTab ? config.activeTab : 0; | ||
112 | - | ||
113 | - var myConf = { | ||
114 | - plain : true, | ||
115 | - activeTab: activeTab, | ||
116 | - defaults: { | ||
117 | - autoHeight: true, | ||
118 | - layout : 'fit', | ||
119 | - bodyStyle: { background : '#dfe8f6' } | ||
120 | - }, | ||
121 | - items: [ | ||
122 | - this.getSampTab(config.clientsStore), | ||
123 | - { xtype : 'paramsMgrPanel', baseId : config.baseId, layout : 'hbox'} | ||
124 | - ] | ||
125 | - }; | ||
126 | - | ||
127 | - Ext.apply (this , Ext.apply (arguments, myConf)); | ||
128 | - } | 106 | + init: function(config) { |
107 | + var me = this; | ||
108 | + | ||
109 | + this.onSwitchConnect = config.onSwitchConnect; | ||
110 | + var activeTab = config.activeTab ? config.activeTab : 0; | ||
111 | + | ||
112 | + var myConf = { | ||
113 | + plain: true, | ||
114 | + activeTab: activeTab, | ||
115 | + defaults: { | ||
116 | + autoHeight: true, | ||
117 | + layout: 'fit', | ||
118 | + bodyStyle: { background: '#dfe8f6' } | ||
119 | + }, | ||
120 | + items: [ | ||
121 | + this.getSampTab(config.clientsStore), | ||
122 | + {xtype: 'paramsMgrPanel', baseId: config.baseId, layout: 'hbox'}, | ||
123 | + {xtype: 'panelEpnTap'} | ||
124 | + ], | ||
125 | + listeners: { | ||
126 | + afterrender: function() { config.loadTab(this.getActiveTab()); }, | ||
127 | + tabchange: function(tabpanel, tab) { config.loadTab(tab); } | ||
128 | + } | ||
129 | + }; | ||
130 | + Ext.apply (this, Ext.apply (arguments, myConf)); | ||
131 | + } | ||
129 | }); | 132 | }); |
js/app/views/IntervalUI.js
@@ -12,12 +12,14 @@ | @@ -12,12 +12,14 @@ | ||
12 | ****************************************************************************** | 12 | ****************************************************************************** |
13 | */ | 13 | */ |
14 | 14 | ||
15 | - | 15 | +/** |
16 | +config: | ||
17 | +- durationLimit: The maximum value of the duration days field (9999 by default). | ||
18 | +*/ | ||
16 | Ext.define('amdaUI.IntervalUI', { | 19 | Ext.define('amdaUI.IntervalUI', { |
17 | extend: 'Ext.container.Container', | 20 | extend: 'Ext.container.Container', |
18 | 21 | ||
19 | alias: 'widget.intervalSelector', | 22 | alias: 'widget.intervalSelector', |
20 | - | ||
21 | activeField : null, | 23 | activeField : null, |
22 | 24 | ||
23 | constructor: function(config) { | 25 | constructor: function(config) { |
@@ -25,6 +27,12 @@ Ext.define('amdaUI.IntervalUI', { | @@ -25,6 +27,12 @@ Ext.define('amdaUI.IntervalUI', { | ||
25 | this.callParent(arguments); | 27 | this.callParent(arguments); |
26 | }, | 28 | }, |
27 | 29 | ||
30 | + /** | ||
31 | + Set the start and stop date, and update the duration field. | ||
32 | + - startDate: A Extjs Date object representing the new start time. | ||
33 | + - stopDate: A Extjs Date object representing the new stop time. | ||
34 | + - return: None. | ||
35 | + */ | ||
28 | setInterval : function(startDate,stopDate) | 36 | setInterval : function(startDate,stopDate) |
29 | { | 37 | { |
30 | // get the search form | 38 | // get the search form |
@@ -43,6 +51,31 @@ Ext.define('amdaUI.IntervalUI', { | @@ -43,6 +51,31 @@ Ext.define('amdaUI.IntervalUI', { | ||
43 | this.updateDuration(); | 51 | this.updateDuration(); |
44 | }, | 52 | }, |
45 | 53 | ||
54 | + /** | ||
55 | + Set the limits values of both startField and stopField date fields. | ||
56 | + */ | ||
57 | + setLimits: function(minValue, maxValue) { | ||
58 | + var form = this.findParentByType('form').getForm(); | ||
59 | + var startField = form.findField('startDate'); | ||
60 | + var stopField = form.findField('stopDate'); | ||
61 | + | ||
62 | + if (startField != null) { | ||
63 | + startField.setMinValue(minValue); | ||
64 | + startField.setMaxValue(maxValue); | ||
65 | + } | ||
66 | + | ||
67 | + if (stopField != null) { | ||
68 | + stopField.setMinValue(minValue); | ||
69 | + stopField.setMaxValue(maxValue); | ||
70 | + } | ||
71 | + | ||
72 | + this.updateDuration(); | ||
73 | + }, | ||
74 | + | ||
75 | + /** | ||
76 | + Get the start time field value. | ||
77 | + - return: A Extjs Date object representing the start time (null if the date is not valid). | ||
78 | + */ | ||
46 | getStartTime : function() | 79 | getStartTime : function() |
47 | { | 80 | { |
48 | // get the search form | 81 | // get the search form |
@@ -53,6 +86,10 @@ Ext.define('amdaUI.IntervalUI', { | @@ -53,6 +86,10 @@ Ext.define('amdaUI.IntervalUI', { | ||
53 | return startField.getValue(); | 86 | return startField.getValue(); |
54 | }, | 87 | }, |
55 | 88 | ||
89 | + /** | ||
90 | + Get the stop time field value. | ||
91 | + - return: A Extjs Date object representing the stop time (null if the date is not valid). | ||
92 | + */ | ||
56 | getStopTime : function() | 93 | getStopTime : function() |
57 | { | 94 | { |
58 | // get the search form | 95 | // get the search form |
@@ -63,14 +100,17 @@ Ext.define('amdaUI.IntervalUI', { | @@ -63,14 +100,17 @@ Ext.define('amdaUI.IntervalUI', { | ||
63 | return stopField.getValue(); | 100 | return stopField.getValue(); |
64 | }, | 101 | }, |
65 | 102 | ||
66 | - updateDuration: function() { | 103 | + /* |
104 | + #### Private methods from here #### | ||
105 | + */ | ||
67 | 106 | ||
107 | + updateDuration: function() { | ||
68 | // get the search form | 108 | // get the search form |
69 | var form = this.findParentByType('form').getForm(); | 109 | var form = this.findParentByType('form').getForm(); |
70 | // get start value | 110 | // get start value |
71 | - var start = form.findField('startDate').getValue(); | 111 | + var start = this.getStartTime(); |
72 | // get stop value | 112 | // get stop value |
73 | - var stop = form.findField('stopDate').getValue(); | 113 | + var stop = this.getStopTime(); |
74 | // if duration computable | 114 | // if duration computable |
75 | if (stop != null && start != null) { | 115 | if (stop != null && start != null) { |
76 | 116 | ||
@@ -81,15 +121,14 @@ Ext.define('amdaUI.IntervalUI', { | @@ -81,15 +121,14 @@ Ext.define('amdaUI.IntervalUI', { | ||
81 | 121 | ||
82 | var durationDays = Math.floor(diff/86400000); | 122 | var durationDays = Math.floor(diff/86400000); |
83 | // set all duration values | 123 | // set all duration values |
84 | - form.findField('durationDay').setValue(Ext.String.leftPad(durationDays,4,'0')); | 124 | + form.findField('durationDay').setValue(Ext.String.leftPad(durationDays, ('' + this.durationLimit).length, '0')); |
85 | form.findField('durationHour').setValue(Ext.String.leftPad(Math.floor(diff/3600000 % 24),2,'0')); | 125 | form.findField('durationHour').setValue(Ext.String.leftPad(Math.floor(diff/3600000 % 24),2,'0')); |
86 | form.findField('durationMin').setValue(Ext.String.leftPad(Math.floor(diff/60000 % 60),2,'0')); | 126 | form.findField('durationMin').setValue(Ext.String.leftPad(Math.floor(diff/60000 % 60),2,'0')); |
87 | form.findField('durationSec').setValue(Ext.String.leftPad(Math.floor(diff/1000 % 60),2,'0')); | 127 | form.findField('durationSec').setValue(Ext.String.leftPad(Math.floor(diff/1000 % 60),2,'0')); |
88 | 128 | ||
89 | - if (durationDays > 9999) { | ||
90 | - form.findField('durationDay').markInvalid('Maximum interval is 9999 days!'); | 129 | + if (durationDays > this.durationLimit) { |
130 | + form.findField('durationDay').markInvalid('Maximum interval is ' + this.durationLimit + ' days!'); | ||
91 | } | 131 | } |
92 | - | ||
93 | } | 132 | } |
94 | 133 | ||
95 | }, | 134 | }, |
@@ -105,7 +144,6 @@ Ext.define('amdaUI.IntervalUI', { | @@ -105,7 +144,6 @@ Ext.define('amdaUI.IntervalUI', { | ||
105 | }, | 144 | }, |
106 | 145 | ||
107 | updateStop: function() { | 146 | updateStop: function() { |
108 | - | ||
109 | // get the time form | 147 | // get the time form |
110 | var form = this.findParentByType('form').getForm(); | 148 | var form = this.findParentByType('form').getForm(); |
111 | // get duration value | 149 | // get duration value |
@@ -150,10 +188,15 @@ Ext.define('amdaUI.IntervalUI', { | @@ -150,10 +188,15 @@ Ext.define('amdaUI.IntervalUI', { | ||
150 | layout: {type: 'hbox', align: 'middle'}, | 188 | layout: {type: 'hbox', align: 'middle'}, |
151 | items: [ | 189 | items: [ |
152 | { | 190 | { |
153 | - xtype: 'datefield', name: fieldName, format: 'Y/m/d H:i:s', | ||
154 | - enforceMaxLength : true, | ||
155 | - maxLength: 19, | ||
156 | - fieldLabel: fieldText, labelAlign: 'right', labelWidth: 60, | 191 | + xtype: 'datefield', |
192 | + name: fieldName, | ||
193 | + emptyText: 'YYYY/MM/DD hh:mm:ss', | ||
194 | + format: 'Y/m/d H:i:s', | ||
195 | + enforceMaxLength: true, | ||
196 | + maxLength: 19, | ||
197 | + fieldLabel: fieldText, | ||
198 | + labelAlign: 'left', | ||
199 | + labelWidth: 60, | ||
157 | listeners: { | 200 | listeners: { |
158 | change: onChangeField, | 201 | change: onChangeField, |
159 | focus: function(field) { | 202 | focus: function(field) { |
@@ -168,7 +211,7 @@ Ext.define('amdaUI.IntervalUI', { | @@ -168,7 +211,7 @@ Ext.define('amdaUI.IntervalUI', { | ||
168 | 211 | ||
169 | getStartField : function() | 212 | getStartField : function() |
170 | { | 213 | { |
171 | - return this.getDateField('startDate','Start Time','start',this.onChangeStartField); | 214 | + return this.getDateField('startDate','Start Time','start', this.onChangeStartField); |
172 | }, | 215 | }, |
173 | 216 | ||
174 | getStopField : function() | 217 | getStopField : function() |
@@ -180,9 +223,12 @@ Ext.define('amdaUI.IntervalUI', { | @@ -180,9 +223,12 @@ Ext.define('amdaUI.IntervalUI', { | ||
180 | { | 223 | { |
181 | return { | 224 | return { |
182 | layout: {type: 'hbox', align: 'middle'}, | 225 | layout: {type: 'hbox', align: 'middle'}, |
183 | - height: 45, | 226 | + margin: 0, |
184 | defaults: { | 227 | defaults: { |
185 | - xtype: 'textfield', labelAlign: 'top', width: 30, | 228 | + xtype: 'textfield', |
229 | + labelAlign: 'left', | ||
230 | + width: 35, | ||
231 | + margin: '0 5 0 0', | ||
186 | allowBlank: false, maxLength:2, enforceMaxLength : true, | 232 | allowBlank: false, maxLength:2, enforceMaxLength : true, |
187 | hideTrigger: true, | 233 | hideTrigger: true, |
188 | regex: /^[0-9]([0-9])*$/i, | 234 | regex: /^[0-9]([0-9])*$/i, |
@@ -196,24 +242,26 @@ Ext.define('amdaUI.IntervalUI', { | @@ -196,24 +242,26 @@ Ext.define('amdaUI.IntervalUI', { | ||
196 | focus: function(field) { | 242 | focus: function(field) { |
197 | this.activeField = 'duration'; | 243 | this.activeField = 'duration'; |
198 | }, | 244 | }, |
245 | + render: function(c) { | ||
246 | + Ext.create('Ext.tip.ToolTip', { | ||
247 | + target: c.getEl(), | ||
248 | + html: c.tip | ||
249 | + }); | ||
250 | + }, | ||
199 | scope : this | 251 | scope : this |
200 | } | 252 | } |
201 | }, | 253 | }, |
202 | items:[ | 254 | items:[ |
203 | - { xtype: 'displayfield', labelWidth: 60, labelAlign: 'right', width: 60, fieldLabel: '<br>Duration'}, | ||
204 | - { xtype: 'component', width: 5}, | ||
205 | - { name: 'durationDay', fieldLabel: 'Days', width: 45, maxLength: 4}, | ||
206 | - { xtype: 'component', width: 5}, | ||
207 | - { name: 'durationHour', fieldLabel: 'Hrs'}, | ||
208 | - { xtype: 'component', width: 5}, | ||
209 | - { name: 'durationMin', fieldLabel: 'Mins'}, | ||
210 | - { xtype: 'component', width: 5}, | ||
211 | - { name: 'durationSec', fieldLabel: 'Secs'} | 255 | + { name: 'durationDay', tip: 'Days', fieldLabel: 'Duration', labelWidth: 60, emptyText: 'Days', width: 100, maxLength: ('' + this.durationLimit).length}, |
256 | + { name: 'durationHour', tip: 'Hours', emptyText: 'Hrs'}, | ||
257 | + { name: 'durationMin', tip: 'Minutes', emptyText: 'Mins'}, | ||
258 | + { name: 'durationSec', tip: 'Seconds', emptyText: 'Secs'} | ||
212 | ] | 259 | ] |
213 | }; | 260 | }; |
214 | }, | 261 | }, |
215 | 262 | ||
216 | init : function(config) { | 263 | init : function(config) { |
264 | + this.durationLimit = config.durationLimit == null ? 9999 : config.durationLimit; // Set duration limit to 9999 by default | ||
217 | 265 | ||
218 | var me = this; | 266 | var me = this; |
219 | 267 | ||
@@ -222,7 +270,7 @@ Ext.define('amdaUI.IntervalUI', { | @@ -222,7 +270,7 @@ Ext.define('amdaUI.IntervalUI', { | ||
222 | plain: true, | 270 | plain: true, |
223 | flex: 1, | 271 | flex: 1, |
224 | layout: 'anchor', | 272 | layout: 'anchor', |
225 | - defaults: { height : 30, xtype : 'container'}, | 273 | + defaults: { margin: '0 0 5 0', xtype : 'container'}, |
226 | 274 | ||
227 | items: [ | 275 | items: [ |
228 | me.getStartField(), | 276 | me.getStartField(), |
js/resources/css/amda.css
@@ -488,3 +488,11 @@ p + p { | @@ -488,3 +488,11 @@ p + p { | ||
488 | filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100) !important; | 488 | filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100) !important; |
489 | opacity: 1.0 !important; | 489 | opacity: 1.0 !important; |
490 | } | 490 | } |
491 | + | ||
492 | +.disabled_row { | ||
493 | + color: gray; | ||
494 | +} | ||
495 | + | ||
496 | +.error_row { | ||
497 | + color: IndianRed; | ||
498 | +} |
php/classes/VOTableMgr.php
@@ -4,14 +4,17 @@ | @@ -4,14 +4,17 @@ | ||
4 | * @version $Id: VOTableMgr.php 2916 2015-05-19 13:08:33Z elena $ | 4 | * @version $Id: VOTableMgr.php 2916 2015-05-19 13:08:33Z elena $ |
5 | */ | 5 | */ |
6 | 6 | ||
7 | -//set DEBUG_MODE to TRUE to have some log information in user data dir | ||
8 | -define("DEBUG_MODE",FALSE); | ||
9 | - | ||
10 | - | ||
11 | -class VOTableMgr | ||
12 | -{ | ||
13 | - public $xml = null; | ||
14 | - private $log, $xp; | 7 | +//set DEBUG_MODE to TRUE to have some log information |
8 | +define("DEBUG_MODE", FALSE); | ||
9 | + | ||
10 | +class VOTableMgr { | ||
11 | + public $xml = null; | ||
12 | + private $log; | ||
13 | + private $xp; | ||
14 | + private $stream; // The stream in the VOTable | ||
15 | + private $c; // Current character position on the stream | ||
16 | + private $is_little_endian; | ||
17 | + private $votable_error = false; | ||
15 | 18 | ||
16 | function __construct() | 19 | function __construct() |
17 | { | 20 | { |
@@ -27,36 +30,58 @@ class VOTableMgr | @@ -27,36 +30,58 @@ class VOTableMgr | ||
27 | 30 | ||
28 | function load($fileName) | 31 | function load($fileName) |
29 | { | 32 | { |
30 | - $this->xml = new DomDocument("1.0"); | ||
31 | - if (!$this->xml->load($fileName)) | ||
32 | - { | ||
33 | - $this->addLog("Cannot load file ".$fileName."\n"); | ||
34 | - return FALSE; | ||
35 | - } | ||
36 | - | ||
37 | - $this->checkIDAttribute(); | ||
38 | - /*if ($this->xml->namespaceURI == '') | ||
39 | - { | ||
40 | - $this->addLog("File don't have a namespace defined\n"); | ||
41 | - if (!$this->xml->createAttributeNS('http://www.ivoa.net/xml/VOTable/v1.1','xmlns')) | ||
42 | - $this->addLog("Cannot create namespace attribute\n"); | ||
43 | - } | ||
44 | - | ||
45 | - $this->addLog($this->xml->namespaceURI."\n");*/ | 33 | + $this->is_little_endian = array_values(unpack('L1L', pack('V', 1)))[0] == 1; |
34 | + | ||
35 | + // see http://php.net/manual/en/domdocument.load.php#91384 | ||
36 | + $options = array( | ||
37 | + 'http' => array( | ||
38 | + 'method' => 'GET', | ||
39 | + 'timeout' => '5', | ||
40 | + 'user_agent' => 'PHP libxml agent', | ||
41 | + // If the query is wrong, epn-tap service returns an HTTP error code 400, along with xml containing some usefull informations. | ||
42 | + 'ignore_errors' => true | ||
43 | + ) | ||
44 | + ); | ||
45 | + $context = stream_context_create($options); | ||
46 | + libxml_set_streams_context($context); | ||
47 | + $this->xml = new DomDocument(); | ||
48 | + | ||
49 | + if (!$this->xml->load($fileName)) { | ||
50 | + $this->votable_error = 'Can not load xml file.'; | ||
51 | + return false; | ||
52 | + } | ||
46 | 53 | ||
47 | - $rootNamespace = $this->xml->lookupNamespaceUri($this->xml->namespaceURI); | 54 | + $this->checkIDAttribute(); |
48 | 55 | ||
49 | - $this->xp = new domxpath($this->xml); | 56 | + $rootNamespace = $this->xml->lookupNamespaceUri($this->xml->namespaceURI); |
57 | + $this->xp = new domxpath($this->xml); | ||
58 | + $this->xp->registerNameSpace('x', $rootNamespace); | ||
50 | 59 | ||
51 | - $this->xp->registerNameSpace('x', $rootNamespace); | 60 | + return true; |
61 | + } | ||
52 | 62 | ||
53 | - return TRUE; | 63 | + function getVotableError() { |
64 | + return $this->votable_error; | ||
54 | } | 65 | } |
55 | 66 | ||
56 | function isValidSchema() | 67 | function isValidSchema() |
57 | { | 68 | { |
58 | - if (!$this->xml) | ||
59 | - return FALSE; | 69 | + if ($this->votable_error != false) { |
70 | + return false; | ||
71 | + } | ||
72 | + | ||
73 | + if (!$this->xml) { | ||
74 | + $this->votable_error = "The returned file is not XML."; | ||
75 | + return false; | ||
76 | + } | ||
77 | + | ||
78 | + $infos = $this->xp->query($this->queryResourceInfo()); | ||
79 | + foreach($infos as $info) { | ||
80 | + if($info->getAttribute('value') == 'ERROR') { | ||
81 | + $this->votable_error = $info->textContent; | ||
82 | + return false; | ||
83 | + } | ||
84 | + } | ||
60 | 85 | ||
61 | 86 | ||
62 | //ToDo - BRE - add validation!! | 87 | //ToDo - BRE - add validation!! |
@@ -117,6 +142,10 @@ class VOTableMgr | @@ -117,6 +142,10 @@ class VOTableMgr | ||
117 | return "//x:RESOURCE"; | 142 | return "//x:RESOURCE"; |
118 | } | 143 | } |
119 | 144 | ||
145 | + protected function queryResourceInfo() { | ||
146 | + return $this->queryResource()."/x:INFO"; | ||
147 | + } | ||
148 | + | ||
120 | protected function queryTable() | 149 | protected function queryTable() |
121 | { | 150 | { |
122 | return $this->queryResource()."/x:TABLE"; | 151 | return $this->queryResource()."/x:TABLE"; |
@@ -161,6 +190,13 @@ class VOTableMgr | @@ -161,6 +190,13 @@ class VOTableMgr | ||
161 | return $this->queryTableData()."/x:TR"; | 190 | return $this->queryTableData()."/x:TR"; |
162 | } | 191 | } |
163 | 192 | ||
193 | + protected function queryBinaryData() { | ||
194 | + return $this->queryData()."/x:BINARY"; | ||
195 | + } | ||
196 | + | ||
197 | + protected function queryStream() { | ||
198 | + return $this->queryBinaryData()."/x:STREAM"; | ||
199 | + } | ||
164 | // | 200 | // |
165 | 201 | ||
166 | public function getVersion() | 202 | public function getVersion() |
@@ -387,6 +423,147 @@ class VOTableMgr | @@ -387,6 +423,147 @@ class VOTableMgr | ||
387 | return $this->getFieldInfo($field); | 423 | return $this->getFieldInfo($field); |
388 | } | 424 | } |
389 | 425 | ||
426 | + /** Get the size of a row according to datatype array length. */ | ||
427 | + private function get_row_size($field_node) { | ||
428 | + $datatype = $field_node->getAttribute("datatype"); | ||
429 | + | ||
430 | + if($datatype == 'boolean') { | ||
431 | + return 1; | ||
432 | + } | ||
433 | + | ||
434 | + switch($datatype) { | ||
435 | + case 'unsignedByte': | ||
436 | + case 'char': | ||
437 | + $block_size = 1; | ||
438 | + break; | ||
439 | + case 'unicodeChar': | ||
440 | + case 'short': | ||
441 | + $block_size = 2; | ||
442 | + break; | ||
443 | + case 'int': | ||
444 | + case 'float': | ||
445 | + $block_size = 4; | ||
446 | + break; | ||
447 | + case 'long': | ||
448 | + case 'double': | ||
449 | + case 'float_complex': | ||
450 | + $block_size = 8; | ||
451 | + break; | ||
452 | + case 'double_complex': | ||
453 | + $block_size = 16; | ||
454 | + default: | ||
455 | + $block_size = 0; | ||
456 | + break; | ||
457 | + } | ||
458 | + | ||
459 | + if($field_node->getAttribute("arraysize") == NULL) { | ||
460 | + $array_size = $block_size; | ||
461 | + } else if("*" == $field_node->getAttribute("arraysize")) { | ||
462 | + $array_size = unpack("Ns", substr($this->stream, $this->c, 4))["s"] * $block_size; | ||
463 | + $this->c+=4; | ||
464 | + } else { | ||
465 | + $array_size = (int)($field_node->getAttribute("arraysize")) * $block_size; | ||
466 | + } | ||
467 | + return $array_size; | ||
468 | + } | ||
469 | + | ||
470 | + /** Get the VOTable stream content.*/ | ||
471 | + public function parseStream() { | ||
472 | + if (! $this->isValidSchema()) { | ||
473 | + error_log('There is an error on the VOTable: ' . $this->votable_error); | ||
474 | + return null; | ||
475 | + } | ||
476 | + $data = Array(); | ||
477 | + $fields = $this->xp->query($this->queryFields()); | ||
478 | + $resource = $this->xp->query($this->queryResource()); | ||
479 | + $nb_columns = $fields->length; | ||
480 | + $row = Array(); | ||
481 | + $n_value = 0; // index of current value | ||
482 | + $this->c = 0; // initialize cursor position. | ||
483 | + $query_stream = $this->xp->query($this->queryStream())->item(0); | ||
484 | + if($query_stream == NULL) { | ||
485 | + $this->votable_error = "There is no STREAM node in the VOTable file."; | ||
486 | + return null; | ||
487 | + } | ||
488 | + $this->stream = base64_decode($query_stream->textContent); | ||
489 | + $stream_len = strlen($this->stream); | ||
490 | + if($stream_len == 0) { | ||
491 | + $this->votable_error = "no result"; | ||
492 | + return null; | ||
493 | + } | ||
494 | + while($this->c < strlen($this->stream)) { | ||
495 | + $col_id = $n_value % $nb_columns; | ||
496 | + $field_node = $fields->item($col_id); | ||
497 | + | ||
498 | + if($col_id == 0) { | ||
499 | + $row = Array(); | ||
500 | + } | ||
501 | + $row[$field_node->getAttribute("ID")] = $this->process_datablock($field_node); | ||
502 | + if($col_id == $nb_columns-1) { | ||
503 | + array_push($data, $row); | ||
504 | + } | ||
505 | + $n_value+=1; | ||
506 | + } | ||
507 | + return $data; | ||
508 | + } | ||
509 | + | ||
510 | + private function JDTodate($jd) { | ||
511 | + list($month, $day, $year) = split('/', JDToGregorian($jd)); | ||
512 | + return "$day/$month/$year"; | ||
513 | + } | ||
514 | + | ||
515 | + private function process_datablock($field_node) { | ||
516 | + $data_type = $field_node->getAttribute("datatype"); | ||
517 | + $row_size = $this->get_row_size($field_node); | ||
518 | + $substr = substr($this->stream, $this->c, $row_size); | ||
519 | + | ||
520 | + switch ($data_type) { | ||
521 | + case 'boolean': | ||
522 | + case 'unsignedByte': | ||
523 | + $b = $substr; | ||
524 | + $res = $b == 'T' || $b == 't' || $b == '1'; | ||
525 | + break; | ||
526 | + case 'char': | ||
527 | + $res = $row_size !=0 ? utf8_encode($substr) : NULL; | ||
528 | + case 'unicodeChar': | ||
529 | + $res = $row_size !=0 ? utf8_encode(str_replace("\0", '', $substr)) : NULL; | ||
530 | + break; | ||
531 | + case 'short': | ||
532 | + $res = unpack('ss', $substr)['s']; | ||
533 | + $res = is_nan($res) ? NULL : $res; | ||
534 | + break; | ||
535 | + case 'int': | ||
536 | + $res = unpack('Ns', $substr)['s']; | ||
537 | + $res = is_nan($res) ? NULL : $res; | ||
538 | + break; | ||
539 | + case 'long': | ||
540 | + $res = unpack('Js', $substr)['s']; // /!\ J -> PHP 5.6 only | ||
541 | + $res = is_nan($res) ? NULL : $res; | ||
542 | + break; | ||
543 | + case 'float': | ||
544 | + $res = unpack('fs', $substr)['s']; | ||
545 | + // If machine is little endian: | ||
546 | + if($this->is_little_endian) { | ||
547 | + $res = unpack('f1f', strrev(pack('f', $res)))['f']; | ||
548 | + } | ||
549 | + $res = is_nan($res) ? NULL : $res; | ||
550 | + break; | ||
551 | + case 'double': | ||
552 | + $res = unpack('ds', $substr)['s']; | ||
553 | + // If machine is little endian: | ||
554 | + if($this->is_little_endian) { | ||
555 | + $res = unpack('d1d', strrev(pack('d', $res)))['d']; | ||
556 | + } | ||
557 | + $res = is_nan($res) ? NULL : $res; | ||
558 | + break; | ||
559 | + default: | ||
560 | + $res = NULL; | ||
561 | + error_log("Unknown datatype: $data_type"); | ||
562 | + break; | ||
563 | + } | ||
564 | + $this->c+=$row_size; | ||
565 | + return $res; | ||
566 | + } | ||
390 | 567 | ||
391 | public function getFieldInfo($field) | 568 | public function getFieldInfo($field) |
392 | { | 569 | { |
@@ -0,0 +1,218 @@ | @@ -0,0 +1,218 @@ | ||
1 | +<?php | ||
2 | + | ||
3 | +include(realpath(dirname(__FILE__) . "/config.php")); | ||
4 | +include(CLASSPATH . "VOTableMgr.php"); | ||
5 | + | ||
6 | +$action = preg_replace("/[^a-zA-Z]+/", "", filter_var($_GET['action'], FILTER_SANITIZE_STRING)); | ||
7 | + | ||
8 | +switch ($action) { | ||
9 | + case 'resolver': | ||
10 | + $response = resolver(); | ||
11 | + break; | ||
12 | + case 'getServices': | ||
13 | + $response = getServices(); | ||
14 | + break; | ||
15 | + case 'getNbResults': | ||
16 | + $response = getNbResults(); | ||
17 | + break; | ||
18 | + case 'getGranules': | ||
19 | + $response = getGranules(); | ||
20 | + break; | ||
21 | + default: | ||
22 | + $response = ['success' => false, 'msg' => 'Unknown action: ' . $action]; | ||
23 | + break; | ||
24 | +} | ||
25 | +echo json_encode($response); | ||
26 | + | ||
27 | +function resolver() { | ||
28 | + $input = filter_var($_GET['input'], FILTER_SANITIZE_URL); | ||
29 | + $resolver_url = "http://voparis-registry.obspm.fr/ssodnet/1/autocomplete?q=%22$input%22"; | ||
30 | + | ||
31 | + $response = ['success' => true, 'metaData' => ['root' => 'data', 'messageProperty' => 'msg']]; | ||
32 | + try { | ||
33 | + $content = file_get_contents($resolver_url); | ||
34 | + } catch (Exception $e) { | ||
35 | + error_log('Resolver access error: ' . $e); | ||
36 | + $response['success'] = false; | ||
37 | + $response['msg'] = "Resolver unreachable on $resolver_url."; | ||
38 | + } | ||
39 | + try { | ||
40 | + $result = json_decode($content, true); | ||
41 | + $targets = array(); | ||
42 | + foreach($result['hits'] as $e) { | ||
43 | + $aliases = '<li>' . join('</li><li>', $e['aliases']) . '</li>'; | ||
44 | + $target = array('name' => $e['name'], 'type' => $e['type'], 'parent' => $e['parent'], 'aliases' => $aliases); | ||
45 | + array_push($targets, $target); | ||
46 | + } | ||
47 | + $response['data'] = $targets; | ||
48 | + } catch (Exception $e) { | ||
49 | + error_log('Resolver type error: ' . $e); | ||
50 | + $response['success'] = false; | ||
51 | + $response['msg'] = 'The resolver returned a bad result.'; | ||
52 | + } | ||
53 | + return $response; | ||
54 | +} | ||
55 | + | ||
56 | +function request($access_url, $query) { | ||
57 | + $votMgr = new VOTableMgr; | ||
58 | + $params = 'FORMAT=votable&LANG=ADQL&REQUEST=doQuery'; | ||
59 | + $url = $access_url . '/sync?' . $params . '&QUERY=' . urlencode(preg_replace('/\s+/', ' ', $query)); // remove also multiple whitespaces | ||
60 | + | ||
61 | + $votMgr->load($url); | ||
62 | + $data = $votMgr->parseStream(); | ||
63 | + $error = $votMgr->getVotableError(); | ||
64 | + | ||
65 | + $response = ['query' => $query, 'metaData' => ['root' => 'data', 'messageProperty' => 'msg']]; | ||
66 | + if($error) { | ||
67 | + $response['success'] = false; | ||
68 | + $response['msg'] = $error; | ||
69 | + } else { | ||
70 | + $response['success'] = true; | ||
71 | + $response['data'] = $data; | ||
72 | + } | ||
73 | + return $response; | ||
74 | +} | ||
75 | + | ||
76 | +/* Return the list of available services by querying some usual registries. */ | ||
77 | +function getServices() { | ||
78 | + $registriesURL = ["http://registry.euro-vo.org/regtap/tap", "http://dc.zah.uni-heidelberg.de/tap", "http://gavo.aip.de/tap", "http://reg.g-vo.org/tap"]; | ||
79 | + $columns = ['short_name', 'res_title', 'ivoid', 'access_url', 'table_name', 'content_type', 'creator_seq', 'content_level', 'reference_url', 'created', 'updated']; | ||
80 | + $query = "SELECT DISTINCT " . implode(', ', $columns) . " FROM rr.resource | ||
81 | + NATURAL JOIN rr.res_schema NATURAL JOIN rr.res_table NATURAL JOIN rr.interface NATURAL JOIN rr.res_detail NATURAL JOIN rr.capability | ||
82 | + WHERE standard_id='ivo://ivoa.net/std/tap' AND intf_type='vs:paramhttp' AND detail_xpath='/capability/dataModel/@ivo-id' | ||
83 | + AND 1=ivo_nocasematch(detail_value, 'ivo://vopdc.obspm/std/EpnCore%') AND table_name LIKE '%.epn_core' ORDER BY short_name, table_name"; | ||
84 | + | ||
85 | + $regNumber = 0; | ||
86 | + for(; $regNumber<count($registriesURL) ; $regNumber++) { | ||
87 | + $response = request($registriesURL[$regNumber], $query); | ||
88 | + if($response['success']) { | ||
89 | + // Add several other parameters and remove AMDA | ||
90 | + for($j=0 ; $j<count($response['data']) ; $j++) { | ||
91 | + $response['data'][$j]['id'] = generateServiceId($response['data'][$j]); | ||
92 | + $response['data'][$j]['nb_results'] = -1; | ||
93 | + $response['data'][$j]['info'] = 'Please make a query.'; | ||
94 | + if($response['data'][$j]['id'] == 'cdpp/amda/amdadb') { | ||
95 | + array_splice($response['data'], $j, 1); | ||
96 | + $j-=1; | ||
97 | + } | ||
98 | + } | ||
99 | + if(isset($lastErrorMesage)) { | ||
100 | + $response['msg'] = $lastErrorMesage; | ||
101 | + } | ||
102 | + break; | ||
103 | + } else { | ||
104 | + $lastErrorMesage = 'Last tried registry (' . $registriesURL[$regNumber] . ') returned this error: ' . $response['msg'] . '.'; | ||
105 | + } | ||
106 | + } | ||
107 | + if(!$response['success']) { | ||
108 | + $response['msg'] = 'Can not access any of these registries: ' . implode(', ', $registriesURL) . ', last error message is ' . $lastErrorMesage; | ||
109 | + } | ||
110 | + return $response; | ||
111 | +} | ||
112 | + | ||
113 | +function getNbResults() { | ||
114 | + $url = filter_var($_GET['url'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
115 | + $tableName = filter_var($_GET['tableName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
116 | + $targetName = filter_var($_GET['targetName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
117 | + $productTypes = filter_var($_GET['productTypes'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
118 | + $timeMin = filter_var($_GET['timeMin'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
119 | + $timeMax = filter_var($_GET['timeMax'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
120 | + | ||
121 | + $query = "SELECT COUNT(*) AS nb_rows FROM $tableName" . createFilter($targetName, $productTypes, $timeMin, $timeMax); | ||
122 | + $response = request($url, $query); | ||
123 | + if($response['success']) { | ||
124 | + $response['success'] = false; | ||
125 | + $response['msg'] = 'The service returned a bad value, can not get the number of results.'; | ||
126 | + if(count($response['data']) < 1) { | ||
127 | + error_log('getNbResults error: Too few returned raws.'); | ||
128 | + } else if(count($response['data']) > 1) { | ||
129 | + error_log('getNbResults error: Too many returned raws.'); | ||
130 | + } else if(!array_key_exists(0, $response['data'])) { | ||
131 | + error_log('getNbResults error: cant find raw item 0'); | ||
132 | + } else if(is_null($response['data'][0])) { | ||
133 | + error_log('getNbResults error: The returned raw is null.'); | ||
134 | + } else if(!array_key_exists("nb_rows", $response['data'][0])) { | ||
135 | + error_log('getNbResults error: cant find nb_rows.'); | ||
136 | + } else if(!is_numeric($response['data'][0]['nb_rows'])) { | ||
137 | + error_log('getNbResults error: The returned value is not a number.'); | ||
138 | + } else { | ||
139 | + $response['success'] = true; | ||
140 | + $response['data'] = (int)($response['data'][0]['nb_rows']); | ||
141 | + $response['msg'] = 'The service returned ' . ($response['data'] == 0 ? 'no' : $response['data']) . ' result' . ($response['data'] > 1 ? 's' : '') . ' for the given query.'; | ||
142 | + } | ||
143 | + } | ||
144 | + return $response; | ||
145 | +} | ||
146 | + | ||
147 | +function getGranules() { | ||
148 | + // TODO: simplify this | ||
149 | + $url = filter_var($_GET['url'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
150 | + $tableName = filter_var($_GET['tableName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
151 | + $targetName = filter_var($_GET['targetName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
152 | + $productTypes = filter_var($_GET['productTypes'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
153 | + $timeMin = filter_var($_GET['timeMin'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
154 | + $timeMax = filter_var($_GET['timeMax'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
155 | + $start = filter_var($_GET['start'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
156 | + $limit = filter_var($_GET['limit'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
157 | + $nbRes = filter_var($_GET['nbRes'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); | ||
158 | + | ||
159 | + $filter = createFilter($targetName, $productTypes, $timeMin, $timeMax); | ||
160 | + $query = "SELECT TOP $limit * FROM $tableName $filter OFFSET $start"; | ||
161 | + // error_log('getGranules query: ' . $query); | ||
162 | + $response = request($url, $query); | ||
163 | + if($response['success']) { | ||
164 | + $visibleColumns = ['granule_gid', 'obs_id', 'dataproduct_type', 'time_min', 'time_max', 'instrument_name', 'processing_level', 'access_estsize', 'thumbnail_url', 'access_url']; // rest are hidden | ||
165 | + $names = ['granule_gid' => 'GID', 'dataproduct_type' => 'Type', 'processing_level' => 'Proc. lvl', 'access_estsize' => 'Size', 'access_url' => 'URL']; // default: pretty printed key name | ||
166 | + $renderers = ['dataproduct_type' => 'type', 'time_min' => 'date', 'time_max' => 'date', 'processing_level' => 'proc_lvl', | ||
167 | + 'access_estsize' => 'size', 'thumbnail_url' => 'img', 'access_url' => 'link', 'access_format' => 'format']; // default: text | ||
168 | + $widths = ['obs_id' => 75, 'time_min' => 75, 'time_max' => 75, 'instrument_name' => 75, 'processing_level' => 60]; // default: 50 | ||
169 | + | ||
170 | + $fields = array(); | ||
171 | + $columns = array(); | ||
172 | + foreach($response['data'][0] as $key => $value) { | ||
173 | + $fields[] = ['name' => $key, 'type' => 'string']; | ||
174 | + $columns[] = [ | ||
175 | + 'dataIndex' => $key, | ||
176 | + 'text' => array_key_exists($key, $names) ? $names[$key] : ucfirst(str_replace('_', ' ', $key)), | ||
177 | + 'width' => array_key_exists($key, $widths) ? $widths[$key] : 50, | ||
178 | + 'hidden' => !in_array($key, $visibleColumns), | ||
179 | + 'renderer' => 'granule.' . (array_key_exists($key, $renderers) ? $renderers[$key] : 'text') | ||
180 | + ]; | ||
181 | + } | ||
182 | + | ||
183 | + $response['total'] = $nbRes; | ||
184 | + $response['metaData']['fields'] = $fields; | ||
185 | + $response['metaData']['columns'] = $columns; | ||
186 | + } | ||
187 | + return $response; | ||
188 | +} | ||
189 | + | ||
190 | +// ----- utils ----- | ||
191 | + | ||
192 | +function createFilter($targetName, $productTypes, $timeMin, $timeMax) { | ||
193 | + $filter = array(); | ||
194 | + if($targetName) { | ||
195 | + array_push($filter, "target_name = '$targetName'"); | ||
196 | + } | ||
197 | + if($productTypes) { | ||
198 | + array_push($filter, "dataproduct_type IN ('" . join("', '", explode(';', $productTypes)) . "')"); | ||
199 | + } | ||
200 | + if($timeMin) { | ||
201 | + array_push($filter, "time_min >= " . dateToJD($timeMin)); | ||
202 | + } | ||
203 | + if($timeMax) { | ||
204 | + array_push($filter, "time_max <= " . dateToJD($timeMax)); | ||
205 | + } | ||
206 | + return (count($filter) > 0 ? ' WHERE ' . join(' AND ', $filter) : ''); | ||
207 | +} | ||
208 | + | ||
209 | +/* Generate a unique service identifier from the service ivoid and the table name. */ | ||
210 | +function generateServiceId($service) { | ||
211 | + return str_replace(['ivo://', '.epn_core'], '', $service['ivoid'] . '/' . $service['table_name']); | ||
212 | +} | ||
213 | + | ||
214 | +function dateToJD($gregorian_date) { | ||
215 | + list($day, $month, $year, $hours, $minutes, $seconds) = preg_split('/[\s\/:]+/', $gregorian_date); | ||
216 | + return GregorianToJD($month, $day, $year) + $hours/24 + $minutes/(24*60) + $seconds/(24*60*60); | ||
217 | +} | ||
218 | +?> |
php/log deleted