Click here to Skip to main content
15,886,565 members
Articles / Desktop Programming / MFC

Scientific charting control

Rate me:
Please Sign up or sign in to vote.
4.84/5 (122 votes)
17 Jan 20055 min read 1.3M   42.6K   405  
Multi-purpose scientific charting control.
// XGraphDataSerie.cpp: Implementierung der Klasse CXGraphDataSerie.
//
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "XGraphDataSerie.h"
#include "XGraph.h"
#include "GfxUtils.h"
#include "math.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

IMPLEMENT_SERIAL( CXGraphDataSerie, CXGraphObject, 1 )

//////////////////////////////////////////////////////////////////////
// Konstruktion/Destruktion
//////////////////////////////////////////////////////////////////////

CXGraphDataSerie::CXGraphDataSerie()
{
	m_bAutoDelete = false;
	m_bVisible = true;
	m_bShowMarker = false;
	m_bFillBeneath = false;
	m_bFillTransparent = true;
	m_pGraph   = NULL;
	m_pData    = NULL;
	m_nLineStyle = PS_SOLID;
	m_nFillStyle = 6;
	m_nLineSize = 1;
	m_gtType = gtLine;
	m_nFirstVisible = 0;
	m_nLastVisible  = 0;
	m_cLabel = _T(" ");
	m_nMarkerType = 0;
	m_nMarker = 0;
	m_nMarkerSize = 10;
	m_nXAxis  = 0;
	m_nYAxis  = 0;
	m_crColor = 0L;
	m_nCount  = 0; 
	m_nIndex  = 0;
	m_nFillCurve = 1;
	m_crFillColor = RGB(255, 255, 255);

}

CXGraphDataSerie::CXGraphDataSerie(const CXGraphDataSerie& copy)
{
	m_pData  = NULL;
	*this = copy;
}

CXGraphDataSerie& CXGraphDataSerie::operator =(const CXGraphDataSerie& copy)
{
	m_bVisible			= copy.m_bVisible;
	m_bShowMarker		= copy.m_bShowMarker;
	m_bFillBeneath		= copy.m_bFillBeneath;
	m_pGraph			= copy.m_pGraph;
	m_nLineStyle		= copy.m_nLineStyle;
	m_nLineSize			= copy.m_nLineSize; 
	m_gtType			= copy.m_gtType;  
	m_nXAxis			= copy.m_nXAxis;
	m_nYAxis			= copy.m_nYAxis;
	m_crColor			= copy.m_crColor;
	m_nFillStyle		= copy.m_nFillStyle;
	m_nFirstVisible		= copy.m_nFirstVisible;
	m_nLastVisible		= copy.m_nLastVisible;
	m_bFillTransparent  = copy.m_bFillTransparent;
	m_CurveRegions      = copy.m_CurveRegions;
	m_nMarkerType	    = copy.m_nMarkerType;
	m_nMarker           = copy.m_nMarker;
	m_nMarkerSize		= copy.m_nMarkerSize;
	m_bAutoDelete		= copy.m_bAutoDelete;
	m_nCount			= copy.m_nCount; 
	m_nIndex            = copy.m_nIndex;
	m_cLabel		    = copy.m_cLabel;
	m_PS                = copy.m_PS;
	m_nFillCurve		= copy.m_nFillCurve;
	m_crFillColor       = copy.m_crFillColor;

	if (copy.m_bAutoDelete && copy.m_pData)
	{
		m_pData = new TDataPoint[copy.m_nCount];
		memcpy(m_pData, copy.m_pData, sizeof(TDataPoint) * copy.m_nCount);
	}
	else
		m_pData	= copy.m_pData;
		
	return *this;
}

CXGraphDataSerie::~CXGraphDataSerie()
{
	if (m_bAutoDelete)
		delete[] m_pData;
}

