/* CurveView.m -- Storage Scope object which can display multiple traces in
 *	first-in-first-out fashion.  This is sort of an digital version of a
 *	long-persistence oscilloscope where you can see the most recent N traces.
 *	Unlike analog scopes, the persistence is not defined by decay time, but
 *	CurveView will simply retain the last N waveforms.  As a new trace is
 *	added, the oldest trace will drop out of the display.  The number of
 *	retained traces N (persistence?) may be specified via method:
 *	- allocTraceCache:(int)ncrvs :(int)npts;.  As with everything, drawing
 *	will slow down as more traces are retained.
 * V0.90 92-1-21 Izumi Ohzawa -- The initial version with constant X spacing.
 * V0.91 92-2-20 Izumi Ohzawa
 *	Added NXImage compositing to provide fixed background.
 *	Added ability to specify trace by (x[], y[]) array.
 * V0.92 92-03-16 Izumi Ohzawa
 *	Added ability to handle Color for traces, data points, and background.
 * V0.94 92-03-16 Izumi Ohzawa
 *	Selection of data points and delegate notification of selection rect.
 *	Comments insertion of data source info that survives Tailor.app edits.

 Usage:
    [curveView freeTraceCache];
    [curveView allocTraceCache: 1 : Ndisp];		// show only Gabor during fitting
    [curveView setScaleX: -(xgsize/2.0+0.2): xgsize/2.0+0.2];	// Xmin Xmax plotrange
    [curveView setScaleY: 1.25*tminmin : 1.25*tmaxmax];	// Fixed Y scale
    [curveView setXvalues: xdisp];			// load X values for continuous curve(s)
    [curveView setDataPoints: xdata :ydata :Npt];	// set fixed data points in BG
    [curveView cacheDataPointsAndBackGround];		// put this stuff in NXImage cache
  int i;
    for(i=0; i<Ndisp; i++)
	ydisp[i] = Gabor(paraset, xdisp[i]);
    [curveView setCurveColor: NX_COLORRED];		// red
    [curveView setNewTrace: ydisp];
    [curveView drawIt:self];

*/

#import "CurveView.h"
#import "matalloc.h" 
#import <stdlib.h>		/* malloc */
#import <time.h>

/* Version of ContourView object.  This is entered into PS code generated after NeXT
   print package prolog.
 */
#define CV_VERSION "% CurveView.m [V0.94 95-01-29] by Izumi Ohzawa, izumi@pinoko.berkeley.edu."

@implementation CurveView


- (BOOL)acceptsFirstMouse { return YES; }
- (BOOL)acceptsFirstResponder { return YES; }

// ==== MOUSE EVENT, CURSOR, MARKER HANDLING METHODS =========================================

/*  This method handles a mouse down.  */
- mouseDown:(NXEvent *)event
{
    if(!CacheOK) return self;
    [window makeFirstResponder:self];	  /* give me the first responder status */
    [self dragMarker:event];		  /* modal drag loop for drag rectangle selection */
    [self cacheDataPointsAndBackGround];  /* redraw data points with new selection */
    [self display];			  /* delete the last of instance drawing */
    return self;
}


