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 @@ |
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 | 96 | source : 'amdaDesktop.InteropModule', |
97 | 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 | 106 | info : { |
100 | 107 | id : 'info-win', |
101 | 108 | icon : 'icon-information', |
... | ... | @@ -281,7 +288,7 @@ Ext.define('amdaApp.AmdaApp', { |
281 | 288 | { name: 'Create/Modify parameter', iconCls: 'edit', module: 'param-win' }, |
282 | 289 | { name: 'Plot data', iconCls: 'plot', module: 'plot-win'}, |
283 | 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 | 292 | { name: 'Download data', iconCls: 'download_manager', module: 'down-win'}, |
286 | 293 | { name: 'Upload data', iconCls: 'mydata', module: 'up-win'}, |
287 | 294 | { name: 'Manage TimeTables', iconCls: 'timeTable', module: 'timetab-win' }, |
... | ... |
... | ... | @@ -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 | * @author Benjamin RENARD |
8 | 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 | 12 | * 23/04/2012: BRE - file creation |
13 | 13 | */ |
... | ... | @@ -18,7 +18,8 @@ Ext.define('amdaDesktop.InteropModule', { |
18 | 18 | |
19 | 19 | requires: [ |
20 | 20 | 'amdaUI.InteropUI', |
21 | - 'amdaDesktop.SampModule' | |
21 | + 'amdaDesktop.SampModule', | |
22 | + 'amdaDesktop.EpnTapModule' | |
22 | 23 | ], |
23 | 24 | |
24 | 25 | contentId : 'interopUI', |
... | ... | @@ -27,10 +28,11 @@ Ext.define('amdaDesktop.InteropModule', { |
27 | 28 | * @cfg {String} window definitions |
28 | 29 | * @required |
29 | 30 | */ |
30 | - height: 580, | |
31 | - width: 850, | |
31 | + height: 650, | |
32 | + width: 1050, | |
32 | 33 | uiType : 'panelInterop', |
33 | 34 | helpTitle :'Help on Interop Module', |
35 | + helpFile: 'interopHelp.html', | |
34 | 36 | |
35 | 37 | samp : null, |
36 | 38 | |
... | ... | @@ -237,6 +239,13 @@ Ext.define('amdaDesktop.InteropModule', { |
237 | 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 | 249 | generateAladinScript : function(urlList, scriptType) |
241 | 250 | { |
242 | 251 | /*var script="reset;"; //reset all views & all planes |
... | ... | @@ -313,8 +322,7 @@ Ext.define('amdaDesktop.InteropModule', { |
313 | 322 | |
314 | 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 | 326 | var url = urlList[i].url; |
319 | 327 | var name = urlList[i].name; |
320 | 328 | script += 'get File(' + url +','+name+');'; |
... | ... | @@ -421,6 +429,7 @@ Ext.define('amdaDesktop.InteropModule', { |
421 | 429 | } |
422 | 430 | var desktop = this.app.getDesktop(); |
423 | 431 | var win = desktop.getWindow(this.id); |
432 | + var activeTab = (config && 'activeTab' in config) ? config['activeTab']: 1; | |
424 | 433 | |
425 | 434 | activeTab = 1; |
426 | 435 | |
... | ... | @@ -430,9 +439,9 @@ Ext.define('amdaDesktop.InteropModule', { |
430 | 439 | id: this.id, |
431 | 440 | title:this.title, |
432 | 441 | layout: 'anchor', |
433 | - width:600, | |
434 | - height:550, | |
435 | - modal: true, | |
442 | + width: 800, | |
443 | + height: 600, | |
444 | + minWidth: 650, | |
436 | 445 | minimizable: false, |
437 | 446 | iconCls: this.icon, |
438 | 447 | animCollapse:false, |
... | ... | @@ -441,12 +450,27 @@ Ext.define('amdaDesktop.InteropModule', { |
441 | 450 | stateful : true, |
442 | 451 | stateId : this.id, |
443 | 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 | 463 | items : [ |
445 | 464 | { |
446 | 465 | xtype: 'panelInterop', |
447 | 466 | clientsStore : this.sampclientsStore, |
448 | 467 | activeTab : activeTab, |
449 | 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 | 474 | onSwitchConnect : function () |
451 | 475 | { |
452 | 476 | me.switchSampConnect(); |
... | ... |
js/app/models/AmdaNode.js
... | ... | @@ -135,6 +135,9 @@ Ext.define('amdaModel.AmdaNode', { |
135 | 135 | else if (this.get('isParameter')) { |
136 | 136 | itemKind = amdaUI.ExplorerUI.ITEM_KIND_PARA; |
137 | 137 | } |
138 | + else if (this.get('rank')) { | |
139 | + itemKind = amdaUI.ExplorerUI.ITEM_KIND_MISS; | |
140 | + } | |
138 | 141 | // other case |
139 | 142 | else { |
140 | 143 | itemKind = amdaUI.ExplorerUI.ITEM_KIND_DIRE; |
... | ... |
js/app/models/LocalParamNode.js
... | ... | @@ -98,6 +98,10 @@ Ext.define('amdaModel.LocalParamNode', |
98 | 98 | text : 'Close All', |
99 | 99 | hidden : true |
100 | 100 | }, { |
101 | + fnId : 'miss-collapseAll', | |
102 | + text : 'Close All', | |
103 | + hidden : true | |
104 | + }, { | |
101 | 105 | fnId : 'para-plotParam', |
102 | 106 | text : 'Plot Parameter', |
103 | 107 | hidden : true |
... | ... | @@ -121,6 +125,10 @@ Ext.define('amdaModel.LocalParamNode', |
121 | 125 | fnId : 'leaf-downParam', |
122 | 126 | text : 'Download Parameter', |
123 | 127 | hidden : true |
128 | + }, { | |
129 | + fnId : 'miss-epnTap', | |
130 | + text : 'Display EPN-TAP services', | |
131 | + hidden : true | |
124 | 132 | }]; |
125 | 133 | |
126 | 134 | return menuItems; |
... | ... | @@ -157,6 +165,9 @@ Ext.define('amdaModel.LocalParamNode', |
157 | 165 | myDesktopApp.warningMsg("Sorry! access to this parameter is restricted"); |
158 | 166 | |
159 | 167 | break; |
168 | + case 'miss-epnTap': | |
169 | + this.displayEpnTap(); | |
170 | + break; | |
160 | 171 | default: |
161 | 172 | break; |
162 | 173 | } |
... | ... | @@ -245,5 +256,24 @@ Ext.define('amdaModel.LocalParamNode', |
245 | 256 | isParameter : function() |
246 | 257 | { |
247 | 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 @@ |
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
js/app/views/InteropUI.js
1 | 1 | /** |
2 | 2 | * Project : AMDA-NG |
3 | - * Name : InteropUI.js | |
3 | + * Name : InteropUI.js | |
4 | 4 | * @class amdaUI.InteropUI |
5 | 5 | * @extends Ext.tab.Panel |
6 | 6 | * @brief Interop Module UI definition (View) |
7 | 7 | * @author Benjamin RENARD |
8 | 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 | 18 | alias: 'widget.panelInterop', |
19 | 19 | |
20 | 20 | requires: [ |
21 | - 'amdaUI.ParamsMgrUI' | |
21 | + 'amdaUI.ParamsMgrUI', 'amdaUI.EpnTapUI' | |
22 | 22 | ], |
23 | 23 | |
24 | 24 | constructor: function(config) { |
... | ... | @@ -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 | ****************************************************************************** |
13 | 13 | */ |
14 | 14 | |
15 | - | |
15 | +/** | |
16 | +config: | |
17 | +- durationLimit: The maximum value of the duration days field (9999 by default). | |
18 | +*/ | |
16 | 19 | Ext.define('amdaUI.IntervalUI', { |
17 | 20 | extend: 'Ext.container.Container', |
18 | 21 | |
19 | 22 | alias: 'widget.intervalSelector', |
20 | - | |
21 | 23 | activeField : null, |
22 | 24 | |
23 | 25 | constructor: function(config) { |
... | ... | @@ -25,6 +27,12 @@ Ext.define('amdaUI.IntervalUI', { |
25 | 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 | 36 | setInterval : function(startDate,stopDate) |
29 | 37 | { |
30 | 38 | // get the search form |
... | ... | @@ -43,6 +51,31 @@ Ext.define('amdaUI.IntervalUI', { |
43 | 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 | 79 | getStartTime : function() |
47 | 80 | { |
48 | 81 | // get the search form |
... | ... | @@ -53,6 +86,10 @@ Ext.define('amdaUI.IntervalUI', { |
53 | 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 | 93 | getStopTime : function() |
57 | 94 | { |
58 | 95 | // get the search form |
... | ... | @@ -63,14 +100,17 @@ Ext.define('amdaUI.IntervalUI', { |
63 | 100 | return stopField.getValue(); |
64 | 101 | }, |
65 | 102 | |
66 | - updateDuration: function() { | |
103 | + /* | |
104 | + #### Private methods from here #### | |
105 | + */ | |
67 | 106 | |
107 | + updateDuration: function() { | |
68 | 108 | // get the search form |
69 | 109 | var form = this.findParentByType('form').getForm(); |
70 | 110 | // get start value |
71 | - var start = form.findField('startDate').getValue(); | |
111 | + var start = this.getStartTime(); | |
72 | 112 | // get stop value |
73 | - var stop = form.findField('stopDate').getValue(); | |
113 | + var stop = this.getStopTime(); | |
74 | 114 | // if duration computable |
75 | 115 | if (stop != null && start != null) { |
76 | 116 | |
... | ... | @@ -81,15 +121,14 @@ Ext.define('amdaUI.IntervalUI', { |
81 | 121 | |
82 | 122 | var durationDays = Math.floor(diff/86400000); |
83 | 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 | 125 | form.findField('durationHour').setValue(Ext.String.leftPad(Math.floor(diff/3600000 % 24),2,'0')); |
86 | 126 | form.findField('durationMin').setValue(Ext.String.leftPad(Math.floor(diff/60000 % 60),2,'0')); |
87 | 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 | 144 | }, |
106 | 145 | |
107 | 146 | updateStop: function() { |
108 | - | |
109 | 147 | // get the time form |
110 | 148 | var form = this.findParentByType('form').getForm(); |
111 | 149 | // get duration value |
... | ... | @@ -150,10 +188,15 @@ Ext.define('amdaUI.IntervalUI', { |
150 | 188 | layout: {type: 'hbox', align: 'middle'}, |
151 | 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 | 200 | listeners: { |
158 | 201 | change: onChangeField, |
159 | 202 | focus: function(field) { |
... | ... | @@ -168,7 +211,7 @@ Ext.define('amdaUI.IntervalUI', { |
168 | 211 | |
169 | 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 | 217 | getStopField : function() |
... | ... | @@ -180,9 +223,12 @@ Ext.define('amdaUI.IntervalUI', { |
180 | 223 | { |
181 | 224 | return { |
182 | 225 | layout: {type: 'hbox', align: 'middle'}, |
183 | - height: 45, | |
226 | + margin: 0, | |
184 | 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 | 232 | allowBlank: false, maxLength:2, enforceMaxLength : true, |
187 | 233 | hideTrigger: true, |
188 | 234 | regex: /^[0-9]([0-9])*$/i, |
... | ... | @@ -196,24 +242,26 @@ Ext.define('amdaUI.IntervalUI', { |
196 | 242 | focus: function(field) { |
197 | 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 | 251 | scope : this |
200 | 252 | } |
201 | 253 | }, |
202 | 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 | 263 | init : function(config) { |
264 | + this.durationLimit = config.durationLimit == null ? 9999 : config.durationLimit; // Set duration limit to 9999 by default | |
217 | 265 | |
218 | 266 | var me = this; |
219 | 267 | |
... | ... | @@ -222,7 +270,7 @@ Ext.define('amdaUI.IntervalUI', { |
222 | 270 | plain: true, |
223 | 271 | flex: 1, |
224 | 272 | layout: 'anchor', |
225 | - defaults: { height : 30, xtype : 'container'}, | |
273 | + defaults: { margin: '0 0 5 0', xtype : 'container'}, | |
226 | 274 | |
227 | 275 | items: [ |
228 | 276 | me.getStartField(), |
... | ... |
js/resources/css/amda.css
php/classes/VOTableMgr.php
... | ... | @@ -4,14 +4,17 @@ |
4 | 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 | 19 | function __construct() |
17 | 20 | { |
... | ... | @@ -27,36 +30,58 @@ class VOTableMgr |
27 | 30 | |
28 | 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 | 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 | 87 | //ToDo - BRE - add validation!! |
... | ... | @@ -117,6 +142,10 @@ class VOTableMgr |
117 | 142 | return "//x:RESOURCE"; |
118 | 143 | } |
119 | 144 | |
145 | + protected function queryResourceInfo() { | |
146 | + return $this->queryResource()."/x:INFO"; | |
147 | + } | |
148 | + | |
120 | 149 | protected function queryTable() |
121 | 150 | { |
122 | 151 | return $this->queryResource()."/x:TABLE"; |
... | ... | @@ -161,6 +190,13 @@ class VOTableMgr |
161 | 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 | 202 | public function getVersion() |
... | ... | @@ -387,6 +423,147 @@ class VOTableMgr |
387 | 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 | 568 | public function getFieldInfo($field) |
392 | 569 | { |
... | ... |
... | ... | @@ -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