org.tellervo.desktop.graph.SkeletonPlot.java Source code

Java tutorial

Introduction

Here is the source code for org.tellervo.desktop.graph.SkeletonPlot.java

Source

/*******************************************************************************
 * Copyright (C) 2012 Peter Brewer
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Peter Brewer
 ******************************************************************************/
package org.tellervo.desktop.graph;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.math.stat.descriptive.DescriptiveStatistics;
import org.apache.commons.math.stat.descriptive.moment.Mean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tellervo.desktop.Range;
import org.tellervo.desktop.Year;
import org.tellervo.desktop.core.App;
import org.tellervo.desktop.hardware.UnsupportedPortParameterException;
import org.tellervo.desktop.hardware.AbstractMeasuringDevice.PortParity;
import org.tellervo.desktop.index.Index;
import org.tellervo.desktop.prefs.Prefs.PrefKey;
import org.tellervo.desktop.sample.Sample;
import org.tellervo.desktop.util.ColorUtils;
import org.tridas.schema.TridasDerivedSeries;
import org.tridas.schema.TridasRemark;

public class SkeletonPlot implements TellervoGraphPlotter {
    private final static Logger log = LoggerFactory.getLogger(SkeletonPlot.class);
    private Integer baselineY = 0;

    protected Boolean areBonesBelowLine = false;

    public enum SkeletonPlotAlgorithm {
        PERCENTILES("Percentiles"), CROPPER1979_0POINT5("Cropper 1979 (0.50 stdev)"), CROPPER1979_0POINT75(
                "Cropper 1979 (0.75 stdev)");

        private String value;

        SkeletonPlotAlgorithm(String str) {
            value = str;
        }

        public String toString() {
            return value;
        }

        public static String[] allValuesAsArray() {
            ArrayList<String> arr = new ArrayList<String>();
            for (SkeletonPlotAlgorithm val : SkeletonPlotAlgorithm.values()) {
                arr.add(val.toString());
            }

            return arr.toArray(new String[0]);
        }

        public static SkeletonPlotAlgorithm fromString(String str) {
            for (SkeletonPlotAlgorithm val : SkeletonPlotAlgorithm.values()) {
                if (val.toString().equals(str))
                    return val;
            }

            return null;
        }

    }

    public SkeletonPlot() {
        // no initializing to do, I am STATELESS!
    }