- (int)dragMarker:(NXEvent *)event
{
int      old_mask, drag_happened=0;
int      i, select_count=0;
float	 sx=0.0, sy, ex=0.0, ey, tx, ty;

    pstart = event->location;		// keep the starting point
    [self convertPoint:&pstart fromView:nil];

    old_mask = [window addToEventMask:NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK];
    event = [NXApp getNextEvent:NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK];

    if([self lockFocus]) {
	PSsetinstance(YES);			// Instance drawing ON
	PSsetgray(0.333333);
	PSsetlinewidth(0.0);
	while (event->type != NX_MOUSEUP) {
	    if (event->type == NX_MOUSEDRAGGED) {
		p = event->location;		
		[self convertPoint:&p fromView:nil];
    	        PSnewinstance();		// erase last rect
		if(!drag_happened)
		    drag_happened = 1;		// indicate that at least 1 drag event came
		PSrectstroke(pstart.x, pstart.y, p.x - pstart.x, p.y - pstart.y);
	    }
	    // Get more events
	    event = [NXApp getNextEvent:NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK
				    waitFor:1000  threshold:NX_BASETHRESHOLD];
	}  /* end of while( <not mouse-up>) loop */
	PSsetinstance(NO);			// Instance drawing OFF
	[self unlockFocus];
    }  /* end if([self lockFocus]) .. */

    // ### MouseUp detected, do necessary things and exit drag loop  ###
    [window setEventMask:old_mask];	// restore original event mask

    sx = (pstart.x - bounds.origin.x)/RX2PX + Xmin;
    sy = (pstart.y - bounds.origin.y)/RY2PY + Ymin;
    ex = (p.x - bounds.origin.x)/RX2PX + Xmin;
    ey = (p.y - bounds.origin.y)/RY2PY + Ymin;
    if(sx > ex) {	// swap start and end X
	tx = ex;
	ex = sx;
	sx = tx;
    }
    if(sy > ey) {	// swap start and end Y
	ty = ey;
	ey = sy;
	sy = ty;
    }

    // indicate points inside rectangle as selected.
    if(drag_happened) {
	for(i=0; i<ndata; i++) {
	    if(xdata[i] > sx && xdata[i] < ex && ydata[i] > sy && ydata[i] < ey) {
		selected[i] = YES;
		select_count++;
	    }
	    else
		selected[i] = NO;
	}
    }
    // If no point is selected, select all
    if(!drag_happened || select_count == 0)
	    for(i=0; i<ndata; i++) selected[i] = YES;

    // Report selection rectangle to delegate in real X, Y unit.
    // Do it on mouse up event.
    if(delegate && [delegate respondsTo:@selector(selectionRectDidMove::::::)]) {
	// tempx, tempy should already be correct and converted
    	[delegate selectionRectDidMove:tag :sx :sy :ex :ey :drag_happened];	// mouse up
    }

    return drag_happened;
}

// Set a comment string to be inserted into PostScript stream when printed or
// copied onto pasteboard as EPS.  The comment string must already be formatted
// as valid PostScript comments: Each new line must start with '%'.
//
// The comment does not appear at the top of EPS file.  It will be inserted
// immmediately AFTER the NeXT printPackage.ps, but before all the drawing
// commands for the contour appear.
// This is used to identify in detail the data used to create the contour
// plot, so we can look inside the EPS file later and relate to the raw data.
//
- setComment:(char *)cstr :(int)len
{
char *ptr, *ptr2, ch;
int  lfcount;
    if(comment) {
	free((void *)comment);
	comment = NULL;
    }
    comment = (char *)malloc((size_t)(len*sizeof(char)+2));
    strncpy(comment, cstr, len);
    comment[len] = '\0';	/* string terminator */

    // We need a duplicate comment with '\n' translated to '\012' char sequence
    if(comment012) {
	free((void *)comment012);
	comment012 = NULL;
    }
    lfcount = 0;
    ptr = comment;
    while( (ch = *ptr++) != '\0')
	if(ch == '\n') lfcount++;
    comment012 = (char *)malloc((size_t)(len + lfcount*4 + 2));
    ptr = comment;
    ptr2 = comment012;
    while( (ch = *ptr++) != '\0') {
	if(ch == '\n') {
	    *ptr2++ = '\\';
	    *ptr2++ = '0'; *ptr2++ = '1'; *ptr2++ = '2'; 
	}
	else
	    *ptr2++ = ch;
    }
    *ptr2 = '\0';
    return self;
}