void CXGraphDataSerie::PrepareClipboard(CFDATASERIE& serie)
{
	serie.nCount = m_nCount;
	serie.nFirstVisible = m_nFirstVisible;
	serie.nLastVisible = m_nLastVisible;
	serie.nXAxis = m_nXAxis;
	serie.nYAxis = m_nYAxis;
	serie.nIndex = m_nIndex;
	serie.nLineSize = m_nLineSize;
	serie.nFillStyle = m_nFillStyle;
	serie.nMarkerType = m_nMarkerType;
	serie.nMarker = m_nMarker;
	serie.nMarkerSize = m_nMarkerSize;
	serie.nLineStyle = m_nLineStyle;
	serie.bAutoDelete = true;
	serie.bShowMarker = m_bShowMarker;
	serie.bFillBeneath = m_bFillBeneath;
	serie.nFillCurve = m_nFillCurve;
	serie.bFillTransparent = m_bFillTransparent;
	serie.crFillColor = m_crFillColor;

	serie.gtType = m_gtType;
#ifndef _WIN32_WCE
	_tcscpy(serie.cLabel, m_cLabel);
#else
	strcpy(serie.cLabel, (const char*)(LPCTSTR) m_cLabel);
#endif

}

void CXGraphDataSerie::DrawMarker(CDCEx *pDC)
{	
	CPoint point;

	int nWidth = m_pGraph->m_clInnerRect.Width() / m_nMarkerSize / 4;

	int nMarkerCount = 2;
	
	if (nWidth > 0)
		nMarkerCount = m_nCount / nWidth;

	if (nMarkerCount <= 0)
		nMarkerCount = 2;

	if (m_pGraph->m_opOperation == CXGraph::opEditCurve)
		nMarkerCount = 1;


	CXGraphAxis& yaxis = m_pGraph->GetYAxis(m_nYAxis);
	CXGraphAxis& xaxis = m_pGraph->GetXAxis(m_nXAxis);

	for (int i = 0; i < m_nCount; i += nMarkerCount)
	{
		CPoint point;

		point.x = xaxis.GetPointForValue(&m_pData[i]).x - (m_nMarkerSize / 2);
		point.y = yaxis.GetPointForValue(&m_pData[i]).y - (m_nMarkerSize / 2);
	
		int nMarker = m_nMarkerType == 0 ? m_pGraph->GetCurveIndex(this) : m_nMarker;

		yaxis.DrawMarker (pDC, point, nMarker, m_nMarkerSize , pDC->m_bMono ? 0L : m_crColor, m_nMarkerType == 1 );
	}
}

void CXGraphDataSerie::CreateCurveGripRgn(CPoint oldPoint, CPoint point)
{
	HITTEST ht;
	
	ht.p1 = oldPoint;
	ht.p2 = point;

	m_CurveRegions.push_back (ht);
}


