Sort PVector array by distance

I actually have a library component that I developed (inspired by GoToLoop’s previous post) – VecArrayTool – that lets you do general sorting of PVector arrays by multiple criteria – x-y order, y-x order, heading, magnitude, etc. – among other things, like shifting and rotating the points en masse, re-centering, finding bounds, etc.

Here is the Processing-style class for use as a .pde file into PDE sketches:

VecArrayTool.pde


/**
 * Methods for manipulating arrays vectors (PVector[]),
 * including centering, mirroring, rotating, translating,
 * and sorting by x, y, heading, or magnitude, as well as
 * finding the bounds and center of a point array.
 * 
 * Sorting may be augmented by writing a custom comparator.
 *
 * Class methods assume that all vectors may be treated as
 * points having a shared origin (0,0).
 */
public static class VecArrayTool {
  /**
   * x comparator: sort vectors by x, then y
   */
  public static final Comparator<PVector> VEC_CMP_X = new Comparator<PVector>() {
    @ Override public final int compare(final PVector a, final PVector b) {
      int cmp;
      return
        (cmp = Float.compare(a.x, b.x)) != 0? cmp :
        Float.compare(a.y, b.y);
    }
  };
  /**
   * y comparator: sort vectors by y, then x
   */
  public static final Comparator<PVector> VEC_CMP_Y = new Comparator<PVector>() {
    @ Override public final int compare(final PVector a, final PVector b) {
      int cmp;
      return
        (cmp = Float.compare(a.y, b.y)) != 0? cmp :
        Float.compare(a.x, b.x);
    }
  };
  /**
   * h comparator: sort vectors by heading (angle of rotation from origin)
   */
  public static final Comparator<PVector> VEC_CMP_HEAD = new Comparator<PVector>() {
    @ Override public final int compare(final PVector a, final PVector b) {
      return Float.compare(a.heading(), b.heading());
    }
  };
  /**
   * m comparator: sort vectors by magnitude (distance from origin)
   */
  public static final Comparator<PVector> VEC_CMP_MAG = new Comparator<PVector>() {
    @ Override public final int compare(final PVector a, final PVector b) {
      return Float.compare(a.mag(), b.mag());
    }
  };

  /**
   * random comparator: example of writing a custom sorting comparator.
   * Use with:
   *
   *   VecArrayTool.sort(vecs, VEC_CMP_RANDOM);
   */
  public static final Comparator<PVector> VEC_CMP_RANDOM = new Comparator<PVector>() {
    @ Override public final int compare(final PVector a, final PVector b) {
      return (Math.random()<0.5) ? -1 : 1;
    }
  };
    
  /**
   * Center a point array based on its bounds.
   * Returns a new array.
   *
   * @param   vecs   array of vectors
   * @return         a new PVector array centered on 0, 0
   */
  public PVector[] center(PVector[] vecs) {
    PVector ctr = this.getCenter(vecs);
    return this.translate(vecs, -ctr.x, -ctr.y);
  }

  /**
   * Get the rectangular bounds of an array of vectors
   * defined as an array of extrema.
   * 
   * These extrema define two corners of a bounding rectangle.
   * The later terms (xmax, ymax) are a coordinate,
   * not width, height.
   *
   * @param   vecs   array of vectors
   * @return         array of two bounds [(xmin, ymin), (xmax, ymax)]
   */
  public PVector[] getBounds(PVector[] vecs) {
    float xmin = vecs[0].x;    
    float xmax = vecs[0].x;
    float ymin = vecs[0].y;    
    float ymax = vecs[0].y;
    for (int i=1; i<vecs.length; i++) {
      xmin = (xmin < vecs[i].x) ? xmin : vecs[i].x;
      xmax = (xmax > vecs[i].x) ? xmax : vecs[i].x;
      ymin = (ymin < vecs[i].y) ? ymin : vecs[i].y;
      ymax = (ymax > vecs[i].y) ? ymax : vecs[i].y;
    }
    PVector[] results = new PVector[]{
      new PVector(xmin, ymin), 
      new PVector(xmax, ymax)
    };
    return results;
    //return new float[]{xmin, ymin, xmax, ymax};
  }  

