1/*
2 * Copyright 2013 AndroidPlot.com
3 *
4 *    Licensed under the Apache License, Version 2.0 (the "License");
5 *    you may not use this file except in compliance with the License.
6 *    You may obtain a copy of the License at
7 *
8 *        http://www.apache.org/licenses/LICENSE-2.0
9 *
10 *    Unless required by applicable law or agreed to in writing, software
11 *    distributed under the License is distributed on an "AS IS" BASIS,
12 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 *    See the License for the specific language governing permissions and
14 *    limitations under the License.
15 */
16
17package com.androidplot.pie;
18
19import android.graphics.*;
20
21import com.androidplot.exception.PlotRenderException;
22import com.androidplot.ui.SeriesRenderer;
23
24import java.util.Set;
25
26public class PieRenderer extends SeriesRenderer<PieChart, Segment, SegmentFormatter> {
27
28    // starting angle to use when drawing the first radial line of the first segment.
29    @SuppressWarnings("FieldCanBeLocal")
30    private float startDeg = 0;
31    private float endDeg = 360;
32
33    // TODO: express donut in units other than px.
34    private float donutSize = 0.5f;
35    private DonutMode donutMode = DonutMode.PERCENT;
36
37    public enum DonutMode {
38        PERCENT,
39        DP,
40        PIXELS
41    }
42
43    public PieRenderer(PieChart plot) {
44        super(plot);
45    }
46
47    public float getRadius(RectF rect) {
48    	return  rect.width() < rect.height() ? rect.width() / 2 : rect.height() / 2;
49    }
50
51    @Override
52    public void onRender(Canvas canvas, RectF plotArea) throws PlotRenderException {
53
54        float radius = getRadius(plotArea);
55        PointF origin = new PointF(plotArea.centerX(), plotArea.centerY());
56
57        double[] values = getValues();
58        double scale = calculateScale(values);
59        float offset = startDeg;
60        Set<Segment> segments = getPlot().getSeriesSet();
61
62        //PointF lastRadial = calculateLineEnd(origin, radius, offset);
63
64        RectF rec = new RectF(origin.x - radius, origin.y - radius, origin.x + radius, origin.y + radius);
65
66        int i = 0;
67        for (Segment segment : segments) {
68            float lastOffset = offset;
69            float sweep = (float) (scale * (values[i]) * 360);
70            offset += sweep;
71            //PointF radial = calculateLineEnd(origin, radius, offset);
72            drawSegment(canvas, rec, segment, getPlot().getFormatter(segment, PieRenderer.class),
73                    radius, lastOffset, sweep);
74            //lastRadial = radial;
75            i++;
76        }
77    }
78
79    protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentFormatter f,
80                               float rad, float startAngle, float sweep) {
81        canvas.save();
82
83        float cx = bounds.centerX();
84        float cy = bounds.centerY();
85
86        float donutSizePx;
87        switch(donutMode) {
88            case PERCENT:
89                donutSizePx = donutSize * rad;
90                break;
91            case PIXELS:
92                donutSizePx = (donutSize > 0)?donutSize:(rad + donutSize);
93                break;
94            default:
95                throw new UnsupportedOperationException("Not yet implemented.");
96        }
97
98        // vertices of the first radial:
99        PointF r1Outer = calculateLineEnd(cx, cy, rad, startAngle);
100        PointF r1Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle);
101
102        // vertices of the second radial:
103        PointF r2Outer = calculateLineEnd(cx, cy, rad, startAngle + sweep);
104        PointF r2Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle + sweep);
105
106        Path clip = new Path();
107
108        //float outerStroke = f.getOuterEdgePaint().getStrokeWidth();
109        //float halfOuterStroke = outerStroke / 2;
110
111        // leave plenty of room on the outside for stroked borders;
112        // necessary because the clipping border is ugly
113        // and cannot be easily anti aliased.  Really we only care about masking off the
114        // radial edges.
115        clip.arcTo(new RectF(bounds.left - rad,
116                bounds.top - rad,
117                bounds.right + rad,
118                bounds.bottom + rad),
119                startAngle, sweep);
120        clip.lineTo(cx, cy);
121        clip.close();
122        canvas.clipPath(clip);
123
124        Path p = new Path();
125        p.arcTo(bounds, startAngle, sweep);
126        p.lineTo(r2Inner.x, r2Inner.y);
127
128        // sweep back to original angle:
129        p.arcTo(new RectF(
130                cx - donutSizePx,
131                cy - donutSizePx,
132                cx + donutSizePx,
133                cy + donutSizePx),
134                startAngle + sweep, -sweep);
135
136        p.close();
137
138        // fill segment:
139        canvas.drawPath(p, f.getFillPaint());
140
141        // draw radial lines
142        canvas.drawLine(r1Inner.x, r1Inner.y, r1Outer.x, r1Outer.y, f.getRadialEdgePaint());
143        canvas.drawLine(r2Inner.x, r2Inner.y, r2Outer.x, r2Outer.y, f.getRadialEdgePaint());
144
145        // draw inner line:
146        canvas.drawCircle(cx, cy, donutSizePx, f.getInnerEdgePaint());
147
148        // draw outer line:
149        canvas.drawCircle(cx, cy, rad, f.getOuterEdgePaint());
150        canvas.restore();
151
152        PointF labelOrigin = calculateLineEnd(cx, cy,
153                (rad-((rad- donutSizePx)/2)), startAngle + (sweep/2));
154
155        // TODO: move segment labelling outside the segment drawing loop
156        // TODO: so that the labels will not be clipped by the edge of the next
157        // TODO: segment being drawn.
158        drawSegmentLabel(canvas, labelOrigin, seg, f);
159    }
160
161    protected void drawSegmentLabel(Canvas canvas, PointF origin,
162                                    Segment seg, SegmentFormatter f) {
163        canvas.drawText(seg.getTitle(), origin.x, origin.y, f.getLabelPaint());
164
165    }
166
167    @Override
168    protected void doDrawLegendIcon(Canvas canvas, RectF rect, SegmentFormatter formatter) {
169        throw new UnsupportedOperationException("Not yet implemented.");
170    }
171
172    /**
173     * Determines how many counts there are per cent of whatever the
174     * pie chart is displaying as a fraction, 1 being 100%.
175     */
176    private double calculateScale(double[] values) {
177        double total = 0;
178        for (int i = 0; i < values.length; i++) {
179			total += values[i];
180		}
181
182        return (1d / total);
183    }
184
185	private double[] getValues() {
186		Set<Segment> segments = getPlot().getSeriesSet();
187		double[] result = new double[segments.size()];
188		int i = 0;
189		for (Segment seg : getPlot().getSeriesSet()) {
190			result[i] = seg.getValue().doubleValue();
191			i++;
192		}
193		return result;
194	}
195
196    private PointF calculateLineEnd(float x, float y, float rad, float deg) {
197        return calculateLineEnd(new PointF(x, y), rad, deg);
198    }
199
200    private PointF calculateLineEnd(PointF origin, float rad, float deg) {
201
202        double radians = deg * Math.PI / 180F;
203        double x = rad * Math.cos(radians);
204        double y = rad * Math.sin(radians);
205
206        // convert to screen space:
207        return new PointF(origin.x + (float) x, origin.y + (float) y);
208    }
209
210    public void setDonutSize(float size, DonutMode mode) {
211        switch(mode) {
212            case PERCENT:
213                if(size < 0 || size > 1) {
214                    throw new IllegalArgumentException(
215                            "Size parameter must be between 0 and 1 when operating in PERCENT mode.");
216                }
217                break;
218            case PIXELS:
219            	break;
220            default:
221                throw new UnsupportedOperationException("Not yet implemented.");
222        }
223        donutMode = mode;
224        donutSize = size;
225    }
226
227    public void setStartDeg(float deg) {
228        startDeg = deg;
229    }
230
231    public void setEndDeg(float deg) {
232        endDeg = deg;
233    }
234}
235