/** * Project: AMDA-NG * Name: EpnTapUI.js * @class amdaUI.EpnTapUI * @extends Ext.tab.Panel * @author Nathanael JOURDANE * 24/10/2016: file creation */ Ext.require(['Ext.grid.plugin.BufferedRenderer']); /** `productTypesStore`: An ExtJS Store containing the list of the different data product types defined on all granules, on all available EPN-TAP services (defined in `generic_data/EpnTapData/metadata.json`, updated periodically with a cron script). This list is used to fill the `productTypeCB` combo box, which is initilized in `EpnTapModule` at the panel creation. - `id`: the data product type IDs, according to the EPN-TAP specification (see https://voparis-confluence.obspm.fr/pages/viewpage.action?pageId=1148225); - `name`: the data product name, according to the EPN-TAP specification (ibid). These IDs and names are hard-defined in the JSon file `generic_data/EpnTapData/dataproduct_types.json`. Notes: - if a granule contains a data product type which is not conform to the EPN-TAP definition (ibid), it is not displayed in this store and an information message is displayed on the JavaScript console during the panel creation. - if a data product type is not present in any of the granules from the EPN-TAP services, it is not present in this store. */ Ext.create('Ext.data.Store', { storeId: 'productTypesStore', autoLoad: true, fields: ['id', 'name', 'desc'], data: [ {'id': 'all', 'name': '--All--', 'desc': 'Select all produt types.'}, {'id': 'clear', 'name': '--Clear--', 'desc': 'Clear the selection.'}, {'id': 'im', 'name': 'Image', 'desc': '2D series of values depending on 2 spatial axes, with measured parameters.'}, {'id': 'ma', 'name': 'Map', 'desc': '2D series of values depending on 2 spatial axes, with derived parameters.'}, {'id': 'sp', 'name': 'Spectrum', 'desc': '1D series of values depending on a spectral axis (or Frequency, Energy, Mass,...).'}, {'id': 'ds', 'name': 'Dynamic spectrum', 'desc': '2D series of values depending on time and on a spectral axis (Frequency, Energy, Mass,...), FoV is homogeneous.'}, {'id': 'sc', 'name': 'Spectral cube', 'desc': '3D series of values depending on 2 spatial axes and on a spectral axis (Frequency, Energy, Mass,..).'}, {'id': 'pr', 'name': 'Profile', 'desc': '1D series of values depending on a spatial axis.'}, {'id': 'vo', 'name': 'Volume', 'desc': '3D series of values depending on 3 spatial axes (spatial coordinates or tabulated values in a volumic grid).'}, {'id': 'mo', 'name': 'Movie', 'desc': '3D series of values depending on 2 spatial axes and on time.'}, // {'id': 'cu', 'name': 'Cube', 'desc': '.'}, {'id': 'ts', 'name': 'Time series', 'desc': '1D series of values depending on time.'}, {'id': 'ca', 'name': 'Catalogue', 'desc': '1D list of elements.'}, {'id': 'ci', 'name': 'Catalogue item', 'desc': '0D list of elements.'} ] }); /** `targetNamesStore`: An ExtJS Store containing the list of the different target names defined on all granules, on all available EPN-TAP services (defined in `generic_data/EpnTapData/metadata.json`, updated periodically with a cron script), which match with the selected data product and target class. This list is used to fill the `targetNameCB` combo box, which is updated by `EpnTapModule` each time a new target class (or, by transitivity, product type) is selected. - `id`: the target name in lowercase, with the underscore between each word; - `name`: the target name, capitalized with spaces between each word (done `EpnTapModule.prettify()`). */ Ext.create('Ext.data.Store', { storeId: 'targetNamesStore', fields: ['id', 'name', 'type', 'parent', 'aliases'], proxy: { type: 'ajax', url: 'php/epntap.php', extraParams: { action: 'resolver' } // listeners: { // exception: function(proxy, response, operation) { // console.log('Error ', response); //TODO: Use ExtJs alert instead // } // } } }); /** `servicesStore`: An ExtJS Store containing the list of the EPN-TAP services (defined in `generic_data/EpnTapData/metadata.json`, updated periodically with a cron script), which contains at least one granule matching with the granules filter (the selected data product type, target class and target name). This list is used to fill the `servicesGrid` table, which is updated by `EpnTapModule` each time a new target name (or, by transitivity, target class or product type) is selected. - `id`: the database name of the service, according to the `table_name` column from the `rr.res_table` in the registry database; - `nbResults`: the number of granules matching with the granules filter for this service; - `shortName`: the service short name, according to the `short_name` column from the `rr.resource` table in the registry database; - `title`: the service title, according to the `res_title` column from the `rr.resource` table in the registry database; - `accessURL`: the service access URL, according to the `access_url` column from the `rr.interface` table in the registry database. */ Ext.create('Ext.data.Store', { storeId: 'servicesStore', autoLoad: true, fields: [ {name: 'id', type: 'string'}, {name: 'short_name', type: 'string'}, {name: 'res_title', type: 'string'}, {name: 'ivoid', type: 'string'}, {name: 'access_url', type: 'string'}, {name: 'table_name', type: 'string'}, {name: 'content_type', type: 'string'}, {name: 'creator_seq', type: 'string'}, {name: 'content_level', type: 'string'}, {name: 'reference_url', type: 'string'}, {name: 'created', type: 'date', dateFormat: 'c'}, {name: 'updated', type: 'date', dateFormat: 'c'}, {name: 'nb_results', type: 'integer'}, {name: 'info', type: 'string'} ], proxy: { type: 'ajax', url: 'php/epntap.php', extraParams : {action: 'getServices'} }, sorters: [ {property: 'nb_results', direction: 'DESC'}, {property: 'short_name', direction: 'ASC'} ], listeners: { // load: function(record) { console.log(record); } } }); /** `granulesStore`: An ExtJS Store containing the list of granules of the selected service (on `servicesGrid`), which match with the granules filter (the selected data product type, target class and target name). This list is used to fill the `granulesGrid` table, which is updated by `EpnTapModule` each time a new service is selected. - `num`: the line number, according to the order of the query response and the current page (see `currentPageLb`); - `dataproduct_type`: the dataproduct_type EPN-TAP parameter, as defined in https://voparis-confluence.obspm.fr/display/VES/EPN-TAP+V2.0+parameters. - `target_name`: the target_name EPN-TAP parameter (ibid); - `time_min`: the time_min EPN-TAP parameter (ibid); - `time_max`: the time_max EPN-TAP parameter (ibid); - `access_format`: the access_format EPN-TAP parameter (ibid); - `granule_uid`: the granule_uid EPN-TAP parameter (ibid); - `access_estsize`: the access_estsize EPN-TAP parameter (ibid); - `access_url`: the access_url EPN-TAP parameter (ibid); - `thumbnail_url`: the thumbnail_url EPN-TAP parameter (ibid). */ // TODO: Add granules filter (see http://docs.sencha.com/extjs/4.0.7/#!/example/grid-filtering/grid-filter-local.html) Ext.define('GranulesModel', { extend: 'Ext.data.Model' // columns are created dynamically }); Ext.create('Ext.data.Store', { storeId: 'granulesStore', model: 'GranulesModel', buffered: true, autoload: false, pageSize: 500, leadingBufferZone: 0, proxy: { type: 'ajax', url: 'php/epntap.php', reader: { type: 'json', root: 'data'}, simpleSortMode: true }, listeners: { 'beforeprefetch': function(store, operation) { var service = Ext.data.StoreManager.lookup('servicesStore').getById(store.selectedService).data; store.getProxy().extraParams = { 'action': 'getGranules', 'url': service['access_url'], 'tableName': service['table_name'], 'targetName': Ext.getCmp('epnTapTargetNameCB').rawValue, 'productTypes': Ext.getCmp('epnTapProductTypeCB').value.join(';'), 'timeMin': Ext.Date.format(Ext.getCmp('epnTapTimeSelector').getStartTime(), 'd/m/Y H:i:s'), 'timeMax': Ext.Date.format(Ext.getCmp('epnTapTimeSelector').getStopTime(), 'd/m/Y H:i:s'), 'nbRes': service['nb_results'] }; }, 'prefetch': function(store, records, successful, operation) { // console.log('(prefetch) operation ' + (successful ? 'success' : 'failed') + ': ', operation); // console.log(operation.params); // console.log(Ext.decode(operation.response.responseText)); }, 'metachange': function(store, meta) { if(meta.metaHash != store.metaHash) { Ext.getCmp('epnTapGranulesGrid').reconfigure(store, meta.columns); store.metaHash = meta.metaHash; } } } }); /** Error are not displayed here, use try/catch each time it's necessary. */ Ext.define('App.util.Format', { override: 'Ext.util.Format', // Utils 'prettify': function(data) { return data.charAt(0).toUpperCase() + data.replace(/_/g, ' ').substr(1).toLowerCase(); }, // Services grid 'serviceTooltip': function(value, data) { for (var key in data) { if(typeof data[key] == 'string' && data[key] != '') { data[key] = data[key].replace(/'/g, ''').replace(/"/g, '"'); } } var infoColor = data.nb_results == -2 ? 'IndianRed' : 'green'; var info = data.info.length > 0 ? '

' + data.info + '

' : ''; var colums = ['res_title', 'ivoid', 'access_url', 'table_name', 'content_type', 'creator_seq', 'content_level', 'reference_url', 'created', 'updated']; var details = ''; for (var key in colums) { if(data[colums[key]] !== '') { var val = colums[key] === 'content_level' ? data[colums[key]].replace(/#/g, ', ') : data[colums[key]]; details += '
  • ' + Ext.util.Format.prettify(colums[key]) + ': ' + val + '
  • '; } } return Ext.String.format("
    {0}
    ", value, info, details); }, 'service.text': function(data, metadata, record) { return Ext.util.Format.serviceTooltip(data, record.data); }, 'service.number': function(data, metadata, record) { value = '' + data; if(data < 0) { value = '-'; } else if(data >= 1000*1000) { value = (data/(1000*1000)).toPrecision(3) + 'm'; } else if(data >= 1000) { value = (data/1000).toPrecision(3) + 'k'; } return Ext.util.Format.serviceTooltip(value, record.data); }, // Granules grid 'granuleTooltip': function(value, data) { for (var key in data) { if(typeof data[key] == 'string' && data[key] != '') { data[key] = data[key].replace(/'/g, ''').replace(/"/g, '"'); } } var tooltip = ''; return Ext.String.format("
    {1}
    ", tooltip, value); }, 'granule.text': function(data, metadata, record) { return Ext.util.Format.granuleTooltip('

    ' + data + '

    ', record.data); }, 'granule.link': function(data, metadata, record) { return Ext.util.Format.granuleTooltip('🗗', record.data); }, 'granule.img': function(data, metadata, record) { return Ext.util.Format.granuleTooltip('', record.data); }, 'granule.type': function(data, metadata, record) { var productTypeDict = Ext.data.StoreManager.lookup('productTypesStore').data.map; return Ext.util.Format.granuleTooltip('

    ' + productTypeDict[data].data.name + '

    ', record.data); }, 'granule.size': function(data, metadata, record) { var size = parseInt(data); var txt = ''; if (isNaN(size)) { } else if (size >= 1024*1024) { txt = (size/(1024*1024)).toPrecision(3) + 'Go'; } else if (size >= 1024) { txt = (size/1024).toPrecision(3) + 'Mo'; } else { txt = size + 'Ko'; } return Ext.util.Format.granuleTooltip('

    ' + txt + '

    ', record.data); }, 'granule.proc_lvl': function(data, metadata, record) { var levels = {1: 'Raw', 2: 'Edited', 3: 'Calibrated', 4: 'Resampled', 5: 'Derived', 6: 'Ancillary'}; return Ext.util.Format.granuleTooltip((data in levels) ? '

    ' + levels[data] + '

    ' : '' + data + '', record.data); }, 'granule.date': function(data, metadata, record) { if(isNaN(data)) { return ''; } var f = Number(data) + 1401 + Math.floor((Math.floor((4 * Number(data) + 274277) / 146097) * 3) / 4) - 38; var e = 4 * f + 3; var g = Math.floor((e % 1461) / 4); var h = 5 * g + 2; var D = Math.floor((h % 153) / 5) + 1; var M = ((Math.floor(h / 153) + 2) % 12) + 1; var Y = Math.floor(e / 1461) - 4716 + Math.floor((12 + 2 - M) / 12); return Ext.util.Format.granuleTooltip('

    ' + Ext.Date.format(new Date(Y, M-1, D), 'Y/m/d') + '

    ', record.data); }, 'granule.format': function(data, metadata, record) { var mimetypeDict = { 'application/fits': 'fits', 'application/x-pds': 'pds', 'image/x-pds': 'pds', 'application/gml+xml': 'gml', 'application/json': 'json', 'application/octet-stream': 'bin, idl, envi or matlab', 'application/pdf': 'pdf', 'application/postscript': 'ps', 'application/vnd.geo+json': 'geojson', 'application/vnd.google-earth.kml+xml': 'kml', 'application/vnd.google-earth.kmz': 'kmz', 'application/vnd.ms-excel': 'xls', 'application/x-asdm': 'asdm', 'application/x-cdf': 'cdf', 'application/x-cdf-istp': 'cdf', 'application/x-cdf-pds4': 'cdf', 'application/x-cef1': 'cef1', 'application/x-cef2': 'cef2', 'application/x-directory': 'dir', 'application/x-fits-bintable': 'bintable', 'application/x-fits-euro3d': 'euro3d', 'application/x-fits-mef': 'mef', 'application/x-geotiff': 'geotiff', 'application/x-hdf': 'hdf', 'application/x-netcdf': 'nc', 'application/x-netcdf4': 'nc', 'application/x-tar': 'tar', 'application/x-tar-gzip': 'gtar', 'application/x-votable+xml': 'votable', 'application/x-votable+xml;content=datalink': 'votable', 'application/zip': 'zip', 'image/fits': 'fits', 'image/gif': 'gif', 'image/jpeg': 'jpeg', 'image/png': 'png', 'image/tiff': 'tiff', 'image/x-fits-gzip': 'fits', 'image/x-fits-hcompress': 'fits', 'text/csv': 'csv', 'text/html': 'html', 'text/plain': 'txt', 'text/tab-separated-values': 'tsv', 'text/xml': 'xml', 'video/mpeg': 'mpeg', 'video/quicktime': 'mov', 'video/x-msvideo': 'avi' }; return Ext.util.Format.granuleTooltip((data in mimetypeDict) ? '

    ' + mimetypeDict[data] + '

    ' : '' + data + '', record.data); } }); /** `EpnTapUI`: The view of the AMDA EPN-TAP module, allowing the user to query and display granules information from EPN-TAP services. Note: The controller part of this module is defined in `js/app/controller/EpnTapModule`. */ Ext.define('amdaUI.EpnTapUI', { extend: 'Ext.panel.Panel', alias: 'widget.panelEpnTap', requires: ['amdaUI.IntervalUI'], /** Method constructor, which basically call the `init()` method to create the EpnTap panel. */ constructor: function(config) { this.init(config); this.callParent(arguments); }, /** Create all the EpnTapPanel UI elements, and apply the AMDA module `config` (which includes the created items). When the panel is correctly rendered, the panel triggers `EpnTapModule.onWindowLoaded()`. Note: All the UI elements creation are defined as functions in this init method and not as methods in order to make them private (ie. to avoid `EpnTapUI.createServicesGrid();`, which doesn't make sense). */ init: function(config) { var myConf = { id: 'epntapTab', title: 'EPN-TAP', layout: 'fit', items: [{ xtype: 'container', layout: { type: 'vbox', pack: 'start', align: 'stretch'}, items: [ this.createServiceFilterPanel(), this.createGridsPanel() ] }] }; Ext.apply(this, Ext.apply(arguments, myConf)); }, /*************************** *** Service filter panel *** ***************************/ /** Create `epnTapServiceFilterPanel`, an ExtJS Panel containing two containers: - the left container, containing the combo boxes (for product type, target class and target name) and the navigation panel; - the right container, containing the time selector. */ createServiceFilterPanel: function() { return { xtype: 'form', id: 'epnTapServiceFilterPanel', layout: { type: 'hbox', pack: 'start', align: 'stretch' }, region: 'north', defaults: { margin: '5 0 5 5'}, items: [{ // Left part xtype : 'container', flex: 1, items: [ this.createTargetNameCB(), this.createProductTypeCB() ] }, { // Middle part xtype : 'container', flex: 1, items: [ this.createTimeSelector() ] }, { // Right part xtype : 'container', items: [ this.createSendButton() ] }] }; }, /** Create `epnTapTargetNameCB`, an ExtJS ComboBox, containing a list of target names corresponding to the selected target class, as defined in `targetNamesStore`, which is initilized by `EpnTapModule`. The selection of a target name triggers `EpnTapModule.onTargetNameCBChanged()`, which basically updates `granulesGrid`. */ createTargetNameCB: function() { return { xtype: 'combobox', id: 'epnTapTargetNameCB', fieldLabel: 'Target name', emptyText: 'Earth, Saturn, 67P, ...', store: Ext.data.StoreManager.lookup('targetNamesStore'), queryMode: 'remote', queryParam: 'input', displayField: 'name', valueField: 'id', margin: '15 0 5 0', labelWidth: 71, minWidth: 20, minChars: 2, hideTrigger: true, listConfig: { getInnerTpl: function() { return '
    {name}
    '; } }, listeners: { render: function(cb) { new Ext.ToolTip({ target: cb.getEl(), html: 'Start to type a text then select a target name (required).' }); } } }; }, /** Create `epnTapProductTypeCB`, an ExtJS ComboBox, containing a list of product types as defined in `epnTapProductTypesStore`, which is initilized by `EpnTapModule`. The selection of a produt type triggers `EpnTapModule.onProductTypeCBChanged()`, which basically update `epnTapGranulesGrid`. */ createProductTypeCB: function() { return { xtype: 'combobox', id: 'epnTapProductTypeCB', fieldLabel: 'Product type', emptyText: 'Image, Time series, ...', store: Ext.data.StoreManager.lookup('productTypesStore'), queryMode: 'local', valueField: 'id', multiSelect: true, displayField: 'name', labelWidth: 71, editable: false, listConfig: { getInnerTpl: function() { return '
    {name}
    '; } }, listeners: { change: function(cb, records) { var val = cb.value[cb.value.length - 1]; if(val === 'all') { cb.select(cb.store.getRange().slice(2)); } else if (val === 'clear') { cb.reset(); } }, render: function(cb) { new Ext.ToolTip({ target: cb.getEl(), html: 'Select one or several data product types (required).' }); } } }; }, /** Create `epnTapTimeSelector`, an IntervalUI object, allowing the user to select a time interval (by filling two dates and/or a duration). See `js/app/views/IntervalUI.js` for more information about this component. */ createTimeSelector: function() { return { xtype: 'intervalSelector', id: 'epnTapTimeSelector' }; }, /*********************** *** Navigation panel *** ***********************/ /** The button used to send the query. */ createSendButton: function() { return { xtype: 'button', id: 'epnTapGetBtn', text: 'Get services', disabled: true, width: 140, height: 50, margin: 10 } }, /************ *** Grids *** ************/ /** Create `epnTapGridsPanel`, an ExtJS Panel, containing `epnTapServicesGrid` and `epnTapGranulesGrid`. After the rendering of the grids, it triggers `epnTapModule.onWindowLoaded()`, which basically fill `epnTapServicesGrid` for the first time. */ createGridsPanel: function() { return { xtype: 'panel', id: 'epnTapGridsPanel', layout: 'fit', height: 440, region: 'center', items: [{ xtype: 'container', layout: { type: 'hbox', pack: 'start', align: 'stretch'}, items: [ this.createServicesGrid(), this.createGranulesGrid() ] }] }; }, /** Create `epnTapServicesGrid`, an ExtJS grid containing the EPN-TAP services matching with the filter form (`serviceFilterPanel`). For each service, this grid displays: - the service name; - the number of granules matching with the filter. Other informations are available through an ExtJS Tooltip, on each row: - short name; - title; - access URL. A click on a service triggers `EpnTapModule.onServiceSelected()`, which basically fills `GranulesGrid` by the service granules. */ createServicesGrid: function() { return { xtype: 'grid', id: 'epnTapServicesGrid', title: 'Services', store: Ext.data.StoreManager.lookup('servicesStore'), flex: 1, columns: [ {text: 'Name', dataIndex: 'short_name', flex: 1, renderer: 'service.text'}, {text: 'Nb res.', dataIndex: 'nb_results', width: 50, renderer: 'service.number'} ], viewConfig: { getRowClass: function(record, index) { var nb_res = record.get('nb_results'); if(nb_res == 0 || nb_res == -1) { return 'disabled_row'; } else if (nb_res == -2) { return 'error_row'; } } } }; }, /** Create `epnTapGranulesGrid`, an ExtJS grid containing the granules of the selected service in `epnTapServicesGrid`. For each granule, this grid displays: - the row number; - the dataproduct type; - the target name; - the min and max times; - the format; - the UID (granule identifier); - the estimated size; - the URL; - the thumbnail. Each of these information are displayed in a specific rendering to improve user experience. For more information about these parameters, see https://voparis-confluence.obspm.fr/display/VES/EPN-TAP+V2.0+parameters. Other informations are available through an ExtJS Tooltip on each row: - currently only the granule thumbnail, in full size. A click on a granule triggers `EpnTapModule.onGranuleSelected()`. */ createGranulesGrid: function() { return { xtype: 'grid', id: 'epnTapGranulesGrid', title: 'Granules', store: Ext.data.StoreManager.lookup('granulesStore'), flex: 4, plugins: { ptype: 'bufferedrenderer', trailingBufferZone: 20, leadingBufferZone: 50 }, columns: [] }; } });