// Copyright 1999-2022 - Universite de Strasbourg/CNRS
// The Aladin Desktop program is developped by the Centre de Donnees
// astronomiques de Strasbourgs (CDS).
// The Aladin Desktop program is distributed under the terms
// of the GNU General Public License version 3.
//
//This file is part of Aladin Desktop.
//
//    Aladin Desktop is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, version 3 of the License.
//
//    Aladin Desktop is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    The GNU General Public License is available in COPYING file
//    along with Aladin Desktop.
//

package cds.aladin;

import java.awt.AWTEvent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.MediaTracker;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.ButtonModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.plaf.basic.BasicTextFieldUI;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;

import cds.aladin.bookmark.FrameBookmarks;
import cds.aladin.prop.PropPanel;
import cds.allsky.Constante;
import cds.astro.Astrotime;
import cds.moc.Moc;
import cds.moc.SMoc;
import cds.mocmulti.BinaryDump;
import cds.mocmulti.MocItem;
import cds.mocmulti.MocItem2;
import cds.mocmulti.MultiMoc;
import cds.mocmulti.MultiMoc2;
import cds.tools.Astrodate;
import cds.tools.MultiPartPostOutputStream;
import cds.tools.Util;
import cds.tools.pixtools.CDSHealpix;

/**
 * Classe qui gère l'arbre du Directory des collections
 * @version 1.0 décembre 2016 - création
 * @author Pierre Fernique [CDS]
 */
public class Directory extends JPanel implements Iterable<MocItem>, GrabItFrame {
   
   static protected final String MMOC = "Multiprop.bin";
   
   static public final String GLUMOCSERVER = "MocServer";

   // Nombre de collections appelables individuellement en parallèle
   static private final int MAX_PARALLEL_QUERY = 30;
   
   // Taille de la vignette
   static final int PREVIEW_SIZE=88;

   static private String DIRECTORY, MULTICOL, HELP;
   static protected String AWCSLAB, AWCSTIP, AWMCSTIP, AWSIATIP, AWSSATIP, AWMOCQLAB, AWMOCQLABTIP, AWMOCTITLE, AWMOC1, AWMOC1TIP,
         AWMOC2, AWMOC2TIP, AWMOC3, AWMOC3TIP ;
   static protected String ALLCOLL, MYLIST, ALLCOLLHTML, MYLISTHTML, AWSKYCOV, AWMOCUNK, AWHIPSRES, AWNBROWS, AWREFPUB, AWPROGACC,
         AWPROGACCTIP, AWDATAACC, AWDATAACCTIP, AWDATAACCTIP1, AWINVIEWTIP, AWMOCQLABTIP2, AWXMATCH, AWXMATCHTIP, AWCRIT,
         AWCRITTIP, AWMOCX, AWMOCXTIP, /* AWTMOCX, AWTMOCXTIP, */AWDM, AWDMTIP, AWPROGEN, AWPROGENTIP, AWSCANONLY, AWSCANONLYTIP, AWLOAD, FPCLOSE,
         AWFRAMEINFOTITLE,AWCGRAPTIP,AWCONE,AWCONETIP,AWACCMODE,AWACCMODETIP,AwDERPROD,AwDERPRODTIP,AWINFOTIP,AWPROPTIP,
         AWBOOKMARKTIP,AWPARAMTIP,AWSTICKTIP,AWCUSTOM,AWCUSTOMTIP;
   static private final String UPDATING = "  updating...";
   static protected final String ROOT_LABEL = "Collections";

   private Aladin aladin; // Référence

   protected MultiMoc2 multiProp; // Le multimoc de stockage des properties des collections
   private Color cbg;             // La couleur du fond
   protected double mocServerVersion=-1;   // Numéro de version du MocServer
   protected long mocServerBiggestTimeStamp=0L; // Plus grand TIMESTAMP connu du MocServer

   private DirectoryFilter directoryFilter = null; // Formulaire de filtrage de l'arbre des collections
   private DirectorySort directorySort = null;     // Gestion du tri de l'arbre des collections
   protected boolean mocServerLoading = false;     // true si on est en train de charger le directory initial
   protected boolean mocServerUpdating = false;    // true si on est en train de mettre à jour le directory
   private boolean flagScanLocal = false;          // true si on a des MOCs dans le multiprop local
   protected boolean flagError = false;            // true si l'expression de filtrage est buggée
   private DirectoryTree dirTree;                  // Le JTree du directory
   protected ArrayList<TreeObjDir> dirList;        // La liste des collections connues

   // Composantes de l'interface
   private QuickFilterField quickFilter;           // Champ de filtrage rapide
   protected FilterCombo comboFilter;              // Menu popup des filtres
   protected IconFilter iconFilter;                // L'icone d'activation du filtrage
   protected IconInside iconInside;                // L'icone d'activation du mode "inside"
   protected IconScan iconScan;                    // L'icone d'activation du scan
   private IconCollapse iconCollapse;              // L'icone pour développer/réduire l'arbre
   private IconSort iconSort;                      // L'icone pour trier l'arbre
   private Timer timer = null;                     // Timer pour le réaffichage lors du chargement
   private JLabel dir = null;                      // Le titre qui apparait au-dessus de l'arbre
   private FrameProp frameProp = null;             // Frame des paramètres pour les différents types d'interrogation

   
   static String LoadingUrl=null;                  // L'URL de la vignette entrain d'être chargée

   // Paramètres d'appel initial du MocServer (construction de l'arbre)
   private static String MOCSERVER_INIT = "*&fields=!hipsgen*&get=record&fmt=asciic";

   // Paramètres de maj par le MocServer (update de l'arbre)
   private static String MOCSERVER_PARAM = "fmt=asciic";

   private JScrollPane scrollTree = null;

   private String S(String k) {
      return aladin.chaine.getString(k);
   }

   private void loadString() {
      DIRECTORY = S("DTLABEL");
      MULTICOL = S("AWMULTICOL");
      HELP = S("Datatree.HELP");
      MYLIST = "";
      MYLISTHTML = "-- " + S("DTWORKLIST") + " --";
      ALLCOLLHTML = "-- " + S("DTALLCOLL") + " --";
      ALLCOLL = ALLCOLLHTML;
      AWFRAMEINFOTITLE = S("AWFRAMEINFOTITLE");
      AWCUSTOM = S("AWCUSTOM");
      AWCUSTOMTIP = S("AWCUSTOMTIP");
      AWCSLAB = S("AWCSLAB");
      AWMCSTIP = S("AWMCSTIP");
      AWCGRAPTIP = S("AWCGRAPTIP");
      AWCONE = S("AWCONE");
      AWCONETIP = S("AWCONETIP");
      AWACCMODE = S("AWACCMODE");
      AWACCMODETIP = S("AWACCMODETIP");
      AwDERPROD = S("AwDERPROD");
      AwDERPRODTIP = S("AwDERPRODTIP");
      AWINFOTIP = S("AWINFOTIP");
      AWPROPTIP = S("AWPROPTIP");
      AWBOOKMARKTIP = S("AWBOOKMARKTIP");
      AWPARAMTIP = S("AWPARAMTIP");
      AWSTICKTIP = S("AWSTICKTIP");
      AWCONETIP = S("AWCONETIP");
      AWSIATIP = S("AWSIATIP");
      AWSSATIP = S("AWSSATIP");
      AWMOCQLAB = S("AWMOCQLAB");
      AWMOCQLABTIP = S("AWMOCQLABTIP");
      AWMOCTITLE = S("AWMOCTITLE");
      AWMOC1 = S("AWMOC1");
      AWMOC1TIP = S("AWMOC1TIP");
      AWMOC2 = S("AWMOC2");
      AWMOC2TIP = S("AWMOC2TIP");
      AWMOC3 = S("AWMOC3");
      AWMOC3TIP = S("AWMOC3TIP");
      AWSKYCOV = S("AWSKYCOV");
      AWMOCUNK = S("AWMOCUNK");
      AWHIPSRES = S("AWHIPSRES");
      AWNBROWS = S("AWNBROWS");
      AWREFPUB = S("AWREFPUB");
      AWPROGACC = S("AWPROGACC");
      AWPROGACCTIP = S("AWPROGACCTIP");
      AWDATAACC = S("AWDATAACC");
      AWDATAACCTIP = S("AWDATAACCTIP");
      AWDATAACCTIP1 = S("AWDATAACCTIP1");
      AWINVIEWTIP = S("AWINVIEWTIP");
      AWMOCQLABTIP2 = S("AWMOCQLABTIP2");
      AWXMATCH = S("AWXMATCH");
      AWXMATCHTIP = S("AWXMATCHTIP");
      AWCRIT = S("AWCRIT");
      AWCRITTIP = S("AWCRITTIP");
      AWMOCX = S("AWMOCX");
      AWMOCXTIP = S("AWMOCXTIP");
//      AWTMOCX = S("AWTMOCX");
//      AWTMOCXTIP = S("AWTMOCXTIP");
      AWDM = S("AWDM");
      AWDMTIP = S("AWDMTIP");
      AWPROGEN = S("AWPROGEN");
      AWPROGENTIP = S("AWPROGENTIP");
      AWSCANONLY = S("AWSCANONLY");
      AWSCANONLYTIP = S("AWSCANONLYTIP");
      AWLOAD = S("AWLOAD");
      FPCLOSE = S("FPCLOSE");
   }
   
   // Fournit une légende pour les couleurs de l'arbre
   class LegIn extends JPanel {
      
      public Dimension getPreferredSize() {
         return new Dimension( super.getPreferredSize().width, 18);
      }
      
      public void paintComponent(Graphics g) {
         super.paintComponent(g);
         g.setColor( cbg );
         g.fillRect(0, 0, getWidth(), getHeight());
         if( !isCheckIn ) return;
         g.setFont(g.getFont().deriveFont(Font.ITALIC));
         
         int x=40;
         int y=14;
         g.setColor(Aladin.COLOR_GREEN);
         Util.fillCircle7(g, x, y-4);
         g.setColor( Aladin.COLOR_LABEL );
         g.drawString("in view",x+7,y);
         
         x+=60;
         g.setColor(Aladin.ORANGE);
         Util.fillCircle7(g, x, y-4);
         g.setColor( Aladin.COLOR_LABEL );
         g.drawString("out view",x+7,y);
      }
   }

