/** * OrbisGIS is a java GIS application dedicated to research in GIScience. * OrbisGIS is developed by the GIS group of the DECIDE team of the * Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>. * * The GIS group of the DECIDE team is located at : * * Laboratoire Lab-STICC – CNRS UMR 6285 * Equipe DECIDE * UNIVERSITÉ DE BRETAGNE-SUD * Institut Universitaire de Technologie de Vannes * 8, Rue Montaigne - BP 561 56017 Vannes Cedex * * OrbisGIS is distributed under GPL 3 license. * * Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488) * Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285) * * This file is part of OrbisGIS. * * OrbisGIS 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. * * OrbisGIS 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 * OrbisGIS. If not, see <http://www.gnu.org/licenses/>. * * For more information, please consult: <http://www.orbisgis.org/> * or contact directly: * info_at_ orbisgis.org */ package org.orbisgis.coremap.renderer.se.graphic; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Area; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.xml.bind.JAXBElement; import net.opengis.se._2_0.thematic.ObjectFactory; import net.opengis.se._2_0.thematic.PieChartType; import net.opengis.se._2_0.thematic.PieSubtypeType; import net.opengis.se._2_0.thematic.SliceType; import org.orbisgis.coremap.map.MapTransform; import org.orbisgis.coremap.renderer.se.SeExceptions.InvalidStyle; import org.orbisgis.coremap.renderer.se.StrokeNode; import org.orbisgis.coremap.renderer.se.SymbolizerNode; import org.orbisgis.coremap.renderer.se.UomNode; import org.orbisgis.coremap.renderer.se.common.Uom; import org.orbisgis.coremap.renderer.se.fill.Fill; import org.orbisgis.coremap.renderer.se.label.StyledText; import org.orbisgis.coremap.renderer.se.parameter.ParameterException; import org.orbisgis.coremap.renderer.se.parameter.SeParameterFactory; import org.orbisgis.coremap.renderer.se.parameter.real.RealLiteral; import org.orbisgis.coremap.renderer.se.parameter.real.RealParameter; import org.orbisgis.coremap.renderer.se.parameter.real.RealParameterContext; import org.orbisgis.coremap.renderer.se.stroke.Stroke; import org.orbisgis.coremap.renderer.se.transform.Transform; import org.orbisgis.coremap.renderer.se.visitors.FeaturesVisitor; /** * A PieChart is a way to render statistical informations directly in the map. * It is a circular chart, that is diveided into sectors. The arc length of * each sector is directly proportional to the value it represents - supposing * the diameter length is the sum of all the represented values. It is built * using the following attributes : * <ul> * <li>A unit of measure (as a {@code UomNode}).</li> * <li>A {@code PieChartSubType}. We can represent a pie chart in a whole circle * or in a half-circle (defautl being the whole circle).</li> * <li>A radius - default is 30 pixels</li> * <li>A hole radius - default is 0. By adding a hole radius, it becomes possible * to render a ring rather than a disc. It should be greater than the radius. * </li> * <li>A Stroke to render its boundary</li> * <li>A transform that can be applied on the graphic.</li> * <li>A list of slices, as described in {@link Slice}</li> * </ul> * </p> * <p>{@code Slices} can be organize in this {@code PieChart}, in order to * change their display order * @author Alexis Guéganno, Maxence Laurent */ public final class PieChart extends Graphic implements StrokeNode, UomNode, TransformNode { private ArrayList<SliceListener> listeners; private static final double DEFAULT_RADIUS_PX = 30; private Uom uom; private PieChartSubType type; private RealParameter radius; private RealParameter holeRadius; private boolean displayValue; private Stroke stroke; private Transform transform; private ArrayList<Slice> slices; /** * A {@code PieChart} can be drawn in a whole circle, or in a half circle. */ public enum PieChartSubType { WHOLE, HALF; } /** * Build a new {@code PieChart}. It does not have any slice, is drawn in a * whole circle, and has a radius of 10. */ public PieChart() { slices = new ArrayList<Slice>(); type = PieChartSubType.WHOLE; radius = new RealLiteral(DEFAULT_RADIUS_PX); this.listeners = new ArrayList<SliceListener>(); } /** * Build a new {@code PieChart} from the give {@code JAXBElement}. * @param pieE * @throws org.orbisgis.coremap.renderer.se.SeExceptions.InvalidStyle */ PieChart(JAXBElement<PieChartType> pieE) throws InvalidStyle { this(); PieChartType t = pieE.getValue(); if (t.getUom() != null) { this.setUom(Uom.fromOgcURN(t.getUom())); } if (t.getTransform() != null) { this.setTransform(new Transform(t.getTransform())); } if (t.getRadius() != null) { this.setRadius(SeParameterFactory.createRealParameter(t.getRadius())); } if (t.getHoleRadius() != null) { this.setHoleRadius(SeParameterFactory.createRealParameter(t.getHoleRadius())); } if (t.getStroke() != null) { this.setStroke(Stroke.createFromJAXBElement(t.getStroke())); } if (t.getPieSubtype() != null && t.getPieSubtype().value().equalsIgnoreCase("half")) { this.setType(PieChartSubType.HALF); } else { this.setType(PieChartSubType.WHOLE); } for (SliceType st : t.getSlice()) { Slice s = new Slice(); s.setName(st.getName()); if (st.getValue() != null) { s.setValue(SeParameterFactory.createRealParameter(st.getValue())); } if (st.getFill() != null) { s.setFill(Fill.createFromJAXBElement(st.getFill())); } if (st.getGap() != null) { s.setGap(SeParameterFactory.createRealParameter(st.getGap())); } this.addSlice(s); } } /** * Add a listener to this {@code PieChart}. * @param lstner */ public void registerListerner(SliceListener lstner) { listeners.add(lstner); } public void fireSliceMoveDown(int i) { for (SliceListener l : listeners) { l.sliceMoveDown(i); } } public void fireSliceMoveUp(int i) { for (SliceListener l : listeners) { l.sliceMoveUp(i); } } public void fireSliceRemoved(int i) { for (SliceListener l : listeners) { l.sliceRemoved(i); } } @Override public Uom getUom() { if (uom != null) { return uom; } else if(getParent() instanceof UomNode){ return ((UomNode)getParent()).getUom(); } else { return Uom.PX; } } @Override public Uom getOwnUom() { return uom; } @Override public void setUom(Uom uom) { this.uom = uom; } /** * Retrieve the number of slices registered in this {@code PieChart}. * @return */ public int getNumSlices() { return slices.size(); } /** * Get the ith {@code Slice} registered in this {@code PieChart}. * @param i * @return * The ith {@code Slice}, or null if {@code i<0 || i>=getNumSlices())}. */ public Slice getSlice(int i) { if (i >= 0 && i < getNumSlices()) { return slices.get(i); } else { return null; } } /** * Remove the ith element in the inner list of {@code Slice}s. * @param i */ public void removeSlice(int i) { if (i >= 0 && i < slices.size()) { slices.remove(i); fireSliceRemoved(i); } } /** * Add an element in the inner list {@code Slice}s. * @param slice * If null, is not added. */ public void addSlice(Slice slice) { if (slice != null) { slices.add(slice); slice.setParent(this); } } /** * Move the ith {@code Slice} up, ie swap the ith and (i+1)th elements. * @param i */ public void moveSliceUp(int i) { // déplace i vers i-1 if (slices.size() > 1) { if (i > 0 && i < slices.size()) { Slice tmp = slices.get(i); slices.set(i, slices.get(i - 1)); slices.set(i - 1, tmp); fireSliceMoveUp(i); } else { // TODO throw } } } /** * Move the ith {@code Slice} down, ie swap the ith and (i-1)th elements. * @param i */ public void moveSliceDown(int i) { // déplace i vers i+1 if (slices.size() > 1) { if (i >= 0 && i < slices.size() - 1) { Slice tmp = slices.get(i); slices.set(i, slices.get(i + 1)); slices.set(i + 1, tmp); fireSliceMoveDown(i); } else { // TODO throw } } } public boolean isDisplayValue() { return displayValue; } public void setDisplayValue(boolean displayValue) { this.displayValue = displayValue; } /** * Get the hole of the radius, if set. Can be null * @return * A {@link RealParameter} placed in a * {@link RealParameterContext#NON_NEGATIVE_CONTEXT}. */ public RealParameter getHoleRadius() { return holeRadius; } /** * Set the radius of the hole that must be kept in this {@code PieChart}. If * greater than 0 and smaller than the {@code PieChart}'s radius, this * {@code PieChart} will be drawn as a crown rather than as a disc. * @param holeRadius */ public void setHoleRadius(RealParameter holeRadius) { this.holeRadius = holeRadius; if (holeRadius != null) { if (this.radius != null){ FeaturesVisitor fv = new FeaturesVisitor(); this.radius.acceptVisitor(fv); if(fv.getResult().isEmpty()) { try { holeRadius.setContext(new RealParameterContext(0.0, radius.getValue(null, -1))); } catch (ParameterException ex) { // don't throw anything since radius does not depends on features } } } holeRadius.setContext(RealParameterContext.NON_NEGATIVE_CONTEXT); holeRadius.setParent(this); } } /** * Get the radius of the {@code PieChart}. * @return * The radius of the {@code PieChart}, as a {@link RealParameter} placed in * a {@link RealParameterContext#NON_NEGATIVE_CONTEXT}. */ public RealParameter getRadius() { return radius; } /** * Set the radius of the {@code PieChart}. * @param radius * A {@link RealParameter}, that is placed by this method in a * {@link RealParameterContext#NON_NEGATIVE_CONTEXT}. */ public void setRadius(RealParameter radius) { this.radius = radius; if (radius != null) { radius.setContext(RealParameterContext.NON_NEGATIVE_CONTEXT); radius.setParent(this); } } @Override public Stroke getStroke() { return stroke; } @Override public void setStroke(Stroke stroke) { this.stroke = stroke; if (stroke != null) { stroke.setParent(this); } } /** * Get the type of this {@code PieChart}. * @return */ public PieChartSubType getType() { return type; } /** * Set the type of this {@code PieChart}. * @param type */ public void setType(PieChartSubType type) { this.type = type; } @Override public Transform getTransform() { return transform; } @Override public void setTransform(Transform transform) { this.transform = transform; if (transform != null) { transform.setParent(this); } } @Override public Rectangle2D getBounds(Map<String,Object> map, MapTransform mt) throws ParameterException, IOException { double r = DEFAULT_RADIUS_PX; if (radius != null) { r = Uom.toPixel(radius.getValue(map), getUom(), mt.getDpi(), mt.getScaleDenominator(), null); } Rectangle2D bounds = new Rectangle2D.Double(-r, -r, 2 * r, 2 * r); /*if (transform != null) { AffineTransform at = transform.getGraphicalAffineTransform(false, map, mt, 2 * r, 2 * r); return at.createTransformedShape(bounds).getBounds2D(); } else { return bounds; }*/ return bounds; } @Override public void draw(Graphics2D g2, Map<String,Object> map, boolean selected, MapTransform mt, AffineTransform fat) throws ParameterException, IOException { AffineTransform at = new AffineTransform(fat); int nSlices = slices.size(); double total = 0.0; double[] values = new double[nSlices]; double[] stackedValues = new double[nSlices]; double[] gaps = new double[nSlices]; double r = PieChart.DEFAULT_RADIUS_PX; // 30px by default if (radius != null) { r = Uom.toPixel(this.getRadius().getValue(map), this.getUom(), mt.getDpi(), mt.getScaleDenominator(), null); // TODO 100% } double holeR = 0.0; Area hole = null; if (this.holeRadius != null) { holeR = Uom.toPixel(this.getHoleRadius().getValue(map), this.getUom(), mt.getDpi(), mt.getScaleDenominator(), r); hole = new Area(new Arc2D.Double(-holeR, -holeR, 2 * holeR, 2 * holeR, 0, 360, Arc2D.CHORD)); } for (int i = 0; i < nSlices; i++) { Slice slc = slices.get(i); values[i] = slc.getValue().getValue(map); total += values[i]; stackedValues[i] = total; RealParameter gap = slc.getGap(); if (gap != null) { gaps[i] = Uom.toPixel(slc.getGap().getValue(map), this.getUom(), mt.getDpi(), mt.getScaleDenominator(), r); } else { gaps[i] = 0.0; } } if (this.getTransform() != null) { at.concatenate(this.getTransform().getGraphicalAffineTransform(false, map, mt, r, r)); } // Now, the total is defines, we can compute percentages and slices begin/end angles double[] percentages = new double[nSlices]; for (int i = 0; i < nSlices; i++) { if (i == 0) { percentages[i] = 0.0; } else { percentages[i] = stackedValues[(i - 1 + nSlices) % nSlices] / total; } } // Create BufferedImage imageWidth x imageWidth Shape[] shapes = new Shape[nSlices]; double maxDeg = 360.0; if (this.getType() == PieChartSubType.HALF) { maxDeg = 180.0; } // Create slices for (int i = 0; i < nSlices; i++) { double aStart = percentages[i] * maxDeg; double aExtend; if (i < nSlices - 1) { aExtend = (percentages[(i + 1) % nSlices] - percentages[i]) * maxDeg; } else { aExtend = maxDeg - (percentages[i]) * maxDeg; } Area gSlc = new Area(new Arc2D.Double(-r, -r, 2 * r, 2 * r, aStart, aExtend, Arc2D.PIE)); if (hole != null) { gSlc.subtract(hole); } double alphaMiddle = (aStart + aExtend / 2.0) * Math.PI / 180.0; // create AT = GraphicTransform + T(gap) AffineTransform gapAt = AffineTransform.getTranslateInstance(Math.cos(alphaMiddle) * gaps[i], -Math.sin(alphaMiddle) * gaps[i]); gapAt.preConcatenate(at); Shape atShp = gapAt.createTransformedShape(gSlc); shapes[i] = atShp; Fill fill = getSlice(i).getFill(); if (fill != null) { fill.draw(g2, map, atShp, selected, mt); } if (displayValue) { double p; if (i == nSlices - 1) { p = 1.0 - percentages[i]; } else { p = percentages[i + 1] - percentages[i]; } p *= 100; StyledText label = new StyledText(Integer.toString((int) Math.round(p))); AffineTransform labelAt = (AffineTransform) gapAt.clone(); double labelPosRatio; if (this.holeRadius != null) { labelPosRatio = (r - holeR) / 2.0 + holeR; } else { labelPosRatio = r * 0.66; } labelAt.concatenate(AffineTransform.getTranslateInstance(Math.cos(alphaMiddle) * labelPosRatio, -Math.sin(alphaMiddle) * labelPosRatio)); Rectangle2D anchor = labelAt.createTransformedShape(new Rectangle2D.Double(0, 0, 1, 1)).getBounds2D(); label.draw(g2, map, selected, mt, AffineTransform.getTranslateInstance(anchor.getCenterX(), anchor.getCenterY()), null); } } // Stokes must be drawn after fills if (stroke != null) { for (int i = 0; i < nSlices; i++) { stroke.draw(g2, map, shapes[i], selected, mt, 0.0); } } } @Override public void updateGraphic() { } @Override public List<SymbolizerNode> getChildren() { List<SymbolizerNode> ls = new ArrayList<SymbolizerNode>(); if (radius != null) { ls.add(radius); } if (holeRadius != null) { ls.add(holeRadius); } if (stroke != null) { ls.add(stroke); } if (transform != null) { ls.add(transform); } ls.addAll(slices); return ls; } @Override public JAXBElement<PieChartType> getJAXBElement() { PieChartType p = new PieChartType(); if (type != null) { if (type == PieChartSubType.HALF) { p.setPieSubtype(PieSubtypeType.HALF); } else { p.setPieSubtype(PieSubtypeType.WHOLE); } } if (uom != null) { p.setUom(uom.toURN()); } if (transform != null) { p.setTransform(this.transform.getJAXBType()); } if (radius != null) { p.setRadius(radius.getJAXBParameterValueType()); } if (holeRadius != null) { p.setHoleRadius(holeRadius.getJAXBParameterValueType()); } if (stroke != null) { p.setStroke(stroke.getJAXBElement()); } List<SliceType> slcs = p.getSlice(); for (Slice s : slices) { slcs.add(s.getJAXBType()); } ObjectFactory of = new ObjectFactory(); return of.createPieChart(p); } }