private void reduceTree(int numColors) {
    for (int level = MAX_LEVEL - 1; level >= 0; level--) {
      Vector v = colorList[level];
      if (v != null && v.size() > 0) {
        for (int j = 0; j < v.size(); j++) {
          OctTreeNode node = (OctTreeNode) v.elementAt(j);
          if (node.children > 0) {
            for (int i = 0; i < 8; i++) {
              OctTreeNode child = node.leaf[i];
              if (child != null) {
                if (!child.isLeaf) LOGGER.debug("not a leaf!");
                node.count += child.count;
                node.totalRed += child.totalRed;
                node.totalGreen += child.totalGreen;
                node.totalBlue += child.totalBlue;
                node.leaf[i] = null;
                node.children--;
                colors--;
                nodes--;
                colorList[level + 1].removeElement(child);
              }
            }
            node.isLeaf = true;
            colors++;
            if (colors <= numColors) return;
          }
        }
      }
    }

    LOGGER.debug("Unable to reduce the OctTree");
  }
Esempio n. 2
0
  /**
   * Load the STUN configuration from a stream.
   *
   * @param stunConfigStream An InputStream with the configuration file.
   * @return A list of loaded servers
   */
  public ArrayList loadSTUNServers(java.io.InputStream stunConfigStream) {
    final ArrayList serversList = new ArrayList();
    String serverName;
    int serverPort;

    try {
      final XmlPullParser parser = new MXParser();
      parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
      parser.setInput(stunConfigStream, "UTF-8");

      int eventType = parser.getEventType();
      do {
        if (eventType == XmlPullParser.START_TAG) {

          // Parse a STUN server definition
          if (parser.getName().equals("stunServer")) {

            serverName = null;
            serverPort = -1;

            // Parse the hostname
            parser.next();
            parser.next();
            serverName = parser.nextText();

            // Parse the port
            parser.next();
            parser.next();
            try {
              serverPort = Integer.parseInt(parser.nextText());
            } catch (final Exception e) {
            }

            // If we have a valid hostname and port, add
            // it to the list.
            if (serverName != null && serverPort != -1) {
              final STUNService service = new STUNService(serverName, serverPort);

              serversList.add(service);
            }
          }
        }
        eventType = parser.next();

      } while (eventType != XmlPullParser.END_DOCUMENT);

    } catch (final XmlPullParserException e) {
      LOGGER.error(e.getMessage(), e);
    } catch (final IOException e) {
      LOGGER.error(e.getMessage(), e);
    }

    currentServer = bestSTUNServer(serversList);

    return serversList;
  }
