From b185823cc64cf068ca684591a59cee41f65bc534 Mon Sep 17 00:00:00 2001
From: Nathanael Jourdane <nathanael.jourdane@irap.omp.eu>
Date: Wed, 1 Feb 2017 17:48:01 +0100
Subject: [PATCH] Use IntervalUI module, code refactoring, add comments on .js files.

---
 js/app/AmdaApp.js                  | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------
 js/app/controllers/EpnTapModule.js | 450 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 js/app/views/EpnTapUI.js           | 857 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 js/app/views/IntervalUI.js         | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------
 php/classes/EpnTapMgr.php          |   4 ++--
 5 files changed, 986 insertions(+), 691 deletions(-)

diff --git a/js/app/AmdaApp.js b/js/app/AmdaApp.js
index 02b5543..baa841c 100755
--- a/js/app/AmdaApp.js
+++ b/js/app/AmdaApp.js
@@ -1,24 +1,24 @@
-/** 
+/**
  * Project  : AMDA-NG4
  * Name     : AmdaApp.js
- * @class   amdaApp.AmdaApp 
+ * @class   amdaApp.AmdaApp
  * @extends Ext.ux.desktop.App
  * @brief   Main class defining Amda Desktop and its Modules
  * @author  Ext JS Library 4.0 Copyright(c) 2006-2011 Sencha Inc. licensing@sencha.com
  */
 
