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
064/**
065 * A ternary plot draws a triangle simplex. The values of the triangle are interpolated from
066 * a few {@link TernaryData} points provided. 
067 * @author Sina Samangooei (ss@ecs.soton.ac.uk)
068 */
069public class TernaryPlot {
070        private static final float ONE_OVER_ROOT3 = (float) (1f/Math.sqrt(3));
071        /**
072         * Holds an a value for the 3 ternary dimensions and a value
073         * @author Sina Samangooei (ss@ecs.soton.ac.uk)
074         */
075        public static class TernaryData extends DoubleFV {
076                /**
077                 * 
078                 */
079                private static final long serialVersionUID = 4560404458888209082L;
080
081                /**
082                 * @param a
083                 * @param b
084                 * @param c
085                 * @param value
086                 */
087                public TernaryData(float a, float b, float c, float value) {
088                        this.values = new double[]{a,b,c};
089                        this.value = value;
090                        
091                }
092                
093                /**
094                 * @return the ternary point projected into 2D
095                 */
096                public Point2d asPoint(){
097                        double a = this.values[0]; double b = this.values[1]; double c = this.values[2];
098                        double x = 0.5 * (2 * b + c ) / (a + b + c);
099                        double y = (Math.sqrt(3) / 2) * ( c ) / (a + b + c);
100                        
101                        return new Point2dImpl((float)x,(float)y);
102                }
103                
104                /**
105                 * the value at a,b,c
106                 */
107                public float value;
108                
109                @Override
110                public int hashCode() {
111                        return Arrays.hashCode(values);
112                }
113                @Override
114                public boolean equals(Object obj) {
115                        return obj instanceof TernaryData && this.hashCode() == obj.hashCode() && this.value == ((TernaryData)obj).value;
116                }
117        }
118        
119        /**
120         * A hash of triangles created from a list of 
121         * @author Sina Samangooei (ss@ecs.soton.ac.uk)
122         */
123        private static class TrenaryDataTriangles {
124                private HashMap<Triangle, List<TernaryData>> triToData;
125                private HashMap<Point2d, TernaryData> pointToTre;
126
127                public TrenaryDataTriangles(List<TernaryData> data){
128                        this.pointToTre = new HashMap<Point2d,TernaryData>();
129                        for (TernaryData trenaryData : data) {
130                                pointToTre.put(trenaryData.asPoint(), trenaryData);
131                        }
132                        this.triToData = new HashMap<Triangle,List<TernaryData>>();
133                        List<Triangle> triangles = DelaunayTriangulator.triangulate(new ArrayList<Point2d>(pointToTre.keySet()));
134                        for (Triangle triangle : triangles) {
135                                List<TernaryData> triangleData = new ArrayList<TernaryData>();
136                                triangleData.add(pointToTre.get(triangle.vertices[0]));
137                                triangleData.add(pointToTre.get(triangle.vertices[1]));
138                                triangleData.add(pointToTre.get(triangle.vertices[2]));
139                                
140                                triToData.put(triangle, triangleData);
141                        }
142                }
143
144                public Triangle getHoldingTriangle(Point2d point) {
145                        for (Triangle t : this.triToData.keySet()) {
146                                if(t.isInsideOnLine(point)){
147                                        return t;
148                                }
149                        }
150                        return null;
151                }
152
153                public TernaryData getPointData(Point2d point) {
154                        return pointToTre.get(point);
155                }
156        }
157        private Triangle tri;
158        private float height;
159        private float width;
160        private List<TernaryData> data;
161        private Point2dImpl pointA;
162        private Point2dImpl pointB;
163        private Point2dImpl pointC;
164        private TrenaryDataTriangles dataTriangles;
165        /**
166         * @param width
167         * @param data
168         */
169        public TernaryPlot(float width, List<TernaryData> data) {
170                this.width = width;
171                this.height = (float) Math.sqrt( (width*width) - ((width*width)/4) );
172                pointA = new Point2dImpl(0, height);
173                pointB = new Point2dImpl(width, height);
174                pointC = new Point2dImpl(width/2, 0);
175                
176                
177                this.tri = new Triangle(new Point2d[]{
178                                pointA,
179                                pointB,
180                                pointC,
181                });
182                
183                
184                this.data = data;
185                if(data.size() > 2){                 
186                        this.dataTriangles = new TrenaryDataTriangles(data);
187                }
188        }
189        
190        /**
191         * @return {@link #draw(TernaryParams)} with the defaults of {@link TernaryParams}
192         */
193        public MBFImage draw() {
194                return draw(new TernaryParams());
195                
196        }
197        
198        /**
199         * @param params
200         * @return draw the plot
201         */
202        public MBFImage draw(TernaryParams params) {
203                
204                int padding = (Integer) params.getTyped(TernaryParams.PADDING);
205                Float[] bgColour = params.getTyped(TernaryParams.BG_COLOUR);
206                
207                
208                MBFImage ret = new MBFImage((int)width + padding*2,(int)height + padding*2,ColourSpace.RGB);
209                ret.fill(bgColour);
210                drawTernaryPlot(ret,params);
211                drawTriangle(ret,params);
212                drawBorder(ret,params);
213                drawScale(ret,params);
214                drawLabels(ret,params);
215                
216                return ret;
217        }
218
219        private void drawScale(MBFImage ret, TernaryParams params) {
220                boolean drawScale = (Boolean) params.getTyped(TernaryParams.DRAW_SCALE);
221                if(!drawScale) return;
222                
223                Map<? extends Attribute, Object> typed = params.getTyped(TernaryParams.SCALE_FONT);
224                FontStyle<Float[]> fs = FontStyle.parseAttributes(typed,ret.createRenderer());
225                
226                int padding = (Integer) params.getTyped(TernaryParams.PADDING);
227                ColourMap cm = params.getTyped(TernaryParams.COLOUR_MAP);
228                Rectangle r = ret.getBounds();
229                r.width = r.width/2.f;
230                r.height = r.height * 2.f;
231                r.scale(0.15f);
232                r.x = width * TernaryParams.TOP_RIGHT_X;
233                r.y = height * TernaryParams.TOP_RIGHT_Y;
234                r.translate(padding, padding);
235                ret.drawShape(r, 2, RGBColour.BLACK);
236                for (float i = r.y; i < r.y + r.height; i++) {
237                        Float[] col = cm.apply(((i - r.y) / r.height));
238                        ret.drawLine((int)r.x, (int)i, (int)(r.x +r.width), (int)i, col);
239                }
240                fs.setVerticalAlignment(VerticalAlignment.VERTICAL_BOTTOM);
241                String minText = params.getTyped(TernaryParams.SCALE_MIN);
242                ret.drawText(minText , (int)r.x - 3, (int)(r.y + r.height), fs);
243                fs.setVerticalAlignment(VerticalAlignment.VERTICAL_TOP);
244                String maxText = params.getTyped(TernaryParams.SCALE_MAX);
245                ret.drawText(maxText , (int)r.x - 3, (int)r.y, fs);
246        }
247
248        private void drawBorder(MBFImage ret, TernaryParams params) {
249                int padding = (Integer) params.getTyped(TernaryParams.PADDING);
250                boolean drawTicks = (Boolean) params.getTyped(TernaryParams.TRIANGLE_BORDER_TICKS);
251                Map<Attribute, Object> fontParams = params.getTyped(TernaryParams.TICK_FONT);
252                FontStyle<Float[]> style = FontStyle.parseAttributes(fontParams, ret.createRenderer());
253                if(drawTicks){
254                        Triangle drawTri = tri.transform(TransformUtilities.translateMatrix(padding, padding));
255                        
256                        
257                        for (int i = 0; i < 3; i++) {
258                                int paddingx = 0;
259                                int paddingy = 0;
260                                switch (i){
261                                case 0:
262                                        // the bottom line
263                                        style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_CENTER);
264                                        style.setVerticalAlignment(VerticalAlignment.VERTICAL_TOP);
265                                        paddingy = 5;
266                                        break;
267                                case 1:
268                                        // the right line
269                                        style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_LEFT);
270                                        style.setVerticalAlignment(VerticalAlignment.VERTICAL_HALF);
271                                        paddingx = 5;
272                                        paddingy = -5;
273                                        break;
274                                case 2:
275                                        // the left line
276                                        style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_RIGHT);
277                                        style.setVerticalAlignment(VerticalAlignment.VERTICAL_HALF);
278                                        paddingx = -5;
279                                        paddingy = -5;
280                                        break;
281                                }
282                                Point2d start = drawTri.vertices[i];
283                                Point2d end = drawTri.vertices[(i+1) % 3];
284                                int nTicks = 10;
285                                for (int j = 0; j < nTicks + 1; j++) {
286                                        Line2d tickLine = new Line2d(start, end);
287                                        double length = tickLine.calculateLength();
288                                        // bring its end to the correct position
289                                        double desired = length - j * (length / nTicks);
290                                        if(desired == 0) desired = 0.001;
291                                        double scale = desired / length;
292                                        double overallScale = scale;
293                                        tickLine = tickLine.transform(TransformUtilities.scaleMatrixAboutPoint(scale, scale, start));
294                                        // make it 10 pixels long
295                                        scale = 5f / tickLine.calculateLength();
296                                        tickLine = tickLine.transform(TransformUtilities.scaleMatrixAboutPoint(scale, scale, tickLine.end));
297                                        // Now rotate it by 90 degrees
298                                        tickLine = tickLine.transform(TransformUtilities.rotationMatrixAboutPoint(-Math.PI/2, tickLine.end.getX(), tickLine.end.getY()));
299                                        int thickness = (Integer) params.getTyped(TernaryParams.TRIANGLE_BORDER_TICK_THICKNESS);
300                                        Float[] col = params.getTyped(TernaryParams.TRIANGLE_BORDER_COLOUR);
301                                        ret.drawLine(tickLine, thickness, col);
302                                        
303                                        Point2d textPoint = tickLine.begin.copy();
304                                        textPoint.translate(paddingx, paddingy);
305//                                      ret.drawText(String.format("%2.2f",overallScale), textPoint, style);
306                                }
307                                
308                        }
309                }
310        }
311
312        private void drawTriangle(MBFImage ret,TernaryParams params) {
313                int padding = (Integer) params.getTyped(TernaryParams.PADDING);
314                boolean drawTriangle = (Boolean) params.getTyped(TernaryParams.TRIANGLE_BORDER);
315                if(drawTriangle){
316                        int thickness = (Integer) params.getTyped(TernaryParams.TRIANGLE_BORDER_THICKNESS);
317                        Float[] col = params.getTyped(TernaryParams.TRIANGLE_BORDER_COLOUR);
318                        ret.drawShape(this.tri.transform(TransformUtilities.translateMatrix(padding, padding)), thickness, col);
319                }
320        }
321
322        private void drawLabels( MBFImage ret, TernaryParams params) {
323                int padding = (Integer) params.getTyped(TernaryParams.PADDING);
324                List<IndependentPair<TernaryData,String>> labels = params.getTyped(TernaryParams.LABELS);
325                Map<? extends Attribute, Object> typed = params.getTyped(TernaryParams.LABEL_FONT);
326                FontStyle<Float[]> fs = FontStyle.parseAttributes(typed,ret.createRenderer());
327                Float[] labelBackground = params.getTyped(TernaryParams.LABEL_BACKGROUND);
328                Float[] labelBorder = params.getTyped(TernaryParams.LABEL_BORDER);
329                int labelPadding = (Integer) params.getTyped(TernaryParams.LABEL_PADDING);
330                FontRenderer<Float[], FontStyle<Float[]>> fontRenderer = fs.getRenderer(ret.createRenderer());
331                if(labels != null){                     
332                        for (IndependentPair<TernaryData, String> labelPoint: labels) {
333                                TernaryData ternaryData = labelPoint.firstObject();
334                                Point2d point = ternaryData.asPoint();
335                                point.setX(point.getX() * width + padding );
336                                point.setY(height - (point.getY() * width ) + padding);
337                                Point2d p = point.copy();
338                                if(point.getY() < height/2){
339                                        point.setY(point.getY() - 10);
340                                }
341                                else{
342                                        point.setY(point.getY() + 35);
343                                }
344                                Rectangle rect = fontRenderer.getBounds(labelPoint.getSecondObject(), (int)point.getX(), (int)point.getY(), fs);
345                                rect.x -= labelPadding;
346                                rect.y -= labelPadding;
347                                rect.width += labelPadding*2;
348                                rect.height += labelPadding*2;
349                                if(labelBackground!=null){
350                                        ret.drawShapeFilled(rect, labelBackground);
351                                }
352                                if(labelBorder!=null){
353                                        ret.drawShape(rect, labelBorder);
354                                }
355                                ret.drawText(labelPoint.getSecondObject(), point, fs);
356                                ret.drawPoint(p , RGBColour.RED, (int) ternaryData.value);
357                        }
358                }
359        }
360
361        private void drawTernaryPlot(MBFImage ret, TernaryParams params) {
362                ColourMap cm = params.getTyped(TernaryParams.COLOUR_MAP);
363                int padding = (Integer) params.getTyped(TernaryParams.PADDING);
364                Float[] bgColour = params.getTyped(TernaryParams.BG_COLOUR);
365                for (int y = 0; y < height + padding; y++) {
366                        for (int x = 0; x < width + padding; x++) {
367                                int xp = x - padding;
368                                int yp = y - padding;
369                                Point2dImpl point = new Point2dImpl(xp,yp);
370                                if (this.tri.isInside(point)){
371                                        TernaryData closest = weightThreeClosest(point);
372                                        Float[] apply = null;
373                                        if(cm!=null)
374                                                apply = cm.apply(1-closest.value);
375                                        else{
376                                                apply = new Float[]{closest.value,closest.value,closest.value};
377                                        }
378                                        
379                                        ret.setPixel(x, y, apply);
380                                }
381                                else{
382                                        ret.setPixel(x, y, bgColour);
383                                }
384                        }
385                }
386        }
387
388        /**
389         * @return draw the triangles generated from the data
390         */
391        public MBFImage drawTriangles() {
392                MBFImage img = new MBFImage((int)width,(int)height,ColourSpace.RGB);
393                for (Triangle tri : this.dataTriangles.triToData.keySet()) {
394                        img.drawShape(tri.transform(TransformUtilities.scaleMatrix(width, height)), RGBColour.RED);
395                }
396                return img;
397        }
398        
399        class DistanceToPointComparator implements Comparator<TernaryData>{
400
401                
402                private TernaryData terneryPoint;
403
404                public DistanceToPointComparator(TernaryData point) {
405                        
406                        this.terneryPoint = point;
407                }
408
409                
410
411                @Override
412                public int compare(TernaryData o1, TernaryData o2) {
413                        double o1d = DoubleFVComparison.EUCLIDEAN.compare(o1, this.terneryPoint);
414                        double o2d = DoubleFVComparison.EUCLIDEAN.compare(o2, this.terneryPoint);
415                        return Double.compare(o1d, o2d);
416                }
417                
418        }
419        private float calcBfromXY(float xn, float yn) {
420                return xn - ONE_OVER_ROOT3 * yn;
421        }
422        
423        private float calcCfromXY(float xn, float yn) {
424                return 2 * ONE_OVER_ROOT3 * yn;
425        }
426
427        private float calcAfromXY(float xn, float yn) {
428                return 1f - xn - ONE_OVER_ROOT3 * yn;
429        }
430        private TernaryData weightThreeClosest(Point2dImpl point) {
431                float xn = (point.x - pointA.x)/width;
432                float yn = (pointA.y - point.y )/width;
433                
434                float a = calcAfromXY(xn,yn);
435                float b = calcBfromXY(xn,yn);
436                float c = calcCfromXY(xn,yn);
437                TernaryData trenData = new TernaryData(a, b, c, 0f);
438                if(data.size() == 1){
439                        return data.get(0);
440                } else if (data.size() == 2){
441                        TernaryData tpa = data.get(0);
442                        TernaryData tpb = data.get(1);
443                        double da = DoubleFVComparison.EUCLIDEAN.compare(tpa, trenData);
444                        double db = DoubleFVComparison.EUCLIDEAN.compare(tpb, trenData);
445                        double sumd = da + db;
446                        trenData.value = (float) ((1 - (da / sumd)) * tpa.value + (1-(db/sumd)) * tpb.value); 
447                }
448                else{
449                        Triangle t = dataTriangles.getHoldingTriangle(new Point2dImpl(xn,yn));
450                        if(t == null) {
451                                return new TernaryData(a, b, c, 0f);
452                        }
453                        Map<Line2d, Point2d> points = t.intersectionSides(
454                                        new Line2d(
455                                                        new Point2dImpl(0,yn),
456                                                        new Point2dImpl(1,yn)
457                                        )
458                        );
459                        
460                        if(points.size() == 2){
461                                Iterator<Line2d> liter = points.keySet().iterator();
462                                Line2d l1 = liter.next();
463                                Line2d l2 = liter.next();
464                                Point2d p1 = points.get(l1);
465                                Point2d p2 = points.get(l2);
466                                
467                                double p1Value = linePointInterp(l1, p1);
468                                double p2Value = linePointInterp(l2, p2);
469                                
470                                double pointValue = linePointInterp(new Line2d(p1,p2), new Point2dImpl(xn,yn),p1Value,p2Value);
471                                
472//                              if((l1.begin.getX() == l1.end.getX() || l2.begin.getX() == l2.end.getX() ) && pointValue <0.5){
473//                                      System.out.println("A vertical line created a 0 value");
474//                              }
475                                
476                                trenData.value = (float) pointValue;
477                                
478                        }
479                        else{ // 0, 1 or more than 2
480                                System.out.println("Found 3 or 0 lines: " + points.size());
481                                return new TernaryData(a, b, c, 0f);
482                        }
483                }
484                return trenData;
485        }
486
487        private double linePointInterp(Line2d line, Point2d point) {
488                TernaryData l1p1data = dataTriangles.getPointData(line.begin);
489                TernaryData l1p2data = dataTriangles.getPointData(line.end);
490                float l1p1datav = l1p1data.value;
491                float l1p2datav = l1p2data.value;
492                
493                return linePointInterp(line, point, l1p1datav, l1p2datav);
494        }
495
496        private double linePointInterp(Line2d line, Point2d point, double lineBeginValue,double lineEndValue) {
497                double l1Len = line.calculateLength();
498                double l1Prop = Line2d.distance(line.begin, point);
499                double p1Value = Interpolation.lerp(l1Prop, 0, lineBeginValue, l1Len, lineEndValue);
500                return p1Value;
501        }
502
503        /**
504         * @param args
505         */
506        public static void main(String[] args) {
507                List<TernaryData> data = new ArrayList<TernaryData>();
508                data.add(new TernaryData(1/3f+0.1f,1/3f-0.1f,1/3f,0.8f));
509                data.add(new TernaryData(1/3f-0.1f,1/3f+0.1f,1/3f,0.2f));
510                data.add(new TernaryData(1f,0,0,0));
511                data.add(new TernaryData(0,1f,0,0));
512                data.add(new TernaryData(0,0,1f,0));
513                TernaryPlot plot = new TernaryPlot(500, data);
514                DisplayUtilities.display(plot.draw());
515                DisplayUtilities.display(plot.drawTriangles());
516        }
517
518        
519
520}