// This generates comments block that survives editing by Tailor.app
// for keeping track of the data source and parameters for the plot.
//
- beginTailorGroupWithComments
{
DPSContext curContext = DPSGetCurrentContext();
long timenow;
char timestr[32];
    // Do it only if printing or copying to pasteboard.
    if(NXDrawingStatus != NX_DRAWING) {
	DPSPrintf(curContext, "\n%% Comments below should survive Tailor editing.\n\n");
        DPSPrintf(curContext, "(%s\\012", CV_VERSION);
	if(comment012)
	    DPSPrintf(curContext, "%s\\012", comment012);
        DPSPrintf(curContext, "%% X (H) range: %g - %g\\012",
		Xmin, (pXmax - pXmin)/RX2PX + Xmin);
        DPSPrintf(curContext, "%% Y (V) range: %g - %g\\012",
		Ymin, (pYmax - pYmin)/RY2PY + Ymin);

	DPSPrintf(curContext, ") TailorGroupBegin\n\n");

	if(comment) {
	  DPSPrintf(curContext, "%% For Acrobat Distiller to generate a Note with comments.\n\n");

	  DPSPrintf(curContext, "[ /Rect [0 0 300 150]\n");
	  DPSPrintf(curContext, "/Contents (");
	  DPSPrintf(curContext, "%s)\n", comment);
	    time(&timenow); strcpy(timestr, ctime(&timenow)); timestr[24] = '\0';
	  DPSPrintf(curContext, "/Title (CurveView plot: %s)\n", timestr);
	  DPSPrintf(curContext, "/Open false\n");   /* initialize for iconized appearance */
	  DPSPrintf(curContext, "/ANN pdfmark\n\n");
	}
    }
    return self;
}

- endTailorGroup
{
DPSContext curContext = DPSGetCurrentContext();
    if(NXDrawingStatus != NX_DRAWING) {
        DPSPrintf(curContext, "\n%% End of ContourView drawing.\n");
	DPSPrintf(curContext, "TailorGroupEnd\n\n");
    }
    return self;
}

// This puts the data trace info right after the %% comments.
// %%EndComments etc moved to after this, which is not good, but OK.
//
- endHeaderComments
{
DPSContext curContext = DPSGetCurrentContext();
    if(NXDrawingStatus != NX_DRAWING) {
        DPSPrintf(curContext, "\n\n%s\n", CV_VERSION);
	if(comment)
	    DPSPrintf(curContext, "%s\n", comment);
        DPSPrintf(curContext, "%% X (H) range: %g - %g\n",
		Xmin, (pXmax - pXmin)/RX2PX + Xmin);
        DPSPrintf(curContext, "%% Y (V) range: %g - %g\n",
		Ymin, (pYmax - pYmin)/RY2PY + Ymin);
	DPSPrintf(curContext, "%%\n\n");
    }
    return [super endHeaderComments];
}

// This is inserted for embedding comments that can persist across
// editing by Tailor.app (FirstClass).  Tailor strips regular comments
// from its output.
//
- endPrologue
{
DPSContext curContext = DPSGetCurrentContext();
    if(NXDrawingStatus != NX_DRAWING) {
	DPSPrintf(curContext, "\n\n%%%%BeginResource: procset (Tailor group constructs)\n");
	DPSPrintf(curContext, "/TailorGroupBegin {pop} def /TailorGroupEnd {} def\n");
	DPSPrintf(curContext, "/pdfmark where\n");
	DPSPrintf(curContext, "{pop} {userdict /pdfmark /cleartomark load put} ifelse\n");
	DPSPrintf(curContext, "%%%%EndResource\n\n");
    }
    return [super endPrologue];
}




- initFrame:(NXRect *)nf
{
    self = [super initFrame:nf];
    Xscaled = 0;
    ClearFlag =1;
    CacheOK = 0;
    ncin = 0;
    icurve =0;
    bbox[0] = pXmin = bounds.origin.x;
    bbox[1] = pYmin = bounds.origin.y;
    bbox[2] = bounds.size.width;
    bbox[3] = bounds.size.height;
    pXmax = bounds.size.width + bounds.origin.x;
    pYmax = bounds.size.height + bounds.origin.y;
    ops = NULL;
    coords = NULL;
    bgImage = NULL;
    xdata = NULL;
    ydata = NULL;
    selected = NULL;
    ndata = 0;
    frameON = NO;
    curveColor = NULL;
    comment = NULL;
    comment012 = NULL;
    // color default settings
    tempColor = NX_COLORBLACK;
    markerColor= NXConvertRGBToColor(1.0, 0.4, 0.4);
    bgColor= NX_COLORWHITE;
    markerSize = 3.0;
    return self;
}