void CXGraphDataSerie::Draw(CDCEx *pDC)
{
	static CPoint oldPoint(-1,-1);
	CPoint point;
	int    i = 0;
	POINT* pPoly;
		
	COLORREF crInverted = RGB(255 - GetRValue(m_crColor),
							  255 - GetGValue(m_crColor),
							  255 - GetBValue(m_crColor));

	CPenSelector ps(pDC->m_bMono ? 0L : (m_bSelected ? crInverted : m_crColor), m_nLineSize, pDC, m_nLineStyle); 

	point.x = m_pGraph->GetXAxis(m_nXAxis).GetPointForValue(&m_pData[i]).x;
	point.y = m_pGraph->GetYAxis(m_nYAxis).GetPointForValue(&m_pData[i]).y;


	if (m_bFillBeneath)
	{
		if (m_nFillCurve != -1)
		{
			CXGraphDataSerie& ds = GetGraph()->GetCurve (m_nFillCurve);
			long nCount = ds.GetCount () ;
			pPoly = new POINT[m_nCount + nCount];

		}
		else
		{
			pPoly = new POINT[m_nCount + 2];
		}

		pPoly[0].x = point.x;
		pPoly[0].y = point.y;
		
	}

	oldPoint = point;

	CXGraphAxis& yaxis = m_pGraph->GetYAxis(m_nYAxis);
	CXGraphAxis& xaxis = m_pGraph->GetXAxis(m_nXAxis);

	if (m_gtType == gtLine)
		pDC->MoveTo(point);

	if (m_gtType == gtScatter)
		pDC->Ellipse (point.x-m_nLineSize, point.y-m_nLineSize,point.x+m_nLineSize, point.y+m_nLineSize);

	m_CurveRegions.clear ();
	
	int nPoint = 1;
	
	for (i = 1; i < m_nCount; i++)
	{
		point.x = xaxis.GetPointForValue(&m_pData[i]).x;
		point.y = yaxis.GetPointForValue(&m_pData[i]).y;
					
		if (point.x != oldPoint.x )
		{
			if ((m_pGraph->m_clInnerRect.PtInRect (point) || m_pGraph->m_clInnerRect.PtInRect (oldPoint)))
				CreateCurveGripRgn(oldPoint, point);
			
			if (m_gtType == gtLine)
				pDC->LineTo(point);

			if (m_gtType == gtScatter)
				pDC->Ellipse (point.x-m_nLineSize, point.y-m_nLineSize,point.x+m_nLineSize, point.y+m_nLineSize);

			if (m_bFillBeneath)
			{
				pPoly[nPoint].x = point.x;
				pPoly[nPoint++].y = point.y;
			}
					
			oldPoint = point;
		}
	}

	if (m_bFillBeneath)
	{
		if (m_nFillCurve != -1)
		{
			CXGraphDataSerie& ds = GetGraph()->GetCurve (m_nFillCurve);
			CXGraphAxis& yaxis =  GetGraph()->GetYAxis (ds.GetYAxis());
			CXGraphAxis& xaxis = GetGraph()->GetXAxis (ds.GetXAxis());

			for (long nFC = ds.GetCount () - 1; nFC >= 0; nFC--)
			{
				point.x = xaxis.GetPointForValue(&ds.m_pData[nFC]).x;
				point.y = yaxis.GetPointForValue(&ds.m_pData[nFC]).y;
				pPoly[nPoint].x = point.x;
				pPoly[nPoint++].y = point.y;
			}

		}
		else
		{
			pPoly[nPoint++] = CPoint(oldPoint.x, m_pGraph->m_clInnerRect.bottom) ;
			pPoly[nPoint++] = CPoint(pPoly[0].x, m_pGraph->m_clInnerRect.bottom) ;
		}
		
		int nOldBkMode = pDC->GetBkMode ();

		pDC->SetBkMode(m_bFillTransparent ? TRANSPARENT : OPAQUE);
		
		int nOldROP2 = R2_COPYPEN;
		
		if (m_bFillTransparent)
			nOldROP2 = pDC->SetROP2 (R2_NOTXORPEN);
		
		
		// Make shure the polygon's outline is invisible
		CPenSelector ps(0, 0, pDC, PS_NULL);

		if (m_nFillStyle == HS_SOLID) 
		{
			// Solid
			
			CBrushSelector bs(pDC->m_bMono ? 0L : m_crFillColor, pDC);
			pDC->Polygon (pPoly, nPoint);
		}
#ifndef _WIN32_WCE
		else
		{
			// Hatched
			CBrushSelector bs(pDC->m_bMono ? 0L : m_crFillColor, m_nFillStyle, pDC);
			pDC->Polygon (pPoly, nPoint);
		}
#endif

		pDC->SetBkMode(nOldBkMode);
		pDC->SetROP2 (nOldROP2);

		delete pPoly;
	}

}

void CXGraphDataSerie::ResetVisibleRange()
{
	m_nFirstVisible = 0;
	m_nLastVisible  = m_nCount - 1;
}


TDataPoint* CXGraphDataSerie::GetLinearTrend(long nPoints)
{
	TDataPoint* pData;
	
	pData = new TDataPoint[m_nCount + nPoints];

	double fYSum  = 0.0,
		   fTYSum = 0.0,
		   fTSum  = 0.0,
		   fT2Sum = 0.0;
			
	for (long i = 0; i < m_nCount; i ++)
	{
		fYSum += m_pData[i].fYVal;
		fTYSum += (m_pData[i].fYVal * (i+1));
		fTSum += (i+1);
		fT2Sum += ((i+1)*(i+1));
	}

	double fDiff = m_pData[1].fXVal - m_pData[0].fXVal;

	for (i = 0; i < (m_nCount + nPoints); i ++)
	{
		if (i < m_nCount)
			pData[i].fXVal = m_pData[i].fXVal;
		else
			pData[i].fXVal = pData[i-1].fXVal + fDiff;

		double fB = (m_nCount * fTYSum - fTSum * fYSum) / (m_nCount * fT2Sum - (fTSum * fTSum));
		double fA = (fYSum - fB * fTSum) / m_nCount;
		pData[i].fYVal = fA + fB * (i+1);
	}

	return pData;
}

