/*
VisAD system for interactive analysis and visualization of numerical
data. Copyright (C) 1996 - 2017 Bill Hibbard, Curtis Rueden, Tom
Rink, Dave Glowacki, Steve Emmerson, Tom Whittaker, Don Murray, and
Tommy Jasmin.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
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
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
MA 02111-1307, USA
*/
package visad;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import visad.util.FloatTupleArray;
/**
* Used by <code>Contour2D</code>'s filled contour algorithm to build a
* <code>VisADTriangleStripArray</code>. Triangle vertices, normals, and colors
* are stored until the <code>compile</code> is called, at which point the
* builder is invalidated. <p> This code is very specific and makes many
* assumptions about the data used to create the strips. Input validation is
* kept to a minimum, and when input is validated it is done with assertions so
* they can be enabled and disabled at runtime.
* <p>
* Arrays of vertices and normals grow dynamically when needed to avoid an
* initial large array allocation.
* @see {@link visad.test.FloatTupleArray}
*/
class TriangleStripBuilder {
static Logger log = Logger.getLogger(TriangleStripBuilder.class.getName());
/**
* Flag enabling strip merging with previous grid boxes.
*/
public static final int MERGE_PREVIOUS = 2 ^ 0;
/**
* Flag disabling all merging. All other policy flags are ignored.
*/
public static final int MERGE_NONE = 0;
/**
* Default policy for strip merging. NOTE: Currently the default is set to
* only merge previous. The algorithm for merging in the current grid box does
* not work properly.
*/
public static final int DEF_MERGE_POLICY = MERGE_PREVIOUS;
/** Vertices for the entire grid. */
private final FloatTupleArray vertices;
/** Normals corresponding to the vertices for the entire grid. */
private final FloatTupleArray normals;
/** Number of grid rows. */
private int gridRows;
/** Number of grid columns. */
private int gridCols;
/** The current grid box number. */
private int curBoxNum;
/** Metadata for the previous grid box. */
private GridBox prevGridBox;
/** Metadata for the current grid box. */
private GridBox curGridBox;
/**
* Strip objects that will be compiled into strip counts and color arrays.
*/
private final List<StripProps> strips;
/** Dimension of the color arrays. */
private final int colorDim;
/** If true merging in the previous grid box will be attempted. */
private boolean mergePrevious;
private VisADTriangleStripArray compiledStrip;
private int numFlipped;
private int numMerged;
/**
* New instance with the default coordinate delta and merging policy.
*
* @param rows Number of grid rows
* @param cols Number of grid cols
* @param colorDim number of color elements
*/
TriangleStripBuilder(int rows, int cols, int colorDim) {
this(rows, cols, colorDim, DEF_MERGE_POLICY);
}
/**
* New instance with the default coordinate delta.
*
* @param rows Number of grid rows
* @param cols Number of grid cols
* @param colorDim number of color elements
* @param mergePolicy A logical OR'ing of <code>MERGE</code> constants that
* specifies the options used when merging strips.
*/
TriangleStripBuilder(int rows, int cols, int colorDim, int mergePolicy) {
this.gridRows = rows;
this.gridCols = cols;
this.curBoxNum = 0;
this.colorDim = colorDim;
this.mergePrevious = (mergePolicy & MERGE_PREVIOUS) == MERGE_PREVIOUS;
log.finer("mergePolicy prev:" + mergePrevious);
this.vertices = FloatTupleArray.Factory.newInstance(2, rows * cols * 4);
this.normals = FloatTupleArray.Factory.newInstance(3, rows * cols * 4);
strips = new ArrayList<StripProps>();
compiledStrip = null;
}
/**
* Compile the computed strips into a scene graph object. The first time compile
* is called a <code>VisADTriangleStripArray</code> is created and subsequent
* calls return the initially compiled object.
*
* @param set Used to convert grid coordinates to display coordinates.
* @return A valid object ready for adding to the scene graph.
* @throws visad.VisADException
*/
public VisADTriangleStripArray compile(Gridded3DSet set) throws VisADException {
int totalBoxes = gridRows * gridCols;
if (curBoxNum != totalBoxes) {
throw new IllegalStateException(String.format("Not finished. On gridbox %s of %s", curBoxNum,
totalBoxes));
}
if (compiledStrip != null) return compiledStrip;
byte[] colors = new byte[colorDim * vertices.size()];
// stripVertexCounts are the number of vertices that comprise each
// individual strip, which were compiled in the StripProps.vertexCount-s
int[] stripVertexCounts = new int[strips.size()];
for (int sIdx = 0, clrIdx = 0; sIdx < strips.size(); sIdx++) {
StripProps strip = strips.get(sIdx);
// each vertex for a strip gets the same color
for (int vIdx = 0; vIdx < strip.vertexCount; vIdx++) {
for (int i = 0; i < colorDim; i++) {
colors[clrIdx++] = strip.color[i];
}
}
stripVertexCounts[sIdx] = strip.vertexCount;
}
// compile normals.
float[][] curNormals = normals.toArray();
float[] newNormals = new float[curNormals.length * curNormals[0].length];
for (int i = 0, j = 0; i < curNormals[0].length; i++) {
newNormals[j++] = curNormals[0][i];
newNormals[j++] = curNormals[1][i];
newNormals[j++] = curNormals[2][i];
}
try {
compiledStrip = new VisADTriangleStripArray();
// number of vertices for each individual strips
compiledStrip.stripVertexCounts = stripVertexCounts;
// number of vertices in this geometry
compiledStrip.vertexCount = vertices.size();
compiledStrip.coordinates = set.gridToValue(vertices.toArray(), true)[0];
compiledStrip.normals = newNormals;
compiledStrip.colors = colors;
} catch (VisADException e) {
// ensure null strip is an exception occurs
compiledStrip = null;
throw e;
}
log.fine("compiled " + toString());
//System.err.println("compiled " + toString());
return compiledStrip;
}
/**
* Add vertices to the builder. Merging is performed according to either the
* default policy or the policy provided during instantiation. Unless an error
* occurs the vertices provided are always either merged with a current strip
* or used to create a new strip.
* <p>
* Assumptions:
* <ol>
* <li>Caller will not give points in an order that will cause triangles to
* be folded over one another.
* <li>Caller will provide strips, not individual triangle vertices.
* <li>Grid is traversed left to right.
* </ol>
*
* @param kase flag indicating the contour case that generated these vertices.
* @param side flag indicating the side of the grid box the vertices end on.
* @param orientation flag indicating the orientation of the vertices.
*/
public void addVerticies(int lvlIdx, float[][] verts, float[][] norms, byte[] color,
byte sideFirst, byte orientFirst, byte sideLast, byte orientLast) {
StripProps strip = null;
//
// MERGE IN PREVIOUS GRID BOX
//
if (mergePrevious && curGridBox.strips.size() == 0) {
strip = mergeToPrevious(lvlIdx, verts, norms, sideFirst, orientFirst, sideLast,
orientLast);
}
//
// CREATE NEW STRIP
//
if (strip == null) {
strip = new StripProps(color, lvlIdx, sideFirst, orientFirst, sideLast, orientLast);
this.vertices.add(verts);
this.normals.add(norms);
strip.vertexCount = verts[0].length;
strips.add(strip);
curGridBox.add(strip);
}
// // strip cannot be null here
// int start = this.vertices.size() - strip.vertexCount;
// log.finest(toString(this.vertices.elements(), start,
// strip.vertexCount));
}
/**
* Attempt to merge vertices with a strip in the previous grid box.
* @param lvlIdx
* @param verts
* @param norms
* @param kase flag indicating the contour case that generated these vertices.
* @param side flag indicating the side of the grid box the vertices end on.
* @param orientation flag indicating the orientation of the vertices.
*
* @return the merged strip, or null if no merge took place
*/
protected StripProps mergeToPrevious(int lvlIdx, float[][] verts, float[][] norms,
byte sideFirst, byte orientFirst, byte sideLast, byte orientLast) {
boolean modified = false;
//currently we can only merge to the last strip added to the prev grid box
StripProps prevStrip = prevGridBox == null ? null : prevGridBox.lastStrip();
boolean firstCol = curBoxNum % gridCols == 1; // can't when in first column
boolean correctLvl = false; // has to have the same color level index
boolean shareSide = false; // have to share a side
boolean oppositeOrient = false; // have to have the opposite orientation
if (prevStrip != null) {
correctLvl = prevStrip.lvlIdx == lvlIdx;
shareSide = prevStrip.sideLast == Contour2D.SIDE_RIGHT && sideFirst == Contour2D.SIDE_LEFT;
if (prevStrip.orientLast == Contour2D.CLOCKWISE) {
oppositeOrient = (orientFirst == Contour2D.CNTRCLOCKWISE);
} else {
oppositeOrient = (orientFirst == Contour2D.CLOCKWISE);
}
}
if (!firstCol && correctLvl && shareSide && oppositeOrient) {
if (!oppositeOrient) {
// FIXME: (1) no flipping for now, (2) if flipped then somehow new orientation has to be recorded.
boolean mustFlip = false;
if (mustFlip) {
flipStrip(verts, norms);
oppositeOrient = true;
numFlipped++;
}
}
// add the new
int num = verts[0].length - 2;
this.vertices.add(verts, 2, num);
this.normals.add(norms, 2, num);
prevStrip.vertexCount += num;
prevStrip.sideLast = sideLast;
prevStrip.orientLast = orientLast;
modified = true;
// since we merged a current box strip with one in the
// previous box, we swap it's ownership to the current box
prevGridBox.strips.remove(prevStrip);
curGridBox.strips.add(prevStrip);
numMerged++;
}
return modified ? prevStrip : null;
}
public String toString() {
float avgLen = strips.size() == 0 ? 0 : vertices.size() / strips.size();
int maxLen = 0;
for (int i=0; i<strips.size(); i++) {
StripProps strip = strips.get(i);
if (strip.vertexCount > maxLen) maxLen = strip.vertexCount;
}
return String.format("<%s numStrips=%s numFlipped=%s avgLen=%s maxLen=%s numMerged=%s>",
TriangleStripBuilder.class.getName(), strips.size(), numFlipped, avgLen, maxLen, numMerged);
}
/**
* Set the current grid box. The previous grid box will be cleared and will
* become the former current grid box.
*
* @param row
* @param col
*/
public void setGridBox(int row, int col) {
if (prevGridBox != null) {
prevGridBox.strips.clear();
}
prevGridBox = curGridBox;
curBoxNum++;
curGridBox = new GridBox(row, col);
}
/**
* Container for grid box metadata.
*/
class GridBox {
int row;
int col;
final List<StripProps> strips;
GridBox(int row, int col) {
this.row = row;
this.col = col;
strips = new ArrayList<StripProps>(3);
}
void add(StripProps strip) {
strips.add(strip);
assert strip.vertexCount >= 3 : "Stripcount < 3";
}
StripProps lastStrip() {
return strips.size() > 0 ? strips.get(strips.size() - 1) : null;
}
@Override
public String toString() {
return String.format("<GridBox row=%s col=%s>", row, col);
}
}
/**
* Container for strip metadata.
*/
class StripProps {
/** Color for all vertices. */
final byte[] color;
/** Number of vertices. Must be at least 2. */
int vertexCount;
/** Index into the contour level array for this strip. */
final int lvlIdx;
final byte sideFirst;
final byte orientFirst;
byte sideLast;
byte orientLast;
boolean flipped = false;
StripProps(byte[] color, int lvlIdx, byte sideFirst, byte orientFirst,
byte sideLast, byte orientLast) {
this.color = color;
this.lvlIdx = lvlIdx;
this.sideFirst = sideFirst;
this.orientFirst = orientFirst;
this.sideLast = sideLast;
this.orientLast = orientLast;
}
@Override
public String toString() {
return String.format("<StripProps count=%s lvlIdx=%s color=%s>", vertexCount, lvlIdx,
Arrays.toString(color));
}
}
/**
* Used in coordinate comparisons.
*/
static final double DEF_COORD_DELTA = 2.1e-5;
static boolean coordEquals(float c1, float c2, double delta) {
return Math.abs(c1 - c2) <= delta;
}
/** Alias for checking coordinate equality. */
static boolean coordEquals(float c1, float c2) {
return coordEquals(c1, c2, DEF_COORD_DELTA);
}
static boolean coordsEqual(FloatTupleArray v1, int idx1, float[][] v2, int idx2, double delta) {
boolean match = true;
for (int dim = 0; dim < v1.dim(); dim++) {
match &= coordEquals(v1.get(dim, idx1), v2[dim][idx2], delta);
}
return match;
}
/**
* Check if coordinates are equal.
*
* @param v1 Vertex array 1
* @param idx1 Index of the coordinates to check in v1
* @param v2 Vertex array 2
* @param idx2 Index of coordinates to check in v2
* @return True if all components at the given indices are equal.
*/
static boolean coordsEqual(FloatTupleArray v1, int idx1, float[][] v2, int idx2) {
return coordsEqual(v1, idx1, v2, idx2, DEF_COORD_DELTA);
}
/**
* Check if 2 vertices of a line match 2 vertices of a triangle.
*
* @param v1 Vertex array of the line
* @param idx1 Index of the first point of the line, where idx1 + 1 is the
* second.
* @param v2 Vertex array of the triangle
* @param idx2 Index of the first vertex of the triangle, where idx+1 and
* idx1+2 will be the next two.
* @param delta delta to use for comparisons
* @return An array with the matching triangle vertices in index 1 and 2 and
* the unmatched triangle vertex index at index 3. If no match is
* found return null.
*/
static synchronized int[] stripCoordEquals(FloatTupleArray stripVerticies, int sIdx,
float[][] triVerticies, int tIdx) {
return stripCoordEquals(stripVerticies, sIdx, triVerticies, tIdx, DEF_COORD_DELTA);
}
static synchronized int[] stripCoordEquals(FloatTupleArray stripVerticies, int sIdx,
float[][] triVerticies, int tIdx, double delta) {
assert sIdx + 1 < stripVerticies.size();
assert tIdx + 2 < triVerticies[0].length;
int s1 = sIdx, s2 = sIdx + 1;
int t1 = tIdx, t2 = tIdx + 1, t3 = tIdx + 2;
if (coordsEqual(stripVerticies, s1, triVerticies, t1, delta)) {
if (coordsEqual(stripVerticies, s2, triVerticies, t2, delta)) {
return new int[] { t1, t2, t3 };
} else if (coordsEqual(stripVerticies, s2, triVerticies, t3, delta)) {
return new int[] { t1, t3, t2 };
}
} else if (coordsEqual(stripVerticies, s1, triVerticies, t2, delta)) {
if (coordsEqual(stripVerticies, s2, triVerticies, t1, delta)) {
return new int[] { t2, t1, t3 };
} else if (coordsEqual(stripVerticies, s2, triVerticies, t3, delta)) {
return new int[] { t2, t3, t1 };
}
} else if (coordsEqual(stripVerticies, s1, triVerticies, t3, delta)) {
if (coordsEqual(stripVerticies, s2, triVerticies, t1, delta)) {
return new int[] { t3, t1, t2 };
} else if (coordsEqual(stripVerticies, s2, triVerticies, t2, delta)) {
return new int[] { t3, t2, t1 };
}
}
return null;
}
static float[] extractCoords(float[][] vertices, int idx) {
float[] coords = new float[vertices.length];
for (int i = 0; i < vertices.length; i++) {
coords[i] = vertices[i][idx];
}
return coords;
}
static String toString(float[][] coords) {
int len = coords.length > 0 ? coords[0].length : 0;
return toString(coords, 0, len);
}
static String toString(float[][] coords, int start, int num) {
StringBuilder buf = new StringBuilder();
for (int i = start, cnt = 0; cnt < num; i++, cnt++) {
buf.append("(");
for (int dim = 0; dim < coords.length - 1; dim++) {
buf.append("" + coords[dim][i] + ",");
}
buf.append("" + coords[coords.length - 1][i] + ") ");
}
return buf.toString();
}
static void flipStrip(float[][] verts, float[][] norms) {
int num = verts[0].length % 2 == 0 ? verts[0].length - 1 : verts[0].length - 2;
for (int i = 0; i < num; i += 2) {
float tv = verts[0][i];
verts[0][i] = verts[0][i + 1];
verts[0][i + 1] = tv;
tv = verts[1][i];
verts[1][i] = verts[1][i + 1];
verts[1][i + 1] = tv;
float tn = norms[0][i];
norms[0][i] = norms[0][i + 1];
norms[0][i + 1] = tn;
tn = norms[1][i];
norms[1][i] = norms[1][i + 1];
norms[1][i + 1] = tn;
tn = norms[2][i];
norms[2][i] = norms[2][i + 1];
norms[2][i + 1] = tn;
}
}
static String toString(FloatTupleArray coords) {
return toString(coords, 0, coords.size());
}
static String toString(FloatTupleArray coords, int start, int num) {
StringBuilder buf = new StringBuilder();
for (int i = start, cnt = 0; cnt < num; i++, cnt++) {
buf.append("(");
for (int dim = 0; dim < coords.dim() - 1; dim++) {
buf.append("" + coords.get(dim, i) + ",");
}
buf.append("" + coords.get(coords.dim() - 1, i) + ") ");
}
return buf.toString();
}
}