- freeTraceCache
{
    if(ops) free(ops);
    if(coords) free_fmatrix(coords, 0, ncurves-1, 0, 2*npoints-1);
    if(bgImage) [bgImage free];
    if(curveColor) free(curveColor);
    ops = NULL;
    coords = NULL;
    bgImage = NULL;
    curveColor = NULL;
    CacheOK = 0;
    return self;
}

- free
{
    [self freeTraceCache];
    if(xdata) free(xdata);
    if(ydata) free(ydata);
    if(selected) free(selected);
    if(comment) free(comment);
    if(comment012) free(comment012);
    [super free];
    return self;

}

// Allocate stuff for DPSDoUserPath() and NXImage for background.
// ncrvs = # of traces in storage memory (first-in first-out)
// npts = # of points to represent each curve.
- allocTraceCache:(int)ncrvs :(int)npts
{
int ic, i;
float xi;
    if(CacheOK)
        [self freeTraceCache];
    npoints = npts;
    ncurves = ncrvs;

    // color arrays for traces
    curveColor = (NXColor *)malloc(ncurves * sizeof(NXColor));
    // Allocate arrays for DPSDoUserPath()
    ops = (char *)malloc((size_t)npoints*sizeof(char));  /* operator array */
    coords = fmatrix(0, ncurves-1, 0, 2*npoints-1);	/* 2-d arracy for UserPath */
    // Allocate NXImage bitmap for storing background stuff, e.g. data points
    bgImage = [[NXImage alloc] initSize:&bounds.size];
    [bgImage useCacheWithDepth:NX_DefaultDepth];

    // Initialize x-coords for simple traces with no X values information.
    xi = pXmax/(npoints-1);
    for(ic=0; ic<ncurves; ic++)
    {
        for(i=0; i<npoints; i++)
        {
	    coords[ic][2*i] = xi*i;
	    if(i)
	        ops[i] = dps_lineto;
	    else
	        ops[i] = dps_moveto;
        }
    }
    Xscaled = 0;		// set it to use X values as specified above
    return self;
}

// Coordinate conversion methods from units of real data to point unit
// used in this view.  Separate functions for X and Y.

- (float)RXtoPX:(float)rx
{
    return(RX2PX*(rx-Xmin) + pXmin);
}

- (float)RYtoPY:(float)ry
{
    return(RY2PY*(ry-Ymin) + pYmin);
}


// Set X values for all traces.  All traces use same X values.
// Use setNewTraceXY:: method if X values change from one trace to another.
- setXvalues: (float *)xa
{
int i,ic;
float xtmp;

    for(i=0; i<npoints; i++)
    {
    	xtmp = [self RXtoPX:xa[i]];		// do scaling
	if(xtmp > pXmax) xtmp = pXmax;
	if(xtmp < pXmin) xtmp = pXmin;
        for(ic=0; ic<ncurves; ic++)
	    coords[ic][2*i] = xtmp;
    }
    return self;
}

// Use this method when X values need not be changed AND all traces
// share the same X values.
- setNewTrace: (float *)ya
{
int i;
float ytmp;
    for(i=0; i<npoints; i++)
    {
        ytmp = [self RYtoPY:ya[i]];		// convert from Real Y to points
        if(ytmp > pYmax) ytmp = pYmax;	// enforce bounds
        if(ytmp < pYmin) ytmp = pYmin;
	coords[icurve][2*i+1] = ytmp;	// DPSDoUserPath can't take out-of-range values!
    }

    curveColor[icurve] = tempColor;
    icurve++;
    icurve %= ncurves;
    ncin++;
    if(ncin > ncurves) ncin = ncurves;
    return self;
}