TDataPoint* CXGraphDataSerie::GetCubicTrend(long nPoints)
{
	TDataPoint* pData;
	
	pData = new TDataPoint[m_nCount + nPoints];

	double fTY3Sum = 0.0,
		   fT6Sum = 0.0;
			
	for (long i = 0; i < m_nCount; i ++)
	{
		fTY3Sum += (m_pData[i].fYVal * pow(i+1, 3));
		fT6Sum  += (pow (i+1, 6));
	}

	double fDiff = m_pData[1].fXVal - m_pData[0].fXVal;

	for (i = 0; i < (m_nCount + nPoints); i ++)
	{
		if (i < m_nCount)
			pData[i].fXVal = m_pData[i].fXVal;
		else
			pData[i].fXVal = pData[i-1].fXVal + fDiff;
	
		pData[i].fYVal = (fTY3Sum/fT6Sum) * pow (i+1,3);
	}

	return pData;
}



TDataPoint* CXGraphDataSerie::GetPolynomialTrend(int nDegree, int nPoints)
{
    TDataPoint* pData;
	
	pData = new TDataPoint[m_nCount + nPoints];

	m_PS.m_nGlobalO = nDegree;
	
	if (!m_PS.Polyfit (m_nCount, nDegree, m_pData))
	{
		delete pData;
		return NULL;
	}

	for ( int i = 0; i < (m_nCount + nPoints); i++ )
	{
		if (i < m_nCount)
			pData[i].fXVal = m_pData[i].fXVal;
		else
			pData[i].fXVal = pData[i-1].fXVal + (pData[i-1].fXVal - pData[i-2].fXVal);

		pData[i].fYVal = m_PS.GetValue (pData[i].fXVal);
	}

    return pData;
}


TDataPoint* CXGraphDataSerie::GetSimpleMovingAverage(int span)
{
    int	   p;
	double fSum = 0.0;
    
	TDataPoint* pData;
	
	pData = new TDataPoint[m_nCount];

	pData[0].fXVal = m_pData[0].fXVal;
	pData[0].fYVal = m_pData[0].fYVal;
	
    for ( int i = 1; i < m_nCount; i++ )
    {
		p = min(i, span);

  	    if ( p <= span )
        {
            fSum = 0.0;

            for ( int y = 0; y < p; y++ )
                fSum += m_pData[i-y].fYVal;
        }
        else
            fSum = fSum - m_pData[i-p].fYVal + m_pData[i].fYVal;
		
        pData[i].fYVal = fSum / (double) p;
		pData[i].fXVal = m_pData[i].fXVal;
    }

    return pData;
}

TDataPoint* CXGraphDataSerie::GetExponentialMovingAverage(int span)
{
    double  se    = 2.0 / ((double)span + 1.0);
    double  le    = 1.0 - se;

	TDataPoint* pData;
	
	pData = new TDataPoint[m_nCount];

	pData[0].fXVal = m_pData[0].fXVal;
	pData[0].fYVal = m_pData[0].fYVal;

    for ( int i = 1; i < m_nCount; i++ )
	{
        pData[i].fYVal = m_pData[i].fYVal * se + pData[i-1].fYVal * le;
		pData[i].fXVal = m_pData[i].fXVal;
	}

    return pData;
}

TDataPoint* CXGraphDataSerie::GetLinearMovingAverage(int span)
{
    TDataPoint* pData;

	double  fSum = 0.0;
    double  fDiv = 0.0;
	int     p;
	
	pData = new TDataPoint[m_nCount];

	pData[0].fXVal = m_pData[0].fXVal;
	pData[0].fYVal = m_pData[0].fYVal;

    for ( int i = 1; i < m_nCount; i++ )
	{
        fSum = 0.0;
        p = min(i, span);

        if ( p <= span )
        {
            fDiv = 0.0;

            for ( int y = 1; y <= p; y++ )
                fDiv += (double) y;
        }

        for ( int y = 0; y < p; y++ )
            fSum += m_pData[i-y].fYVal * (p - y);

        pData[i].fYVal = fSum / fDiv;
		pData[i].fXVal = m_pData[i].fXVal;
    }

	return pData;
}