   public Directory(Aladin aladin, Color cbg) {
      this.aladin = aladin;
      loadString();
      multiProp = new MultiMoc2();
      this.cbg = cbg;
      
//       POUR LES TESTS => Surcharge de l'URL du MocServer
      if( aladin.MOCPROTO || aladin.MOCLOCAL ) {
         String mochost = aladin.MOCLOCAL ? "http://localhost:8080/MocServer/query"
                                          : "http://alasky4.u-strasbg.fr:8080/MocServer/query";
         aladin.glu.aladinDic.put(GLUMOCSERVER,mochost+"?$1");
         aladin.glu.aladinDic.put("MOC_url",mochost+"?$1");
         System.err.println("WARNING: use an alternate MocServer for test =>"+mochost+" !!!");
      }

      setBackground(cbg);
      setLayout(new BorderLayout(0, 0));
      setBorder(BorderFactory.createEmptyBorder(9, 3, 12, 0));
      
      JPanel pTitre1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 35, 0));
      pTitre1.setBackground(cbg);
      dir = new JLabel(DIRECTORY);
      Util.toolTip(dir, S("DTLABELTIP"), true);
      dir.setFont(dir.getFont().deriveFont(Font.BOLD));
      dir.setForeground(Aladin.COLOR_LABEL);
      pTitre1.add(dir);
      
      LegIn inLeg = new LegIn();
      
      JPanel pTitre = new JPanel( new BorderLayout(0,0) );
      pTitre.setBackground(cbg);
      pTitre.add( pTitre1, BorderLayout.NORTH );
      pTitre.add( inLeg, BorderLayout.CENTER );

      // L'arbre avec sa scrollbar
      dirTree = new DirectoryTree(aladin, cbg);
      scrollTree = new JScrollPane(dirTree, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
      scrollTree.setBorder(BorderFactory.createEmptyBorder(16, 0, 0, 0));
      scrollTree.setBackground(cbg);
      // scrollTree.getViewport().setOpaque(true);
      // scrollTree.getViewport().setBackground(cbg);
      scrollTree.setOpaque(false);
      scrollTree.setViewportBorder(null);

      if( Aladin.DARK_THEME ) {
         scrollTree.getVerticalScrollBar().setUI(new MyScrollBarUI());
         scrollTree.getHorizontalScrollBar().setUI(new MyScrollBarUI());
      }

      // Le controle des filtres
      String s = S("DTSELECTTIP");
      JLabel labelFilter = new JLabel(S("DTSELECT"));
      Util.toolTip(labelFilter, s, true);
      labelFilter.setFont(labelFilter.getFont().deriveFont(Font.BOLD));
      labelFilter.setForeground(Aladin.COLOR_LABEL);

      quickFilter = new QuickFilterField(10);
      Util.toolTip(quickFilter, s, true);

      s = S("DTCOMBOTIP");
      JLabel fromLabel = new JLabel(S("DTFROM"));
      Util.toolTip(fromLabel, s, true);
      fromLabel.setFont(fromLabel.getFont().deriveFont(Font.BOLD));
      fromLabel.setForeground(Aladin.COLOR_LABEL);

      comboFilter = new FilterCombo(s);

//      JLabel plus = new JLabel("  + ");
      JLabel plus = new JLabel(new ImageIcon(aladin.getImagette("editplus.png")));
      plus.setBorder( BorderFactory.createEmptyBorder(26, 2,0,0));
      Util.toolTip(plus, S("DTPLUSTIP"), true);
      plus.setFont(Aladin.LBOLD);
      plus.setForeground(Aladin.COLOR_LABEL);
      final Aladin a = aladin;
      plus.addMouseListener(new MouseListener() {
         public void mouseReleased(MouseEvent e) { 
            
            SwingUtilities.invokeLater(new Runnable() {
               public void run() {
                  Aladin.makeCursor(a, Aladin.WAITCURSOR);
                  openAdvancedFilterFrame();
                  Aladin.makeCursor(a, Aladin.DEFAULTCURSOR);
              }
            });

          }
         public void mousePressed(MouseEvent e) { }
         public void mouseExited(MouseEvent e) { }
         public void mouseEntered(MouseEvent e) { }
         public void mouseClicked(MouseEvent e) { }
      });

      // Pour que le quickFilter et le popupFilter aient même taille et soient alignés, je les place
      // tous les deux dans un GridBagLayoutPanel qui sera CENTER dans BorderLayout, et à EAST de ce panel
      // le bouton du " + ". Ca permet également d'éviter de faire disparaitre ce "+" lorsque le bandeau
      // du Directory tree est trop étroit
      //
      // select XXXXXXXX |
      // from XXXXXXXX | +
      //
      JPanel plusFilter = new JPanel(new BorderLayout(0, 0));
      plusFilter.setBackground(cbg);
      plusFilter.add(plus, BorderLayout.EAST); //BorderLayout.SOUTH);

      GridBagConstraints c = new GridBagConstraints();
      GridBagLayout g = new GridBagLayout();
      c.fill = GridBagConstraints.BOTH; // J'agrandirai les composantes
      c.insets = new Insets(2, 3, 3, 2);
      JPanel panelFilter1 = new JPanel(g);
      panelFilter1.setBackground(cbg);
      PropPanel.addCouple(null, panelFilter1, labelFilter, null, quickFilter, g, c, GridBagConstraints.WEST,
            GridBagConstraints.HORIZONTAL);
      PropPanel.addCouple(null, panelFilter1, fromLabel, null, comboFilter, g, c, GridBagConstraints.WEST,
            GridBagConstraints.HORIZONTAL);

      JPanel panelFilter = new JPanel(new BorderLayout(0, 0));
      panelFilter.setBorder(BorderFactory.createEmptyBorder(5, 5, 10, 5));
      panelFilter.setBackground(cbg);
      panelFilter.add(panelFilter1, BorderLayout.CENTER);
      panelFilter.add(plusFilter, BorderLayout.EAST);

      // Les icones de controle tout en bas
      iconFilter = new IconFilter(aladin);
      iconCollapse = new IconCollapse(aladin);
      iconSort = new IconSort(aladin);
      iconInside = new IconInside(aladin);
      iconScan = new IconScan(aladin);

      JPanel pControl = new JPanel(new FlowLayout(FlowLayout.LEFT, 1, 1));
      pControl.setBackground(cbg);
      pControl.add(iconCollapse);
      pControl.add(iconSort);
      pControl.add(iconInside);
      pControl.add(iconScan);
      pControl.add(iconFilter);

      JPanel panelControl = new JPanel(new BorderLayout(0, 0));
      panelControl.setBackground(cbg);
      panelControl.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));
      panelControl.add(panelFilter, BorderLayout.NORTH);
      panelControl.add(pControl, BorderLayout.SOUTH);

      JPanel panelTree = new JPanel(new BorderLayout(0, 0));
      panelTree.setBackground(cbg);
      panelTree.add(pTitre, BorderLayout.NORTH);
      panelTree.add(scrollTree, BorderLayout.CENTER);
 
      add(panelTree, BorderLayout.CENTER);
      add(panelControl, BorderLayout.SOUTH);

      // Actions sur le clic d'un noeud de l'arbre
      dirTree.addMouseListener(new MouseAdapter() {
         public void mouseExited(MouseEvent e) {
            if( timerTip!=null ) { timerTip.stop(); timer=null; }
         }
//         public void mousePressed(MouseEvent e) {
         public void mouseReleased(MouseEvent e) {
            if( timerTip!=null ) timerTip.stop();
            toHighLighted = null;
            TreePath tp = dirTree.getPathForLocation(e.getX(), e.getY());
            if( tp == null ) hideInfo();
            else {
               ArrayList<TreeObjDir> treeObjs = getSelectedTreeObjDir();

               // Double-clic, on effectue l'action par défaut du noeud
               if( e.getClickCount() == 2 ) loadMulti(treeObjs);

               // Simple clic => on montre les informations associées au noeud
               else {
                  if( treeObjs.size() == 1 ) selectInStack(treeObjs.get(0).internalId);
                  showInfo(treeObjs, e);
               }
            }

            iconCollapse.repaint();
            iconSort.repaint();
            resetWasExpanded();
            repaint();
         }
      });

      dirTree.addMouseMotionListener(new MouseMotionListener() {

         public void mouseMoved(MouseEvent e) {
            
            if( Aladin.aladin.inHelp  ) Aladin.aladin.help.setText(HELP);
            
            // Aide Tip sur ce bouton ?
            if( timerTip==null ) timerTip = new Timer(6000, new ActionListener() {
               public void actionPerformed(ActionEvent e) { showTip(); }
            }); 
            timerTip.restart();


            if( frameInfo == null || !frameInfo.isVisible() ) return;
            
            // La maj automatique de la fenêtre d'accès est invalidée si elle a été épinglée
            // il faut explicitement cliqué sur les noeuds
            if( decorated ) return;
            
            TreePath tp = dirTree.getPathForLocation(e.getX(), e.getY());
            if( tp == null ) return;
            if( getSelectedTreeObjDir().size() > 1 ) return;

            // Si on se déplace davantage vers la droite que vers le bas ou le haut
            // => on ne change pas
            Point p = e.getPoint();
            if( lastMove != null && 5 * (p.x - lastMove.x) > Math.abs(p.y - lastMove.y) ) {
               lastMove = p;
               return;
            }
            lastMove = p;

            ArrayList<TreeObjDir> treeObjs = new ArrayList<>();
            DefaultMutableTreeNode to = (DefaultMutableTreeNode) tp.getLastPathComponent();
            if( !to.isLeaf() ) return;
            treeObjs.add((TreeObjDir) to.getUserObject());
            if( treeObjs.size() == 1 ) {
               showInfo(treeObjs, e);
               toHighLighted = treeObjs.get(0);
               dirTree.repaint();
               // if( showInfo(treeObjs,e) ) dirTree.setSelectionPath(tp);
            }
         }

         public void mouseDragged(MouseEvent e) {
         }
      });

      // Chargement des paramètres initiaux
      initParams();

      // Chargement de l'arbre initial
      (new Thread() {
         public void run() {
            updateTree();
         }
      }).start();
   }
   
   // Le drop par défaut considère que cela vient de l'arbre des collections => cf DirectoryTree
   protected void doDrop() {
      ArrayList<TreeObjDir> treeObjs = getSelectedTreeObjDir();
      loadMulti(treeObjs);
   }
   
   // Affichage du tip associé au bouton courant
   private void showTip() {  aladin.configuration.showHelpIfOk("Datatree.HELP"); }
   
   private Timer timerTip = null;


   protected void selectInStack(String id) {
      aladin.calque.selectPlan(id);
   }

   /** Ouvre l'arbre en montrant le noeud associé au path spécifié */
   protected boolean showTreePath(String path) {
      if( !isVisible() || !hasCollections() || path == null ) return false;
      return dirTree.showBranch(path);
   }

   /** Ouvre l'arbre en montrant le noeud associé à l'id (venant du stack) spécifié */
   protected void showTreeObj(String id) {
      if( !isVisible() || !hasCollections() || id == null ) return;
      int i = id.indexOf('~');
      if( i < 0 ) i = id.indexOf(' ');
      if( i > 0 ) id = id.substring(0, i);
      dirTree.showTreeObj(id);
   }
   
   /** Retourne true si le directory contient quelque chose (avant filtrage éventuel) */
   protected boolean hasCollections() {
      return dirList != null && dirList.size() > 0;
   }

   /** Reset du filtre (combobox) possitionné */
   protected void reset() {
      directoryFilter.reset();
   }

   /** Reset complet du filtrage (quickFilter+combobox) */
   protected void fullReset() { fullReset(false); }
   protected void fullReset(boolean flagDefaultExpand ) {
      quickFilter.setText("");
      comboFilter.setSelectedIndex(0);
      reset();
      if( flagDefaultExpand ) dirTree.defaultExpand();

   }

   // Mémorisation de la dernière position de la souris en mouseMoved()
   private Point lastMove = null;

   // Mise à jour du formulaire des filtres s'il est visible
   protected void updateWidget() {
      if( directoryFilter != null && directoryFilter.isShowing() ) directoryFilter.updateWidget();
   }

   /** Met à jour la liste des filtres */
   protected void updateDirFilter() {
      if( comboFilter == null ) return;

      ActionListener[] a = comboFilter.getListeners(ActionListener.class);
      if( a != null ) for( ActionListener a1 : a )
         comboFilter.removeActionListener(a1);

      String current = (String) comboFilter.getSelectedItem();
      comboFilter.removeAllItems();
      comboFilter.addItem(ALLCOLLHTML);

      String mylist = aladin.configuration.filterExpr.get(MYLIST);
      if( mylist != null ) {
         comboFilter.addItem(Directory.MYLISTHTML);
      }

      for( String name : aladin.configuration.filterExpr.keySet() ) {
         if( name.equals(ALLCOLL) ) continue;
         if( name.equals(MYLIST) ) continue;
         comboFilter.addItem(name);
      }

      if( current != null ) comboFilter.setSelectedItem(current);

      if( a != null ) {
         for( ActionListener a1 : a ) comboFilter.addActionListener(a1);
      }
   }

   private TreeObjDir toHighLighted = null;

   /** retourne le noeud qu'il faut encadrer (passage de la souris dessus) */
   protected TreeObjDir getTreeObjDirHighLighted() {
      return toHighLighted;
   }

   class FilterCombo extends JComboBox<String> {
      FilterCombo(String tooltip) {
         super();
         setUI(new MyComboBoxUI());
         // setPrototypeDisplayValue("000000000000000");
         Util.toolTip(this, tooltip, true);
         setFont(Aladin.PLAIN);
         addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
               filtre((String) getSelectedItem());
            }
         });
         updateWidgets();
      }

      void updateWidgets() {
         boolean actif = iconFilter != null && iconFilter.isActivated();
         setBackground(actif ? Aladin.COLOR_TEXT_BACKGROUND : Aladin.COLOR_TEXT_BACKGROUND.darker());
         // setForeground( actif ? Aladin.COLOR_TEXT_FOREGROUND : Aladin.COLOR_CONTROL_FOREGROUND_UNAVAILABLE );
      }
   }

   protected void focusSearch() {
      quickFilter.focus(S("DTYRKW"));
   }

   /** Classe pour un JTextField avec reset en bout de champ (petite croix rouge) */
   class QuickFilterField extends JTextField implements KeyListener/* ,MouseListener,MouseMotionListener */ {
      private Rectangle cross = null;

      QuickFilterField(int nChar) {
         super(nChar);
         addKeyListener(this);
         setUI(new BasicTextFieldUI());
         updateWidgets();
      }

      public void keyTyped(KeyEvent e) {
      }

      public void keyPressed(KeyEvent e) {
      }

      public void keyReleased(KeyEvent e) {
         if( e.getKeyCode() == KeyEvent.VK_ENTER ) {
            if( timer != null ) timer.stop();
//            timer = null;
            doFiltre();
         } else filtre();
      }

      Timer timer = null;

      private void filtre() {
         if( timer == null ) {
            timer = new Timer(500, new ActionListener() {
               public void actionPerformed(ActionEvent e) {
                  doFiltre();
                  requestFocus();
                  timer = null;
               }
            });
            timer.setRepeats(false);
            timer.start();
         } else {
            timer.restart();
         }
         updateWidgets();
      }

      /**
       * Fait clignoter le champ pour attirer l'attention de l'utilisateur et demande le focus sur le champ de saisie
       */
      protected void focus(String s) {
         focus(s, null);
      }

      protected void focus(String s, final String initial) {
         setText(s);

         (new Thread() {
            Color def = getBackground();

            Color deff = getForeground();

            public void run() {
               for( int i = 0; i < 2; i++ ) {
                  setBackground(Color.green);
                  setForeground(Color.black);
                  Util.pause(800);
                  setBackground(def);
                  setForeground(deff);
                  Util.pause(200);
               }
               if( initial == null ) {
                  setText("");
                  requestFocusInWindow();
               } else {
                  setText(initial);
                  requestFocusInWindow();
                  setCaretPosition(getText().length());
               }
               updateWidgets();
            }
         }).start();
      }

      public Dimension getMaximumSize() {
         Dimension d = super.getMaximumSize();
         d.width = 150;
         return d;
      }

      void updateWidgets() {
         boolean actif = iconFilter != null && iconFilter.isActivated();
         setBackground(actif ? Aladin.COLOR_TEXT_BACKGROUND : Aladin.COLOR_CONTROL_BACKGROUND_UNAVAILABLE); // Aladin.COLOR_TEXT_BACKGROUND.darker()
                                                                                                            // );
         setForeground(actif ? Aladin.COLOR_TEXT_FOREGROUND : Aladin.COLOR_CONTROL_FOREGROUND_UNAVAILABLE);
      }

      boolean in(int x) {
         if( cross == null ) return false;
         return x >= cross.x;
      }

      public void paintComponent(Graphics g) {
         try {
            super.paintComponent(g);
            int x = getWidth() - X - 8;
            int y = getHeight() / 2 - X / 2;
            if( mocServerLoading || mocServerUpdating ) {
               Slide.drawBall(g, x, y, blinkState ? Color.white : Color.green);
            }
         } catch( Exception e ) {
         }
      }

      static private final int X = 6;
   }

   /** Retourne true si on a sélectionné quelque chose de scannable */
   protected boolean isScannable() {
      return getSelectedTreeObjDirScannable().size() >= 1;
   }
   
   /** Retourne true si on a sélectionné quelque chose qui est triable */
   protected boolean isSortable() {
      TreePath tp = dirTree.getSelectionPath();
      if( tp==null ) return false;
      DefaultMutableTreeNode to = (DefaultMutableTreeNode) tp.getLastPathComponent();
      return to.getChildCount()>1;
   }

   /** Récupération de la liste des TreeObj sélectionnées qui n'ont pas de MOC à disposition */
   private ArrayList<TreeObjDir> getSelectedTreeObjDirScannable() {
      ArrayList<TreeObjDir> treeObjs = new ArrayList<>();
      for( TreeObjDir to : getSelectedTreeObjDir() ) {
         if( !to.hasMoc() ) treeObjs.add(to);
      }
      return treeObjs;
   }

   /** Récupération de la liste des TreeObj sélectionnées */
   private ArrayList<TreeObjDir> getSelectedTreeObjDir() {
      TreePath[] tps = dirTree.getSelectionPaths();
      ArrayList<TreeObjDir> treeObjs = new ArrayList<>();
      if( tps != null ) {
         for( int i = 0; i < tps.length; i++ ) {
            DefaultMutableTreeNode to = (DefaultMutableTreeNode) tps[i].getLastPathComponent();
            addObj(treeObjs, to);
         }
      }
      return treeObjs;
   }

   private void addObj(ArrayList<TreeObjDir> treeObjs, DefaultMutableTreeNode to) {
      if( to.isLeaf() ) {
         Object obj = to.getUserObject();
         if( obj instanceof TreeObjDir ) treeObjs.add((TreeObjDir) obj);

      } else {
         int n = to.getChildCount();
         for( int i = 0; i < n; i++ ) {
            addObj(treeObjs, (DefaultMutableTreeNode) to.getChildAt(i));
         }
      }
   }

   // La frame d'affichage des informations du HiPS cliqué à la souris
   private FrameInfo frameInfo = null;

   /** Cache la fenêtre des infos du HiPS */
   private void hideInfo() {
      showInfo(null, null);
      toHighLighted = null;
      repaint();
   }

   /** Chargements simultanés de toutes les collections sélectionnées */
   private void loadMulti(ArrayList<TreeObjDir> treeObjs) {
      if( treeObjs.size() == 0 ) return;
      hideInfo();
      if( tooMany(treeObjs.size()) ) return;
      for( TreeObjDir to : treeObjs ) 
         to.load();
   }

   private boolean tooMany(int n) {
      if( n > 20 ) {
         if( !aladin.confirmation(S("DTTOOMANY") + " (" + n + ")") ) return true;
      }
      return false;
   }

   /**
    * Affiche la fenetre des infos des Collections passées en paramètre
    * @param treeObjs les noeuds correspondant aux collections sélectionnées, ou null si effacement de la fenêtre
    * @param e évènement souris pour récupérer la position absolue où il faut afficher la fenêtre d'info
    */
   private boolean showInfo(ArrayList<TreeObjDir> treeObjs, MouseEvent e) {
      if( treeObjs == null || treeObjs.size() == 0 ) {
         if( frameInfo == null ) return false;
         frameInfo.setVisible(false);
         return false;
      }
      boolean created = false;
      if( frameInfo == null ) {
         created=true;
         frameInfo = new FrameInfo(decorated);
      }
      if( !frameInfo.setCollections(treeObjs) ) return false;

      Point p = e.getLocationOnScreen();
      int w = 350;
      int h = 120;
      //      int x = p.x + 50;
      int x = aladin.getLocationOnScreen().x+aladin.splitHiPSWidth.getDividerLocation();
      int y = p.y - 30;
      if( y < 0 ) y = 0;

      // Calcul de la position de la fenêtre si elle n'était pas "décorée"
      rectFrameInfo = new Rectangle( x, y, Math.max(w, frameInfo.getWidth()), Math.max(h, frameInfo.getHeight()) );
      
      // Positionnement de la fenêtre si elle n'est pas "décorée", ou si elle vient d'être créée
      if( !decorated || created )  frameInfo.setBounds( rectFrameInfo );

      frameInfo.setVisible(true);
      if( decorated ) frameInfo.toFront();
      
      this.decorated = decorated;
      treeObjsShown = treeObjs;

      return true;
   }
   
   private boolean decorated=false;
   private Rectangle rectFrameInfo = null;
   private ArrayList<TreeObjDir> treeObjsShown = null;
   
   private boolean reshowInfo(boolean decorated) {
      if( frameInfo==null || !frameInfo.isVisible() ) return false;
      Rectangle bounds = frameInfo.getBounds();
      frameInfo.dispose();
      frameInfo = new FrameInfo(decorated);
      this.decorated=decorated;
      if( !frameInfo.setCollections(treeObjsShown) ) return false;
      frameInfo.setBounds( !decorated ? rectFrameInfo : bounds );
      frameInfo.setVisible(true);
      return true;
      
   }

   // Gestion du scanning
   private boolean isScanning = false; // true si un scan est en cours

   synchronized private void setScanning(boolean flag) {
      isScanning = flag;
   }

   synchronized protected boolean isScanning() {
      return isScanning;
   }

   // Liste des collections en cours de scanning
   private ArrayList<TreeObjDir> scanTreeObjs = null;

   // Le gestionnaire du pool de threads du scan
   private ExecutorService scanService = null;

   // true lorsqu'on a demandé l'arrêt du scan courant
   private boolean abortScan = false;

   /**
    * Demande l'arrêt anticipé du scan en cours. Et force la sortie en exception des collections en cours de scanning en fermant
    * manu-militari leur flux de données vers le serveur. Cette technique n'est pas vraiment super, mais c'est mieux que rien
    */
   synchronized protected void abortScan() {
      if( !isScanning() ) return;
      abortScan = true;

      if( scanService != null ) scanService.shutdownNow();

      // On essaye de forcer la main aux collections en cours de scanning
      // en fermant le flux
      (new Thread() {
         public void run() {
            for( TreeObjDir to : scanTreeObjs ) {
               if( to.isScanning() ) to.abortScan();
            }
         }
      }).start();
   }

   /** true si le scan en cours doit être arrêté dès que possible */
   synchronized protected boolean isAbortingScan() {
      return abortScan;
   }

   /**
    * Lancement d'un scan sur les collections sélectionnées qui ne disposent pas d'un MOC. Lance une requête CS ou SIA ou SSA sur
    * le champ courant, et construit avec les positions retournées un MOC local dans le MultiProp. Celui-ci sera utilisé en ajout
    * du MocServer pour déterminer si la collection a ou n'a pas de résultat dans le champ courant; Afin de connaitre les champs
    * déjà scannés, un MOC de couverture sera également mémorisé dans multiProp (mocRef).
    */
   protected void scan() {

      // On va éviter de lancer plusieurs scans en parallèle.
      if( isScanning() ) {
         aladin.warning(S("DTSCANRUN"));
         return;
      }

      // Déployement de l'arbre pour montrer les collections concernées
      dirTree.allExpand(dirTree.getSelectionPath());

      // Liste des collections sélectionnées scannables
      final ArrayList<TreeObjDir> treeObjs = getSelectedTreeObjDirScannable();

      // Demande de confirmation du scan s'il concerne beaucoup de collections
      final int n = treeObjs.size();
      if( tooMany(n) ) return;

      // On va travaillé avec un pool de threads
      final ExecutorService service = Executors.newFixedThreadPool(10);

      // Mémorisation de la liste pour un éventuel abort (cf abortScan())
      scanTreeObjs = treeObjs;
      scanService = service;

      setScanning(true);
      (new Thread() {

         public void run() {

            // Le timer permet de réafficher l'arbre toutes les demi secondes et ainsi
            // de faire clignoter le logo "noMoc" des collections en cours de scanning
            startTimer();

            try {
               abortScan = false;
               flagScanLocal = true; // le MultiProp local doit être pris en compte

               Aladin.trace(4, "Directory.scan()... Launched on " + n + " data sets...");

               for( final TreeObjDir to : treeObjs ) {

                  // On scanne chaque collection (10 en parallèle)
                  service.execute(new Runnable() {
                     public void run() {
                        try {
                           // System.out.println("Scanning "+to.internalId+"...");
                           MocItem2 mo = multiProp.getItem(to.internalId);
                           to.scan(mo);
                           // System.out.println(mo.mocId+" => "+mo.getMocRef().todebug());

                        } catch( Exception e ) {
                           if( aladin.levelTrace >= 3 ) e.printStackTrace();
                        }
                     }
                  });
               }
               service.shutdown();

               // On ajoute au fur et à mesure les nouvelles collections qui ont été scannées
               while( !service.isTerminated() ) {
                  resumeIn(ResumeMode.LOCALADD);
                  Util.pause(500);
                  // System.out.println("Scan processing...");
               }
            } finally {
               setScanning(false);
               scanTreeObjs = null;
               scanService = null;
               stopTimer();
               Util.pause(200);
               resumeIn(ResumeMode.FORCE); // Un dernier resume complet de l'arbre
               Aladin.trace(4, "Directory.scan() finished");
               repaint();
            }
         }
      }).start();
   }

   /** Création/ouverture/fermeture du formulaire de filtrage de l'arbre des Collections */
   protected void openAdvancedFilterFrame() {
      if( directoryFilter == null ) directoryFilter = new DirectoryFilter(aladin);
//      if( directoryFilter.isVisible() ) directoryFilter.setVisible(false);
//      else {
         String name = (String) comboFilter.getSelectedItem();
         if( name.equals(ALLCOLLHTML) ) {
            name = MYLIST;
            aladin.configuration.setDirFilter(name, "", null);
            updateDirFilter();
            comboFilter.setSelectedItem(MYLISTHTML);
         }

         directoryFilter.showFilter();
//      }
   }

   /** Filtrage de l'arbre des Collections */
   protected void doFiltre() {
      if( directoryFilter == null ) directoryFilter = new DirectoryFilter(aladin);

      int i = aladin.directory.comboFilter.getSelectedIndex();
      if( iconFilter.isActivated() && i > 0 ) {
         aladin.directory.filtre((String) aladin.directory.comboFilter.getSelectedItem());

      } else {
         directoryFilter.submitAction(false);
      }

      if( getNbVisible() <1000 ) dirTree.allExpand();
   }

   /** Activation d'un filtre préalablement sauvegardé */
   protected void filtre(String name) {

      if( name.equals(ALLCOLLHTML) ) name = ALLCOLL;
      else if( name.equals(MYLISTHTML) ) name = MYLIST;

      // System.out.println("filter("+name+")");

      String expr = name.equals(ALLCOLL) ? "*" : aladin.configuration.filterExpr.get(name);
      Moc moc = name.equals(ALLCOLL) ? null : aladin.configuration.filterMoc.get(name);

      if( expr != null || moc != null ) {
         if( directoryFilter == null ) directoryFilter = new DirectoryFilter(aladin);

         int intersect = DirectoryFilter.getIntersect(moc);
         directoryFilter.setSpecificalFilter(name, expr, moc, intersect);
      }
      
      directoryFilter.toFront();
   }

   /** Positionne une contrainte, soit en texte libre, soit cle=valeur */
   protected String getQuickFilterExpr() {
      return getKeyWordExpr(quickFilter.getText().trim());
   }

   /** Génère une contrainte, soit en texte libre, soit cle=valeur */
   protected String getKeyWordExpr(String s) {
      String s1;
      if( s.length() == 0 ) return s;

      String expr;

      // si ça commence par ivo://xxx on le remplace par ID=xxx
      if( s.startsWith("ivo://") ) s = "ID=" + s.substring(6);

      int i = s.indexOf('=');
      if( i < 0 ) i = s.indexOf('>');
      if( i < 0 ) i = s.indexOf('<');

      // S'il s'agit directement d'une expression, rien à faire, ...
      if( i > 0 && (aladin.directory.isFieldName((s1 = s.substring(0, i).trim())) || s1.indexOf('*') >= 0) ) expr = s;

      // Sinon il faut générer l'expression en fonction d'une liste de mots clés
      else {
         // expr = "obs_title,obs_collection,ID="+DirectoryFilter.jokerize(s);

         StringBuilder s2 = null;
         Tok tok = new Tok(s);
         while( tok.hasMoreTokens() ) {
            expr = "obs_title,obs_collection,obs_collection_label,client_category,bib_reference,ID=" + DirectoryFilter.jokerize(tok.nextToken());
            if( s2 == null ) s2 = new StringBuilder(expr);
            else s2.append(" && " + expr);
         }
         expr = s2.toString();
      }

      return expr;
   }

   /** Retourne true si la chaine est une clé de propriété d'au moins un enregistrement */
   protected boolean isFieldName(String s) {
      return multiProp.isFieldName(s);
   }

   /**
    * Retourne le node correspondant à une identiciation
    * @param A l'identificateur de la collection à chercher
    * @param flagSubstring true si on prend en compte le cas d'une sous-chaine
    * @param acceleration 0 - match exact 1 - substring sur label 2 - match exact puis substring sur l'IVOID (ex: Simbad ok pour
    *           CDS/Simbad) puis du menu (ex DssColored ok pour Optical/DSS/DssColored)
    * @return le Hips node trouvé, null sinon NECESSAIRE POUR get Hips(XXX) MAIS IL FAUDRA CHANGER CELA
    */
   protected TreeObjDir getTreeObjDir(String A) {
      return getTreeObjDir(A, 0);
   }

   protected TreeObjDir getTreeObjDir(String A, int mode) {
      for( TreeObjDir to : dirList ) {
         if( !to.isHiPS() ) continue;
         if( idEquals(A,to.id) || A.equals(to.label) || idEquals(A,to.internalId) ) return to;
         if( mode == 1 && Util.indexOfIgnoreCase(to.label, A) >= 0 ) return to;
         if( mode == 2 ) {
            if( to.internalId != null && to.internalId.endsWith(A) ) return to;

            int offset = to.label.lastIndexOf('/');
            if( A.equals(to.label.substring(offset + 1)) ) return to;
         }
      }

      if( mode == 2 ) {
         for( TreeObjDir to : dirList ) {
            if( !to.isHiPS() ) continue;
            int offset = to.label.lastIndexOf('/');
            if( Util.indexOfIgnoreCase(to.label.substring(offset + 1), A) >= 0 ) return to;
         }
      }
      return null;
   }
   
   /** Teste l'égalité de deux ID, sachant que la case n'a pas d'incidence pour l'authority */
   protected boolean idEquals(String id1, String id2) {
      // je teste en insensitive case l'authority
      int i1= id1.indexOf('/');
      int i2= id2.indexOf('/');
      if( i1==i2 && i1>0 ) {
         if( !id1.substring(0,i1).equalsIgnoreCase(id2.substring(0,i2)) ) return false;
         return id1.substring(i1+1).equals(id2.substring(i2+1));
      }
      return id1.equals(id2);
   }

   /**
    * Création du plan via script - méthode identique à celle de la série des classes ServerXXX (de fait, reprise de ServerHips de
    * la version Aladin v9)
    */
   protected int createPlane(String target, String radius, String criteria, String label, String origin) {
      String survey;
      int defaultMode = PlanBG.UNKNOWN;

      if( criteria == null || criteria.trim().length() == 0 ) survey = "CDS/P/DSS2/color";
      else {
         Tok tok = new Tok(criteria, ", ");
         survey = tok.nextToken();

         while( tok.hasMoreTokens() ) {
            String s = tok.nextToken();
            if( s.equalsIgnoreCase("Fits") ) defaultMode = PlanBG.FITS;
            else if( s.equalsIgnoreCase("Jpeg") || s.equalsIgnoreCase("jpg") || s.equalsIgnoreCase("png") )
               defaultMode = PlanBG.JPEG;
         }
      }

      TreeObjDir to = getTreeObjDir(survey, 2);
      if( to == null ) {
         Aladin.error(this, "Progressive survey (HiPS) unknown [" + survey + "]", 1);
         return -1;
      }

      // Pour éviter de charger 2x le même plan HiPS (le test sur label==null permet de ne pas prendre
      // en compte les commandes scripts du genre toto=get ... pour éviter un popup inutile et intempestif
      if( label==null && !Aladin.NOGUI && aladin.calque.isBGAlreadyLoaded(to.internalId) ) {
         if( !aladin.confirmation(aladin, aladin.chaine.getString("HIPSALREADYLOADED")) ) return -1;
      }

      try {
         if( defaultMode != PlanBG.UNKNOWN ) to.setDefaultMode(defaultMode);
      } catch( Exception e ) {
         aladin.command.printConsole("!!! " + e.getMessage());
      }

      return aladin.hips(to, label, target, radius);
   }

   /**
    * Returne l'URL du service_url indiqué (cs,sia,ssa,tap...) pour l'enregistrement properties correspondant au mocid
    * @param service_url (cs,sia,ssa,tap...)
    * @param mocId
    * @return l'URL demandé, ou null si non trouvé
    */
   protected String resolveServiceUrl(String service_url, String mocId) {

      service_url = service_url.toLowerCase();
      
      // un éventuel préfixe ivo:// doit être ignoré
      if( mocId.startsWith("ivo://") ) mocId = mocId.substring(6);

      MocItem m = multiProp.getItem(mocId);
      if( m == null ) return null;

      // Recherche du champ correspondant au service_url indiqué
      String key = service_url + "_service_url";
      String s = m.prop.get(key);
      if( s == null ) return null;

      // Il peut y avoir plusieurs URLs séparées par des TAB. Il faut donc les nettoyer toutes
      StringBuilder url1 = null;
      Tok tok = new Tok(s, "\t");
      while( tok.hasMoreTokens() ) {
         String url = tok.nextToken();

         // Petit nettoyage de l'URL (le VO registry regorge d'imagination...)
         if( service_url.equals("tap") ) {
            if( url.endsWith("/") ) url = url.substring(0,url.length()-1);
         } else {
            if( !url.endsWith("?") && !url.endsWith("&") ) url += "?";
         }

         int pos;
         String fmt;

         // On enlève un éventuel &REQUEST=queryData redondant
         if( service_url.equalsIgnoreCase("ssa") ) {
            fmt = "&REQUEST=queryData";
            if( (pos = Util.indexOfIgnoreCase(url, fmt)) >= 0 ) {
               url = url.substring(0, pos) + url.substring(pos + fmt.length());
            }

            // On trouve vraiment n'importe quoi dans le VO registry !
            url = removeURLParam(url,"POS");
            url = removeURLParam(url,"SIZE");
            
        } else if( service_url.equalsIgnoreCase("sia") ) {
            fmt = "&FORMAT=image/fits";
            if( (pos = Util.indexOfIgnoreCase(url, fmt)) >= 0 ) {
               url = url.substring(0, pos) + url.substring(pos + fmt.length());
            }
            
            // On trouve vraiment n'importe quoi dans le VO registry !
            url = removeURLParam(url,"POS");
            url = removeURLParam(url,"SIZE");
         }

         if( url1 == null ) url1 = new StringBuilder(url);
         else url1.append("\t" + url);
      }

      return url1.toString();
   }
   
   // Supprime d'une URL un paramètre spécifique. Enlève le ? si plus nécessaire
   private String removeURLParam(String url,String paramName) {
      if( url==null || paramName==null ) return url;
      boolean lastParam=false;
      int i = url.indexOf(paramName);
      if( i==-1 ) return url;
      int j = url.indexOf('&',i+1);
      if( j<0 ) lastParam=true;
      boolean firstParam=(i>0 && url.charAt(i-1)=='?');
      if( firstParam && lastParam ) i--;   // pour manger le ?
      return url.substring(0,i) + (lastParam ? "" : url.substring(j+1,url.length()) );
   }

   @Override
   public Iterator<MocItem> iterator() { return multiProp.iterator(); }

   // False lorsque la première initialisation de l'arbre est faite
   private boolean init = true;
   
