chiark / gitweb /
formatting: move code about to make more sense, and improve block comments, etc....
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 2 Apr 2011 19:09:00 +0000 (20:09 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 2 Apr 2011 19:09:00 +0000 (20:09 +0100)
src/net/chiark/yarrg/MarketUploader.java

index f189408424e946fd879a216b409e97b83bb7ccfb..c39b0f85ac3402e3cd3158ff4308aef7d9f38752 100644 (file)
@@ -21,35 +21,42 @@ import java.util.regex.*;
 import java.util.prefs.Preferences;
 import java.beans.*;
 
-/**
-     MarketUploader is a class that handles the uploading of market
-     data from Yohoho! Puzzle Pirates via the Java Accessibility
-     API.
-*
-     MarketUploader initializes after the main YPP window has
-     initialized. It provides a simple window with a "Capture
-     Market Data" button displayed.  Upon clicking this button, a
-     progress dialog is displayed, and the data is processed and
-     submitted to the YARRG and PCTB servers. If any errors occur,
-     an error dialog is shown, and processing returns, the button
-     becoming re-enabled.
-*/
+/*
+ *     MarketUploader is a class that handles the uploading of market
+ *     data from Yohoho! Puzzle Pirates via the Java Accessibility
+ *     API.
+ *
+ *     MarketUploader initializes after the main YPP window has
+ *     initialized. It provides a simple window with a "Capture
+ *     Market Data" button displayed.  Upon clicking this button, a
+ *     progress dialog is displayed, and the data is processed and
+ *     submitted to the YARRG and PCTB servers. If any errors occur,
+ *     an error dialog is shown, and processing returns, the button
+ *     becoming re-enabled.
+ */
 public class MarketUploader
 implements Runnable, TopLevelWindowListener, GUIInitializedListener {
+  // UI object references which are set during startup
   private JFrame frame = null;
   private Window window = null;
+
+  // Genuinely global variables
+  public PrintStream dtxt = null;
+  private int uploadcounter = 0;
+
+  // UI objects which are enabled/disabled, cleared/set, created/destroyed,
+  //  etc., for each upload
   private JButton findMarket = null;
   private JLabel resultSummary = null;
   private JLabel arbitrageResult = null;
-  private int unknownPCTBcommods = 0;
-  private long startTime = 0;
   private ProgressMonitor progmon = null;
 
+  // PCTB protocol parameters
   private final static String PCTB_LIVE_HOST_URL = "http://pctb.crabdance.com/";
   private final static String PCTB_TEST_HOST_URL = "http://pctb.ilk.org/";
   private String PCTB_HOST_URL;
 
-  // Yarrg protocol parameters
+  // YARRG protocol parameters
   private final static String YARRG_CLIENTNAME = "jpctb greenend";
   private final static String YARRG_CLIENTVERSION =
            net.chiark.yarrg.Version.version;
@@ -60,18 +67,29 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     "http://upload.yarrg.chiark.net/test/commod-update-receiver";
   private String YARRG_URL;
 
+  // Preferences
   private boolean uploadToYarrg;
   private boolean uploadToPCTB;
   private boolean showArbitrage;
 
+  // Values cleared/set for each upload, or used during upload processing
+  private long startTime = 0;
+
   private String islandName = null;
   private String oceanName = null;
   private java.util.concurrent.CountDownLatch latch = null;
 
   private AccessibleContext sidePanel;
+
+  // PCTB-specific variables
+  private int unknownPCTBcommods = 0;
   private HashMap<String,Integer> commodMap;
-  public PrintStream dtxt = null;
-  private int uploadcounter = 0;
+
+
+  /*****************************************
+   * UPLOAD-TARGET-INDEPENDENT CODE        *
+   *****************************************/
+
 
   /*
    * UTILITY METHODS AND SUBCLASSES
@@ -109,164 +127,18 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     strm.write(data);
     strm.close();
   }
-       
-  /**
-   *   An abstract market offer, entailing a commodity being bought or sold by
-   *   a shoppe, for a certain price in a certain quantity. Not instantiable.
-   *
-   *   @see Buy
-   *   @see Sell
-   */
-  abstract class Offer {
-    public int commodity, price, quantity, shoppe;
-    /**
-     * Create an offer from <code>record</code>, determining the shoppe Id from
-     * <code>stallMap</code> and the commodity Id from <code>commodMap</code>.
-     * <code>priceIndex</code> should be the index of the price in the record
-     * (the quantity will be <code>priceIndex + 1</code>).
-     *
-     * @param record the record with data to create the offer from
-     * @param stallMap a map containing the ids of the various stalls
-     * @param commodMap a map containing the ids of the various commodities
-     * @param priceIndex the index of the price in the record
-     */
-    public Offer(ArrayList<String> record, 
-                LinkedHashMap<String,Integer> stallMap, 
-                HashMap<String,Integer> commodMap,
-                int priceIndex) {
-      Integer commodId = commodMap.get(record.get(0));
-      if(commodId == null) {
-       throw new IllegalArgumentException();
-      }
-      commodity = commodId.intValue();
-      price = Integer.parseInt(record.get(priceIndex));
-      String qty = record.get(priceIndex+1);
-      quantity = parseQty(qty);
-      shoppe = stallMap.get(record.get(1)).intValue();
-    }
-               
-    /**
-     * Returns a human-readable version of this offer, useful for debugging
-     * 
-     * @return human-readable offer
-     */
-    public String toString() {
-      return "[C:" + commodity + ",$" + price + ",Q:"
-       + quantity + ",S:" + shoppe + "]";
-    }
-  }
-       
-  /**
-   *   An offer from a shoppe or stall to buy a certain quantity of a
-   *   commodity for a certain price. If placed in an ordered Set,
-   *   sorts by commodity index ascending, then by buy price
-   *   descending, and finally by stall id ascending.
-   */
-  class Buy extends Offer implements Comparable<Buy> {
-    /**
-     * Creates a new <code>Buy</code> offer from the given
-     * <code>record</code> using the other parameters to determine
-     * stall id and commodity id of the offer.
-     *
-     * @param record the record with data to create the offer from
-     * @param stallMap a map containing the ids of the various stalls
-     * @param commodMap a map containing the ids of the various commodities
-     */
-    public Buy(ArrayList<String> record,
-              LinkedHashMap<String,Integer> stallMap,
-              HashMap<String,Integer> commodMap) {
-      super(record,stallMap,commodMap,2);
-    }
-               
-    /**
-     * Sorts by commodity index ascending, then price descending,
-     * then stall id ascending.
-     */
-    public int compareTo(Buy buy) {
-      // organize by: commodity index, price, stall index
-      if(commodity == buy.commodity) {
-       // organize by price, then by stall index
-       if(price == buy.price) {
-         // organize by stall index
-         return shoppe>buy.shoppe ? 1 : -1;
-       } else if(price > buy.price) {
-         return -1;
-       } else {
-         return 1;
-       }
-      } else if(commodity > buy.commodity) {
-       return 1;
-      } else {
-       return -1;
-      }
-    }
-  }
-       
-  /**
-   *   An offer from a shoppe or stall to sell a certain quantity of
-   *   a commodity for a certain price. If placed in an ordered Set,
-   *   sorts by commodity index ascending, then by sell price
-   *   ascending, and finally by stall id ascending.
-   */
-  class Sell extends Offer implements Comparable<Sell> {
-    /**
-     * Creates a new <code>Sell</code> offer from the given
-     * <code>record</code> using the other parameters to determine
-     * stall id and commodity id of the offer.
-     *
-     * @param record the record with data to create the offer from
-     * @param stallMap a map containing the ids of the various stalls
-     * @param commodMap a map containing the ids of the various commodities
-     */
-    public Sell(ArrayList<String> record,
-               LinkedHashMap<String,Integer> stallMap,
-               HashMap<String,Integer> commodMap) {
-      super(record,stallMap,commodMap,4);
-    }
-               
-    /**
-     * Sorts by commodity index ascending, then price ascending, then
-     * stall id ascending.
-     */
-    public int compareTo(Sell sell) {
-      // organize by: commodity index, price, stall index
-      if(commodity == sell.commodity) {
-       // organize by price, then by stall index
-       if(price == sell.price) {
-         // organize by stall index
-         return shoppe>sell.shoppe ? 1 : -1;
-       } else if(price > sell.price) {
-         return 1;
-       } else {
-         return -1;
-       }
-      } else if(commodity > sell.commodity) {
-       return 1;
-      } else {
-       return -1;
-      }
-    }
-  }
 
-  private void progressNote(final String s_in) throws Exception {
-    new UIA() { public void body() {
-      String arb = null;
-      arb = arbitrageResult.getText();
-      String s = s_in;
-      if (arb != null && arb.length() != 0)
-       s = "<html>" + arb + "<br>" + s;
-      progmon.setNote(s);
-    }}.exec("progressNote "+s_in);
-  }
        
   /*
    * ENTRY POINT AND STARTUP
    *
    * Main thread and/or event thread
    */
-  /*
-   *   Entry point.  Read our preferences.
-   */
+
+  public static void main(String[] args) {
+    new MarketUploader();
+  }
+
   public MarketUploader() {
     Preferences prefs = Preferences.userNodeForPackage(getClass());
 
@@ -296,7 +168,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
 
   /*
    * We arrange to wait for the GUI to be initialised, then look at
-   * every top-level window, and if it
+   * every top-level window to see if the Puzzle Pirates window turns up.
    */
   public void run() {
     debuglog("MarketUploader run()...");
@@ -329,6 +201,7 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     String name = w.getAccessibleContext().getAccessibleName();
     debuglog("MarketUploader checking toplevel "+name);
     if (!name.equals("Puzzle Pirates"))
+      // Only if we're running alongside a Window named "Puzzle Pirates"
       return;
     debuglog("MarketUploader found toplevel, creating gui");
     window = w;
@@ -336,12 +209,8 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     frame.setVisible(true);
   }
        
-  /**
-   *   Set up the GUI, with its window and one-button
-   *   interface. Only initialize if we're running alongside
-   *   a Window named "Puzzle Pirates" though.
-   */
   private void createGUI() {
+    // Actually set up our GUI
     on_ui_thread();
     frame = new JFrame("Jarrg Uploader");
     frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
@@ -352,7 +221,9 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     findMarket = new JButton("Upload Market Data");
     findMarket.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
+         // This is called when the user clicks "upload"
          on_ui_thread();
+
          uploadcounter++;
          findMarket.setEnabled(false);
          resultSummary.setText("");
@@ -523,6 +394,16 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     error(whole_msg);
   }
 
+  private void progressNote(final String s_in) throws Exception {
+    new UIA() { public void body() {
+      String arb = null;
+      arb = arbitrageResult.getText();
+      String s = s_in;
+      if (arb != null && arb.length() != 0)
+       s = "<html>" + arb + "<br>" + s;
+      progmon.setNote(s);
+    }}.exec("progressNote "+s_in);
+  }
   private void setProgress(final int nv) throws Exception {
     new UIA() { public void body() {
       progmon.setProgress(nv);
@@ -535,118 +416,17 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
       return new Boolean(can);
     }}.exec("checkCancelled").booleanValue();
   }
-       
-  /*
-   * GUI MANIPULATION CALLBACKS
-   */
-
-  private PropertyChangeListener changeListener = new PropertyChangeListener() {
-    public void propertyChange(PropertyChangeEvent e) {
-      on_ui_thread();
-      if(e.getNewValue() != null && 
-        e.getPropertyName().equals
-        (AccessibleContext.ACCESSIBLE_CHILD_PROPERTY)) {
-       Accessible islandInfo =
-         descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0,0,1,2});;
-       String text = islandInfo.getAccessibleContext().getAccessibleText()
-         .getAtIndex(AccessibleText.SENTENCE,0);
-       int index = text.indexOf(":");
-       String name = text.substring(0,index);
-       islandName = name;
-       // debuglog(islandName);
-       sidePanel.removePropertyChangeListener(this);
-       latch.countDown();
-      }
-    }
-  };
-
-  private void getIsland() {
-    on_ui_thread();
-
-    // If the league tracker is there, we can skip the faff
-    // and ask for its tooltip, since we're on a boat
-
-    Accessible leagueTrackerContainer =
-      descendNodes(window,new int[] {0,1,0,0,2,1});
-    Accessible leagueTrackerItself =
-      descendByClass(leagueTrackerContainer,
-                    "com.threerings.yohoho.sea.client.LeagueTracker");
-    Accessible leagueTracker = descend(leagueTrackerItself, 1);
-    try {
-      islandName = ((JLabel)leagueTracker).getToolTipText();
-      latch = null;
-    } catch (NullPointerException e) {
-      // evidently we're actually on an island
-
-      islandName = null;
-      AccessibleContext chatArea =
-       descendNodes(window,new int[] {0,1,0,0,0,2,0,0,2})
-       .getAccessibleContext();
-      // attach the property change listener to the outer sunshine
-      // panel if the "ahoy" tab is not active, otherwise attach it to
-      // the scroll panel in the "ahoy" tab.
-      if(!"com.threerings.piracy.client.AttentionListPanel".
-        equals(descendNodes(window,new int[] {0,1,0,0,2,2,0})
-               .getClass().getCanonicalName())) {
-       sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2})
-         .getAccessibleContext();
-      } else {
-       sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0})
-         .getAccessibleContext();
-      }
-      sidePanel.addPropertyChangeListener(changeListener);
-      latch = new java.util.concurrent.CountDownLatch(1);
-      // make the Players Online ("/who") panel appear
-      AccessibleEditableText chat = chatArea.getAccessibleEditableText();
-      chat.setTextContents("/w");
-      int c = chatArea.getAccessibleAction().getAccessibleActionCount();
-      for(int i=0;i<c;i++) {
-       if("notify-field-accept".equals(chatArea.getAccessibleAction()
-                                       .getAccessibleActionDescription(i))) {
-         chatArea.getAccessibleAction().doAccessibleAction(i);
-       }
-      }
-    }
-  }
-
-  /**
-   *      Find the ocean name from the window title, and set global
-   *      oceanName variable
-   */
-  private void getOcean() {
-    on_ui_thread();
-    oceanName = null;
-    AccessibleContext topwindow = window.getAccessibleContext();
-    oceanName = topwindow.getAccessibleName()
-      .replaceAll(".*on the (\\w+) ocean", "$1");
-  }
 
 
-  /**
-   *   Run the data collection process, and upload the results. This
-   *   is the method that calls most of the other worker methods for
-   *   the process. If an error occurs, the method will call the
-   *   error method and return early, freeing up the button to be
-   *   clicked again.
-   *
-   *   @exception Exception if an error we didn't expect occured
+  /*
+   * ACTUAL DATA COLLECTION AND UPLOAD
    */
