/**
 * Copyright 2010-2014 Jingjing Li.
 *
 * This file is part of jplot2d.
 *
 * jplot2d is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or any later version.
 *
 * jplot2d 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 Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with jplot2d. If not, see .
 */
package org.jplot2d.element.impl;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;

import org.jplot2d.element.HAlign;
import org.jplot2d.element.Plot;
import org.jplot2d.element.LegendPosition;
import org.jplot2d.element.VAlign;
import org.jplot2d.util.DoubleDimension2D;

/**
 * @author Jingjing Li
 * 
 */
public class LegendImpl extends ComponentImpl implements LegendEx {

	private static final double VERTICAL_BORDER = 2;

	private static final double HORIZONTAL_BORDER = 8;

	private static final double COLUMN_SPACE = 8;

	private static final Color BORDER_COLOR = Color.BLACK;

	private boolean enabled = true;

	private double locX, locY;

	private double width, height;

	private LegendPosition position = LegendPosition.BOTTOMCENTER;

	private HAlign halign = HAlign.CENTER;

	private VAlign valign = VAlign.TOP;

	private double rowSpacingFactor = 0.125;

	private boolean borderVisible = true;

	private Dimension2D maxItemSize;

	private int columns = 1, rows = 1;

	private double lengthConstraint = Double.NaN;

	private boolean sizeCalculationNeeded;

	private boolean layoutItemsNeeded;

	/**
	 * If this legend is enabled, or top level legend, the collection contains all items managed by this legend, include
	 * items pushed by disabled legend of subplots. If this legend is disabled, the collection is empty.
	 */
	private final Collection items = new ArrayList();

	/**
	 * The visible item count. only be maintained when this legend is enabled or is top level legend.
	 */
	private int visibleItemNum;

	public LegendImpl() {
		setSelectable(true);
		setMovable(true);
	}

	public String getId() {
		if (getParent() != null) {
			return "Legend";
		} else {
			return "Legend@" + Integer.toHexString(System.identityHashCode(this));
		}
	}

	public InvokeStep getInvokeStepFormParent() {
		if (parent == null) {
			return null;
		}

		Method method;
		try {
			method = Plot.class.getMethod("getLegend");
		} catch (NoSuchMethodException e) {
			throw new Error(e);
		}
		return new InvokeStep(method);
	}

	public PlotEx getParent() {
		return (PlotEx) super.getParent();
	}

	/**
	 * This method should be called when the visibility or position changed.
	 */
	private void invalidatePlot() {
		if (getParent() != null) {
			getParent().invalidate();
		}
	}

	/*
	 * Only contribute contents when it has visible items.
	 */
	public boolean canContribute() {
		return visibleItemNum > 0;
	}

	public void setVisible(boolean visible) {
		super.setVisible(visible);
		if (canContribute() && getPosition() != LegendPosition.FREE) {
			invalidatePlot();
		}
	}

	public void thisEffectiveColorChanged() {
		redraw(this);
	}

	public void thisEffectiveFontChanged() {
		for (LegendItemEx item : items) {
			item.legendEffectiveFontChanged();
		}
	}

	public Dimension2D getSize() {
		return new DoubleDimension2D(width, height);
	}

	public Rectangle2D getBounds() {
		double x, y;
		switch (getHAlign()) {
		case RIGHT:
			x = -width;
			break;
		case CENTER:
			x = -width / 2;
			break;
		default:
			x = 0;
		}
		switch (getVAlign()) {
		case TOP:
			y = -height;
			break;
		case MIDDLE:
			y = -height / 2;
			break;
		default:
			y = 0;
		}
		return new Rectangle2D.Double(x, y, width, height);
	}

	public Point2D getLocation() {
		return new Point2D.Double(locX, locY);
	}

	public void setLocation(Point2D loc) {
		setLocation(loc.getX(), loc.getY());
	}

	public void setLocation(double locX, double locY) {
		directLocation(locX, locY);
		setPosition(LegendPosition.FREE);
	}

	public void directLocation(double locX, double locY) {
		if (this.locX != locX || this.locY != locY) {
			this.locX = locX;
			this.locY = locY;
			redraw(this);
		}
	}

	public LegendPosition getPosition() {
		return position;
	}

	public void setPosition(LegendPosition position) {
		if (position == null) {
			position = LegendPosition.FREE;
		}
		if (this.position != position) {
			this.position = position;
			if (isVisible() && canContribute()) {
				invalidatePlot();
			}
		}
	}