// CA NE FONCTIONNE PAS CORRECTEMENT ACTUELLEMENT - PF DEC 2017
   protected void reload() {
//      init = true;
//      try {
//         initMultiProp(true);
//         buildTree();
//      } finally {
//         postTreeProcess(true);
//         init = false;
//      }
   }

   /** Maj initiale de l'arbre - et maj des menus Aladin correspondants */
   protected void updateTree() {
      try {
         initMultiProp(false);
         buildTree();
      } finally {
         postTreeProcess(true);
         init = false;
      }
   }

   /**
    * (Re)construction de l'arbre en fonction de l'état précédent de l'arbre et de la valeur des différents flags associés aux
    * noeuds
    * @param refCount true si on doit faire les décomptages des noeuds en tant que référence sinon état courant
    * @return true s'il y a eu au-moins une modif
    */
   private void buildTree() {
      DirectoryModel model = (DirectoryModel) dirTree.getModel();
      model.resetCreate();
      for( TreeObjDir to : dirList ) {
         model.createTreeBranch(to);
         int n;
//         if( DirectoryModel.NB>(n=model.countDescendance()) ) {
//            System.err.println("Bizarre en position NB="+DirectoryModel.NB+" desc="+n+" pour "+to.path+" ["+to+"]");
//         }
      }
      int nb = initCounter(model);
      updateTitre(nb);
   }
   
   private int nbVisible=-1;
   
   /** Retourne le nombre de feuilles de l'arbre visibles */
   private int getNbVisible() { return nbVisible==-1 ? dirList.size() : nbVisible; }

   // Initialisation du compteur de référence en fonction d'un TreeModel
   private int initCounter(DirectoryModel model) {
      // Initialisation du compteur de référence
      HashMap<String, Integer> hs = new HashMap<>();
      int n = model.countDescendance(hs);
      counter = hs;
      return n;
   }

   /** Mise à jour du titre au-dessus de l'arbre en fonction des compteurs */
   private void updateTitre(int nb) {
      String t = DIRECTORY;
      if( nb != -1 && dirList != null ) { //&& nb < dirList.size() ) {
         if( nb < dirList.size() ) {
            t = "<html>" + t + "<font color=\"#D0D0F0\"> &rarr; " + nb + " / " + dirList.size() + "</font></html>";
         } else {
            t = "<html>" + t + "<font color=\"#D0D0F0\"> &rarr; " + nb + "</font></html>";
         }
         nbVisible=nb;
      } else nbVisible=-1;
      
      dir.setText(t);
      if( directoryFilter != null && dirList != null )
         directoryFilter.setLabelResume(nb, dirList.size(), quickFilter.getText().trim().length() > 0);

   }

   // Compteurs des noeuds
   private HashMap<String, Integer> counter = null;

   private HashSet<String> wasExpanded = null;

   protected void resetWasExpanded() {
      wasExpanded = null;
   }
   
   /** Réaffichage de l'arbre avec un nouveau tri */
   protected void resumeSort() {
      aladin.makeCursor(aladin, Aladin.WAITCURSOR);
      if( frameInfo!=null ) aladin.makeCursor(frameInfo, Aladin.WAITCURSOR);
      
      wasExpanded = null;
      resumeTree(dirList, false, true, true);
      
      aladin.makeCursor(aladin, Aladin.DEFAULTCURSOR);
      if( frameInfo!=null ) aladin.makeCursor(frameInfo, Aladin.DEFAULTCURSOR);
   }
   
   /** retourne true si le mode de tri actuel est global */
   protected boolean isGlobalSorted() { return directorySort.isGlobalSorted(); }

   /**
    * Reconstruction de l'arbre en utilisant à chaque fois un nouveau model pour éviter les conflits d'accès et éviter la lenteur
    * des évènements de maj des état de l'arbre (après de très nombreux tests, c'est de loin la meilleure méthode que j'ai trouvée
    * même si elle peut paraitre étonnante à première vue.)
    * @param tmpDirList
    */
   private void rebuildTree(ArrayList<TreeObjDir> tmpDirList, boolean defaultExpand, boolean initCounter,boolean initSort) {
      boolean insideActivated = iconInside.isActivated();
      
      long t0 = System.currentTimeMillis();
      T(null);

      // Mémorisation temporaire des états expanded/collapsed
      if( wasExpanded == null ) {
         wasExpanded = new HashSet<>();
         backupState(new TreePath(dirTree.root), wasExpanded, dirTree);
      }
      
      T("backupState");

      // Mémorisation temporaire du premier noeud sélectionné
      TreePath tp = dirTree.getSelectionPath();

      // Génération d'un nouveau model prenant en compte les filtres
      DirectoryModel model = new DirectoryModel(aladin);

      // S'il y a des noeuds à cacher, on va regénérer un tableau intermédiaire des noeuds restants,
      // mais en le post-triant pour que les branches de l'arbre restent dans le même ordre que précédemment
      ArrayList<TreeObjDir> tmpDirList1 = new ArrayList<>(tmpDirList.size());
      for( TreeObjDir to : tmpDirList ) {
         boolean mustBeActivated = !to.isHidden() && (!insideActivated || insideActivated && to.getIsIn() != 0);
         if( initSort ) {
            directorySort.setInternalSortKey(to.id,to.prop);
            to.setTri();
            to.setPath();
         }
         if( mustBeActivated ) tmpDirList1.add(to);
      }
      
      T("Select nodes");
      
      Collections.sort(tmpDirList1, TreeObj.getComparator());
      
      T("Sort nodes");
      
      model.resetCreate();
      for( TreeObjDir to : tmpDirList1 ) model.createTreeBranch(to);
      
      T("Create tree");

      if( initCounter )  initCounter(model);
//      else updateTitre( model.countDescendance());
      updateTitre( model.countDescendance());
      
      T("Init counters");

      // Répercussion des états des feuilles sur les branches
      model.populateFlagIn();
      T("Populate flags");

      try {
         dirTree.suspendListener();

         // Ouverture minimal des noeuds
         if( defaultExpand ) dirTree.defaultExpand();
         
         // Remplacement du model dans l'arbre affiché
         dirTree.setModel(model);

         T("setModel");

         dirTree.restoreListener();
         
         // Restauration des états expanded/collapses + compteurs de référence
         restoreState(new TreePath(model.root), defaultExpand ? null : wasExpanded, counter, dirTree);
         
         T("Restore tree state");

         // Restauration des noeuds sélectionnées
         if( tp != null ) {
            dirTree.suspendListener();
            showTreePath(getPathString(tp));
            dirTree.restoreListener();
        }
         
         
      } catch( Exception e ) {
         if( aladin.levelTrace>=3 ) e.printStackTrace();
         
      // En cas de souci
      } finally {
         dirTree.restoreListener();
      }

      aladin.trace(4,"ResumeTree done in "+(System.currentTimeMillis()-t0)+"ms");

   }
   
   private boolean TEST=false;
   private long t0;
   private void T(String s) {
      if( !TEST ) return;
      long t = System.currentTimeMillis();
      if( s==null ) { t0=t; return; }
      System.out.println(s+" => "+(t-t0));
      t0=t;
   }

   /**
    * Retourne le path sous forme de chaine - sans le premier "/" et "" pour la racine ex => Image/Optical/DSS
    */
   private String getPathString(TreePath p) {
      boolean first = true;
      StringBuilder path = null;
      for( Object n : p.getPath() ) {
         if( first ) {
            first = false;
            continue;
         } // on ne prend pas la racine
         if( path == null ) path = new StringBuilder(n + "");
         else path.append("/" + n);
      }
      return path == null ? "" : path.toString();
   }

   /** Mémorise dans une HashSet les branches qui sont expanded */
   private void backupState(TreePath parent, HashSet<String> wasExpanded, DirectoryTree tree) {
      DefaultMutableTreeNode lastNode = (DefaultMutableTreeNode) parent.getLastPathComponent();
      TreeObj to = (TreeObj) lastNode.getUserObject();
      if( tree.isExpanded(parent) ) wasExpanded.add(to.path);

      if( lastNode.getChildCount() >= 0 ) {
         for( Enumeration e = lastNode.children(); e.hasMoreElements(); ) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
            backupState(parent.pathByAddingChild(node), wasExpanded, tree);
         }
      }
   }

