001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.vis.ternary;
031
032import java.text.AttributedCharacterIterator.Attribute;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.Comparator;
036import java.util.HashMap;
037import java.util.Iterator;
038import java.util.List;
039import java.util.Map;
040
041import org.openimaj.feature.DoubleFV;
042import org.openimaj.feature.DoubleFVComparison;
043import org.openimaj.image.DisplayUtilities;
044import org.openimaj.image.MBFImage;
045import org.openimaj.image.colour.ColourMap;
046import org.openimaj.image.colour.ColourSpace;
047import org.openimaj.image.colour.RGBColour;
048import org.openimaj.image.typography.FontRenderer;
049import org.openimaj.image.typography.FontStyle;
050import org.openimaj.image.typography.FontStyle.HorizontalAlignment;
051import org.openimaj.image.typography.FontStyle.VerticalAlignment;
052import org.openimaj.math.geometry.line.Line2d;
053import org.openimaj.math.geometry.point.Point2d;
054import org.openimaj.math.geometry.point.Point2dImpl;
055import org.openimaj.math.geometry.shape.Rectangle;
056import org.openimaj.math.geometry.shape.Triangle;
057import org.openimaj.math.geometry.transforms.TransformUtilities;
058import org.openimaj.math.geometry.triangulation.DelaunayTriangulator;
059import org.openimaj.math.util.Interpolation;
060import org.openimaj.util.pair.IndependentPair;
061
062/**
063 * A ternary plot draws a triangle simplex. The values of the triangle are
064 * interpolated from a few {@link TernaryData} points provided.
065 * 
066 * @author Sina Samangooei (ss@ecs.soton.ac.uk)
067 */
068public class TernaryPlot {
069        private static final float ONE_OVER_ROOT3 = (float) (1f / Math.sqrt(3));
070
071        /**
072         * Holds an a value for the 3 ternary dimensions and a value
073         * 
074         * @author Sina Samangooei (ss@ecs.soton.ac.uk)
075         */
076        public static class TernaryData extends DoubleFV {
077                /**
078                 *
079                 */
080                private static final long serialVersionUID = 4560404458888209082L;
081
082                /**
083                 * @param a
084                 * @param b
085                 * @param c
086                 * @param value
087                 */
088                public TernaryData(float a, float b, float c, float value) {
089                        this.values = new double[] { a, b, c };
090                        this.value = value;
091
092                }
093
094                /**
095                 * @return the ternary point projected into 2D
096                 */
097                public Point2d asPoint() {
098                        final double a = this.values[0];
099                        final double b = this.values[1];
100                        final double c = this.values[2];
101                        final double x = 0.5 * (2 * b + c) / (a + b + c);
102                        final double y = (Math.sqrt(3) / 2) * (c) / (a + b + c);
103
104                        return new Point2dImpl((float) x, (float) y);
105                }
106
107                /**
108                 * the value at a,b,c
109                 */
110                public float value;
111
112                @Override
113                public int hashCode() {
114                        return Arrays.hashCode(values);
115                }
116
117                @Override
118                public boolean equals(Object obj) {
119                        return obj instanceof TernaryData && this.hashCode() == obj.hashCode()
120                                        && this.value == ((TernaryData) obj).value;
121                }
122        }
123
124        /**
125         * A hash of triangles created from a list of
126         * 
127         * @author Sina Samangooei (ss@ecs.soton.ac.uk)
128         */
129        private static class TrenaryDataTriangles {
130                private HashMap<Triangle, List<TernaryData>> triToData;
131                private HashMap<Point2d, TernaryData> pointToTre;
132
133                public TrenaryDataTriangles(List<TernaryData> data) {
134                        this.pointToTre = new HashMap<Point2d, TernaryData>();
135                        for (final TernaryData trenaryData : data) {
136                                pointToTre.put(trenaryData.asPoint(), trenaryData);
137                        }
138                        this.triToData = new HashMap<Triangle, List<TernaryData>>();
139                        final List<Triangle> triangles = DelaunayTriangulator
140                                        .triangulate(new ArrayList<Point2d>(pointToTre.keySet()));
141                        for (final Triangle triangle : triangles) {
142                                final List<TernaryData> triangleData = new ArrayList<TernaryData>();
143                                triangleData.add(pointToTre.get(triangle.vertices[0]));
144                                triangleData.add(pointToTre.get(triangle.vertices[1]));
145                                triangleData.add(pointToTre.get(triangle.vertices[2]));
146
147                                triToData.put(triangle, triangleData);
148                        }
149                }
150
151                public Triangle getHoldingTriangle(Point2d point) {
152                        for (final Triangle t : this.triToData.keySet()) {
153                                if (t.isInsideOnLine(point)) {
154                                        return t;
155                                }
156                        }
157                        return null;
158                }
159
160                public TernaryData getPointData(Point2d point) {
161                        return pointToTre.get(point);
162                }
163        }
164
165        private Triangle tri;
166        private float height;
167        private float width;
168        private List<TernaryData> data;
169        private Point2dImpl pointA;
170        private Point2dImpl pointB;
171        private Point2dImpl pointC;
172        private TrenaryDataTriangles dataTriangles;
173
174        /**
175         * @param width
176         * @param data
177         */
178        public TernaryPlot(float width, List<TernaryData> data) {
179                this.width = width;
180                this.height = (float) Math.sqrt((width * width) - ((width * width) / 4));
181                pointA = new Point2dImpl(0, height);
182                pointB = new Point2dImpl(width, height);
183                pointC = new Point2dImpl(width / 2, 0);
184
185                this.tri = new Triangle(new Point2d[] {
186                                pointA,
187                                pointB,
188                                pointC,
189                });
190
191                this.data = data;
192                if (data.size() > 2) {
193                        this.dataTriangles = new TrenaryDataTriangles(data);
194                }
195        }
196
197        /**
198         * @return {@link #draw(TernaryParams)} with the defaults of
199         *         {@link TernaryParams}
200         */
201        public MBFImage draw() {
202                return draw(new TernaryParams());
203
204        }
205
206        /**
207         * @param params
208         * @return draw the plot
209         */
210        public MBFImage draw(TernaryParams params) {
211
212                final int padding = (Integer) params.getTyped(TernaryParams.PADDING);
213                final Float[] bgColour = params.getTyped(TernaryParams.BG_COLOUR);
214
215                final MBFImage ret = new MBFImage((int) width + padding * 2, (int) height + padding * 2, ColourSpace.RGB);
216                ret.fill(bgColour);
217                drawTernaryPlot(ret, params);
218                drawTriangle(ret, params);
219                drawBorder(ret, params);
220                drawScale(ret, params);
221                drawLabels(ret, params);
222
223                return ret;
224        }
225
226        private void drawScale(MBFImage ret, TernaryParams params) {
227                final boolean drawScale = (Boolean) params.getTyped(TernaryParams.DRAW_SCALE);
228                if (!drawScale)
229                        return;
230
231                final Map<? extends Attribute, Object> typed = params.getTyped(TernaryParams.SCALE_FONT);
232                final FontStyle<Float[]> fs = FontStyle.parseAttributes(typed, ret.createRenderer());
233
234                final int padding = (Integer) params.getTyped(TernaryParams.PADDING);
235                final ColourMap cm = params.getTyped(TernaryParams.COLOUR_MAP);
236                final Rectangle r = ret.getBounds();
237                r.width = r.width / 2.f;
238                r.height = r.height * 2.f;
239                r.scale(0.15f);
240                r.x = width * TernaryParams.TOP_RIGHT_X;
241                r.y = height * TernaryParams.TOP_RIGHT_Y;
242                r.translate(padding, padding);
243                ret.drawShape(r, 2, RGBColour.BLACK);
244                for (float i = r.y; i < r.y + r.height; i++) {
245                        final Float[] col = cm.apply(((i - r.y) / r.height));
246                        ret.drawLine((int) r.x, (int) i, (int) (r.x + r.width), (int) i, col);
247                }
248                fs.setVerticalAlignment(VerticalAlignment.VERTICAL_BOTTOM);
249                final String minText = params.getTyped(TernaryParams.SCALE_MIN);
250                ret.drawText(minText, (int) r.x - 3, (int) (r.y + r.height), fs);
251                fs.setVerticalAlignment(VerticalAlignment.VERTICAL_TOP);
252                final String maxText = params.getTyped(TernaryParams.SCALE_MAX);
253                ret.drawText(maxText, (int) r.x - 3, (int) r.y, fs);
254        }
255
256        private void drawBorder(MBFImage ret, TernaryParams params) {
257                final int padding = (Integer) params.getTyped(TernaryParams.PADDING);
258                final boolean drawTicks = (Boolean) params.getTyped(TernaryParams.TRIANGLE_BORDER_TICKS);
259                final Map<Attribute, Object> fontParams = params.getTyped(TernaryParams.TICK_FONT);
260                final FontStyle<Float[]> style = FontStyle.parseAttributes(fontParams, ret.createRenderer());
261                if (drawTicks) {
262                        final Triangle drawTri = tri.transform(TransformUtilities.translateMatrix(padding, padding));
263
264                        for (int i = 0; i < 3; i++) {
265                                int paddingx = 0;
266                                int paddingy = 0;
267                                switch (i) {
268                                case 0:
269                                        // the bottom line
270                                        style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_CENTER);
271                                        style.setVerticalAlignment(VerticalAlignment.VERTICAL_TOP);
272                                        paddingy = 5;
273                                        break;
274                                case 1:
275                                        // the right line
276                                        style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_LEFT);
277                                        style.setVerticalAlignment(VerticalAlignment.VERTICAL_HALF);
278                                        paddingx = 5;
279                                        paddingy = -5;
280                                        break;
281                                case 2:
282                                        // the left line
283                                        style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_RIGHT);
284                                        style.setVerticalAlignment(VerticalAlignment.VERTICAL_HALF);
285                                        paddingx = -5;
286                                        paddingy = -5;
287                                        break;
288                                }
289                                final Point2d start = drawTri.vertices[i];
290                                final Point2d end = drawTri.vertices[(i + 1) % 3];
291                                final int nTicks = 10;
292                                for (int j = 0; j < nTicks + 1; j++) {
293                                        Line2d tickLine = new Line2d(start, end);
294                                        final double length = tickLine.calculateLength();
295                                        // bring its end to the correct position
296                                        double desired = length - j * (length / nTicks);
297                                        if (desired == 0)
298                                                desired = 0.001;
299                                        double scale = desired / length;
300                                        // double overallScale = scale;
301                                        tickLine = tickLine.transform(TransformUtilities.scaleMatrixAboutPoint(scale, scale, start));
302                                        // make it 10 pixels long
303                                        scale = 5f / tickLine.calculateLength();
304                                        tickLine = tickLine.transform(TransformUtilities.scaleMatrixAboutPoint(scale, scale, tickLine.end));
305                                        // Now rotate it by 90 degrees
306                                        tickLine = tickLine.transform(TransformUtilities.rotationMatrixAboutPoint(-Math.PI / 2,
307                                                        tickLine.end.getX(), tickLine.end.getY()));
308                                        final int thickness = (Integer) params.getTyped(TernaryParams.TRIANGLE_BORDER_TICK_THICKNESS);
309                                        final Float[] col = params.getTyped(TernaryParams.TRIANGLE_BORDER_COLOUR);
310                                        ret.drawLine(tickLine, thickness, col);
311
312                                        final Point2d textPoint = tickLine.begin.copy();
313                                        textPoint.translate(paddingx, paddingy);
314                                        // ret.drawText(String.format("%2.2f",overallScale),
315                                        // textPoint, style);
316                                }
317
318                        }
319                }
320        }
321
322        private void drawTriangle(MBFImage ret, TernaryParams params) {
323                final int padding = (Integer) params.getTyped(TernaryParams.PADDING);
324                final boolean drawTriangle = (Boolean) params.getTyped(TernaryParams.TRIANGLE_BORDER);
325                if (drawTriangle) {
326                        final int thickness = (Integer) params.getTyped(TernaryParams.TRIANGLE_BORDER_THICKNESS);
327                        final Float[] col = params.getTyped(TernaryParams.TRIANGLE_BORDER_COLOUR);
328                        ret.drawShape(this.tri.transform(TransformUtilities.translateMatrix(padding, padding)), thickness, col);
329                }
330        }
331
332        private void drawLabels(MBFImage ret, TernaryParams params) {
333                final int padding = (Integer) params.getTyped(TernaryParams.PADDING);
334                final List<IndependentPair<TernaryData, String>> labels = params.getTyped(TernaryParams.LABELS);
335                final Map<? extends Attribute, Object> typed = params.getTyped(TernaryParams.LABEL_FONT);
336                final FontStyle<Float[]> fs = FontStyle.parseAttributes(typed, ret.createRenderer());
337                final Float[] labelBackground = params.getTyped(TernaryParams.LABEL_BACKGROUND);
338                final Float[] labelBorder = params.getTyped(TernaryParams.LABEL_BORDER);
339                final int labelPadding = (Integer) params.getTyped(TernaryParams.LABEL_PADDING);
340                final FontRenderer<Float[], FontStyle<Float[]>> fontRenderer = fs.getRenderer(ret.createRenderer());
341                if (labels != null) {
342                        for (final IndependentPair<TernaryData, String> labelPoint : labels) {
343                                final TernaryData ternaryData = labelPoint.firstObject();
344                                final Point2d point = ternaryData.asPoint();
345                                point.setX(point.getX() * width + padding);
346                                point.setY(height - (point.getY() * width) + padding);
347                                final Point2d p = point.copy();
348                                if (point.getY() < height / 2) {
349                                        point.setY(point.getY() - 10);
350                                }
351                                else {
352                                        point.setY(point.getY() + 35);
353                                }
354                                final Rectangle rect = fontRenderer.getBounds(labelPoint.getSecondObject(), (int) point.getX(),
355                                                (int) point.getY(), fs);
356                                rect.x -= labelPadding;
357                                rect.y -= labelPadding;
358                                rect.width += labelPadding * 2;
359                                rect.height += labelPadding * 2;
360                                if (labelBackground != null) {
361                                        ret.drawShapeFilled(rect, labelBackground);
362                                }
363                                if (labelBorder != null) {
364                                        ret.drawShape(rect, labelBorder);
365                                }
366                                ret.drawText(labelPoint.getSecondObject(), point, fs);
367                                ret.drawPoint(p, RGBColour.RED, (int) ternaryData.value);
368                        }
369                }
370        }
371
372        private void drawTernaryPlot(MBFImage ret, TernaryParams params) {
373                final ColourMap cm = params.getTyped(TernaryParams.COLOUR_MAP);
374                final int padding = (Integer) params.getTyped(TernaryParams.PADDING);
375                final Float[] bgColour = params.getTyped(TernaryParams.BG_COLOUR);
376                for (int y = 0; y < height + padding; y++) {
377                        for (int x = 0; x < width + padding; x++) {
378                                final int xp = x - padding;
379                                final int yp = y - padding;
380                                final Point2dImpl point = new Point2dImpl(xp, yp);
381                                if (this.tri.isInside(point)) {
382                                        final TernaryData closest = weightThreeClosest(point);
383                                        Float[] apply = null;
384                                        if (cm != null)
385                                                apply = cm.apply(1 - closest.value);
386                                        else {
387                                                apply = new Float[] { closest.value, closest.value, closest.value };
388                                        }
389
390                                        ret.setPixel(x, y, apply);
391                                }
392                                else {
393                                        ret.setPixel(x, y, bgColour);
394                                }
395                        }
396                }
397        }
398
399        /**
400         * @return draw the triangles generated from the data
401         */
402        public MBFImage drawTriangles() {
403                final MBFImage img = new MBFImage((int) width, (int) height, ColourSpace.RGB);
404                for (final Triangle tri : this.dataTriangles.triToData.keySet()) {
405                        img.drawShape(tri.transform(TransformUtilities.scaleMatrix(width, height)), RGBColour.RED);
406                }
407                return img;
408        }
409
410        class DistanceToPointComparator implements Comparator<TernaryData> {
411
412                private TernaryData terneryPoint;
413
414                public DistanceToPointComparator(TernaryData point) {
415
416                        this.terneryPoint = point;
417                }
418
419                @Override
420                public int compare(TernaryData o1, TernaryData o2) {
421                        final double o1d = DoubleFVComparison.EUCLIDEAN.compare(o1, this.terneryPoint);
422                        final double o2d = DoubleFVComparison.EUCLIDEAN.compare(o2, this.terneryPoint);
423                        return Double.compare(o1d, o2d);
424                }
425
426        }
427
428        private float calcBfromXY(float xn, float yn) {
429                return xn - ONE_OVER_ROOT3 * yn;
430        }
431
432        private float calcCfromXY(float xn, float yn) {
433                return 2 * ONE_OVER_ROOT3 * yn;
434        }
435
436        private float calcAfromXY(float xn, float yn) {
437                return 1f - xn - ONE_OVER_ROOT3 * yn;
438        }
439
440        private TernaryData weightThreeClosest(Point2dImpl point) {
441                final float xn = (point.x - pointA.x) / width;
442                final float yn = (pointA.y - point.y) / width;
443
444                final float a = calcAfromXY(xn, yn);
445                final float b = calcBfromXY(xn, yn);
446                final float c = calcCfromXY(xn, yn);
447                final TernaryData trenData = new TernaryData(a, b, c, 0f);
448                if (data.size() == 1) {
449                        return data.get(0);
450                } else if (data.size() == 2) {
451                        final TernaryData tpa = data.get(0);
452                        final TernaryData tpb = data.get(1);
453                        final double da = DoubleFVComparison.EUCLIDEAN.compare(tpa, trenData);
454                        final double db = DoubleFVComparison.EUCLIDEAN.compare(tpb, trenData);
455                        final double sumd = da + db;
456                        trenData.value = (float) ((1 - (da / sumd)) * tpa.value + (1 - (db / sumd)) * tpb.value);
457                }
458                else {
459                        final Triangle t = dataTriangles.getHoldingTriangle(new Point2dImpl(xn, yn));
460                        if (t == null) {
461                                return new TernaryData(a, b, c, 0f);
462                        }
463                        final Map<Line2d, Point2d> points = t.intersectionSides(
464                                        new Line2d(
465                                                        new Point2dImpl(0, yn),
466                                                        new Point2dImpl(1, yn)
467                                                        )
468                                        );
469
470                        if (points.size() == 2) {
471                                final Iterator<Line2d> liter = points.keySet().iterator();
472                                final Line2d l1 = liter.next();
473                                final Line2d l2 = liter.next();
474                                final Point2d p1 = points.get(l1);
475                                final Point2d p2 = points.get(l2);
476
477                                final double p1Value = linePointInterp(l1, p1);
478                                final double p2Value = linePointInterp(l2, p2);
479
480                                final double pointValue = linePointInterp(new Line2d(p1, p2), new Point2dImpl(xn, yn), p1Value, p2Value);
481
482                                // if((l1.begin.getX() == l1.end.getX() || l2.begin.getX() ==
483                                // l2.end.getX() ) && pointValue <0.5){
484                                // System.out.println("A vertical line created a 0 value");
485                                // }
486
487                                trenData.value = (float) pointValue;
488
489                        }
490                        else { // 0, 1 or more than 2
491                                System.out.println("Found 3 or 0 lines: " + points.size());
492                                return new TernaryData(a, b, c, 0f);
493                        }
494                }
495                return trenData;
496        }
497
498        private double linePointInterp(Line2d line, Point2d point) {
499                final TernaryData l1p1data = dataTriangles.getPointData(line.begin);
500                final TernaryData l1p2data = dataTriangles.getPointData(line.end);
501                final float l1p1datav = l1p1data.value;
502                final float l1p2datav = l1p2data.value;
503
504                return linePointInterp(line, point, l1p1datav, l1p2datav);
505        }
506
507        private double linePointInterp(Line2d line, Point2d point, double lineBeginValue, double lineEndValue) {
508                final double l1Len = line.calculateLength();
509                final double l1Prop = Line2d.distance(line.begin, point);
510                final double p1Value = Interpolation.lerp(l1Prop, 0, lineBeginValue, l1Len, lineEndValue);
511                return p1Value;
512        }
513
514        /**
515         * @param args
516         */
517        public static void main(String[] args) {
518                final List<TernaryData> data = new ArrayList<TernaryData>();
519                data.add(new TernaryData(1 / 3f + 0.1f, 1 / 3f - 0.1f, 1 / 3f, 0.8f));
520                data.add(new TernaryData(1 / 3f - 0.1f, 1 / 3f + 0.1f, 1 / 3f, 0.2f));
521                data.add(new TernaryData(1f, 0, 0, 0));
522                data.add(new TernaryData(0, 1f, 0, 0));
523                data.add(new TernaryData(0, 0, 1f, 0));
524                final TernaryPlot plot = new TernaryPlot(500, data);
525                DisplayUtilities.display(plot.draw());
526                DisplayUtilities.display(plot.drawTriangles());
527        }
528
529}