// Use this method when both X and Y values are given for each trace.
// xa[] -- X coordinate array of npoints.
// ya[] -- Y coordinate array of npoints.
// Currently all traces must have the same number of points.
// Ideally, this should be specifiable for each trace provided that the number of
// points for a trace does not exceed allocated array size.
- setNewTraceXY:(float *)xa :(float *)ya
{
int i;
float ytmp=0.0,xtmp=0.0;
    for(i=0; i<npoints; i++)
    {
        ytmp = [self RYtoPY:ya[i]];		// convert from Real Y to points
        if(ytmp > pYmax) ytmp = pYmax;	// enforce bounds
        if(ytmp < pYmin) ytmp = pYmin;
    	xtmp = [self RXtoPX:xa[i]];		// do scaling for X
	if(xtmp > pXmax) xtmp = pXmax;
	if(xtmp < pXmin) xtmp = pXmin;
	coords[icurve][2*i] = xtmp;
	coords[icurve][2*i+1] = ytmp;	// DPSDoUserPath can't take out-of-range values!
    }
    curveColor[icurve] = tempColor;
    icurve++;
    icurve %= ncurves;
    ncin++;
    if(ncin > ncurves) ncin = ncurves;
    return self;

}

- setDataPoints:(float *)xd :(float *)yd :(int)nnd
{
int i;
    if(nnd == 0) {
	ndata = 0;
	return self;
    }
    // do reallocation only if # of data points has changed.
    if(ndata != nnd) {
    	if(xdata) free(xdata);
	if(ydata) free(ydata);
	if(selected) free(selected);
    	xdata = (float *)malloc((size_t)nnd*sizeof(float));
    	ydata = (float *)malloc((size_t)nnd*sizeof(float));
    	selected = (BOOL *)malloc((size_t)nnd*sizeof(BOOL));
    }
    ndata = nnd;
    for(i=0; i<ndata; i++) {
	xdata[i] = xd[i];
	ydata[i] = yd[i];
	selected[i] = YES;		// indicate all points selected
    }
    return self;
}


// This method will plot 'nnd' data points (xd[i], yd[i]) into the NXImage cache,
// and XY axes as background.  This NXImage bitmap cache will be composited to the
// view before traces are drawn.  Assumption here is that data points do not change
// a lot, but traces are drawn many more times.  This assumption may not be correct,
// in which case other optimizations must be made.
- cacheDataPointsAndBackGround
{
    // Draw fixed image into NXImage cache..    
    if([bgImage lockFocus])
    {
	[self drawDataPointsAndBackGround];
	[bgImage unlockFocus];
    } /* end of if([bgImage lockFocus])... */
    CacheOK = 1;
    return self;
}


// This method will plot 'ndata' data points (xdata[i], ydata[i]) into a cache or a view.
- drawDataPointsAndBackGround
{
int i;
	NXSetColor(bgColor);
    	NXRectFill(&bounds);	/* clear */

	// X-Y axes
	PSsetgray(0.666667);	/* light gray */
	PSmoveto(pXmin, [self RYtoPY:0.0]);
	PSlineto(pXmax, [self RYtoPY:0.0]);	// horizontal line at Y=0
	PSmoveto([self RXtoPX:0.0], pYmin);
	PSlineto([self RXtoPX:0.0], pYmax);	// vertical line at X=0
	PSstroke();

	// Frame rectangle
	if(NXDrawingStatus == NX_DRAWING || frameON) {
	    PSsetgray(0.0);		/* black */
	    PSrectstroke(pXmin,pYmin+0.1,pXmax-0.25,pYmax-0.25);	/* frame rectangle */
	}
	if(ndata == 0) return self;	// don't go further if no data points

	// Data points as colored circles
	NXSetColor(markerColor);
	// Plot data points from storage (instance vars).
    	for(i=0; i<ndata; i++) {
	    PSarc([self RXtoPX:xdata[i]], [self RYtoPY:ydata[i]], markerSize, 0.0, 360.0);
	    PSclosepath();
	    if(selected[i])
	        PSfill();		// filled circle for selected points
	    else
		PSstroke();		// open circle for unselected points
	}
	return self;
}