//   private String parent(String path) {
//      int i = path.lastIndexOf('/');
//      if( i < 0 ) return "";
//      return path.substring(0, i);
//   }

   /**
    * Restaure l'état des branches en fonction d'une hashSet des branches qui étaient expanded, ainsi que d'une hashmap donnant
    * les décomptes de référence pour chaque branche.
    */
   private void restoreState(TreePath parent, HashSet<String> wasExpanded, HashMap<String, Integer> counter, JTree tree) {
      DefaultMutableTreeNode lastNode = (DefaultMutableTreeNode) parent.getLastPathComponent();
      String path = getPathString(parent);

      // Récupération de l'état expanded/collapsed
      if( wasExpanded != null ) {
         if( wasExpanded.contains(path) ) tree.expandPath(parent);
      }

      // Récupération du compteur de référence
      Integer nbRef = counter.get(path);
      if( nbRef != null ) {
         TreeObj to = (TreeObj) lastNode.getUserObject();
         to.nbRef = nbRef.intValue();
      }

      if( lastNode.getChildCount() >= 0 ) {
         for( Enumeration e = lastNode.children(); e.hasMoreElements(); ) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
            restoreState(parent.pathByAddingChild(node), wasExpanded, counter, tree);
         }
      }
   }
   
   /** Réaffichage de l'arbre en fonction des flags courants */
   protected void resumeTree() {
      resumeTree(dirList, false, false, false);
   }

   /** Remplacement et réaffichage de l'arbre avec une nouvelle liste de noeuds */
   protected void replaceTree(ArrayList<TreeObjDir> tmpDirList) {
      resumeTree(tmpDirList, true, true, true);
      dirList = tmpDirList;
   }

   /** Réaffichage de l'arbre */
   private void resumeTree(ArrayList<TreeObjDir> tmpDirList, boolean defaultExpand, boolean initCounter,boolean initSort) {
      try {
         rebuildTree(tmpDirList, defaultExpand, initCounter, initSort);
         validate();
         postTreeProcess(defaultExpand);
      } finally {

         // Pour permettre le changement du curseur d'attente de la fenêtre de filtrage
         if( directoryFilter != null ) aladin.makeCursor(directoryFilter, Aladin.DEFAULTCURSOR);
      }

   }

   /** retourne true si un filtre est positionné */
   protected boolean hasFilter() {
      String s = quickFilter.getText();
      if( s == null || s.startsWith(UPDATING) ) s = "";
      return s.length() > 0 || comboFilter.getSelectedIndex() > 0;
   }

   // Mémorisation du dernier filtre demandé (pour éviter de l'appliquer 2x de suite)
   private String oExpr = null;

   private int oIntersect = -1;

   private Moc oMoc = null;

   /**
    * Filtrage et réaffichage de l'arbre en fonction des contraintes indiquées dans params
    * @param expr expression ensembliste de filtrage voir doc multimoc.scan(...)
    * @param moc filtrage, null si aucun
    * @param intersect pour le filtrage spatial: MultiMoc.OVERLAPS, ENCLOSED ou COVERS
    */
   protected void resumeFilter(String expr, Moc moc, int intersect) {
      try {

         // Ajout de la contrainte du filtre rapide à l'expression issue du filtre global
         String quick = getQuickFilterExpr();
         if( quick.length() > 0 ) {
            if( expr.length() == 0 || expr.equals("*") ) expr = quick;
            else expr = "(" + expr + ") && " + quick;
         }

         // On vient-ti pas de le faire ?
         if( expr.equals(oExpr) && (moc == oMoc || moc != null && moc.equals(oMoc) && intersect == oIntersect) ) { return; }
         oExpr = expr;
         oIntersect = intersect;
         oMoc = moc == null ? null : moc.clone();

         // logs
         String smoc = moc == null ? "" : directoryFilter.getASCII(moc);
         if( smoc.length() > 0 || expr.length() > 0 && !expr.equals("*") ) {
            String log = expr + (smoc.length() > 0 ? " AND MOC:" + smoc : "");
            aladin.trace(4, "Directory.resumeFilter() => " + log);
            aladin.glu.log("DirectoryFilter", log);
         }

         // Filtrage
         try {
            checkFilter(expr, moc, intersect);
            flagError = false;
         } catch( Exception e ) {
            if( Aladin.levelTrace >= 3 ) e.printStackTrace();
            flagError = true;
         }

         // Regénération de l'arbre
         resumeTree();

         if( getNbVisible() <1000 ) dirTree.allExpand();

         updateWidgets();

      } catch( Exception e ) {
         if( Aladin.levelTrace >= 3 ) e.printStackTrace();
      }
   }

   private void updateWidgets() {
      iconFilter.repaint();

      quickFilter.updateWidgets();
      comboFilter.updateWidgets();
   }

   static protected enum ResumeMode {
      NORMAL, FORCE, LOCALADD
   };

   /** Réaffichage de l'arbre en fonction des Hips in/out de la vue courante */
   protected void resumeIn() {
      resumeIn(ResumeMode.NORMAL);
   }

   protected void resumeIn(ResumeMode mode) {
      if( !Aladin.NETWORK ) return;
      if( !checkIn(mode) ) return;
      if( iconInside.isActivated() ) resumeTree();
      else ((DirectoryModel) dirTree.getModel()).populateFlagIn();
      repaint();
   }

   /**
    * Positionnement des flags isHidden() de l'arbre en fonction des contraintes de filtrage
    * @param expr expression ensembliste de filtrage voir doc multimoc.scan(...)
    * @param moc filtrage, null si aucun
    * @param intersect pour le filtrage spatial, OVERLAPS, ENCLOSED ou COVERS
    */
   private void checkFilter(String expr, Moc moc, int intersect) throws Exception {

      // Filtrage par expression
      long t0 = System.currentTimeMillis();
      ArrayList<String> ids = multiProp.scan((Moc) null, expr, false, -1, -1);

      // Filtrage spatial
      ArrayList<String> ids1 = filtrageSpatial(moc, intersect);

      // Positionnement des flags isHidden() en fonction du filtrage
      HashSet<String> set = new HashSet<>(ids.size());
      for( String s : ids ) {
         if( ids1 != null && !ids1.contains(s) ) continue;
         set.add(s);
      }
      int nbHidden=0;
      for( TreeObjDir to : dirList ) {
         boolean flagHidden=!set.contains(to.internalId);
         to.setHidden(flagHidden);
         if( flagHidden ) nbHidden++;
      }
   }

   private Moc oldMocSpatial = null;

   private ArrayList<String> oldIds = null;

   private int oldIntersect = MultiMoc.OVERLAPS;

   /**
    * Filtrage spatial sur le MocServer distant. Utilise un cache pour éviter de faire plusieurs fois de suite la même requête
    * @param moc
    * @param intersect pour le filtrage spatial, OVERLAPS, ENCLOSED ou COVERS
    * @return la liste des IDs qui matchent
    */
   private ArrayList<String> filtrageSpatial(Moc moc, int intersect) {
      if( moc == null ) return null;
      if( oldMocSpatial != null && intersect == oldIntersect && oldMocSpatial.equals(moc) ) return oldIds;

      oldIntersect = intersect;
      oldMocSpatial = moc;
      try {
         oldIds = filtrageSpatial1(moc, intersect);
      } catch( Exception e ) {
         oldIds = null;
         if( aladin.levelTrace >= 3 ) e.printStackTrace();
      }
      return oldIds;
   }

   /**
    * Filtrage sur le MocServer distant. Utilise un cache pour éviter de faire => voir filtrageSpatial(...)
    */
   private ArrayList<String> filtrageSpatial1(Moc moc, int intersect) throws Exception {
      String url = aladin.glu.getURL(GLUMOCSERVER).toString();
      int i = url.lastIndexOf('?');
      if( i > 0 ) url = url.substring(0, i);

      MultiPartPostOutputStream.setTmpDir(Aladin.CACHEDIR);
      String boundary = MultiPartPostOutputStream.createBoundary();
      HttpURLConnection urlConn = (HttpURLConnection) MultiPartPostOutputStream.createConnection(new URL(url));
      urlConn.setRequestProperty("Accept", "*/*");
      urlConn.setRequestProperty("Content-Type", MultiPartPostOutputStream.getContentType(boundary));
      urlConn.setRequestProperty("Connection", "Keep-Alive");
      urlConn.setRequestProperty("Cache-Control", "no-cache");
      MultiPartPostOutputStream out = new MultiPartPostOutputStream(urlConn.getOutputStream(), boundary);
      if( intersect != MultiMoc.OVERLAPS ) out.writeField("intersect", MultiMoc.INTERSECT[intersect]);
      File tmp = File.createTempFile("tmp", "fits");
      tmp.deleteOnExit();
      FileOutputStream fo = new FileOutputStream(tmp);
      try {
         moc.writeFITS(fo);
      } finally {
         try {
            fo.close();
         } catch( Exception e ) {
         }
      }
      out.writeFile("moc", null, tmp, false);
      out.close();

      // récupération de chaque ID concernée (1 par ligne)
      BufferedReader in = null;
      ArrayList<String> ids = null;
      try {
         in = new BufferedReader(new InputStreamReader(urlConn.getInputStream()));
         String s;
         ids = new ArrayList<>();
         while( (s = in.readLine()) != null )
            ids.add(getId(s));
      } finally {
         in.close();
      }

      return ids;
   }

   /**
    * Détermination de l'ordre pour avoir 30 cellules dans la distance
    * @param size taille à couvrir (en degrés)
    * @return order HEALPix approprié
    */
   static public int getAppropriateOrder(double size) {
      int order = 4;
      if( size == 0 ) order = SMoc.MAXORD_S;
      else {
         double pixRes = size / 30;
         double degrad = Math.toDegrees(1.0);
         double skyArea = 4. * Math.PI * degrad * degrad;
         double res = Math.sqrt(skyArea / (12 * 16 * 16));
         while( order < SMoc.MAXORD_S && res > pixRes ) {
            res /= 2;
            order++;
         }
      }
      return order;
   }

   /**
    * Retourne true si pour la collection identifiée par "id" on dispose d'un MOC local sur la zone décrite par mocQuery (càd que
    * leur intersection n'est pas nulle
    */
   private boolean hasLocalMoc(String id, SMoc mocQuery) throws Exception {
      if( mocQuery == null ) return false;
      MocItem2 mo = multiProp.getItem(id);
      if( mo.mocRef == null ) return false;
      return mo.mocRef.isIntersecting(mocQuery);
   }

   // Dernier champs interrogé sur le MocServer
   private Coord oc = null;
   
   private double orange [] = new double[]{ -1, -1 };

   private double osize = -1;

   private HashSet<String> previousSet = null;
   
   private boolean isCheckIn = false;

   /**
    * Interroge le MocServer pour connaître les Collections disponibles dans le champ. Met à jour l'arbre en conséquence
    * @param mode RESUME_IN_IF_NEEDED, RESUME_IN_FORCE, RESUME_IN_FORCE_LOCAL
    */
   private boolean checkIn(ResumeMode mode) {
      if( !dialogOk() ) return false;
      if( aladin.isAnimated() ) return false;
      if( aladin.view.getCurrentView().flagClicAndDrag ) return false;

      // Le champ est trop grand ou que la vue n'a pas de réf spatiale ?
      // => on suppose que tous les HiPS sont a priori visibles
      ViewSimple v = aladin.view.getCurrentView();
      if( v.isFree() /* || v.isAllSky() */|| !Projection.isOk(v.getProj()) || (v.isPlot() && !v.isPlotTime()) ) {
         boolean modif = false;
         for( TreeObjDir to : dirList ) {
            if( !modif && to.getIsIn() != -1 ) modif = true;
            to.setIn(-1);
         }
         isCheckIn=false;
         return modif;
      }

      try {

         HashSet<String> set = mode == ResumeMode.LOCALADD ? previousSet : new HashSet<String>();

         // Pour éviter de faire 2x la même chose de suite
         Coord c=null;
         double size=0; 
         if( !v.isPlotTime() ) {
            c = v.getCooCentre();
            size = v.getTaille();
         }
         boolean sameLocation = c==null && oc==null || c!=null && c.equals(oc) && size == osize;

         double [] range = v.getTimeRange();
         boolean sameTime =  Double.isNaN(range[0]) && Double.isNaN(orange[0]) && Double.isNaN(range[1]) && Double.isNaN(orange[1])
               || orange[0]==range[0] && orange[1]==range[1];


         String params;
         if( mode == ResumeMode.NORMAL && sameLocation && sameTime ) return false;
         oc = c;
         osize = size;
         orange = range;


         boolean withSpace = c!=null;
         boolean withTime = mocServerVersion>=5 && (!Double.isNaN(range[0]) || !Double.isNaN(range[1]));
         boolean withsys =  mocServerVersion>=5.05;

         if( !withTime && !withSpace ) return true;
         
         String sysParam="";
         if( withsys && withSpace ) {
            String body = v.pref.getBody();
            if( body!=null ) {
               if( body.equals(Plan.BODYSKY) ) body="C";
               sysParam="&spacesys="+URLEncoder.encode(body);
            }
         }

         // Interrogation du MocServer distant...
         BufferedReader in = null;

         params = MOCSERVER_PARAM+sysParam;
         if( withSpace ) {
            double taille = v.getTaille();
            if( taille > 90 ) {
               params += "&moc=0/0-11";
               
               // Interrogation par cercle
            } else if( taille > 45 ) {
               params += "&RA=" + c.al + "&DEC=" + c.del + "&SR=" + size * Math.sqrt(2);

               // Interrogation par rectangle
            } else {
               StringBuilder s1 = new StringBuilder("Polygon");
               for( Coord c1 : v.getCooCorners() ) s1.append(" " + c1.al + " " + c1.del);
               params += "&stc=" + URLEncoder.encode(s1.toString());
            }
         }
         if( withTime ) {
            double mjdmin = Double.isNaN(range[0]) ? 0 : Astrotime.JD2MJD(range[0]);
            double mjdmax = Double.isNaN(range[1]) ? Double.MAX_VALUE : Astrotime.JD2MJD(range[1]);
            params += "&TIME="+URLEncoder.encode(mjdmin+" "+mjdmax);
         }

         try {
            if( mode == ResumeMode.FORCE || !sameLocation || !sameTime ) {
               URL u = aladin.glu.getURL(GLUMOCSERVER, params, true);

               Aladin.trace(3, "Directory.hipsUpdate: Contacting MocServer : " + u);
               in = new BufferedReader(new InputStreamReader(Util.openStream(u)));
               String s;

               // récupération de chaque ID concernée (1 par ligne)
               while( (s = in.readLine()) != null ) set.add(getId(s));
            }

         } catch( EOFException e ) {
         } finally {
            if( in != null ) in.close();
         }

         // Interrogation du Multimoc interne (uniquement par cercle)
         SMoc mocQuery = null;
         if( flagScanLocal && (mode == ResumeMode.FORCE || mode == ResumeMode.LOCALADD || !sameLocation) ) {

            // Construction d'un MOC qui englobe le cercle couvrant le champ de vue courant
            int order = getAppropriateOrder(size);
            mocQuery = CDSHealpix.getMocByCircle(order, c.al, c.del, Math.toRadians(size / 2), true);

            // System.out.println("Moc d'interrogation => "+mocQuery.todebug());

            ArrayList<String> mocIds = multiProp.scan(mocQuery);
            if( mocIds != null ) for( String id : mocIds )
               set.add(id);
         }

         // Vérification si ça a changé depuis la dernière fois
         if( previousSet != null && previousSet != set && previousSet.equals(set) ) {
            // System.out.println("Pas de changement depuis la dernière fois");
            return false;
         }
         previousSet = set;

         // Positionnement des flags correspondants
         for( TreeObjDir to : dirList ) {
            if( !to.hasMoc() && !hasLocalMoc(to.internalId, mocQuery) ) to.setIn(-1);
            else to.setIn(set.contains(to.internalId) ? 1 : 0);
         }
         isCheckIn=true;

      } catch( Exception e1 ) {
         if( Aladin.levelTrace >= 3 ) e1.printStackTrace();
      }

      return true;
   }

   /** Retourne true si l'arbre est développé selon le défaut prévu */
   protected boolean isDefaultExpand() {
      return dirTree.isDefaultExpand();
   }
   
   /** Affichage du popupmenu de tri par branches */
   protected void triBranch(Component parent, int x, int y) {
      TreePath tps = dirTree.getSelectionPath();
      if( tps==null ) return;
      DefaultMutableTreeNode to = (DefaultMutableTreeNode) tps.getLastPathComponent();
      TreeObj t = (TreeObj)to.getUserObject();
      JPopupMenu popup = directorySort.createBranchPopup( t.path );
      popup.show(parent,x,y);
   }

   /** Affichage du popupmenu de tri global */
   protected void triGlobal(Component parent, int x, int y) {
      JPopupMenu popup = directorySort.createGlobalPopup( );
      popup.show(parent,x,y);
   }

   /** Collapse/Expande l'arbre en fonction du noeud courant et de l'état de l'arbre */
   protected void collapseOrNot() {

      // Si j'ai une ou plusieurs branches sélectionnées...
      TreePath[] tps = dirTree.getSelectionPaths();
      if( tps != null ) {

         // Sélection d'une feuille => on collapse tout l'arbre sauf la feuille
         if( tps.length == 1 && ((DefaultMutableTreeNode) tps[0].getLastPathComponent()).isLeaf() ) {
            dirTree.defaultExpand();
            dirTree.expandPath(tps[0].getParentPath());
            dirTree.setSelectionPath(tps[0]);

            // Selection de plusieurs noeud
         } else {

            int mode = 0; // -1 collapse, 0-pas encore initialisé, 1 expand
            for( TreePath tp : tps ) {

               // cas particulier de la racine => on ne referme pas tout
               if( tp.getPathCount() == 1 ) {
                  if( mode == 1 || isDefaultExpand() ) {
                     mode = 1;
                     dirTree.allExpand();
                  } else {
                     mode = -1;
                     dirTree.defaultExpand();
                  }

                  // Cas de la sélection d'une branche
               } else {

                  // S'il n'est pas totalement ouvert, je le fais
                  if( mode == 1 || dirTree.isCollapsed(tp) ) {
                     mode = 1;
                     dirTree.allExpand(tp);
                  } else {
                     mode = -1;
                     dirTree.collapseRec(tp);
                  }
               }
            }
         }

         // Rien n'est sélectionné => comme si on avait sélectionné la racine
      } else {
         if( isDefaultExpand() ) dirTree.allExpand();
         else dirTree.defaultExpand();
      }
      resetWasExpanded();
   }

   /** Retourne true s'il n'y a pas d'arbre HiPS */
   protected boolean isFree() {
      return dirTree == null || dirTree.root == null;
   }

   /** Traitement à appliquer après la génération ou la régénération de l'arbre */
   private void postTreeProcess(boolean minimalExpand) {

      // filter.setEnabled( dialogOk() );
      if( minimalExpand ) dirTree.minimalExpand();

      // Mise en route ou arrêt du thread de coloration de l'arbre en fonction des Collections
      // présentes ou non dans la vue courante
      if( hasCollections() ) startInsideUpdater();
      else stopInsideUpdater();
   }

   /** Ajoute si nécessaire les propriétés d'un fichier "properties" distant */
   private void addRemoteProp(MyProperties prop) {
      String u = prop.getProperty("hips_service_url");
      if( u == null || prop.getProperty("hips_order") != null ) return;

      InputStreamReader in = null;
      try {
         in = new InputStreamReader(Util.openAnyStream(u + "/properties"), "UTF-8");
         MyProperties aProp = new MyProperties();
         aProp.load(in);

         for( String key : aProp.getKeys() ) {
            if( prop.get(key) == null ) prop.put(key, aProp.get(key));
         }
      } catch( Exception e ) {
      } finally {
         try {
            if( in != null ) in.close();
         } catch( Exception e ) {
         }
      }

   }

   private boolean interruptServerReading;

   /** (Re)génération du Multiprop en fonction d'un stream d'enregistrements properties */
   protected int loadMultiProp(InputStreamReader in, boolean addition, String path) throws Exception {
      return loadMultiProp(in,addition,path,false);
   }
   protected int loadMultiProp(InputStreamReader in, boolean addition, String path,boolean debug) throws Exception {
      MyProperties prop;

      boolean mocServerReading = true;
      interruptServerReading = false;
      int n = 0;
      int rm = 0;
      String memo = "";

      if( debug ) aladin.trace(3,"loadMultiProp debug:");
      try {
         String id=null;
         memo = quickFilter.getText();
         quickFilter.setEditable(false);
         quickFilter.setForeground(Aladin.COLOR_CONTROL_FOREGROUND_UNAVAILABLE);

         while( mocServerReading && !interruptServerReading ) {
            prop = new MyProperties();
            mocServerReading = prop.loadRecord(in);
            if( prop.size() == 0 || (id=MultiMoc.getID(prop)) == null ) continue;
            

            // Définition locale => PROP_ORIGIN = local
            if( addition ) {
               prop.setProperty("PROP_ORIGIN", "local");

               // si HiPS, peut être requière un complément d'information par accès au fichier properties distant ?
               addRemoteProp(prop);

               // Dans le cas d'un fichier local, on conserve on éventuel path pour le HiPS
               if( path != null ) prop.setProperty("hips_service_url", path);
            }

            try {
               if( prop.getProperty("MOCSERVER_REMOVE") != null ) {
                  multiProp.remove(id);
                  rm++;
               } else multiProp.add(prop);
               quickFilter.setText(UPDATING + " (" + n + ")");

            } catch( Exception e ) {
               if( Aladin.levelTrace >= 3 ) e.printStackTrace();
            }

            n++;
            // if( n%100==0 ) Util.pause(10);
         }
         if( interruptServerReading ) aladin.trace(3, "MocServer update interrupted !");
         if( n > 0 ) aladin.trace(3,
               "Directory.loadMultiProp(..) " + (n - rm) + " prop loaded " + (rm > 0 ? " - " + rm + " removed" : "") + "...");
      } finally {
         try {
            in.close();
         } catch( Exception e ) {
         }
         quickFilter.setEditable(true);
         quickFilter.setForeground(Aladin.COLOR_TEXT_FOREGROUND);
         quickFilter.setText(memo);
      }

      return n;
   }
   

   /** Interruption du chargement des infos du MocServer */
   protected void interruptMocServerReading() {
      interruptServerReading = true;
   }
   
   private boolean infoVizieROld;   // true si on doit afficher un message concernant les enr. VizieR traduits
   
   /**
    * Génération de la liste des collections en fonction du contenu du MultiProp La liste est triée Les URLs HiPS seront
    * mémorisées dans le Glu afin de pouvoir gérer les sites miroirs
    */
   private ArrayList<TreeObjDir> populateMultiProp() {
      infoVizieROld=true;
      ArrayList<TreeObjDir> listReg = new ArrayList<>(30000);
      multiple=null;  // Il faut reseter la liste des collections à plusieurs "feuilles"
      
//      for( MocItem mi : this ) populateProp(listReg, mi.prop);
      Iterator<MocItem> it = multiProp.iterator(true);
      while( it.hasNext() ) populateProp(listReg, (it.next()).prop);
      
      Collections.sort(listReg, TreeObj.getComparator());
      return listReg;
   }

   private static final String AD = "AladinDesktop";

   private boolean hasValidProfile(MyProperties prop) {
      Iterator<String> it = prop.getIteratorValues("client_application");
      if( it == null ) return true;
      boolean rep=true;
      boolean first=true;
      
      while( it.hasNext() ) {
         String profile = it.next();
         Tok tok = new Tok(profile);
         while( tok.hasMoreTokens() ) {
            profile = tok.nextToken();
            int offset = 0;
            if( (offset = profile.indexOf(AD)) < 0 ) continue;
            profile = profile.substring(offset + AD.length()).trim();

            // Si j'ai une mention de AladinDesktop, j'analyse le suffixe éventuel (ex: AladinDesktopBeta>9.6)
            if( profile.length() > 0 ) {
               // Si plusieurs indications, il en suffit d'une valide pour que ça passe
               if( first ) { first=false; rep=false; }
               String gluProfile = getGluProfile(profile);
//                System.out.println("Profile = "+gluProfile);
               rep |= aladin.glu.hasValidProfile(gluProfile);
            }
         }
      }
      return rep;
   }

   /** Retourne true si on a au moins une clé vers une url d'accès */
   private boolean hasURLkey(MyProperties prop) {
      for( String k : prop.getKeys() ) {
         if( k.indexOf("url") >= 0 ) return true;
      }
      return false;
   }

   // Conversion d'un profile à la "properties", en un profile à la "GLU"
   // Exemple: "Beta>9.6" sera converti en "beta >9.6"
   private String getGluProfile(String s) {
      StringBuilder rep = new StringBuilder();
      int mode = 0;
      for( char c : s.toCharArray() ) {
         switch( mode ) {
            case 0: // Avant un nouveau token ou dans un mot
               if( Character.isUpperCase(c) ) rep.append(" " + Character.toLowerCase(c));
               else if( isDigit(c) || c == '<' || c == '>' ) {
                  mode = 1;
                  rep.append(" " + c);
               } else rep.append(c);
               break;
            case 1: // Dans un numéro de version (token numérique)
               if( c == '<' || c == '>' ) rep.append(" " + c);
               else if( !isDigit(c) && c != '=' ) {
                  mode = 0;
                  rep.append(" " + Character.toLowerCase(c));
               } else rep.append(c);
               break;
         }
      }
      return rep.toString();
   }
   
   private boolean isDigit(char c) { return Character.isDigit(c) || c=='.'; }

   /**
    * Ajout d'une collection correspondant à un enregistrement prop, ainsi que des entrées GLU associées
    * @param prop Enregistrement des propriétés
    * @param localFile true si cet enregistrement a été chargé localement par l'utilisateur (ACTUELLEMENT NON UTILISE)
    */
   private void populateProp(ArrayList<TreeObjDir> listReg, MyProperties prop) {

      // Détermination de l'identificateur
      String id = MultiMoc.getID(prop);
      if( id == null ) {
         System.err.println(
               "Directory.populateProp error - getID returns null => ignored [" + prop.toString().replace("\n", " ") + "]");
         return;
      }
      
      // S'agit-il d'un hips privé ?
      String status = prop.getProperty("hips_status");
      if( status != null && status.indexOf("private") >= 0 ) return;

      // Est-ce qu'il y a un "profile", et si oui, est-il compatible avec cette version d'Aladin
      if( !hasValidProfile(prop) ) return;

      // Ajustement local des propriétés
      try {
         if( !propAdjust(id, prop) ) return;
      } catch( Exception e ) {
         if( aladin.levelTrace>=3 ) e.printStackTrace();
         return;
      }
      
      // Y a-t-il au moins un moyen d'accès ?
      if( !hasURLkey(prop) ) return;
      

      String HIPSU = Constante.KEY_HIPS_SERVICE_URL;

      // Dans le cas où il n'y a pas de mirroir, mémorisation de l'URL directement
      if( prop.getProperty(HIPSU + "_1") == null ) {
         String url = prop.getProperty(HIPSU);
         if( url != null ) aladin.glu.aladinDic.put(id, url);

         // Dans le cas où il y a des mirroirs => mémorisation des indirections
      } else {

         // Enregistrement de tous les mirroirs comme des indirections
         StringBuilder indirection = null;
         for( int i = 0; true; i++ ) {
            String url = prop.getProperty(HIPSU + (i == 0 ? "" : "_" + i));
            if( url == null ) break;

            String subid = id + "_" + i;
            aladin.glu.aladinDic.put(subid, url);

            if( indirection == null ) indirection = new StringBuilder("%I " + subid);
            else indirection.append("\t" + subid);
         }

         // Mémorisation des indirections possibles sous la forme %I id0\tid1\t...
         if( indirection != null ) aladin.glu.aladinDic.put(id, indirection.toString());
      }
      

      // Ajout dans la liste des noeuds d'arbre
      listReg.add(new TreeObjDir(aladin, id, prop));
   }
   
   
   /**
    * Ajustement des propriétés
    * @return true si on doit garder l'enregsitrement
    */
   private boolean propAdjust(String id, MyProperties prop) {
      
      // Insertion de la date de publication de l'article de référence
      String bib = prop.get("bib_reference");
      if( bib != null ) {
         try {
            int year = Integer.parseInt(bib.substring(0, 4));
            prop.replaceValue("bib_year", "" + year);
            
            // Ajout de l'entrée bib_reference_url (si elle n'existe pas)
            if( prop.get("bib_reference_url") == null ) {
               prop.replaceValue("bib_reference_url", "https://ui.adsabs.harvard.edu/?#abs/"+URLEncoder.encode(bib));
            }

         } catch( Exception e ) { }
      }

      // Traitement spécifique CDS
      propAdjust1(id, prop);
      
      String category = prop.getProperty(Constante.KEY_CLIENT_CATEGORY);
      if( category==null ) {
         category = DirectorySort.OTHERS;
         prop.setProperty(Constante.KEY_CLIENT_CATEGORY, category);
         
      // Suppression des données Planéto si on n'est pas en mode Planéto
      } else {
         if( category.startsWith("Solar sys") ) {
            if( !aladin.configuration.isPlanet() ) return false;
         }
      }
      
      
      boolean local = prop.getProperty("PROP_ORIGIN") != null;

      // Rangement dans la branche "Adds" si chargement local
      if( local && !category.startsWith(DirectorySort.ADDS+"/") ) {
         category = DirectorySort.ADDS+"/" + category;
         prop.replaceValue(Constante.KEY_CLIENT_CATEGORY, category);
      }
      
      // Génération de la clé de tri
      directorySort.setInternalSortKey(id,prop);
      
      return true;
   }
   
   // POURRA ETRE SUPPRIME POUR LA V12 - PF 31 janvier 2020
   private void propAdjust1(String id, MyProperties prop) {
      
      // Ne concerne pas VizieR => on ne bidouille pas dans le client
      if( !id.startsWith("CDS/") || id.equals("CDS/Simbad") ) return;      
      

      // Déjà fait en amont => on ne bidouille plus dans le client (en fait si, mais juste le minimum)
      if( prop.get("client_category")!=null ) return;
      
      String type = prop.getProperty("dataproduct_type");
      if( type == null || type.indexOf("catalog") < 0 ) {
         
         // Cas des enregistrements VizieR des tables temporelles non décrites.
         // POUR LE MOMENT ON LES GARDE AVEC UN ACCES TAP UNIQUEMENT
         String tmoc = prop.get("moc_type");
         if( tmoc!=null && tmoc.equals("tmoc") ) {
            prop.add("dataproduct_type","catalog");
            
         // Sinon on ne prend pas en compte
         } else return;
      }
      
      
//      // Gère les conversions des propriétés VizieR pour l'ancienne syntaxe Thomas B.
//      // Si on ne trouve pas le nom de la table en suffixe du title c'est que c'est déjà fait en amont
//      boolean oldVizieRMode=false;
//      String obslabel = prop.get("obs_label");
//      String title = prop.get("obs_title");
//      if( title!=null && obslabel!=null ) {
//         int i = title.lastIndexOf('(');
//         if( i>0 && title.length()>3 ) {
//            String table = title.substring(i+1,title.length()-1);
//            if( obslabel.equals(table) ) {
//               oldVizieRMode=true; 
//               if( infoVizieROld ) {
//                  infoVizieROld=false;
//                  if( aladin.levelTrace>=3 ) System.err.println("Directory.propAdjust1(...) => prop VizieR old syntax found and translated (ex: "+id+"...)");
//               }
//            }
//         }
//      }

      // Nettoyage des macros latex qui trainent
      if( cleanLatexMacro(prop) ) {
         Aladin.trace(4,"Directory.propAdjust1(...) => VizieR macro in ["+id+"]");
      }

      // Ajout de l'entrée TAP (si elle n'existe pas)
      if( prop.get("tap_service_url") == null ) {
         prop.replaceValue("tap_service_url", "http://tapvizier.u-strasbg.fr/TAPVizieR/tap/");
      }

      // Détermination de la catégorie
      prop.replaceValue("client_category", "Catalog/VizieR");
      
      // Réorganisation des champs en attendant que Thomas le fasse en amont
      //
      // exemple: 
      //
      // ID                    = CDS/I/337/gaia
      // obs_collection        = Gaia-DR1
      // obs_title             = Gaia DR1 (Gaia Collaboration, 2016) (gaia)
      // obs_description       = GaiaSource data
      // obs_collection_label  = Gaia-DR1
      //
      // S'il s'agit d'une table unique d'un catalogue (CDS/I/337/* == 1)
      //    1) je vire le nom de la table à la fin de obs_title si présent
      //    2) j'ajoute en préfixe l'alias principal si nécessaire (obs_collection_label ajouté à obs_title)
      //    3) je mémorise le nouveau titre (obs_title)
      //
      // En revanche, s'il s'agit d'une table d'un catalogue ayant plusieurs table (CDS/I/337/* > 1)
      //    1) Je vire le nom de la table à la fin de obs_title => (gaia)
      //    2) Je remplace la obs_collection par ce titre => Gaia DR1 (Gaia Collaboration, 2016)
      //    3) Je récupère le obs_description, ajoute en suffixe le nom de la table récupéré en 1 et l'enregistre
      //       comme nouveau obs_title => GaiaSource data (gaia)
      

      boolean hasMultiple = hasMultiple( getCatParent(id) );
      if( !hasMultiple ) {

         // Je vire le nom de la table a la fin du titre
         String titre = prop.get("obs_title");
         boolean modif=false;
//         if( oldVizieRMode ) {
            int i;
            if( titre != null && (i = titre.lastIndexOf('(')) > 0 ) {
               titre = titre.substring(0, i - 1);
               modif=true;
            }
//         }

         // J'ajoute le petit nom de la collection en suffixe si nécessaire
         String label = prop.getFirst("obs_collection_label");
         if( label!=null && titre!=null ) {
            titre = addLabelPrefix(label,titre);
            modif=true;
         }
         if( modif ) prop.replaceValue("obs_title", titre);

      } else /* if( oldVizieRMode ) */ {
         String titre = prop.get("obs_title");
         if( titre != null ) {

            // Je remonte le nom du catalog (obs_description sans la parenthèse finale) sur la branche (obs_collection)
            int i = titre.lastIndexOf('(');
            int j = titre.indexOf(')', i);
            if( i > 0 && j > i ) {

               String desc = prop.get("obs_description");
               if( desc != null ) {
                  String newTitre = desc + titre.substring(i - 1, j + 1);
                  prop.replaceValue("obs_title", newTitre);
               }
               prop.replaceValue("obs_collection",titre.substring(0, i-1));
               prop.replaceValue("obs_description",titre.substring(0, i-1));
            }
         }
      }

    }
   
   
   static private boolean isSep(char c) { return c==' ' || c=='-' || c=='_'; }
   
   // Ajoute le label en préfixe si les premiers mots sont différents
   // Supprime éventuellement l'article The en début de titre
   static protected String addLabelPrefix(String label, String title) {
      if( label==null ) return title;
      
      // Probablement déjà fait
      if( title.startsWith(label) ) return title;
      
      String title1 = title;
      if( title.startsWith("The ") ) title = title.substring(4);
     
      char [] a1 = label.toCharArray();
      char [] a2 = title.toCharArray();
      
      for( int i=0, j=-1; i<a1.length; i++ ) {
         if( isSep( a1[i] ) ) continue;
         while( ++j<a2.length && isSep( a2[j] ) );
         if( j==a2.length || Character.toUpperCase( a1[i] ) != Character.toUpperCase( a2[j] ) ) {
            return label+" - "+title1;
         }
      }
      return title;
   }
   
   static private final String[] TEXTKEYS = { "obs_title", "obs_description", "obs_label", "obs_collection" };

   /** Nettoyage des macros Latex oublié par VizieR */
   private boolean cleanLatexMacro(MyProperties prop) {
      boolean rep=false;
      for( String key : TEXTKEYS ) {
         String s = prop.get(key);
         if( s == null ) continue;
         if( s.indexOf('\\') < 0 ) continue;
         String s1 = cleanLatexMacro(s);
         if( !s.equals(s1) ) {
            prop.replaceValue(key, s1);
            rep = true;
         }
      }
      return rep;
   }

   static final private int AVANT = 0, MACRO = 1, IN = 2, NEXT = 3, MACRO1 = 4, NAMEMACRO = 5, INMACRO = 6;

   /**
    * Suppression des macros Latex présent dans la chaine ex: texte avant \macro{contenu{ou plus}} texte avant...
    */
   static public String cleanLatexMacro(String s) {
      StringBuilder s1 = new StringBuilder();
      StringBuilder macro = new StringBuilder();
      char[] a = s.toCharArray();
      int mode = 0;
      int level = 0;
      boolean keepIn = false;

      for( char c : a ) {
         switch( mode ) {
            case AVANT:
               if( c == '{' ) mode = MACRO1;
               else if( c == '\\' ) {
                  mode = MACRO;
                  macro.replace(0, macro.length(), "");
               } else s1.append(c);
               break;
            case MACRO:
               if( c == '\\' ) macro.replace(0, macro.length(), "");
               else if( c == '{' ) {
                  keepIn = macro.toString().equals("vFile") || macro.toString().equals("em") || macro.toString().equals("bf");
                  mode = IN;
               } else if( !Character.isJavaIdentifierPart(c) ) {
                  String s2 = resolveMacro(macro.toString());
                  if( s2.length() > 0 ) {
                     s1.append(s2);
                     s1.append(c);
                  } else {
                     int n = s1.length() - 1;
                     if( c == ')' && s1.charAt(n) == '(' ) s1.deleteCharAt(n);
                     else if( c == ']' && s1.charAt(n) == '[' ) s1.deleteCharAt(n);
                     else if( c == '}' && s1.charAt(n) == '{' ) s1.deleteCharAt(n);
                  }
                  mode = AVANT;
               } else macro.append(c);
               break;
            case IN:
               if( c == '}' && level == 0 ) mode = NEXT;
               else if( c == '{' ) level++;
               else if( c == '}' ) level--;
               else if( keepIn ) s1.append(c);
               break;
            case NEXT:
               if( c == '{' ) {
                  mode = IN;
                  keepIn = false;
               } else {
                  mode = AVANT;
                  int n = s1.length() - 1;
                  if( c == ')' && s1.charAt(n) == '(' ) s1.deleteCharAt(n);
                  else if( c == ']' && s1.charAt(n) == '[' ) s1.deleteCharAt(n);
                  else if( c == '}' && s1.charAt(n) == '{' ) s1.deleteCharAt(n);
                  else s1.append(c);
               }
               break;
            case MACRO1:
               if( c != '\\' ) {
                  s1.append('{');
                  s1.append(c);
                  mode = AVANT;
               } // Fausse alerte
               else mode = NAMEMACRO;
               break;
            case NAMEMACRO:
               if( c == '\\' ) break;
               if( !Character.isJavaIdentifierPart(c) ) mode = INMACRO;
               break;
            case INMACRO:
               if( c == '}' ) mode = AVANT;
               else s1.append(c);
               break;
         }
      }
      // System.out.println("\nAVANT: "+s+"\nAPRES: "+s1.toString());
      return s1.toString();
   }

   static final private String MACRO_LIST[] = { "deg", "originalcolumnnames" };

   static final private String MACRO_CONV[] = { "°", "" };

   static private String resolveMacro(String macro) {
      int i = Util.indexInArrayOf(macro, MACRO_LIST);
      if( i < 0 ) return macro;
      return MACRO_CONV[i];
   }

   // Mémorise les paths de l'arbre qui ont de multiples feuilles
   private HashSet<String> multiple = null;

   /** Retourne true si le path contient plusieurs feuilles */
   protected boolean hasMultiple(String path) {
      if( multiple == null ) {
         multiple = new HashSet<>(multiProp.size());
         HashSet<String> un = new HashSet<>(multiProp.size());
         for( MocItem mi : multiProp ) {
            String parent = getCatParent(mi.mocId);
            if( un.contains(parent) ) multiple.add(parent);
            else un.add(parent);
         }
      }
      return multiple.contains(path);
   }

   /**
    * Provides the properties java object describing the collection identified by id
    * @param id Identifier (ex: CDS/Simbad)
    * @return The properties (very similar to basic java Properties object)
    */
   protected MyProperties getProp(String id) {
      return multiProp.getProperties(id);
   }
   
   /** Retourne les properties associées à l'id (venant du stack) spécifié */
   protected MyProperties getProperties(String id) {
      if( id==null ) return null;
      int i = id.indexOf('~');
      if( i < 0 ) i = id.indexOf(' ');
      if( i > 0 ) id = id.substring(0, i);
      return getProp(id);
   }

   /**
    * Provides the list of pre-selected TAP servers Output syntax: ID url description...
    * @throws Exception
    */
   protected ArrayList<String> getPredefinedTAPServers() throws Exception {
      return getTAPServersByMocServer("client_tap_mainlist=*");
   }
   
   //returns multiPropId
   protected ArrayList<String> getPredefinedTAPServersMultiProp() throws Exception {
	      return getTAPServersMultiPropByMocServer("client_tap_mainlist=*");
   }

   /**
    * provides the list of Tap servers matching the keyword(s) - blank separated (AND logic, applied on ID, obs_title et
    * obs_collection Output syntax: ID url description...
    * @throws Exception
    */
   protected ArrayList<String> geTAPServers(String keyword) throws Exception {
      if( keyword == null || keyword.trim().length() == 0 ) return getPredefinedTAPServers();
      return getTAPServersByMocServer("(" + getKeyWordExpr(keyword) + ")");
   }

   // For tap server list, slight change
   protected ArrayList<String> getTAPServers(String keyword) throws Exception {
      if( keyword == null || keyword.trim().length() == 0 ) return getTAPServersByMocServer("(*)");
      return getTAPServersMultiPropByMocServer("(" + getKeyWordExpr(keyword) + ")");
   }

   /**
    * Provides the list of TAP server matching the MocServer query Output syntax: ID url description...
    * @param query
    * @return
    * @throws Exception
    */
   protected ArrayList<String> getTAPServersByMocServer(String query) throws Exception {
      ArrayList<String> b = new ArrayList<>();
      ArrayList<String> a = multiProp.scan((Moc) null, "tap_service_url*=* && " + query, false, -1, -1);
      for( String id : a ) {
         // String auth = Util.getSubpath(id, 0);
         MocItem mi = multiProp.getItem(id);
         String url = mi.prop.get("tap_service_url");
         String desc = mi.prop.get("obs_title");
         if( desc == null ) desc = mi.prop.get("obs_collection");
         String s = id + "  " + url + " " + desc;
         b.add(s);
      }
      return b;
   }
   
 //returns multiPropId
	protected ArrayList<String> getTAPServersMultiPropByMocServer(String query) throws Exception {
		ArrayList<String> a = multiProp.scan((Moc) null, "tap_service_url*=* && " + query, false, -1, -1);
		return a;
	}

   // protected ArrayList<String> getBigTAPServers(int limitNbCat) throws Exception {
   //
   // ArrayList<String> a = multiProp.scan( (SMoc)null, "tap_service_url*=*", false, -1, -1);
   //
   // Map<String, Integer> map = new HashMap<String, Integer>();
   // for( String id : a) {
   //
   // // Cas particulier à écarter pour ne pas poser souci avec les catalogues VizieR
   // if( id.startsWith("CDS/Simbad") ) continue;
   //
   // String auth = Util.getSubpath(id, 0);
   // int m;
   // Integer n = map.get(auth);
   // if( n==null ) m=0;
   // else m = n;
   // m++;
   // map.put(auth,m);
   // }
   //
   // // On trie
   // Map<String, Integer> map1 = DirectoryFilter.sortByValues(map, 1);
   //
   // // On prend uniquement les serveurs qui ont au moins limitNbCat collections
   // ArrayList<String> b = new ArrayList<String>();
   // for( String auth : map1.keySet() ) {
   //
   // Integer n = map.get(auth);
   // if( n<limitNbCat ) break;
   //
   // // On cherche la première entrée correspondante pour récupérer une URL TAP
   // for( String id : a ) {
   // String auth1 = Util.getSubpath(id, 0);
   // if( !auth1.equals(auth) ) continue;
   //
   // MocItem mi = multiProp.getItem(id);
   // String url = mi.prop.get("tap_service_url");
   //
   // // On mémorise pour le tableau résultat.
   // if( auth.equals("CDS") ) auth="cds.vizier";
   // String s = auth+" "+url+" "+n+" collections";
   // b.add(s);
   // break;
   // }
   // }
   //
   // return b;
   // }
	//

	/** Retourne le code de la catégorie des catalogues, null sinon (ex: CDS/I/246/out => I) */
	static protected String getCatCode(String id) {
	   return Util.getSubpath(id, 1);
	}

	/** Retourne le code de la catégorie des catalogues, null sinon (ex: CDS/I/246/out => 246) */
	static protected String getCatNumber(String id) {
	   return Util.getSubpath(id, 2);
	}

	/** retourne l'abbréviation du journal (ex: CDS/J/A+A/171/261/table1 => A+A) */
	static protected String getJournalCode(String id) {
	   return Util.getSubpath(id, 2);
	}

	/** retourne numéro du journal (ex: CDS/J/A+A/171/261/table1 => 171/261 ) */
	static protected String getJournalNum(String id) {
	   return Util.getSubpath(id, 3,2);
	}

	/**
	 * Retourne le suffixe de l'identificateur d'un catalogue => tous ce qui suit le code de catégorie (ex: CDS/I/246/out =>
	 * 246/out)
	 */
   static protected String getCatSuffix(String id) {
      return Util.getSubpath(id, 2, 2);
   }

   /**
    * Retourne le préfixe parent d'un identificateur de catalogue => tout ce qui précède le dernier mot (ex: CDS/I/246/out =>
    * CDS/I/246)
    */
   static protected String getCatParent(String id) {
      int i = id.lastIndexOf('/');
      if( i > 0 ) return id.substring(0, i);
      return null;
   }
   
   // Sauvegarde dans le cache du MultiMoc sous forme binaire
   private boolean cacheWrite() {
      // On ne sauvegarde pas dans le cache les essais en cours
      // sinon il y aura souci lorsqu'on reprendra la config normale
//      if( aladin.MOCLOCAL || aladin.MOCV2 ) return false;
      if( aladin.MOCLOCAL ) return false;
      
      try {
         String s = aladin.cache.getCacheDir() + Util.FS + MMOC;
         (new BinaryDump()).save(multiProp, s);
         aladin.trace(3, "Multiprop stored in cache [" + s + "]");
      } catch( Exception e ) {
         if( aladin.levelTrace >= 3 ) e.printStackTrace();
         return false;
      }
      return true;
   }

   // Génération du MultiMoc depuis la sauvegarde binaire du cache
   private boolean cacheRead() {
      
      if( aladin.MOCLOCAL ) return false;
      
      try {
         long t0 = System.currentTimeMillis();
         String s = aladin.cache.getCacheDir() + Util.FS + MMOC;
         MultiMoc multiProp = (new BinaryDump()).load(s);
         this.multiProp = new MultiMoc2(multiProp);
         aladin.trace(3, "Multiprop loaded (" + multiProp.size() + " rec.) from cache [" + s + "] in "
               + (System.currentTimeMillis() - t0) + "ms...");
      } catch( Exception e ) {
         if( e.getMessage()!=null ) aladin.trace(1,"Multiprop cache ignored: "+e.getMessage());
         return false;
      }
      return true;
   }

   /**
    * Retourne la date d'estampillage la plus tardive de l'ensemble des enregistrements du multiProp. 0 si aucun
    */
   private long getMultiPropTimeStamp() { return multiProp.getBiggestTimeStamp(); }

   // Retourne la date du cache
   private long getCacheTimeStamp() {
      String s = aladin.cache.getCacheDir() + Util.FS + MMOC;
      return (new File(s)).lastModified();
   }

   /** Retourne true si le HipsStore est prêt */
   protected boolean dialogOk() {
      return !init /* !mocServerLoading && multiProp.size()>0 */;
   }

   /** Chargement des descriptions de l'arbre par le MocServer */
   private void initMultiProp(final boolean flagReload) {

      // Tentative de rechargement depuis le cache
      if( cacheRead() ) {
         startTimer();
         while( !init )
            Util.pause(100);
            (new Thread("updateFromMocServer") {
               public void run() {
                  try {
                     quickFilter.setEditable(false);
                     quickFilter.setText(UPDATING);
                     if( updateFromMocServer(flagReload) > 0 ) {
                        cacheWrite();
                        final ArrayList<TreeObjDir> tmpListReg = populateMultiProp();
                        SwingUtilities.invokeLater(new Runnable() {
                           public void run() {
                              replaceTree(tmpListReg);
                              stopTimer();
                           }
                        });
                     } else stopTimer();
                  } finally {
                     quickFilter.setText("");
                     quickFilter.setEditable(true);
                  }
               }
            }).start();

            // Le cache est vide => il faut charger depuis le MocServer
      } else {


         // L'initialisation se fait en deux temps pour pouvoir laisser
         // l'utilisateur travailler plus rapidement
         String s = "client_application=AladinDesktop" + (!Aladin.PROTO ? "*" : "")
               + "&hips_service_url=*&fields=!obs_description,!hipsgen*&fmt=gzip-ascii&get=record";
         loadFromMocServer(s);

         startTimer();
         while( !init ) Util.pause(100);
         (new Thread("updateFromMocServer") {
            public void run() {
               if( loadFromMocServer(MOCSERVER_INIT) > 0 ) {
                  cacheWrite();
                  final ArrayList<TreeObjDir> tmpListReg = populateMultiProp();
                  SwingUtilities.invokeLater(new Runnable() {
                     public void run() {
                        replaceTree(tmpListReg);
                        stopTimer();
                     }
                  });
               } else stopTimer();
            }
         }).start();
      }

      this.dirList = populateMultiProp();
   }
   
   /** Ajout de nouvelles collections */
   protected boolean addHipsProp(InputStreamReader in, boolean addition, String path) {
      try {
         // System.out.println("addHipsProp => "+path);
         loadMultiProp(in, addition, path);
         ArrayList<TreeObjDir> tmpDirList = populateMultiProp();
         rebuildTree(tmpDirList, false, true, true);
         dirList = tmpDirList;
      } catch( Exception e ) {
         if( aladin.levelTrace > 3 ) e.printStackTrace();
         return false;
      }
      return true;
   }
   
   /** Positionne le numéro de version du MocServer (ligne 1), ainsi que le plus grand TIMESTAMP utilisé (ligne 2)
    * >=5.0 indique que le MocServer prend en compte la dimension temporelle
    */
   private void setMocserverVersion() {
      if( !Aladin.NETWORK ) return;
      if( mocServerVersion==-2 ) return;    // déjà testé sans succès
      
      DataInputStream dis=null;
      try {
         String url =aladin.glu.getURL(GLUMOCSERVER)+"?help=version";
         dis = new DataInputStream( Util.openAnyStream(url));
         double v = Double.parseDouble( dis.readLine().trim() );
         aladin.trace(3,"MocServer version="+v);
         mocServerVersion=v;
         
         try {
            mocServerBiggestTimeStamp = Long.parseLong( dis.readLine().trim() );
         } catch( Exception e1 ) {}
      } 
      catch( Exception e ) { mocServerVersion=-2; }
      finally {
         try { dis.close(); } catch( Exception e ) {}
      }
   }

   /**
    * Demande de maj des enr. via le MocServer en passant en POST la liste des IDs des collections que l'on connait déjà + une
    * date d'estampillage pour chacune d'elles. Si ça ne fonctionne pas, on tentera une maj plus basique
    * @return le nombre de records chargés
    */
   private int updateFromMocServer(boolean flagReload) {
      if( !Aladin.NETWORK ) return 0;
      long t0 = System.currentTimeMillis();
      URL url;
      try {
         url = aladin.glu.getURL(GLUMOCSERVER);
      } catch( Exception e ) {
         if( aladin.levelTrace >= 3 ) e.printStackTrace();
         return -1;
      }
      
      // Test et mémorisation de la version du MocServer
      setMocserverVersion();
      
      // Si plus grand TIMESTAMP connu du MocServer, comparaison avec le plus grand TIMESTAMP local
      if( mocServerBiggestTimeStamp>0L ) {
         if( getMultiPropTimeStamp()==mocServerBiggestTimeStamp ) {
            aladin.trace(3, "MocServer update not required => local cache up to date (last update: "
               +Astrodate.JDToDate( Astrodate.UnixToJD(mocServerBiggestTimeStamp/1000L))+")");
            return 0;
         }
      }

      try {
         mocServerUpdating = true;
         MultiPartPostOutputStream.setTmpDir(Aladin.CACHEDIR);
         String boundary = MultiPartPostOutputStream.createBoundary();
         URLConnection urlConn = MultiPartPostOutputStream.createConnection(url);
         urlConn.setRequestProperty("Accept", "*/*");
         urlConn.setRequestProperty("Content-Type", MultiPartPostOutputStream.getContentType(boundary));
         // set some other request headers...
         urlConn.setRequestProperty("Connection", "Keep-Alive");
         urlConn.setRequestProperty("Cache-Control", "no-cache");
         MultiPartPostOutputStream out = new MultiPartPostOutputStream(urlConn.getOutputStream(), boundary);

         File tmpMoc = File.createTempFile("moc", ".txt");
         tmpMoc.deleteOnExit();

         BufferedWriter fo = new BufferedWriter(new FileWriter(tmpMoc));
         try {
            int i=0;
            for( MocItem mi : this ) {
               String s = mi.mocId + "=" + (flagReload ? "0":mi.getPropTimeStamp()) + "\n";
               fo.write(s.toCharArray());
            }
         } finally {
            try {
               fo.close();
            } catch( Exception e ) {
            }
         }

         out.writeField("fmt", "asciic");
//       out.writeField("get", "record-v2");
         out.writeField("get", "record");
         out.writeFile("maj", null, tmpMoc, true);
         out.close();
         aladin.trace(4, "ID list sent");

         int n = loadMultiProp(new InputStreamReader(urlConn.getInputStream()), false, null,true);
         Aladin.trace(3,
               "Multiprop updated in " + (System.currentTimeMillis() - t0) + "ms => " + n + " record" + (n > 1 ? "s" : ""));
         return n;

      } catch( Exception e ) {
         if( aladin.levelTrace >= 3 ) e.printStackTrace();
         System.err.println("An error occured while updating the MocServer service => try the basic mode...");
         return updateFromMocServerBasic();
      } finally {
         mocServerUpdating = false;
      }
   }

   // /** Gunzippe le flux si nécessaire */
   // private InputStream gunzipIfRequired(InputStream in) throws Exception {
   // PushbackInputStream pb = new PushbackInputStream( in, 2 );
   // byte[] signature = new byte[2];
   // pb.read( signature );
   // pb.unread( signature );
   // int head = ( signature[0] & 0xff ) | ( ( signature[1] << 8 ) & 0xff00 );
   // boolean isGziped = GZIPInputStream.GZIP_MAGIC == head;
   // if( !isGziped ) return pb;
   // return new GZIPInputStream(pb);
   // }

   /**
    * Demande de maj des enr. via le MocServer en fonction d'une seule date spécifique. La date d'estampillage de référence sera
    * la plus tardive de l'ensemble des enr. déjà copiés. Et si aucun, ce sera la date du fichier de cache (avec un risque de
    * décalage des horloges entre le MocServer et la machine locale. Cette méthode de maj ne permet pas la suppression des rec
    * obsolètes, ni le complément de maj si la précédente maj a été interrompue en cours.
    * @return le nombre de records chargés
    */
   private int updateFromMocServerBasic() {
      long ts = getMultiPropTimeStamp();
      if( ts == 0L ) ts = getCacheTimeStamp();
      return loadFromMocServer("TIMESTAMP=>" + ts + "&fmt=asciic&get=record");
   }

   private int loadFromMocServer(String params) {
      if( !Aladin.NETWORK ) return -1;

      InputStreamReader in = null;
      boolean eof = false;
      int n = 0;

      String text = params.indexOf("TIMESTAMP") >= 0 ? "updat" : "load";

      // Recherche sur le site principal, et sinon sur un site miroir
      try {
         mocServerLoading = true;

         long t0 = System.currentTimeMillis();
         Aladin.trace(3, text + "ing Multiprop definitions from MocServer...");

         String u = aladin.glu.getURL(GLUMOCSERVER, params, true).toString();
         setMocserverVersion();
         try {
            in = new InputStreamReader(Util.openStream(u, false, true, 3000));
            if( in == null ) throw new Exception("cache openStream error");

            // Peut être un site esclave actif ?
         } catch( EOFException e1 ) {
            eof = true;
         } catch( Exception e ) {
            if( !aladin.glu.checkIndirection(GLUMOCSERVER, null) ) throw e;
            u = aladin.glu.getURL(GLUMOCSERVER, params, true).toString();
            try {
               in = new InputStreamReader(Util.openStream(u, false, true, -1));
            } catch( EOFException e1 ) {
               eof = true;
            }
         }

         if( !eof ) n = loadMultiProp(in, false, null);
         Aladin.trace(3, "Multiprop " + text + "ed in " + (System.currentTimeMillis() - t0) + "ms => " + n + " record"
               + (n > 1 ? "s" : ""));

      } catch( Exception e1 ) {
         if( Aladin.levelTrace >= 3 ) e1.printStackTrace();
         return -1;

      } finally {
         mocServerLoading = false;
         if( in != null ) { try { in.close(); } catch( Exception e ) { }
         }
      }
      return n;
   }
   
   private String getId(String ivoid) {
      if( ivoid.startsWith("ivo://") ) return ivoid.substring(6);
      return ivoid;
   }

   /** Retourne le nombre max du nombre de lignes des catalogues et tables */
   protected int getNbRowMax() {
      long max = 0L;
      for( MocItem mi : this ) {
         try {
            long n = Long.parseLong(mi.prop.get("nb_rows"));
            if( n > max ) max = n;
         } catch( Exception e ) {
         }
      }
      return (int) (max);
   }

   /** Retourne le nombre d'items matchant la contrainte */
   protected int getNumber(String contrainte) {
      try {
         return multiProp.scan(contrainte).size();
      } catch( Exception e ) {
      }
      return -1;
   }

   /** true si on peut raisonnablement faire un updating des HiPS visibles dans la vue */
   private boolean isReadyForUpdating() {
      return !mocServerLoading && isVisible() && getWidth() > 0;
   }

   protected boolean blinkState = true;

   /** Réaffichage du Panel tous les demi-secondes afin de faire clignoter le message d'attente */
   private void startTimer() {
      if( timer == null ) {
         timer = new Timer(500, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
               blinkState = !blinkState;
               repaint();
            }
         });
      }
      if( !timer.isRunning() ) {
         // System.out.println("Timer started");
         timer.start();
      }
   }

   private void stopTimer() {
      if( timer != null && timer.isRunning() ) {
         // System.out.println("Timer stopped");
         timer.stop();
      }
   }

   /************** Pour faire la maj en continue des HiPS visibles dans la vue *******************************/

   transient private Thread threadUpdater = null;

   private boolean encore = true;

   private void startInsideUpdater() {
      if( threadUpdater == null ) {
         threadUpdater = new Updater("RegUpdater");
         threadUpdater.start();
      } else encore = true;
   }

   private void stopInsideUpdater() {
      encore = false;
   }

   class Updater extends Thread {
      public Updater(String s) {
         super(s);
      }

      public void run() {
         encore = true;
         // System.out.println("Registry Tree updater running");
         while( encore ) {
            try {
//                System.out.println("Hips updater checking...");
               if( isReadyForUpdating() ) resumeIn();
               Thread.currentThread().sleep(1000);
            } catch( Exception e ) {
            }
         }
         // System.out.println("Registry Tree updater stopped");
         threadUpdater = null;
      }
   }

   /****************** Gestion des paramètres des différents modes d'interrogation *************************/

   private HashMap<String, String> params = null;

   private void initParams() {
      TreeObjDir.loadString(aladin.chaine);
      params = TreeObjDir.paramsFactory();
      directorySort = new DirectorySort(aladin);
   }

   protected void setParam(String key, String val) {
      params.put(key, val);
   }

   protected String getParam(String key) {
      return params.get(key);
   }

   /** Remet à jour les différents widgets du FrameInfo (s'il est visible) */
   protected void resumeFrameInfo() {
      if( frameInfo == null || !frameInfo.isVisible() ) return;
      frameInfo.resumePanel();
      frameInfo.updateWidget();
   }
   
   /********* Classe gérant une fenêtre d'information associée au HiPS cliqué dans l'arbre des HiPS ***************/

   private class FrameInfo extends JFrame implements ActionListener {

      ArrayList<TreeObjDir> treeObjs = null; // hips dont il faut afficher les informations

      JPanel panelInfo = null; // le panel qui contient les infos (sera remplacé à chaque nouveau hips)

      JCheckBox hipsBx = null, /* tmocBx=null,*/  mocBx = null, mociBx = null, mocuBx, progBx = null, dmBx = null, siaBx = null, ssaBx = null,
            customBx = null, csBx = null, msBx = null, allBx = null, tapBx = null, xmatchBx = null, globalBx = null, liveBx = null,
            assocBx=null;
      ConeField target = null;
      JPanel targetPanel = null;
      JButton load = null, grab = null;

      FrameInfo(boolean decorated) {
         
         Aladin.setIcon(this);
         setTitle(AWFRAMEINFOTITLE);
         enableEvents(AWTEvent.WINDOW_EVENT_MASK);
         Util.setCloseShortcut(this, false, aladin);
         
         JPanel contentPane = (JPanel) getContentPane();
         contentPane.setLayout(new BorderLayout(5, 5));
         contentPane.setBackground(new Color(240, 240, 250));
         setBackground( contentPane.getBackground() );
         contentPane.setBorder(BorderFactory.createLineBorder(Color.black));
         setUndecorated(!decorated);
         pack();
      }

      public void setVisible(boolean flag) {

         // Si on efface la fenêtre popup, on resete aussi la mémorisation
         // de la liste des TreeObj concerné par le dernier affichage
         if( !flag ) {
            treeObjs = null;
            setTarget(null);
         } else updateWidget();

         super.setVisible(flag);
      }

      @Override
      public void actionPerformed(ActionEvent e) {
         updateWidget();
      }
      
      String sTarget=null;
      String sRadius=null;
      
      protected void setTarget(String s) { sTarget = s; }
      protected void setRadius(String s) { sRadius = s; }

      private void updateWidget() {
         if( load == null ) return;
         boolean activateLoad = false;
         boolean activateTarget = false;
         if( hipsBx != null )   activateLoad |= hipsBx.isSelected();
         if( mocBx != null )    activateLoad |= mocBx.isSelected();
//         if( tmocBx != null )   activateLoad |= tmocBx.isSelected();
         if( mociBx != null )   activateLoad |= mociBx.isSelected();
         if( mocuBx != null )   activateLoad |= mocuBx.isSelected();
         if( progBx != null )   activateLoad |= progBx.isSelected();
         if( dmBx != null )     activateLoad |= dmBx.isSelected();
         if( siaBx != null )  { activateLoad |= siaBx.isSelected();  activateTarget |= siaBx.isSelected(); }
         if( ssaBx != null )  { activateLoad |= ssaBx.isSelected();  activateTarget |= ssaBx.isSelected(); }
         if( csBx != null )   { activateLoad |= csBx.isSelected();   activateTarget |= csBx.isSelected(); }
         if( liveBx != null ) { activateLoad |= liveBx.isSelected(); activateTarget |= liveBx.isSelected(); }
         if( assocBx != null ){ activateLoad |= assocBx.isSelected();activateTarget |= assocBx.isSelected(); }
         if( customBx != null ) activateLoad |= customBx.isSelected();
         if( msBx != null )     activateLoad |= msBx.isSelected();
         if( allBx != null )    activateLoad |= allBx.isSelected();
         if( tapBx != null )    activateLoad |= tapBx.isSelected();
         if( xmatchBx != null ) activateLoad |= xmatchBx.isSelected();
         if( globalBx != null ) activateLoad |= globalBx.isSelected();
         load.setEnabled(activateLoad);
         
         if( activateTarget ) {
            if( sTarget!=null && sTarget.trim().length()==0  ) sTarget=null;
            String s = sTarget==null?"":sTarget;
            if( s.length()>0 && sRadius!=null && sRadius.trim().length()>0 ) s=s+"   "+sRadius;
            target.setText(s);
            int n = targetPanel.getComponentCount();
            if( n<2 ) {
               targetPanel.removeAll();
               targetPanel.add( target );
               if( Projection.isOk( aladin.view.getCurrentView().getProj()) ) targetPanel.add( grab );
               validate();
            }
            String info = getDefaultCone();
            target.setInfo(info==null ? "Object or coordinates ?":info);
         } else {
            if( targetPanel!=null && targetPanel.getComponentCount()>0 ) {
               targetPanel.removeAll();
               validate();
            }
         }
         
         // Changement de nom et de tooltip sur les checkbox "in view"
         if( target!=null && targetPanel!=null ) {
            boolean inView = (targetPanel.getComponentCount()==0 || target.getText().trim().length()==0) && getDefaultCone()!=null;
            for( JCheckBox cb : new JCheckBox[]{ csBx, siaBx, ssaBx }) {
               if( cb==null ) continue;
               cb.setText( inView  ? AWCSLAB : AWCONE );
               Util.toolTip(cb, inView ? AWCSTIP : AWCONETIP);
            }
         }
      }
      
      protected String getDefaultCone() {
         Coord coo;
         if( aladin.view.isFree() || !Projection.isOk( aladin.view.getCurrentView().getProj()) ) {
            return null;
         }
         String target;
         try {
            coo = aladin.view.getCurrentView().getCooCentre();
            target = aladin.localisation.getFrameCoord( coo.getDeg() );
         } catch( Exception e ) { return null; }
         
         double radius = aladin.view.getCurrentView().getTaille();
         return target+"   "+Coord.getUnit( radius );
      }
      
      boolean isSame(ArrayList<TreeObjDir> a1, ArrayList<TreeObjDir> a2) {
         if( a1 == null && a2 == null ) return true;
         if( a1 == null || a2 == null ) return false;
         if( a1.size() != a2.size() ) return false;
         for( TreeObjDir ti : a1 ) {
            if( !a2.contains(ti) ) return false;
         }
         return true;
      }

      /** Positionne les collections concernées, et regénère le panel en fonction */
      boolean setCollections(ArrayList<TreeObjDir> tos) {
         if( isSame(tos, treeObjs) ) {
            toFront();
            return false;
         }
         this.treeObjs = tos;
         resumePanel();
         validate();
         SwingUtilities.invokeLater(new Runnable() {
            public void run() {
               pack();
               toFront();
               repaint();
            }
         });
         return true;
      }

      // Le Panel de la fenêtre info qui gère localement une croix de fermeture
      class MyInfoPanel extends JPanel implements MouseListener, MouseMotionListener {
         static private final int W = 6;

         Rectangle cross = null;

         boolean inCross;

         MyInfoPanel( Color background ) {
            super();
            setBackground( background );
            setLayout(new BorderLayout());
            setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 7));
            addMouseListener(this);
            addMouseMotionListener(this);
         }
         
         public void paint(Graphics g) {
            super.paint(g);
            if( !decorated ) drawCross(g, getWidth() - W - 6, 4);
         }

         private void drawCross(Graphics g, int x, int y) {
            g.setColor(inCross ? Color.red : Color.gray);
            g.drawLine(x, y, x + W, y + W);
            g.drawLine(x + 1, y, x + W + 1, y + W);
            g.drawLine(x + 2, y, x + W + 2, y + W);
            g.drawLine(x + W, y, x, y + W);
            g.drawLine(x + W + 1, y, x + 1, y + W);
            g.drawLine(x + W + 2, y, x + 2, y + W);
            cross = new Rectangle(x, y - 2, W + 2, W + 2);
         }


         public void mousePressed(MouseEvent e) {
            if( inCross ) hideInfo();
         }

         public void mouseMoved(MouseEvent e) {
            if( cross!=null ) {
               boolean in1 = cross.contains(e.getPoint());
               if( in1 != inCross ) repaint();
               inCross = in1;
            }
         }
         
         public void mouseClicked(MouseEvent e) { }
         public void mouseReleased(MouseEvent e) { }
         public void mouseEntered(MouseEvent e) { }
         public void mouseExited(MouseEvent e) { }
         public void mouseDragged(MouseEvent e) { }
      }
      
      private void epingle() { reshowInfo( !decorated ); }

      /** Reconstruit le panel des informations en fonction des collections courantes */
      void resumePanel() {
         JPanel contentPane = (JPanel) getContentPane();
         if( panelInfo != null ) contentPane.remove(panelInfo);

         panelInfo = new MyInfoPanel( contentPane.getBackground() );

         String s;
         boolean flagScan = false;
         boolean flagSort = false;
         long nbRows = -1;
         MyAnchor a;
         GridBagConstraints c = new GridBagConstraints();
         GridBagLayout g = new GridBagLayout();
         c.fill = GridBagConstraints.BOTH;
         c.insets = new Insets(0, 1, 0, 5);
         JPanel p = new JPanel(g);

         TreeObjDir to = null;
         boolean hasView = !aladin.view.isFree();
//         boolean hasSmallView = hasView && aladin.view.getCurrentView().getTaille() < 30;
         boolean hasMocPol = aladin.view.hasMocPolSelected();
         boolean hasRegion = aladin.calque.getNbPlanMoc() > 0 || hasMocPol;
         boolean hasLoadedCat = aladin.calque.getNbPlanCat() > 0;
         
         TreePath tp = dirTree.getSelectionPath();
         if( tp!=null ) {
            String branch = ((TreeObj)((DefaultMutableTreeNode) tp.getLastPathComponent()).getUserObject()).path;
            flagSort = directorySort.hasBranchesRules(branch);
         }

         NoneSelectedButtonGroup bg = new NoneSelectedButtonGroup();

         // En cas de sélection multiples de collections
         if( treeObjs.size() > 1 ) {
            StringBuilder list = null;
            String sList = null, more = null;
            boolean hasCS = false;
            boolean hasSIA = false;
            boolean hasSSA = false;
            boolean hasCDScat = false;
            boolean hasMoc = false;
            boolean hasHips = false;
            boolean hasGlobalAccess = false;
            boolean hasVizieRTimeOnly =false;
            boolean hasVizieRAssocData = false;
            int nbIn = 0;
            int nbInMayBe = 0;
            int nbInHips = 0;

            for( TreeObjDir to1 : treeObjs ) {
               if( list == null ) list = new StringBuilder(to1.internalId);
               else list.append(", " + to1.internalId);
               if( sList == null && list.length() > 80 ) sList = list + "...";
               if( !hasVizieRTimeOnly && to1.isVizierTimeOnly() ) hasVizieRTimeOnly=true;
               if( !hasCS && to1.hasCS() ) hasCS = true;
               if( !hasSIA && to1.hasSIA() ) hasSIA = true;
               if( !hasSSA && to1.hasSSA() ) hasSSA = true;
               if( !hasMoc && to1.hasMoc() ) hasMoc = true;
               if( !hasHips && to1.hasHips() ) hasHips = true;
               if( !hasGlobalAccess && to1.hasGlobalAccess() ) hasGlobalAccess = true;
               if( !hasCDScat && to1.isCDSCatalog() ) hasCDScat = true;
               if( !flagScan && !to1.hasMoc() ) flagScan = true; 
               int inFlag = to1.getIsIn();
               if( inFlag == 1 ) nbIn++;
               else if( inFlag == -1 ) nbInMayBe++;
               if( inFlag != 0 && to1.hasHips() ) nbInHips++;
            }

            a = new MyAnchor(aladin, MULTICOL + ": " + treeObjs.size(), 50, null, null);
            a.setFont(a.getFont().deriveFont(Font.BOLD));
            a.setFont(a.getFont().deriveFont(a.getFont().getSize2D() + 1));
            a.setForeground(Aladin.COLOR_GREEN);
            PropPanel.addCouple(p, null, a, g, c);

            if( sList != null ) more = list.toString();
            else sList = list.toString();

            JPanel mocAndMore = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
            JCheckBox bx;
            mociBx = mocBx = csBx = liveBx = customBx = null;
//            tmocBx = null;

            // S'il n'y a pas trop de collections, on pourra les appeler en parallèle
            boolean flagTooMany = false;
            if( treeObjs.size() > MAX_PARALLEL_QUERY ) flagTooMany = true;
            else if( !hasVizieRTimeOnly )  {

               // On utilise le checkbox CS pour cumuler à la fois les accès CS,SIA et SSA
               if( hasCS || hasSIA || hasSSA || hasCDScat || hasGlobalAccess ) {
                  csBx = bx = new JCheckBox(AWCSLAB);
                  bx.addActionListener(this);
                  mocAndMore.add(bx);
                  String info = nbIn + nbInMayBe == 0 ? "(no data in the view)"
                        : "(" + (nbIn + nbInMayBe) + " collections " + (nbInMayBe > 0 ? " may " : "should")
                              + " have data in the view)";
                  Util.toolTip(bx, Util.fold(AWMCSTIP + "\n" + info, 100, true));
//                  bx.setEnabled(hasSmallView && Projection.isOk(aladin.view.getCurrentView().getProj()) && nbIn + nbInMayBe > 0);
//                  bx.setSelected(hasSmallView && Projection.isOk(aladin.view.getCurrentView().getProj()) && bx.isEnabled());
                  bx.setEnabled(true);
                  bx.setSelected(true);
                  bg.add(bx);
               }

               if( hasHips ) {
                  hipsBx = bx = new JCheckBox(AWPROGACC);
                  bx.addActionListener(this);
                  mocAndMore.add(bx);
                  if( csBx == null || !csBx.isSelected() ) bx.setSelected(true);
                  String info = "(" + nbInHips + " coll. have data in the field)";
                  Util.toolTip(bx, Util.fold(AWPROGACCTIP + "\n" + info, 100, true));
               }

               if( hasCDScat && hasRegion ) {
                  msBx = bx = new JCheckBox(AWMOCQLAB);
                  bx.addActionListener(this);
                  mocAndMore.add(bx);
                  Util.toolTip(bx, AWMOCQLABTIP);
                  bx.setEnabled(hasRegion);
                  bg.add(bx);

               }

               if( (csBx != null || hipsBx != null || msBx != null || customBx != null ) && (hasMoc) ) {
                  JLabel labelPlus = new JLabel(" + ");
                  mocAndMore.add(labelPlus);
               }
            }

            if( hasMoc ) {
               JLabel labelMoc = new JLabel(AWMOCTITLE);
               labelMoc.setFont(labelMoc.getFont().deriveFont(Font.ITALIC));
               labelMoc.setForeground(Color.gray);
               mocAndMore.add(labelMoc);

               if( !flagTooMany ) {
                  mocBx = bx = new JCheckBox(AWMOC1);
                  bx.addActionListener(this);
                  mocAndMore.add(bx);
                  Util.toolTip(bx, AWMOC1TIP);
                  
//                  tmocBx = bx = new JCheckBox(AWTMOC1);
//                  bx.addActionListener(this);
//                  mocAndMore.add(bx);
//                  Util.toolTip(bx, AWTMOC1TIP);
               }

               mocuBx = bx = new JCheckBox(AWMOC2);
               bx.addActionListener(this);
               mocAndMore.add(bx);
               Util.toolTip(bx, AWMOC2TIP);

               mociBx = bx = new JCheckBox(AWMOC3);
               bx.addActionListener(this);
               mocAndMore.add(bx);
               Util.toolTip(bx, AWMOC3TIP);
            }
            
            c.insets.top = 1;
            a = new MyAnchor(aladin, sList, 100, more, null);
            a.setForeground(Aladin.COLOR_BLUE);
            PropPanel.addCouple(p, null, a, g, c);

            if( mocAndMore.getComponentCount() > 0 ) {

               JLabel label1 = new JLabel("Access mode");
               String tip = "Access protocols available for this collection. According to your current "
                     + "view and/or the selected stack planes, some access protocols may or may " + "not be activated.";
               Util.toolTip(label1, Util.fold(tip, 100, true));
               label1.setForeground(Color.gray.brighter());
               label1.setFont(label1.getFont().deriveFont(Font.ITALIC));

               GridBagConstraints c1 = new GridBagConstraints();
               GridBagLayout g1 = new GridBagLayout();
               c1.fill = GridBagConstraints.BOTH; // J'agrandirai les composantes
               c1.insets = new Insets(0, 0, 0, 0);
               JPanel loadPanel = new JPanel(g1);

               PropPanel.addFilet(p, g, c, 5, 2);
               PropPanel.addCouple(null, loadPanel, flagTooMany ? null : label1, null, mocAndMore, g1, c1,
                     GridBagConstraints.CENTER);
               PropPanel.addCouple(p, "", loadPanel, g, c);

            }

         // Une seule collection
         } else {
            to = treeObjs.get(0);
            String titre = to.getVizieRLongTitre();

            if( to.verboseDescr != null || titre != null ) {
               s = titre+"\n \n"+ (to.verboseDescr == null ? "" : to.verboseDescr);
               String s1 = titre != null && titre.length() > 60 ? (titre.substring(0, 58) + "...") : titre;
               a = new MyAnchor(aladin, s1, 50, s, null);
               a.setFont(a.getFont().deriveFont(Font.BOLD));
               a.setFont(a.getFont().deriveFont(a.getFont().getSize2D() + 1));
               a.setForeground(Aladin.COLOR_GREEN);
               PropPanel.addCouple(p, null, a, g, c);
            }

            String provenance = to.getProperty("prov_progenitor");
            String copyright = to.copyright == null ? to.copyrightUrl : to.copyright;
            if( provenance != null || copyright != null ) {
               String url=null;
               s = null;
               
               if( provenance!=null && provenance.startsWith("http") ) { url = provenance; provenance=null; }
               else url=to.copyrightUrl;
               if( url==null ) url = to.getProperty("prov_progenitor_url");
               
               s = provenance;
               if( provenance!=null ) s=provenance;
               if( s==null && copyright!=null ) s=copyright;
               if( s!=null && s.startsWith("http") ) {
                  if( url==null ) url=s;
                  s=null;
               }
               if( s==null ) s=Util.getSubpath(to.id, 0);
               s = "Provenance: "+s+" ";
               
               a = new MyAnchor(aladin, s, 50, url, null);
               a.setForeground(Color.gray);
               PropPanel.addCouple(p, null, a, g, c);
            }

            JPanel p1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 0,0));
            if( to.hasMoc() ) {
               boolean isIn = to.getIsIn() == 1;
               s = to.getSpaceCoverage();
               if( s!=null ) {
                  s = AWSKYCOV + " " + s + " ";
                  a = new MyAnchor(aladin, s, 50, null, null);
                  a.setForeground(Color.gray);
                  p1.add(a);
                  if( hasView ) {
                     a.setToolTipText(isIn ? "Data available in the current view" : "No data in the current view");
                  }
               }

            } else {
               s = AWMOCUNK;
               a = new MyAnchor(aladin, s, 50, null, null);
               a.setForeground(Aladin.ORANGE);
               p1.add(a);

            }
            s = to.getEnergy();
            if( s != null ) {
               s = "    "  + s + " ";
               a = new MyAnchor(aladin, s, 50, null, null);
               a.setForeground(Color.gray);
               p1.add(a);
            }
            
            s = to.getPeriod();
            if( s!=null ) {
               s = "    "  + s + " ";
               a = new MyAnchor(aladin, s, 50, null, null);
               a.setForeground(Color.gray);
               p1.add(a);

            } else {
               s = to.getProperty("bib_year");
               if( s != null ) {
                  String url = to.getProperty("bib_reference_url");
                  
                  // Pas de lien, juste l'année de publi
                  if( url==null ) {
                     a = new MyAnchor(aladin, "    " + AWREFPUB + " " + s + " ", 50, null, null);
                     a.setForeground(Color.gray);
                     p1.add(a);
                     
                  // ON met une URL sur l'année de publi
                  } else {
                     a = new MyAnchor(aladin, "    " + AWREFPUB + " ", 50, null, null);
                     a.setForeground(Color.gray);
                     p1.add(a);
                     a = new MyAnchor(aladin, s + " ", 50, null, url);
                     p1.add(a);
                  }
               }
            }

          s = to.getProperty(Constante.KEY_HIPS_PIXEL_SCALE);
          if( s != null ) {
             try {
                s = Coord.getUnit(Double.parseDouble(s));
             } catch( Exception e ) {
             }
             s = "    " + AWHIPSRES + " " + s + " ";
             a = new MyAnchor(aladin, s, 50, null, null);
             a.setForeground(Color.gray);
             p1.add(a);
          }
          s = to.getProperty(Constante.KEY_NB_ROWS);
          if( s != null ) {
             try {
                nbRows = Long.parseLong(s);
             } catch( Exception e ) {
             }
             s = String.format("%,d", nbRows);
             s = "    " + AWNBROWS + " " + s + " ";
             a = new MyAnchor(aladin, s, 50, null, null);
             a.setForeground(Color.gray);
             p1.add(a);
          }
          
            if( p1.getComponentCount() > 0 ) PropPanel.addCouple(p, null, p1, g, c);

            JPanel accessPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 3, 0));
            JCheckBox bx;
            customBx = hipsBx = mocBx = mociBx = progBx = dmBx = csBx = liveBx = null;