Esempio n. 3
0
  /**
   * Load a list of services: STUN servers and ports. Some public STUN servers are:
   *
   * <p>
   *
   * <pre>
   *               iphone-stun.freenet.de:3478
   *               larry.gloo.net:3478
   *               stun.xten.net:3478
   *               stun.fwdnet.net
   *               stun.fwd.org (no DNS SRV record)
   *               stun01.sipphone.com (no DNS SRV record)
   *               stun.softjoys.com (no DNS SRV record)
   *               stun.voipbuster.com (no DNS SRV record)
   *               stun.voxgratia.org (no DNS SRV record)
   *               stun.noc.ams-ix.net
   * </pre>
   *
   * <p>This list should be contained in a file in the "META-INF" directory
   *
   * @return a list of services
   */
  public ArrayList loadSTUNServers() {
    final ArrayList serversList = new ArrayList();

    // Load the STUN configuration
    try {
      // Get an array of class loaders to try loading the config from.
      final ClassLoader[] classLoaders = new ClassLoader[2];
      classLoaders[0] = new STUNResolver() {}.getClass().getClassLoader();
      classLoaders[1] = Thread.currentThread().getContextClassLoader();

      for (final ClassLoader classLoader : classLoaders) {
        final Enumeration stunConfigEnum = classLoader.getResources(STUNSERVERS_FILENAME);

        while (stunConfigEnum.hasMoreElements() && serversList.isEmpty()) {
          final URL url = (URL) stunConfigEnum.nextElement();
          java.io.InputStream stunConfigStream = null;

          stunConfigStream = url.openStream();
          serversList.addAll(loadSTUNServers(stunConfigStream));
          stunConfigStream.close();
        }
      }
    } catch (final Exception e) {
      LOGGER.error(e.getMessage(), e);
    }

    return serversList;
  }
  /**
   * Get the color table index for a color.
   *
   * @param rgb the color
   * @return the index
   */
  public int getIndexForColor(int rgb) {
    int red = (rgb >> 16) & 0xff;
    int green = (rgb >> 8) & 0xff;
    int blue = rgb & 0xff;

    OctTreeNode node = root;

    for (int level = 0; level <= MAX_LEVEL; level++) {
      OctTreeNode child;
      int bit = 0x80 >> level;

      int index = 0;
      if ((red & bit) != 0) index += 4;
      if ((green & bit) != 0) index += 2;
      if ((blue & bit) != 0) index += 1;

      child = node.leaf[index];

      if (child == null) return node.index;
      else if (child.isLeaf) return child.index;
      else node = child;
    }
    LOGGER.debug("getIndexForColor failed");
    return 0;
  }
  private void insertColor(int rgb) {
    int red = (rgb >> 16) & 0xff;
    int green = (rgb >> 8) & 0xff;
    int blue = rgb & 0xff;

    OctTreeNode node = root;

    //		LOGGER.debug("insertColor="+Integer.toHexString(rgb));
    for (int level = 0; level <= MAX_LEVEL; level++) {
      OctTreeNode child;
      int bit = 0x80 >> level;

      int index = 0;
      if ((red & bit) != 0) index += 4;
      if ((green & bit) != 0) index += 2;
      if ((blue & bit) != 0) index += 1;

      child = node.leaf[index];

      if (child == null) {
        node.children++;

        child = new OctTreeNode();
        child.parent = node;
        node.leaf[index] = child;
        node.isLeaf = false;
        nodes++;
        colorList[level].addElement(child);

        if (level == MAX_LEVEL) {
          child.isLeaf = true;
          child.count = 1;
          child.totalRed = red;
          child.totalGreen = green;
          child.totalBlue = blue;
          child.level = level;
          colors++;
          return;
        }

        node = child;
      } else if (child.isLeaf) {
        child.count++;
        child.totalRed += red;
        child.totalGreen += green;
        child.totalBlue += blue;
        return;
      } else node = child;
    }
    LOGGER.debug("insertColor failed");
  }
Esempio n. 6
0
  /** Resolve the IP and obtain a valid transport method. */
  @Override
  public synchronized void resolve(JingleSession session) throws XMPPException {

    setResolveInit();

    clearCandidates();

    final TransportCandidate candidate =
        new TransportCandidate.Fixed(resolvedPublicIP, getFreePort());
    candidate.setLocalIp(resolvedLocalIP);

    LOGGER.debug("RESOLVING : " + resolvedPublicIP + ":" + candidate.getPort());

    addCandidate(candidate);

    setResolveEnd();
  }
Esempio n. 7
0
/**
 * Transport resolver using the JSTUN library, to discover public IP and use it as a candidate.
 *
 * <p>The goal of this resolver is to take possible to establish and manage out-of-band connections
 * between two XMPP entities, even if they are behind Network Address Translators (NATs) or
 * firewalls.
 *
 * @author Thiago Camargo
 */
public class STUNResolver extends TransportResolver {

  /** STUN service definition. */
  protected class STUNService {

    private String hostname; // The hostname of the service

    private int port; // The port number

    /** Default constructor, without name and port. */
    public STUNService() {
      this(null, -1);
    }

    /**
     * Basic constructor, with the hostname and port
     *
     * @param hostname The hostname
     * @param port The port
     */
    public STUNService(String hostname, int port) {
      super();

      this.hostname = hostname;
      this.port = port;
    }

