// Pipe.java - simulated venturi tube with inviscid, incompressible fluid and // uniform velocity curves // Copyright 2003 mark mitchell // No permission is granted for using this applet, or any derivative products, // for commercial purposes // 9/19/2003 mark mitchell // 9/24/2003 mark mitchell ported from swing to standard awt, so internet // explorer users could run it. this port: // (1) slightly hurt the object orientation, // (2) required removing container classes (hence the array kludges) // (3) introduced flickering // 9/25/2003 mark mitchell applied 'update(){paint()}' trick and // double buffering to stop flicker // 9/25/2003 mark mitchell added time AverageTimer since Java timer // is only good to about 50 milliseconds, to reduce jerkiness // 11/5/2003 mark mitchell changed flow marker from oval to clipart // 11/6/2003 mark mitchell switched flow marker from png to jpeg for // internet explorer // 11/6/2003 mark mitchell added roundoffNoOsc // 12/14/2003 mark mitchell added section tics // 12/16/2003 mark mitchell added shading of pressure and velocity energies // 2/20/2004 mark mitchell added hand cursor // 2/27/2004 mark mitchell made curves window taller // 3/16/2004 mark mitchell added tic marks in curves // 4/1/2004 mark mitchell display 'Curves' on screen rather than the less // understood 'Profiles' // 5/9/2004 mark mitchell added flow separation flag. this required narrowing // jointRight and jointWide from one third of applet height, to narrower // 2/28/2005 mark mitchell added handles at entry and exit // 3/15/2005 mark mitchell added margins on left and right so new handles // at entry and exit would no longer be clipped in half // 8/5/2005 mark mitchell fixed margins on left and right // command line parameters are all listed in java console // to compile this and create a jar file for uploading you will need // the java development kit (jdk) from http:java.sun.com/j2se. then // perform the following steps: // 1) javac -target 1.1 Pipe.java // 2) jar cvf Pipe.jar *.class // 3) rm *.class import java.applet.*; import java.awt.*; import java.awt.event.*; class AverageTimer { // this class will take the current time, and return // an "averaged" time-slice back. It gives us better // results than simply using Java's timer at face value. this // class was borrowed from // http://www.spacejack.org/games/dbufanim/DBufAnim.java long cArray[] = new long[ConstClass.numberSamples]; // circular array int cArrayIndex = 0; // current index to circular array public AverageTimer(long t) { int i; // fill array with an assumed rate so far // the actual times will quickly fix this, after numberSamples samples for (i = ConstClass.numberSamples - 1; i >= 0; i--) { cArray[i] = t - ((ConstClass.numberSamples - i) * ConstClass.frameDelay); } cArrayIndex = 0; } // by supplying the current time (by using System.currentTimeMillis()) // we calculate the average time over the past numberSamples frames, and // record this current time to continue keeping the average accurate as // possible public long deltaT (long curT) { int id = cArrayIndex-1; // calc index to previous sample if (id < 0) { id += ConstClass.numberSamples; } long dt = curT - cArray[id]; // diff time since prev sample if (dt > ConstClass.maxTimestep) { // slice was too big so advance previous times long ct = dt - ConstClass.maxTimestep; for (int i = 0; i < ConstClass.numberSamples; i++) { cArray[i] += ct; } } long t = curT - cArray[cArrayIndex]; // time diff since oldest time cArray[cArrayIndex] = curT; // save the current one cArrayIndex = (cArrayIndex + 1) % ConstClass.numberSamples; // advance index dt = t / (long)ConstClass.numberSamples; // calculate final average return dt; } } interface ConstClass { // handle parameters public static final Color handleColor = Color.yellow; public static final int handleHalfHeight = 5; public static final int handleHalfWidth = handleHalfHeight; // keep square // stuff for all panels public static final Color borderColor = Color.black; public static final Color titleColor = Color.black; public static final Color marginColor = Color.white; public static final Font titleFont = new Font("Helvetica", Font.BOLD, 22); // stuff for all panel geometries public static final int horizontalMargin = handleHalfWidth; public static final int appletWidthWithMargin = 500; public static final int appletLeft = horizontalMargin; public static final int appletRight = appletWidthWithMargin - horizontalMargin; public static final int appletWidth = appletRight - appletLeft + 1; public static final int xTitle = appletLeft + 5, yTitle = 24; // stuff for pipe panel public static final String pipeTitle = new String("Pipe Demo"); public static final int pipeHeight = 300; public static final int pipeHalfHeight = pipeHeight / 2; public static final Color pipeBackgroundColor = new Color(220, 220, 220); public static final Color flowRateColor = Color.cyan; public static final Color areaColor = Color.green; public static final Color pressureColor = Color.red; public static final Color velocityColor = Color.blue; public static final int minHalfHeight = 5; public static final double boundaryMin = 5; // pixels public static final double angleExpansionMax = 7.0 * 3.1416 / 180.0; public static final int xFlowSeparation = appletRight - 73; public static final int yFlowSeparation = pipeHeight - 33; // stuff for curves panel. legend strings are in same vertical order // that their corresponding graphs are in for the initial configuration public static final String curvesTitle = new String("Curves"); public static final int curvesHeight = 140; public static final Color curvesBackgroundColor = Color.white; public static final String areaLegend = new String("Area"); public static final String flowRateLegend = new String("Flow Rate"); public static final String pressureLegend = new String("Pressure"); public static final String velocityLegend = new String("Velocity"); public static final int xAreaLegend = appletRight - 140; public static final int yAreaLegend = 95; public static final int xFlowRateLegend = appletRight - 140; public static final int yFlowRateLegend = 60; public static final int xPressureLegend = appletRight - 140; public static final int yPressureLegend = 25; public static final int xVelocityLegend = appletRight - 140; public static final int yVelocityLegend = 130; public static final int numberTics = 5; public static final int ticLength = 10; public static final Font ticMarkFont = new Font("Helvetica", Font.PLAIN, 9); public static final Color ticColor = Color.black; public static final Color energyPressureColor = new Color(255, 232, 232); public static final Color energyVelocityColor = new Color(232, 232, 255); // copyright public static final String copyright = new String ("\1002003 mark mitchell http://home.earthlink.net/~mmc1919"); public static final int xCopyright = appletLeft + 3; public static final int yCopyright = pipeHeight - 6; public static final Color copyrightColor = new Color(173, 173, 173); public static final Font legendFont = new Font("Helvetica", Font.PLAIN, 10); // milliseconds between animation frames public static final int wakeupDelay = 20; // multiple of 20 milliseconds public static final int frameDelay = 3 * wakeupDelay; public static final int blinkPhases = 12; // # phases between blinks public static final int numberSamples = 6; // # samples to average public static final int maxTimestep = 3 * frameDelay; // time out limit // section parameters public static final Color waterColor = new Color(200, 200, 250); // blue public static final Color tubeColor = Color.black; // marker parameters public static final double markerDensity = 0.016; // markers per pixel^2 public static final int markersPerColumn = 6; // # markers in a column public static final Color markerColor = Color.red; public static final int markerDiameter = 6; // diameter of marker circles public static final double markerRadius = markerDiameter / 2.0; // assume constant density to get the trapezoidal area per column public static final double areaPerColumn = markersPerColumn / markerDensity; // use arrays as containers since microsoft does not do containers public static final int maxJoints = 4; public static final int maxSections = 3; public static final int maxMarkers = 5000; } class CreateMarkerReturn { int numMarkers; double area; int phase; double boundary; public CreateMarkerReturn(int numMarkersIn, double areaIn, int phaseIn, double boundaryIn) { numMarkers = numMarkersIn; area = areaIn; phase = phaseIn; boundary = boundaryIn; } } class CurvesPanel extends Panel { int numJoints = 0; Joint joints [] = new Joint [ConstClass.maxJoints]; String ticLabels [] = new String [ConstClass.numberTics]; // draw pressure and velocity curves using single call and arrays, so that // (1) pressure line can be filled above with velocity energy color, // and below with pressure energy color // (2) the calls are a little faster hopefully int xPoints [] = new int [ConstClass.appletWidthWithMargin + 3]; int yPoints [] = new int [ConstClass.appletWidthWithMargin + 3]; // cross sectional area assumes pipe is a rectangular solid, so // vertical width can vary but depth remains constant (area for // round pipe would vary as the square of the vertical width) private double areaAtJoint(Joint j) { // return cross-sectional area at a joint return j.getYHigh() - j.getYLow(); } private double areaBetweenJoints(Joint jPrev, Joint jNext, int x) { // return cross-sectional area at some point between two joints double denominator = jNext.getX() - jPrev.getX(); if (Math.abs(denominator) < 0.00001) { return 0.0; } else { double s = (x - jPrev.getX()) / denominator; return (1.0 - s) * (jPrev.getYHigh() - jPrev.getYLow()) + s * (jNext.getYHigh() - jNext.getYLow()); } } public CurvesPanel() { Dimension size = new Dimension(ConstClass.appletWidthWithMargin, ConstClass.curvesHeight); setBounds(0, ConstClass.pipeHeight, ConstClass.appletWidthWithMargin, ConstClass.curvesHeight); setBackground(ConstClass.curvesBackgroundColor); for (int i = 0; i < ConstClass.numberTics; i++) { int percent = (int) (((ConstClass.numberTics - 1 - i) * 100.0) / (ConstClass.numberTics - 1) + 0.5); ticLabels [i] = String.valueOf(percent) + "%"; } } private int graphToScreen(double x, double xMin, double xMax, int yMin, int yMax) { // convert graph quantity x into screen coordinates return (int) (yMin + (yMax - yMin) * (x - xMin) / (xMax - xMin) + 0.5); } public void paint(Graphics g) { double areaMin = 2.0 * ConstClass.minHalfHeight - 1; double areaMax = ConstClass.pipeHeight; int yMin = ConstClass.curvesHeight - 1; int yMax = 0; if (numJoints > 0) { // paint areas paintPressureVelocity(false, g, areaMin, areaMax, yMin, yMax); // paint border after areas but before curves so curves are not broken // when they intersect border paintBorder(g); // paint curves paintPressureVelocity(true, g, areaMin, areaMax, yMin, yMax); paintArea(g, areaMin, areaMax, yMin, yMax); paintFlowRate(g, areaMin, areaMax, yMin, yMax); } else { paintBorder(g); } paintSectionTics(g); paintScaleTics(g); // title g.setColor(ConstClass.titleColor); g.setFont(ConstClass.titleFont); g.drawString(ConstClass.curvesTitle, ConstClass.xTitle, ConstClass.yTitle); paintLegend(g); // left and right margins g.setColor(ConstClass.marginColor); g.fillRect(0, ConstClass.pipeHeight, ConstClass.horizontalMargin, ConstClass.curvesHeight); g.fillRect(ConstClass.appletRight - 1, ConstClass.pipeHeight, ConstClass.horizontalMargin + 1, ConstClass.curvesHeight); } private void paintArea(Graphics g, double areaMin, double areaMax, int yMin, int yMax) { g.setColor(ConstClass.areaColor); Joint jPrev = joints [0]; double area = areaAtJoint(jPrev); int xLast = jPrev.getX(); int yLast = graphToScreen(area, 0.0, areaMax, yMin, yMax); for (int i = 1; i < numJoints; i++) { Joint jNext = joints [i]; area = areaAtJoint(jNext); int xNext = jNext.getX(); if (xNext >= ConstClass.appletRight) xNext = ConstClass.appletRight; int yNext = graphToScreen(area, 0.0, areaMax, yMin, yMax); g.drawLine(xLast, yLast, xNext, yNext); xLast = xNext; yLast = yNext; } } private void paintBorder(Graphics g) { g.setColor(ConstClass.pipeBackgroundColor); g.drawLine(ConstClass.appletLeft, 0, ConstClass.appletLeft, ConstClass.curvesHeight - 1); g.drawLine(ConstClass.appletLeft, ConstClass.curvesHeight - 1, ConstClass.appletRight - 2, ConstClass.curvesHeight - 1); g.drawLine(ConstClass.appletRight - 2, ConstClass.curvesHeight - 1, ConstClass.appletRight - 2, 0); } private void paintFlowRate(Graphics g, double areaMin, double areaMax, int yMin, int yMax) { // flow rate is always constant g.setColor(ConstClass.flowRateColor); g.drawLine(ConstClass.appletLeft, ConstClass.curvesHeight / 2, ConstClass.appletRight - 2, ConstClass.curvesHeight / 2); } private void paintLegend(Graphics g) { g.setColor(ConstClass.areaColor); g.drawString(ConstClass.areaLegend, ConstClass.appletLeft + ConstClass.xAreaLegend, ConstClass.yAreaLegend); g.setColor(ConstClass.flowRateColor); g.drawString(ConstClass.flowRateLegend, ConstClass.appletLeft + ConstClass.xFlowRateLegend, ConstClass.yFlowRateLegend); g.setColor(ConstClass.pressureColor); g.drawString(ConstClass.pressureLegend, ConstClass.appletLeft + ConstClass.xPressureLegend, ConstClass.yPressureLegend); g.setColor(ConstClass.velocityColor); g.drawString(ConstClass.velocityLegend, ConstClass.appletLeft + ConstClass.xVelocityLegend, ConstClass.yVelocityLegend); } private void paintPressureVelocity(boolean paintCurvesElseAreas, Graphics g, double areaMin, double areaMax, int yMin, int yMax) { // continuity equation tells us that density x area x velocity // is constant. bernoulli tells us that // pressure + density x velocity-squared is constant double velocityMin = velocityFromArea(areaMax); double velocityMax = velocityFromArea(areaMin); double pressureMin = pressureFromVelocity(velocityMax, velocityMax); double pressureMax = pressureFromVelocity(velocityMin, velocityMax); for (int pass = 0; pass < 2; pass++) { Joint jPrev = joints [0]; int xPrev = jPrev.getX(); int y = 0; for (int i = 0; i < numJoints; i++) { Joint jNext = joints [i]; int xNext = jNext.getX(); for (int x = xPrev; x < xNext; x++) { double area = areaBetweenJoints(jPrev, jNext, x); double velocity = velocityFromArea(area); double pressure = pressureFromVelocity(velocity, velocityMax); switch (pass) { case 0: y = graphToScreen(pressure, pressureMin, pressureMax, yMin, yMax); break; case 1: y = graphToScreen(velocity, velocityMin, velocityMax, yMin, yMax); break; } xPoints [x - ConstClass.appletLeft] = x; yPoints [x - ConstClass.appletLeft] = y; } jPrev = jNext; xPrev = xNext; } switch (pass) { case 0: if (paintCurvesElseAreas) { // draw pressure curve line g.setColor(ConstClass.pressureColor); g.drawPolyline(xPoints, yPoints, ConstClass.appletWidth - 3); } else { // fill underneath pressure curve line with pressure // energy color g.setColor(ConstClass.energyPressureColor); xPoints [ConstClass.appletWidth - 2] = ConstClass.appletRight; yPoints [ConstClass.appletWidth - 2] = ConstClass.curvesHeight; xPoints [ConstClass.appletWidth - 1] = ConstClass.appletLeft; yPoints [ConstClass.appletWidth - 1] = ConstClass.curvesHeight; g.fillPolygon(xPoints, yPoints, ConstClass.appletWidth); // fill above pressure curves line with velocity energy color g.setColor(ConstClass.energyVelocityColor); xPoints [ConstClass.appletWidth - 2] = ConstClass.appletRight; yPoints [ConstClass.appletWidth - 2] = 0; xPoints [ConstClass.appletWidth - 1] = ConstClass.appletLeft; yPoints [ConstClass.appletWidth - 1] = 0; g.fillPolygon(xPoints, yPoints, ConstClass.appletWidth); } break; case 1: if (paintCurvesElseAreas) { // draw velocity curve line g.setColor(ConstClass.velocityColor); g.drawPolyline(xPoints, yPoints, ConstClass.appletWidth - 3); } break; } } } private void paintScaleTics(Graphics g) { // show tics at left g.setColor(ConstClass.ticColor); g.setFont(ConstClass.ticMarkFont); for (int i = 0; i < ConstClass.numberTics; i++) { // tic mark int y = (int) ((i * (ConstClass.curvesHeight - 1)) / (ConstClass.numberTics - 1.0)); g.drawLine(ConstClass.appletLeft, y, ConstClass.appletLeft + ConstClass.ticLength, y); // tic label g.drawString(ticLabels [i], ConstClass.appletLeft + ConstClass.ticLength, y); } } private void paintSectionTics(Graphics g) { // paint one section tic at top, and one at bottom, at each joint int j; g.setColor(ConstClass.ticColor); for (j = 1; j < numJoints - 1; j++) { g.drawLine(joints [j].getX(), 0, joints [j].getX(), ConstClass.ticLength); g.drawLine(joints [j].getX(), ConstClass.curvesHeight, joints [j].getX(), ConstClass.curvesHeight - ConstClass.ticLength); } } private double pressureFromVelocity(double v, double vMax) { return vMax * vMax - v * v; } public void removeStaleJoints() { numJoints = 0; } public void setJoints(int numJointsIn, Joint jointsIn []) { numJoints = numJointsIn; for (int i = 0; i < numJoints; i++) { joints [i] = jointsIn [i]; } repaint(); } private double velocityFromArea(double area) { return 1.0 / area; } } class Joint { // a joint is the transition from one trapezoidal tube area to another // trapezoidal tube area. all joints can be resized and moved // x coordinate and half width give horizontal position and vertical // extent. the joint is infinitely thin horizontally, and twice // hH in height. the center of the handle is at // (xPipe, pipeHalfHeight - hH) private int x, hH; public int getX() { return x; } public int getYHigh() { return ConstClass.pipeHalfHeight + hH; } public int getYLow() { return ConstClass.pipeHalfHeight - hH; } public boolean hit(int xEvent, int yEvent) { // return true if coordinates are within the handle if ((xEvent < x - ConstClass.handleHalfWidth) || (x + ConstClass.handleHalfWidth < xEvent)) { return false; } if ((yEvent < ConstClass.pipeHalfHeight - hH - ConstClass.handleHalfHeight) || (ConstClass.pipeHalfHeight - hH + ConstClass.handleHalfHeight < yEvent)) { return false; } return true; } public Joint(int xIn, int hHIn) { x = xIn; hH = hHIn; } public void paintComponent(Graphics g) { // compute handle extent int xLeft = x - ConstClass.handleHalfWidth; int yBottom = ConstClass.pipeHalfHeight - hH - ConstClass.handleHalfHeight; int xWidth = 2 * ConstClass.handleHalfWidth; int yHeight = xWidth; // show handle g.setColor(ConstClass.handleColor); g.fillRect(xLeft, yBottom, xWidth, yHeight); g.setColor(ConstClass.borderColor); g.drawRect(xLeft, yBottom, xWidth, yHeight); } public void setX(int xNew, Joint leftConstraint, Joint rightConstraint) { // prevent handle overlap int handleFullWidth = 2 * ConstClass.handleHalfWidth; // before applying constraints x = xNew; // satisfy left constraint if (leftConstraint == null) { x = ConstClass.appletLeft; } else { if (x < leftConstraint.x + handleFullWidth) { x = leftConstraint.x + handleFullWidth; } } // satisfy right constraint if (rightConstraint == null) { x = ConstClass.appletRight - 1; } else { if (x > rightConstraint.x - handleFullWidth) { x = rightConstraint.x - handleFullWidth; } } } public void setY(int yNew) { hH = ConstClass.pipeHalfHeight - yNew; // satisfy low constraint if (hH < ConstClass.minHalfHeight) { hH = ConstClass.minHalfHeight; } // satisfy high constraint if (hH > ConstClass.pipeHalfHeight - ConstClass.handleHalfHeight) { hH = ConstClass.pipeHalfHeight - ConstClass.handleHalfHeight; } } public int xOffset(int xEvent) { return xEvent - x; } public int yOffset(int yEvent) { return yEvent - (ConstClass.pipeHalfHeight - hH); } } class Marker { // coordinates of center int x, y; // phase between 0 and blinkPhases int phase; public int getX() { return x; } public int getY() { return y; } public Marker(int xIn, int yIn, int phaseIn) { x = xIn; y = yIn; phase = phaseIn; } } public class Pipe extends Applet implements Runnable { // panels, from top to bottom PipePanel pipe; CurvesPanel curves; // animation Thread animationThread; AverageTimer averageTimer = null; Image flowMarkerImage; // flow marker shows how fluid is flowing Image flowSeparationImage; // flag shows when flow separation is occuring // debug boolean traceSpew; int maxTimesteps; private boolean cmdLineBoolean(String name, boolean dflt) { String valueField = getParameter(name); if (valueField != null) { if (valueField.length() > 0) { boolean value = Boolean.valueOf(valueField).booleanValue(); System.out.println("Name: " + name + " Value: " + value); return value; } } System.out.println("Name: " + name + " Value: " + dflt + " (default)"); return dflt; } private int cmdLineInteger(String name, int dflt) { String valueField = getParameter(name); if (valueField != null) { if (valueField.length() > 0) { int value = Integer.parseInt(valueField); System.out.println("Name: " + name + " Value: " + value); return value; } } System.out.println("Name: " + name + " Value: " + dflt + " (default)"); return dflt; } public void init() { setLayout(null); System.out.println(""); System.out.println("Copyright 2003, Mark Mitchell"); System.out.println("http://home.earthlink.net/~mmc1919"); // java applet will run at this priority int priority = cmdLineInteger("priority", Thread.MAX_PRIORITY); // true to replace numerous local repaints by global repaint boolean combineRefreshes = cmdLineBoolean("combineRefreshes", false); // true to turn on trace spew traceSpew = cmdLineBoolean("traceSpew", false); // 0 for no limit, else limit the number of time steps maxTimesteps = cmdLineInteger("maxTimesteps", 0); // true to turn on copyright boolean showCopyright = cmdLineBoolean("showCopyright", false); flowMarkerImage = getImage(getCodeBase(), "img/flowMarker.gif"); flowSeparationImage = getImage(getCodeBase(), "img/flowSeparation.gif"); pipe = new PipePanel(combineRefreshes, flowMarkerImage, flowSeparationImage, traceSpew, showCopyright); curves = new CurvesPanel(); add(pipe); add(curves); // start animation thread animationThread = new Thread(this); animationThread.setPriority(priority); animationThread.start(); averageTimer = new AverageTimer(System.currentTimeMillis()); } public void paint(Graphics g) { pipe.paint(g); curves.paint(g); } public void run() { StepAnimationReturn rtn = new StepAnimationReturn(); int numTimesteps = 0; long timeElapsed = 0; while (true) { if (averageTimer == null) { // kludge for internet explorer race condition: make // sure averageTimer has been defined timeElapsed += ConstClass.wakeupDelay; } else { timeElapsed += averageTimer.deltaT(System.currentTimeMillis()); } if (traceSpew) { // System.out.println("timeElapsed " + timeElapsed); } if (timeElapsed > ConstClass.frameDelay) { timeElapsed = 0; // pipe panel passes current joints to curves panel rtn = pipe.stepAnimation(); if (rtn.curvesRefresh) { curves.setJoints(rtn.numJoints, rtn.joints); } } if ((maxTimesteps > 0) && (++numTimesteps > maxTimesteps)) { System.exit(-1); } try { Thread.sleep(ConstClass.wakeupDelay); } catch(InterruptedException e) { } } } } class PipePanel extends Panel implements MouseListener, MouseMotionListener { private boolean combineRefreshes; // true for global repaint, else // local repaints private boolean traceSpew; // true to turn on trace spew private int maxTimesteps; // 0 for no limit, else limit the number // of time steps private boolean showCopyright; // true to show copyright (a bit obnoxious) // from left to right the joints are labeled: // narrow (fixed and invisible) // left (adjustable and visible) // right (adjustable and visible) // wide (fixed and invisible) // initially the joints are defined so we have a narrow // tube, then a transition to a large tube, then a wide tube private Joint jointIn = new Joint(ConstClass.appletLeft, ConstClass.pipeHeight / 14); private Joint jointLeft = new Joint(ConstClass.appletLeft + ConstClass.appletWidth / 3, ConstClass.pipeHeight / 14); private Joint jointRight = new Joint(ConstClass.appletLeft + (2 * ConstClass.appletWidth) / 3, ConstClass.pipeHeight / 6); private Joint jointOut = new Joint(ConstClass.appletRight - 1, ConstClass.pipeHeight / 6); // marshal the joints as an array list boolean curvesRefresh = false; // true if joints changed so need refresh int numJoints = 0; Joint joints [] = new Joint [ConstClass.maxJoints]; // pipe sections private Section sectionIn = new Section(jointIn, jointLeft); private Section sectionMiddle = new Section(jointLeft, jointRight); private Section sectionOut = new Section(jointRight, jointOut); // marshal the sections as an array list int numSections = 0; Section sections [] = new Section [ConstClass.maxSections]; // each flow marker is a simple geometric shape or picture. the file // for that picture must exist in the same directory as the jar file private int numMarkers = 0; private Marker markers [] = new Marker [ConstClass.maxMarkers]; private Image flowMarkerImage; // flow separation private boolean flowSeparation; private Image flowSeparationImage; // dragging stuff private boolean dragging; // true when dragging is in progress private Joint draggedJoint; // joint that is being dragged if dragging private Joint leftConstraint; // joint to left of dragged joint, or null private Joint rightConstraint; // joint to right of dragged joint, or null private int xOffset, yOffset; // distance from center of handle of dragged // joint to the cursor when user clicked // animation private int phase = 0; private StepAnimationReturn stepAnimationRtn; // allocated once for speed // double buffering required since awt (unlike swing) does not have // built-in support for it Image offscreen; // cursor Cursor handCursor = new Cursor(Cursor.HAND_CURSOR); public void configSeparation(boolean on) { if (traceSpew) { System.out.println("configSeparation"); } curvesRefresh = true; deleteMarkers(); createMarkers(); repaint(); } private void createMarkers() { // to maintain consistency of the marker column separations across // joints, each section passes the parameter s to the succeeding // section. s ranges from 0 (previous section used up none of the // separation between marker columns) to 1 (previous section used // up all of the separation between marker columns) CreateMarkerReturn rtn = new CreateMarkerReturn(numMarkers, 0.0, 0, 0.0); for (int i = 0; i < numSections; i++) { rtn = sections [i].createMarkers(rtn.numMarkers, markers, rtn.area, rtn.phase, rtn.boundary); } numMarkers = rtn.numMarkers; } private void deleteMarkers() { for (int i = 0; i < numMarkers; i++) markers [i] = null; numMarkers = 0; } private boolean findJoint(int xEvent, int yEvent) { // only look for adjustable handles dragging = false; if (jointIn.hit(xEvent, yEvent)) { dragging = true; draggedJoint = jointIn; leftConstraint = null; rightConstraint = jointLeft; } else if (jointLeft.hit(xEvent, yEvent)) { dragging = true; draggedJoint = jointLeft; leftConstraint = jointIn; rightConstraint = jointRight; } else if (jointRight.hit(xEvent, yEvent)) { dragging = true; draggedJoint = jointRight; leftConstraint = jointLeft; rightConstraint = jointOut; } else if (jointOut.hit(xEvent, yEvent)) { dragging = true; draggedJoint = jointOut; leftConstraint = jointRight; rightConstraint = null; } if (dragging) { xOffset = draggedJoint.xOffset(xEvent); yOffset = draggedJoint.yOffset(yEvent); } return dragging; } private void invalidateMarkers() { // invalidate minimal regions encompassing previous // and next markers. alternative approach is to // redraw whole screen, but that gives flicker int phaseBefore = phase - 1; if (phaseBefore < 0) { phaseBefore += ConstClass.blinkPhases; } if (combineRefreshes) { repaint(); } else { invalidateMarkersForPhase(phaseBefore); invalidateMarkersForPhase(phase); } } private void invalidateMarkersForPhase(int phase) { for (int i = 0; i < numMarkers; i++) { Marker m = markers [i]; if (m.phase == phase) { repaint(m.getX(), m.getY(), ConstClass.markerDiameter, ConstClass.markerDiameter); } } } public void mouseClicked(MouseEvent evt) { } public void mouseDragged(MouseEvent evt) { if (!dragging) return; draggedJoint.setX(evt.getX() - xOffset, leftConstraint, rightConstraint); draggedJoint.setY(evt.getY() - yOffset); curvesRefresh = true; repaint(); } public void mouseEntered(MouseEvent evt) { } public void mouseExited(MouseEvent evt) { } public void mouseMoved(MouseEvent evt) { if (jointIn.hit(evt.getX(), evt.getY()) || jointLeft.hit(evt.getX(), evt.getY()) || jointRight.hit(evt.getX(), evt.getY()) || jointOut.hit(evt.getX(), evt.getY())) { setCursor(handCursor); } else { setCursor(Cursor.getDefaultCursor()); } if (dragging) { repaint(); } } public void mousePressed(MouseEvent evt) { if (dragging) return; if (findJoint(evt.getX(), evt.getY())) { deleteMarkers(); } } public void mouseReleased(MouseEvent evt) { if (dragging) { draggedJoint = null; dragging = false; createMarkers(); setFlowSeparationFlag(); repaint(); } } public void paint(Graphics g) { // double buffering if (offscreen == null) { offscreen = createImage(ConstClass.appletWidthWithMargin, ConstClass.pipeHeight); } Graphics gOff = offscreen.getGraphics(); // paint all sections sectionIn.paintComponent(gOff); sectionMiddle.paintComponent(gOff); sectionOut.paintComponent(gOff); // draw markers associated with the current phase gOff.setColor(ConstClass.markerColor); for (int i = numMarkers - 1; i >= 0; i--) { Marker m = markers [i]; if (m.phase == phase) { // you can use either fillOval or fillRect here // to draw geometric flow marker gOff.drawImage(flowMarkerImage, m.getX(), m.getY(), this); } } // left and right margins gOff.setColor(ConstClass.marginColor); gOff.fillRect(0, 0, ConstClass.horizontalMargin, ConstClass.pipeHeight); gOff.fillRect(ConstClass.appletRight - 1, 0, ConstClass.horizontalMargin + 1, ConstClass.pipeHeight); // flow separation modeling flag if (flowSeparation) { gOff.drawImage(flowSeparationImage, ConstClass.xFlowSeparation, ConstClass.yFlowSeparation, this); } // only paint the visible joints jointIn.paintComponent(gOff); jointLeft.paintComponent(gOff); jointRight.paintComponent(gOff); jointOut.paintComponent(gOff); // title gOff.setColor(ConstClass.titleColor); gOff.setFont(ConstClass.titleFont); gOff.drawString(ConstClass.pipeTitle, ConstClass.xTitle, ConstClass.yTitle); if (showCopyright) paintCopyright(gOff); g.drawImage(offscreen, 0, 0, null); gOff.dispose(); } private void paintCopyright(Graphics g) { g.setFont(ConstClass.legendFont); g.setColor(ConstClass.copyrightColor); g.drawString(ConstClass.copyright, ConstClass.xCopyright, ConstClass.yCopyright); } public PipePanel(boolean combineRefreshesIn, Image flowMarkerImageIn, Image flowSeparationImageIn, boolean traceSpewIn, boolean showCopyrightIn) { Dimension size = new Dimension(ConstClass.appletWidth, ConstClass.pipeHeight); setBounds(0, 0, ConstClass.appletWidthWithMargin, ConstClass.pipeHeight); combineRefreshes = combineRefreshesIn; traceSpew = traceSpewIn; showCopyright = showCopyrightIn; addMouseListener(this); addMouseMotionListener(this); numJoints = 0; joints [numJoints++] = jointIn; joints [numJoints++] = jointLeft; joints [numJoints++] = jointRight; joints [numJoints++] = jointOut; if (numJoints > ConstClass.maxJoints) { System.out.println("Too many joints"); System.exit(-1); } numSections = 0; sections [numSections++] = sectionIn; sections [numSections++] = sectionMiddle; sections [numSections++] = sectionOut; // need to create the first set of markers after creating the sections numMarkers = 0; createMarkers(); stepAnimationRtn = new StepAnimationReturn(); setBackground(ConstClass.pipeBackgroundColor); flowMarkerImage = flowMarkerImageIn; flowSeparationImage = flowSeparationImageIn; setFlowSeparationFlag(); curvesRefresh = true; } private void setFlowSeparationFlag() { // show flow separation flag if any joint is expanding by // more than angleExpansionMax degrees flowSeparation = false; for (int i = 0; i < numSections; i++) { if (sections [i].flowIsSeparating()) { flowSeparation = true; break; } } } public StepAnimationReturn stepAnimation() { phase = (phase + 1) % ConstClass.blinkPhases; if (traceSpew) { // System.out.println("stepAnimation"); } invalidateMarkers(); stepAnimationRtn.numJoints = numJoints; stepAnimationRtn.joints = joints; stepAnimationRtn.curvesRefresh = curvesRefresh; // return value will notify curves that refresh is needed, // so we can safely turn off the local flag curvesRefresh = false; return stepAnimationRtn; } public void update(Graphics g) { // no need to erase current screen, so just call paint paint(g); } } class Section { Joint jointIn, jointLeft, jointRight, jointOut; int xWater [], yWater[]; private double sNoSolution = -99999.9; public CreateMarkerReturn createMarkers(int numMarkers, Marker markers [], double areaAdjustment, int phase, double boundary) { double xLast = jointLeft.getX(); double hHLast = jointLeft.getYHigh() - ConstClass.pipeHalfHeight; double xRight = jointRight.getX(); double hHRight = jointRight.getYHigh() - ConstClass.pipeHalfHeight; // account for any area already assigned to previous column double hAreaAdjusted = ConstClass.areaPerColumn / 2.0 - areaAdjustment; double areaAdjusted = ConstClass.areaPerColumn - areaAdjustment; // loop through columns, left to right double xColumn = xLast, hwColumn = hHLast; while (true) { if (hAreaAdjusted > 0) { double sHalfway = sFromArea(hAreaAdjusted, xLast, hHLast, xRight, hHRight); if (sHalfway == sNoSolution) { break; } double xPrev = xColumn; double hwPrev = hwColumn; xColumn = (1.0 - sHalfway) * xLast + sHalfway * xRight; hwColumn = (1.0 - sHalfway) * hHLast + sHalfway * hHRight; if (xColumn > xRight) { break; } if (xColumn > 0) { numMarkers = createMarkersInColumn(numMarkers, markers, xColumn, hwColumn - boundary, phase); } } double sAllTheWay = sFromArea(areaAdjusted, xLast, hHLast, xRight, hHRight); if (sAllTheWay == sNoSolution) { break; } double xNext = (1.0 - sAllTheWay) * xLast + sAllTheWay * xRight; double hwNext = (1.0 - sAllTheWay) * hHLast + sAllTheWay * hHRight; if (xNext > xRight) { break; } // get ready for next column xLast = xNext; hHLast = hwNext; hAreaAdjusted = ConstClass.areaPerColumn / 2.0; areaAdjusted = ConstClass.areaPerColumn; phase = (phase + 1) % ConstClass.blinkPhases; } // return new markers and information for continuity CreateMarkerReturn rtn = new CreateMarkerReturn(numMarkers, trapezoidalArea(xLast, hHLast, xRight, hHRight), phase, boundary); return rtn; } private int createMarkersInColumn(int numMarkers, Marker markers [], double xColumn, double hwColumn, int phase) { // insert markers for one column double markerSeparation = (2.0 * hwColumn) / ConstClass.markersPerColumn; double yMarker = ConstClass.pipeHalfHeight - hwColumn + markerSeparation / 2.0; for (int i = 0; i < ConstClass.markersPerColumn; i++) { Marker m = new Marker(roundoffNoOsc (xColumn, false), roundoffNoOsc (yMarker, true), phase); if (numMarkers > ConstClass.maxMarkers) { System.out.println("Too many markers"); System.exit(-1); } markers [numMarkers++] = m; yMarker += markerSeparation; } return numMarkers; } public boolean flowIsSeparating() { double rise = jointRight.getYHigh() - jointLeft.getYHigh(); double run = jointRight.getX() - jointLeft.getX(); if (run < 1.0) return false; return ConstClass.angleExpansionMax < Math.atan(rise / run); } public void paintComponent(Graphics g) { // background g.setColor(ConstClass.pipeBackgroundColor); xWater [0] = jointLeft.getX(); yWater [0] = 0; xWater [1] = jointRight.getX(); yWater [1] = 0; xWater [2] = jointRight.getX(); yWater [2] = jointRight.getYLow(); xWater [3] = jointLeft.getX(); yWater [3] = jointLeft.getYLow(); g.fillPolygon(xWater, yWater, 4); xWater [0] = jointLeft.getX(); yWater [0] = ConstClass.appletWidth; xWater [1] = jointRight.getX(); yWater [1] = ConstClass.appletWidth; xWater [2] = jointRight.getX(); yWater [2] = jointRight.getYHigh(); xWater [3] = jointLeft.getX(); yWater [3] = jointLeft.getYHigh(); g.fillPolygon(xWater, yWater, 4); // show water g.setColor(ConstClass.waterColor); xWater [0] = jointLeft.getX(); yWater [0] = jointLeft.getYLow(); xWater [1] = jointRight.getX(); yWater [1] = jointRight.getYLow(); xWater [2] = jointRight.getX(); yWater [2] = jointRight.getYHigh(); xWater [3] = jointLeft.getX(); yWater [3] = jointLeft.getYHigh(); g.fillPolygon(xWater, yWater, 4); // draw upper and lower bounds of this section g.setColor(ConstClass.tubeColor); g.drawLine(jointLeft.getX(), jointLeft.getYLow(), jointRight.getX(), jointRight.getYLow()); g.drawLine(jointLeft.getX(), jointLeft.getYHigh(), jointRight.getX(), jointRight.getYHigh()); } private int roundoffNoOsc(double c, boolean noOsc) { // given the double precision coordinate of the center of the flow // marker, return the integer coordinate of the top left corner. // // the complication is that simply rounding off causes vertical // oscillations about horizontal lines, so if the coordinate // is within epsilon of a pixel boundary, it is always rounded up double darg = c - ConstClass.markerRadius + 0.5; int iarg = (int) darg; if (noOsc) { if (Math.abs (iarg + 1 - darg) < 0.01) iarg += 1; } return iarg; } public Section(Joint jointLeftIn, Joint jointRightIn) { jointLeft = jointLeftIn; jointRight = jointRightIn; xWater = new int[4]; yWater = new int[4]; } private double sFromArea(double area, double xLast, double hHLast, double xRight, double hHRight) { // compute the s parameter that would give // the specified trapezoidal area, using // trapezoidalArea = 0.5 (height1 + height2) width // since height2 depends on the width we // will parameterize both height2 and width using s, where // height2 = (1 - s) hLast + (s) wRight // width = s (xRight - xLast) double hLast = 2.0 * hHLast; // convert to full width double wRight = 2.0 * hHRight; // convert to full width double xDelta = xRight - xLast; double yDelta = wRight - hLast; // rearranging, we have // 2 area = (2 hLast + s yDelta) (s xDelta) double s; if (Math.abs(xDelta) < 0.0001) { s = sNoSolution; } else if (Math.abs(yDelta) < 0.0001) { s = area / (hLast * xDelta); //System.out.println("s linear " + s); } else { double a = xDelta * yDelta; double b = 2.0 * hLast * xDelta; double c = -2.0 * area; double radical = b * b - 4.0 * a * c; if (radical < 0.0001) { s = sNoSolution; } else { s = (-b + Math.sqrt(radical)) / (2.0 * a); //System.out.println("s quadratic " + s); } } return s; } private double trapezoidalArea(double xLast, double hHLast, double xRight, double hHRight) { double hLast = 2.0 * hHLast; double hRight = 2.0 * hHRight; return 0.5 * (hLast + hRight) * (xRight - xLast); } } class StepAnimationReturn { int numJoints = 0; Joint joints []; boolean curvesRefresh; // true if need refreshsince joints have changed }