-  private class YarrgTimestampFetcher extends Thread {
-    public YarrgTimestampFetcher(int counter) {
-      super("MarketUploader-YarrgTimestampFetcher-"+uploadcounter);
-    }
-    public String ts = null;
-    public void run() {
-      try {
-       ts = getYarrgTimestamp();
-       debuglog("(async) yarrg timestamp ready.");
-      } catch(Exception e) {
-       error("Error getting YARRG timestamp: "+e);
-      }
-    }
-  };
 
   private void runUpload(int counter) throws Exception {
+    // Runs the data collection process, and upload the results.
+    // In most cases of error, we call error() (which synchronously
+    // reports the error) and then simply return.
+
     on_our_thread();
 
     boolean doneyarrg = false, donepctb = false;
@@ -834,104 +614,203 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
       if(checkCancelled()) {
        return;
       }
-      progressNote("PCTB: Waiting ...");
-      debuglog("pctb finish...");
-      donepctb = finishUpload(in);
-      debuglog("pctb done.");
-    }
-    setProgress(99);
-
-    String summary;
-    if ((uploadToPCTB && !donepctb) ||
-       (uploadToYarrg && !doneyarrg)) {
-      summary= "trouble";
-    } else if (unknownPCTBcommods != 0) {
-      summary= "PCTB lacks "+unknownPCTBcommods+" commod(s)";
-    } else if (donepctb || doneyarrg) {
-      summary= "Done " + islandName;
-    } else {
-      summary= "uploaded nowhere!";
+      progressNote("PCTB: Waiting ...");
+      debuglog("pctb finish...");
+      donepctb = finishUpload(in);
+      debuglog("pctb done.");
+    }
+    setProgress(99);
+
+    String summary;
+    if ((uploadToPCTB && !donepctb) ||
+       (uploadToYarrg && !doneyarrg)) {
+      summary= "trouble";
+    } else if (unknownPCTBcommods != 0) {
+      summary= "PCTB lacks "+unknownPCTBcommods+" commod(s)";
+    } else if (donepctb || doneyarrg) {
+      summary= "Done " + islandName;
+    } else {
+      summary= "uploaded nowhere!";
+    }
+    final String summary_final = summary;
+    new UIX() { public void body() {
+      resultSummary.setText(summary_final);
+    }}.exec("resultSummary.setText");
+
+    debuglog("done.");
+  }
+
+  /*
+   * UPLOAD HELPER FUNCTIONS FOR EXTRACTING SPECIFIC UI DATA
+   */
+       
+  private ArrayList<ArrayList<String>> getData(AccessibleTable table) {
+    // Gets the offer data out of the table and returns it as an ArrayList
+
+    on_ui_thread();
+    ArrayList<ArrayList<String>> data = new ArrayList<ArrayList<String>>();
+    for (int i = 0; i < table.getAccessibleRowCount(); i++) {
+      ArrayList<String> row = new ArrayList<String>();
+      for (int j = 0; j < table.getAccessibleColumnCount(); j++) {
+       row.add(table.getAccessibleAt(i, j)
+               .getAccessibleContext().getAccessibleName());
+      }
+      data.add(row);
+    }
+    return data;
+  }
+       
+  public AccessibleTable findMarketTable() {
+    // Return the table containing market data if it exists, otherwise null.
+    on_ui_thread();
+
+    Accessible node1 = window;
+    Accessible node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,0,1,0,0}); 
+      // commod market
+    // commod market: {0,1,0,0,0,0,1,0,0,1,0}  {0,1,0,0,0,0,1,0,1,0,0,1,0,0})
+    // debuglog(node);
+    if (!(node instanceof JTable)) {
+      node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,1,0,0,1,0,0});
+        // commod market
+    }
+    if (!(node instanceof JTable)) return null;
+    AccessibleTable table = node.getAccessibleContext().getAccessibleTable();
+    // debuglog(table);
+    return table;
+  }
+       
+  private boolean isDisplayAll() {
+    // Returns true iff the "Display:" menu on the commodities
+    // interface in YPP is set to "All"
+    on_ui_thread();
+
+    Accessible button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,0,0,1});
+    if(!(button instanceof JButton)) {
+      button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,1,0,0,0,1});
+    }
+    String display = button.getAccessibleContext().getAccessibleName();
+    if(!display.equals("All")) {
+      return false;
+    }
+    return true;
+  }
+       
+  /*
+   * FUNCTIONS AND CALLBACKS FOR FINDING ISLAND AND OCEAN
+   */
+
+  private void getOcean() {
+    // Finds the ocean name from the window title.
+    // Stores it in the global oceanName
+    on_ui_thread();
+
+    oceanName = null;
+    AccessibleContext topwindow = window.getAccessibleContext();
+    oceanName = topwindow.getAccessibleName()
+      .replaceAll(".*on the (\\w+) ocean", "$1");
+  }
+
+  private void getIsland() {
+    // Tries to find the island name.  Either:
+    //  (a) sets the islandName global
+    // or
+    //  (b) sets latch to a new CountDownLatch, and arranges that 
+    //      at some point later, islandName will be set and the latch
+    //      decremented to zero
+    on_ui_thread();
+
+    // If the league tracker is there, we can skip the faff
+    // and ask for its tooltip, since we're on a boat
+
+    Accessible leagueTrackerContainer =
+      descendNodes(window,new int[] {0,1,0,0,2,1});
+    Accessible leagueTrackerItself =
+      descendByClass(leagueTrackerContainer,
+                    "com.threerings.yohoho.sea.client.LeagueTracker");
+    Accessible leagueTracker = descend(leagueTrackerItself, 1);
+    try {
+      islandName = ((JLabel)leagueTracker).getToolTipText();
+      latch = null;
+    } catch (NullPointerException e) {
+      // evidently we're actually on an island
+
+      islandName = null;
+      AccessibleContext chatArea =
+       descendNodes(window,new int[] {0,1,0,0,0,2,0,0,2})
+       .getAccessibleContext();
+      // attach the property change listener to the outer sunshine
+      // panel if the "ahoy" tab is not active, otherwise attach it to
+      // the scroll panel in the "ahoy" tab.
+      if(!"com.threerings.piracy.client.AttentionListPanel".
+        equals(descendNodes(window,new int[] {0,1,0,0,2,2,0})
+               .getClass().getCanonicalName())) {
+       sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2})
+         .getAccessibleContext();
+      } else {
+       sidePanel = descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0})
+         .getAccessibleContext();
+      }
+      sidePanel.addPropertyChangeListener(changeListener);
+      latch = new java.util.concurrent.CountDownLatch(1);
+      // make the Players Online ("/who") panel appear
+      AccessibleEditableText chat = chatArea.getAccessibleEditableText();
+      chat.setTextContents("/w");
+      int c = chatArea.getAccessibleAction().getAccessibleActionCount();
+      for(int i=0;i<c;i++) {
+       if("notify-field-accept".equals(chatArea.getAccessibleAction()
+                                       .getAccessibleActionDescription(i))) {
+         chatArea.getAccessibleAction().doAccessibleAction(i);
+       }
+      }
     }