    /**
     * Check a binding with the STUN currentServer.
     *
     * <p>Note: this function blocks for some time, waiting for a response.
     *
     * @return true if the currentServer is usable.
     */
    public boolean checkBinding() {
      final boolean result = false;

      try {
        final BindingLifetimeTest binding = new BindingLifetimeTest(hostname, port);

        binding.test();

        while (true) {
          Thread.sleep(5000);
          if (binding.getLifetime() != -1) {
            if (binding.isCompleted()) {
              return true;
            }
          } else {
            break;
          }
        }
      } catch (final Exception e) {
        LOGGER.error(e.getMessage(), e);
      }

      return result;
    }

    /**
     * Get the host name of the STUN service.
     *
     * @return The host name
     */
    public String getHostname() {
      return hostname;
    }

    /**
     * Get the port of the STUN service
     *
     * @return The port number where the STUN server is waiting.
     */
    public int getPort() {
      return port;
    }

    /**
     * Basic format test: the service is not null.
     *
     * @return true if the hostname and port are null
     */
    public boolean isNull() {
      if (hostname == null) {
        return true;
      } else if (hostname.length() == 0) {
        return true;
      } else if (port < 0) {
        return true;
      } else {
        return false;
      }
    }

    /**
     * Set the hostname of the STUN service.
     *
     * @param hostname The host name of the service.
     */
    public void setHostname(String hostname) {
      this.hostname = hostname;
    }

    /**
     * Set the port number for the STUN service.
     *
     * @param port The port number.
     */
    public void setPort(int port) {
      this.port = port;
    }
  }

  private static final SmackLogger LOGGER = SmackLogger.getLogger(STUNResolver.class);

  // The filename where the STUN servers are stored.
  public static final String STUNSERVERS_FILENAME = "META-INF/stun-config.xml";

  // Current STUN server we are using
  protected STUNService currentServer;

  protected Thread resolverThread;

  protected int defaultPort;
  protected String resolvedPublicIP;

  protected String resolvedLocalIP;

  /** Constructor with default STUN server. */
  public STUNResolver() {
    super();

    defaultPort = 0;
    currentServer = new STUNService();
  }

  /**
   * Constructor with a default port.
   *
   * @param defaultPort Port to use by default.
   */
  public STUNResolver(int defaultPort) {
    this();

    this.defaultPort = defaultPort;
  }

  /**
   * Get the best usable STUN server from a list.
   *
   * @return the best STUN server that can be used.
   */
  private STUNService bestSTUNServer(ArrayList listServers) {
    if (listServers.isEmpty()) {
      return null;
    } else {
      // TODO: this should use some more advanced criteria...
      return (STUNService) listServers.get(0);
    }
  }

  /**
   * Cancel any operation.
   *
   * @see TransportResolver#cancel()
   */
  @Override
  public synchronized void cancel() throws XMPPException {
    if (isResolving()) {
      resolverThread.interrupt();
      setResolveEnd();
    }
  }

  /**
   * Clear the list of candidates and start the resolution again.
   *
   * @see TransportResolver#clear()
   */
  @Override
  public synchronized void clear() throws XMPPException {
    defaultPort = 0;
    super.clear();
  }

  /**
   * Get the name of the current STUN server.
   *
   * @return the name of the STUN server
   */
  public String getCurrentServerName() {
    if (!currentServer.isNull()) {
      return currentServer.getHostname();
    } else {
      return null;
    }
  }

  /**
   * Get the port of the current STUN server.
   *
   * @return the port of the STUN server
   */
  public int getCurrentServerPort() {
    if (!currentServer.isNull()) {
      return currentServer.getPort();
    } else {
      return 0;
    }
  }

