package ua.stu.view.scpview; import java.math.BigDecimal; import java.math.RoundingMode; import ua.stu.scplib.attribute.GraphicAttribute; import ua.stu.scplib.attribute.GraphicAttributeBase; import ua.stu.scplib.data.DataHandler; import ua.stu.scplib.tools.Scale; import and.awt.BasicStroke; import and.awt.Color; import and.awt.Stroke; import and.awt.geom.GeneralPath; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.FloatMath; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.widget.Scroller; import android.widget.TextView; import net.pbdavey.awt.AwtView; import net.pbdavey.awt.Font; import net.pbdavey.awt.Graphics2D; import net.pbdavey.awt.RenderingHints; @SuppressLint("FloatMath") public class GraphicView extends AwtView { // object which holds all required data for drawing private DataHandler h = null; //class of channels private DrawChanels drawChanels=null; // scale function private Scale scale = new Scale(); // window size param. private int W = 920; private int H = 600; // scroll window param. private int SW = 920; private int SH = 600; // display metrics private int displayHeight; private int displayWidht; private int displayDensity; // scale parameters private String scaleHeightValueText = ""; private String scaleWidthValueText = ""; // inch constant private float duim = (float) 25.4; // screen size in dpi private int sizeScreen = 126; //set of graphic attributes private GraphicAttributeBase g; //the number of tiles to display per column private int nTilesPerColumn = 12; //the number of tiles to display per row (if 1, then nTilesPerColumn should == numberOfChannels) private int nTilesPerRow = 1; //how may pixels to use to represent one millivolt private float yPixelsInMillivolts; //how may pixels to use to represent one millisecond private float xPixelsInMilliseconds; //how much of the sample data to skip, specified in milliseconds from the start of the samples private float timeOffsetInMilliSeconds; // color map Color curveColor = Color.blue; // basic font Font font = null; // array for channel offsets private float offsets[] = new float[12]; // status bar variables private double speed = 0; private double gain = 0; private int time = 0; // X scroll offset private float xOffset = 0; // Y scroll offset private float yOffset = 0; // touch mode [DEFAULT] private int touchMode = GestureListener.MODE_BASIC; // flag - fill linear rectangle private boolean fillRect = false; // status bar private TextView tvStatus = null; // invert flag private boolean invert = false; // touch gestures private GestureDetector gestureDetector; // scroll private Scroller scroller; public void initscale(GestureListener mg){ gestureDetector = new GestureDetector(getContext(),mg); scroller = new Scroller(getContext()); } public GraphicView(Context context) { super(context); // init scrollbars setHorizontalScrollBarEnabled(true); setVerticalScrollBarEnabled(true); TypedArray a = context.obtainStyledAttributes(R.styleable.View); initializeScrollbars(a); a.recycle(); } public GraphicView(Context context, AttributeSet attribSet) { super(context, attribSet); // init scrollbars setHorizontalScrollBarEnabled(true); setVerticalScrollBarEnabled(true); TypedArray a = context.obtainStyledAttributes(R.styleable.View); initializeScrollbars(a); a.recycle(); } // optional speed values private static double[] SPEEDS = {12.5,25,50,100}; // optional volt values private static double[] VOLTS = {2.5,5,10,20}; private int indexX; private int indexY; // gesture types private static final int NONE = -1; private static final int DRAG = 1; private static final int ZOOM = 2; private static final int EndZOOM = 3; private static final int UP = 5; private static final int DOWN = 6; private static final int CLICK = 10; private float oldDist; // gesture mode private int mode; @Override public boolean dispatchTouchEvent (MotionEvent event) { int actionMask = event.getActionMasked(); // check touchMode switch (this.touchMode) { case GestureListener.MODE_BASIC: switch (actionMask) { case MotionEvent.ACTION_POINTER_DOWN: oldDist = spacing(event); if (oldDist > 10f) { mode = ZOOM; } break; case MotionEvent.ACTION_DOWN: mode = DRAG; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: mode = NONE; break; case MotionEvent.ACTION_MOVE: if (mode == DRAG) { if (!scroller.isFinished()) scroller.abortAnimation(); } else if (mode == ZOOM) { float newDist = spacing(event); if (newDist > 10f) { float scale = newDist / oldDist; if(this.ZoomIt(scale)){ invalidate(); mode=EndZOOM; } } break; } } if( (mode ==DRAG)||(mode==NONE) ) gestureDetector.onTouchEvent(event); break; case GestureListener.MODE_LINEAR: switch(actionMask) { // click case MotionEvent.ACTION_DOWN: mode = CLICK; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: if (mode == CLICK) { // fill/clear rectangle if ((scale.insideRect(event.getX(), event.getY()))) { if (fillRect == true) fillRect = false; else fillRect = true; } invalidate(); } break; case MotionEvent.ACTION_MOVE: mode = NONE; boolean move = scale.move(event.getX(), event.getY()); if (move) { // redraw while moving invalidate(); // update tv status setTime(0); } break; } break; } return true; } private boolean ZoomIt(float Zoom){ if(Zoom > 1.2){ double y = nextY(DOWN); double x = nextX(UP); reDrawY(y); reDrawX(x); return true; } else if(Zoom < 0.8){ double x = nextX(DOWN); double y = nextY(UP); reDrawX(x); reDrawY(y); return true; } return false; } private float spacing(MotionEvent event) { float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return FloatMath.sqrt(x * x + y * y); } private double nextY ( int mode ) { double out = NONE; indexY = getIndexY(gain); if ( mode == UP ) { for (int i = 0; i < VOLTS.length; i++) { if ( VOLTS[i] > gain ) { out = VOLTS[i]; break; } } } else if ( mode == DOWN ) { if ( (indexY - 1) != NONE ) out = VOLTS[indexY - 1]; else out = VOLTS[0]; } return out; } private double nextX ( int mode ) { double out = NONE; indexX = getIndexX(speed); if ( mode == UP ) { for (int i = 0; i < VOLTS.length; i++) { if ( SPEEDS[i] > speed ) { out = SPEEDS[i]; break; } } } else if ( mode == DOWN ) { if ( (indexX - 1) != NONE ) out = SPEEDS[indexX - 1]; else out = SPEEDS[0]; } return out; } private void reDrawY( double y ) { if ( y != NONE ) this.setYScale((float)( y )); } private void reDrawX( double x ) { if ( x != NONE ) this.setXScale((float)( x )); } private int getIndexX( double value ) { int index = NONE; for (int i = 0; i < SPEEDS.length; i++) if ( SPEEDS[i] == value ) index = i; return index; } private int getIndexY( double value ) { int index = NONE; for (int i = 0; i < VOLTS.length; i++) if ( VOLTS[i] == value ) index = i; return index; } /* * Initializing */ @Override public void init() { if (h!=null) { g = h.getGraphic(); if (drawChanels != null) drawChanels.setGraphicAttributeBase(g); } } /** * Drawing */ @Override public void paint(Graphics2D g2) { // check DataHandler if (h == null) return; init(); // draw ECG drawECG(g2); // draw Scale if ((this.touchMode == GestureListener.MODE_LINEAR) && (scale.isFull())) { // save previous graphic options Stroke defaultStroke = g2.getStroke(); Font defaultFont = g2.getFont(); // set bolder line g2.setStroke(new BasicStroke((float) 3)); // set color g2.setColor(Color.green); // make line path GeneralPath thePath = new GeneralPath(); thePath.reset(); // make a rectangle thePath.moveTo(scale.getX1() + xOffset,scale.getY1() + yOffset); thePath.lineTo(scale.getX2() + xOffset,scale.getY1() + yOffset); thePath.lineTo(scale.getX2() + xOffset,scale.getY2() + yOffset); thePath.lineTo(scale.getX1() + xOffset,scale.getY2() + yOffset); thePath.lineTo(scale.getX1() + xOffset,scale.getY1() + yOffset); // draw rectangle g2.draw(thePath); // update scale values updateScaleParameters(); // restore previous options g2.setStroke(defaultStroke); g2.setFont(defaultFont); // set black color for line g2.setColor(Color.black); // draw text for rectangles g2.drawString(scaleHeightValueText, scale.getMaxX() + 5 + xOffset, scale.getMidddleHeight() + yOffset); g2.drawString(scaleWidthValueText, scale.getMiddleWight() - 20 + xOffset, scale.getMaxY() + 15 + yOffset); // restore default color g2.setColor(curveColor); // ------------------ g2.setStroke(defaultStroke); g2.setFont(defaultFont); // ------------------ // fill rectangle with lines if (fillRect == true) { drawRect(g2, scale.getRectTopX() + xOffset, scale.getRectTopY() + yOffset, scale.getRectW(), scale.getRectH()); } } return; } private void updateScaleParameters() { // get width and height, convert to millimeters double width = scale.getRectW()/(float)(displayDensity/duim); double height = scale.getRectH()/(float)(displayDensity/duim); // round to 0.01 width = new BigDecimal(width).setScale(2, RoundingMode.UP).doubleValue(); height = new BigDecimal(height).setScale(2, RoundingMode.UP).doubleValue(); // make string values scaleWidthValueText = String.valueOf(width) + " mm."; scaleHeightValueText = String.valueOf(height) + " mm."; } /* * Draw a rectangle of lines */ private void drawRect(Graphics2D g, float x, float y,float w, float h) { // set green color for rectangle g.setColor(Color.green); // get count of vertical lines int frequency = 10; int cnt = (int)w/frequency; GeneralPath p = new GeneralPath(); // make vertical lines for (int i=1;i<cnt + 1;i++) { p.moveTo(x + i*frequency, y); p.lineTo(x + i*frequency, y + h); } int cnt2 = (int)h/frequency; // make horizontal lines for (int i=1;i<cnt2 + 1;i++) { p.moveTo(x, y + i*frequency); p.lineTo(x + w, y + i*frequency); } // draw rectangle g.draw(p); // restore default color g.setColor(curveColor); } /** * Basic ECG draw * */ private void drawECG(Graphics2D g2) { // set font font = new Font("Ubuntu",0,(14)); // get curve parameters float widthOfTileInPixels = getW()/nTilesPerRow; float widthOfTileInMilliSeconds = widthOfTileInPixels/xPixelsInMilliseconds; // ugly without g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); // set default color g2.setColor(curveColor); g2.setStroke(new BasicStroke((float) 0.7)); float widthOfSampleInPixels=g.getSamplingIntervalInMilliSeconds()*xPixelsInMilliseconds; int timeOffsetInSamples = (int)(timeOffsetInMilliSeconds/g.getSamplingIntervalInMilliSeconds()); int widthOfTileInSamples = (int)(widthOfTileInMilliSeconds/g.getSamplingIntervalInMilliSeconds()); int usableSamples = g.getNumberOfSamplesPerChannel()-timeOffsetInSamples; if (usableSamples <= 0) return; else if (usableSamples > widthOfTileInSamples) usableSamples = widthOfTileInSamples - 1; //bound between channels float drawingOffsetY = 0; //offset for channel middle line float currentYChannelOffset = 0; float maxY,minY; int channel = 0; GeneralPath thePath = new GeneralPath(); //calculating the ideal title offset float tile = yPixelsInMillivolts*duim/sizeScreen; float yIdealTiles = 4*tile - (int)(tile/2); // offset from the top of the screen float beforeY = 10; // float between names of channels float borderBetweenChannelNames = 15; // main cycle for (int row=0;row<nTilesPerColumn && channel < g.getNumberOfChannels();++row) { //getting the maximum and minimum values of Y offset maxY = -9999; minY = 9999; int k = timeOffsetInSamples; float curY = 0; short[] currenSamplesForThisChannel = g.getSamples()[g.getDisplaySequence()[channel]]; float currentRescaleY = g.getAmplitudeScalingFactorInMilliVolts()[g.getDisplaySequence()[channel]]*yPixelsInMillivolts; // getting the width of channel for (int j=1;j<usableSamples;++j) { if (invert) curY = currenSamplesForThisChannel[k]*currentRescaleY; else curY = -currenSamplesForThisChannel[k]*currentRescaleY; if (curY < minY) minY = curY; if (curY > maxY) maxY = curY; k++; } // Calculating offset Y for current channel currentYChannelOffset = drawingOffsetY - minY; for (int col=0;col < nTilesPerRow && channel < g.getNumberOfChannels();++col) { short[] samplesForThisChannel = g.getSamples()[g.getDisplaySequence()[channel]]; int i = timeOffsetInSamples; // YScale attribute float rescaleY = g.getAmplitudeScalingFactorInMilliVolts()[g.getDisplaySequence()[channel]]*yPixelsInMillivolts; float fromXValue = 0; float fromYValue; if (invert) fromYValue = currentYChannelOffset + samplesForThisChannel[i]*rescaleY; else fromYValue = currentYChannelOffset - samplesForThisChannel[i]*rescaleY; // value for auto-trimming float delta = 0; if (fromYValue > beforeY + (maxY - minY)) delta = beforeY + (maxY - minY) - fromYValue +20; if (fromYValue < beforeY + yIdealTiles + borderBetweenChannelNames) delta = beforeY + yIdealTiles - fromYValue + borderBetweenChannelNames; beforeY = fromYValue + delta; if (col == 0) offsets[row] = fromYValue + delta; thePath.reset(); thePath.moveTo(fromXValue,fromYValue + delta); ++i; for (int j=1;j<usableSamples;++j) { float toXValue = fromXValue + widthOfSampleInPixels; float toYValue; if (invert) toYValue = currentYChannelOffset + samplesForThisChannel[i]*rescaleY + delta; else toYValue = currentYChannelOffset - samplesForThisChannel[i]*rescaleY + delta; i++; if ((int)fromXValue != (int)toXValue || (int)fromYValue != (int)toYValue) thePath.lineTo(toXValue,toYValue); fromXValue = toXValue; fromYValue = toYValue; } g2.draw(thePath); ++channel; } // calculating bound between channels drawingOffsetY = (currentYChannelOffset + maxY); } currentYChannelOffset = 0; // update offsets for channels, and redraw them if (drawChanels!=null) { drawChanels.setOffsets(offsets); drawChanels.invalidate(); } } public GraphicAttributeBase getG() { return g; } public void setG(GraphicAttributeBase g) { this.g = g; } public int getnTilesPerColumn() { return nTilesPerColumn; } public void setnTilesPerColumn(int nTilesPerColumn) { this.nTilesPerColumn = nTilesPerColumn; } public int getnTilesPerRow() { return nTilesPerRow; } public void setnTilesPerRow(int nTilesPerRow) { this.nTilesPerRow = nTilesPerRow; } public float getyPixelsInMillivolts() { return yPixelsInMillivolts; } public void setyPixelsInMillivolts(int yPixelsInMillivolts) { Log.d("setyPixelsInMillivolts",Integer.toString(yPixelsInMillivolts)); this.yPixelsInMillivolts = yPixelsInMillivolts; if (drawChanels!=null){ drawChanels.setyPixelsInMillivolts(yPixelsInMillivolts); drawChanels.invalidate(); } } public float getxPixelsInMilliseconds() { return xPixelsInMilliseconds; } public void setxPixelsInMilliseconds(int xPixelsInMilliseconds) { Log.d("setxPixelsInMilliseconds",Integer.toString(xPixelsInMilliseconds)); this.xPixelsInMilliseconds = xPixelsInMilliseconds; } public float getTimeOffsetInMilliSeconds() { return timeOffsetInMilliSeconds; } public void setTimeOffsetInMilliSeconds(float timeOffsetInMilliSeconds) { this.timeOffsetInMilliSeconds = timeOffsetInMilliSeconds; } public void setFont(Font font) { this.font = font; } public void setH(DataHandler h) { this.h = h; } public void setSW(int sW) { SW = sW; } public void setSH(int sH) { SH =sH; } public int getSW() { return (SW); } public int getSH() { return (SH); } public int getW() { return (W); } public void setW(int w) { W = w; } public int getH() { return H; } public void setFont(String fontName) { this.font = new Font(fontName,0,14); } void setGraphicParameters(GraphicAttribute g) { this.g = g; } public void setYScale(float santimetersPerMillivolt) { float millimetersPerMillivolt = santimetersPerMillivolt/10; this.yPixelsInMillivolts = (7/millimetersPerMillivolt/(duim/sizeScreen)); if (drawChanels != null){ drawChanels.setYScale(millimetersPerMillivolt); drawChanels.invalidate(); this.gain = santimetersPerMillivolt; } // update status field updateStatus(); } public void setXScale(float millimetersPerSecond) { this.xPixelsInMilliseconds = (float)( (millimetersPerSecond*(3.15/5)/(1000*duim/sizeScreen))); this.W = (int) (millimetersPerSecond*32); this.SW = (int) ((int)millimetersPerSecond*(32.65)+50); this.speed = millimetersPerSecond; // update status field updateStatus(); } public void setInvert(boolean invert) { this.invert = invert; if (drawChanels != null){ drawChanels.setInvert(invert); drawChanels.invalidate(); } } public void setDrawChanels(DrawChanels drawChanels){ this.drawChanels = drawChanels; } public void setTvStatus(TextView tvStatus){ this.tvStatus = tvStatus; } public void setGraphicColor(Color c){ this.curveColor = c; } public Scroller getScroller() { return scroller; } public void setXScrollOffset(float distanse) { // update x offset xOffset += distanse; // update status setTime(distanse); } public void setYScrollOffset(float distanse) { yOffset += distanse; } private void updateStatus() { // check tvStatus and file if (tvStatus != null && this.h != null) { // prepare text values String text = time + " from start " + speed + " mm/sec. " + gain + " mV/mm."; // if scale mode selected if (this.touchMode == GestureListener.MODE_LINEAR) // update status both with scale parameters text += " \tScale: height " + scaleHeightValueText + ", width " + scaleWidthValueText; // set prepared text tvStatus.setText(text); } } private void setTime(float distanse) { // koef. to make value equal 10 seconds float koef = (float) 2.05; // check data nadler if(h != null){ if (g.isFlNonsection2()) this.time += (int) distanse*(((g.getNumberOfSamplesPerChannel())*koef)/getW())*2; else this.time += (int) distanse*(((g.getNumberOfSamplesPerChannel())*koef)/getW()); // update status field updateStatus(); } } public void setXScrollOffsetNull() { // check data handler if (h != null) { // set time value for 0 this.time = 0; // update status field updateStatus(); } } public void setYScrollOffsetnull() { this.yOffset = 0; } public void setMode(int mode) { // update mode this.touchMode = mode; // clear scale if (mode == GestureListener.MODE_BASIC) { // clear scale values scale.clear(); // clear rectangle parameters scaleHeightValueText = ""; scaleWidthValueText = ""; // set fill rectangle flag fillRect = false; // update status field updateStatus(); } // create basic rectangle (scale) else if (mode == GestureListener.MODE_LINEAR) { // create basic Scale rectangle scale.makeBasicRect(displayWidht, displayHeight); // update Scale height and width updateScaleParameters(); // update status field updateStatus(); } } public void setDisplayMetrics(int width, int height, int density) { this.displayDensity = density; this.displayHeight = height; this.displayWidht = width; } }