  /**
   * Get center of the array of vectors,
   * defined as the center of the x/y bounds.
   *
   * @param   vecs   array of vectors
   * @return         center x,y
   */
  public PVector getCenter(PVector[] vecs) {
    PVector[] bnds = this.getBounds(vecs);
    return PVector.lerp(bnds[0], bnds[1], 0.5f);
  }

  /**
   * Get width of the array of vectors, defined on the x axis.
   *
   * @param   vecs   array of vectors
   * @return         width
   */
  public float getWidth(PVector[] vecs) {
    PVector[] bnds = this.getBounds(vecs);
    return bnds[1].x - bnds[0].x;
  }

  /**
   * Get height of the array of vectors, defined on the y axis.
   *
   * @param   vecs   array of vectors
   * @return         height
   */
  public float getHeight(PVector[] vecs) {
    PVector[] bnds = this.getBounds(vecs);
    return bnds[1].y - bnds[0].y;
  }

  /**
   * Mirror (reflect) an array of vectors by x, or y, or x/y.
   * Returns a new array.
   *
   * Works around the origin.
   * To mirror a point cloud around its local center,
   * first center, then mirror:
   *
   *   VecArrayTool vat = new VecArrayTool();
   *   PVector[] mirrored = vat.mirror(vat.center(vecs), true, false).
   *
   * To preserve the offset of the resulting cloud, save the
   * center and translate back:
   *
   *   VecArrayTool vat = new VecArrayTool();
   *   PVector ctr = vat.getCenter(vecs);
   *   PVector[] mirrored = vat.mirror(vat.center(vecs), true, false).
   *   mirrored = vat.translate(ctr);
   *
   * 
   * @param   vecs   array of vectors
   * @param   x      mirror across x axis? (horizontal mirror)
   * @param   y      mirror across y axis? (vertical mirror)
   * @return  a new array of points in the form {@code PVector[]}
   */
  public PVector[] mirror(PVector[] vecs, boolean x, boolean y) {
    PVector results[] = new PVector[vecs.length];
    for (int i=0; i<vecs.length; i++) {
      float vx = vecs[i].x;
      float vy = vecs[i].y;
      results[i] = new PVector(x?-vx:vx, y?-vy:vy);
    }
    return results;
  }

  //PVector[] rotate(PVector[] vecs) {
  //}

  /**
   * @param   vecs  a PVector array of points
   */
  public void shuffle(PVector[] vecs) {
    Collections.shuffle(Arrays.asList(vecs));
  }

  /**
   * Sort a point array by a comparison method.
   * Modifies in-place.
   *
   * The method may be specified as a mode string or passed in
   * as a comparator object.
   *
   * Modes include:
   *
   *   "x" : sort by x, then y
   *   "y" : sort by y, then x
   *   "h" : sort by heading
   *   "m" : sort by magnitude
   *
   * Calling with an object may also use built-in modes, e.g.:
   *
   *   VecArrayTool.sort(vecs, VecArrayTool.VEC_CMP_MAG);
   *
   * Heading and magnitude are calculated from the origin.
   * Some applications may wish to center the point cloud first
   * before sorting.
   *
   * @param   vecs   array of vectors
   * @param   mode   sorting mode ("x" / "y" / "h" / "m")
   */
  public void sort(PVector[] vecs, String mode) {
    if (mode.equals("x")) {
      this.sort(vecs, VEC_CMP_X);
    } else if (mode.equals("y")) {
      this.sort(vecs, VEC_CMP_Y);
    } else if (mode.equals("h")) {
      this.sort(vecs, VEC_CMP_HEAD);
    } else if (mode.equals("m")) {
      this.sort(vecs, VEC_CMP_MAG);
    } else {
      // leave unsorted
    }
  }
  /**
   * @param   vecs   array of vectors
   * @param   cmp    a {@code Comparator<PVector>}
   */
  public void sort(PVector[] vecs, Comparator<PVector> cmp) {
    Arrays.sort(vecs, cmp);
  }

  /**
   * Return an ArrayList of vectors.
   * 
   * @param   vecs   array of vectors
   * @return  new points in an {@code ArrayList<PVector>}
   */
  public ArrayList<PVector> toArrayList(PVector[] vecs) {
    return new ArrayList<PVector>(Arrays.asList(vecs));
  }

  /**
   * Translate all points in array by an offset.
   * Offset may be defined x, y or as a PVector (x,y).
   * Returns a new array.
   *
   * @param   vecs   array of vectors
   * @param   x      x offset to translate
   * @param   y      y offset to translate
   * @return         new array of vectors
   */
  public PVector[] translate(PVector[] vecs, float x, float y) {
    return this.translate(vecs, new PVector(x, y));
  }
  /**
   * @param   vecs    array of vectors
   * @param   xyoff   PVector x,y offset to translate
   * @return          new array of vectors
   */
  public PVector[] translate(PVector[] vecs, PVector xyoff) {
    PVector[] results = new PVector[vecs.length];
    for (int i=0; i<vecs.length; i++) {
      results[i] = PVector.add(vecs[i], xyoff);
    }
    return results;
  }
}

…and here is a demo sketch. Press . to change sorting styles, +/- to add/remove points, space to shuffle.

import java.util.Arrays;
import java.util.Collections;
import java.util.ArrayList;
import java.util.Comparator;

PVector[] vecs;
VecArrayTool vat;
String[] modes;
int pointcount;
int modenum;

void setup() {
  size(400, 400);
  ellipseMode(CENTER);
  pointcount = 5;
  modes = new String[]{"x", "y", "h", "m"};
  modenum = 0;
  vat = new VecArrayTool();
  vecs = newPoints(5);
}

void draw() {
  background(255);
  vecs = vat.center(vecs);
  vecs = vat.translate(vecs, width/2, height/2);
  for (PVector vec : vecs) {
    ellipse(vec.x, vec.y, 10, 10);
  }
  drawCurve(vecs, color(0));
  pushStyle();
  fill(0);
  text(modes[modenum], 2, height-5);
  popStyle();
}

void keyReleased() {
  if (key==' ') {  // random points
    vecs = newPoints(pointcount);
  }
  if (key=='.') {  // switch sorting mode
    modenum = (modenum+1) % modes.length;
    vat.sort(vecs, modes[modenum]);
  }
  if (key=='+'||key=='=') {  // add point
    pointcount += 1;
    vecs = Arrays.copyOf(vecs, pointcount);
    vecs[pointcount-1] = new PVector((int)random(width), (int)random(height));
  }
  if (key=='-'||key=='_') {  // drop point
    if(pointcount > 2){
      pointcount -=1;
      vecs = Arrays.copyOf(vecs, pointcount);
    }
  }
  if (key=='m') {  // mirror horizontally
    vecs = vat.mirror(vecs, true, false);
  }
}

void drawCurve(PVector[] vecs, int c) {
  pushStyle();
  noFill();
  stroke(c);
  beginShape();
  curveVertex(vecs[0].x, vecs[0].y);
  for (int i=0; i<vecs.length; i++) {
    curveVertex(vecs[i].x, vecs[i].y);
  }
  curveVertex(vecs[vecs.length-1].x, vecs[vecs.length-1].y);
  endShape();
  popStyle();
}

PVector[] newPoints(int count) {
  PVector[] results = new PVector[count];
  for (int i=0; i<count; i++) {
    results[i] = new PVector((int)random(width), (int)random(height));
  }
  return results;
}

Edit
To make the VecArrayTool.pde a .java file instead:

  1. change extension name to .java
  2. drop keyword “static” from the class line
  3. add required imports to the file:
import java.util.Arrays;
import java.util.Collections;
import java.util.ArrayList;
import java.util.Comparator;
import processing.core.PVector;
1 Like