    // PERF: too many new's!  can i memoize this?  or just use constants for the 6(?) strokes i use?
    protected BasicStroke makeStroke(float width, boolean dotted) {
        if (dotted)
            return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10f, new float[] { 8f },
                    0f);
        else
            return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
    }

    // We don't do any y transform for the standard plot; this is here for overriding.
    protected int yTransform(float y) {
        return (int) y;
    }

    protected boolean validValue(int value) {
        // if MR, draw a vertical line -- use Sample.MR, for now
        if (value <= Sample.missingRingValue)
            return false;
        return true;
    }

    // TESTING: perf
    protected Rectangle tempRect = new Rectangle();

    // returns the maximum size, in pixels, that the graph will take up.
    public int getYRange(GraphSettings gInfo, Graph g) {
        float unitScale = gInfo.getHundredUnitHeight() / 100.0f; // the size of 1 "unit" in pixels.
        int miny = 0; // minimum always starts at zero...
        int maxy = Integer.MIN_VALUE;
        int value;

        int n = g.graph.getRingWidthData().size();
        for (int i = 0; i < n; i++) {
            value = yTransform(g.graph.getRingWidthData().get(i).intValue() * g.scale);
            int y = (int) (value * unitScale) - (int) (g.yoffset * unitScale);

            if (y < miny)
                miny = y;
            if (y > maxy)
                maxy = y;
        }

        return maxy - miny;
    }

    public void draw(GraphSettings gInfo, Graphics2D g2, int bottom, Graph g, int thickness, int xscroll) {
        // cache yearsize, we use this a lot
        int yearWidth = gInfo.getYearWidth(); // the size of a year, in pixels
        float unitScale = gInfo.getHundredUnitHeight() / 100.0f; // the size of 1 "unit" in pixels.

        // set pen
        boolean dotted = (gInfo.isDottedIndexes() && (g.graph instanceof Index));
        g2.setStroke(makeStroke(thickness, dotted));

        // If it's a chronology, point bones down
        /*if(g.graph instanceof Sample)
        {
           if (((Sample) g.graph).getSeries() instanceof TridasDerivedSeries)
           {
        this.areBonesBelowLine = true;
           }
        }*/

        // left/right
        int l = g2.getClipBounds().x;
        int r = l + g2.getClipBounds().width;

        // no data?  stop.
        if (g.graph.getRingWidthData().isEmpty())
            return;

        // compare g.getClipBounds() to [x,0]..[x+yearSize*data.size(),bottom]
        tempRect.x = yearWidth * (g.graph.getStart().diff(gInfo.getDrawBounds().getStart()) + g.xoffset); // REDUNDANT! see x later
        tempRect.y = 0; // - g.yoffset, IF you're sure there are no negative values (but there are)
        tempRect.width = yearWidth * (g.graph.getRingWidthData().size() - 1);
        tempRect.height = bottom;
        // TODO: compute top/bottom as min/max?
        // REFACTOR: will this be obsolete with the start/end stuff below?
        if (!tempRect.intersects(g2.getClipBounds())) {
            // skip this graph, it's off the screen
            return;
        }

        // Draw standard line
        int x = yearWidth * (g.graph.getStart().diff(gInfo.getDrawBounds().getStart()) + g.xoffset);
        //int value1 = yTransform((float) getMeanValue(g));
        int value1 = yTransform((float) baselineY);
        int baseYVal = bottom - (int) (value1 * unitScale) - (int) (g.yoffset * unitScale);

        try {
            // x-position
            int x1 = x;
            int x2 = yearWidth * (g.graph.getStart().diff(gInfo.getDrawBounds().getStart()) + g.xoffset
                    + g.graph.getRingWidthData().size());

            // if we're past the end, draw only as far as we need
            if (x2 > r + yearWidth) {
                x2 = r + yearWidth;
            }

            //log.debug("Drawing mean line: "+x1+", "+meanYVal+", "+x2+", "+meanYVal);
            g2.drawLine(x1, baseYVal, x2, baseYVal);

            // Calcs for start/end triangles
            int YLineHeight = 50;
            int YTriangleHeight = 25;
            int XTriangleWidth = 15;
            if (areBonesBelowLine) {
                YLineHeight = -50;
                YTriangleHeight = -25;
            }

            // Draw start triangle
            g2.drawLine(x1, baseYVal, x1, baseYVal - YTriangleHeight);
            g2.drawLine(x1, baseYVal, x1 - XTriangleWidth, baseYVal - YTriangleHeight);
            g2.drawLine(x1 - XTriangleWidth, baseYVal - YTriangleHeight, x1, baseYVal - YTriangleHeight);
            g2.drawLine(x1, baseYVal, x1, baseYVal - YLineHeight);

            // Draw end triangle
            g2.drawLine(x2, baseYVal, x2, baseYVal - YTriangleHeight);
            g2.drawLine(x2, baseYVal, x2 + XTriangleWidth, baseYVal - YTriangleHeight);
            g2.drawLine(x2 + XTriangleWidth, baseYVal - YTriangleHeight, x2, baseYVal - YTriangleHeight);
            g2.drawLine(x2, baseYVal, x2, baseYVal - YLineHeight);

        } catch (ClassCastException cce) {

        }

        int value;
        try {
            value = ((Number) g.graph.getRingWidthData().get(0)).intValue();
            value = yTransform(value * g.scale);
        } catch (ClassCastException cce) {
            value = yTransform(0);
        }

        int n = g.graph.getRingWidthData().size();

        for (int i = 1; i < n; i++) {
            // new x-position for this point
            x += yearWidth;

            // if we're past the end, draw what we've got, and say goodbye
            // (go +_yearsize so the line going off the screen is visible)
            if (x > r + yearWidth) {
                break;
            }

            // Extract the window of interest
            int ringsEitherSideOfFocus = (App.prefs.getIntPref(PrefKey.STATS_SKELETON_PLOT_WINDOW_SIZE, 7) - 1) / 2;

            // Convert to ArrayList first as its easier to handle
            ArrayList<Double> ringWidths = new ArrayList<Double>();
            for (int z = 0; z < n; z++) {
                ringWidths.add((double) g.graph.getRingWidthData().get(z).intValue());
            }

            int firstind = i - 1 - ringsEitherSideOfFocus;
            int lastind = i + ringsEitherSideOfFocus;
            if (firstind < 0)
                firstind = 0;
            if (lastind > n)
                lastind = n;
            int size = lastind - firstind;

            double[] window = new double[size];

            int t = 0;
            for (int w = firstind; w < lastind; w++) {
                window[t] = ringWidths.get(w);
                t++;
            }

            DescriptiveStatistics windowStats = new DescriptiveStatistics(window);
            /*if(i<7 ) 
            {
               log.debug("Stats for ring: "+i);
               try{
                  log.debug("  Window 0: "+window[0]);
                  log.debug("  Window 1: "+window[1]);
                  log.debug("  Window 2: "+window[2]);
                  log.debug("  Window 3: "+window[3]);
                  log.debug("  Window 4: "+window[4]);
                  log.debug("  Window 5: "+window[5]);
                  log.debug("  Window 6: "+window[6]);
               } catch (ArrayIndexOutOfBoundsException e){}
               log.debug("  Mean  is "+i+" - "+(int) windowStats.getMean());
               log.debug("  Min   is "+i+" - "+(int) windowStats.getMin());
               log.debug("  Std   is "+i+" - "+(int) windowStats.getStandardDeviation());
               log.debug("  Std/2 is "+i+" - "+(int) windowStats.getStandardDeviation()/2);
            }*/

            // y-position for this point
            try {
                value = yTransform(((Number) g.graph.getRingWidthData().get(i)).intValue() * g.scale);
            } catch (ClassCastException cce) {
                value = yTransform(0); // e.g., if it's being edited, it's still a string
                // BAD!  instead: (1) draw what i've got so far, and (2) NEXT point is a move-to.
                // -- try to parse String as an integer?
            }
            int y = bottom - (int) (value * unitScale) - (int) (g.yoffset * unitScale);

            // Calculate skeleton category
            Integer skeletonCateogory = null;

            String prefAlg = App.prefs.getPref(PrefKey.STATS_SKELETON_PLOT_ALGORITHM,
                    SkeletonPlotAlgorithm.PERCENTILES.toString());

            if (prefAlg.equals(SkeletonPlotAlgorithm.PERCENTILES.toString())) {
                skeletonCateogory = getSkeletonCategoryFromPercentiles(value, windowStats);
            } else if (prefAlg.equals(SkeletonPlotAlgorithm.CROPPER1979_0POINT5.toString())) {
                skeletonCateogory = getSkeletonCategoryFromCropper1979(value, windowStats, 0.5);
            } else if (prefAlg.equals(SkeletonPlotAlgorithm.CROPPER1979_0POINT75.toString())) {
                skeletonCateogory = getSkeletonCategoryFromCropper1979(value, windowStats, 0.75);
            }

            // Draw the skeleton line
            if (areBonesBelowLine) {
                g2.drawLine(x, baseYVal, x, baseYVal + (skeletonCateogory * 5));
            } else {
                g2.drawLine(x, baseYVal, x, baseYVal - (skeletonCateogory * 5));
            }

            // Try and paint remark icons
            try {
                List<TridasRemark> remarks = g.graph.getTridasValues().get(i).getRemarks();

                if (areBonesBelowLine) {
                    Graph.drawRemarkIcons(g2, gInfo, remarks, g.graph.getTridasValues().get(i), x, baseYVal, false);
                } else {
                    Graph.drawRemarkIcons(g2, gInfo, remarks, g.graph.getTridasValues().get(i), x, baseYVal, true);
                }

            } catch (Exception e) {
                log.error("Exception drawing icons to graph: " + e.getClass());
            }

        }
    }

    public void setShowBonesBelow(Boolean b) {
        areBonesBelowLine = b;
    }

    // if it's within this many pixels, it's considered a hit (see "correct?" comment)
    private final static int NEAR = 5;

    public boolean contact(GraphSettings gInfo, Graph g, Point p, int bottom) {
        // snap to year
        int yearWidth = gInfo.getYearWidth();
        int firstYearIdx = (p.x / yearWidth) - 1;
        int nYears = 3;

        // Check three years' worth of data: the year prior to and after the year the mouse is inside
        Year startYear = gInfo.getDrawBounds().getStart().add(firstYearIdx);
        List<Point2D> points = this.getPointsFrom(gInfo, g, startYear, nYears, bottom);

        int nLines = points.size() - 1;
        for (int i = 0; i < nLines; i++) {
            Line2D line = new Line2D.Float(points.get(i), points.get(i + 1));

            int distance = Math.round((float) line.ptSegDist(p));
            if (distance <= NEAR)
                return true;
        }

        return false;
    }

    private Integer getSkeletonCategoryFromCropper1979(Integer value, DescriptiveStatistics windowStats,
            Double criticalLevel) {
        Integer skeletonCategory = 0;

        if (criticalLevel == null)
            criticalLevel = 0.5;
        double mean = windowStats.getMean();
        double stdev = windowStats.getStandardDeviation();
        double smallRingThreshold = mean - (stdev * criticalLevel);
        int min = (int) windowStats.getMin();

        if (value == min) {
            skeletonCategory = 10;
        } else if (value > smallRingThreshold) {
            skeletonCategory = 0;
        } else {
            Integer range = (int) (smallRingThreshold - min);
            Integer categoryStepSize = range / 10;
            skeletonCategory = (int) (0 - ((value - smallRingThreshold) / categoryStepSize));
        }

        return skeletonCategory;
    }

    private Integer getSkeletonCategoryFromPercentiles(Integer value, DescriptiveStatistics windowStats) {
        Integer skeletonCategory = 0;
        // Calculate skeleton category
        if (value == (int) windowStats.getMin()) {
            skeletonCategory = 10;
        } else if (value < windowStats.getPercentile(10)) {
            skeletonCategory = 9;
        } else if (value < windowStats.getPercentile(15)) {
            skeletonCategory = 8;
        } else if (value < windowStats.getPercentile(20)) {
            skeletonCategory = 7;
        } else if (value < windowStats.getPercentile(25)) {
            skeletonCategory = 6;
        } else if (value < windowStats.getPercentile(30)) {
            skeletonCategory = 5;
        } else if (value < windowStats.getPercentile(35)) {
            skeletonCategory = 4;
        } else if (value < windowStats.getPercentile(40)) {
            skeletonCategory = 3;
        } else if (value < windowStats.getPercentile(45)) {
            skeletonCategory = 2;
        } else if (value < windowStats.getPercentile(50)) {
            skeletonCategory = 1;
        }

        return skeletonCategory;
    }

    private final int getDataValue(Graph g, Year y) {
        int i = y.diff(g.graph.getStart().add(g.xoffset));
        return g.graph.getRingWidthData().get(i).intValue();
    }

    private final int getYValue(Graph g, float unitScale, int value, int bottom) {
        return bottom - (int) (yTransform(value * g.scale) * unitScale) - (int) (g.yoffset * unitScale); // DUPLICATE: this line appears above 3 times
    }

    private final List<Point2D> getPointsFrom(GraphSettings gInfo, Graph g, Year startYear, int nYears,
            int bottom) {

        // make a list of points
        // this is ok, because we know points are continuous
        List<Point2D> points = new ArrayList<Point2D>(nYears);

        int yearWidth = gInfo.getYearWidth();
        float unitScale = gInfo.getHundredUnitHeight() / 100.0f;

        List<? extends Number> data = g.graph.getRingWidthData();
        double[] dataDbl = new double[data.size()];
        for (int i = 0; i < data.size(); i++) {
            Integer intval = (Integer) data.get(i);
            dataDbl[i] = (double) intval;
        }
        int x = yearWidth * (g.graph.getStart().diff(gInfo.getDrawBounds().getStart()) + g.xoffset);
        //int value1 = yTransform((float) stats.getMean());
        int value1 = yTransform((float) baselineY);

        int meanYVal = bottom - (int) (value1 * unitScale) - (int) (g.yoffset * unitScale);
        try {
            // x-position
            int x1 = x;
            int x2 = yearWidth * (g.graph.getStart().diff(gInfo.getDrawBounds().getStart()) + g.xoffset
                    + g.graph.getRingWidthData().size());

            points.add(new Point2D.Float(x1, meanYVal));
            points.add(new Point2D.Float(x2, meanYVal));

        } catch (ClassCastException cce) {

        }

        return points;
    }

    protected final int getPosition(GraphSettings gInfo, Graph g, Year y, int bottom) {
        return getYValue(g, gInfo.getHundredUnitHeight() / 100.0f, getDataValue(g, y), bottom);
    }

    @Override
    public int getFirstValue(Graph g) {

        return yTransform((float) baselineY);
    }

    // REFACTOR: use this same method above when actually drawing it
}