⛏️ index : haiku.git

/*
 * Copyright 2009, Ingo Weinhold, ingo_weinhold@gmx.de.
 * Distributed under the terms of the MIT License.
 */

#include "chart/LegendChartAxis.h"

#include <limits.h>
#include <stdio.h>

#include <algorithm>
#include <new>

#include <Font.h>
#include <View.h>

#include "chart/ChartLegend.h"
#include "chart/ChartAxisLegendSource.h"


static const int32 kChartRulerDistance = 2;
static const int32 kRulerSize = 3;
static const int32 kRulerMarkSize = 3;
static const int32 kRulerLegendDistance = 2;
static const int32 kChartLegendDistance
	= kChartRulerDistance + kRulerSize + kRulerMarkSize + kRulerLegendDistance;



struct LegendChartAxis::LegendInfo {
	ChartLegend*	legend;
	double			value;
	BSize			size;

	LegendInfo(ChartLegend* legend, double value, BSize size)
		:
		legend(legend),
		value(value),
		size(size)
	{
	}

	~LegendInfo()
	{
		delete legend;
	}
};


float
LegendChartAxis::_LegendPosition(double value, float legendSize,
	float totalSize, double scale)
{
	float position = (value - fRange.min) * scale - legendSize / 2;
	if (position + legendSize > totalSize)
		position = totalSize - legendSize;
	if (position < 0)
		position = 0;
	return position;
}


void
LegendChartAxis::_FilterLegends(int32 totalSize, int32 spacing,
	float BSize::* sizeField)
{
	// compute the min/max legend levels
	int32 legendCount = fLegends.CountItems();
	int32 minLevel = INT_MAX;
	int32 maxLevel = 0;

	for (int32 i = 0; i < legendCount; i++) {
		LegendInfo* info = fLegends.ItemAt(i);
		int32 level = info->legend->Level();
		if (level < minLevel)
			minLevel = level;
		if (level > maxLevel)
			maxLevel = level;
	}

	if (maxLevel <= 0)
		return;

	double rangeSize = fRange.max - fRange.min;
	if (rangeSize == 0)
		rangeSize = 1.0;
	double scale = (double)totalSize / rangeSize;

	// Filter out all higher level legends colliding with lower level or
	// preceeding same-level legends. We iterate backwards from the lower to
	// the higher levels
	for (int32 level = std::max(minLevel, (int32)0); level <= maxLevel;) {
		legendCount = fLegends.CountItems();

		// get the first legend position/end
		LegendInfo* info = fLegends.ItemAt(0);
		float position = _LegendPosition(info->value, info->size.*sizeField,
			(float)totalSize, scale);;

		int32 previousEnd = (int32)ceilf(position + info->size.*sizeField);
		int32 previousLevel = info->legend->Level();

		for (int32 i = 1; (info = fLegends.ItemAt(i)) != NULL; i++) {
			float position = _LegendPosition(info->value, info->size.*sizeField,
				(float)totalSize, scale);;

			if (position - spacing < previousEnd
				&& (previousLevel <= level
					|| info->legend->Level() <= level)
				&& std::max(previousLevel, info->legend->Level()) > 0) {
				// The item intersects with the previous one -- remove the
				// one at the higher level.
				if (info->legend->Level() >= previousLevel) {
					// This item is at the higher level -- remove it.
					delete fLegends.RemoveItemAt(i);
					i--;
					continue;
				}

				// The previous item is at the higher level -- remove it.
				delete fLegends.RemoveItemAt(i - 1);
				i--;
			}

			if (i == 0 && position < 0)
				position = 0;
			previousEnd = (int32)ceilf(position + info->size.*sizeField);
			previousLevel = info->legend->Level();
		}

		// repeat with the level as long as we've removed something
		if (legendCount == fLegends.CountItems())
			level++;
	}
}


LegendChartAxis::LegendChartAxis(ChartAxisLegendSource* legendSource,
	ChartLegendRenderer* legendRenderer)
	:
	fLegendSource(legendSource),
	fLegendRenderer(legendRenderer),
	fLocation(CHART_AXIS_BOTTOM),
	fRange(),
	fFrame(),
	fLegends(20),
	fHorizontalSpacing(20),
	fVerticalSpacing(10),
	fLayoutValid(false)
{
}