- autoScale
{
   return self;
}

- setScaleY:(float)ymin : (float)ymax
{
   Ymin = ymin;
   RY2PY = (pYmax - pYmin)/(ymax - ymin);	// scaling factor for RealY -> PointY
   return self;
}
- setScaleX:(float)xmin :(float)xmax
{
   [self setXscalingEnable: 1];			// Enable X scaling
   Xmin = xmin;
   RX2PX = (pXmax - pXmin)/(xmax - xmin);	// scaling factor for RealX -> PointX
   return self;
}

// Set X scaling flag -- Y scaling is always enabled.
- setXscalingEnable:(int)xse
{
int ic, i;
float xi;
    Xscaled = xse;
    if(!Xscaled)
    {
      xi = pXmax/(npoints-1);
      for(ic=0; ic<ncurves; ic++)
        for(i=0; i<npoints; i++)
	    coords[ic][2*i] = xi*i;
    }
    return self;
}

- setFrameEnabled:(BOOL)yn
{
    frameON = yn;
    return self;
}



- setCurveColor:(NXColor)color
{
    tempColor = color;
    return self;
}

- setMarkerColor:(NXColor)color
{
    markerColor = color;
    return self;
}

- setMarkerSize: (float)size
{
	markerSize = size;
	return self;
}

- setBgColor:(NXColor)color
{
    bgColor = color;
    return self;
}


- drawIt:sender
{
    ClearFlag =0;
    [self display];
    return self;
}

- clear:sender
{
    ClearFlag =1;
    [self display];
    return self;
}



- copy:sender
{
    [self copyPScode:sender];
    return self;
}


// Copies the current view into the Pasteboard as PostScript.
// Following code taken from the Graph example in NextDeveloper/Examples.
//
- copyPScode:sender
{
    NXStream *psStream;
    id        pb;
    char     *data;
    int       dataLen, maxDataLen;

  /* Open a stream on memory where we will collect the PostScript */
    psStream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
    if (!psStream)
      return self;

  /* Tell the Pasteboard we're going to copy PostScript */
    pb = [Pasteboard new];
    [pb declareTypes:&NXPostScriptPboardType num:1 owner:self];

  /* writes the PostScript for the whole plot as EPS into the stream */
    [self copyPSCodeInside:&bounds to:psStream];

  /* get the buffered up PostScript out of the stream */
    NXGetMemoryBuffer(psStream, &data, &dataLen, &maxDataLen);

  /* put the buffer in the Pasteboard, free the stream (and the buffer) */
    [pb writeType:NXPostScriptPboardType data:data length:dataLen];
    NXCloseMemory(psStream, NX_FREEBUFFER);
    return self;
}



// This will override the default -drawSelf::, which does no real drawing.
// Should never be called directly.  It will be called by [self display];

- drawSelf:(const NXRect *)r:(int)count
{
int ic;
    
    if(ClearFlag || !CacheOK) {
    	PSsetgray(1.0);
    	NXRectFill(&bounds);	/* clear */
    }
    else {
	if(NXDrawingStatus == NX_DRAWING) {
    	    [bgImage composite: NX_SOVER toPoint: &bounds.origin ];
    	    PSsetlinewidth(0.0);
	}
	else {
	    [self beginTailorGroupWithComments];
	    [self drawDataPointsAndBackGround];		// for printing/copying
    	    PSsetlinewidth(1.0);
	}

	// Now draw dynamic traces..
    	for(ic=0; ic<ncin; ic++) {
	    NXSetColor(curveColor[ic]);
            DPSDoUserPath(coords[ic], npoints*2, dps_float, ops, npoints, bbox, dps_ustroke);
	}

	if(NXDrawingStatus != NX_DRAWING)
	    [self endTailorGroup];
    }
    return self;
}

- setDelegate:anObject
{
    delegate = anObject;
    return self;
}

- delegate
{
    return delegate;
}

- setTag:(int)t
{
    tag = t;
    return self;
}
- (int)tag
{
    return tag;
}


@end