  /**
   * Initialize the resolver.
   *
   * @throws XMPPException
   */
  @Override
  public void initialize() throws XMPPException {
    LOGGER.debug("Initialized");
    if (!isResolving() && !isResolved()) {
      // Get the best STUN server available
      if (currentServer.isNull()) {
        loadSTUNServers();
      }
      // We should have a valid STUN server by now...
      if (!currentServer.isNull()) {

        clearCandidates();

        resolverThread =
            new Thread(
                new Runnable() {
                  @Override
                  public void run() {
                    // Iterate through the list of interfaces, and ask
                    // to the STUN server for our address.
                    try {
                      final Enumeration ifaces = NetworkInterface.getNetworkInterfaces();
                      String candAddress;
                      int candPort;

                      while (ifaces.hasMoreElements()) {

                        final NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
                        final Enumeration iaddresses = iface.getInetAddresses();

                        while (iaddresses.hasMoreElements()) {
                          final InetAddress iaddress = (InetAddress) iaddresses.nextElement();
                          if (!iaddress.isLoopbackAddress() && !iaddress.isLinkLocalAddress()) {

                            // Reset the candidate
                            candAddress = null;
                            candPort = -1;

                            final DiscoveryTest test =
                                new DiscoveryTest(
                                    iaddress, currentServer.getHostname(), currentServer.getPort());
                            try {
                              // Run the tests and get the
                              // discovery
                              // information, where all the
                              // info is stored...
                              final DiscoveryInfo di = test.test();

                              candAddress =
                                  di.getPublicIP() != null
                                      ? di.getPublicIP().getHostAddress()
                                      : null;

                              // Get a valid port
                              if (defaultPort == 0) {
                                candPort = getFreePort();
                              } else {
                                candPort = defaultPort;
                              }

                              // If we have a valid candidate,
                              // add it to the list.
                              if (candAddress != null && candPort >= 0) {
                                final TransportCandidate candidate =
                                    new TransportCandidate.Fixed(candAddress, candPort);
                                candidate.setLocalIp(
                                    iaddress.getHostAddress() != null
                                        ? iaddress.getHostAddress()
                                        : iaddress.getHostName());
                                addCandidate(candidate);

                                resolvedPublicIP = candidate.getIp();
                                resolvedLocalIP = candidate.getLocalIp();
                                return;
                              }
                            } catch (final Exception e) {
                              LOGGER.error(e.getMessage(), e);
                            }
                          }
                        }
                      }
                    } catch (final SocketException e) {
                      LOGGER.error(e.getMessage(), e);
                    } finally {
                      setInitialized();
                    }
                  }
                },
                "Waiting for all the transport candidates checks...");

        resolverThread.setName("STUN resolver");
        resolverThread.start();
      } else {
        throw new IllegalStateException("No valid STUN server found.");
      }
    }
  }

  /**
   * Return true if the service is working.
   *
   * @see TransportResolver#isResolving()
   */
  @Override
  public boolean isResolving() {
    return super.isResolving() && resolverThread != null;
  }

  /**
   * Load a list of services: STUN servers and ports. Some public STUN servers are:
   *
   * <p>
   *
   * <pre>
   *               iphone-stun.freenet.de:3478
   *               larry.gloo.net:3478
   *               stun.xten.net:3478
   *               stun.fwdnet.net
   *               stun.fwd.org (no DNS SRV record)
   *               stun01.sipphone.com (no DNS SRV record)
   *               stun.softjoys.com (no DNS SRV record)
   *               stun.voipbuster.com (no DNS SRV record)
   *               stun.voxgratia.org (no DNS SRV record)
   *               stun.noc.ams-ix.net
   * </pre>
   *
   * <p>This list should be contained in a file in the "META-INF" directory
   *
   * @return a list of services
   */
  public ArrayList loadSTUNServers() {
    final ArrayList serversList = new ArrayList();

    // Load the STUN configuration
    try {
      // Get an array of class loaders to try loading the config from.
      final ClassLoader[] classLoaders = new ClassLoader[2];
      classLoaders[0] = new STUNResolver() {}.getClass().getClassLoader();
      classLoaders[1] = Thread.currentThread().getContextClassLoader();

      for (final ClassLoader classLoader : classLoaders) {
        final Enumeration stunConfigEnum = classLoader.getResources(STUNSERVERS_FILENAME);

        while (stunConfigEnum.hasMoreElements() && serversList.isEmpty()) {
          final URL url = (URL) stunConfigEnum.nextElement();
          java.io.InputStream stunConfigStream = null;

          stunConfigStream = url.openStream();
          serversList.addAll(loadSTUNServers(stunConfigStream));
          stunConfigStream.close();
        }
      }
    } catch (final Exception e) {
      LOGGER.error(e.getMessage(), e);
    }

    return serversList;
  }