LegendChartAxis::~LegendChartAxis()
{
}


void
LegendChartAxis::SetLocation(ChartAxisLocation location)
{
	if (location != fLocation) {
		fLocation = location;
		_InvalidateLayout();
	}
}


void
LegendChartAxis::SetRange(const ChartDataRange& range)
{
	if (range != fRange) {
		fRange = range;
		_InvalidateLayout();
	}
}


void
LegendChartAxis::SetFrame(BRect frame)
{
	if (frame != fFrame) {
		fFrame = frame;
		_InvalidateLayout();
	}
}


BSize
LegendChartAxis::PreferredSize(BView* view, BSize maxSize)
{
	// estimate the maximum legend count we might need
	float hSpacing, vSpacing;
	int32 maxLegends = _EstimateMaxLegendCount(view, maxSize, &hSpacing,
		&vSpacing);
	BSize spacing(hSpacing, vSpacing);
	if (maxLegends < 4)
		maxLegends = 4;

	// get the legends
	ChartLegend* legends[maxLegends];
	double values[maxLegends];

	int32 legendCount = fLegendSource->GetAxisLegends(fRange, legends, values,
		maxLegends);

	// get the sizes, delete the legends, and compute the preferred size
	float BSize::* sizeField;
	float BSize::* otherSizeField;
	if (fLocation == CHART_AXIS_LEFT || fLocation == CHART_AXIS_RIGHT) {
		sizeField = &BSize::height;
		otherSizeField = &BSize::width;
	} else {
		sizeField = &BSize::width;
		otherSizeField = &BSize::height;
	}

	BSize preferredSize;

	for (int32 i = 0; i < legendCount; i++) {
		ChartLegend* legend = legends[i];
		BSize size = fLegendRenderer->LegendSize(legend, view);
		delete legend;

		if (size.*sizeField > preferredSize.*sizeField)
			preferredSize.*sizeField = size.*sizeField;
		if (size.*otherSizeField > preferredSize.*otherSizeField)
			preferredSize.*otherSizeField = size.*otherSizeField;
	}

	// Suppose we want to have at least 2 legends.
	preferredSize.*sizeField
		= ceilf(preferredSize.*sizeField * 2 + spacing.*sizeField);
	preferredSize.*otherSizeField += kChartLegendDistance;

	return preferredSize;
}


