Chart.java

package de.slothsoft.charts;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import de.slothsoft.charts.common.Border;
import de.slothsoft.charts.common.Title;
import de.slothsoft.charts.internal.RefreshListeners;

/**
 * This is the base class this library is for. It represents an abstract chart of some
 * sort, weather it's a line chart or something entirely different. Classes are connected
 * like this: <br>
 * <img src="https://raw.githubusercontent.com/wiki/slothsoft/charts/images/UML.png" alt=
 * "UML Diagram">
 *
 * @author Stef Schulz
 * @since 0.1.0
 */

public abstract class Chart {

	private final Border border = new Border();
	private final Title title = new Title();

	private int backgroundColor = 0xFFFFFFFF;

	private final List<ChartPart> chartParts = new ArrayList<>();
	RefreshListeners refreshListeners = new RefreshListeners(this);

	/**
	 * Default constructor.
	 */

	public Chart() {
		addChartPart(this.border);
		addChartPart(this.title);
	}

	protected void addChartPart(ChartPart chartPart) {
		chartPart.addRefreshListener(e -> fireRefreshNeeded());
		this.chartParts.add(chartPart);
	}

	/**
	 * Paints the current content onto the graphic context. Checks the instructions for
	 * what to paint. The instructions contain the area in display coordinates, starting
	 * from the top left with 0|0 and ending bottom right with something like 800|600.
	 *
	 * @param gc graphic context; coordinates are relative to the screen
	 * @param instructions additional instructions like the area to paint on
	 */

	public void paintOn(GraphicContext gc, PaintInstructions instructions) {
		gc.setColor(this.backgroundColor);
		gc.fillRectangle(instructions.area);

		// paint all the parts that make up the chart

		Area graphArea = instructions.getArea();
		for (final ChartPart part : fetchChartParts()) {
			part.paintOn(gc, instructions.area(graphArea));
			graphArea = part.snipNecessarySpace(graphArea);
		}

		// now paint the actual graph

		try {
			gc.clip(graphArea);
			gc.translate(graphArea.getStartX(), graphArea.getStartY());

			paintGraph(gc,
					instructions.area(new Area(graphArea.endX - graphArea.startX, graphArea.endY - graphArea.startY)));
		} finally {
			gc.translate(-graphArea.getStartX(), -graphArea.getEndY());
			gc.clip(null);
		}
	}

	/**
	 * Calculates the graph area by removing the {@link ChartPart}s from the entire width
	 * and height.
	 *
	 * @param width the entire width
	 * @param height the entire height
	 * @return the graph area
	 */

	public Area calculateGraphArea(double width, double height) {
		Area result = new Area(width, height);
		for (final ChartPart part : fetchChartParts()) {
			result = part.snipNecessarySpace(result);
		}
		return result;
	}

	/**
	 * Returns all the {@link ChartPart}s this chart nows about. Override this method to
	 * add custom parts.
	 *
	 * @return a list of chart parts
	 */

	protected final Collection<ChartPart> fetchChartParts() {
		return Collections.unmodifiableList(this.chartParts);
	}

	/**
	 * Paints the actual graph (the white part in <a href=
	 * "https://github.com/slothsoft/charts/wiki/Preliminary-Considerations">Preliminary
	 * Considerations</a>). Coordinates are starting from the top left with 0|0 and ending
	 * bottom right with something like 800|600.
	 *
	 * @param gc graphic context; coordinates are relative to the screen
	 * @param instructions additional instructions like the area to paint on
	 */

	protected abstract void paintGraph(GraphicContext gc, PaintInstructions instructions);

	/**
	 * Fires a default event for the {@link RefreshListener}s of this chart.
	 */

	protected void fireRefreshNeeded() {
		fireRefreshNeeded(new RefreshListener.Event(this));
	}

	/**
	 * Fires an event for the {@link RefreshListener}s of this chart.
	 *
	 * @param event the event to be fired
	 */

	protected void fireRefreshNeeded(RefreshListener.Event event) {
		this.refreshListeners.fireRefreshNeeded(event);
	}

	/**
	 * Adds a refresh listener that is called whenever this {@link Chart} needs to be
	 * redrawn by the GUI.
	 *
	 * @param listener a listener
	 */

	public void addRefreshListener(RefreshListener listener) {
		this.refreshListeners.addRefreshListener(listener);
	}

	/**
	 * Removes a refresh listener that was called whenever this {@link Chart} needed to be
	 * redrawn by the GUI. Does nothing if the listener was never added.
	 *
	 * @param listener a listener
	 */

	public void removeRefreshListener(RefreshListener listener) {
		this.refreshListeners.removeRefreshListener(listener);
	}

	/**
	 * Returns the border this chart has. The border is supposed to be around everything
	 * else like this:<br>
	 * <img src=
	 * "https://raw.githubusercontent.com/wiki/slothsoft/charts/images/chart-design.png"
	 * alt="Chart Parts">
	 *
	 * @return the border
	 */

	public Border getBorder() {
		return this.border;
	}

	/**
	 * Returns the title this chart has. The title is supposed to be on the top of
	 * everything like this:<br>
	 * <img src=
	 * "https://raw.githubusercontent.com/wiki/slothsoft/charts/images/chart-design.png"
	 * alt="Chart Parts">
	 *
	 * @return the border
	 */

	public Title getTitle() {
		return this.title;
	}

	/**
	 * Returns the color as ARGB int, e.g. red is <code>0xFFFF0000</code> and blue is
	 * <code>0xFF0000FF</code>.
	 *
	 * @return the color
	 */

	public int getBackgroundColor() {
		return this.backgroundColor;
	}

	/**
	 * Sets the color as ARGB int, e.g. red is <code>0xFFFF0000</code> and blue is
	 * <code>0xFF0000FF</code>.
	 *
	 * @param newBackgroundColor the color
	 * @return this instance
	 */

	public Chart backgroundColor(int newBackgroundColor) {
		setBackgroundColor(newBackgroundColor);
		return this;
	}

	/**
	 * Sets the color as ARGB int, e.g. red is <code>0xFFFF0000</code> and blue is
	 * <code>0xFF0000FF</code>.
	 *
	 * @param backgroundColor the color
	 */

	public void setBackgroundColor(int backgroundColor) {
		final int oldBackgroundColor = this.backgroundColor;
		this.backgroundColor = backgroundColor;
		if (oldBackgroundColor != this.backgroundColor) {
			fireRefreshNeeded();
		}
	}

}