//            tmocBx = null;
            assocBx = siaBx = ssaBx = allBx = globalBx = tapBx = xmatchBx = msBx = null;
            if( to.hasHips() ) {
               hipsBx = bx = new JCheckBox(AWPROGACC);
               bx.addActionListener(this);
               accessPanel.add(bx);
               bg.add(bx);
               bx.setSelected(true);
               Util.toolTip(bx, AWPROGACCTIP);
            }

            if( to.hasGlobalAccess() ) {
               globalBx = bx = new JCheckBox(AWDATAACC);
               bx.addActionListener(this);
               accessPanel.add(bx);
               bx.setSelected(true);
               bg.add(bx);
               Util.toolTip(bx, AWDATAACCTIP);
            }

            if( to.hasSIA() ) {
               siaBx = bx = new JCheckBox(AWCSLAB);
               bx.addActionListener(this);
               accessPanel.add(bx);
               bg.add(bx);
               bx.setSelected( !to.hasCustom() );
               String proto = to.hasSIAv2() ? "SIAv2" : "SIA";
               String s1 = AWSIATIP;
               s1 = s1.replace("SIA", proto);
               Util.toolTip(bx, s1, true);
            }

            if( to.hasSSA() ) {
               ssaBx = bx = new JCheckBox(AWCSLAB);
               bx.addActionListener(this);
               bg.add(bx);
               accessPanel.add(bx);
               bx.setSelected( to.hasCustom() );
               Util.toolTip(bx, AWSSATIP, true);
            }

            if( to.isCDSCatalog() && !to.isVizierTimeOnly() ) {
               boolean allCat = nbRows < 2000;
               if( hipsBx != null ) bg.add(hipsBx);

               if( nbRows != -1 && nbRows < 100000 ) {
                  allBx = bx = new JCheckBox(AWDATAACC);
                  bx.addActionListener(this);
                  accessPanel.add(bx);
                  bg.add(bx);
                  bx.setSelected(!to.hasHips() && allCat);
                  Util.toolTip(bx, AWDATAACCTIP1);
               }

               csBx = bx = new JCheckBox(AWCSLAB);
               bx.addActionListener(this);
               accessPanel.add(bx);
               bx.setSelected(!to.hasHips() && !allCat);
               bx.setToolTipText(AWINVIEWTIP);
               bg.add(bx);
               
               if( to.isSimbadLive() ) {
                  liveBx = bx = new JCheckBox("live");
                  bx.addActionListener(this);
                  accessPanel.add(bx);
                  bg.add(bx);
                  bx.setToolTipText("Access to Simbad \"live\" (slower but with no cache)");
               }
               
               msBx = bx = new JCheckBox(AWMOCQLAB);
               bx.addActionListener(this);
               accessPanel.add(bx);
               Util.toolTip(bx, AWMOCQLABTIP);
               bx.setEnabled(hasRegion);
               bg.add(bx);

               xmatchBx = bx = new JCheckBox(AWXMATCH);
               bx.addActionListener(this);
               accessPanel.add(bx);
               bx.setEnabled(hasLoadedCat);
               Util.toolTip(bx, AWXMATCHTIP, true);
               bg.add(bx);

               // Positionnement de la sélection par défaut
               boolean onRegion = aladin.calque.getFirstSelectedPlan() instanceof PlanMoc || hasMocPol;
               if( onRegion && msBx != null ) msBx.setSelected(true);
               else if( allBx != null && nbRows < 10000 ) allBx.setSelected(true);
               else if( csBx.isEnabled() ) csBx.setSelected(true);

            } 
               
            if( to.hasCustom() ) {
               customBx = bx = new JCheckBox(AWCUSTOM);
               Util.toolTip(bx, AWCUSTOMTIP);
               bx.addActionListener(this);
               accessPanel.add(bx);
               bx.setSelected(true);
               bg.add(bx);
            }

            // Peut être y a-t-il tout de même un simple accès par cone
            if( csBx==null && to.getCSUrl() != null ) {
               csBx = bx = new JCheckBox(AWCSLAB);
               bx.addActionListener(this);
               accessPanel.add(bx);
               bx.setSelected( hipsBx==null && customBx==null);
               Util.toolTip(bx, AWINVIEWTIP);
               bg.add(bx);
            }

            if( to.hasTAP() ) {
               tapBx = bx = new JCheckBox(AWCRIT);
               bx.addActionListener(this);
               accessPanel.add(bx);
               bx.setSelected(csBx == null && siaBx == null && ssaBx == null 
                           && customBx == null && assocBx == null );
               Util.toolTip(bx, AWCRITTIP, true);
               bg.add(bx);
            }
            
            // Juste pour ne pas sélectioner un truc inactivé
            if( csBx != null     && !csBx.isEnabled() )     csBx.setSelected(false);
            if( liveBx != null   && !liveBx.isEnabled() )   liveBx.setSelected(false);
            if( siaBx != null    && !siaBx.isEnabled() )    siaBx.setSelected(false);
            if( ssaBx != null    && !ssaBx.isEnabled() )    ssaBx.setSelected(false);
            if( assocBx != null  && !assocBx.isEnabled() )  assocBx.setSelected(false);
            if( customBx != null && !customBx.isEnabled() ) customBx.setSelected(false);

            JPanel productPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 3, 0));

            if( to.hasMoc() ) {
               mocBx = bx = new JCheckBox(to.getMocType()+" "+AWMOCX);
               bx.addActionListener(this);
               productPanel.add(bx);
               Util.toolTip(bx, AWMOCXTIP, true);
            }
            
            if( to.hasVizieRAssocData() ) {
               assocBx= bx = new JCheckBox("Assoc.data");
               productPanel.add(bx);
               bx.addActionListener(this);
               bg.add(bx);
               Util.toolTip(bx, "Data (images) associated to this catalog");
            }