void
LegendChartAxis::Render(BView* view, BRect updateRect)
{
	if (!_ValidateLayout(view))
		return;

	float valueDirection;
	float rulerDirection;
	float BSize::* sizeField;
	float BSize::* otherSizeField;
	float BPoint::* pointField;
	float BPoint::* otherPointField;

	switch (fLocation) {
		case CHART_AXIS_LEFT:
		case CHART_AXIS_RIGHT:
			valueDirection = -1;
			rulerDirection = fLocation == CHART_AXIS_LEFT ? -1 : 1;
			sizeField = &BSize::height;
			otherSizeField = &BSize::width;
			pointField = &BPoint::y;
			otherPointField = &BPoint::x;
			break;
		case CHART_AXIS_TOP:
		case CHART_AXIS_BOTTOM:
			valueDirection = 1;
			rulerDirection = fLocation == CHART_AXIS_TOP ? -1 : 1;
			sizeField = &BSize::width;
			otherSizeField = &BSize::height;
			pointField = &BPoint::x;
			otherPointField = &BPoint::y;
			break;
		default:
			return;
	}

	float totalSize = floorf(fFrame.Size().*sizeField) + 1;
	double rangeSize = fRange.max - fRange.min;
	if (rangeSize == 0)
		rangeSize = 1.0;
	double scale = (double)totalSize / rangeSize;

	// draw the ruler
	float rulerStart = fFrame.LeftBottom().*pointField;
	float rulerChartClosest = rulerDirection == 1
		? fFrame.LeftTop().*otherPointField + kChartRulerDistance
		: fFrame.RightBottom().*otherPointField - kChartRulerDistance;
	float rulerEnd = fFrame.RightTop().*pointField;
	float rulerChartDistant = rulerChartClosest + rulerDirection * kRulerSize;

	rgb_color black = { 0, 0, 0, 255 };
	view->BeginLineArray(3 + fLegends.CountItems());
	BPoint first;
	first.*pointField = rulerStart;
	first.*otherPointField = rulerChartClosest;
	BPoint second = first;
	second.*otherPointField = rulerChartDistant;
	BPoint third = second;
	third.*pointField = rulerEnd;
	BPoint fourth = third;
	fourth.*otherPointField = rulerChartClosest;
	view->AddLine(first, second, black);
	view->AddLine(second, third, black);
	view->AddLine(third, fourth, black);

	// marks
	for (int32 i = 0; LegendInfo* info = fLegends.ItemAt(i); i++) {
		float position = (info->value - fRange.min) * scale;
		position = rulerStart + valueDirection * position;
		first.*pointField = position;
		first.*otherPointField = rulerChartDistant;
		second.*pointField = position;
		second.*otherPointField = rulerChartDistant
			+ rulerDirection * kRulerMarkSize;
		view->AddLine(first, second, black);
	}
	view->EndLineArray();

	// draw the legends
	float legendRulerClosest = rulerChartDistant
		+ rulerDirection * (kRulerMarkSize + kRulerLegendDistance);

	for (int32 i = 0; LegendInfo* info = fLegends.ItemAt(i); i++) {
		float position = _LegendPosition(info->value, info->size.*sizeField,
			(float)totalSize, scale);;

		first.*pointField = rulerStart
			+ (valueDirection == 1
				? position : -position - info->size.*sizeField);
		first.*otherPointField = rulerDirection == 1
			? legendRulerClosest
			: legendRulerClosest - info->size.*otherSizeField;

		fLegendRenderer->RenderLegend(info->legend, view, first);
	}
}


void
LegendChartAxis::_InvalidateLayout()
{
	fLayoutValid = false;
}


bool
LegendChartAxis::_ValidateLayout(BView* view)
{
	if (fLayoutValid)
		return true;

	fLegends.MakeEmpty();

	int32 width = fFrame.IntegerWidth() + 1;
	int32 height = fFrame.IntegerHeight() + 1;

	// estimate the maximum legend count we might need
	int32 maxLegends = _EstimateMaxLegendCount(view, fFrame.Size(),
		&fHorizontalSpacing, &fVerticalSpacing);

	if (maxLegends == 0)
		return false;

	// get the legends
	ChartLegend* legends[maxLegends];
	double values[maxLegends];

	int32 legendCount = fLegendSource->GetAxisLegends(fRange, legends, values,
		maxLegends);
	if (legendCount == 0)
		return false;

	// create legend infos
	for (int32 i = 0; i < legendCount; i++) {
		ChartLegend* legend = legends[i];
		BSize size = fLegendRenderer->LegendSize(legend, view);
		LegendInfo* info = new(std::nothrow) LegendInfo(legend, values[i],
			size);
		if (info == NULL || !fLegends.AddItem(info)) {
			// TODO: Report error!
			delete info;
			for (int32 k = i; k < legendCount; k++)
				delete legends[k];
			return false;
		}
	}

	if (fLocation == CHART_AXIS_LEFT || fLocation == CHART_AXIS_RIGHT)
		_FilterLegends(height, fVerticalSpacing, &BSize::height);
	else
		_FilterLegends(width, fHorizontalSpacing, &BSize::width);

	fLayoutValid = true;
	return true;
}


int32
LegendChartAxis::_EstimateMaxLegendCount(BView* view, BSize size,
	float* _hSpacing, float* _vSpacing)
{
	// get the legend spacing
	fLegendRenderer->GetMinimumLegendSpacing(view, _hSpacing, _vSpacing);

	// estimate the maximum legend count we might need
	if (fLocation == CHART_AXIS_LEFT || fLocation == CHART_AXIS_RIGHT)
		return (int32)((size.IntegerHeight() + 1) / (10 + *_vSpacing));
	return (int32)((size.IntegerWidth() + 1) / (20 + *_hSpacing));
}