Commit 30cd92df4e9fa7ad0efa02eafd71124b36fc4dba

Authored by Roipoussiere
2 parents 10200969 11f8b45b

Fix merge conflicts

.gitignore
... ... @@ -14,6 +14,7 @@ php/bin
14 14 *~
15 15 logs/*
16 16 php/log
  17 +_test*
17 18  
18 19 # Ignore these files to safely work on a configured local project:
19 20 desktop.php
... ...
help/images/epntap_from_tree.png 0 → 100644

25.3 KB

help/interopHelp.html 0 → 100644
... ... @@ -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(&#39;amdaApp.AmdaApp&#39;, {
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(&#39;amdaApp.AmdaApp&#39;, {
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' },
... ...
js/app/controllers/EpnTapModule.js 0 → 100644
... ... @@ -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(&#39;amdaDesktop.InteropModule&#39;, {
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(&#39;amdaDesktop.InteropModule&#39;, {
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(&#39;amdaDesktop.InteropModule&#39;, {
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(&#39;amdaDesktop.InteropModule&#39;, {
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(&#39;amdaDesktop.InteropModule&#39;, {
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(&#39;amdaDesktop.InteropModule&#39;, {
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(&#39;amdaDesktop.InteropModule&#39;, {
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(&#39;amdaModel.AmdaNode&#39;, {
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(&#39;amdaModel.LocalParamNode&#39;,
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(&#39;amdaModel.LocalParamNode&#39;,
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(&#39;amdaModel.LocalParamNode&#39;,
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(&#39;amdaModel.LocalParamNode&#39;,
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 });
... ...
js/app/views/EpnTapUI.js 0 → 100644
... ... @@ -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, '&#39;').replace(/"/g, '&#34;');
  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, '&#39;').replace(/"/g, '&#34;');
  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 + '">&#x1F5D7;</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(&#39;amdaUI.ExplorerUI&#39;, {
100 100 ITEM_KIND_LEAF : 'leaf',
101 101 ITEM_KIND_DIRE : 'dire',
102 102 ITEM_KIND_PARA : 'para',
  103 + ITEM_KIND_MISS : 'miss'
103 104 },
104 105  
105 106 initComponent : function (config) {
... ...
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(&#39;amdaUI.InteropUI&#39;, {
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(&#39;amdaUI.InteropUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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(&#39;amdaUI.IntervalUI&#39;, {
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/lib/ext 0 → 120000
... ... @@ -0,0 +1 @@
  1 +/home/nathanael/CDPP/AMDA_CLIENT/ext-4.2.4.1720
0 2 \ No newline at end of file
... ...
js/resources/css/amda.css
... ... @@ -488,3 +488,11 @@ p + p {
488 488 filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100) !important;
489 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 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 {
... ...
php/epntap.php 0 → 100644
... ... @@ -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