inline double W_BARTLETT(double n, double k, double d = 1.0)
{
    return (n == 0) ? 0 : (d * (1 - fabs((k - 0.5 * n) / (0.5 * n))));
}

TDataPoint* CXGraphDataSerie::GetTriangularMovingAverage(int span)
{
    TDataPoint* pData;

	double  fSum = 0.0;
    double  fDiv = 0.0;
	int     p;
	
	pData = new TDataPoint[m_nCount];

	pData[0].fXVal = m_pData[0].fXVal;
	pData[0].fYVal = m_pData[0].fYVal;

    for ( int i = 1; i < m_nCount; i++ )
	{
        fSum = 0.0;
        p    = min(i, span);

        if ( p <= span )
        {
            fDiv = 0.0;

            for ( int y = 1; y <= p; y++ )
                fDiv += W_BARTLETT(p+1, y, p);
        }

        for ( int y = 1; y <= p; y++ )
            fSum += m_pData[i-y+1].fYVal * W_BARTLETT(p+1, y, p);

        pData[i].fYVal = fSum / fDiv;
		pData[i].fXVal = m_pData[i].fXVal;
    }

    return pData;
}


TDataPoint* CXGraphDataSerie::GetSineWeightedMovingAverage(int span)
{
    TDataPoint* pData;

	double  fSum = 0.0;
    double  fDiv = 0.0;
	double  d    = 0.0;
	double  f    = _PI / (double) (span + 1);
	int     p;
	
	pData = new TDataPoint[m_nCount];

	pData[0].fXVal = m_pData[0].fXVal;
	pData[0].fYVal = m_pData[0].fYVal;

    for ( int i = 1; i < m_nCount; i++ )
	{
        fSum = 0.0;
        p    = min( i, span );

        for ( int y = 0; y < p; y++ )
            fSum += sin((double)(p - y) * f) * m_pData[i-y].fYVal;

        if ( i <= span )
        {
            d = 0.0;
            for ( int y = 1; y <= p; y++ )
                d += sin((double) y * f);
        }

        pData[i].fYVal = fSum / d;
		pData[i].fXVal = m_pData[i].fXVal;
    }

    return pData;
}

BOOL CXGraphDataSerie::HitTestLine(POINT pt0, POINT pt1, POINT ptMouse, int nWidth)
{
  VECTOR2D tt0, tt1;
  double   dist;
  int      nHalfWidth;
  //
  //Get the half width of the line to adjust for hit testing of wide lines.
  //
  nHalfWidth = (nWidth/2 < 1) ? 1 : nWidth/2;
  
  //
  //Convert the line into a vector using the two endpoints.
  //
  POINTS2VECTOR2D(pt0, pt1, tt0);
  
  //Convert the line from the left endpoint to the mouse point into a vector.
  //
  POINTS2VECTOR2D(pt0, ptMouse, tt1);
  //
  //Obtain the distance of the point from the line.
  //
  dist = vDistFromPointToLine(&pt0, &pt1, &ptMouse);
  //
  //Return TRUE if the distance of the point from the line is within the width 
  //of the line
  //

  BOOL bRet = (dist >= -nHalfWidth && dist <= nHalfWidth);
  
  return bRet;
}



void CXGraphDataSerie::Serialize( CArchive& archive )
{
	int nHelper;

	CXGraphObject::Serialize (archive);

    if( archive.IsStoring() )
    {
		archive << m_nCount;

		archive.Write (m_pData, sizeof(TDataPoint) * m_nCount);
		
		archive << m_nFirstVisible;
		archive << m_nLastVisible;
		archive << m_nXAxis;
		archive << m_nYAxis;
		archive << m_nIndex;
		archive << m_nLineSize;
		archive << m_nFillStyle;
		archive << m_nMarkerType;
		archive << m_nMarker;
		archive << m_nMarkerSize;
		archive << m_nLineStyle;
		archive << m_bShowMarker;
		archive << m_bFillBeneath;
		archive << m_bFillTransparent;
		archive << m_nFillCurve;
		archive << m_crFillColor;
		archive << m_cLabel;
		nHelper = (int) m_gtType;
		archive << nHelper;

		
    }
	else
    {
		archive >> m_nCount;

		m_bAutoDelete = true;

		m_pData = new TDataPoint[m_nCount];
		
		archive.Read (m_pData, sizeof(TDataPoint) * m_nCount);

		archive >> m_nFirstVisible;
		archive >> m_nLastVisible;
		archive >> m_nXAxis;
		archive >> m_nYAxis;
		archive >> m_nIndex;
		archive >> m_nLineSize;
		archive >> m_nFillStyle;
		archive >> m_nMarkerType;
		archive >> m_nMarker;
		archive >> m_nMarkerSize;
		archive >> m_nLineStyle;
		archive >> m_bShowMarker;
		archive >> m_bFillBeneath;
		archive >> m_bFillTransparent;
		archive >> m_nFillCurve;
		archive >> m_crFillColor;
		archive >> m_cLabel;
		archive >> nHelper;
		m_gtType = (EGraphType) nHelper;
    }
}


