├── .clj-kondo └── config.edn ├── .gitignore ├── INTERNALS.md ├── LICENSE ├── README.md ├── data ├── GvHD-FSC-H │ ├── s10a01 │ ├── s10a02 │ ├── s10a03 │ ├── s10a04 │ ├── s10a05 │ ├── s10a06 │ ├── s10a07 │ ├── s5a01 │ ├── s5a02 │ ├── s5a03 │ ├── s5a04 │ ├── s5a05 │ ├── s5a06 │ ├── s5a07 │ ├── s6a01 │ ├── s6a02 │ ├── s6a03 │ ├── s6a04 │ ├── s6a05 │ ├── s6a06 │ ├── s6a07 │ ├── s7a01 │ ├── s7a02 │ ├── s7a03 │ ├── s7a04 │ ├── s7a05 │ ├── s7a06 │ ├── s7a07 │ ├── s9a01 │ ├── s9a02 │ ├── s9a03 │ ├── s9a04 │ ├── s9a05 │ ├── s9a06 │ └── s9a07 ├── barley.json ├── cars.json ├── chem97.json ├── co2-concentration.csv ├── disasters.csv ├── driving.json ├── earthquake.json ├── faithful.json ├── gapminder-health-income.csv ├── gvhd10.json ├── laucnty17.csv ├── movies.json ├── nhanes.json ├── oats.json ├── population.json ├── postdoc.json ├── quakes.json ├── seattle-weather.csv ├── ssd.json ├── stocks.csv ├── sunspot_year.json ├── titanic.json ├── unemployment-across-industries.json └── usarrests.json ├── project.clj ├── resources └── clj-kondo.exports │ └── cljplot │ └── cljplot │ ├── config.edn │ └── hooks │ └── common.clj ├── results ├── anim │ └── res.gif ├── bar.jpg ├── bars.jpg ├── box.jpg ├── bubble.jpg ├── connected.jpg ├── cont4_cont8.jpg ├── density-line.jpg ├── density.jpg ├── examples │ ├── acf.jpg │ ├── complex.jpg │ ├── contour.jpg │ ├── contour2d.jpg │ ├── field-trace.jpg │ ├── field.jpg │ ├── function2d.jpg │ ├── gp-posterior.jpg │ ├── gp-posteriors-20.jpg │ ├── gp-predict.jpg │ ├── gp-priors-20.jpg │ ├── heatmap-matrix-ann.jpg │ ├── heatmap-matrix.jpg │ ├── heatmap.jpg │ ├── kernel-densities.jpg │ ├── lag.jpg │ ├── lines.jpg │ ├── logo.jpg │ ├── ma2.jpg │ ├── point-cloud.jpg │ ├── ppplot.jpg │ ├── qqplot.jpg │ ├── random-walk-wrapped.jpg │ ├── strips.jpg │ ├── vector-field.jpg │ └── vnoise.jpg ├── functions.jpg ├── histogram-percents.jpg ├── histogram.jpg ├── lattice │ ├── figure_1.1.jpg │ ├── figure_1.2.jpg │ ├── figure_1.3.jpg │ ├── figure_13.1.jpg │ ├── figure_13.2.jpg │ ├── figure_14.1.jpg │ ├── figure_14.2.jpg │ ├── figure_14.3.jpg │ ├── figure_14.4.jpg │ ├── figure_2.1.jpg │ ├── figure_2.10.jpg │ ├── figure_2.2.jpg │ ├── figure_2.3.jpg │ ├── figure_2.4.jpg │ ├── figure_2.6.jpg │ ├── figure_2.7.jpg │ ├── figure_2.8.jpg │ ├── figure_2.9.jpg │ ├── figure_3.1.jpg │ ├── figure_3.10.jpg │ ├── figure_3.11.jpg │ ├── figure_3.12.jpg │ ├── figure_3.13.jpg │ ├── figure_3.14.jpg │ ├── figure_3.15.jpg │ ├── figure_3.16.jpg │ ├── figure_3.2.jpg │ ├── figure_3.2v2.jpg │ ├── figure_3.3.jpg │ ├── figure_3.4.jpg │ ├── figure_3.5.jpg │ ├── figure_3.6.jpg │ ├── figure_3.7.jpg │ ├── figure_3.8.jpg │ ├── figure_3.9.jpg │ ├── figure_4.1.jpg │ ├── figure_4.2.jpg │ ├── figure_4.3.jpg │ ├── figure_4.4.jpg │ ├── figure_4.5.jpg │ ├── figure_4.6.jpg │ └── figure_4.7.jpg ├── line.jpg ├── lollipops.jpg ├── lollipops2.jpg ├── min-max-mean.jpg ├── rug.jpg ├── rugs.jpg ├── scatter-sides.jpg ├── scatters2-rug.jpg ├── strip-distorted.jpg ├── strip.jpg ├── vega │ ├── area-overlay.jpg │ ├── area.jpg │ ├── bar-aggregate.jpg │ ├── bar-color.jpg │ ├── bar-gantt.jpg │ ├── bar-grouped.jpg │ ├── bar.jpg │ ├── box-plot.jpg │ ├── circle-binned.jpg │ ├── circle-bubble-hi.jpg │ ├── circle-natural-disasters.jpg │ ├── circles.jpg │ ├── color-with-shape.jpg │ ├── connected-scatterplot.jpg │ ├── histogram.jpg │ ├── layer-line-co2-concentration.jpg │ ├── line-color.jpg │ ├── line-overlay.jpg │ ├── line-spline.jpg │ ├── line-step.jpg │ ├── line.jpg │ ├── nulls.jpg │ ├── point-bubble.jpg │ ├── point-errorbar-ci.jpg │ ├── point-errorbar-stddev.jpg │ ├── point2d.jpg │ ├── stacked-area-normalize.jpg │ ├── stacked-area-stream.jpg │ ├── stacked-area.jpg │ ├── stacked-bar-h.jpg │ ├── stacked-bar-layer.jpg │ ├── stacked-bar-normalize.jpg │ ├── stacked-bar-weather.jpg │ ├── table-binned-heatmap-marginal.jpg │ ├── table-binned-heatmap.jpg │ ├── text-scatterplot-colored.jpg │ ├── tick-dot.jpg │ ├── tick-strip.jpg │ └── trail-color.jpg ├── violin.jpg └── violins.jpg ├── sketches ├── bayesian_optimisation.clj ├── examples.clj ├── lattice.clj └── vega.clj └── src ├── cljplot ├── axis.clj ├── build.clj ├── common.clj ├── config.clj ├── core.clj ├── impl │ ├── free.clj │ ├── heatmap.clj │ ├── histogram.clj │ ├── label.clj │ ├── line.clj │ ├── math.clj │ ├── scatter.clj │ ├── strips.clj │ └── time_series.clj ├── render.clj └── scale.clj └── marchingsquares ├── Algorithm.java ├── Cell.java ├── Grid.java └── PathGenerator.java /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:config-paths ["../resources/clj-kondo.exports/cljplot/cljplot"]} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *.jar 4 | *.class 5 | /lib/ 6 | /classes/ 7 | /target/ 8 | /checkouts/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .lein-failures 13 | .nrepl-port 14 | .cpcache/ 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CLJPLOT 2 | 3 | > **_INFO (2020-08-17):_** I'm preparing new version based on tech.ml.dataset as data input which is going to solve issues with data processing and labels. Also rethinking the way of better management of data dimensions and their representation through trellis/colors/axes etc. It happens very slowly, but happens. Stay tuned. 4 | 5 |
6 |
7 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
Implementation of the Marching Squares algorithm described in: 15 | * {@code https://en.wikipedia.org/wiki/Marching_squares}
16 | */ 17 | public class Algorithm 18 | { 19 | private static final ExecutorService ES = Executors.newCachedThreadPool(); 20 | double[] isovalues; 21 | public double min,max; 22 | final double[][] data; 23 | 24 | public Algorithm(final double[][] data) throws IllegalArgumentException { 25 | super(); 26 | double min = data[0][0]; 27 | double max = min; 28 | int rowCount = data.length; 29 | int colCount = data[0].length; 30 | double here; 31 | for (int i = 0; i < rowCount; i++) { 32 | for (int j = 0; j < colCount; j++) { 33 | here = data[i][j]; 34 | min = Math.min(min, here); 35 | max = Math.max(max, here); 36 | } 37 | } 38 | 39 | if (min == max) { 40 | throw new IllegalArgumentException("All values are equal. Cannot build contours for a constant field"); 41 | } 42 | 43 | this.min = min; 44 | this.max = max; 45 | this.data = pad(data, min - 1.0); 46 | 47 | } 48 | 49 | private static Grid contour(double[][] data, double isovalue) { 50 | final int rowCount = data.length; 51 | final int colCount = data[0].length; 52 | 53 | // Every 2x2 block of pixels in the binary image forms a contouring cell, 54 | // so the whole image is represented by a grid of such cells. Note that 55 | // this contouring grid is one cell smaller in each direction than the 56 | // original 2D field. 57 | Cell[][] cells = new Cell[rowCount - 1][colCount - 1]; 58 | for (int r = 0; r < rowCount - 1; r++) { 59 | for (int c = 0; c < colCount - 1; c++) { 60 | // Compose the 4 bits at the corners of the cell to build a binary 61 | // index: walk around the cell in a clockwise direction appending 62 | // the bit to the index, using bitwise OR and left-shift, from most 63 | // significant bit at the top left, to least significant bit at the 64 | // bottom left. The resulting 4-bit index can have 16 possible 65 | // values in the range 0-15. 66 | int ndx = 0; 67 | final double tl = data[r + 1][c ]; 68 | final double tr = data[r + 1][c + 1]; 69 | final double br = data[r ][c + 1]; 70 | final double bl = data[r ][c ]; 71 | ndx |= (tl > isovalue ? 0 : 8); 72 | ndx |= (tr > isovalue ? 0 : 4); 73 | ndx |= (br > isovalue ? 0 : 2); 74 | ndx |= (bl > isovalue ? 0 : 1); 75 | boolean flipped = false; 76 | if (ndx == 5 || ndx == 10) { 77 | // resolve the ambiguity by using the average data value for the 78 | // center of the cell to choose between different connections of 79 | // the interpolated points. 80 | double center = (tl + tr + br + bl) / 4.0; 81 | if (ndx == 5 && center < isovalue) { 82 | flipped = true; 83 | } else if (ndx == 10 && center < isovalue) { 84 | flipped = true; 85 | } 86 | } 87 | // NOTE (rsn) - we only populate the grid w/ non-trivial cells; 88 | // i.e. those w/ an index different than 0 and 15. 89 | if (ndx != 0 && ndx != 15) { 90 | // Apply linear interpolation between the original field data 91 | // values to find the exact position of the contour line along 92 | // the edges of the cell. 93 | float left = 0.5F; 94 | float top = 0.5F; 95 | float right = 0.5F; 96 | float bottom = 0.5F; 97 | switch (ndx) { 98 | case 1: 99 | left = (float)((isovalue - bl) / (tl - bl)); 100 | bottom = (float)((isovalue - bl) / (br - bl)); 101 | break; 102 | case 2: 103 | bottom = (float)((isovalue - bl) / (br - bl)); 104 | right = (float)((isovalue - br) / (tr - br)); 105 | break; 106 | case 3: 107 | left = (float)((isovalue - bl) / (tl - bl)); 108 | right = (float)((isovalue - br) / (tr - br)); 109 | break; 110 | case 4: 111 | top = (float)((isovalue - tl) / (tr - tl)); 112 | right = (float)((isovalue - br) / (tr - br)); 113 | break; 114 | case 5: 115 | left = (float)((isovalue - bl) / (tl - bl)); 116 | bottom = (float)((isovalue - bl) / (br - bl)); 117 | top = (float)((isovalue - tl) / (tr - tl)); 118 | right = (float)((isovalue - br) / (tr - br)); 119 | break; 120 | case 6: 121 | bottom = (float)((isovalue - bl) / (br - bl)); 122 | top = (float)((isovalue - tl) / (tr - tl)); 123 | break; 124 | case 7: 125 | left = (float)((isovalue - bl) / (tl - bl)); 126 | top = (float)((isovalue - tl) / (tr - tl)); 127 | break; 128 | case 8: 129 | left = (float)((isovalue - bl) / (tl - bl)); 130 | top = (float)((isovalue - tl) / (tr - tl)); 131 | break; 132 | case 9: 133 | bottom = (float)((isovalue - bl) / (br - bl)); 134 | top = (float)((isovalue - tl) / (tr - tl)); 135 | break; 136 | case 10: 137 | left = (float)((isovalue - bl) / (tl - bl)); 138 | bottom = (float)((isovalue - bl) / (br - bl)); 139 | top = (float)((isovalue - tl) / (tr - tl)); 140 | right = (float)((isovalue - br) / (tr - br)); 141 | break; 142 | case 11: 143 | top = (float)((isovalue - tl) / (tr - tl)); 144 | right = (float)((isovalue - br) / (tr - br)); 145 | break; 146 | case 12: 147 | left = (float)((isovalue - bl) / (tl - bl)); 148 | right = (float)((isovalue - br) / (tr - br)); 149 | break; 150 | case 13: 151 | bottom = (float)((isovalue - bl) / (br - bl)); 152 | right = (float)((isovalue - br) / (tr - br)); 153 | break; 154 | case 14: 155 | left = (float)((isovalue - bl) / (tl - bl)); 156 | bottom = (float)((isovalue - bl) / (br - bl)); 157 | break; 158 | default: // shouldn't happen 159 | final String m = "Unexpected cell index " + ndx; 160 | throw new IllegalStateException(m); 161 | } 162 | 163 | cells[r][c] = new Cell(ndx, flipped, left, top, right, bottom); 164 | } 165 | } 166 | } 167 | final Grid result = new Grid(cells, isovalue); 168 | return result; 169 | } 170 | 171 | /** 172 | *Pad data with a given 'guard' value.
173 | * 174 | * @param data matrix to pad. 175 | * @param guard the value to use for padding. It's expected to be less than 176 | * the minimum of all data cell values. 177 | * @return the resulting padded matrix which will be larger by 2 in both 178 | * directions. 179 | */ 180 | private static double[][] pad(double[][] data, double guard) { 181 | final int rowCount = data.length; 182 | final int colCount = data[0].length; 183 | double[][] result = new double[rowCount + 2][colCount + 2]; 184 | 185 | // top and bottom rows 186 | for (int j = 0; j < colCount + 2; j++) { 187 | result[0][j] = guard; 188 | result[rowCount + 1][j] = guard; 189 | } 190 | 191 | // left- and right-most columns excl. top and bottom rows 192 | for (int i = 1; i < rowCount + 1; i++) { 193 | result[i][0] = guard; 194 | result[i][colCount + 1] = guard; 195 | } 196 | 197 | // the middle 198 | for (int i = 0; i < rowCount; i++) { 199 | System.arraycopy(data[i], 0, result[i + 1], 1, colCount); 200 | } 201 | 202 | return result; 203 | } 204 | 205 | public GeneralPath[] buildContours(final double[] levels) 206 | throws InterruptedException, ExecutionException { 207 | isovalues = levels; 208 | 209 | return doConcurrent(); 210 | } 211 | 212 | private GeneralPath[] doConcurrent() 213 | throws InterruptedException, ExecutionException { 214 | final CollectionA non-immutable class describing a Marching Squares Contour Cell.
5 | */ 6 | class Cell 7 | { 8 | static enum Side { LEFT, RIGHT, TOP, BOTTOM, NONE } 9 | private byte cellNdx; 10 | private final boolean flipped; 11 | private final float left, top, right, bottom; 12 | 13 | 14 | Cell(int ndx, boolean flipped, float left, float top, float right, float bottom) { 15 | super(); 16 | this.cellNdx = (byte) ndx; 17 | if (flipped && ndx != 5 && ndx != 10) { 18 | System.out.println("Cell: Only saddle cells can be flipped. Will set the " 19 | + "'flipped' flag to FALSE for this (" + ndx + ") cell's index"); 20 | this.flipped = false; 21 | } else { 22 | this.flipped = flipped; 23 | } 24 | this.left = left; 25 | this.top = top; 26 | this.right = right; 27 | this.bottom = bottom; 28 | } 29 | 30 | /** 31 | *Clear this cell's index.
32 | * 33 | *When building up shapes, it is possible to have disjoint regions and 34 | * holes in them. An easy way to build up a new shape from the cell's index 35 | * is to build sub-paths for one isoline at a time. As the shape is built 36 | * up, it is necessary to erase the (single) line afterward so that subsequent 37 | * searches for isolines will not loop indefinitely. 38 | */ 39 | void clear() { 40 | switch (cellNdx) { 41 | case 0: 42 | case 5: 43 | case 10: 44 | case 15: 45 | break; 46 | default: 47 | cellNdx = 15; 48 | } 49 | } 50 | 51 | /** @return this cell's algorithm index. */ 52 | byte getCellNdx() { 53 | return cellNdx; 54 | } 55 | 56 | /** 57 | * @param edge which side crossing is wanted. 58 | * @return crossing coordinates (already) normalized to [0.0..1.0]. 59 | */ 60 | float[] getXY(Side edge) { 61 | switch (edge) { 62 | case BOTTOM: return new float[] { bottom, 0.0F }; 63 | case LEFT: return new float[] { 0.0F, left }; 64 | case RIGHT: return new float[] { 1.0F, right }; 65 | case TOP: return new float[] { top, 1.0F }; 66 | default: 67 | throw new IllegalStateException("getXY: N/A w/o a non-trivial edge"); 68 | } 69 | } 70 | 71 | /** @return true if this Cell is a Saddle case. Returns false otherwise. */ 72 | boolean isSaddle() { 73 | return cellNdx == 5 || cellNdx == 10; 74 | } 75 | 76 | /** @return true if this Cell is trivial; otherwise returns false. */ 77 | boolean isTrivial() { 78 | return cellNdx == 0 || cellNdx == 15; 79 | } 80 | 81 | /** @return whether this cell is flipped or not. */ 82 | boolean isFlipped() { 83 | return flipped; 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | return new StringBuilder("Cell{index=").append(cellNdx) 89 | .append(", flipped? ").append(flipped) 90 | .append(", left=").append(left) 91 | .append(", top=").append(top) 92 | .append(", right=").append(right) 93 | .append(", bottom=").append(bottom) 94 | .append('}') 95 | .toString(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/marchingsquares/Grid.java: -------------------------------------------------------------------------------- 1 | package marchingsquares; 2 | 3 | /** 4 | *
Given a two-dimensional scalar field (rectangular array of individual 5 | * numerical values) the Marching Squares algorithm applies a threshold 6 | * (a.k.a contour level or isovalue) to make a binary image containing:
7 | * 8 | *Every 2x2 block of pixels in the binary image forms a contouring cell, so 14 | * the whole image is represented by a grid of such cells (shown in green in 15 | * the picture below). Note that this contouring grid is one cell smaller in 16 | * each direction than the original 2D data field.
17 | */ 18 | class Grid 19 | { 20 | final Cell[][] cells; 21 | final int rowCount; 22 | final int colCount; 23 | final double threshold; 24 | private transient String str; 25 | 26 | 27 | Grid(final Cell[][] cells, final double threshold) { 28 | super(); 29 | this.cells = cells; 30 | rowCount = cells.length; 31 | colCount = cells[0].length; 32 | this.threshold = threshold; 33 | } 34 | 35 | Cell getCellAt(final int r, final int c) { 36 | return cells[r][c]; 37 | } 38 | 39 | int getCellNdxAt(final int r, final int c) { 40 | final Cell cell = cells[r][c]; 41 | if (cell == null) return 0; 42 | return cells[r][c].getCellNdx(); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | if (str == null) { 48 | str = new StringBuilder("Grid{rowCount=").append(rowCount) 49 | .append(", colCount=").append(colCount) 50 | .append(", threshold=").append(threshold) 51 | .append('}') 52 | .toString(); 53 | } 54 | return str; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/marchingsquares/PathGenerator.java: -------------------------------------------------------------------------------- 1 | package marchingsquares; 2 | 3 | import static marchingsquares.Cell.Side.BOTTOM; 4 | import static marchingsquares.Cell.Side.LEFT; 5 | import static marchingsquares.Cell.Side.NONE; 6 | import static marchingsquares.Cell.Side.RIGHT; 7 | import static marchingsquares.Cell.Side.TOP; 8 | 9 | import java.awt.geom.GeneralPath; 10 | 11 | import marchingsquares.Cell.Side; 12 | 13 | /** 14 | *An object that knows how to translate a Grid of Marching Squares Contour 15 | * Cells into a Java AWT General Path.
16 | */ 17 | public class PathGenerator 18 | { 19 | private static final double EPSILON = 1E-7; 20 | 21 | 22 | PathGenerator() { 23 | super(); 24 | } 25 | 26 | /** 27 | *Construct a GeneralPath representing the isoline, itself represented 28 | * by a given Grid.
29 | * 30 | *IMPLEMENTATION NOTE: This method is destructive. It alters 31 | * the Grid instance as it generates the resulting path. If the 'original' 32 | * Grid instance is needed after invoking this method then it's the 33 | * responsibility of the caller to deep clone it before passing it here.
34 | * 35 | * @param grid the matrix of contour cells w/ side crossing coordinates 36 | * already interpolated and normalized; i.e. in the range 0.0..1.0. 37 | * @return the geometries of a contour, including sub-path(s) for disjoint 38 | * areas and holes. 39 | */ 40 | GeneralPath generate(final Grid grid) { 41 | GeneralPath result = new GeneralPath(GeneralPath.WIND_EVEN_ODD); 42 | for (int r = 0; r < grid.rowCount; r++) { 43 | for (int c = 0; c < grid.colCount; c++) { 44 | // find a start node... 45 | final Cell cell = grid.getCellAt(r, c); 46 | if (cell != null && !cell.isTrivial() && !cell.isSaddle()) { 47 | // complete the [sub-]path and close it 48 | update(grid, r, c, result); 49 | } 50 | } 51 | } 52 | return result; 53 | } 54 | 55 | /** 56 | *Return the first side that should be used in a CCW traversal.
57 | * 58 | * @param cell the Cell to process. 59 | * @param prev previous side, only used for saddle cells. 60 | * @return the 1st side of the line segment of the designated cell. 61 | */ 62 | private Side firstSide(final Cell cell, final Side prev) { 63 | switch (cell.getCellNdx()) { 64 | case 1: 65 | case 3: 66 | case 7: 67 | return LEFT; 68 | case 2: 69 | case 6: 70 | case 14: 71 | return BOTTOM; 72 | case 4: 73 | case 11: 74 | case 12: 75 | case 13: 76 | return RIGHT; 77 | case 8: 78 | case 9: 79 | return TOP; 80 | case 5: 81 | switch (prev) { 82 | case LEFT: return RIGHT; 83 | case RIGHT: return LEFT; 84 | default: 85 | final String m = "Saddle w/ no connected neighbour; Cell = " + cell 86 | + ", previous side = " + prev; 87 | System.err.println("firstSide: " + m + ". Throw ISE"); 88 | throw new IllegalStateException(m); 89 | } 90 | case 10: 91 | switch (prev) { 92 | case BOTTOM: return TOP; 93 | case TOP: return BOTTOM; 94 | default: 95 | final String m = "Saddle w/ no connected neighbour; Cell = " + cell 96 | + ", previous side = " + prev; 97 | System.err.println("firstSide: " + m + ". Throw ISE"); 98 | throw new IllegalStateException(m); 99 | } 100 | default: 101 | final String m = "Attempt to use a trivial cell as a start node: " + cell; 102 | System.err.println("firstSide: " + m + ". Throw ISE"); 103 | throw new IllegalStateException(m); 104 | } 105 | } 106 | 107 | /** 108 | *Find the side on which lies the next cell to use in a CCW traversal.
109 | * 110 | * @param cell the Cell to process. 111 | * @param prev previous side, only used for saddle cells. 112 | * @return side where the next cell is to be picked. 113 | */ 114 | private Side nextSide(final Cell cell, final Side prev) { 115 | return secondSide(cell, prev); 116 | } 117 | 118 | /** 119 | *Return the second side that should be used in a CCW traversal.
120 | * 121 | * @param cell the Cell to process. 122 | * @param prev previous side, only used for saddle cells. 123 | * @return the 2nd side of the line segment of the designated cell. 124 | */ 125 | private Side secondSide(final Cell cell, final Side prev) { 126 | switch (cell.getCellNdx()) { 127 | case 8: 128 | case 12: 129 | case 14: 130 | return LEFT; 131 | case 1: 132 | case 9: 133 | case 13: 134 | return BOTTOM; 135 | case 2: 136 | case 3: 137 | case 11: 138 | return RIGHT; 139 | case 4: 140 | case 6: 141 | case 7: 142 | return TOP; 143 | case 5: 144 | switch (prev) { 145 | case LEFT: return cell.isFlipped() ? BOTTOM : TOP; 146 | case RIGHT: return cell.isFlipped() ? TOP : BOTTOM; 147 | default: 148 | final String m = "Saddle w/ no connected neighbour; Cell = " + cell 149 | + ", previous side = " + prev; 150 | System.err.println("secondSide: " + m + ". Throw ISE"); 151 | throw new IllegalStateException(m); 152 | } 153 | case 10: 154 | switch (prev) { 155 | case BOTTOM: return cell.isFlipped() ? RIGHT : LEFT; 156 | case TOP: return cell.isFlipped() ? LEFT : RIGHT; 157 | default: 158 | final String m = "Saddle w/ no connected neighbour; Cell = " + cell 159 | + ", previous side = " + prev; 160 | System.err.println("secondSide: " + m + ". Throw ISE"); 161 | throw new IllegalStateException(m); 162 | } 163 | default: 164 | final String m = "Attempt to use a trivial Cell as a node: " + cell; 165 | System.err.println("secondSide: " + m + ". Throw ISE"); 166 | throw new IllegalStateException(m); 167 | } 168 | } 169 | 170 | /** 171 | *A given contour can be made up of multiple disconnected regions, each 172 | * potentially having multiple holes. Both regions and holes are captured as 173 | * individual sub-paths.
174 | * 175 | *The process is iterative. It starts w/ an empty GeneralPath instance 176 | * and continues until all Cells are processed. With every invocation the 177 | * GeneralPath object is updated to reflect the new sub-path(s).
178 | * 179 | *Once a non-saddle cell is used it is cleared so as to ensure it will 180 | * not be re-used when finding sub-paths w/in the original path.
181 | * 182 | * @param grid on input the matrix of cells representing a given contour. 183 | * Note that the process will alter the Cells, so on output the original 184 | * Grid instance _will_ be modified. In other words this method is NOT 185 | * idempotent when using the same object references and values. 186 | * @param r row index of the start Cell. 187 | * @param c column index of the start Cell. 188 | * @param path a non-null GeneralPath instance to update. 189 | */ 190 | private void update(Grid grid, int r, int c, GeneralPath path) { 191 | Side prevSide = NONE; 192 | 193 | Cell start = grid.getCellAt(r, c); 194 | float[] pt = start.getXY(firstSide(start, prevSide)); 195 | float x = c + pt[0]; // may throw NPE 196 | float y = r + pt[1]; // likewise 197 | path.moveTo(x, y); // prepare for a new sub-path 198 | 199 | pt = start.getXY(secondSide(start, prevSide)); 200 | float xPrev = c + pt[0]; 201 | float yPrev = r + pt[1]; 202 | 203 | prevSide = nextSide(start, prevSide); 204 | switch (prevSide) { 205 | case BOTTOM: r--; break; 206 | case LEFT: c--; break; 207 | case RIGHT: c++; break; 208 | case TOP: r++; // fall through 209 | default: // keeps compiler happy + handle NONE case which should never happen 210 | break; 211 | } 212 | start.clear(); 213 | 214 | Cell currentCell = grid.getCellAt(r, c); 215 | while (start != currentCell) { // we want object reference equality 216 | pt = currentCell.getXY(secondSide(currentCell, prevSide)); 217 | x = c + pt[0]; 218 | y = r + pt[1]; 219 | if (Math.abs(x - xPrev) > EPSILON && Math.abs(y - yPrev) > EPSILON) { 220 | path.lineTo(x, y); 221 | } 222 | xPrev = x; 223 | yPrev = y; 224 | prevSide = nextSide(currentCell, prevSide); 225 | switch (prevSide) { 226 | case BOTTOM: r--; break; 227 | case LEFT: c--; break; 228 | case RIGHT: c++; break; 229 | case TOP: r++; break; 230 | default: 231 | System.out.println("update: Potential loop! Current cell = " 232 | + currentCell + ", previous side = " + prevSide); 233 | break; 234 | } 235 | currentCell.clear(); 236 | currentCell = grid.getCellAt(r, c); 237 | } 238 | 239 | path.closePath(); 240 | } 241 | } 242 | --------------------------------------------------------------------------------