  /**
   * Load the STUN configuration from a stream.
   *
   * @param stunConfigStream An InputStream with the configuration file.
   * @return A list of loaded servers
   */
  public ArrayList loadSTUNServers(java.io.InputStream stunConfigStream) {
    final ArrayList serversList = new ArrayList();
    String serverName;
    int serverPort;

    try {
      final XmlPullParser parser = new MXParser();
      parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
      parser.setInput(stunConfigStream, "UTF-8");

      int eventType = parser.getEventType();
      do {
        if (eventType == XmlPullParser.START_TAG) {

          // Parse a STUN server definition
          if (parser.getName().equals("stunServer")) {

            serverName = null;
            serverPort = -1;

            // Parse the hostname
            parser.next();
            parser.next();
            serverName = parser.nextText();

            // Parse the port
            parser.next();
            parser.next();
            try {
              serverPort = Integer.parseInt(parser.nextText());
            } catch (final Exception e) {
            }

            // If we have a valid hostname and port, add
            // it to the list.
            if (serverName != null && serverPort != -1) {
              final STUNService service = new STUNService(serverName, serverPort);

              serversList.add(service);
            }
          }
        }
        eventType = parser.next();

      } while (eventType != XmlPullParser.END_DOCUMENT);

    } catch (final XmlPullParserException e) {
      LOGGER.error(e.getMessage(), e);
    } catch (final IOException e) {
      LOGGER.error(e.getMessage(), e);
    }

    currentServer = bestSTUNServer(serversList);

    return serversList;
  }

  /** Resolve the IP and obtain a valid transport method. */
  @Override
  public synchronized void resolve(JingleSession session) throws XMPPException {

    setResolveInit();

    clearCandidates();

    final TransportCandidate candidate =
        new TransportCandidate.Fixed(resolvedPublicIP, getFreePort());
    candidate.setLocalIp(resolvedLocalIP);

    LOGGER.debug("RESOLVING : " + resolvedPublicIP + ":" + candidate.getPort());

    addCandidate(candidate);

    setResolveEnd();
  }

  /**
   * Set the STUN server name and port
   *
   * @param ip the STUN server name
   * @param port the STUN server port
   */
  public void setSTUNService(String ip, int port) {
    currentServer = new STUNService(ip, port);
  }
}
Esempio n. 8
0
  /**
   * Initialize the resolver.
   *
   * @throws XMPPException
   */
  @Override
  public void initialize() throws XMPPException {
    LOGGER.debug("Initialized");
    if (!isResolving() && !isResolved()) {
      // Get the best STUN server available
      if (currentServer.isNull()) {
        loadSTUNServers();
      }
      // We should have a valid STUN server by now...
      if (!currentServer.isNull()) {

        clearCandidates();

        resolverThread =
            new Thread(
                new Runnable() {
                  @Override
                  public void run() {
                    // Iterate through the list of interfaces, and ask
                    // to the STUN server for our address.
                    try {
                      final Enumeration ifaces = NetworkInterface.getNetworkInterfaces();
                      String candAddress;
                      int candPort;

                      while (ifaces.hasMoreElements()) {

                        final NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
                        final Enumeration iaddresses = iface.getInetAddresses();

                        while (iaddresses.hasMoreElements()) {
                          final InetAddress iaddress = (InetAddress) iaddresses.nextElement();
                          if (!iaddress.isLoopbackAddress() && !iaddress.isLinkLocalAddress()) {

                            // Reset the candidate
                            candAddress = null;
                            candPort = -1;

                            final DiscoveryTest test =
                                new DiscoveryTest(
                                    iaddress, currentServer.getHostname(), currentServer.getPort());
                            try {
                              // Run the tests and get the
                              // discovery
                              // information, where all the
                              // info is stored...
                              final DiscoveryInfo di = test.test();

                              candAddress =
                                  di.getPublicIP() != null
                                      ? di.getPublicIP().getHostAddress()
                                      : null;

                              // Get a valid port
                              if (defaultPort == 0) {
                                candPort = getFreePort();
                              } else {
                                candPort = defaultPort;
                              }

                              // If we have a valid candidate,
                              // add it to the list.
                              if (candAddress != null && candPort >= 0) {
                                final TransportCandidate candidate =
                                    new TransportCandidate.Fixed(candAddress, candPort);
                                candidate.setLocalIp(
                                    iaddress.getHostAddress() != null
                                        ? iaddress.getHostAddress()
                                        : iaddress.getHostName());
                                addCandidate(candidate);

                                resolvedPublicIP = candidate.getIp();
                                resolvedLocalIP = candidate.getLocalIp();
                                return;
                              }
                            } catch (final Exception e) {
                              LOGGER.error(e.getMessage(), e);
                            }
                          }
                        }
                      }
                    } catch (final SocketException e) {
                      LOGGER.error(e.getMessage(), e);
                    } finally {
                      setInitialized();
                    }
                  }
                },
                "Waiting for all the transport candidates checks...");

        resolverThread.setName("STUN resolver");
        resolverThread.start();
      } else {
        throw new IllegalStateException("No valid STUN server found.");
      }
    }
  }