- 
+
 Ext.define('amdaApp.AmdaApp', {
     extend: 'Ext.ux.desktop.App',
 
-    requires: [      
+    requires: [
        'Ext.window.MessageBox',
        'Ext.ux.desktop.ShortcutModel',
        'amdaUI.SampToolBarUI',
        'amdaDesktop.DynamicModule',
        'MyDesktop.Settings'
     ],
-    
+
     dynamicModules: {
 	visu : {
     		id          : 'visu-win',
@@ -26,7 +26,7 @@ Ext.define('amdaApp.AmdaApp', {
     		title       : 'Visualization',
     		source      : 'amdaDesktop.VisuModule',
     		useLauncher : true
-    	},    
+    	},
 	statistics : {
     		id          : 'statistics-win',
     		icon        : 'icon-statistics',
@@ -97,22 +97,22 @@ Ext.define('amdaApp.AmdaApp', {
     		source      : 'amdaDesktop.InteropModule',
     		useLauncher : true
     	},
-      epntap : {
-    		id          : 'epntap-win',
-    		icon        : 'icon-epntap',
-    		title       : 'EPN-TAP data',
-    		source      : 'amdaDesktop.EpnTapModule',
-    		useLauncher : true
-    	},
+		epntap : {
+			id          : 'epntap-win',
+			icon        : 'icon-epntap',
+			title       : 'EPN-TAP data',
+			source      : 'amdaDesktop.EpnTapModule',
+			useLauncher : true
+		},
     	info : {
     		id          : 'info-win',
     		icon        : 'icon-information',
     		title       : 'About AMDA',
     		source      : 'amdaDesktop.AboutModule',
     		useLauncher : false
-    	},	
+    	},
     	explorer : {
-    		id          : 'explorer-win',	
+    		id          : 'explorer-win',
     	    icon        : 'icon-elements',
     	    title       : 'Workspace Explorer',
     	    source      : 'amdaDesktop.ExplorerModule',
@@ -162,13 +162,13 @@ Ext.define('amdaApp.AmdaApp', {
     	    useLauncher : false
     	}
     },
-    
-// IDs of Modules working with  parameters; used in Alias Node 
+
+// IDs of Modules working with  parameters; used in Alias Node
     paramModulesID : ['plot-win', 'param-win', 'search-win', 'down-win'],
-   
+
 // Important system constants
     MAX_UPLOADED_FILE_SIZE : 30000000, // 30MB
-    
+
     listeners : {
 		scope : this,
 		beforeunload : function ()
@@ -179,19 +179,19 @@ Ext.define('amdaApp.AmdaApp', {
 				interopModule.forceSampDisconnect();
 			return true;
 		},
-		ready : function () 
+		ready : function ()
                 {
 				  //AKKA - Clean user WS
 	          AmdaAction.cleanUserWS(function(res,e){},this);
 		}
         },
-    
+
     init: function() {
         // custom logic before getXYZ methods get called...
 
         this.callParent();
 
-        // now ready...      
+        // now ready...
         //override createWindow method of desktop
         Ext.override(Ext.ux.desktop.Desktop, {
             createWindow: function (config, cls) {
@@ -206,16 +206,16 @@ Ext.define('amdaApp.AmdaApp', {
                             w.resizer.widthIncrement = me.xTickSize;
                             w.resizer.heightIncrement = me.yTickSize;
                         }
-                        
+
                         if (w.y < 0)
                             w.el.setY(0);
-                        
+
                         if (w.x + w.width > me.el.getWidth())
                             w.el.setX(me.el.getWidth()-w.width);
-                        
+
                         if (w.y + w.height > me.el.getHeight())
                             w.el.setY((me.el.getHeight()-w.height > 0) ? me.el.getHeight()-w.height : 0);
-                        
+
                     },
                     single: true
                 });
@@ -224,12 +224,12 @@ Ext.define('amdaApp.AmdaApp', {
         });
 
     },
-//create InfoBox 
+//create InfoBox
       infoMsg : function(msg) {
 	  Ext.Msg.show({
 	      title: 'AMDA Info',
 	      cls: 'infoMsg',
-	      msg: msg,	 
+	      msg: msg,
 	      modal: false,
 	      autoScroll: true,
 	      resizable: true,
@@ -237,35 +237,35 @@ Ext.define('amdaApp.AmdaApp', {
 	      buttons: Ext.Msg.OK
 	  });
 	},
-	
-//create WarningBox 
+
+//create WarningBox
       warningMsg : function(msg) {
 	  Ext.Msg.show({
 	      title: 'Attention',
-	      msg: msg,	 
+	      msg: msg,
 	      icon: Ext.Msg.WARNING,
 	      buttons: Ext.Msg.OK
 	  });
 	},
-	
-//create ErrorBox 
+
+//create ErrorBox
       errorMsg : function(msg) {
 	  Ext.Msg.show({
 	      title: 'Failure',
-	      msg: msg,	 
+	      msg: msg,
 	      icon: Ext.Msg.ERROR,
 	      buttons: Ext.Msg.OK
 	  });
-	},	   
-				  
+	},
+
     getModules : function(){
     	var allModules = [];
-    	
+
     	//Add dynamic modules
     	Ext.Object.each(this.dynamicModules, function(key, def) {
     		allModules.push(new amdaDesktop.DynamicModule(def.id, def.icon, def.title, def.source, def.useLauncher));
     	});
-    	
+
         return allModules;
     },
 
@@ -284,9 +284,9 @@ Ext.define('amdaApp.AmdaApp', {
 
             shortcuts: Ext.create('Ext.data.Store', {
                 model: 'Ext.ux.desktop.ShortcutModel',
-                data: [  
-                   { name: 'Help',  iconCls: 'help', module: 'help-win' },  
-                   { name: 'Create/Modify parameter',  iconCls: 'edit', module: 'param-win' },                     
+                data: [
+                   { name: 'Help',  iconCls: 'help', module: 'help-win' },
+                   { name: 'Create/Modify parameter',  iconCls: 'edit', module: 'param-win' },
                    { name: 'Plot data',  iconCls: 'plot', module: 'plot-win'},
                    { name: 'Data mining',  iconCls: 'search', module: 'search-win'},
 		   { name: 'Statistics',  iconCls: 'statistics', module: 'statistics-win'},
@@ -296,8 +296,8 @@ Ext.define('amdaApp.AmdaApp', {
                    { name: 'TimeTables operations',   iconCls: 'operations', module: 'ttsOpe-win' },
                    { name: 'Manage catalogs',  iconCls: 'catalog', module: 'catalog-win'},
                    { name: 'Visualize catalogs',  iconCls: 'visu_catalog', module: 'visu-win'},
-                   { name: 'EPN-TAP',   iconCls: 'epntap', module: 'epntap-win' },
-                   { name: 'Interoperability',   iconCls: 'interop', module: 'interop-win' }
+                   { name: 'Interoperability',   iconCls: 'interop', module: 'interop-win' },
+					{ name: 'EPN-TAP',   iconCls: 'epntap', module: 'epntap-win' }
                ]
             }),
 
@@ -310,7 +310,7 @@ Ext.define('amdaApp.AmdaApp', {
     getStartConfig : function() {
         var me = this, ret = me.callParent();
         return Ext.apply(ret, {
-            title: sessionID, 
+            title: sessionID,
             iconCls: 'icon-user',
             height: 270,
             toolConfig: {
@@ -322,7 +322,7 @@ Ext.define('amdaApp.AmdaApp', {
                         handler: me.onSettings,
                         scope: me
                     },
-                    '-',                    
+                    '-',
      /*               {
                         text : 'Manage Workspaces',
                         iconCls : 'icon-manage-ws',
@@ -372,9 +372,9 @@ Ext.define('amdaApp.AmdaApp', {
                             this.getLoadedModule(moduleId.replace('-tool', ''), true, function(module) {
                             	module.createWindow();
                             });
-                            
+
                         },
-                        scope : this  
+                        scope : this
                     }, {
                         text : 'Help',
                         iconCls : 'icon-help',
@@ -407,24 +407,24 @@ Ext.define('amdaApp.AmdaApp', {
                     }, '-', {
                         text : 'Logout',
                         iconCls : 'logout',
-                        scope : this,                     
+                        scope : this,
                         handler : me.onLogout
                     }
                 ]
             }
         });
     },
-    
+
     getModuleDefinition: function(id) {
     	return this.getModule(id);
     },
-    
+
     getLoadedModule: function(id, forceLoad, onReady) {
     	var moduleDef = this.getModuleDefinition(id);
     	if (!moduleDef)
     		return null;
     	if (!moduleDef.isReady())
-    	{	
+    	{
     		if (forceLoad)
     		{
     			//loadMask.show();
@@ -440,14 +440,14 @@ Ext.define('amdaApp.AmdaApp', {
 			onReady(moduleDef.get());
     	return moduleDef.get();
     },
-    
+
     getTaskbarConfig: function () {
         var ret = this.callParent();
         return Ext.apply(ret, {
             quickStart: [],
             trayItems: [
                 {
-                	name: this.dynamicModules.feedback.title, iconCls: 'icon-feedback', 
+                	name: this.dynamicModules.feedback.title, iconCls: 'icon-feedback',
                 	tooltip: { text: this.dynamicModules.feedback.title, align: 'bl-tl' },
                 	overflowText: this.dynamicModules.feedback.title,
                 	iconCls: this.dynamicModules.feedback.icon,
@@ -460,7 +460,7 @@ Ext.define('amdaApp.AmdaApp', {
                 	}
                 },
                 {
-                    name: this.dynamicModules.info.title, iconCls: 'icon-information', 
+                    name: this.dynamicModules.info.title, iconCls: 'icon-information',
                 	tooltip: { text: this.dynamicModules.info.title, align: 'bl-tl' },
                 	overflowText: this.dynamicModules.info.title,
                 	iconCls: this.dynamicModules.info.icon,
@@ -476,7 +476,7 @@ Ext.define('amdaApp.AmdaApp', {
                     name: 'Logout', iconCls : 'logout',
                     tooltip: { text: 'Logout', align: 'bl-tl' },
                     overflowText: 'Logout',
-                    scope : this,                     
+                    scope : this,
                     handler : this.onLogout
                 },
                 '-',
@@ -486,23 +486,23 @@ Ext.define('amdaApp.AmdaApp', {
     },
 
     onLogout: function (obj,e) {
-    	e.stopEvent();	
-    //	var interopModule = this.getModule(amdaDesktop.InteropModule.id);       
-    	if (isGuest) { 
-           this.guestLogout();             
+    	e.stopEvent();
+    //	var interopModule = this.getModule(amdaDesktop.InteropModule.id);
+    	if (isGuest) {
+           this.guestLogout();
         }
         else {
             this.saveSessionState();
-        }       
+        }
     },
-    
+
     onGetUserInfo : function (result, e){
     	var t = e.getTransaction();
-		if (e.status) 
-		{	
+		if (e.status)
+		{
 			if (result && result.success)
 			{
-				// SUCCESS  
+				// SUCCESS
 				var msg = '<b>Login :</b> '+result['login']+'<br/>';
 				msg += ('<b>Last Name :</b> '+result['name']+'<br/>');
 				msg += ('<b>First Name :</b> '+result['first_name']+'<br/>');
@@ -510,7 +510,7 @@ Ext.define('amdaApp.AmdaApp', {
 				msg += ('<b>Email :</b> '+result['email']+'<br/>');
 				msg += ('<b>Registration date :</b> '+result['date']+'<br/>');
 				msg += ('<b>Receive Newsletter :</b> '+(result['news'] == "1"?"true":"false")+'<br/>');
-				
+
 				this.infoMsg(msg);
 			}
 			else
@@ -523,46 +523,46 @@ Ext.define('amdaApp.AmdaApp', {
 		{
 			// FAILURE
 			this.errorMsg('Cannot get user info : '+e.message);
-		} 
+		}
 	},
-        
-        
+
+
     guestLogout: function()
     {
         Ext.Msg.show({
-                        title : 'Logout', 
+                        title : 'Logout',
                         msg   :'Your guest workspace is to be deleted. Continue logout ?',
                         buttons : Ext.Msg.YESNO,
-                        iconCls : 'logout', 
+                        iconCls : 'logout',
                         fn : function(btn) {
                           if (btn == 'yes')
                           {
                             AmdaAction.logout(true, function(){
                                 sessionID = '';
-                                window.location.href ='index.html';  
-                            });                                                                                      
-                          }                           
+                                window.location.href ='index.html';
+                            });
+                          }
                         }
-                    });               
+                    });
     },
-    
+
     forceLogout: function()
     {
       //  myDesktopApp.warningMsg('Your guest session is finished');
         AmdaAction.logout(true, function(){
                                 sessionID = '';
-                                window.location.href ='index.html';  
-                            });  
+                                window.location.href ='index.html';
+                            });
     },
-        
+
     saveSessionState : function()
     {
         var me = this;
         Ext.Msg.show({
-                        title : 'Logout', 
+                        title : 'Logout',
                         msg   :'Do you want to keep current windows sizes and locations<br/> for the next sessions?',
                         buttons : Ext.Msg.YESNOCANCEL,
-                        iconCls : 'logout', 
+                        iconCls : 'logout',
                         fn : function(btn) {
 	                  if (btn == 'yes')
 	                  {
@@ -577,8 +577,8 @@ Ext.define('amdaApp.AmdaApp', {
                                             window.location.href ='index.html';
                                     }
                             });
-                            Ext.state.Manager.getProvider().saveState();   
-                            AmdaAction.logout();         
+                            Ext.state.Manager.getProvider().saveState();
+                            AmdaAction.logout();
 	                  }
 	                  else if (btn == 'no')
 	                  {
@@ -590,9 +590,9 @@ Ext.define('amdaApp.AmdaApp', {
                                             window.location.href ='index.html';
                                     }
                             });
-                            Ext.state.Manager.getProvider().set(me.desktop.id+'_wallpaper',me.desktop.getWallpaper()); 
-                            Ext.state.Manager.getProvider().saveLastTime(); 
-                            AmdaAction.logout(); 
+                            Ext.state.Manager.getProvider().set(me.desktop.id+'_wallpaper',me.desktop.getWallpaper());
+                            Ext.state.Manager.getProvider().saveLastTime();
+                            AmdaAction.logout();
 	                  }
                         }
     			});
@@ -604,7 +604,7 @@ Ext.define('amdaApp.AmdaApp', {
         });
         dlg.show();
     },
-    
+
     /**
      * initialization at the start of AMDA-NG webApplication
      */
@@ -612,64 +612,63 @@ Ext.define('amdaApp.AmdaApp', {
         moduleIds = new Ext.util.MixedCollection();
         // Adding Workspace Explorer Id
         moduleIds.add(this.dynamicModules.explorer.id);
-                           
+
         moduleIds.each(function(item) {
             this.getLoadedModule(item, true, function (module) {
             	module.createWindow();
             });
         }, this);
-                
+
         if (freeSpace < diskQuota / 20) {
-            myDesktopApp.warningMsg('Think of cleaning up your work space.<br/>Only ' + 
+            myDesktopApp.warningMsg('Think of cleaning up your work space.<br/>Only ' +
             Math.round(freeSpace/1024/1024)+ 'MB of '+Math.round(diskQuota/1024/1024) +
             'MB rests');
         }
-                    
+
         if (isFirstVisit && !isGuest) {
-            if (isSpecialInfo) {                    
+            if (isSpecialInfo) {
                 myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.info.id, true, function(module) {
 			module.createWindow(isSpecialInfo, 'Welcome to AMDA', true);
 		});
             } else {
-	
+
                 myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.help.id, true, function(module) {
 			module.createWindow();
 		});
             }
-        } 
+        }
         else {
             if (isSpecialInfo && !isNewInfo) {
-		myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.info.id, true, function(module) {                    
+		myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.info.id, true, function(module) {
                 	module.createWindow(isSpecialInfo, 'Welcome to AMDA', true);
                	});
             }
         }
-        
+
          if (isNewInfo) {
             myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.info.id, true, function(module) {
-		//  module.createWindow('releaseNotes.' + AMDAVERSION, 'New Release V'+ AMDAVERSION);              
+		//  module.createWindow('releaseNotes.' + AMDAVERSION, 'New Release V'+ AMDAVERSION);
 		module.createWindow(news, 'Amda Latest News');
 	    });
         }
-        
+
         if (isGuest) {
             myDesktopApp.warningMsg("Welcome to Guest Session<br/>Guest session lasts for "+
                                     guestSessionDuration/3600+" h maximum<br/>"+
                                     "For extended use time and functionalities (saved sessions)<br/> please register at amda@irap.omp.eu");
             Ext.Function.defer(myDesktopApp.warningMsg,(guestSessionDuration-300)*1000, this, ["Your session will be closed in 5 min!"]);
-            Ext.Function.defer(myDesktopApp.forceLogout, guestSessionDuration*1000); 
+            Ext.Function.defer(myDesktopApp.forceLogout, guestSessionDuration*1000);
         }
 
-        
-        
+
+
         this.desktop.taskbar.tray.width = 130;
         this.desktop.taskbar.insert(4,new amdaUI.SampToolBarUI({id : 'samptb', onSwitchConnect : function ()
       	  {
         	myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.interop.id, true, function(module) {
         		module.switchSampConnect();
         	});
-        	
+
     	  }}));
     }
 });
-
diff --git a/js/app/controllers/EpnTapModule.js b/js/app/controllers/EpnTapModule.js
index 04f61d4..e8ea920 100644
--- a/js/app/controllers/EpnTapModule.js
+++ b/js/app/controllers/EpnTapModule.js
@@ -7,7 +7,7 @@
  * @author  Nathanael Jourdane
  */
 
-// Load text with Ajax synchronously: takes path to file and optional MIME type
+// Load text with Ajax synchronously: takes path to file and optional MIME type.
 function loadTextFileAjaxSync(filePath, mimeType) {
 	var xmlhttp=new XMLHttpRequest();
 	xmlhttp.open("GET", filePath, false);
@@ -24,47 +24,9 @@ function loadTextFileAjaxSync(filePath, mimeType) {
 	}
 }
 
-function prettify(name) {
-	return name.charAt(0).toUpperCase() + name.replace(/_/g, ' ').substr(1).toLowerCase();
-}
-
-function allPrettify(name) {
-	return 'All ' + (name[name.length-1] == 's' ? name : name + 's').replace(/_/g, ' ').toLowerCase();
-}
-
-function isLatest(newStrDate, oldStrDate) {
-	if (newStrDate === null) {
-		return false;
-	}
-	if (oldStrDate === null) {
-		return true;
-	}
-
-	var newDate = newStrDate.split('/');
-	var oldDate = oldStrDate.split('/');
-
-	if(newDate[2]>oldDate[2]) {
-		return true;
-	} else if(newDate[2]<oldDate[2]) {
-		return false;
-	}
-	if(newDate[1]>oldDate[1]) {
-		return true;
-	} else if(newDate[1]<oldDate[1]) {
-		return false;
-	}
-	if(newDate[0]>oldDate[0]) {
-		return true;
-	} else {
-		return false;
-	}
-}
-
 Ext.define('amdaDesktop.EpnTapModule', {
 
 	extend: 'amdaDesktop.AmdaModule',
-
-	// requires: ['amdaUI.EpnTapUI', 'amdaReader.EpnTapReader'],
 	requires: ['amdaUI.EpnTapUI'],
 	contentId : 'EpnTapUI',
 
@@ -81,8 +43,13 @@ Ext.define('amdaDesktop.EpnTapModule', {
 	width : 1000,
 	height: 550,
 
-	/** @class Module initialisation. */
+	/**
+	Module initialisation.
+	*/
 	init: function() {
+
+		// TODO: Utiliser des stores pour accéder aux fichiers JS !!
+
 		this.metadata = JSON.parse(loadTextFileAjaxSync('../../generic_data/EpnTapData/metadata.json', 'application/json'));
 		this.services = JSON.parse(loadTextFileAjaxSync('../../generic_data/EpnTapData/services.json', 'application/json'));
 		this.productTypeDict = JSON.parse(loadTextFileAjaxSync('../../generic_data/EpnTapData/dataproduct_types.json', 'application/json'));
@@ -101,36 +68,93 @@ Ext.define('amdaDesktop.EpnTapModule', {
 		};
 	},
 
+	/**
+	Capitalize a name and replace underscores with spaces.
+	- `name`: The string to make pretty.
+	*/
+	prettify: function(name) {
+		return name.charAt(0).toUpperCase() + name.replace(/_/g, ' ').substr(1).toLowerCase();
+	},
+
+	/**
+	Capitalize a name, replace underscores with spaces, and write it in a plurial form.
+	- `name`: The string to make pretty.
+	*/
+	allPrettify: function(name) {
+		return 'All ' + (name[name.length-1] == 's' ? name : name + 's').replace(/_/g, ' ').toLowerCase();
+	},
+
+	/**
+	Compare two dates formated as dd/mm/yyyy and return:
+	- `true` if `newStrDate` is more recent than `oldStrDate`, or if `oldStrDate` is null;
+	- `false` if `oldStrDate` is more recent than `newStrDate`, or if `newStrDate` is null;
+	- `null` if a date is not well formed.
+	*/
+	isLatest: function(newStrDate, oldStrDate) {
+		if (newStrDate === null) {
+			return false;
+		}
+		if (oldStrDate === null) {
+			return true;
+		}
+
+		var newDate = newStrDate.split('/');
+		var oldDate = oldStrDate.split('/');
+
+		if(newDate[2]>oldDate[2]) {
+			return true;
+		} else if(newDate[2]<oldDate[2]) {
+			return false;
+		}
+		if(newDate[1]>oldDate[1]) {
+			return true;
+		} else if(newDate[1]<oldDate[1]) {
+			return false;
+		}
+		if(newDate[0]>oldDate[0]) {
+			return true;
+		} else {
+			return false;
+		}
+		return null;
+	},
+
+	/****************************
+	*** Service filter events ***
+	****************************/
+
+	/**
+	Trigerred after the render of `gridsPanel` (containing `servicesGrid` and `granulesGrid`). Among other things,
+	initializes the `productType` combobox and the `servicesGrid` table.
+	*/
 	onWindowLoaded: function() {
-		// UI elements
-		this.dataProdutTypeCB = Ext.getCmp('productTypeCB');
-		this.targetClassCB = Ext.getCmp('targetClassCB');
-		this.targetNameCB = Ext.getCmp('targetNameCB');
-		this.startTimeDF = Ext.getCmp('startTimeDF');
-		this.stopTimeDF = Ext.getCmp('stopTimeDF');
-
-		this.servicesGrid = Ext.getCmp('servicesGrid');
-		this.granulesGrid = Ext.getCmp('granulesGrid');
-
-		this.rowsPerPageNf = Ext.getCmp('rowsPerPageNf');
-		this.currentPageLb = Ext.getCmp('currentPageLb');
-		this.totalPagesLb = Ext.getCmp('totalPagesLb');
-
-		this.previousBtn = Ext.getCmp('previousPageBtn');
-		this.nextBtn = Ext.getCmp('nextPageBtn');
-		this.firstBtn = Ext.getCmp('firstPageBtn');
-		this.lastBtn = Ext.getCmp('lastPageBtn');
-
-		this.dataProdutTypeCB.getStore().removeAll();
-		this.dataProdutTypeCB.getStore().add({'id': 'all', 'name': 'All data product types'});
+
+		this.productTypeCB = Ext.getCmp('epnTapProductTypeCB');
+		this.targetClassCB = Ext.getCmp('epnTapTargetClassCB');
+		this.targetNameCB = Ext.getCmp('epnTapTargetNameCB');
+		this.timeSelector = Ext.getCmp('epnTapTimeSelector');
+		this.rowsPerPageNf = Ext.getCmp('epnTapRowsPerPageNf');
+		this.servicesGrid = Ext.getCmp('epnTapServicesGrid');
+		this.granulesGrid = Ext.getCmp('epnTapGranulesGrid');
+		this.currentPageLb = Ext.getCmp('epnTapCurrentPageLb');
+		this.totalPagesLb = Ext.getCmp('epnTapTotalPagesLb');
+		this.firstPageBtn = Ext.getCmp('epnTapFirstPageBtn');
+		this.previousPageBtn = Ext.getCmp('epnTapPreviousPageBtn');
+		this.nextPageBtn = Ext.getCmp('epnTapNextPageBtn');
+		this.lastPageBtn = Ext.getCmp('epnTapLastPageBtn');
+
+		this.timeSelector.setInterval(new Date(), new Date()); // TODO: use min/max dates
+
+		this.productTypeCB.getStore().removeAll();
+		this.productTypeCB.getStore().add({'id': 'all', 'name': 'All data product types'});
 		for (var productTypeId in this.metadata) {
 			if (productTypeId in this.productTypeDict) {
-				this.dataProdutTypeCB.getStore().add({'id': productTypeId, 'name': prettify(this.productTypeDict[productTypeId])});
+				this.productTypeCB.getStore().add({'id': productTypeId, 'name': this.prettify(this.productTypeDict[productTypeId])});
 			} else {
 				console.log('Unknown data product type "' + productTypeId + '"');
 			}
 		}
-		this.dataProdutTypeCB.select('all');
+		this.productTypeCB.select('all');
 
 		this.targetClassCB.getStore().removeAll();
 		this.targetClassCB.getStore().add({'id': 'all', 'name': 'All target names'});
@@ -145,26 +169,28 @@ Ext.define('amdaDesktop.EpnTapModule', {
 		this.updateServices();
 	},
 
-	// *** form events ***
-
+	/**
+	Trigerred when a new item is selected in `productTypeCB` (see `EpnTapUI.createProductTypeCB()`). Among other things,
+	updates the `targetClassCB` combobox and the `servicesGrid` table.
+	*/
 	onProductTypeCBChanged: function() {
 		this.targetClassCB.getStore().removeAll();
 		this.targetNameCB.getStore().removeAll();
 		this.targetNameCB.disable();
 
-		if (this.dataProdutTypeCB.value == 'all') {
+		if (this.productTypeCB.value == 'all') {
 			this.targetClassCB.disable();
 		} else {
-			var targetClasses = this.metadata[this.dataProdutTypeCB.value];
+			var targetClasses = this.metadata[this.productTypeCB.value];
 
 			if (Object.keys(targetClasses).length == 1) {
-				this.targetClassCB.getStore().add({'id': Object.keys(targetClasses)[0], 'name': prettify(Object.keys(targetClasses)[0])});
+				this.targetClassCB.getStore().add({'id': Object.keys(targetClasses)[0], 'name': this.prettify(Object.keys(targetClasses)[0])});
 				this.targetClassCB.disable();
 				this.targetClassCB.select(this.targetClassCB.getStore().getAt(0)['internalId']);
 			} else {
-				this.targetClassCB.getStore().add({'id': 'all', 'name': allPrettify(this.productTypeDict[this.dataProdutTypeCB.value])});
+				this.targetClassCB.getStore().add({'id': 'all', 'name': this.allPrettify(this.productTypeDict[this.productTypeCB.value])});
 				for (var targetClassId in targetClasses) {
-					this.targetClassCB.getStore().add({'id': targetClassId, 'name': prettify(targetClassId)});
+					this.targetClassCB.getStore().add({'id': targetClassId, 'name': this.prettify(targetClassId)});
 				}
 				this.targetClassCB.select('all');
 				this.targetClassCB.enable();
@@ -175,6 +201,10 @@ Ext.define('amdaDesktop.EpnTapModule', {
 		this.updateServices();
 	},
 
+	/**
+	Trigerred when a new item is selected in `targetClassCB` (see `EpnTapUI.createTargetClassCB()`). Among other things,
+	updates the `targetNameCB` combobox and the `servicesGrid` table.
+	*/
 	onTargetClassCBChanged: function() {
 		this.targetNameCB.getStore().removeAll();
 
@@ -183,16 +213,16 @@ Ext.define('amdaDesktop.EpnTapModule', {
 			this.targetNameCB.select('all');
 			this.targetNameCB.disable();
 		} else {
-			var targetNames = this.metadata[this.dataProdutTypeCB.value][this.targetClassCB.value];
+			var targetNames = this.metadata[this.productTypeCB.value][this.targetClassCB.value];
 
 			if (Object.keys(targetNames).length == 1) {
-				this.targetNameCB.getStore().add({'id': Object.keys(targetNames)[0], 'name': prettify(Object.keys(targetNames)[0])});
+				this.targetNameCB.getStore().add({'id': Object.keys(targetNames)[0], 'name': this.prettify(Object.keys(targetNames)[0])});
 				this.targetNameCB.select(this.targetNameCB.getStore().getAt(0)['internalId']);
 				this.targetNameCB.disable();
 			} else {
-				this.targetNameCB.getStore().add({'id': 'all', 'name': allPrettify(this.targetClassCB.value)});
+				this.targetNameCB.getStore().add({'id': 'all', 'name': this.allPrettify(this.targetClassCB.value)});
 				for (var targetNameId in targetNames) {
-					this.targetNameCB.getStore().add({'id': targetNameId, 'name': prettify(targetNameId)});
+					this.targetNameCB.getStore().add({'id': targetNameId, 'name': this.prettify(targetNameId)});
 				}
 				this.targetNameCB.select('all');
 				this.targetNameCB.enable();
@@ -201,89 +231,112 @@ Ext.define('amdaDesktop.EpnTapModule', {
 		this.updateServices();
 	},
 
+	/**
+	Trigerred when a new item is selected in `targetNameCB` (see `EpnTapUI.createTargetNameCB()`). Updates the
+	`servicesGrid` table.
+	*/
 	onTargetNameCBChanged: function() {
 		this.updateServices();
 	},
 
+	/**
+	Trigerred when the value of `rowsPerPageNf` is updated (see `EpnTapUI.createRowsPerPageNf()`). Do nothing yet, used
+	for debug purposes only.
+	*/
 	onRowsPerPageChanged: function() {
-		console.log("rows per page: " + this.rowsPerPageNf.value);
+		console.log("rows per page: " + this.productTypeCB);
 	},
 
-	// *** Buttons events ***
+	/*********************
+	*** Buttons events ***
+	*********************/
+
+	/**
+	Disable or enable the navigation buttons (see `EpnTapUI.createNavigationPanel()`).
+	*/
+	disableNavBtns: function(firt, previous, next, last) {
+		Ext.getCmp('epnTapFirstPageBtn').setDisabled(firt);
+		Ext.getCmp('epnTapPreviousPageBtn').setDisabled(previous);
+		Ext.getCmp('epnTapNextPageBtn').setDisabled(next);
+		Ext.getCmp('epnTapLastPageBtn').setDisabled(last);
+	},
 
+	/**
+	Trigerred when the `firstPageBtn` button is clicked (see `EpnTapUI.createNavigationPanel()`). Among other things,
+	send a new query and fill `granulesGrid`.
+	*/
 	onFirstPageBtnClicked: function() {
-		this.wait();
-		this.currentPageLb.setText('1');
-
-		this.nextBtn.setDisabled(false);
-		this.lastBtn.setDisabled(false);
-		this.firstBtn.setDisabled(true);
-		this.previousBtn.setDisabled(true);
-
+		var newPageNumber = 1;
+		var limit = Number(this.rowsPerPageNf.value);
+		var offset = 0;
 		var selectedServiceURL = this.services[this.selectedServiceId]['accessurl'];
-		var limit = this.rowsPerPageNf.value;
-		var offset = '0';
 
+		this.wait();
+		this.disableNavBtns(true, true, false, false);
+		this.currentPageLb.setText('' + newPageNumber);
 		AmdaAction.epnTapGetGranules(this.selectedServiceId, selectedServiceURL, this.filter, this.select, limit, offset, this.fillGranules);
 	},
 
+	/**
+	Trigerred when the `previousPageBtn` button is clicked (see `EpnTapUI.createNavigationPanel()`). Among other things,
+	send a new query and fill `granulesGrid`.
+	*/
 	onPreviousPageBtnClicked: function() {
-		this.wait();
 		var newPageNumber = Number(this.currentPageLb.text) - 1;
-		this.currentPageLb.setText('' + newPageNumber);
-
-		this.nextBtn.setDisabled(false);
-		this.lastBtn.setDisabled(false);
-		if (this.currentPageLb.text === '1') {
-			this.previousBtn.setDisabled(true);
-			this.firstBtn.setDisabled(true);
-		}
-
+		var limit = Number(this.rowsPerPageNf.value);
+		var offset = (newPageNumber-1) * limit;
 		var selectedServiceURL = this.services[this.selectedServiceId]['accessurl'];
-		var limit = this.rowsPerPageNf.value;
-		var offset = '' + (newPageNumber-1) * Number(this.rowsPerPageNf.value);
 
+		this.wait();
+		this.currentPageLb.setText('' + newPageNumber);
+		var isFirstPage = this.currentPageLb.text === '1';
+		this.disableNavBtns(isFirstPage, isFirstPage, false, false);
 		AmdaAction.epnTapGetGranules(this.selectedServiceId, selectedServiceURL, this.filter, this.select, limit, offset, this.fillGranules);
 	},
 
+	/**
+	Trigerred when the `nextPageBtn` button is clicked (see `EpnTapUI.createNavigationPanel()`). Among other things,
+	send a new query and fill `granulesGrid`.
+	*/
 	onNextPageBtnClicked: function() {
-		this.wait();
 		var newPageNumber = Number(this.currentPageLb.text) + 1;
-		this.currentPageLb.setText('' + newPageNumber);
-
-		this.previousBtn.setDisabled(false);
-		this.firstBtn.setDisabled(false);
-		if (this.currentPageLb.text === this.totalPagesLb.text) {
-			this.nextBtn.setDisabled(true);
-			this.lastBtn.setDisabled(true);
-		}
-
+		var limit = Number(this.rowsPerPageNf.value);
+		var offset = (newPageNumber-1) * limit;
 		var selectedServiceURL = this.services[this.selectedServiceId]['accessurl'];
-		var limit = this.rowsPerPageNf.value;
-		var offset = '' + (newPageNumber-1) * Number(this.rowsPerPageNf.value);
 
+		this.wait();
+		this.currentPageLb.setText('' + newPageNumber);
+		var isLastPage = this.currentPageLb.text == this.totalPagesLb.text;
+		this.disableNavBtns(false, false, isLastPage, isLastPage);
 		AmdaAction.epnTapGetGranules(this.selectedServiceId, selectedServiceURL, this.filter, this.select, limit, offset, this.fillGranules);
 	},
 
+	/**
+	Trigerred when the `lastPageBtn` button is clicked (see `EpnTapUI.createNavigationPanel()`). Among other things,
+	send a new query and fill `granulesGrid`.
+	*/
 	onLastPageBtnClicked: function() {
-		this.wait();
-		var newPageNumber = this.totalPagesLb.text;
-		this.currentPageLb.setText('' + newPageNumber);
-
-		this.previousBtn.setDisabled(false);
-		this.firstBtn.setDisabled(false);
-		this.nextBtn.setDisabled(true);
-		this.lastBtn.setDisabled(true);
-
+		var newPageNumber = Number(this.totalPagesLb.text);
+		var limit = Number(this.rowsPerPageNf.value);
+		var offset = (newPageNumber-1) * limit;
 		var selectedServiceURL = this.services[this.selectedServiceId]['accessurl'];
-		var limit = this.rowsPerPageNf.value;
-		var offset = '' + (newPageNumber-1) * Number(this.rowsPerPageNf.value);
 
+		console.log(newPageNumber, limit, offset, selectedServiceURL);
+
+		this.wait();
+		this.currentPageLb.setText('' + newPageNumber);
+		this.disableNavBtns(false, false, true, true);
 		AmdaAction.epnTapGetGranules(this.selectedServiceId, selectedServiceURL, this.filter, this.select, limit, offset, this.fillGranules);
 	},
 
-	// *** Grid click events ***
+	/*******************
+	*** Grids events ***
+	*******************/
 
+	/**
+	Trigerred when a row is clicked in `servicesGrid` table (see `EpnTapUI.createServicesGrid()`). Among other things,
+	send a new query and fill `granulesGrid`.
+	*/
 	onServiceSelected: function(selectedServiceId) {
 		this.wait();
 		this.selectedServiceId = selectedServiceId;
@@ -302,64 +355,32 @@ Ext.define('amdaDesktop.EpnTapModule', {
 			}
 		}
 		this.filter = Array(
-			this.dataProdutTypeCB.value !== 'all' ? this.dataProdutTypeCB.value : null, // product type
+			this.productTypeCB.value !== 'all' ? this.productTypeCB.value : null, // product type
 			this.targetNameCB.value !== 'all' ? this.targetNameCB.value : null, // target name
-			Ext.getCmp('startTimeDF').getRawValue() !== '' ? Ext.getCmp('startTimeDF').getRawValue() : null, // start time
-			Ext.getCmp('stopTimeDF').getRawValue() !== '' ? Ext.getCmp('stopTimeDF').getRawValue() : null // stop time
+			this.timeSelector.getStartTime() !== '' ? this.timeSelector.getStartTime() : null, // start time
+			this.timeSelector.getStopTime() !== '' ? this.timeSelector.getStopTime() : null // stop time
 		);
 
-		var limit = this.rowsPerPageNf.value;
 		AmdaAction.epnTapGetNbRows(selectedServiceId, selectedServiceURL, this.filter, this.updateNbRows);
-		AmdaAction.epnTapGetGranules(selectedServiceId, selectedServiceURL, this.filter, this.select, limit, 0, this.fillGranules);
+		AmdaAction.epnTapGetGranules(selectedServiceId, selectedServiceURL, this.filter, this.select, this.rowsPerPageNf.value, 0, this.fillGranules);
 	},
 
-	onGranuleSelected: function() {
-		// console.log('selected granule: ' + granule.targetName);
+	/**
+	Trigerred when a row is clicked in `granulesGrid` table (see `EpnTapUI.createGranulesGrid()`). Do nothing yet, used
+	for debug purposes only.
+	*/
+	onGranuleSelected: function(ui) {
+		console.log('selected granule: ' + granule.targetName);
 	},
 
-	// *** Other functions ***
-
-	updateNbRows: function(nb_results) {
-		/* /!\ Can not get `this`. */
-		var totalPages = '' + Math.ceil(Number(nb_results) / Ext.getCmp('rowsPerPageNf').value);
-
-		Ext.getCmp('currentPageLb').setText('1');
-		Ext.getCmp('totalPagesLb').setText(totalPages);
-		Ext.getCmp('previousPageBtn').setDisabled(true);
-		Ext.getCmp('firstPageBtn').setDisabled(true);
-		if (totalPages === '1') {
-			Ext.getCmp('nextPageBtn').setDisabled(true);
-			Ext.getCmp('lastPageBtn').setDisabled(true);
-		} else {
-			Ext.getCmp('nextPageBtn').setDisabled(false);
-			Ext.getCmp('lastPageBtn').setDisabled(false);
-		}
-	},
-
-	fillGranules: function(granules) {
-		/* /!\ Can not get `this`. */
-
-		if (granules == null) {
-			console.log("There is no granules to add.");
-		} else {
-			try {
-				console.log('Added granules:', granules);
-				Ext.getCmp('granulesGrid').getStore().removeAll();
-				Ext.getCmp('granulesGrid').getStore().add(granules);
-			} catch( e ) {
-				console.log('Can not add granules: ' + e);
-			}
-		}
-		Ext.getCmp('servicesGrid').setDisabled(false);
-		Ext.getCmp('servicesGrid').getEl().setStyle('cursor', 'default'); // CSS is correctly changed but without visible result.
-	},
-
-	wait: function() {
-		this.servicesGrid.getEl().setStyle('cursor', 'wait');
-		this.servicesGrid.setDisabled(true); // CSS is correctly changed but without visible result.
-	},
+	/**********************
+	*** Other functions ***
+	**********************/
 
-	updateServices: function() {
+	/**
+	Update the services store (see `EpnTapUI.servicesStore`), according to the field values in `serviceFilterPanel`.
+	*/
+	updateServices: function(ui) {
 		this.servicesGrid.getStore().removeAll();
 		this.granulesGrid.getStore().removeAll();
 
@@ -368,9 +389,12 @@ Ext.define('amdaDesktop.EpnTapModule', {
 		var timeMaxArr = null;
 		var timeMin = null;
 		var timeMax = null;
+		var productType = this.productTypeCB.value;
+		var targetClass = this.targetClassCB.value;
+		var targetName = this.targetNameCB.value;
 
 		var filterDict = new Array();
-		if(this.dataProdutTypeCB.value === 'all') {
+		if(productType === 'all') {
 			for (var dpt in this.metadata) {
 				for (var tc in this.metadata[dpt]) {
 					for (tn in this.metadata[dpt][tc]) {
@@ -378,10 +402,10 @@ Ext.define('amdaDesktop.EpnTapModule', {
 							service = this.metadata[dpt][tc][tn][serv];
 							timeMinArr = service[1].split('/');
 							filterDict[serv] = service[0] + (serv in filterDict ? filterDict[serv] : 0);
-							if (isLatest(service[1], timeMin)) {
+							if (this.isLatest(service[1], timeMin)) {
 								timeMin = service[1];
 							}
-							if (isLatest(service[2], timeMax)) {
+							if (this.isLatest(service[2], timeMax)) {
 								timeMax = service[2];
 							}
 						}
@@ -389,41 +413,41 @@ Ext.define('amdaDesktop.EpnTapModule', {
 				}
 			}
 		} else if (this.targetClassCB.value === 'all') {
-			for (var tc in this.metadata[this.dataProdutTypeCB.value]) {
-				for (tn in this.metadata[this.dataProdutTypeCB.value][tc]) {
-					for (serv in this.metadata[this.dataProdutTypeCB.value][tc][tn]) {
-						service = this.metadata[this.dataProdutTypeCB.value][tc][tn][serv];
+			for (var tc in this.metadata[productType]) {
+				for (tn in this.metadata[productType][tc]) {
+					for (serv in this.metadata[productType][tc][tn]) {
+						service = this.metadata[productType][tc][tn][serv];
 						filterDict[serv] = service[0] + (serv in filterDict ? filterDict[serv] : 0);
-						if (isLatest(service[1], timeMin)) {
+						if (this.isLatest(service[1], timeMin)) {
 							timeMin = service[1];
 						}
-						if (isLatest(service[2], timeMax)) {
+						if (this.isLatest(service[2], timeMax)) {
 							timeMax = service[2];
 						}
 					}
 				}
 			}
 		} else if (this.targetNameCB.value === 'all') {
-			for (tn in this.metadata[this.dataProdutTypeCB.value][this.targetClassCB.value]) {
-				for (serv in this.metadata[this.dataProdutTypeCB.value][this.targetClassCB.value][tn]) {
-					service = this.metadata[this.dataProdutTypeCB.value][this.targetClassCB.value][tn][serv];
+			for (tn in this.metadata[productType][targetClass]) {
+				for (serv in this.metadata[productType][targetClass][tn]) {
+					service = this.metadata[productType][targetClass][tn][serv];
 					filterDict[serv] = service[0] + (serv in filterDict ? filterDict[serv] : 0);
-					if (isLatest(service[1], timeMin)) {
+					if (this.isLatest(service[1], timeMin)) {
 						timeMin = service[1];
 					}
-					if (isLatest(service[2], timeMax)) {
+					if (this.isLatest(service[2], timeMax)) {
 						timeMax = service[2];
 					}
 				}
 			}
 		} else {
-			for (serv in this.metadata[this.dataProdutTypeCB.value][this.targetClassCB.value][this.targetNameCB.value]) {
-				service = this.metadata[this.dataProdutTypeCB.value][this.targetClassCB.value][this.targetNameCB.value][serv];
+			for (serv in this.metadata[productType][targetClass][targetName]) {
+				service = this.metadata[productType][targetClass][targetName][serv];
 				filterDict[serv] = service[0] + (serv in filterDict ? filterDict[serv] : 0);
-				if (isLatest(service[1], timeMin)) {
+				if (this.isLatest(service[1], timeMin)) {
 					timeMin = service[1];
 				}
-				if (isLatest(service[2], timeMax)) {
+				if (this.isLatest(service[2], timeMax)) {
 					timeMax = service[2];
 				}
 			}
@@ -443,6 +467,54 @@ Ext.define('amdaDesktop.EpnTapModule', {
 			var service = this.services[filter[s][0]];
 			this.servicesGrid.getStore().add({'id': filter[s][0], 'nbResults': filter[s][1], 'shortName': service['shortname'], 'title': service['title'], 'accessURL': service['accessurl']});
 		}
+	},
+
+	/**
+	Callback function, called from the PHP script when the query result is received, when a service is selected.
+
+	Among other things, update the `epnTapCurrentPageLb` label (see `EpnTapUI.createNavigationPanel()`).
+	*/
+	updateNbRows: function(nb_results) {
+		var totalPages = '' + Math.ceil(Number(nb_results) / Ext.getCmp('epnTapRowsPerPageNf').value);
+
+		Ext.getCmp('epnTapCurrentPageLb').setText('1');
+		Ext.getCmp('epnTapTotalPagesLb').setText(totalPages);
+
+		Ext.getCmp('epnTapPreviousPageBtn').setDisabled(true);
+		Ext.getCmp('epnTapFirstPageBtn').setDisabled(true);
+		Ext.getCmp('epnTapNextPageBtn').setDisabled(totalPages === '1');
+		Ext.getCmp('epnTapLastPageBtn').setDisabled(totalPages === '1');
+	},
+
+	/**
+	Callback function, called from the PHP script when the query result is received, when a service is selected or a
+	navigation button is clicked.
+
+	Among other things, fill the `epnTapGranulesGrid` table (see `EpnTapUI.granulesStore`).
+	*/
+	fillGranules: function(granules) {
+		if (granules == null) {
+			console.log("There is no granules to add.");
+		} else {
+			try {
+				console.log('Added granules:', granules);
+				Ext.getCmp('epnTapGranulesGrid').getStore().removeAll();
+				Ext.getCmp('epnTapGranulesGrid').getStore().add(granules);
+			} catch( e ) {
+				console.log('Can not add granules: ' + e);
+			}
+		}
+		Ext.getCmp('epnTapServicesGrid').setDisabled(false);
+		Ext.getCmp('epnTapServicesGrid').getEl().setStyle('cursor', 'default'); // CSS is correctly changed but without visible result.
+	},
+
+	/**
+	Called before to send a query. Set the EpnTap panel in "waiting mode", informing to the user that a request is
+	processing. The altered elements are resetted in `fillGranules()`.
+	*/
+	wait: function() {
+		this.servicesGrid.getEl().setStyle('cursor', 'wait');
+		this.servicesGrid.setDisabled(true); // CSS is correctly changed but without visible result.
 	}
 
 });
diff --git a/js/app/views/EpnTapUI.js b/js/app/views/EpnTapUI.js
index 1f47ae8..b6cf074 100644
--- a/js/app/views/EpnTapUI.js
+++ b/js/app/views/EpnTapUI.js
@@ -3,379 +3,590 @@
  * Name: EpnTapUI.js
  * @class amdaUI.EpnTapUI
  * @extends Ext.tab.Panel
- * @brief client for EPN-TAP services (View)
  * @author Nathanael JOURDANE
  * 24/10/2016: file creation
  */
 
- Ext.create('Ext.data.Store', {
+// TODO: Déplacer les stores dans un fichier séparé dans js.stores !
+
+/**
+`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',
 	fields: ['id', 'name']
 });
 
+/**
+`targetClassesStore`: An ExtJS Store containing the list of the different target classes 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 type.
+
+This list is used to fill the `targetClassCB` combo box, which is updated by `EpnTapModule` each time a new product type
+is selected.
+
+- `id`: the target class in lowercase, with the underscore between each word;
+- `name`: the target class, capitalized with spaces between each word (done `EpnTapModule.prettify()`).
+*/
 Ext.create('Ext.data.Store', {
 	storeId:'targetClassesStore',
 	fields: ['id', 'name']
 });
 
+/**
+`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']
 });
 
+/**
+`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',
 	fields: ['id', 'nbResults', 'shortName', 'title', 'accessURL']
 });
 
+/**
+`granulesStore`: An ExtJS Store containing the list of granules of the selected service (on `servicesGrid`), which match
+with tge 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).
+*/
 Ext.create('Ext.data.Store', {
 	storeId:'granulesStore',
 	fields:['num', 'dataproduct_type', 'target_name', 'time_min', 'time_max', 'access_format', 'granule_uid', 'access_estsize', 'access_url', 'thumbnail_url']
 });
 
+/**
+`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.container.Container',
+	extend: 'Ext.panel.Panel',
 	alias: 'widget.panelEpnTap',
+	requires: ['amdaUI.IntervalUI'],
 
-	txtRender: function(val) {
-		return '<p style="white-space: normal;">' + val + '</p>';
-	},
-	linkRender: function(val) {
-		return '<a href="' + val + '">data</a>';
-	},
-	imgRender: function(val) {
-		return '<img width="40px height="40px" src="' + val + '">';
-	},
-	dptRender: function(val) {
-		var productTypeDict = myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.epntap.id).productTypeDict;
-		return (val in productTypeDict) ? '<p style="white-space: normal;">' + productTypeDict[val] + '</p>' : '<em>' + val + '</em>';
-	},
-	formatRender: function(val) {
-		var mimetypeDict = myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.epntap.id).mimetypeDict;
-		return (val in mimetypeDict) ? mimetypeDict[val] : '<em style="white-space: normal;">' + val + '</em>';
-	},
-	sizeRender: function(val) {
-		var size = parseInt(val);
-		if (isNaN(size)) {
-			return '';
-		} else if (size >= 1024*1024) {
-			return (size/(1024*1024)).toPrecision(3) + 'Go';
-		} else if (size >= 1024) {
-			return (size/1024).toPrecision(3) + 'Mo';
-		} else {
-			return size + 'Ko';
-		}
-	},
-
+	/**
+	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 mod = myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.epntap.id);
+
+		/************
+		*** Grids ***
+		************/
+
+		/**
+		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 the tooltip `serviceTooltip`.
+
+		A click on a service triggers `EpnTapModule.onServiceSelected()`, which basically fills `GranulesGrid` by the
+		service granules.
+		*/
+		var createServicesGrid = function() {
+			return new Ext.grid.Panel({
+				id: 'epnTapServicesGrid',
+				title: 'Services',
+				store: Ext.data.StoreManager.lookup('servicesStore'),
+				flex: 1,
+				columns: [
+					{text: 'Name', dataIndex: 'id', flex: 3},
+					{text: 'Results', dataIndex: 'nbResults', flex: 2}
+				],
+				renderer: function(value, metadata,record) {
+					return getExpandableImage(value, metadata,record);
+				},
+				listeners: {
+					'cellclick': function(grid, td, cellIndex, record) { mod.onServiceSelected(record.data['id']); }
+				},
+				renderTo: Ext.getBody()
+			});
+		};
 
-		var epnTapModule = myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.epntap.id);
-
-		// *** Grids ***
-
-		this.servicesGrid = new Ext.grid.Panel({
-			id: 'servicesGrid',
-			title: 'Services',
-			store: Ext.data.StoreManager.lookup('servicesStore'),
-			flex: 1,
-			columns: [
-				{text: 'Name', dataIndex: 'id', flex: 3},
-				{text: 'Results', dataIndex: 'nbResults', flex: 2}
-			],
-			renderer: function(value, metadata,record) {
-				return getExpandableImage(value, metadata,record);
-			},
-			listeners: {
-				'cellclick': function(grid, td, cellIndex, record) {
-					epnTapModule.onServiceSelected(record.data['id']);
-				}
-			},
-			renderTo: Ext.getBody()
-		});
-
-		this.serviceTooltip = new Ext.tip.ToolTip({
-			id: 'serviceTooltip',
-			target: Ext.getCmp('servicesGrid').getView().el,
-			delegate: Ext.getCmp('servicesGrid').getView().itemSelector,
-			trackMouse: true,
-			listeners: {
-				beforeshow: function updateTipBody(tooltip) {
-					var service = Ext.getCmp('servicesGrid').getView().getRecord(tooltip.triggerElement);
-					var ttContent = '<h3>' + service.get('shortName') + '</h3>';
-					ttContent += '<p>' + service.get('title') + '</p>';
-					ttContent += '<p>' + service.get('accessURL') + '</p>';
-					tooltip.update(ttContent);
-				}
-			},
-			renderTo: Ext.getBody()
-		});
-
-		this.granulesGrid = new Ext.grid.Panel({
-			id: 'granulesGrid',
-			title: 'Granules',
-			store: Ext.data.StoreManager.lookup('granulesStore'),
-			flex: 5,
-			columns: [
-				{ text: 'Num',  dataIndex: 'num', flex: 1, renderer: this.txtRender },
-				{ text: 'Type',  dataIndex: 'dataproduct_type', flex: 2, renderer: this.dptRender },
-				{ text: 'Target', dataIndex: 'target_name', flex: 2, renderer: this.txtRender },
-				{ text: 'Time min', dataIndex: 'time_min', flex: 2, renderer: this.txtRender },
-				{ text: 'Time max', dataIndex: 'time_max', flex: 2, renderer: this.txtRender },
-				{ text: 'Format', dataIndex: 'access_format', flex: 2, renderer: this.formatRender },
-				{ text: 'uid', dataIndex: 'granule_uid', flex: 2, renderer: this.txtRender },
-				{ text: 'Size', dataIndex: 'access_estsize', flex: 1, renderer: this.sizeRender },
-				{ text: 'URL', dataIndex: 'access_url', flex: 1, renderer: this.linkRender },
-				{ text: 'Thumb.', dataIndex: 'thumbnail_url', flex: 1, renderer: this.imgRender}
-			],
-			listeners: {
-				'cellclick': function(grid, td, cellIndex, record) {
-					epnTapModule.onGranuleSelected(record.data['id']);
-				}
-			},
-			renderTo: Ext.getBody()
-		});
-
-		this.granuleTooltip = new Ext.tip.ToolTip({
-			id: 'granuleTooltip',
-			target: Ext.getCmp('granulesGrid').getView().el,
-			delegate: Ext.getCmp('granulesGrid').getView().itemSelector,
-			trackMouse: true,
-			listeners: {
-				beforeshow: function updateTipBody(tooltip) {
-					var thumb = Ext.getCmp('granulesGrid').getView().getRecord(tooltip.triggerElement).get('thumbnail_url');
-					tooltip.update('<img src="' + thumb + '">');
-				}
-			},
-			renderTo: Ext.getBody()
-		});
-
-		// *** Service filter elements, left part ***
-
-		this.productTypeCB = new Ext.form.field.ComboBox({
-			id: 'productTypeCB',
-			fieldLabel: 'Product type',
-			store: Ext.data.StoreManager.lookup('productTypesStore'),
-			queryMode: 'local',
-			displayField: 'name',
-			valueField: 'id',
-			name: 'productType',
-			editable: false,
-			listeners: {
-				'select': function(combo) {
-					epnTapModule.onProductTypeCBChanged(combo.value);
+		/**
+		Create `epnTapServiceTooltip`, an ExtJS tooltip for the `servicesGrid` rows, in order to display additional
+		information for each service, such as:
+			- short name;
+			- title;
+			- access URL.
+		*/
+		var createServiceTooltip = function() {
+			return new Ext.tip.ToolTip({
+				id: 'epnTapServiceTooltip',
+				target: Ext.getCmp('epnTapServicesGrid').getView().el,
+				delegate: Ext.getCmp('epnTapServicesGrid').getView().itemSelector,
+				trackMouse: true,
+				listeners: {
+					beforeshow: function updateTipBody(tooltip) {
+						var service = Ext.getCmp('servicesGrid').getView().getRecord(tooltip.triggerElement);
+						var ttContent = '<h3>' + service.get('shortName') + '</h3>';
+						ttContent += '<p>' + service.get('title') + '</p>';
+						ttContent += '<p>' + service.get('accessURL') + '</p>';
+						tooltip.update(ttContent);
+					}
+				},
+				renderTo: Ext.getBody()
+			});
+		};
+
+		/**
+		Create `epnTapGranulesGrid`, an ExtJS grid containing the granules of the selected service in
+		`epnTapServiceGrid`.
+
+		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.
+
+		For more information about these parameters, see https://voparis-confluence.obspm.fr/display/VES/EPN-TAP+V2.0+parameters.
+		Each of these information are displayed in a specific rendering to improve user experience.
+		Other informations are available through the tooltip `granuleTooltip`.
+
+		A click on a granule triggers `EpnTapModule.onGranuleSelected()`.
+		*/
+		var createGranulesGrid = function() {
+			var txtRender = function(val) {
+				return '<p style="white-space: normal;">' + val + '</p>';
+			};
+			var linkRender = function(val) {
+				return '<a href="' + val + '">data</a>';
+			};
+			var imgRender = function(val) {
+				return '<img width="40px height="40px" src="' + val + '">';
+			};
+			var dptRender = function(val) {
+				return (val in mod.productTypeDict) ? '<p style="white-space: normal;">' + mod.productTypeDict[val] + '</p>' : '<em>' + val + '</em>';
+			};
+			var formatRender = function(val) {
+				return (val in mod.mimetypeDict) ? mod.mimetypeDict[val] : '<em style="white-space: normal;">' + val + '</em>';
+			};
+			var sizeRender = function(val) {
+				var size = parseInt(val);
+				if (isNaN(size)) {
+					return '';
+				} else if (size >= 1024*1024) {
+					return (size/(1024*1024)).toPrecision(3) + 'Go';
+				} else if (size >= 1024) {
+					return (size/1024).toPrecision(3) + 'Mo';
+				} else {
+					return size + 'Ko';
 				}
-			}
-		});
-
-		this.targetClassCB = new Ext.form.field.ComboBox({
-			id: 'targetClassCB',
-			fieldLabel: 'Target class',
-			store: Ext.data.StoreManager.lookup('targetClassesStore'),
-			queryMode: 'local',
-			displayField: 'name',
-			valueField: 'id',
-			name: 'targetClass',
-			editable: false,
-			listeners: {
-				'select': function(combo) {
-					epnTapModule.onTargetClassCBChanged(combo.value);
+			};
+
+			return new Ext.grid.Panel({
+				id: 'epnTapGranulesGrid',
+				title: 'Granules',
+				store: Ext.data.StoreManager.lookup('granulesStore'),
+				flex: 5,
+				columns: [
+					{ text: 'Num',  dataIndex: 'num', flex: 1, renderer: txtRender },
+					{ text: 'Type',  dataIndex: 'dataproduct_type', flex: 2, renderer: dptRender },
+					{ text: 'Target', dataIndex: 'target_name', flex: 2, renderer: txtRender },
+					{ text: 'Time min', dataIndex: 'time_min', flex: 2, renderer: txtRender },
+					{ text: 'Time max', dataIndex: 'time_max', flex: 2, renderer: txtRender },
+					{ text: 'Format', dataIndex: 'access_format', flex: 2, renderer: formatRender },
+					{ text: 'uid', dataIndex: 'granule_uid', flex: 2, renderer: txtRender },
+					{ text: 'Size', dataIndex: 'access_estsize', flex: 1, renderer: sizeRender },
+					{ text: 'URL', dataIndex: 'access_url', flex: 1, renderer: linkRender },
+					{ text: 'Thumb.', dataIndex: 'thumbnail_url', flex: 1, renderer: imgRender}
+				],
+				listeners: {
+					'cellclick': function(grid, td, cellIndex, record) { mod.onGranuleSelected(record.data['id']); }
+				},
+				renderTo: Ext.getBody()
+			});
+		};
+
+		/**
+		Create `epnTapGranuleTooltip`, an ExtJS tooltip for the `granulesGrid` rows, in order to display additional
+		information for each granule which is currently only the granule thumbnail, in full size.
+		*/
+		var createGranuleTooltip = function() {
+			return new Ext.tip.ToolTip({
+				id: 'epnTapGranuleTooltip',
+				target: Ext.getCmp('granulesGrid').getView().el,
+				delegate: Ext.getCmp('granulesGrid').getView().itemSelector,
+				trackMouse: true,
+				listeners: {
+					beforeshow: function updateTipBody(tooltip) {
+						var thumb = Ext.getCmp('granulesGrid').getView().getRecord(tooltip.triggerElement).get('thumbnail_url');
+						tooltip.update('<img src="' + thumb + '">');
+					}
+				},
+				renderTo: Ext.getBody()
+			});
+		};
+
+		/**
+		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.
+		*/
+		var createGridsPanel = function() {
+			var self = this;
+			return new Ext.panel.Panel({
+				id: 'epnTapGridsPanel',
+				region: 'center',
+				height: 350,
+				layout: { type: 'hbox', pack: 'start', align: 'stretch' },
+				items: [
+					createServicesGrid(),
+					createGranulesGrid()
+				],
+				listeners: {
+					afterrender: function() { mod.onWindowLoaded(self); }
 				}
-			}
-		});
-
-		this.targetNameCB = new Ext.form.field.ComboBox({
-			id: 'targetNameCB',
-			fieldLabel: 'Target name',
-			store: Ext.data.StoreManager.lookup('targetNamesStore'),
-			queryMode: 'local',
-			displayField: 'name',
-			valueField: 'id',
-			name: 'targetName',
-			triggerAction: 'all',
-			typeAhead: true,
-			mode: 'remote',
-			minChars: 2,
-			forceSelection: true,
-			listeners: {
-				'select': function(combo) {
-					epnTapModule.onTargetNameCBChanged(combo.value);
+			});
+		};
+
+		/***************************
+		*** Service filter panel ***
+		***************************/
+
+		/**
+		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
+		`epnTapTargetClassCB` and `epnTapGranulesGrid`.
+		*/
+		var createProductTypeCB = function() {
+			return new Ext.form.field.ComboBox({
+				id: 'epnTapProductTypeCB',
+				fieldLabel: 'Product type',
+				store: Ext.data.StoreManager.lookup('productTypesStore'),
+				queryMode: 'local',
+				displayField: 'name',
+				valueField: 'id',
+				name: 'productType',
+				editable: false,
+				listeners: {
+					'select': function(combo) { mod.onProductTypeCBChanged(combo.value); }
 				}
-			}
-		});
-
-		// *** Service filter elements, right part ***
-
-		this.startTimeDF = new Ext.form.field.Date({
-			id: 'startTimeDF',
-			fieldLabel: 'Start time',
-			format: 'Y/m/d H:i:s',
-			width: 100,
-			listeners: {
-				'select': function(dateField, value) {
-					epnTapModule.onTargetNameCBChanged(value);
+			});
+		};
+
+		/**
+		Create `epnTapTargetClassCB`, an ExtJS ComboBox, containing a list of target classes corresponding to the
+		selected product type, as defined in `targetClassesStore`, which is initilized by `EpnTapModule`.
+
+		The selection of a target class triggers the `EpnTapModule.onTargetClassCBChanged()`, which basically updates
+		`targetNameCB` and `granulesGrid`.
+		*/
+		var createTargetClassCB = function() {
+			return new Ext.form.field.ComboBox({
+				id: 'epnTapTargetClassCB',
+				fieldLabel: 'Target class',
+				store: Ext.data.StoreManager.lookup('targetClassesStore'),
+				queryMode: 'local',
+				displayField: 'name',
+				valueField: 'id',
+				name: 'targetClass',
+				editable: false,
+				listeners: {
+					'select': function(combo) { mod.onTargetClassCBChanged(combo.value); }
 				}
-			}
-		});
-
-		this.stopTimeDF = new Ext.form.field.Date({
-			id: 'stopTimeDF',
-			fieldLabel: 'Stop time',
-			format: 'Y/m/d H:i:s',
-			width: 100,
-			listeners: {
-				'select': function(dateField, value) {
-					epnTapModule.onTargetNameCBChanged(value);
+			});
+		};
+
+		/**
+		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`.
+		*/
+		var createTargetNameCB = function() {
+			return new Ext.form.field.ComboBox({
+				id: 'epnTapTargetNameCB',
+				fieldLabel: 'Target name',
+				store: Ext.data.StoreManager.lookup('targetNamesStore'),
+				queryMode: 'local',
+				displayField: 'name',
+				valueField: 'id',
+				name: 'targetName',
+				triggerAction: 'all',
+				typeAhead: true,
+				mode: 'remote',
+				minChars: 2,
+				forceSelection: true,
+				listeners: {
+					'select': function(combo) { mod.onTargetNameCBChanged(combo.value); }
 				}
-			}
-		});
-
-		this.durationPanel = new Ext.panel.Panel({
-			id: 'duration',
-			layout: { type: 'hbox', pack: 'start', align: 'stretch' },
-			border: false,
-			defaults: { width: 60, margin: '0, 5, 0, 5', xtype: 'numberfield', listeners: { 'select': function(elmt) { epnTapModule.onDurationChanged(elmt); } } },
-			items: [{
-				id: 'days',
-				margin: '0, 5, 0, 0',
-				fieldLabel: 'Duration',
-				emptyText: 'Days',
-				width: 170
-			}, {
-				id: 'hours',
-				emptyText: 'Hours'
-			}, {
-				id: 'minutes',
-				emptyText: 'Min.'
-			}, {
-				id: 'seconds',
-				emptyText: 'Sec.'
-			}]
-		});
-
-		this.rowPerPageNf = new Ext.form.field.Number({
-			id: 'rowsPerPageNf',
-			fieldLabel: 'Rows per page',
-			margin: '4 0 4 0',
-			width: 160,
-			height: 20,
-			value: 20,
-			minValue: 1,
-			maxValue: 2000,
-			listeners: {
-				'change': function(rowPerPageNf, newValue) {
-					epnTapModule.onRowsPerPageChanged(newValue);
+			});
+		};
+
+		/**
+		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.
+		*/
+		var createServiceFilterPanel = function() {
+			return new Ext.panel.Panel({
+				id: 'epnTapServiceFilterPanel',
+				region : 'north',
+				layout: { type: 'hbox', pack: 'start', align: 'stretch' },
+				defaults: { margin: 5 },
+				items: [{ // Left part
+					xtype : 'container',
+					layout: 'form',
+					flex: 2,
+					items: [
+						createProductTypeCB(),
+						createTargetClassCB(),
+						createTargetNameCB(),
+						{
+							xtype: 'panel',
+							layout: { type: 'hbox', pack: 'start', align: 'stretch' },
+							border: false,
+							items: [
+								createRowPerPageNf(),
+								createNavigationPanel()
+							]
+						}
+					]
+				}, { // Right part
+					xtype : 'form',
+					id: 'epnTapIntervalSelector',
+					layout: 'form',
+					border: 'false',
+					flex: 2,
+					items: [
+						createTimeSelector()
+					]
+				}]
+			});
+		};
+
+		/**
+		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.
+		*/
+		var createTimeSelector = function() {
+			return Ext.create('amdaUI.IntervalUI', {
+				id: 'epnTapTimeSelector'
+			});
+		};
+
+		/***********************
+		*** Navigation panel ***
+		***********************/
+
+		/**
+		Create `epnTapRowsPerPageNf`, a ExtJS Number field, allowing the user to select the number of rows to display in
+		`epnTapGranulesGrid`.
+
+		When a new number is entered, it triggers `EpnTapModule.onRowsPerPageChanged()`.
+		*/
+		var createRowPerPageNf = function() {
+			return new Ext.form.field.Number({
+				id: 'epnTapRowsPerPageNf',
+				fieldLabel: 'Rows per page',
+				margin: '4 0 4 0',
+				width: 160,
+				height: 20,
+				value: 20,
+				minValue: 1,
+				maxValue: 2000,
+				listeners: {
+					'change': function(rowPerPageNf, newValue) { mod.onRowsPerPageChanged(newValue); }
 				}
-			}
-		});
-
-		// *** Panels ***
-
-		this.pageSelectPanel = new Ext.panel.Panel({
-			id: 'pageSelect',
-			border: false,
-			margin: '2 0 2 50',
-			defaults: { margin: '0 5 0 5', width: 20, xtype: 'button', disabled: true},
-			items: [{
-				xtype: 'label',
-				text: 'Page:'
-			}, {
-				id: 'firstPageBtn',
-				text: '|<',
-				tooltip: 'First page',
-				handler: function() { epnTapModule.onFirstPageBtnClicked(); }
-			}, {
-				id: 'previousPageBtn',
-				text: '<',
-				tooltip: 'Previous page',
-				handler: function() { epnTapModule.onPreviousPageBtnClicked(); }
-			}, {
-				xtype: 'label',
-				id: 'currentPageLb',
-				tooltip: 'Current page',
-				text: '-'
-			}, {
-				xtype: 'label',
-				text: '/'
-			}, {
-				xtype: 'label',
-				id: 'totalPagesLb',
-				tooltip: 'Total pages',
-				text: '-'
-			}, {
-				id: 'nextPageBtn',
-				text: '>',
-				tooltip: 'Next page',
-				handler: function() { epnTapModule.onNextPageBtnClicked(); }
-			}, {
-				id: 'lastPageBtn',
-				text: '>|',
-				tooltip: 'Last page',
-				handler: function() { epnTapModule.onLastPageBtnClicked(); }
-			}]
-		});
-
-		this.granulePagePanel = new Ext.panel.Panel({
-			id: 'granulePagePanel',
-			layout: { type: 'hbox', pack: 'start', align: 'stretch' },
-			border: false,
-			items: [this.rowPerPageNf, this.pageSelectPanel]
-		});
-
-		this.serviceFilterPanel = new Ext.panel.Panel({
-			id: 'serviceFilterPanel',
-			region : 'north',
-			layout: { type: 'hbox', pack: 'start', align: 'stretch' },
-			defaults: { margin: 5 },
-			items: [{ // Left part
-				xtype : 'container',
-				layout: 'form',
-				flex: 2,
-				items: [ this.productTypeCB, this.targetClassCB, this.targetNameCB ]
-			}, { // Right part
-				xtype : 'container',
-				layout: 'form',
-				flex: 2,
-				items: [ this.startTimeDF, this.stopTimeDF, this.durationPanel, this.granulePagePanel ]
-			}]
-		});
-
-		this.gridsPanel = new Ext.panel.Panel({
-			id: 'gridsPanel',
-			region: 'center',
-			height: 350,
-			layout: { type: 'hbox', pack: 'start', align: 'stretch' },
-			items: [ this.servicesGrid, this.granulesGrid ],
-			listeners: {
-				afterrender: function() { epnTapModule.onWindowLoaded(); }
-			}
-		});
-
-		this.infoPanel = new Ext.panel.Panel({
-			id: 'infoPanel',
-			region: 'south',
-			title: 'Information',
-			collapsible: true,
-			flex: 0,
-			height: 100,
-			autoHide: false,
-			bodyStyle: 'padding: 5px',
-			iconCls: 'icon-information',
-			loader: { autoLoad: true, url: helpDir + 'epnTapHOWTO' }
-		});
+			});
+		};
+
+		/**
+		Create `epnTapNavigationPanel`, an ExtJSPanel containing several elements in order to navigate through the
+		different pages of the query result. If the number of results is highter than the `epnTapRowsPerPageNf` field
+		value, the result appears to be displayed in different pages. This panel is used to select and display the page
+		number, mainly with these following elements:
+		- `epnTapFirstPageBtn`: an ExtJS Button, used to come back to the first page of result,
+				handling `EpnTapModule.onFirstPageBtnClicked()`;
+		- `epnTapPreviousPageBtn`: an ExtJS Button, used to come back to the previous page of result,
+				handling `EpnTapModule.onPreviousPageBtnClicked()`;
+		- `epnTapCurrentPageLb`: an ExtJS Label, displaying the actual current page; TODO: use a Number field instead!
+		- `epnTapTotalPagesLb`: an ExtJS Label, displaying the total page number of results (according to the
+		`epnTapRowsPerPageNf` field value);
+		- `epnTapNextPageBtn`: an ExtJS Button, used to go to the next page of result,
+				handling `EpnTapModule.onNextPageBtnClicked()`;
+		- `epnTapLastPageBtn`: an ExtJS Button, used to come back to the last page of result,
+				handling `EpnTapModule.onLastPageBtnClicked()`.
+
+		Note: Pages are not actually a "graphical filter": when the user navigate through the pages, a new query is send
+		to the server with the corresponding range, which improves the response time on large requests.
+		*/
+		var createNavigationPanel = function() {
+			return new Ext.panel.Panel({
+				name: 'epnTapNavigationPanel',
+				border: false,
+				margin: '2 0 2 50',
+				defaults: { margin: '0 5 0 5', width: 20, xtype: 'button', disabled: true},
+				items: [{
+					xtype: 'label',
+					text: 'Page:'
+				}, {
+					id: 'epnTapFirstPageBtn',
+					text: '|<',
+					tooltip: 'First page',
+					handler: function() { mod.onFirstPageBtnClicked(); }
+				}, {
+					id: 'epnTapPreviousPageBtn',
+					text: '<',
+					tooltip: 'Previous page',
+					handler: function() { mod.onPreviousPageBtnClicked(); }
+				}, {
+					xtype: 'label',
+					id: 'epnTapCurrentPageLb',
+					tooltip: 'Current page',
+					text: '-'
+				}, {
+					xtype: 'label',
+					text: '/'
+				}, {
+					xtype: 'label',
+					id: 'epnTapTotalPagesLb',
+					tooltip: 'Total pages',
+					text: '-'
+				}, {
+					id: 'epnTapNextPageBtn',
+					text: '>',
+					tooltip: 'Next page',
+					handler: function() { mod.onNextPageBtnClicked(); }
+				}, {
+					id: 'epnTapLastPageBtn',
+					text: '>|',
+					tooltip: 'Last page',
+					handler: function() { mod.onLastPageBtnClicked(); }
+				}]
+			});
+		};
+
+		/*******************
+		*** Other panels ***
+		*******************/
+
+		/**
+		Create `epnTapInfoPanel`, an ExtJS Panel used to display a brief user guide about how to use this module.
+		*/
+		var createInfoPanel = function() {
+			return new Ext.panel.Panel({
+				id: 'epnTapInfoPanel',
+				region: 'south',
+				title: 'Information',
+				collapsible: true,
+				flex: 0,
+				height: 100,
+				autoHide: false,
+				bodyStyle: 'padding: 5px',
+				iconCls: 'icon-information',
+				loader: { autoLoad: true, url: helpDir + 'epnTapHOWTO' }
+			});
+		};
+
+		// TODO tester ceci:
+		// config.title = 'EPN-TAP';
+		// config.layout = 'border';
+		// config.items = [
+		// 	createServiceFilterPanel(config.targetName),
+		// 	createGridsPanel(),
+		// 	createInfoPanel()
+		// ];
+		// Ext.apply(this, Ext.apply(arguments, config));
 
 		var myConf = {
 			width: 1000,
 			height: 550,
 			layout: 'border',
-			items: [ this.serviceFilterPanel, this.gridsPanel, this.infoPanel ]
+			items: [
+				createServiceFilterPanel(),
+				createGridsPanel(),
+				createInfoPanel()
+			]
 		};
-
 		Ext.apply(this, Ext.apply(arguments, myConf));
-
 	}
 });
diff --git a/js/app/views/IntervalUI.js b/js/app/views/IntervalUI.js
index c716491..67163b6 100644
--- a/js/app/views/IntervalUI.js
+++ b/js/app/views/IntervalUI.js
@@ -6,7 +6,7 @@
  * @brief    common component to select interval
  * @author 	 Benjamin
  * @version  $Id: IntervalUI.js 2077 2014-02-11 11:33:36Z elena $
- * @todo Validations 
+ * @todo Validations
  *****************************************************************************
  * FT Id     :   Date   : Name - Description
  ******************************************************************************
@@ -15,17 +15,21 @@
 
 Ext.define('amdaUI.IntervalUI', {
     extend: 'Ext.container.Container',
-    
     alias: 'widget.intervalSelector',
-
     activeField : null,
-    
+
 	constructor: function(config) {
 		this.init(config);
 		this.callParent(arguments);
 	},
-    
-	setInterval : function(startDate,stopDate)
+
+	/**
+		Set the start and stop date, and update the duration field.
+		- startDate: A Extjs Date object representing the new start time.
+		- stopDate: A Extjs Date object representing the new stop time.
+		- return: None.
+	*/
+	setInterval : function(startDate, stopDate)
 	{
 		// get the search form
         var form = this.findParentByType('form').getForm();
@@ -33,44 +37,55 @@ Ext.define('amdaUI.IntervalUI', {
         var startField = form.findField('startDate');
         // get stop field
         var stopField = form.findField('stopDate');
-        
+
         if (startField != null)
         	startField.setValue(startDate);
-        
+
         if (stopField != null)
         	stopField.setValue(stopDate);
-        
+
         this.updateDuration();
 	},
-	
+
+	/**
+		Get the start time field value.
+		- return: A Extjs Date object representing the start time.
+	*/
 	getStartTime : function()
 	{
 		// get the search form
         var form = this.findParentByType('form').getForm();
         // get start field
         var startField = form.findField('startDate');
-        
+
         return startField.getValue();
 	},
-	
+
+	/**
+		Get the stop time field value.
+		- return: A Extjs Date object representing the stop time.
+	*/
 	getStopTime : function()
 	{
 		// get the search form
         var form = this.findParentByType('form').getForm();
         // get stop field
         var stopField = form.findField('stopDate');
-        
-        return stopField.getValue();		
+
+        return stopField.getValue();
 	},
-	
-    updateDuration: function() {
 
+	/*
+		#### Private methods from here ####
+	*/
+
+    updateDuration: function() {
         // get the search form
         var form = this.findParentByType('form').getForm();
         // get start value
-        var start = form.findField('startDate').getValue();
+        var start = this.getStartTime();
         // get stop value
-        var stop = form.findField('stopDate').getValue();
+        var stop = this.getStopTime();
         // if duration computable
         if (stop != null && start != null) {
 
@@ -78,18 +93,17 @@ Ext.define('amdaUI.IntervalUI', {
             var zoneOffset = stop.getTimezoneOffset() - start.getTimezoneOffset();
             // compute duration
             var diff = stop - start - zoneOffset*60000;
-            
+
             var durationDays = Math.floor(diff/86400000);
             // set all duration values
-            form.findField('durationDay').setValue(Ext.String.leftPad(durationDays,4,'0')); 
+            form.findField('durationDay').setValue(Ext.String.leftPad(durationDays,4,'0'));
             form.findField('durationHour').setValue(Ext.String.leftPad(Math.floor(diff/3600000 % 24),2,'0'));
             form.findField('durationMin').setValue(Ext.String.leftPad(Math.floor(diff/60000 % 60),2,'0'));
             form.findField('durationSec').setValue(Ext.String.leftPad(Math.floor(diff/1000 % 60),2,'0'));
-           
-            if (durationDays > 9999) {                   
+
+            if (durationDays > 9999) {
                 form.findField('durationDay').markInvalid('Maximum interval is 9999 days!');
             }
-            
         }
 
     },
@@ -99,13 +113,12 @@ Ext.define('amdaUI.IntervalUI', {
         var form = this.findParentByType('form').getForm();
         // get global validation status for duration fields
         return (
-                form.findField('durationDay').isValid() && form.findField('durationHour').isValid() 
+                form.findField('durationDay').isValid() && form.findField('durationHour').isValid()
                 && form.findField('durationMin').isValid() && form.findField('durationSec').isValid()
         );// return true if all duration fields are Valid false otherwise
     },
 
     updateStop: function() {
-
         // get the time form
         var form = this.findParentByType('form').getForm();
         // get duration value
@@ -121,68 +134,72 @@ Ext.define('amdaUI.IntervalUI', {
         form.findField('stopDate').setValue(stop);
 
     },
-	
-	onChangeStartField : function(field, newValue, oldValue)         
-	{           
-            if (field.isValid())  {
-            // get the search form
-                var form = this.findParentByType('form').getForm();
-                // set to the stop datefield the newValue as minValue
-                form.findField('stopDate').setMinValue(newValue);
-                // if it's a user modification
-                if (oldValue != null && this.activeField == 'start')  {
-                    // launch the update of duration fields
-                    this.updateDuration();
-                }
-        }	
+
+	onChangeStartField : function(field, newValue, oldValue)
+	{
+        if (field.isValid())  {
+        // get the search form
+            var form = this.findParentByType('form').getForm();
+            // set to the stop datefield the newValue as minValue
+            form.findField('stopDate').setMinValue(newValue);
+            // if it's a user modification
+            if (oldValue != null && this.activeField == 'start')  {
+                // launch the update of duration fields
+                this.updateDuration();
+            }
+    }
 	},
-	
-	onChangeStopField: function(field, newValue, oldValue){
-        if (field.isValid() && oldValue != null  && this.activeField == 'stop')  {
-            // launch the update of duration fields                              
-            this.updateDuration();  
-        }       
+
+	onChangeStopField: function(field, newValue, oldValue) {
+        if (field.isValid() && oldValue != null  && this.activeField == 'stop') {
+            // launch the update of duration fields
+            this.updateDuration();
+        }
     },
-	
+
 	getDateField : function(fieldName,fieldText,fieldId,onChangeField)
 	{
 		return {
 			layout: {type: 'hbox', align: 'middle'},
             items: [
-                { 
-                    xtype: 'datefield', name: fieldName, format: 'Y/m/d H:i:s',
-                    enforceMaxLength : true,
-                    maxLength: 19,
-                    fieldLabel: fieldText, labelAlign: 'right', labelWidth: 60,
+                {
+					xtype: 'datefield',
+					name: fieldName,
+					format: 'Y/m/d H:i:s',
+					enforceMaxLength: true,
+					maxLength: 19,
+					fieldLabel: fieldText,
+					labelAlign: 'right',
+					labelWidth: 60,
                     listeners: {
                         change: onChangeField,
                         focus: function(field) {
                             this.activeField = fieldId;
                         },
                         scope : this
-                    } 
-                } 
-            ]       
+                    }
+                }
+            ]
         };
 	},
-	
+
 	getStartField : function()
 	{
-		return this.getDateField('startDate','Start Time','start',this.onChangeStartField);
+		return this.getDateField('startDate','Start Time','start', this.onChangeStartField);
 	},
-	
+
 	getStopField : function()
 	{
 		return this.getDateField('stopDate','Stop Time','stop',this.onChangeStopField);
 	},
-	
+
 	getDurationField : function()
 	{
             return {
             layout: {type: 'hbox', align: 'middle'},
             height: 45,
-            defaults: { 
-                xtype: 'textfield', labelAlign: 'top', width: 30, 
+            defaults: {
+                xtype: 'textfield', labelAlign: 'top', width: 30,
                 allowBlank: false, maxLength:2, enforceMaxLength : true,
                 hideTrigger: true,
                 regex: /^[0-9]([0-9])*$/i,
@@ -190,15 +207,15 @@ Ext.define('amdaUI.IntervalUI', {
                     change: function(field, newValue, oldValue){
                         if (this.isValidDuration() && oldValue != null && this.activeField == 'duration')  {
                             // launch the update of stop datefield
-                            this.updateStop();  
-                        }       
+                            this.updateStop();
+                        }
                     },
                     focus: function(field) {
-                        this.activeField = 'duration';                  
-                    },  
+                        this.activeField = 'duration';
+                    },
                     scope : this
-                } 
-            },                           
+                }
+            },
             items:[
                 { xtype: 'displayfield', labelWidth: 60, labelAlign: 'right', width: 60, fieldLabel: '<br>Duration'},
                 { xtype: 'component', width: 5},
@@ -206,20 +223,19 @@ Ext.define('amdaUI.IntervalUI', {
                 { xtype: 'component', width: 5},
                 { name: 'durationHour', fieldLabel: 'Hrs'},
                 { xtype: 'component', width: 5},
-                { name: 'durationMin', fieldLabel: 'Mins'},  
+                { name: 'durationMin', fieldLabel: 'Mins'},
                 { xtype: 'component', width: 5},
                 { name: 'durationSec', fieldLabel: 'Secs'}
             ]
         };
 	},
-	
+
     init : function(config) {
-        
         var me = this;
-        
+
         var myConf = {
                     border: false,
-                    plain: true, 
+                    plain: true,
                     flex: 1,
                     layout: 'anchor',
                     defaults: { height : 30, xtype : 'container'},
@@ -230,9 +246,6 @@ Ext.define('amdaUI.IntervalUI', {
                             me.getDurationField()
                         ]
         };
-
-        Ext.apply (this , Ext.apply (arguments, myConf));  
+        Ext.apply (this , Ext.apply (arguments, myConf));
 	}
 });
-    
-    
\ No newline at end of file
diff --git a/php/classes/EpnTapMgr.php b/php/classes/EpnTapMgr.php
index 2dc60b0..0a41b1c 100644
--- a/php/classes/EpnTapMgr.php
+++ b/php/classes/EpnTapMgr.php
@@ -54,7 +54,7 @@ class EpnTapMgr {
 
 	/* filter order: product type, target name, time min, time max */
 	public function getGranules($table_name, $access_url, $filter, $select, $limit, $offset) {
-		$query = "SELECT TOP $limit " . join(', ', $select) . " FROM $table_name.epn_core " . $this->createFilter($filter[0], $filter[1], $filter[2], $filter[3]) . " OFFSET $offset";
+		$query = "SELECT TOP {$limit} " . join(', ', $select) . " FROM {$table_name}.epn_core " . $this->createFilter($filter[0], $filter[1], $filter[2], $filter[3]) . " OFFSET {$offset}";
 		// return $query;
 		$result = $this->request($access_url, $query);
 		for ($i = 0 ; $i < sizeof($result) ; $i++) {
@@ -67,7 +67,7 @@ class EpnTapMgr {
 
 	/* filter order: product type, target name, time min, time max */
 	public function getNbRows($table_name, $access_url, $filter) {
-		$query = "SELECT COUNT(*) AS nb_rows FROM $table_name.epn_core " . $this->createFilter($filter[0], $filter[1], $filter[2], $filter[3]);
+		$query = "SELECT COUNT(*) AS nb_rows FROM {$table_name}.epn_core " . $this->createFilter($filter[0], $filter[1], $filter[2], $filter[3]);
 		// return $query;
 		return $this->request($access_url, $query)[0]['nb_rows'];
 	}
--
libgit2 0.21.2