/**
   * Updates the particle view. This should be called on each draw cycle in order to update the
   * positions of all nodes and edges in the viewer. If you need to update the positions of
   * particles without drawing it (e.g. to speed up movement, call updateParticles() instead.
   */
  public void draw() {
    parent.pushStyle();
    parent.pushMatrix();
    zoomer.transform();
    updateCentroid();
    centroid.tick();

    parent.translate(width / 2, height / 2);
    parent.scale(centroid.getZ());
    parent.translate(-centroid.getX(), -centroid.getY());

    if (!isPaused) {
      updateParticles();
    }

    // Ensure that any selected element is positioned at the mouse location.
    if (selectedNode != null) {
      Particle p = nodes.get(selectedNode);
      p.makeFixed();
      float mX = (zoomer.getMouseCoord().x - (width / 2)) / centroid.getZ() + centroid.getX();
      float mY = (zoomer.getMouseCoord().y - (height / 2)) / centroid.getZ() + centroid.getY();
      p.position().set(mX, mY, 0);
    }

    // Draw edges if we have positive stroke weight.
    if (parent.g.strokeWeight > 0) {
      parent.stroke(0, 180);
      parent.noFill();

      for (Map.Entry<E, Spring> row : edges.entrySet()) {
        E edge = row.getKey();
        Spring spring = row.getValue();
        Vector3D p1 = spring.getOneEnd().position();
        Vector3D p2 = spring.getTheOtherEnd().position();
        edge.draw(parent, p1.x(), p1.y(), p2.x(), p2.y());
      }
    }

    // Draw nodes.
    parent.noStroke();
    parent.fill(120, 50, 50, 180);

    for (Map.Entry<N, Particle> row : nodes.entrySet()) {
      N node = row.getKey();
      Vector3D p = row.getValue().position();
      node.draw(parent, p.x(), p.y());
    }

    parent.popMatrix();
    parent.popStyle();
  }
  /**
   * Reports the node nearest to the given screen coordinates but within the given radius.
   *
   * @param x x screen coordinate to query
   * @param y y screen coordinate to query
   * @param radius Radius within which to search for nodes. If negative, all nodes are searched.
   * @return Node nearest to the given screen coordinates or null if no nodes found within the given
   *     radius of the coordinates.
   */
  public N getNearest(float x, float y, float radius) {
    float mX = (x - width / 2) / centroid.getZ() + centroid.getX();
    float mY = (y - height / 2) / centroid.getZ() + centroid.getY();

    float nearestDSq = radius * radius;
    N nearestNode = null;

    for (Map.Entry<N, Particle> row : nodes.entrySet()) {
      N node = row.getKey();
      Particle p = row.getValue();

      float px = p.position().x();
      float py = p.position().y();
      float dSq = (px - mX) * (px - mX) + (py - mY) * (py - mY);
      if (dSq < nearestDSq) {
        nearestDSq = dSq;
        nearestNode = node;
      }
    }
    return nearestNode;
  }
  /** Allows a node to be selected with the mouse. */
  public void selectNearestWithMouse() {
    if (!zoomer.isMouseCaptured()) {
      float mX = (zoomer.getMouseCoord().x - (width / 2)) / centroid.getZ() + centroid.getX();
      float mY = (zoomer.getMouseCoord().y - (height / 2)) / centroid.getZ() + centroid.getY();

      if (selectedNode == null) {
        float nearestDSq = Float.MAX_VALUE;

        for (Map.Entry<N, Particle> row : nodes.entrySet()) {
          N node = row.getKey();
          Particle p = row.getValue();

          float px = p.position().x();
          float py = p.position().y();
          float dSq = (px - mX) * (px - mX) + (py - mY) * (py - mY);
          if (dSq < nearestDSq) {
            nearestDSq = dSq;
            selectedNode = node;
          }
        }
      }
    }
  }
  /** Centres the particle view on the currently visible nodes. */
  private void updateCentroid() {
    float xMax = Float.NEGATIVE_INFINITY,
        xMin = Float.POSITIVE_INFINITY,
        yMin = Float.POSITIVE_INFINITY,
        yMax = Float.NEGATIVE_INFINITY;

    for (int i = 0; i < physics.getNumParticles(); ++i) {
      Particle p = physics.getParticle(i);
      xMax = Math.max(xMax, p.position().x());
      xMin = Math.min(xMin, p.position().x());
      yMin = Math.min(yMin, p.position().y());
      yMax = Math.max(yMax, p.position().y());
    }

    float xRange = xMax - xMin;
    float yRange = yMax - yMin;

    if ((xRange == 0) && (yRange == 0)) {
      xRange = Math.max(1, xMax);
      yRange = Math.max(1, yMax);
    }
    float zScale = (float) Math.min(height / (yRange * 1.2), width / (xRange * 1.2));
    centroid.setTarget(xMin + 0.5f * xRange, yMin + 0.5f * yRange, zScale);
  }