	public HAlign getHAlign() {
		return halign;
	}

	public void setHAlign(HAlign halign) {
		this.halign = halign;
		layoutItemsNeeded = true;
	}

	public VAlign getVAlign() {
		return valign;
	}

	public void setVAlign(VAlign valign) {
		this.valign = valign;
		layoutItemsNeeded = true;
	}

	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		boolean oldContribution = this.canContribute();

		if (enabled) {
			LegendEx oldLegend = getEnabledLegend(getParent());
			LegendItemEx[] allItems = oldLegend.getItems();
			this.enabled = enabled;
			for (LegendItemEx item : allItems) {
				if (getEnabledLegend(item.getParent().getParent().getParent()) == this) {
					oldLegend.removeLegendItem(item);
					this.addLegendItem(item);
				}
			}
		} else {
			this.enabled = enabled;
			LegendEx newLegend = getEnabledLegend(getParent());
			if (newLegend != this) {
				for (LegendItemEx item : items) {
					newLegend.addLegendItem(item);
				}
				items.clear();
				visibleItemNum = 0;
				maxItemSize = null;
			}
		}

		if (isVisible() && oldContribution != this.canContribute() && getPosition() != LegendPosition.FREE) {
			invalidatePlot();
		}
	}

	public void putItemsToEnabledLegend() {
		if (!isEnabled() && items.size() > 0) {
			LegendEx newLegend = getEnabledLegend(getParent());
			for (LegendItemEx item : items) {
				newLegend.addLegendItem(item);
			}
			items.clear();
			visibleItemNum = 0;
			maxItemSize = null;
		}
	}

	/**
	 * Return an enable legend who can host legend items for the given plot. This method always return a legend. If all
	 * legend is disabled, the top level legend is returned.
	 * 
	 * @param plot
	 * @return an enable legend
	 */
	private static LegendEx getEnabledLegend(PlotEx plot) {
		if (plot.getLegend().isEnabled()) {
			return plot.getLegend();
		} else if (plot.getParent() == null) {
			return plot.getLegend();
		} else {
			return getEnabledLegend(plot.getParent());
		}
	}

	public int getColumns() {
		return columns;
	}

	public void setColumns(int columns) {
		if (columns < 1) {
			throw new IllegalArgumentException("Columns number must great than 0.");
		}
		this.columns = columns;

		sizeCalculationNeeded = true;
		redraw(this);
	}

	public double getRowSpacingFactor() {
		return rowSpacingFactor;
	}

	public void setRowSpacingFactor(double factor) {
		rowSpacingFactor = factor;
		sizeCalculationNeeded = true;
	}

	public boolean isBorderVisible() {
		return borderVisible;
	}

	public void setBorderVisible(boolean visible) {
		borderVisible = visible;
		redraw(this);
	}

	public LegendItemEx[] getItems() {
		return items.toArray(new LegendItemEx[items.size()]);
	}

	public void addLegendItem(LegendItemEx item) {
		if (isEnabled() || getParent().getParent() == null) {
			items.add(item);
			item.setLegend(this);
			if (isItemVisible(item)) {
				incVisibleItemNum();
				maxItemSize = null;
				sizeCalculationNeeded = true;
				if (isVisible()) {
					redraw(this);
				}
			}
		} else {
			getParent().getParent().getLegend().addLegendItem(item);
		}
	}

	public void removeLegendItem(LegendItemEx item) {
		if (isEnabled() || getParent().getParent() == null) {
			if (isItemVisible(item)) {
				decVisibleItemNum();
				if (item.getSize().equals(maxItemSize)) {
					maxItemSize = null;
				}
				sizeCalculationNeeded = true;
				redraw(this);
			}
			items.remove(item);
			item.setLegend(null);
		} else {
			getParent().getParent().getLegend().removeLegendItem(item);
		}
	}

	public void itemVisibilityChanged(LegendItemImpl item) {
		if (item.canContribute()) {
			if (item.isVisible()) {
				incVisibleItemNum();
				maxItemSize = null;
			} else {
				decVisibleItemNum();
				if (item.getSize().equals(maxItemSize)) {
					maxItemSize = null;
				}
			}
			sizeCalculationNeeded = true;
			redraw(this);
		}
	}

	public void itemContribitivityChanged(LegendItemImpl item) {
		if (item.isVisible()) {
			if (item.canContribute()) {
				incVisibleItemNum();
			} else {
				decVisibleItemNum();
			}
			maxItemSize = null;
			sizeCalculationNeeded = true;
			redraw(this);
		}
	}

	/**
	 * Returns true when both visible and contributable are true. Otherwise returns
	 * false.
	 * 
	 * @param item
	 *            the legend item
	 * @return true when both visible and contributable are true
	 */
	private boolean isItemVisible(LegendItemEx item) {
		return item.isVisible() && item.canContribute();
	}

	private void incVisibleItemNum() {
		visibleItemNum++;
		if (visibleItemNum == 1 && isVisible()) {
			invalidatePlot();
		}
	}

	private void decVisibleItemNum() {
		visibleItemNum--;
		if (visibleItemNum == 0 && isVisible()) {
			invalidatePlot();
		}
	}

	public void itemSizeChanged(LegendItemEx item) {
		if (item.isVisible()) {
			maxItemSize = null;
			sizeCalculationNeeded = true;
			redraw(this);
		}
	}

	@Override
	public void copyFrom(ElementEx src) {
		super.copyFrom(src);

		LegendImpl legend = (LegendImpl) src;
		enabled = legend.enabled;
		locX = legend.locX;
		locY = legend.locY;
		width = legend.width;
		height = legend.height;
		position = legend.position;
		halign = legend.halign;
		valign = legend.valign;
		rowSpacingFactor = legend.rowSpacingFactor;
		borderVisible = legend.borderVisible;
		maxItemSize = legend.maxItemSize;
		rows = legend.rows;
		columns = legend.columns;
		lengthConstraint = legend.lengthConstraint;
		sizeCalculationNeeded = legend.sizeCalculationNeeded;
		layoutItemsNeeded = legend.layoutItemsNeeded;
		visibleItemNum = legend.visibleItemNum;
	}

	public double getThickness() {
		switch (getPosition()) {
		case TOPLEFT:
		case TOPCENTER:
		case TOPRIGHT:
		case BOTTOMLEFT:
		case BOTTOMCENTER:
		case BOTTOMRIGHT:
			return getSize().getHeight();
		case LEFTTOP:
		case LEFTMIDDLE:
		case LEFTBOTTOM:
		case RIGHTTOP:
		case RIGHTMIDDLE:
		case RIGHTBOTTOM:
			return getSize().getWidth();
		default:
			return 0;
		}
	}

	public void calcSize() {

		if (visibleItemNum == 0) {
			width = 0;
			height = 0;
			sizeCalculationNeeded = false;
			return;
		}

		PlotEx plot = getParent();
		switch (getPosition()) {
		case TOPLEFT:
		case TOPCENTER:
		case TOPRIGHT:
		case BOTTOMLEFT:
		case BOTTOMCENTER:
		case BOTTOMRIGHT: {
			double legendWidth = plot.getSize().getWidth() - plot.getMargin().getExtraLeft()
					- plot.getMargin().getExtraRight();
			setLengthConstraint(legendWidth);
			break;
		}
		case LEFTTOP:
		case LEFTMIDDLE:
		case LEFTBOTTOM:
		case RIGHTTOP:
		case RIGHTMIDDLE:
		case RIGHTBOTTOM: {
			double contentHeight = plot.getContentSize().getHeight();
			setLengthConstraint(contentHeight);
			break;
		}
		default:
			// calculate rows
			int nrow = visibleItemNum / columns;
			if (visibleItemNum % columns > 0) {
				nrow++;
			}
			rows = nrow;
		}

		if (sizeCalculationNeeded) {
			switch (getPosition()) {
			case TOPLEFT:
			case TOPCENTER:
			case TOPRIGHT:
			case BOTTOMLEFT:
			case BOTTOMCENTER:
			case BOTTOMRIGHT: {
				fitColumnsToWidth(lengthConstraint);
				break;
			}
			case LEFTTOP:
			case LEFTMIDDLE:
			case LEFTBOTTOM:
			case RIGHTTOP:
			case RIGHTMIDDLE:
			case RIGHTBOTTOM: {
				fitRowsToHeight(lengthConstraint);
				break;
			}
			}

			width = (getMaxItemSize().getWidth() + COLUMN_SPACE) * columns - COLUMN_SPACE + 2 * HORIZONTAL_BORDER;
			height = getMaxItemSize().getHeight() * rows + getMaxItemSize().getHeight() * rowSpacingFactor * (rows - 1)
					+ 2 * VERTICAL_BORDER;

			sizeCalculationNeeded = false;
			layoutItemsNeeded = true;
		}

		if (layoutItemsNeeded) {
			layoutItems();
		}
	}

	private void setLengthConstraint(double length) {
		if (lengthConstraint != length) {
			lengthConstraint = length;
			sizeCalculationNeeded = true;
		}
	}

	/**
	 * Calculate the proper column number to fit the given width.
	 * 
	 * @param width
	 *            the width to fit for
	 */
	private void fitColumnsToWidth(double width) {
		Dimension2D lisize = getMaxItemSize();

		// the max possible columns
		double maxColumns = (width - 2 * HORIZONTAL_BORDER + COLUMN_SPACE) / (lisize.getWidth() + COLUMN_SPACE);
		int ncol = (int) maxColumns;
		if (ncol < 1) {
			ncol = 1;
		} else if (ncol > visibleItemNum) {
			ncol = visibleItemNum;
		}
		int nrow = visibleItemNum / ncol;
		if (visibleItemNum % ncol > 0) {
			nrow++;
		}

		columns = ncol;
		rows = nrow;
	}

	/**
	 * Calculate the proper column number to fit the given height.
	 * 
	 * @param height
	 *            the height to fit for
	 */
	private void fitRowsToHeight(double height) {
		Dimension2D lisize = getMaxItemSize();

		double maxRows = ((height - 2 * VERTICAL_BORDER) / lisize.getHeight() + rowSpacingFactor)
				/ (1 + rowSpacingFactor);
		int nrow = (int) maxRows;
		if (nrow < 1) {
			nrow = 1;
		} else if (nrow > visibleItemNum) {
			nrow = visibleItemNum;
		}
		int ncol = visibleItemNum / nrow;
		if (visibleItemNum % nrow > 0) {
			ncol++;
		}

		columns = ncol;
		rows = nrow;
	}

	/**
	 * Locate all items of this legend.
	 */
	private void layoutItems() {
		if ((visibleItemNum == 0) || !isVisible()) {
			return;
		}

		double[] lipx = new double[columns];
		double[] lipy = new double[rows];
		for (int i = 0; i < columns; i++) {
			lipx[i] = HORIZONTAL_BORDER + i * getMaxItemSize().getWidth() + i * COLUMN_SPACE;
		}
		for (int i = 0; i < rows; i++) {
			lipy[i] = VERTICAL_BORDER + (rows - i - 0.5) * getMaxItemSize().getHeight() + (rows - 1 - i)
					* getMaxItemSize().getHeight() * rowSpacingFactor;
		}

		Rectangle2D bounds = getBounds();
		double xoff = bounds.getX();
		double yoff = bounds.getY();
		int col = 0, row = 0;
		for (LegendItemEx item : items) {
			if (isItemVisible(item)) {
				item.setLocation(lipx[col] + xoff, lipy[row] + yoff);
				col++;
				if (col >= columns) {
					col = 0;
					row++;
				}
			}
		}
	}

	/**
	 * find largest legend item size
	 */
	private Dimension2D getMaxItemSize() {
		if (maxItemSize != null) {
			return maxItemSize;
		}

		double maxWidth = 0;
		double maxHeight = 0;
		for (LegendItemEx item : items) {
			if (isItemVisible(item)) {
				Dimension2D psize = item.getSize();
				double pwidth = psize.getWidth();
				double pheight = psize.getHeight();
				if (maxWidth < pwidth) {
					maxWidth = pwidth;
				}
				if (maxHeight < pheight) {
					maxHeight = pheight;
				}
			}
		}

		maxItemSize = new DoubleDimension2D(maxWidth, maxHeight);
		return maxItemSize;
	}

	public void draw(Graphics2D graphics) {
		if (!canContribute()) {
			return;
		}

		// drawBounds(graphics);

		// transform to relative paper space
		Graphics2D g = (Graphics2D) graphics.create();
		g.transform(getPaperTransform().getTransform());

		if (borderVisible) {
			Object aahint = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
			g.setColor(BORDER_COLOR);
			g.draw(getBounds());
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, aahint);
		}

		for (LegendItemEx item : items) {
			if (isItemVisible(item)) {
				item.draw(g);
			}
		}

		g.dispose();
	}

}

 

Ads help maintain this website.