/////////////////////////////////////////////////////////////////

bool CPolynomialSolver::Solve(double a[], double b[], int n)
{
	for (int i = 0; i < n; i ++)
	{
		// find pivot
		double mag = 0;
		int pivot = -1;
		
		for (int j = i; j < n; j ++)
		{
			double mag2 = fabs(a[i + j * n]);
			if (mag2 > mag)
			{
				mag = mag2;
				pivot = j;
			}
		}
			
		// no pivot: error
		if (pivot == -1 || mag == 0) 
			return false;
		
		// move pivot row into position
		if (pivot != i)
		{
			double temp;
			for (int j = i; j < n; j ++)
			{
				temp = a[j + i * n];
				a[j + i * n] = a[j + pivot * n];
				a[j + pivot * n] = temp;
			}
				
			temp = b[i];
			b[i] = b[pivot];
			b[pivot] = temp;
		}
			
		// normalize pivot row
		mag = a[i + i * n];
		for (j = i; j < n; j ++) a[j + i * n] /= mag;
		b[i] /= mag;
		
		// eliminate pivot row component from other rows
		for (int i2 = 0; i2 < n; i2 ++)
		{
			if (i2 == i) continue;
			
			double mag2 = a[i + i2 * n];
			
			for (int j = i; j < n; j ++) 
				a[j + i2 * n] -= mag2 * a[j + i * n];
			
			b[i2] -= mag2 * b[i];
		}
	}

	return true;
}

bool CPolynomialSolver::Polyfit(int nRows, int nOrder, TDataPoint *pData)
{
	int rows   = nRows;
	int order  = nOrder;
	m_nGlobalO = order;

	double *base   = new double[order * rows];
	double *alpha  = new double[order * order];
	double *alpha2 = new double[order * order];
	double *beta   = new double[order];
			
	// calc base
	for (int i = 0; i < order; i ++)
	{
		for (int j = 0; j < rows; j ++)
		{
			int k = i + j * order;
			base[k] = i == 0 ? 1.0 : pData[j].fXVal * base[k - 1];
		}
	}
				
	// calc alpha2
	for (i = 0; i < order; i ++)
	{
		for (int j = 0; j <= i; j ++)
		{
			double sum = 0.0;
			for (int k = 0; k < rows; k ++)
			{
				int k2 = i + k * order;
				int k3 = j + k * order;
				sum += base[k2] * base[k3];
			}

			int k2 = i + j * order;

			alpha2[k2] = sum;
			
			if (i != j)
			{
				k2 = j + i * order;
				alpha2[k2] = sum;
			}
		}
	}
	
	// calc beta
	for (int j = 0; j < order; j ++)
	{
		double sum = 0;
		for (int k = 0; k < rows; k ++)
		{
			int k3 = j + k * order;
			sum += pData[k].fYVal  * base[k3];
		}

		beta[j] = sum;
	}
		
	// get alpha
	for (j = 0; j < order * order; j ++) 
		alpha[j] = alpha2[j];
	
	// solve for params
	bool bRes = Solve(alpha, beta, order);
				
	for (j = 0; j < order; j ++)
		m_fC[j] = beta[j];
	
	delete base;
	delete beta;
	delete alpha;
	delete alpha2;

	return bRes;
}


double CPolynomialSolver::GetValue(double fX)
{
	double fRes = 0.0;

	for (int i = 0; i < m_nGlobalO; i++)
		fRes += (m_fC[i] * pow(fX, i));

	return fRes;
}


By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions