/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.math; import java.awt.geom.Line2D; import java.io.Serializable; import javax.vecmath.MismatchedSizeException; import org.apache.sis.math.Vector; import org.apache.sis.util.ArraysExt; import org.geotoolkit.resources.Errors; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; /** * Utility methods related to a pair of {@link Vector} objects. The {@code VectorPair} constructor * expects two vectors in argument, namely <var>X</var> and <var>Y</var>. Every operations defined * in this class create <cite>views</cite> of the given vectors; they never copy or calculate data * values. Because they are views, callers should not change the values of the original vectors, * unless propagation to the views is really wanted. * * @author Martin Desruisseaux (Geomatys) * @version 3.00 * * @since 3.00 * @module */ public class VectorPair implements Serializable { /** * For cross-version compatibility. */ private static final long serialVersionUID = 8330893190189236019L; /** * The vectors specified to the {@code VectorPair} constructor, * or the result of the last operation performed on this object. */ protected Vector X, Y; /** * Creates a new pair of vector. Whatever the two given vectors need to have the * same length or not depends on the operation to be applied on this pair of vectors. * * @param X The first vector of the pair. * @param Y The second vector of the pair. */ public VectorPair(final Vector X, final Vector Y) { ensureNonNull("X", this.X = X); ensureNonNull("Y", this.Y = Y); } /** * Returns the vector of <var>x</var> values. * * @return The vector of <var>x</var> values. */ public Vector getX() { return X; } /** * Returns the vector of <var>y</var> values. * * @return The vector of <var>y</var> values. */ public Vector getY() { return Y; } /** * Returns the length of the two vectors, or thrown an exception if they don't * have the same length. * * @return The vector length, which is the same for the two vectors. * @throws MismatchedSizeException if the two vectors don't have the same length. */ public int length() throws MismatchedSizeException { final int length = Y.size(); if (length != X.size()) { throw new MismatchedSizeException(Errors.format(Errors.Keys.MismatchedArrayLength)); } return length; } /** * Merges consecutive colinear segments, if any. More specifically for any index <var>i</var>, * if the point having the coordinate {@code (x[i], y[i])} lies on the line segment defined by * the two end points {@code (x[i-1], y[i-1])} and {@code (x[i+1], y[i+1])}, then the point at * index <var>i</var> will be dropped. * <p> * If the (<var>X</var>,<var>Y</var>) vectors are data to be plotted, then this method can be * used for simplifying the data without visual impact. * * @param xTolerance The maximal distance measured along the <var>x</var> axis to consider * that a point lies on a line segment. * @param yTolerance The maximal distance measured along the <var>y</var> axis to consider * that a point lies on a line segment. * @throws MismatchedSizeException If the <var>X</var> and <var>Y</var> vectors don't have * the same length. */ public void omitColinearPoints(final double xTolerance, double yTolerance) throws MismatchedSizeException { final int length = length(); // Invoked first for forcing an inconditional check of vectors length. final double ratio = yTolerance / xTolerance; if (!Double.isNaN(ratio)) { final Vector X = this.X; final Vector Y = this.Y; int[] indices = null; int count = 0; if (length >= 3) { yTolerance *= yTolerance; double x1 = X.doubleValue(0) * ratio; double y1 = Y.doubleValue(0); double x2 = X.doubleValue(1) * ratio; double y2 = Y.doubleValue(1); for (int i=2; i<length; i++) { double x3 = X.doubleValue(i) * ratio; double y3 = Y.doubleValue(i); final double dsq = Line2D.ptSegDistSq(x1, y1, x3, y3, x2, y2); if (dsq <= yTolerance) { // Found a colinear point: (x2,y2) is on the line segment (x1,y1)-(x3,y3) if (indices == null) { indices = new int[length - 1]; while (count < i) { indices[count] = count++; } } // Overwrite the point in the middle (x2,y2) with the last one (x3,y3). indices[count-1] = i; } else { // The point is not colinear, so remember that we must keep it. if (indices != null) { indices[count++] = i; } x1 = x2; y1 = y2; } x2 = x3; y2 = y3; } } if (indices != null) { indices = ArraysExt.resize(indices, count); this.X = X.pick(indices); this.Y = Y.pick(indices); } } } /** * Computes views of (<var>X</var>,<var>Y</var>) vectors where every diagonal line is replaced * by a horizontal line followed by a vertical line. For this purpose, every <var>x</var> and * <var>y</var> values are repeated once. A graph of the resulting vectors would have the visual * appareance of a stair, or the outer limit of a histogram. * <p> * When invoked with {@code direction=0}, This method can be used before to plot * (<var>X</var>,<var>Y</var>) data where <var>X</var> can takes only some fixed values, * in order to visualy emphase its discontinuous nature. When invoked with positive or * negative {@code direction} argument, this method can be used for plotting upper or lower * limit respectively (for example computed from a standard deviation) of the above data. * * {@section On input} * Starting with <var>X</var><sub>i</sub> and <var>Y</var><sub>i</sub> as the input vectors before * this method is invoked, take <var>s</var> to be the length of the <var>Y</var><sub>i</sub> vector, * then the length of the <var>X</var><sub>i</sub> vector must be <var>s</var>+1. * * {@section On output} * Let define <var>X</var><sub>o</sub> and <var>Y</var><sub>o</sub> the output vectors after * this method has been invoked. The length of those two vectors will be at most 2<var>s</var> * (they will be exactly of that length if {@code abs(direction) <= 1}). The first point and the * last point in the output vectors will be the same than the first point and the last point in * the input vectors. More specifically, assuming that the output vectors have the maximal length: * <p> * <ul> * <li>(<var>X</var><sub>o</sub>[0], <var>Y</var><sub>o</sub>[0]) = * (<var>X</var><sub>i</sub>[0], <var>Y</var><sub>i</sub>[0])</li> * <li>(<var>X</var><sub>o</sub>[2s-1], <var>Y</var><sub>o</sub>[2s-1]) = * (<var>X</var><sub>i</sub>[s], <var>Y</var><sub>i</sub>[s-1])</li> * </ul> * <p> * It is often a good idea to invoke {@link #omitColinearPoints} after this method. * * @param direction Controls the order of horizontal and vertical lines.<ul> * <li>If zero (neutral), then the order of line segments will always be <cite>horizontal * line followed by vertical line</cite>, which produce the appearance of a histogram * outer limit.</li> * <li>If +1 or -1, then some vertical lines may appear before their horizontal counterpart, * when it makes the value of <var>y</var> higher (if {@code direction} is +1) or lower * (if {@code direction} is -1) than they would otherwise be. So {@code direction} can * be interpreted as the sign of the allowed change of <var>y</var> values.</li> * <li>If +2 or -2, then the same reordering than +1 or -1 is executed, followed by:<ul> * <li>If {@code direction} is +2, the removal of lower point when the <var>y</var> * values are going down and up again at the same <var>x</var> value.</li> * <li>If {@code direction} is -2, the removal of higher point when the <var>y</var> * values are going up and down again at the same <var>x</var> value.</li> * </ul> * So +2 and -2 argument can be interpreted as shifting the main <var>y</var> value * toward positive or negative infinity slightly more than what +1 or -1 does.</li> * </ul> * @throws MismatchedSizeException If the length of the <var>X</var> vector is not equal to * the length of the <var>Y</var> vector + 1. */ public void makeStepwise(int direction) throws MismatchedSizeException { final Vector X = this.X; final Vector Y = this.Y; final int length = Y.size(); if (length+1 != X.size()) { throw new MismatchedSizeException(); } int[] Xi = new int[length*2]; int[] Yi = new int[length*2]; if (length != 0) { boolean swap = false; double x0 = X.doubleValue(0); double y0 = Y.doubleValue(0); for (int i=0,j=0; i<length; i++) { if (direction != 0) { if (i+1 == length) { // We have reached the end of the vector. Never swap the last // index, otherwise we get an IndexOutOfBoundsException. swap = false; } else { final double xi = X.doubleValue(i+1); final double yi = Y.doubleValue(i+1); int xs = (xi > x0) ? +1 : (xi < x0) ? -1 : 0; // We want 0 for NaN. if (xs != 0) { boolean up = (direction >= 0) ^ (xs < 0) ^ swap; if (up ? (yi > y0) : (yi < y0)) { swap = !swap; } } x0 = xi; y0 = yi; } } Xi[j] = i; Yi[j++] = i; Xi[j] = swap ? i : i+1; Yi[j++] = swap ? i+1 : i; } assert ArraysExt.isSorted(Xi, false); assert ArraysExt.isSorted(Yi, false); /* * At this point we are done. However if 'direction' is different than 0, then the * index swapping may have caused situations where a Y value goes down, then up at * the same X value. The code below will erase the down point (or the opposite if * 'direction' is negative instead than positive). */ if (Math.abs(direction) >= 2) { direction = Integer.signum(direction); int size = Xi.length; for (int i=size; --i>=2;) { if (Xi[i] == Xi[i-2]) { final double y1, y2; y0 = Y.doubleValue(Yi[i-2]) * direction; y1 = Y.doubleValue(Yi[i-1]) * direction; y2 = Y.doubleValue(Yi[i ]) * direction; if (y1 < Math.min(y0, y2)) { System.arraycopy(Xi, i, Xi, i-1, size-i); System.arraycopy(Yi, i, Yi, i-1, size-i); size--; } } } Xi = ArraysExt.resize(Xi, size); Yi = ArraysExt.resize(Yi, size); } } this.X = X.pick(Xi); this.Y = Y.pick(Yi); } }