/**
   * Takes a bit mask representing selected rows and causes it to become the Current Row Subset for
   * the given table. Usually this means creating a new Row Subset corresponding to that mask prior
   * to applying it. However, in the special case that the mask is identical to an existing subset,
   * that one will be used instead.
   *
   * @param tcModel topcat model
   * @param mask row selection mask
   * @param baseName name of the sending application
   */
  private void applyNewSubset(final TopcatModel tcModel, BitSet mask, String appName) {

    /* See if this is identical to an existing subset.  If so, don't
     * create a new one.  It's arguable whether this is the behaviour
     * that you want, but at least until we have some way to delete
     * subsets it's probably best to do it like this to cut down on
     * subset proliferation. */
    RowSubset matching = null;
    for (Iterator it = tcModel.getSubsets().iterator(); matching == null && it.hasNext(); ) {
      RowSubset rset = (RowSubset) it.next();
      int nrow = Tables.checkedLongToInt(tcModel.getDataModel().getRowCount());
      if (matches(mask, rset, nrow)) {
        matching = rset;
      }
    }

    /* If we've found an existing set with the same content,
     * apply that one. */
    if (matching != null) {
      final RowSubset rset = matching;
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              tcModel.applySubset(rset);
            }
          });
    }

    /* Otherwise make sure we have a unique name for the new subset. */
    else {
      int ipset = 0;
      for (Iterator it = tcModel.getSubsets().iterator(); it.hasNext(); ) {
        String setName = ((RowSubset) it.next()).getName();
        if (setName.matches(appName + "-[0-9]+")) {
          String digits = setName.substring(appName.length() + 1);
          ipset = Math.max(ipset, Integer.parseInt(digits));
        }
      }
      String setName = appName + '-' + (ipset + 1);

      /* Then construct, add and apply the new subset. */
      final RowSubset rset = new BitsRowSubset(setName, mask);
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              tcModel.addSubset(rset);
              tcModel.applySubset(rset);
            }
          });
    }
  }
 /**
  * Attempts to locate a table by its ID. This is currently a URL string; either one got from a
  * previous VOT_LOADURL message or one inherent in the table.
  *
  * @param tableId table identifier URL string
  * @return tableWithRows object corresponding to tableId, or null
  */
 private TableWithRows lookupTable(String tableId) {
   TableWithRows tr = (TableWithRows) idMap_.get(tableId);
   if (tr != null) {
     return tr;
   } else {
     ListModel tablesList = ControlWindow.getInstance().getTablesListModel();
     for (int i = 0; i < tablesList.getSize(); i++) {
       TopcatModel tcModel = (TopcatModel) tablesList.getElementAt(i);
       URL url = tcModel.getDataModel().getBaseTable().getURL();
       if (URLUtils.sameResource(url, URLUtils.makeURL(tableId))) {
         return new TableWithRows(tcModel, null);
       }
     }
   }
   return null;
 }
  /**
   * Transmits a request for listening applications to highlight a given table row.
   *
   * @param tcModel topcat model of table to broadcast
   * @param lrow row index within tcModel
   * @param recipients array of plastic IDs for target applications; if null, broadcast will be to
   *     all
   * @return true iff message was broadcast successfully
   */
  public boolean highlightRow(TopcatModel tcModel, long lrow, URI[] recipients) throws IOException {

    /* Get the hub and ID. */
    register();
    PlasticHubListener hub = getHub();
    URI plasticId = getRegisteredId();
    int irow = Tables.checkedLongToInt(lrow);

    /* See if the table we're broadcasting the row for is any of the
     * tables we've previously broadcast.  If so, send the row using the
     * same ID. */
    boolean done = false;
    int sendRow = -1;
    String tableId = null;
    for (Iterator it = idMap_.entrySet().iterator(); !done && it.hasNext(); ) {
      Map.Entry entry = (Map.Entry) it.next();
      TableWithRows tr = (TableWithRows) entry.getValue();
      TopcatModel tcm = (TopcatModel) tr.tcModelRef_.get();
      if (tcm != null && tcm == tcModel) {
        int[] rowMap = tr.rowMap_;
        if (rowMap == null) {
          sendRow = irow;
        } else {
          for (int j = 0; j < rowMap.length; j++) {
            if (irow == rowMap[j]) {
              sendRow = j;
              break;
            }
          }
        }
        tableId = entry.getKey().toString();
        done = true;
      }
    }

    /* If that didn't result in any sends, try using the basic URL of
     * the table. */
    if (!done) {
      URL url = URLUtils.fixURL(tcModel.getDataModel().getBaseTable().getURL());
      if (url != null) {
        sendRow = irow;
        tableId = url.toString();
        done = true;
      }
    }

    /* Send the message if we've got the arguments. */
    if (done && sendRow >= 0) {
      List args =
          Arrays.asList(
              new Object[] {
                tableId, new Integer(sendRow),
              });
      if (recipients == null) {
        hub.requestAsynch(plasticId, MessageId.VOT_HIGHLIGHTOBJECT, args);
      } else {
        hub.requestToSubsetAsynch(
            plasticId, MessageId.VOT_HIGHLIGHTOBJECT, args, Arrays.asList(recipients));
      }
      return true;
    }

    /* Otherwise return failure status. */
    else {
      return false;
    }
  }
  /**
   * Sends a row subset to a specific list of PLASTIC listeners. It uses the <code>
   * ivo://votech.org/votable/showObjects</code> message.
   *
   * @param tcModel topcat model
   * @param rset row subset within tcModel
   * @param hub hub object
   * @param plasticId registration ID for this application
   * @param recipients listeners to receive it; null means do a broadcast
   */
  private void transmitSubset(
      TopcatModel tcModel,
      RowSubset rset,
      PlasticHubListener hub,
      URI plasticId,
      final URI[] recipients)
      throws IOException {

    /* See if the table we're broadcasting the set for is any of the
     * tables we've previously broadcast.  If so, send the rows using
     * the same ID. */
    boolean done = false;
    for (Iterator it = idMap_.entrySet().iterator(); it.hasNext(); ) {
      Map.Entry entry = (Map.Entry) it.next();
      String tableId = entry.getKey().toString();
      TableWithRows tr = (TableWithRows) entry.getValue();
      TopcatModel tcm = (TopcatModel) tr.tcModelRef_.get();
      if (tcm != null && tcm == tcModel) {
        List rowList = new ArrayList();

        /* Assemble a list of rows, possibly modulated by the
         * row order when the table was sent originally. */
        int[] rowMap = tr.rowMap_;
        if (rowMap == null) {
          int nrow = (int) Math.min((long) Integer.MAX_VALUE, tcModel.getDataModel().getRowCount());
          for (int i = 0; i < nrow; i++) {
            if (rset.isIncluded(i)) {
              rowList.add(new Integer(i));
            }
          }
        } else {
          int nrow = rowMap.length;
          for (int i = 0; i < nrow; i++) {
            if (rset.isIncluded(rowMap[i])) {
              rowList.add(new Integer(i));
            }
          }
        }

        /* Send the request. */
        List argList = Arrays.asList(new Object[] {tableId, rowList});
        if (recipients == null) {
          hub.requestAsynch(plasticId, MessageId.VOT_SHOWOBJECTS, argList);
        } else {
          hub.requestToSubsetAsynch(
              plasticId, MessageId.VOT_SHOWOBJECTS, argList, Arrays.asList(recipients));
        }
        done = true;
      }
    }

    /* If that didn't result in any sends, try using the basic URL of
     * the table. */
    if (!done) {
      URL url = URLUtils.fixURL(tcModel.getDataModel().getBaseTable().getURL());
      if (url != null) {
        List rowList = new ArrayList();
        int nrow = (int) Math.min((long) Integer.MAX_VALUE, tcModel.getDataModel().getRowCount());
        for (int i = 0; i < nrow; i++) {
          if (rset.isIncluded(i)) {
            rowList.add(new Integer(i));
          }
        }
        List argList = Arrays.asList(new Object[] {url.toString(), rowList});
        if (recipients == null) {
          hub.requestAsynch(plasticId, MessageId.VOT_SHOWOBJECTS, argList);
        } else {
          hub.requestToSubsetAsynch(
              plasticId, MessageId.VOT_SHOWOBJECTS, argList, Arrays.asList(recipients));
        }
      }
    }
  }