-    final String summary_final = summary;
-    new UIX() { public void body() {
-      resultSummary.setText(summary_final);
-    }}.exec("resultSummary.setText");
-
-    debuglog("done.");
   }
-       
-  /**
-   *   Get the offer data out of the table and cache it in an
-   *   <code>ArrayList</code>.
-   *   
-   *   @param table the <code>AccessibleTable</code> containing the market data
-   *   @return an array of record arrays, each representing a row of the table
-   */
-  private ArrayList<ArrayList<String>> getData(AccessibleTable table) {
-    on_ui_thread();
-    ArrayList<ArrayList<String>> data = new ArrayList<ArrayList<String>>();
-    for (int i = 0; i < table.getAccessibleRowCount(); i++) {
-      ArrayList<String> row = new ArrayList<String>();
-      for (int j = 0; j < table.getAccessibleColumnCount(); j++) {
-       row.add(table.getAccessibleAt(i, j)
-               .getAccessibleContext().getAccessibleName());
+
+  private PropertyChangeListener changeListener = new PropertyChangeListener() {
+    // used by getIsland
+    public void propertyChange(PropertyChangeEvent e) {
+      on_ui_thread();
+      if(e.getNewValue() != null && 
+        e.getPropertyName().equals
+        (AccessibleContext.ACCESSIBLE_CHILD_PROPERTY)) {
+       Accessible islandInfo =
+         descendNodes(window,new int[] {0,1,0,0,2,2,0,0,0,0,1,2});;
+       String text = islandInfo.getAccessibleContext().getAccessibleText()
+         .getAtIndex(AccessibleText.SENTENCE,0);
+       int index = text.indexOf(":");
+       String name = text.substring(0,index);
+       islandName = name;
+       // debuglog(islandName);
+       sidePanel.removePropertyChangeListener(this);
+       latch.countDown();
       }
-      data.add(row);
-    }
-    return data;
-  }
-       
-  /**
-   *   @return the table containing market data if it exists,
-   *   otherwise <code>null</code>
-   */
-  public AccessibleTable findMarketTable() {
-    on_ui_thread();
-    Accessible node1 = window;
-    Accessible node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,0,1,0,0}); 
-      // commod market
-    // commod market: {0,1,0,0,0,0,1,0,0,1,0}  {0,1,0,0,0,0,1,0,1,0,0,1,0,0})
-    // debuglog(node);
-    if (!(node instanceof JTable)) {
-      node = descendNodes(node1,new int[] {0,1,0,0,0,0,1,0,1,0,0,1,0,0});
-        // commod market
     }
-    if (!(node instanceof JTable)) return null;
-    AccessibleTable table = node.getAccessibleContext().getAccessibleTable();
-    // debuglog(table);
-    return table;
-  }
-       
-  /**
-   *   Utility method to descend through several levels of Accessible children
-   *   at once.
+  };
+
+  /*
+   * UTILITY FUNCTIONS FOR WALKING THE UI
    *
-   *   @param parent the node on which to start the descent
-   *   @param path an array of ints, each int being the index of the next
-   *   accessible child to descend.
-   *   @return the <code>Accessible</code> reached by following the
-   *   descent path, or <code>null</code> if the desired path was
-   *   invalid.
+   * These functions all return null if the specified path or child
+   * was not found.
    */
+
   private Accessible descendNodes(Accessible parent, int[] path) {
+    // Descends through several levels of Accessible children in one call.
+    // path[] is an array of ints, each int being the index into the array
+    // of children at a particular point, and thus selects the specific
+    // accessible child to descend to.
     on_ui_thread();
+
     for(int i=0;i<path.length;i++) {
       if (null == (parent = descend(parent, path[i]))) return null;
     }
     return parent;
   }
-       
-  /**
-   *   Descends one level to the specified child of the parent
-   *   <code>Accessible</code> "node".
-   *   
-   *   @param parent the node with children
-   *   @param childNum the index of the child of <code>parent</code> to return
-   *   @return the <code>childNum</code> child of <code>parent</code>
-   *   or <code>null</code> if the child is not found.
-   */
+
   private Accessible descend(Accessible parent, int childNum) {
+    // Descends one level to the specified child of the parent
+    // childNum is the index of the child within parent
     on_ui_thread();
+
     if (parent == null) return null;
     int children = parent.getAccessibleContext().getAccessibleChildrenCount();
     if (childNum >= children) {
@@ -944,15 +823,10 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     return child;
   }
 
-  /**
-   *   Descends one level to the child which has the specified class.
-   *   
-   *   @param parent the node with children
-   *   @param classname the name of the class, as a string
-   *   @return the child or <code>null</code> if the child is not found.
-   */
   private Accessible descendByClass(Accessible parent, String classname) {
+    // Descends one level to the first child which has the specified class.
     on_ui_thread();
+
     if (parent == null) return null;
     AccessibleContext ac = parent.getAccessibleContext();
     int children = ac.getAccessibleChildrenCount();
@@ -966,31 +840,150 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     debuglog("DESCEND CLASS "+classname+" NOT FOUND");
     return null;
   }
+  
 
-  public static void main(String[] args) {
-    new MarketUploader();
-  }
+  /*****************************************
+   * PCTB-SPECIFIC HELPER FUNCTIONS ETC.   *
+   *****************************************/
 
   /**
-   *   Returns true if the "Display:" menu on the commodities
-   *   interface in YPP is set to "All"
+   *   An abstract market offer, entailing a commodity being bought or sold by
+   *   a shoppe, for a certain price in a certain quantity. Not instantiable.
    *
-   *   @return <code>true</code> if all commodities are displayed,
-   *   otherwise <code>false</code>
+   *   @see Buy
+   *   @see Sell
    */
-  private boolean isDisplayAll() {
-    on_ui_thread();
-    Accessible button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,0,0,1});
-    if(!(button instanceof JButton)) {
-      button = descendNodes(window,new int[] {0,1,0,0,0,0,1,0,1,0,0,0,1});
+  abstract class Offer {
+    public int commodity, price, quantity, shoppe;
+    /**
+     * Create an offer from <code>record</code>, determining the shoppe Id from
+     * <code>stallMap</code> and the commodity Id from <code>commodMap</code>.
+     * <code>priceIndex</code> should be the index of the price in the record
+     * (the quantity will be <code>priceIndex + 1</code>).
+     *
+     * @param record the record with data to create the offer from
+     * @param stallMap a map containing the ids of the various stalls
+     * @param commodMap a map containing the ids of the various commodities
+     * @param priceIndex the index of the price in the record
+     */
+    public Offer(ArrayList<String> record, 
+                LinkedHashMap<String,Integer> stallMap, 
+                HashMap<String,Integer> commodMap,
+                int priceIndex) {
+      Integer commodId = commodMap.get(record.get(0));
+      if(commodId == null) {
+       throw new IllegalArgumentException();
+      }
+      commodity = commodId.intValue();
+      price = Integer.parseInt(record.get(priceIndex));
+      String qty = record.get(priceIndex+1);
+      quantity = parseQty(qty);
+      shoppe = stallMap.get(record.get(1)).intValue();
     }
-    String display = button.getAccessibleContext().getAccessibleName();
-    if(!display.equals("All")) {
-      return false;
+               
+    /**
+     * Returns a human-readable version of this offer, useful for debugging
+     * 
+     * @return human-readable offer
+     */
+    public String toString() {
+      return "[C:" + commodity + ",$" + price + ",Q:"
+       + quantity + ",S:" + shoppe + "]";
+    }
+  }
+       
+  /**
+   *   An offer from a shoppe or stall to buy a certain quantity of a
+   *   commodity for a certain price. If placed in an ordered Set,
+   *   sorts by commodity index ascending, then by buy price
+   *   descending, and finally by stall id ascending.
+   */
+  class Buy extends Offer implements Comparable<Buy> {
+    /**
+     * Creates a new <code>Buy</code> offer from the given
+     * <code>record</code> using the other parameters to determine
+     * stall id and commodity id of the offer.
+     *
+     * @param record the record with data to create the offer from
+     * @param stallMap a map containing the ids of the various stalls
+     * @param commodMap a map containing the ids of the various commodities
+     */
+    public Buy(ArrayList<String> record,
+              LinkedHashMap<String,Integer> stallMap,
+              HashMap<String,Integer> commodMap) {
+      super(record,stallMap,commodMap,2);
+    }
+               
+    /**
+     * Sorts by commodity index ascending, then price descending,
+     * then stall id ascending.
+     */
+    public int compareTo(Buy buy) {
+      // organize by: commodity index, price, stall index
+      if(commodity == buy.commodity) {
+       // organize by price, then by stall index
+       if(price == buy.price) {
+         // organize by stall index
+         return shoppe>buy.shoppe ? 1 : -1;
+       } else if(price > buy.price) {
+         return -1;
+       } else {
+         return 1;
+       }
+      } else if(commodity > buy.commodity) {
+       return 1;
+      } else {
+       return -1;
+      }
     }
-    return true;
   }
        
+  /**
+   *   An offer from a shoppe or stall to sell a certain quantity of
+   *   a commodity for a certain price. If placed in an ordered Set,
+   *   sorts by commodity index ascending, then by sell price
+   *   ascending, and finally by stall id ascending.
+   */
+  class Sell extends Offer implements Comparable<Sell> {
+    /**
+     * Creates a new <code>Sell</code> offer from the given
+     * <code>record</code> using the other parameters to determine
+     * stall id and commodity id of the offer.
+     *
+     * @param record the record with data to create the offer from
+     * @param stallMap a map containing the ids of the various stalls
+     * @param commodMap a map containing the ids of the various commodities
+     */
+    public Sell(ArrayList<String> record,
+               LinkedHashMap<String,Integer> stallMap,
+               HashMap<String,Integer> commodMap) {
+      super(record,stallMap,commodMap,4);
+    }
+               
+    /**
+     * Sorts by commodity index ascending, then price ascending, then
+     * stall id ascending.
+     */
+    public int compareTo(Sell sell) {
+      // organize by: commodity index, price, stall index
+      if(commodity == sell.commodity) {
+       // organize by price, then by stall index
+       if(price == sell.price) {
+         // organize by stall index
+         return shoppe>sell.shoppe ? 1 : -1;
+       } else if(price > sell.price) {
+         return 1;
+       } else {
+         return -1;
+       }
+      } else if(commodity > sell.commodity) {
+       return 1;
+      } else {
+       return -1;
+      }
+    }
+  }
+
   /**
    *   Gets the list of commodities and their associated commodity ids.
    *
@@ -1340,6 +1333,11 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     }
   }
 
+
+  /*****************************************
+   * YARRG-SPECIFIC HELPER FUNCTIONS ETC.  *
+   *****************************************/
+
   private InputStream post_for_yarrg(ClientHttpRequest http)
   throws IOException {
     on_our_thread();
@@ -1351,6 +1349,21 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     return http.resultstream();
   }
 
+  private class YarrgTimestampFetcher extends Thread {
+    public YarrgTimestampFetcher(int counter) {
+      super("MarketUploader-YarrgTimestampFetcher-"+uploadcounter);
+    }
+    public String ts = null;
+    public void run() {
+      try {
+       ts = getYarrgTimestamp();
+       debuglog("(async) yarrg timestamp ready.");
+      } catch(Exception e) {
+       error("Error getting YARRG timestamp: "+e);
+      }
+    }
+  };
+
   private String getYarrgTimestamp() throws IOException {
     ClientHttpRequest http = new ClientHttpRequest (YARRG_URL);
     http.setParameter("clientname", YARRG_CLIENTNAME);
@@ -1429,6 +1442,10 @@ implements Runnable, TopLevelWindowListener, GUIInitializedListener {
     return profit;
   }
 
+  /*****************************************
+   * ARBITRAGE                             *
+   *****************************************/
+
   private class arbitrageOfferComparator implements Comparator {
     public int compare(Object o1, Object o2) {
       int p1 = ((int[])o1)[0];