/**
 * An image Quantizer based on the Octree algorithm. This is a very basic implementation at present
 * and could be much improved by picking the nodes to reduce more carefully (i.e. not completely at
 * random) when I get the time.
 */
public class OctTreeQuantizer implements Quantizer {

  private static final SmackLogger LOGGER = SmackLogger.getLogger(OctTreeQuantizer.class);

  /** The greatest depth the tree is allowed to reach */
  static final int MAX_LEVEL = 5;

  /** An Octtree node. */
  class OctTreeNode {
    int children;
    int level;
    OctTreeNode parent;
    OctTreeNode leaf[] = new OctTreeNode[8];
    boolean isLeaf;
    int count;
    int totalRed;
    int totalGreen;
    int totalBlue;
    int index;

    /** A debugging method which prints the tree out. */
    public void list(PrintStream s, int level) {
      String indentStr = "";
      for (int i = 0; i < level; i++) indentStr += " ";
      if (count == 0) LOGGER.debug(indentStr + index + ": count=" + count);
      else
        LOGGER.debug(
            indentStr
                + index
                + ": count="
                + count
                + " red="
                + (totalRed / count)
                + " green="
                + (totalGreen / count)
                + " blue="
                + (totalBlue / count));
      for (int i = 0; i < 8; i++) if (leaf[i] != null) leaf[i].list(s, level + 2);
    }
  }

  private int nodes = 0;
  private OctTreeNode root;
  private int reduceColors;
  private int maximumColors;
  private int colors = 0;
  private Vector[] colorList;

  public OctTreeQuantizer() {
    setup(256);
    colorList = new Vector[MAX_LEVEL + 1];
    for (int i = 0; i < MAX_LEVEL + 1; i++) colorList[i] = new Vector();
    root = new OctTreeNode();
  }

  /**
   * Initialize the quantizer. This should be called before adding any pixels.
   *
   * @param numColors the number of colors we're quantizing to.
   */
  public void setup(int numColors) {
    maximumColors = numColors;
    reduceColors = Math.max(512, numColors * 2);
  }

  /**
   * Add pixels to the quantizer.
   *
   * @param pixels the array of ARGB pixels
   * @param offset the offset into the array
   * @param count the count of pixels
   */
  public void addPixels(int[] pixels, int offset, int count) {
    for (int i = 0; i < count; i++) {
      insertColor(pixels[i + offset]);
      if (colors > reduceColors) reduceTree(reduceColors);
    }
  }

  /**
   * Get the color table index for a color.
   *
   * @param rgb the color
   * @return the index
   */
  public int getIndexForColor(int rgb) {
    int red = (rgb >> 16) & 0xff;
    int green = (rgb >> 8) & 0xff;
    int blue = rgb & 0xff;

    OctTreeNode node = root;

    for (int level = 0; level <= MAX_LEVEL; level++) {
      OctTreeNode child;
      int bit = 0x80 >> level;

      int index = 0;
      if ((red & bit) != 0) index += 4;
      if ((green & bit) != 0) index += 2;
      if ((blue & bit) != 0) index += 1;

      child = node.leaf[index];

      if (child == null) return node.index;
      else if (child.isLeaf) return child.index;
      else node = child;
    }
    LOGGER.debug("getIndexForColor failed");
    return 0;
  }