//            if( to.hasTMoc() ) {
//               tmocBx = bx = new JCheckBox(AWTMOCX);
//               bx.addActionListener(this);
//               productPanel.add(bx);
//               Util.toolTip(bx, AWTMOCXTIP, true);
//            }

            if( to.isCDSCatalog() && !to.isVizierTimeOnly()
                && !to.isSimbad() && nbRows != -1 && nbRows >= 10000 ) {
               dmBx = bx = new JCheckBox(AWDM);
               bx.addActionListener(this);
               productPanel.add(bx);
               Util.toolTip(bx, AWDMTIP, true);
            } else {
               if( to.getProgenitorsUrl() != null ) {
                  progBx = bx = new JCheckBox(AWPROGEN);
                  bx.addActionListener(this);
                  productPanel.add(bx);
                  Util.toolTip(bx, AWPROGENTIP, true);
               }
            }
            
            // Les links vont créer des ancres à la queue leu leu. Au max 5 
            if( to.hasLinks() ) {
               try {
                  int j=0;
                  String s1 = to.prop.get("client_link");
                  Tok tok = new Tok(s1,"\t");
                  String linkId,linkText;
                  while( tok.hasMoreTokens() && j<5 ) {
                     s1 = tok.nextToken();
                     int i = s1.indexOf(' ');
                     if( i<0 ) { linkId = s1; linkText="assoc-"+(j+1); }
                     else { linkId = s1.substring(0,i); linkText=s1.substring(i).trim(); }
                     final String linkIdf = linkId;
                     String prefix = j==0 ? "    +  ":", ";
                     MyAnchor link = new MyAnchor(aladin, prefix+linkText, false, new ActionListener() {
                        public void actionPerformed(ActionEvent e) { showTreeObj( linkIdf ); }
                     });
                     productPanel.add(link);
                     j++;
                  }
               } catch( Exception e1 ) {
                  if( Aladin.levelTrace>=3 ) e1.printStackTrace();
               }
            }

            GridBagConstraints c1 = new GridBagConstraints();
            GridBagLayout g1 = new GridBagLayout();
            c1.fill = GridBagConstraints.BOTH;
            c1.insets = new Insets(0, 0, 0, 0);
            JPanel loadPanel = new JPanel(g1);

            JLabel label1 = new JLabel(AWACCMODE+" ");
            JLabel label2 = new JLabel(AwDERPROD+" ");
            String tip = AWACCMODETIP;
            Util.toolTip(label1, Util.fold(tip, 100, true));
            String tip2 = AwDERPRODTIP;
            Util.toolTip(label2, Util.fold(tip2, 100, true));
            label1.setForeground(Color.gray.brighter());
            label2.setForeground(Color.gray.brighter());
            label1.setFont(label1.getFont().deriveFont(Font.ITALIC));
            label2.setFont(label2.getFont().deriveFont(Font.ITALIC));

            PropPanel.addCouple(null, loadPanel, label1, null, accessPanel, g1, c1, GridBagConstraints.EAST);
            int nbAccess = accessPanel.getComponentCount();
            int nbProduct = productPanel.getComponentCount();
            if( nbProduct > 0 ) {

               // Ca va faire trop de checkboxes sur une ligne => on replie sur 2 lignes
               if( nbAccess + nbProduct > 5 )
                  PropPanel.addCouple(null, loadPanel, label2, null, productPanel, g1, c1, GridBagConstraints.EAST);

               // Sinon on ajoute au bout de la précédente ligne
               else {
                  accessPanel.add(new JLabel(" + "));
                  for( Component cp : productPanel.getComponents() )
                     accessPanel.add(cp);
                  label1.setText(label1.getText() + " & " + label2.getText());
               }
            }

            PropPanel.addFilet(p, g, c, 5, 2);
            PropPanel.addCouple(p, "", loadPanel, g, c);
         }

         panelInfo.add(p, BorderLayout.CENTER);

         JPanel control = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
         JPanel precontrol = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
         control.setBackground(contentPane.getBackground());

         Preview preview = null;
         JButton b = null;
         JTextField t = null;
         JPanel submitPanel = new JPanel(new FlowLayout());

         if( treeObjs.size() == 1 ) {

            preview = new Preview(to);

            // Info
            if( to.hasInfo() ) {
               b = new JButton(new ImageIcon(Aladin.aladin.getImagette("Info.png")));
               b.setToolTipText(AWINFOTIP);
               b.setMargin(new Insets(0, 0, 0, 0));
               b.setBorderPainted(false);
               b.setContentAreaFilled(false);
               control.add(b);
               b.addActionListener(new ActionListener() {
                  public void actionPerformed(ActionEvent e) {
                     info();
                  }
               });
            }

            // Properties
            b = new JButton(new ImageIcon(Aladin.aladin.getImagette("Prop.png")));
            b.setToolTipText(AWPROPTIP);
            b.setMargin(new Insets(0, 0, 0, 0));
            b.setBorderPainted(false);
            b.setContentAreaFilled(false);
            control.add(b);
            b.addActionListener(new ActionListener() {
               public void actionPerformed(ActionEvent e) {
                  prop();
               }
            });

            // Bookmark
            b = new JButton(new ImageIcon(Aladin.aladin.getImagette("Bookmark.png")));
            Util.toolTip(b, AWBOOKMARKTIP);
            b.setMargin(new Insets(0, 0, 0, 0));
            b.setBorderPainted(false);
            b.setContentAreaFilled(false);
            control.add(b);
            b.addActionListener(new ActionListener() {
               public void actionPerformed(ActionEvent e) {
                  bookmark();
               }
            });

            // Paramètres
            if( to.hasProp() ) {
               b = new JButton(new ImageIcon(Aladin.aladin.getImagette("settings.png")));
               b.setToolTipText(AWPARAMTIP);
               b.setMargin(new Insets(0, 0, 0, 0));
               b.setBorderPainted(false);
               b.setContentAreaFilled(false);
               final TreeObjDir to1 = to;
               b.addActionListener(new ActionListener() {
                  public void actionPerformed(ActionEvent e) {
                     parameters(to1);
                  }
               });
               control.add(b);
            }

         } else {
            
            b = new JButton(new ImageIcon( Aladin.aladin.getImagette("Expand.png")));
            b.setMargin(new Insets(0, 0, 0, 0));
            b.setBorderPainted(false);
            b.setContentAreaFilled(false);
            Util.toolTip(b, aladin.chaine.getString("COLLAPSETIP"), true);
            b.setFont(b.getFont().deriveFont(Font.BOLD));

            precontrol.add(b);
            b.addActionListener(new ActionListener() {
               public void actionPerformed(ActionEvent e) {
                  iconCollapse.submit();
               }
            });
           
            if( flagSort ) {
               b = new JButton(new ImageIcon( Aladin.aladin.getImagette("Sort.png")));
               b.setMargin(new Insets(0, 0, 0, 0));
               b.setBorderPainted(false);
               b.setContentAreaFilled(false);
               Util.toolTip(b, aladin.chaine.getString("SORTTIP"), true);
               b.setFont(b.getFont().deriveFont(Font.BOLD));
            }

            precontrol.add(b);
            final JButton bfinal = b;
            b.addActionListener(new ActionListener() {
               public void actionPerformed(ActionEvent e) {
                  aladin.directory.triBranch( bfinal, 5,5);
               }
            });
            
            if( flagScan ) {
               b = new JButton(new ImageIcon( Aladin.aladin.getImagette("icon_searchAitoff.png"))); // AWSCANONLY);
               b.setMargin(new Insets(0, 0, 0, 0));
               b.setBorderPainted(false);
               b.setContentAreaFilled(false);
               b.setEnabled(hasView);
               Util.toolTip(b, AWSCANONLYTIP, true);
               b.setFont(b.getFont().deriveFont(Font.BOLD));

               precontrol.add(b);
               b.addActionListener(new ActionListener() {
                  public void actionPerformed(ActionEvent e) {
                     scan();
                     hideInfo();
                  }
               });
            }
         }
         
         // Epinglette
         b = new JButton(new ImageIcon(Aladin.aladin.getImagette("Pin.png")));
         b.setToolTipText(AWSTICKTIP);
         b.setMargin(new Insets(0, 0, 0, 0));
         b.setBorderPainted(false);
         b.setContentAreaFilled(false);
         b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) { epingle(); }
         });
         control.add(b);
         
         load = b = new JButton(AWLOAD);
         b.setFont(b.getFont().deriveFont(Font.BOLD));
         b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
               submit();
               if( !decorated ) hideInfo();
               else aladin.f.toFront();
            }
         });
         submitPanel.add(b);

         if( siaBx!=null || ssaBx!=null || csBx!=null || assocBx!=null ) {
            
            targetPanel = new JPanel( new FlowLayout(FlowLayout.RIGHT,0,0) );
            t = target = new ConeField();
            Util.toolTip(t, AWCONETIP);
            t.addKeyListener( new KeyListener() {
               public void keyPressed(KeyEvent e) { }
               public void keyTyped(KeyEvent e) { }
               public void keyReleased(KeyEvent e) {
                  if( grabIt ) aladin.view.getCurrentView().stopGrabIt();
                  if( e.getKeyCode()==KeyEvent.VK_ENTER ) {
                    if( ((JTextField)e.getSource()).getText().trim().length()==0 ) {
                       setTarget(null);
                    } else {
                       submit();
                       setVisible(false);
                    }
                 }
               }
            });
            targetPanel.add( t );
           
            // Grab
            grab = b = new JButton(new ImageIcon(Aladin.aladin.getImagette("Grab.png")));
            Util.toolTip(b, AWCGRAPTIP);
            b.setMargin(new Insets(0, 0, 0, 0));
            b.setBorderPainted(false);
            b.setContentAreaFilled(false);
            b.addActionListener(new ActionListener() {
               public void actionPerformed(ActionEvent e) {
                  startGrabIt();
               }
            });
            if( hasView ) targetPanel.add( b );
            
            submitPanel.add(targetPanel);
         }
         
         JPanel bas = new JPanel(new BorderLayout(0, 0));
         if( precontrol.getComponentCount()>0 ) bas.add(precontrol, BorderLayout.WEST);
         bas.add(submitPanel, BorderLayout.CENTER);
         bas.add(control, BorderLayout.EAST);

         if( to != null && to.internalId != null ) {
            LabelCopy x = new LabelCopy(to.internalId, getBackground());
            bas.add(x, BorderLayout.WEST);
         }

         contentPane.add(panelInfo, BorderLayout.CENTER);
         panelInfo.add(bas, BorderLayout.SOUTH);

         if( preview != null ) panelInfo.add(preview, BorderLayout.WEST);
         contentPane.validate();
      }
      
      protected void startGrabIt() {
         grabIt=true;
         targetPanel.removeAll();
         targetPanel.add( target );
         targetPanel.add( grab );
         validate();
         aladin.getFrame(aladin).toFront();
         
         if( aladin.firstGrab && aladin.configuration.isHelp() && aladin.configuration.showHelpIfOk("GRABINFO") ) {
            aladin.firstGrab=false;
         }

      }
      
      // Label copiable, qui se centre verticalement
      class LabelCopy extends JPanel {
         LabelCopy(String label, Color background) {
            super(new FlowLayout(FlowLayout.CENTER,0,0));
            setBackground(background);
            final JTextPane x = new JTextPane();
            x.setText(label+" ");
            x.setEditable(false);
            x.setBackground(background);
            x.setBorder(null);
            x.setForeground(Aladin.COLOR_BLUE);
            x.setFont(x.getFont().deriveFont(Font.BOLD + Font.ITALIC));
            x.addMouseListener( new MouseAdapter() {
               public void mousePressed(MouseEvent e) {
                  x.setSelectionStart(0);
                  x.setSelectionEnd(x.getText().length()-1 );
               }
            });
            add(x);
            setBorder( BorderFactory.createEmptyBorder(8,0,0,0));
         }
      }

      class Preview extends JPanel {

         TreeObjDir to;

         Preview(TreeObjDir to) {
            super();
            this.to = to;
            
            int mode=0; // 0-le preview par defaut   1-une vignette dynamique
            double radius=0;
            Coord target=null;
            
            // Est-il possible de demander une vignette du champ courant
            if( !to.isCatalog() && !to.previewErrorHips2Fits && to.getIsIn()==1 ) {
               // Récupération du champ de vue courant
               try {
                  radius = aladin.view.getCurrentView().getTaille();
                  target = aladin.view.getCurrentView().getCooCentre();
                  
                  // Si trop grand, on va prendre le preview par defaut
                  mode = radius>10 && !to.previewError ? 0 : 1;  
               } catch( Exception e) {  }
            }
            
            // Chargement de la vignette à la position courante
            if( mode==1 ) {
               try {
                  
                  // On constuit l'URL pour Hips2fits
                  String sys = aladin.localisation.getFrameGeneric()==Localisation.GAL ? "gal":"icrs";
                  String proj = aladin.projSelector.getProjName();
                  String param = target.al+" "+target.del+" "+radius+" "+to.getID()
                                +" "+PREVIEW_SIZE+" "+sys+" "+proj+" jpg";
                  final String url = aladin.glu.getURL("Hips2Fits", param, false, false).toString();
                  
                  // Déjà fait ? => rien à faire
                  if( url.equals( to.previewUrlHips2Fits ) ) return;
                  
                  // On change de champ
                  to.previewImgHips2Fits = null;
                  (new Thread() { public void run() { load(1,url);  } }).start();
               } catch( Exception e ) {
                  to.previewErrorHips2Fits=true;
                  mode=0;  // on ce sera le preview
               }
            }
            
            // Chargement du preview par défaut
            if( mode==0 ) {

               // Déjà fait ?
               if( to.previewError || to.previewImg != null ) return;

               // Pas de preview ?
               if( !to.hasPreview() ) { to.previewError = true; return; }
               

               final String url = to.getPreviewUrl();
               (new Thread() { public void run() { load(0,url);  } }).start();
            }
         }
         

         // mode=0 => chargement du preview officiel
         // mode=1 => chargement d'une vignette par Hipstofits
         void load(int mode,String url) {

            LoadingUrl=url;
            to.previewLoading=true;
            
            // On lache la main, histoire de faciliter l'affichage
            Util.pause(30);
            
            // On a visiblement changer de vignette => interruption du chargement
            if( !url.equals(LoadingUrl) ) return;
            
            aladin.trace(5,"Loading hips2Fits "+url);
            
            long t0 = Util.getTime();
            
            MyInputStream is = null;
            try {
               is = new MyInputStream(Util.openStream(url));
               byte buf[] = is.readFully();
               if( buf.length == 0 ) throw new Exception();
               Image im = Toolkit.getDefaultToolkit().createImage(buf);

               MediaTracker mt = new MediaTracker(this);
               mt.addImage(im, 0);
               mt.waitForAll();
               
               if( mode==1 ) {
                  to.previewImgHips2Fits=im;
                  to.previewUrlHips2Fits=url;
                  
               } else {
                  to.previewImg = im;
                  to.previewImgHips2Fits = null;  // on n'affichera plus la vignette 
                  to.previewUrlHips2Fits=null;
              }

            } catch( Exception e ) {
               if( mode==0 ) to.previewError = true;
               else to.previewErrorHips2Fits = true;
            } finally {
               to.previewLoading=false;
               if( is != null ) try {
                  is.close();
               } catch( Exception e1 ) { }
            }
            
//            System.err.println("Loading hips2fits in "+(Util.getTime()-t0)+" => "+url);
            repaint();
         }

         public Dimension getPreferredSize() {
            return new Dimension(PREVIEW_SIZE, PREVIEW_SIZE);
         }

         public void paintComponent(Graphics g) {
            paintComponent1(g);
            int w = getWidth();
            int h = getHeight();
            int x,y;
            String s;
            
            if( to.isNew() ) {
               g.setFont(Aladin.ITALIC);
               s = "New " + (to.isNewObsRelease() ? "release" : to.isNewHips() ? "HiPS" : "!");
               x = w / 2 - g.getFontMetrics().stringWidth(s) / 2 + 3;
               y = 12;
               Util.drawNew(g, x - 4, y - 6, Color.yellow);
               g.drawString(s, x, y);
            }

            boolean in = to.getIsIn() == 1;
            g.setFont(Aladin.SPLAIN);
            Color c = in ? Aladin.COLOR_GREEN.brighter().brighter() : Aladin.ORANGE.brighter();
            s = to.getDataType();
            x = w / 2 - g.getFontMetrics().stringWidth(s) / 2 + 3;
            y = h-16;
            g.setColor( c );
            g.drawString(s, x, y);
            
            // Une table uniquement temporelle ?
            boolean timeTable = to.getMocType().indexOf('T')>=0;
            if( timeTable ) {
               Slide.drawClock(g, x-10, y-4, 4, c, Color.white);
            }

            if( to.getIsIn() != -1 ) {
               g.setFont(Aladin.SPLAIN);
               s = in ? (timeTable?"events":"data")+" in view" : "out of view";
               x = w / 2 - g.getFontMetrics().stringWidth(s) / 2 + (in ? 0 : 3);
               y = h - 4;
               if( !in ) Util.drawWarning(g, x - 10, y - 7, c, Color.black);
               g.setColor( c );
               g.drawString(s, x, y);
            }
            
         }

         private void paintComponent1(Graphics g) {
            super.paintComponent(g);
            int ws = getWidth();
            int hs = getHeight();
            g.setColor(Color.gray);
            g.fillRect(0, 0, ws, hs);
            
            boolean noImg = to.previewImg == null && to.previewImgHips2Fits == null;

            if( noImg || to.previewLoading ) {
               g.setColor(Color.lightGray);
               String s = to.previewLoading ? "loading" : "no preview";
               g.setFont(Aladin.ITALIC);
               java.awt.FontMetrics fm = g.getFontMetrics();
               g.drawString(s, ws / 2 - fm.stringWidth(s) / 2, hs / 2 + 4);
               return;
            }

            Image img = to.previewImgHips2Fits!=null ? to.previewImgHips2Fits : to.previewImg;

            int wi = img.getWidth(this);
            int hi = img.getHeight(this);
            boolean vertical = Math.abs(1 - (double) hi / hs) < Math.abs(1 - (double) wi / ws);
            double sx2, sy2;
            if( vertical ) {
               sy2 = hi;
               sx2 = ws * ((double) hi / hs);
            } else {
               sx2 = wi;
               sy2 = hs * ((double) wi / ws);
            }

            if( hi < hs ) g.translate(0, (hs - hi) / 2);
            g.drawImage(img, 0, 0, ws, hs, 0, 0, (int) sx2, (int) sy2, this);
            if( hi < hs ) g.translate(0, -(hs - hi) / 2);
         }
      }
      
      class ConeField extends JTextField {
         String info=null;
         ConeField() { super(26); }
         
         void setInfo(String s) { info=s; }
         
         public void paintComponent(Graphics g) {
            super.paintComponent(g);
            if( info!=null && getText().length()==0 ) {
               g.setColor( Color.lightGray );
               g.setFont( getFont().deriveFont(Font.ITALIC) );
               g.drawString(info,5,getHeight()/2+ g.getFontMetrics().getAscent()/2-2 );
            }
         }
         
         
      }
      
      private class NoneSelectedButtonGroup extends ButtonGroup {
         public void setSelected(ButtonModel model, boolean selected) {
            if( selected ) super.setSelected(model, selected);
            else clearSelection();
         }
      }

      // Ajout du bookmark qui correspond à la sélection des checkboxes
      void bookmark() {
         StringBuilder bkm = new StringBuilder();
         TreeObjDir to = treeObjs.get(0);
         if( allBx != null    && allBx.isSelected() )    addBkm(bkm, to.getAllBkm());
         if( globalBx != null && globalBx.isSelected() ) addBkm(bkm, to.getGlobalAccessBkm());
         if( siaBx != null    && siaBx.isSelected() )    addBkm(bkm, to.getSIABkm());
         if( ssaBx != null    && ssaBx.isSelected() )    addBkm(bkm, to.getSSABkm());
         if( csBx != null     && csBx.isSelected() )     addBkm(bkm, to.getCSBkm());
         if( liveBx != null   && liveBx.isSelected() )   addBkm(bkm, to.getLiveSimbadBkm());
         if( hipsBx != null   && hipsBx.isSelected() )   addBkm(bkm, to.getHipsBkm());
         if( mocBx != null    && mocBx.isSelected() )    addBkm(bkm, to.getMocBkm());
//         if( tmocBx != null   && tmocBx.isSelected() )   addBkm(bkm, to.getTMocBkm());
         if( progBx != null   && progBx.isSelected() )   addBkm(bkm, to.getProgenitorsBkm());
//         if( customBx != null && customBx.isSelected() ) addBkm(bkm, to.getCustomBkm());

         // LES 3 AUTRES ACCES NE SONT PAS ACTUELLEMENT ACCESSIBLES PAR COMMANDE SCRIPT
         // => PAS DE BOOKMARK POSSIBLE
         // if( msBx!=null && msBx.isSelected() ) to.queryByMoc();
         // if( dmBx!=null && dmBx.isSelected() ) to.loadDensityMap();
         // if( tapBx!=null && tapBx.isSelected() ) to.queryByTap();

         if( bkm.length() == 0 ) return;

         String name = to.internalId;
         FrameBookmarks fb = aladin.bookmarks.getFrameBookmarks();
         fb.setVisibleEdit();
         fb.createNewBookmark(name, "$TARGET,$RADIUS", "Load " + name + " on the view", bkm.toString());
      }

      // Ajout d'une commande au bookmark en cours de construction
      private void addBkm(StringBuilder bkm, String cmd) {
         if( bkm.length() > 0 ) bkm.append("\n");
         bkm.append("   " + cmd);
      }

      /** Affiche la fenêtre des paramètres */
      void parameters(TreeObjDir to) {
         if( !to.hasProp() ) {
            if( frameProp != null ) frameProp.setVisible(false);
            return;
         }
         if( frameProp == null ) frameProp = new FrameProp(aladin, "Query parameters", to);
         else frameProp.updateAndShow(to);
      }

      void prop() {
         try {
            TreeObjDir to1 = treeObjs.get(0);
            new FrameHipsProperties(to1.internalId + " properties", to1.prop.getRecord(null));
         } catch( Exception e2 ) {
         }
      }

      void info() {
         TreeObjDir to = treeObjs.get(0);
         String url = to.getInfoUrl();
         aladin.glu.showDocument(url);
      }

      void submit() {
         if( treeObjs.size() == 0 ) return;

         // Interrogation par region, mais comme c'est un cercle, on fait directement un CS
         // plutôt qu'un query by MOC
         if( msBx != null && msBx.isSelected() && aladin.view.vselobj.size() == 1 ) {
            Obj o = aladin.view.vselobj.get(0);
            if( o instanceof Cercle || o instanceof SourceStat ) {
               try {
                  double ra = o.getRa();
                  double de = o.getDec();
                  double radius = o.getRadius();
                  for( TreeObjDir to : treeObjs )
                     to.loadCS(new Coord(ra, de), radius);
                  return;
               } catch( Exception e ) {
               }
            }
         }

         PlanMoc planMoc = null;
         if( msBx != null && msBx.isSelected() ) {

            // Création d'un plan MOC à partir d'une région sélectionnée
            // dans un plan tool
            if( aladin.view.hasMocPolSelected() ) {

               SMoc moc = null;
               try {
                  moc = aladin.createMocByRegions(-1,true);
               } catch( Exception e ) {
               }
               if( moc != null ) {
                  Coord c = aladin.calque.getTargetBG(null, null);
                  double rad = aladin.calque.getRadiusBG(null, null, null);
                  planMoc = new PlanMoc(aladin, moc, "moc", c, rad,null);
               } else {
                  aladin.warning("MOC creation failed !\nYour graphical region must be circles, "
                        + "and/or polygons counter-clock oriented");
                  return;
               }

               // Détermination du PlanMoc directement dans la pile
            } else {

               for( Object p : aladin.calque.getSelectedPlanes() ) {
                  if( p instanceof PlanMoc && ((PlanMoc) p).flagOk ) {
                     planMoc = (PlanMoc) p;
                     break;
                  }
               }
               if( planMoc == null ) {
                  for( Object p : aladin.calque.getPlans() ) {
                     if( p instanceof PlanMoc && ((PlanMoc) p).flagOk ) {
                        planMoc = (PlanMoc) p;
                        break;
                     }
                  }
               }
            }

            if( planMoc == null ) {
               aladin.warning("You need to select a graphical region or a MOC plane in the stack");
               return;
            }
         }
         
         // Y a-t-il un cone spécifique ?
         String cone = target==null ? null : target.getText().trim();
         if( cone!=null && cone.length()==0 ) cone=null;

         // Accès à une collection
         if( treeObjs.size() == 1 ) {
            TreeObjDir to = treeObjs.get(0);
            if( globalBx != null && globalBx.isSelected() ) to.loadGlobalAccess();
            if( allBx != null    && allBx.isSelected() )    to.loadAll();
            if( siaBx != null    && siaBx.isSelected() )    to.loadSIA(cone);
            if( assocBx != null  && assocBx.isSelected() )  to.loadAssoc(cone);
            if( ssaBx != null    && ssaBx.isSelected() )    to.loadSSA(cone);
            if( csBx != null     && csBx.isSelected() )     to.loadCS(cone);
            if( customBx!=null   && customBx.isSelected() ) to.loadCustom();
            if( liveBx != null   && liveBx.isSelected() )   to.loadLiveSimbad(cone);
            if( msBx != null     && msBx.isSelected() )     to.queryByMoc(planMoc);
            if( hipsBx != null   && hipsBx.isSelected() )   to.loadHips();
            if( mocBx != null    && mocBx.isSelected() )    to.loadMoc();
//            if( tmocBx != null   && tmocBx.isSelected() )   to.loadTMoc();
            if( progBx != null   && progBx.isSelected() )   to.loadProgenitors();
            if( dmBx != null     && dmBx.isSelected() )     to.loadDensityMap();
            if( tapBx != null    && tapBx.isSelected() )    to.queryByTap();
            if( xmatchBx != null && xmatchBx.isSelected() ) to.queryByXmatch();

            // Accès à plusieurs collections simultanément
         } else {

            // CS et assimilés
            if( csBx != null && csBx.isSelected() ) {
               if( tooMany(treeObjs.size()) ) return;
               
               
               for( TreeObjDir to : removeDoublon(treeObjs) ) {
                  if( to.getIsIn() == 0 ) continue; // Je n'interroge pas les collections hors champs
                  if( to.hasSIA() ) to.loadSIA(cone);
                  else if( to.hasSSA() ) to.loadSSA(cone);
                  else if( to.hasGlobalAccess() ) to.loadGlobalAccess();
                  else to.loadCS(cone);
               }
            }

            if( hipsBx != null && hipsBx.isSelected() ) {
               if( tooMany(treeObjs.size()) ) return;
               for( TreeObjDir to : treeObjs ) {
                  if( to.hasHips() ) to.loadHips();
               }
            }

            // MOC search
            if( msBx != null && msBx.isSelected() ) {
               for( TreeObjDir to : treeObjs ) {
                  if( to.isCDSCatalog() ) to.queryByMoc(planMoc);
               }
            }

            // Union des MOCs
            if( mocBx != null && mocBx.isSelected() ) {
               if( tooMany(treeObjs.size()) ) return;
               multiMocLoad(treeObjs, MultiMocMode.EACH);
            }
            
            if( mocuBx != null && mocuBx.isSelected() ) multiMocLoad(treeObjs, MultiMocMode.UNION);
            if( mociBx != null && mociBx.isSelected() ) multiMocLoad(treeObjs, MultiMocMode.INTER);
         }

      }

      /** Chargement de chaque MOC ou de l'union ou intersection des Mocs */
      void multiMocLoad(ArrayList<TreeObjDir> treeObjs, MultiMocMode mode) {

         // Chaque MOC individuellement
         if( mode == MultiMocMode.EACH ) {
            for( TreeObjDir to : treeObjs ) to.loadMoc();
            return;
         }

         // Union ou Intersection
         StringBuilder params = null;
         for( TreeObjDir to : treeObjs ) {
            if( params == null ) params = new StringBuilder(to.internalId);
            else params.append("," + to.internalId);
         }
         String cmd = (mode == MultiMocMode.INTER ? "iMOCs" : "MOCs") + "=get Moc(" + Tok.quote(params.toString())
               + (mode == MultiMocMode.INTER ? ",imoc" : "") + ")";
         aladin.execAsyncCommand(cmd);

      }
   }
   
   // Suppression des doublons SIA, SSA et CS éventuels pour faire plaisir à Markus
   private ArrayList<TreeObjDir> removeDoublon( ArrayList<TreeObjDir> treeObjs ) {
      
      LinkedHashMap<String, TreeObjDir> map = new LinkedHashMap<>();
      for( TreeObjDir to : treeObjs ) {
         if( to.getIsIn() == 0 ) continue; // Je n'interroge pas les collections hors champs
         if( to.hasSIA() ) {
            String urlList = resolveServiceUrl("SIA",to.internalId);
            if( urlList!=null ) {
               Tok tokUrlList = new Tok(urlList,"\t");
               while( tokUrlList.hasMoreTokens() ) {
                  String u = tokUrlList.nextToken();
                  if( map.get(u)==null ) map.put(u,to);
               }
            }
         }
         else if( to.hasSSA() ) {
            String urlList = resolveServiceUrl("SSA",to.internalId);
            if( urlList!=null ) {
               Tok tokUrlList = new Tok(urlList,"\t");
               while( tokUrlList.hasMoreTokens() ) {
                  String u = tokUrlList.nextToken();
                  if( map.get(u)==null ) map.put(u,to);
               }
            }
         }
         else if( to.hasGlobalAccess() ) map.put( to.getGlobalAccessCmd(), to);
         else {
            String urlList = resolveServiceUrl("CS",to.internalId);
            if( urlList!=null ) {
               Tok tokUrlList = new Tok(urlList,"\t");
               while( tokUrlList.hasMoreTokens() ) {
                  String u = tokUrlList.nextToken();
                  if( map.get(u)==null ) map.put(u,to);
               }
            }
         }
      }
      
      ArrayList<TreeObjDir> n = new ArrayList<>();
      for( TreeObjDir to : map.values() ) n.add( to);
      return n;
   }
   
   static private enum MultiMocMode {
      EACH, UNION, INTER
   }
   
   protected boolean grabIt=false;
   public boolean isGrabIt() { return grabIt; }
   public void stopGrabIt() { grabIt=false; }
   public void setGrabItCoord(Coord c) { //double x, double y) {
      String sexaCoord = GrabUtil.getGrabItCoord(aladin, c); //x, y);
      frameInfo.setTarget( aladin.localisation.getFrameCoord(sexaCoord) );
      frameInfo.updateWidget();

   }
   public void setGrabItRadius(double x1, double y1, double x2, double y2) {
      String radius = GrabUtil.setGrabItRadius(aladin, null, x1,y1, x2,y2);
      frameInfo.setRadius( radius );
      frameInfo.updateWidget();
   }
   
   public void toFront() { frameInfo.toFront(); }
   
   /** Cache la fenêtre des infos si possible */
   public void hideInfoIfPossible() {
      System.out.println("Bip");
      if( frameInfo==null || !frameInfo.isVisible() ) return;
      if( decorated ) return;
      frameInfo.setVisible(false);

   }
}