  private void insertColor(int rgb) {
    int red = (rgb >> 16) & 0xff;
    int green = (rgb >> 8) & 0xff;
    int blue = rgb & 0xff;

    OctTreeNode node = root;

    //		LOGGER.debug("insertColor="+Integer.toHexString(rgb));
    for (int level = 0; level <= MAX_LEVEL; level++) {
      OctTreeNode child;
      int bit = 0x80 >> level;

      int index = 0;
      if ((red & bit) != 0) index += 4;
      if ((green & bit) != 0) index += 2;
      if ((blue & bit) != 0) index += 1;

      child = node.leaf[index];

      if (child == null) {
        node.children++;

        child = new OctTreeNode();
        child.parent = node;
        node.leaf[index] = child;
        node.isLeaf = false;
        nodes++;
        colorList[level].addElement(child);

        if (level == MAX_LEVEL) {
          child.isLeaf = true;
          child.count = 1;
          child.totalRed = red;
          child.totalGreen = green;
          child.totalBlue = blue;
          child.level = level;
          colors++;
          return;
        }

        node = child;
      } else if (child.isLeaf) {
        child.count++;
        child.totalRed += red;
        child.totalGreen += green;
        child.totalBlue += blue;
        return;
      } else node = child;
    }
    LOGGER.debug("insertColor failed");
  }

  private void reduceTree(int numColors) {
    for (int level = MAX_LEVEL - 1; level >= 0; level--) {
      Vector v = colorList[level];
      if (v != null && v.size() > 0) {
        for (int j = 0; j < v.size(); j++) {
          OctTreeNode node = (OctTreeNode) v.elementAt(j);
          if (node.children > 0) {
            for (int i = 0; i < 8; i++) {
              OctTreeNode child = node.leaf[i];
              if (child != null) {
                if (!child.isLeaf) LOGGER.debug("not a leaf!");
                node.count += child.count;
                node.totalRed += child.totalRed;
                node.totalGreen += child.totalGreen;
                node.totalBlue += child.totalBlue;
                node.leaf[i] = null;
                node.children--;
                colors--;
                nodes--;
                colorList[level + 1].removeElement(child);
              }
            }
            node.isLeaf = true;
            colors++;
            if (colors <= numColors) return;
          }
        }
      }
    }

    LOGGER.debug("Unable to reduce the OctTree");
  }

  /**
   * Build the color table.
   *
   * @return the color table
   */
  public int[] buildColorTable() {
    int[] table = new int[colors];
    buildColorTable(root, table, 0);
    return table;
  }

  /**
   * A quick way to use the quantizer. Just create a table the right size and pass in the pixels.
   *
   * @param inPixels the input colors
   * @param table the output color table
   */
  public void buildColorTable(int[] inPixels, int[] table) {
    int count = inPixels.length;
    maximumColors = table.length;
    for (int i = 0; i < count; i++) {
      insertColor(inPixels[i]);
      if (colors > reduceColors) reduceTree(reduceColors);
    }
    if (colors > maximumColors) reduceTree(maximumColors);
    buildColorTable(root, table, 0);
  }

  private int buildColorTable(OctTreeNode node, int[] table, int index) {
    if (colors > maximumColors) reduceTree(maximumColors);

    if (node.isLeaf) {
      int count = node.count;
      table[index] =
          0xff000000
              | ((node.totalRed / count) << 16)
              | ((node.totalGreen / count) << 8)
              | node.totalBlue / count;
      node.index = index++;
    } else {
      for (int i = 0; i < 8; i++) {
        if (node.leaf[i] != null) {
          node.index = index;
          index = buildColorTable(node.leaf[i], table, index);
        }
      }
    }
    return index;
  }
}