jcm1-source/0000755000076500011320000000000011741343635012207 5ustar djefacultyjcm1-source/ScatterPlotApplet.java0000644000076500011320000003556311741343635016500 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ import edu.hws.jcm.awt.*; import edu.hws.jcm.data.*; import edu.hws.jcm.draw.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import java.util.*; import java.applet.Applet; /** * A ScatterPlotApplet shows a scatter plot of data from a DataTableInput. * The user can enter the data in a two-column table that is shown in * the applet. It is also possible to configure the applet with a menu * of file names. These files, which must be in the same directory as * the Web page on which the applet appears, will appear in a menu. * A file can contain data for the table, with two numbers per line. * When the user loads the file, the data replaces the data in the table. */ public class ScatterPlotApplet extends Applet implements ActionListener { private Frame frame; // If non-null, a separate window. private String frameTitle; // Title for the separate window. private Button launchButton; // If non-null, then clicking this buttons opens a separate window. private String launchButtonName; // Name for the launch button. private DataTableInput table; // The table for input of data. private ScatterPlot scatterPlot; // The scatter plot of the data. private DisplayCanvas canvas; // The DisplayCanvas on which the plot is drawn. private Button loadFileButton; // When clicked, a data file is loaded. private Choice fileMenu; // Pop-up menu containing names of functions. private String[] fileNames; // Names of data files associated with menu entries. private Controller mainController; // Controller from the main JCMPanel. /** * The init() method is called by the system to set up the applet. * If the applet does not appear as a button, then init() creates the main panel of the applet * and calls setUpMainPanel to set it up. */ public void init() { frameTitle = getParameter("FrameTitle"); // Get title to be used for separate window, if any. if (frameTitle == null) { frameTitle = "Scatter Plots"; int pos = frameTitle.lastIndexOf('.'); if (pos > -1) frameTitle = frameTitle.substring(pos+1); } setLayout(new BorderLayout()); int height = getSize().height; launchButtonName = getParameter("LaunchButtonName"); if ( (height > 0 && height <= 50) || launchButtonName != null) { // Use a separater window and only show a button in the applet. if (launchButtonName == null) launchButtonName = "Launch " + frameTitle; launchButton = new Button(launchButtonName); add(launchButton, BorderLayout.CENTER); launchButton.addActionListener(this); } else { // Show the main panel in the applet, not in a separate window. add(makeMainPanel(), BorderLayout.CENTER); } } /* * Create the main panel of the applet. */ public Panel makeMainPanel() { // Make the main panel JCMPanel panel = new JCMPanel(2); mainController = panel.getController(); panel.setBackground(new Color(0,0,180)); panel.setInsetGap(2); setLayout(new BorderLayout()); // Make a DataInputTable with two columns table = new DataTableInput(null, 2); table.setColumnName(0, getParameter("ColumnName1", "X")); table.setColumnName(1, getParameter("ColumnName2", "Y")); table.setThrowErrors(true); if ( "yes".equalsIgnoreCase(getParameter("ShowColumnTitles","yes"))) table.setShowColumnTitles(true); if ( "yes".equalsIgnoreCase(getParameter("ShowRowNumbers","yes"))) table.setShowRowNumbers(true); // Make input boxes for getting expressions that can include // the variables associated with the table. Initially, the // expressions are just the column names. Parser parser = new Parser(); table.addVariablesToParser(parser); ExpressionInput input1 = new ExpressionInput(table.getColumnName(0),parser); input1.setOnUserAction(mainController); ExpressionInput input2 = new ExpressionInput(table.getColumnName(1),parser); input2.setOnUserAction(mainController); // Make a scatter plot that graphs the first expressiong vs. the second expression. scatterPlot = new ScatterPlot(table, input1.getExpression(), input2.getExpression()); if ( ! "yes".equalsIgnoreCase(getParameter("ShowRegressionLine","yes"))) scatterPlot.setShowRegressionLine(false); if ( ! "yes".equalsIgnoreCase(getParameter("MissingValueIsError","yes"))) scatterPlot.setMissingValueIsError(false); // Create the display canvas where the scater plot will be shown. canvas = new DisplayCanvas(); canvas.add(new Axes()); canvas.add(scatterPlot); mainController.setErrorReporter(canvas); // A compute button to recompute everything. ComputeButton computeButton = new ComputeButton("Update Display"); computeButton.setOnUserAction(mainController); computeButton.setBackground(Color.lightGray); // A menu of files that can be loaded. If no filenames are provided as // applet parameters, then menu is null. Panel menu = makefileMenu(); // Lay out the components in the applet. JCMPanel inputPanel = null; Panel bottom = null; //might not be a JCMPanel if ( "yes".equalsIgnoreCase(getParameter("UseExpressionInputs","yes"))) { inputPanel = new JCMPanel(1,2); inputPanel.setBackground(Color.lightGray); JCMPanel leftInput = new JCMPanel(); leftInput.add(new Label(" Plot: "), BorderLayout.WEST); leftInput.add(input1, BorderLayout.CENTER); inputPanel.add(leftInput); JCMPanel rightInput = new JCMPanel(); rightInput.add(new Label(" versus: "), BorderLayout.WEST); rightInput.add(input2, BorderLayout.CENTER); inputPanel.add(rightInput); bottom = new JCMPanel(new BorderLayout(12,3)); bottom.add(inputPanel, BorderLayout.CENTER); bottom.add(computeButton, BorderLayout.EAST); } if ( scatterPlot.getShowRegressionLine() && "yes".equalsIgnoreCase(getParameter("ShowStats","yes")) ) { // Make a display label to show some statistics about the data. DisplayLabel dl = new DisplayLabel( "Slope = #; Intercept = #; Correlation = #", new Value[] { scatterPlot.getValueObject(ScatterPlot.SLOPE), scatterPlot.getValueObject(ScatterPlot.INTERCEPT), scatterPlot.getValueObject(ScatterPlot.CORRELATION) } ); dl.setAlignment(Label.CENTER); dl.setBackground(Color.lightGray); dl.setForeground(new Color(200,0,0)); dl.setFont(new Font("Serif",Font.PLAIN,14)); if (bottom != null) bottom.add(dl, BorderLayout.SOUTH); else { bottom = new JCMPanel(new BorderLayout(12,3)); bottom.add(dl, BorderLayout.CENTER); bottom.add(computeButton, BorderLayout.EAST); } } if (bottom == null) { if (menu != null) menu.add(computeButton, BorderLayout.EAST); else { bottom = new Panel(); bottom.add(computeButton); } } panel.add(canvas, BorderLayout.CENTER); panel.add(table, BorderLayout.WEST); if (bottom != null) panel.add(bottom, BorderLayout.SOUTH); if (menu != null) panel.add(menu, BorderLayout.NORTH); else { String title = getParameter("PanelTitle"); if (title != null) { Label pt = new Label(title, Label.CENTER); pt.setBackground(Color.lightGray); pt.setForeground(new Color(200,0,0)); pt.setFont(new Font("Serif",Font.PLAIN,14)); panel.add(pt, BorderLayout.NORTH); } } return panel; } // end makeMainPanel() private Panel makefileMenu() { // If the applet tag contains params named "File", "File1", "File2", ..., use // their values to make a file menu. If the value of the param contains a ";", // then the first part, up to the ";", goes into the menu and the second part // is the name of the file. If there is no ";", then the entire value is // shown in the menu and is also used as the name of the file. The actual // files must be in the same directory as the Web page that contains the applet. Vector names = new Vector(); fileMenu = new Choice(); String file = getParameter("File"); int ct = 1; if (file == null) { file = getParameter("File1"); ct = 2; } while (file != null) { file = file.trim(); int pos = file.indexOf(";"); String menuEntry; if (pos == -1) menuEntry = file; else { menuEntry = file.substring(0,pos).trim(); file = file.substring(pos+1).trim(); } names.addElement(file); fileMenu.add(menuEntry); file = getParameter("File" + ct); ct++; } if (names.size() == 0) { fileMenu = null; return null; } else { fileNames = new String[names.size()]; for (int i = 0; i < names.size(); i++) fileNames[i] = (String)names.elementAt(i); Panel p = new Panel(); p.setBackground(Color.lightGray); p.setLayout(new BorderLayout(5,5)); p.add(fileMenu,BorderLayout.CENTER); loadFileButton = new Button("Load Data File: "); loadFileButton.addActionListener(this); p.add(loadFileButton,BorderLayout.WEST); fileMenu.setBackground(Color.white); return p; } } private void doLoadFile(String name) { // Load the file from the same directory as the Web page and put the data // from the file into the table. The file should contain two numbers on // each line. InputStream in; try { URL url = new URL(getDocumentBase(), name); in = url.openStream(); } catch (Exception e) { canvas.setErrorMessage(null,"Unable to open file named \"" + name + "\": " + e); return; } Reader inputReader = new InputStreamReader(in); try { table.readFromStream(inputReader); inputReader.close(); } catch (Exception e) { canvas.setErrorMessage(null,"Unable to get data from file \"" + name + "\": " + e.getMessage()); return; } mainController.compute(); } /** * Respond when user clicks a button; not meant to be called directly. * This opens and closes the separate window. */ synchronized public void actionPerformed(ActionEvent evt) { Object source = evt.getSource(); if (loadFileButton != null && source == loadFileButton) { doLoadFile( fileNames[fileMenu.getSelectedIndex()] ); } else if (source == launchButton && launchButton != null) { // Open or close separate frame. launchButton.setEnabled(false); if (frame == null) { frame = new Frame(frameTitle); frame.add(makeMainPanel()); frame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { frame.dispose(); } public void windowClosed(WindowEvent evt) { frameClosed(); } } ); frame.pack(); frame.setLocation(50,50); frame.show(); launchButton.setLabel("Close Window"); launchButton.setEnabled(true); } else { frame.dispose(); } } } synchronized private void frameClosed() { // respond when separate window closes. frame = null; launchButton.setLabel(launchButtonName); launchButton.setEnabled(true); } /** * Return the applet parameter with a given param name, but if no * such applet param exists, return a default value instead. */ protected String getParameter(String paramName, String defaultValue) { String val = getParameter(paramName); return (val == null)? defaultValue : val; } } // end class ScatterPlotApplet jcm1-source/FamiliesOfGraphs.java0000644000076500011320000003155511741343635016246 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // An applet belonging to the class FamiliesOfGraphs displays a graph // of a function that can depend on one or more parameters. The values of // the parameters are controlled by the user using sliders at the bottom of // the applet. import java.awt.*; import java.applet.Applet; import java.util.*; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; public class FamiliesOfGraphs extends GenericGraphApplet { // Declare some private variables that are created in one method in // this class and used in a second method. private Function func; // The function that is graphed. private Graph1D graph; // The graph of the function. private Vector sliders; // Elements of this vector are the VariableSlider // objects that represent the parameter values. // The sliders are created in the setUpParser() method. protected void setUpParser() { // Override this to add VariableSliders to parser. // Get the data for the sliders from applet params named "Parameter", "Parameter1", ... // The sliders are created and the variables are added to the parser by the // addParameter() method, which is defined below. sliders = new Vector(); int ct = 0; String param = getParameter("Parameter"); if (param == null) { ct++; param = getParameter("Parameter" + ct); } while (true) { if (param == null) break; addParameter(param); ct++; param = getParameter("Parameter" + ct); } // If no parameters were specified in applet params, create one with name "k". if (sliders.size() == 0) addParameter("k"); super.setUpParser(); // Call this last so function definitions // in applet params can use the parameter names // that have just been added to the parser // (even though it's probably not a good idea). // Note that this also defines the independent variable, // whose name is given by the applet param "Variable" // and which is referred to as xVar in this program. VariableSlider slide = (VariableSlider)sliders.elementAt(0); String def = getParameter("Function", "sin(" + slide.getName() + " * " + xVar.getName() + ")"); parameterDefaults = new Hashtable(); // I want to set a different default value for // the "Function" applet param. parameterDefaults.put("Function",def); } // end setUpParser() private void addParameter(String data) { // Create a VariableSlider from the information in name and add it to the // Vector of sliders. The data must contain the name of the variable // associated with the slider. The name can be followed by a ";" and up to // three numbers. (If there is no ";", a space after the name will do.) // The numbers can be separated by commas, spaces, or tabs. The first // number gives the minimum value on the slider, the second gives the maximum, // and the third gives the initial value of the slider variable. double min = -5, max = 5, val = 0; // min, max, and value for slider data = data.trim(); int pos = data.indexOf(';'); if (pos < 0) pos = data.indexOf(' '); String name; // The name of the parameter if (pos < 0) { // If there is no space or ";", the data is just the name of the variable. name = data; } else { // Get the name from the front of the data, then look for min, max, and val. String nums = data.substring(pos+1); name = data.substring(0,pos).trim(); StringTokenizer toks = new StringTokenizer(nums," ,\t"); try { if (toks.hasMoreElements()) min = (new Double(toks.nextToken())).doubleValue(); if (toks.hasMoreElements()) max = (new Double(toks.nextToken())).doubleValue(); if (toks.hasMoreElements()) val = (new Double(toks.nextToken())).doubleValue(); } catch (NumberFormatException e) { min = -5; max = 5; val = 0; } } // Create the slider, adding the associated variable to the parser, and set its value. VariableSlider slide = new VariableSlider(name, new Constant(min), new Constant(max), parser); slide.setVal(val); sliders.addElement(slide); // Save the slider in the array of sliders for later use. } // end setUpParser(); protected void setUpBottomPanel() { // Overridden to add the sliders at the bottom of the applet. super.setUpBottomPanel(); // Do the default setup. // Create a panel holding all the sliders, with a display label for each slider to show its value. JCMPanel sliderPanel = new JCMPanel(); sliderPanel.setLayout(new GridLayout(0,1,3,3)); sliderPanel.setBackground(getColorParam("PanelBackground", Color.lightGray)); for (int i = 0; i < sliders.size(); i++) { JCMPanel p = new JCMPanel(); VariableSlider slide = (VariableSlider)sliders.elementAt(i); p.add(slide, BorderLayout.CENTER); p.add(new DisplayLabel(" " + slide.getName() + " = # ", new Value[] { slide.getVariable() } ), BorderLayout.EAST); sliderPanel.add(p); slide.setOnUserAction(mainController); } // If there is a functionInput box, then the SOUTH position of the mainPanel already contains // the inputPanel that contains that box. If so, add the new panel to the SOUTH position of // the inputPanel. (This is a good place, in general, to put extra input objects.) // If there is no inputPanel, then the SOUTH position of the mainPanel is empty, so put // the newly created panel there. if (inputPanel != null) inputPanel.add(sliderPanel, BorderLayout.SOUTH); else mainPanel.add(sliderPanel, BorderLayout.SOUTH); } // end setUpBottomPanel() protected void setUpCanvas() { // Overridden to add the graph to the canvas. super.setUpCanvas(); // Do the default setup. // When setUpCanvas() is called, the functionInput already exists, if one is // to be used, since it is created in setUpBopttomPanel(), which is called // before setUpCanvas. If functionInput exists, add a graph of the function // from functionInput to the canvas. If not, create a graph of the function // specified by the parameter named "Function" (or use sin(k*x) if none is specified). if (functionInput != null) func = functionInput.getFunction(xVar); else { String def = getParameter("Function"); // default value is set in setUpParser() func = new SimpleFunction( parser.parse(def), xVar ); } // Create a graph of the function and add it to the canvas. graph = new Graph1D(func); graph.setColor(getColorParam("GraphColor", Color.magenta)); canvas.add(graph); } // end setUpCanvas protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For the FamiliesOfGraphs applet, the example string should contain // an expression that defines the function to be graphed. This can optionally // be followed by a semicolon and a list of four or more numbers. // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The // remaining numbers occur in groups of three and specify the minimumn, // maximum and values of the parameters, in the // same order that they were encountered in the setUpParser() method. int pos = example.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // Get limits from example text. String nums = example.substring(pos+1); example = example.substring(0,pos); StringTokenizer toks = new StringTokenizer(nums, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } } int i = 0; while (i < sliders.size() && toks.hasMoreElements()) { // Look for a value for the i-th slider. try { double min = (new Double(toks.nextToken())).doubleValue(); double max = (new Double(toks.nextToken())).doubleValue(); double d = (new Double(toks.nextToken())).doubleValue(); VariableSlider slider = ((VariableSlider)sliders.elementAt(i)); slider.setMin(new Constant(min)); slider.setMax(new Constant(max)); slider.setVal(d); } catch (Exception e) { } i++; } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); } else { // If there is no user input, set the function in the graph directly. try { func = new SimpleFunction( parser.parse(example), xVar ); graph.setFunction(func); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); } // end doLoadExample() } // end class FamiliesOfGraphs jcm1-source/EpsilonDelta.java0000644000076500011320000003464611741343635015452 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ import java.awt.*; import java.applet.Applet; import java.util.StringTokenizer; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; /** * An applet for exploring the epsilon-delta definition of a limit. */ public class EpsilonDelta extends GenericGraphApplet { // Declare some private variables that are created in one method in // this class and used in a second method. private VariableInput xInput; private VariableInput epsilonInput; private VariableInput deltaInput; private VariableInput limitInput; private VariableSlider xSlider; private VariableSlider epsilonSlider; private VariableSlider deltaSlider; private VariableSlider limitSlider; private Controller subController; private Variable xValue, limitValue; private Function func; // The function that is graphed. private Graph1D graph; // The graph of the function. protected void setUpBottomPanel() { super.setUpBottomPanel(); subController = new Controller(); mainController.add(subController); JCMPanel inputs = new JCMPanel(3); subController.add(inputs); inputs.setBackground(getColorParam("PanelBackground", Color.lightGray)); if (inputPanel == null) mainPanel.add(inputs,BorderLayout.SOUTH); else inputPanel.add(inputs,BorderLayout.SOUTH); JCMPanel left = new JCMPanel(0,1,2); JCMPanel right = new JCMPanel(0,1,2); JCMPanel middle = new JCMPanel(0,1,2); inputs.add(middle, BorderLayout.CENTER); inputs.add(left, BorderLayout.WEST); inputs.add(right, BorderLayout.EAST); double[] a = getNumericParam("AValue"); double avalue = (a == null || a.length < 1)? 0 : a[0]; if ("yes".equalsIgnoreCase(getParameter("UseAInput","yes"))) { xSlider = new VariableSlider(); xInput = new VariableInput(); xInput.setVal(avalue); xSlider.setVal(avalue); xInput.setThrowErrors(false); subController.add(new Tie(xSlider, xInput)); xValue = xInput.getVariable(); left.add(new Label("limit at a = ",Label.RIGHT)); middle.add(xSlider); right.add(xInput); } else { xValue = new Variable(); xValue.setVal(avalue); } a = getNumericParam("LimitValue"); double Lvalue = (a == null || a.length < 1)? 1 : a[0]; if ("yes".equalsIgnoreCase(getParameter("UseLimitInput","yes"))) { limitSlider = new VariableSlider(); limitInput = new VariableInput(); limitInput.setVal(Lvalue); limitSlider.setVal(Lvalue); limitInput.setThrowErrors(false); subController.add(new Tie(limitSlider, limitInput)); limitValue = limitInput.getVariable(); left.add(new Label(" test limit L = ",Label.RIGHT)); middle.add(limitSlider); right.add(limitInput); } else { limitValue = new Variable(); limitValue.setVal(Lvalue); } a = getNumericParam("EpsilonValue"); double epsilonValue = (a == null || a.length < 1)? 0.25 : a[0]; epsilonSlider = new VariableSlider(new Constant(0), new Constant(2)); epsilonInput = new VariableInput(); epsilonInput.setVal(epsilonValue); epsilonSlider.setVal(epsilonValue); epsilonInput.setThrowErrors(false); subController.add(new Tie(epsilonSlider, epsilonInput)); left.add(new Label("epsilon = ", Label.RIGHT)); middle.add(epsilonSlider); right.add(epsilonInput); a = getNumericParam("DeltaValue"); double deltaValue = (a == null || a.length < 1)? 1 : a[0]; deltaSlider = new VariableSlider(new Constant(0), new Constant(2)); deltaInput = new VariableInput(); deltaInput.setVal(deltaValue); deltaSlider.setVal(deltaValue); deltaInput.setThrowErrors(false); subController.add(new Tie(deltaSlider, deltaInput)); left.add(new Label("delta = ", Label.RIGHT)); middle.add(deltaSlider); right.add(deltaInput); } protected void setUpCanvas() { // Override this to add more stuff to the canvas. // When setUpCanvas is called, the functionInput already exists, if one is // to be used, since it is created in setUpBopttomPanel(), which is called // before setUpCanvas. If functionInput exists, add a graph of the function // from functionInput to the canvas. If not, create a graph of the function // specified by the parameter named "Function". if (functionInput != null) func = functionInput.getFunction(xVar); else { String def = getParameter("Function", "abs(" + xVar.getName() + ") ^ " + xVar.getName()); Function f = new SimpleFunction( parser.parse(def), xVar ); func = new WrapperFunction(f); } graph = new Graph1D(func); graph.setColor(getColorParam("GraphColor", Color.black)); Value xMinusDelta = new ValueMath(xValue, deltaInput, '-'); Value xPlusDelta = new ValueMath(xValue, deltaInput, '+'); Value limitMinusEpsilon = new ValueMath(limitValue, epsilonInput, '-'); Value limitPlusEpsilon = new ValueMath(limitValue, epsilonInput, '+'); Value xmin = canvas.getCoordinateRect().getValueObject(CoordinateRect.XMIN); Value xmax = canvas.getCoordinateRect().getValueObject(CoordinateRect.XMAX); Value ymin = canvas.getCoordinateRect().getValueObject(CoordinateRect.YMIN); Value ymax = canvas.getCoordinateRect().getValueObject(CoordinateRect.YMAX); if (xSlider != null) { xSlider.setMin(xmin); xSlider.setMax(xmax); } if (limitSlider != null) { limitSlider.setMin(ymin); limitSlider.setMax(ymax); } DrawGeometric deltaBox = new DrawGeometric(DrawGeometric.RECT_ABSOLUTE, xMinusDelta, ymin, xPlusDelta, ymax); deltaBox.setFillColor(new Color(225,255,225)); deltaBox.setLineWidth(0); DrawGeometric epsilonBox = new DrawGeometric(DrawGeometric.RECT_ABSOLUTE, xmin, limitMinusEpsilon, xmax, limitPlusEpsilon); epsilonBox.setFillColor(new Color(255,230,230)); epsilonBox.setLineWidth(0); DrawGeometric overlap = new DrawGeometric(DrawGeometric.RECT_ABSOLUTE, xMinusDelta, limitMinusEpsilon, xPlusDelta,limitPlusEpsilon); overlap.setFillColor(new Color(255,255,225)); overlap.setColor(Color.yellow); DrawGeometric xLine = new DrawGeometric(DrawGeometric.LINE_ABSOLUTE, xValue, ymin, xValue, ymax); xLine.setColor(new Color(130,255,130)); DrawGeometric limitLine = new DrawGeometric(DrawGeometric.LINE_ABSOLUTE, xmin, limitValue, xmax, limitValue); limitLine.setColor(new Color(255,150,150)); canvas.add(deltaBox); canvas.add(epsilonBox); canvas.add(overlap); canvas.add(xLine); canvas.add(limitLine); DrawString ds = new DrawString("a = #\nL = #\nf(a) = #", DrawString.TOP_LEFT, new Value[] { xValue, limitValue, new ValueMath(func,xValue) }); ds.setBackgroundColor(Color.white); ds.setFrameWidth(1); subController.add(ds); subController.add(deltaBox); subController.add(epsilonBox); subController.add(overlap); subController.add(xLine); subController.add(limitLine); mainController.remove(canvas); mainController.add(graph); canvas.getCoordinateRect().setOnChange(mainController); deltaSlider.setOnUserAction(subController); epsilonSlider.setOnUserAction(subController); deltaInput.setOnTextChange(subController); epsilonInput.setOnTextChange(subController); subController.add(deltaSlider); subController.add(epsilonSlider); subController.add(deltaInput); subController.add(epsilonInput); if (xInput != null) { xSlider.setOnUserAction(subController); xInput.setOnTextChange(subController); subController.add(xSlider); subController.add(xInput); } if (limitInput != null) { limitSlider.setOnUserAction(subController); limitInput.setOnTextChange(subController); subController.add(limitSlider); subController.add(limitInput); } super.setUpCanvas(); // Do the common setup: Add the axes, grid, etc canvas.add(graph); canvas.add(ds); } // end setUpCanvas() protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // After the function definition, there can be a semicolon and // up to ten numbers (numbers can be separated by spaces and/or commas). // The first four numbers specify the limits on the coordinate rect. // . int pos = example.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // get limits from example text String limitsText = example.substring(pos+1); example = example.substring(0,pos); StringTokenizer toks = new StringTokenizer(limitsText, " ,"); double nums[] = new double[toks.countTokens()]; for (int i = 0; i < nums.length; i++) { try { nums[i] = (new Double(toks.nextToken())).doubleValue(); } catch (Exception e) { nums[i] = Double.NaN; } } for (int i = 0; i < 4; i++) if (nums.length >= i && !Double.isNaN(nums[i])) limits[i] = nums[i]; if (nums.length > 4 && !Double.isNaN(nums[4])) xValue.setVal( nums[4] ); else xValue.setVal((limits[0]+limits[1])/2); if (nums.length > 5 && !Double.isNaN(nums[5])) limitValue.setVal( nums[5] ); else limitValue.setVal((limits[0]+limits[1])/2); if (nums.length > 8 && !Double.isNaN(nums[8])) epsilonSlider.setMax( new Constant(nums[8]) ); else epsilonSlider.setMax(new Constant(Math.abs(limits[2]-limits[3])/2)); if (nums.length > 9 && !Double.isNaN(nums[9])) deltaSlider.setMax( new Constant(nums[9]) ); else deltaSlider.setMax(new Constant(Math.abs(limits[0]-limits[1])/2)); if (nums.length > 6 && !Double.isNaN(nums[6])) { epsilonInput.setVal( nums[6] ); epsilonSlider.setVal( nums[6] ); } else { epsilonInput.setVal(Math.abs(limits[2]-limits[3])/8); epsilonSlider.setVal(Math.abs(limits[2]-limits[3])/8); } if (nums.length > 7 && !Double.isNaN(nums[7])) { deltaInput.setVal( nums[7] ); deltaSlider.setVal( nums[7] ); } else { deltaInput.setVal(Math.abs(limits[0]-limits[1])/8); deltaSlider.setVal(Math.abs(limits[0]-limits[1])/8); } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); } else { // If there is no user input, set the function in the graph directly. // Also, in this case, func is a "WrapperFunction". Set the // definition of that WrapperFunction to be the same as f try { Function f = new SimpleFunction( parser.parse(example), xVar ); ((WrapperFunction)func).setFunction(f); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); } // end doLoadExample() } // end class EpsilonDelta jcm1-source/GenericGraphApplet.java0000644000076500011320000011277311741343635016571 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ import java.awt.*; import java.awt.event.*; import java.applet.*; import java.util.*; import edu.hws.jcm.data.*; import edu.hws.jcm.draw.*; import edu.hws.jcm.awt.*; import edu.hws.jcm.functions.*; /** * GenericGraphApplet serves as a base class for applets that have a DisplayCanvas in the CENTER of * a BorderLayout and that are configurable by a large number of applet parameters. This base class * sets up the basic structure of the applet and processes many applet parameters. Most of the * work is done in methods that can be overridden in subclasses. If the height of * the applet is greater than 100 and if the applet parameter "LaunchButtonName" is not defined, * then the main panel of the applet appears in the applet itself. Otherwise, * the applet appears as a button. Clicking the button opens the main panel of the * applet in a separate, resizable frame. */ public class GenericGraphApplet extends Applet implements ActionListener, ItemListener { /** * The parser which is used for parsing input from the functionInput box. */ protected Parser parser; /** * The main panel of the applet, containing a display canvas and other components. */ protected JCMPanel mainPanel; /** * The Controller from the mainPanel. */ protected Controller mainController; /** * The canvas for displaying axes, graphs, etc. */ protected DisplayCanvas canvas; /** * Controls the limits of the x- and y-coordinates on the canvas. */ protected LimitControlPanel limitsPanel; /** * An input box for inputting an expression. */ protected ExpressionInput functionInput; /** * The variable that is created and added to the parser. The default name of this * variable is "x", but that can be changed using the "Variable" applet param. * The variable is created in the setUpParser() method. */ protected Variable xVar; /** * A panel containing the functionInput. It appears at the bottom of the main panel. */ protected JCMPanel inputPanel; /** * A panel containing examples specified in applet parameters, if any. It appears * at the top of the main panel. */ protected JCMPanel exampleMenuPanel; /** * A button that the user presses to recompute the display. (Ths user can also just * press return in one of the input boxes.) */ protected Button computeButton; /** * The string that is used as the title of the frame, if the applet appears as * a launch button. This title is also used on the launch button, unless the * applet parameter "LaunchButtonName" has a value. This is set in the init() method. */ protected String frameTitle; /** * The default size for the frame when the applet runs a launch button. * Can be overidden by the FrameSize applet param. */ protected int[] defaultFrameSize = { 550, 400} ; /** * A hash table that, if non-null, can hold values for applet params. While this is * not set to a non-null value in this class, its value can be set in the setUpAppletDefaults() * method. The values in this hash table will be returned by the getParameter() method * when no value is provided in the applet. For example, this class uses a default value * of "s" vor the the param "Variable". A subclass could put a different value in the * parameterDefaults hash table, and this value will be used in preference to "x". */ protected Hashtable parameterDefaults; /** * The init() method is called by the system to set up the applet. * If the applet does not appear as a button, then init() creates the main panel of the applet * and calls setUpMainPanel to set it up. If any error occurs during calls to setUpParser(), * setUpExampleMenu(), or setUpMainPanel(), then the applet will just show an error message, * and stack trace will be printed to standard output. */ public void init() { setUpParameterDefaults(); frameTitle = getParameter("FrameTitle"); if (frameTitle == null) { frameTitle = this.getClass().getName(); int pos = frameTitle.lastIndexOf('.'); if (pos > -1) frameTitle = frameTitle.substring(pos+1); } setLayout(new BorderLayout()); int height = getSize().height; launchButtonName = getParameter("LaunchButtonName"); if ( (height > 0 && height < 100) || launchButtonName != null) { if (launchButtonName == null) launchButtonName = "Launch " + frameTitle; launchButton = new Button(launchButtonName); add(launchButton, BorderLayout.CENTER); launchButton.addActionListener(this); } else { mainPanel = new JCMPanel(); try { setUpMainPanel(); add(mainPanel, BorderLayout.CENTER); } catch (Exception e) { System.out.println("Error while opening applet:"); e.printStackTrace(); TextArea message = new TextArea("An error occurred while setting up the applet:\n\n"); message.append(e.toString()); add(message, BorderLayout.CENTER); } } } /** * This method is called to set up the main panel of the applet. The main panel object, of type JCMPanel, * already exists in the variable mainPanel when it is called. It begins by calling setUpParser() * and setUpExampleMenu(). This method processes applet parameters * "Insets", "BackgroundColor", and "ForegroundColor" to set the gap and colors of the panel. It * creates a DisplayCanvas and, if the applet parameter "UseLimitsPanel" is not equal to "no", it * creates a LimitControlPanel. The coordinate limits are set using the "Limits" applet param, if * presetn. This method assigns the Controller from the mainPanel to the member variable * mainController. It then calls setUpBottomPanel(), setUpTopPanel(), setUpCanvas(), addCanvasBorder(), * and, if there is a limits panel, setUpLimitsPanel(), in that order. Finally, if the value * of the applet parameter "LoadFirstExample" is not "no", and if any examples were specifed, then * the first ewxample is loaded. */ protected void setUpMainPanel() { parser = new Parser(null,0); setUpParser(); setUpExampleMenu(); double[] gap = getNumericParam("Insets"); if (gap == null || gap.length == 0 || gap[0] < 0 || gap[0] > 50) mainPanel.setInsetGap(3); else mainPanel.setInsetGap( (int)Math.round(gap[0]) ); Color color; color = getColorParam("BackgroundColor", Color.gray); mainPanel.setBackground(color); color = getColorParam("ForegroundColor", Color.black); mainPanel.setForeground(color); canvas = new DisplayCanvas(); double[] limits = getNumericParam("Limits"); if (limits != null && limits.length >= 4) canvas.getCoordinateRect().setLimits(limits); if ( "yes".equalsIgnoreCase(getParameter("UseLimitsPanel", "yes")) ) { String h = getParameter("XName"); if (h == null) h = getParameter("Variable","x"); String v =getParameter("YName","y"); limitsPanel = new LimitControlPanel( h+"min", h+"max", v+"min", v+"max", 0, false ); } mainController = mainPanel.getController(); setUpBottomPanel(); setUpTopPanel(); setUpCanvas(); addCanvasBorder(); if ( limitsPanel != null ) setUpLimitsPanel(); String loadDefault = (loadExampleButton == null)? "yes" : "no"; if (exampleStrings != null && exampleStrings.size() > 0 && ! "no".equalsIgnoreCase(getParameter("LoadFirstExample",loadDefault))) doLoadExample( (String)exampleStrings.elementAt(0) ); } /** * This method is called by mainPanel() to set up the display canvas and add it to the * main panel. The canvas already exists in the member variable canvas when this * method is called. This method adds only a set of axes to the canvas, sets the * mainController to report errors using the canvas, and adds the canvas to the * CENTER of the main panel. This method processes applet parameters "UsePanner", * "CanvasColor", "UseMouseZoom", and "UseOffscreenCanvas". Typically, this * method will be overridden in subclasses to add more Drawable items to the * canvas. In this case, super.setUpCanvas() should be called first. */ protected void setUpCanvas() { Color color; color = getColorParam("CanvasColor"); if (color != null) canvas.setBackground(color); if (! "no".equalsIgnoreCase(getParameter("UsePanner", "no")) ) canvas.add(new Panner()); if ( ! "no".equalsIgnoreCase(getParameter("UseGrid", "no")) ) { Grid g = new Grid(); color = getColorParam("GridColor"); if (color != null) g.setColor(color); canvas.add(g); } canvas.add(makeAxes()); if ( ! "no".equalsIgnoreCase(getParameter("UseMouseZoom", "no")) ) canvas.setHandleMouseZooms(true); if ( "yes".equalsIgnoreCase(getParameter("UseOffscreenCanvas", "yes")) ) canvas.setUseOffscreenCanvas(true); mainController.setErrorReporter(canvas); mainPanel.add(canvas, BorderLayout.CENTER); } /** * Construct a set of Axes, based on applet params "AxesColor", "AxesLightColor", * "XLabel", "YLabel", "LabelColor". */ protected Axes makeAxes() { Axes axes = new Axes(); Color color = getColorParam("AxesColor"); if (color != null) axes.setAxesColor(color); color = getColorParam("AxesLightColor"); if (color != null) axes.setLightAxesColor(color); String str = getParameter("XLabel"); if (str != null) axes.setXLabel(str); str = getParameter("YLabel"); axes.setYLabel(str); color = getColorParam("LabelColor"); if (color != null) axes.setLabelColor(color); return axes; } /** * This method is called by setUpMainPanel() to add a border to the canvas (since the border is typically * the last thing that should be added, on top of anything else in the canvas. It processes the * applet parameters "BorderWidth" and "BorderColor". If the border width is zero, no border is added. * (The default width is 2.) */ protected void addCanvasBorder() { int borderWidth; double[] bw = getNumericParam("BorderWidth"); if (bw == null || bw.length == 0 || bw[0] > 25) borderWidth = 2; else borderWidth = (int)Math.round(bw[0]); if (borderWidth > 0) canvas.add( new DrawBorder( getColorParam("BorderColor", Color.black), borderWidth ) ); } /** * This method checks the applet parameter "UseFunctionInput". If the value is anything but "no", * then a panel is created that contains an ExpressionInput (stored in the member variable functionInput) * and possibly a ComputeButton and lable for the input box. This panel is a JCMPanel using BorderLayout. * In is stored in the member variable inputPanel and is added to the SOUTH position of the mainPanel. * The method also processes applet parameters "Function", "UseComputeButton", "ComputeButtonName", and * "FunctionLabel". The ComputeButton, if it exists, is stored in the member variable computeButton. * Note that nothing at all is done by this method if the value of the applet parameter "UseFunctionInput" is no. * */ protected void setUpBottomPanel() { if ( "yes".equalsIgnoreCase(getParameter("UseFunctionInput", "yes")) ) { String func = getParameter("Function"); String varName = xVar.getName(); if (func == null) func = "abs(" + varName + " ) ^ " + varName; functionInput = new ExpressionInput(func,parser); inputPanel = new JCMPanel(); inputPanel.setBackground( getColorParam("PanelBackground", Color.lightGray) ); inputPanel.add(functionInput, BorderLayout.CENTER); if ( "yes".equalsIgnoreCase(getParameter("UseComputeButton", "yes")) ) { String cname = getParameter("ComputeButtonName", "New Function"); computeButton = new Button(cname); inputPanel.add(computeButton, BorderLayout.EAST); computeButton.addActionListener(this); } String flabel = getParameter("FunctionLabel"); if (flabel == null) flabel = " f(" + varName + ") = "; if ( ! "none".equalsIgnoreCase(flabel) ) inputPanel.add(new Label(flabel), BorderLayout.WEST); mainPanel.add(inputPanel, BorderLayout.SOUTH); functionInput.setOnUserAction(mainPanel.getController()); } } /** * This method sets up the limit control panel and adds it to the main panel. The limit control panel * already exists when this method is called and is stored in the member variable limitsPanel. The * applet parameters "TwoLimitsColumns", "UseSetLimitsButton", "UseZoomButtons", "UseEqualizeButtons", * "UseRestoreButton", "PanelBackground", and "LimitsOnLeft" are processed. The limits panel is * set to report its errors using the display canvas. */ protected void setUpLimitsPanel() { limitsPanel.addCoords(canvas); if ( ! "no".equalsIgnoreCase(getParameter("TwoLimitsColumns", "no")) ) limitsPanel.setUseTwoColumnsIfPossible(true); int buttons = 0; if ( "yes".equalsIgnoreCase(getParameter("UseSetLimitsButton", "yes")) ) buttons |= LimitControlPanel.SET_LIMITS; if ( ! "no".equalsIgnoreCase(getParameter("UseZoomButtons", "no")) ) buttons |= LimitControlPanel.ZOOM_IN | LimitControlPanel.ZOOM_OUT; if ( ! "no".equalsIgnoreCase(getParameter("UseEqualizeButton", "no")) ) buttons |= LimitControlPanel.EQUALIZE; if ( ! "no".equalsIgnoreCase(getParameter("UseRestoreButton", "no")) ) buttons |= LimitControlPanel.RESTORE; if (buttons != 0) limitsPanel.addButtons(buttons); limitsPanel.setBackground( getColorParam("PanelBackground", Color.lightGray) ); if ( ! "yes".equalsIgnoreCase(getParameter("LimitsOnLeft", "no"))) mainPanel.add(limitsPanel, BorderLayout.EAST); else mainPanel.add(limitsPanel, BorderLayout.WEST); limitsPanel.setErrorReporter(canvas); } /** * This method is called by setUpMainPanel() to add a panel at the top of the applet. * If there is an example menu, then the panel containing that menu is added to the * NORTH position of the main panel. Otherwise, the value of the applet parameter * named "PanelTitle" is checked. If this value exists, it is placed in a label at the top of the panel. * The color of the text on the label is given by the valur of the applet parameter * "TitleColor" (default is new Color(200,0,0)). */ protected void setUpTopPanel() { if (exampleMenuPanel != null) mainPanel.add(exampleMenuPanel, BorderLayout.NORTH); else { String title = getParameter("PanelTitle"); if (title != null) { Label titleLabel = new Label(title, Label.CENTER); titleLabel.setForeground( getColorParam("TitleForeground", new Color(200,0,0)) ); titleLabel.setBackground( getColorParam("TitleBackground", Color.lightGray) ); titleLabel.setFont( new Font("Serif", Font.PLAIN, 14) ); mainPanel.add(titleLabel, BorderLayout.NORTH); } } } /** * This method processes applet parameters that specify examples for the applet and adds them * to a menu of examples. If any examples exist, they are placed in a menu and a panel * is created to hold the menu. The panel is stored in the instance variable exampleMenuPanel, * which is used in the setUpTopPanel() method. This method is called by init(). If you don't * want to process examples, you could override it to do nothing. If you do want to process * examples, you have to override the doLoadExample() method to process a string from the menu. * Examples are specified in applet parameters named "Example", "Example1", "Example2", .... * The value should consist of a name for the example (which appears in the menu) followed by * a ";" followed by a string that defines the example. The string that defines the example * is passed to the doLoadExample() method when the user loads the example. */ protected void setUpExampleMenu() { Vector strings = new Vector(); Vector names = new Vector(); int ct = 0; String paramName = "Example"; String param = getParameter(paramName); if (param == null) { ct++; paramName = "Example" + ct; param = getParameter(paramName); } while (true) { if (param == null) break; int pos = param.indexOf(';'); if (pos < 0) { strings.addElement(param); names.addElement(param); } else { strings.addElement(param.substring(pos+1)); names.addElement(param.substring(0,pos)); } ct++; paramName = "Example" + ct; param = getParameter(paramName); } if (strings.size() == 0) return; exampleStrings = strings; exampleStrings.trimToSize(); exampleMenuPanel = new JCMPanel(); if ("yes".equalsIgnoreCase(getParameter("UseLoadButton","yes"))) { loadExampleButton = new Button("Load Example: "); loadExampleButton.setBackground(Color.lightGray); loadExampleButton.addActionListener(this); } Component list; if (names.size() == 1) list = new Label( (String)names.elementAt(0), Label.CENTER ); else { exampleMenu = new Choice(); list = exampleMenu; for (int i = 0; i < names.size(); i++) exampleMenu.add( (String)names.elementAt(i) ); if (loadExampleButton == null) exampleMenu.addItemListener(this); } list.setBackground(Color.white); exampleMenuPanel.add(list, BorderLayout.CENTER); if (loadExampleButton != null) exampleMenuPanel.add(loadExampleButton, BorderLayout.WEST); } /** * This method is called by setUpMainPanel() to set up the parser to be used in the applet. * When it is called, a parser already esists and is stored in the member variable * named parser. This method configures the parser according to the values of * the applet parameters "StandardFunctions", "Booleans", "OptionalStars", "OptionalParens", * and "Factorials". It then looks for function defintions in applet parameters named * "Define", "Define1", "Define2", ...., and if any are found the functions are added * to the parser so that they can be used in expressions. A function definition can * take a form such as "g(x)=x^2" or "fred(s,t) = 3*s + sin(t)", for example, or it * can be defined by a table of values. A table function is defined using the * syntax decribed in the parseTableFuncDef() method. Finally, a variable is * created and added to the parser using the value of the applet param "Variable", * with "x" as the default variable name. */ protected void setUpParser() { if ( "yes".equalsIgnoreCase(getParameter("StandardFunctions", "yes")) ) parser.addOptions(Parser.STANDARD_FUNCTIONS); if ( "yes".equalsIgnoreCase(getParameter("Booleans", "yes")) ) parser.addOptions(Parser.BOOLEANS); if ( ! "no".equalsIgnoreCase(getParameter("OptionalStars", "no")) ) parser.addOptions(Parser.OPTIONAL_STARS); if ( ! "no".equalsIgnoreCase(getParameter("OptionalParens", "no")) ) parser.addOptions(Parser.OPTIONAL_PARENS); if ( ! "no".equalsIgnoreCase(getParameter("Factorials", "no")) ) parser.addOptions(Parser.FACTORIAL); if ( "yes".equalsIgnoreCase(getParameter("Summations", "yes")) ) parser.add(new SummationParser()); String str = getParameter("Define"); if (str != null) define(str); int ct = 1; while (true) { str = getParameter("Define" + ct); if (str == null) break; define(str); ct++; } xVar = new Variable( getParameter("Variable", "x") ); parser.add( xVar ); } /** * This method is called when the user loads an example from the example menu (if any). * The parameter is the string that defines the example. By default, this method does * nothhing. It should be overridden to load the example. */ protected void doLoadExample(String example) { } /** * This method, which is empty in the GenericGraphApplet class, can be defined in a subclass * to set default values for applet params that are different from the ones provided in * this class. The method should create a new HashTable, assign it to the instance * variable parameterDefaults, and then add name/value pairs to the hash table. This * method is called at the very beginning of the init() method. */ protected void setUpParameterDefaults() { } /** * Override the standard applet method getParameter(String) so that when no param value * is provided in the applet tag, and if parameterDefaults is non-null, it will check for a value * the parameterDefaults. (The parameterDefaults instance variable can be defined in the * setUpParameterDefaults() method.) */ public String getParameter(String paramName) { String val = super.getParameter(paramName); if (val == null && parameterDefaults != null) val = (String)parameterDefaults.get(paramName); return val; } /** * Get the value of an applet parameter, but return a default if the value is null. * * @param paramName The name of the applet parameter. * @param defaultValue The value to be returned if getParameter(paramName) is null. */ protected String getParameter(String paramName, String defaultValue) { String val = getParameter(paramName); return val == null ? defaultValue : val; } /** * Get The value of an applet parameter that consists of a list of numbers. * The parameter value, if any, is parsed and returned as array of double values. * The numbers can be separated by commas, spaces, tabs, or semicolons. If there * is a parse error, null is returned. */ protected double[] getNumericParam(String paramName) { return getNumericParam(paramName, null); } /** * Get The value of an applet parameter that consists of a list of numbers. * The parameter value, if any, is parsed and returned as array of double values. * The numbers can be separated by commas, spaces, tabs, or semicolons. * * @param paramName The name of the applet parameter. * @param defaultValue The value to be returned if getParameter(paramName) is null or is not a valid list of numbers. */ protected double[] getNumericParam(String paramName, double[] defaults) { String data = getParameter(paramName); if (data == null) return defaults; StringTokenizer tokenizer = new StringTokenizer(data," \t,;"); int count = tokenizer.countTokens(); if (count == 0) return defaults; double[] numbers = new double[count]; for (int i = 0; i < count; i++) { try { Double d = new Double(tokenizer.nextToken()); numbers[i] = d.doubleValue(); } catch (NumberFormatException e) { return defaults; } } return numbers; } /** * Get The value of an applet parameter that specifies a color. The color can be specfied * as a list of three numbers in the range 0 to 255 or by one of the standard color names * ("black", "red", "blue", "green", "yellow", "cyan", "magenta", "gray", "darkgray", * "lightgray", "pink", "orange", "white"). Color names are not case sensitive. If * the value of the applet parameter is null does not specify a legal color, then * the return value is null. */ protected Color getColorParam(String data) { return getColorParam(data,null); } /** * Get The value of an applet parameter that specifies a color. The color can be specfied * as a list of three numbers in the range 0 to 255 or by one of the standard color names * ("black", "red", "blue", "green", "yellow", "cyan", "magenta", "gray", "darkgray", * "lightgray", "pink", "orange", "white"). Color names are not case sensitive. * * @param paramName The name of the applet parameter. * @param defaultColor The value to be returned if getParameter(paramName) is null or is not a valid color. */ protected Color getColorParam(String paramName, Color defaultColor) { String data = getParameter(paramName); if (data == null || data.trim().length() == 0) return defaultColor; data = data.trim(); if (Character.isLetter(data.charAt(0))) { for (int i = 0; i < colorNames.length; i++) if (data.equalsIgnoreCase(colorNames[i])) return colors[i]; return defaultColor; } else { double[] nums = getNumericParam(paramName,null); if (nums == null || nums.length < 3) return defaultColor; if (nums[0] < 0 || nums[0] > 255 || nums[1] < 0 || nums[1] > 255 || nums[2] < 0 || nums[2] > 255) return defaultColor; return new Color((int)Math.round(nums[0]), (int)Math.round(nums[1]), (int)Math.round(nums[2])); } } // ------------------ Implementation details -------------------------------- /** * Releases the resources used by the display canvas when the applet is stopped. */ public void stop() { if (canvas != null && frame == null) canvas.releaseResources(); } /** * Closes the frame (if any) when the applet is destroyed. */ public synchronized void destroy() { if (frame != null) frame.dispose(); } private Choice exampleMenu; private Button loadExampleButton; private Button launchButton; private String launchButtonName; private Frame frame; private Vector exampleStrings; private String[] colorNames = { "black", "red", "blue", "green", "yellow", "cyan", "magenta", "gray", "darkgray", "lightgray", "pink", "orange", "white" }; private Color[] colors = { Color.black, Color.red, Color.blue, Color.green, Color.yellow, Color.cyan, Color.magenta, Color.gray, Color.darkGray, Color.lightGray, Color.pink, Color.orange, Color.white }; private void define(String str) { // Parse a function definition string from one of the applet parameters named // "Define", "Define1", "Define2", ... String funcDef = str; String name, def; String[] paramNames; int pos; try { pos = funcDef.indexOf("="); if (pos < 0) throw new ParseError("Missing \"=\"", null); def = funcDef.substring(pos+1).trim(); funcDef = funcDef.substring(0,pos); if (def.toLowerCase().startsWith("table")) { name = funcDef; pos = name.indexOf("("); if (pos > 0) name = name.substring(0,pos).trim(); TableFunction tf = parseTableFuncDef(def); tf.setName(name); parser.add(tf); } else { pos = funcDef.indexOf("("); if (pos < 0) throw new ParseError("Missing \"(\"", null); name = funcDef.substring(0,pos).trim(); if (name.length() == 0) throw new ParseError("Missing function name", null); funcDef = funcDef.substring(pos+1); pos = funcDef.indexOf(")"); if (pos < 0) throw new ParseError("Missing \")\"", null); funcDef = funcDef.substring(0,pos).trim(); if (funcDef.length() == 0) throw new ParseError("Missing parameter names", null); StringTokenizer toks = new StringTokenizer(funcDef,","); int ct = toks.countTokens(); paramNames = new String[ct]; for (int i = 0; i < ct; i++) paramNames[i] = toks.nextToken(); new ExpressionFunction(name, paramNames, def, parser); } } catch (ParseError e) { throw new IllegalArgumentException("Error parsing function \"" + str + "\":" + e.getMessage()); } } /** * Create a TableFunction from a string. The string can start with the word "table", * which is ignored. The next item can optionally be one of the table styles "smooth", * "linear", or "step". The default is "smooth". Then values must be specified. If the * next word is "intervals", it can be followed by numbers giving the number of * intervals, the minumum x, and the maximum x, and the y-values for up to (intervals+1) points * evenly distributed between xmin and xmax (unassigned y-values will be zero). * If no data is specified, a table function with * 6 intervals between xmin = -5 and xmax = 5 and all y-values zero is created. * If "intervals" is not specified, the * remaining items are numbers giving pairs of (x,y)-values. Items can be separated * spaces, tabs, and commas. A ParseError will be thrown if the data is illegal. */ protected TableFunction parseTableFuncDef(String def) { try { TableFunction func = new TableFunction(); StringTokenizer toks = new StringTokenizer(def, " \t,"); String tok = null; if (toks.hasMoreTokens()) { tok = toks.nextToken(); if (tok.equalsIgnoreCase("table") && toks.hasMoreTokens()) tok = toks.nextToken(); } if ("step".equalsIgnoreCase(tok)) { func.setStyle(TableFunction.STEP); if (toks.hasMoreTokens()) tok = toks.nextToken(); } else if ("linear".equalsIgnoreCase(tok)) { func.setStyle(TableFunction.PIECEWISE_LINEAR); if (toks.hasMoreTokens()) tok = toks.nextToken(); } else if ("smooth".equalsIgnoreCase(tok) && toks.hasMoreTokens()) { if (toks.hasMoreTokens()) tok = toks.nextToken(); } boolean useIntervals = "intervals".equalsIgnoreCase(tok); if (useIntervals && toks.hasMoreTokens()) tok = toks.nextToken(); double[] nums = new double[toks.countTokens() + 1]; try { nums[0] = (new Double(tok)).doubleValue(); } catch (NumberFormatException e) { throw new ParseError("Unexpected token \"" + tok + "\".",null); } try { for (int i = 1; i < nums.length; i++) nums[i] = (new Double(toks.nextToken())).doubleValue(); } catch (NumberFormatException e) { throw new ParseError("Illegal number.", null); } if (useIntervals) { int ct = (nums.length == 0)? 6 : (int)Math.round(nums[0]); if (ct < 1 || ct > 500) ct = 6; double xmin = (nums.length < 2)? -5 : nums[1]; double xmax = (nums.length < 3)? xmin + 10 : nums[2]; if (xmax <= xmin) throw new ParseError("xmax in table must be greater than xmin", null); func.addIntervals(ct,xmin,xmax); for (int i = 3; i < nums.length && (i-3) <= ct; i++) { if (i-3 < ct) func.setY(i-3,nums[i]); } } else { if (nums.length < 4) throw new ParseError("At least two points must be provided for table function.", null); if (nums.length % 2 == 1) throw new ParseError("Can't define an table function with an odd number of values.", null); for (int i = 0; i < nums.length/2; i++) { func.addPoint(nums[2*i],nums[2*i+1]); } } return func; } catch (Exception e) { throw new ParseError("Error while parsing table function: " + e.getMessage(), null); } } /** * Respond when user clicks a button; not meant to be called directly. */ public void actionPerformed(ActionEvent evt) { Object source = evt.getSource(); if (source == computeButton && computeButton != null) mainController.compute(); else if (source == launchButton && launchButton != null) doLaunchButton(); else if (source == loadExampleButton && exampleStrings != null) { if (exampleStrings.size() == 1) doLoadExample( (String)exampleStrings.elementAt(0) ); else doLoadExample( (String)exampleStrings.elementAt(exampleMenu.getSelectedIndex()) ); } } /** * Respond when user chooses an example from the example menu. (This will only * happen if the param UseLoadButton is not set to "yes".) */ public void itemStateChanged(ItemEvent evt) { if (evt.getSource() == exampleMenu) doLoadExample( (String)exampleStrings.elementAt(exampleMenu.getSelectedIndex()) ); } private synchronized void doLaunchButton() { launchButton.setEnabled(false); if (frame == null) { frame = new Frame(frameTitle); mainPanel = new JCMPanel(); try { setUpMainPanel(); frame.add(mainPanel, BorderLayout.CENTER); } catch (Throwable e) { System.out.println("Error while opening window:"); e.printStackTrace(); TextArea message = new TextArea("An error occurred while setting up this window:\n\n"); message.append(e.toString()); frame.add(message, BorderLayout.CENTER); } frame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { frame.dispose(); } public void windowClosed(WindowEvent evt) { frameClosed(); } } ); double[] frameSize = getNumericParam("FrameSize"); if (frameSize == null || frameSize.length < 2 || frameSize[0] < 100 || frameSize[0] > 800 || frameSize[1] < 100 || frameSize[1] > 600) frame.setSize(defaultFrameSize[0], defaultFrameSize[1]); else frame.setSize((int)Math.round(frameSize[0]), (int)Math.round(frameSize[1])); frame.setLocation(50,50); frame.show(); launchButton.setLabel("Close Window"); launchButton.setEnabled(true); } else { frame.dispose(); } } private synchronized void frameClosed() { frame = null; launchButton.setLabel(launchButtonName); launchButton.setEnabled(true); mainPanel = null; canvas = null; limitsPanel = null; inputPanel = null; exampleMenuPanel = null; loadExampleButton = null; computeButton = null; parser = null; } } // end class GenericGraphApplet jcm1-source/IntegralCurves.java0000644000076500011320000006212311741343635016013 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // This applet displays a vector field (f1(x,y),f2(x,y)) and integral curves // for that vector field (although the integral curve feature can be turned off // with an applet param). The drawing of the curves is animated; they are // drawn segment-by-segment. In the default setup, a curve is started when the // user clicks on the canvas. A curve can also be started by entering the // starting x and y coords in a pair of text input boxes and clicking a button. import java.awt.*; import java.awt.event.*; import java.applet.Applet; import java.util.*; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; public class IntegralCurves extends GenericGraphApplet { private Variable yVar; // The seond variable, usually y. private Function xFunc,yFunc; // The functions that give the components of the vector field private ExpressionInput functionInput2; // For inputting yFunc. private VectorField field; // The vector/direction field private Animator animator; // for incrementally drawing integral curves. private Vector curves = new Vector(); // Holds the integral curves private VariableInput deltaT; // input the deltaT for the curve double dt = 0.1; // The value of delat t in the case where there is no deltaT input box private VariableInput xStart,yStart; // Starting point for curve private Choice methodChoice; // select integration method private Button startCurveButton; // user clicks to start curve from (x,y) in xStart, yStart input boxes private Button clearButton; // clears curves private Color curveColor; // color for integral curves private Draw curveDrawer = new Draw(); // A DrawTemp object that draws one segment of the integral curves. private double[] nextPoint = new double[2]; // Help in computing next point of integral curve. private double[] params = new double[2]; // ditto private static final int RK4 = 0, RK2 = 1, EULER = 2; // constants for integration methos private class Curve { // holds the data for one integral curve double dt; int method; double x,y; // point on the curve double lastX = Double.NaN, lastY; // previous point, so we can draw a line. } private class Draw implements DrawTemp { // For drawing the next segment in each integral curve (as a DrawTemp) public void draw(Graphics g, CoordinateRect coords) { int size = curves.size(); g.setColor(curveColor); for (int i = 0; i < size; i++) { Curve c = (Curve)(curves.elementAt(i)); if (! (Double.isNaN(c.x) || Double.isNaN(c.y) || Double.isNaN(c.lastX) || Double.isNaN(c.lastY)) ) { int x1 = coords.xToPixel(c.lastX); int y1 = coords.yToPixel(c.lastY); int x2 = coords.xToPixel(c.x); int y2 = coords.yToPixel(c.y); g.drawLine(x1,y1,x2,y2); } } } } protected void setUpParser() { // create the "y" variable; also set up some parameter defaults. yVar = new Variable(getParameter("Variable2","y")); parser.add(yVar); super.setUpParser(); // sets up xVar, among other things. parameterDefaults = new Hashtable(); parameterDefaults.put("FunctionLabel", " f1(" + xVar.getName() + "," + yVar.getName() + ") = "); parameterDefaults.put("FunctionLabel2", " f2(" + xVar.getName() + "," + yVar.getName() + ") = "); parameterDefaults.put("Function", " " + yVar.getName() + " - 0.1*" + xVar.getName()); parameterDefaults.put("Function2", " - " + xVar.getName() + " - 0.1*" + yVar.getName()); defaultFrameSize = new int[] { 580, 440 }; } protected void setUpCanvas() { // Override this to add more stuff to the canvas. super.setUpCanvas(); // Do the common setup: Add the axes and // set up the vector field and add it to the canvas if (functionInput != null) { xFunc = functionInput.getFunction(new Variable[] {xVar,yVar}); yFunc = functionInput2.getFunction(new Variable[] {xVar,yVar}); } else { String xFuncDef = getParameter("Function"); String yFuncDef = getParameter("Function2"); Function f = new SimpleFunction( parser.parse(xFuncDef), new Variable[] {xVar,yVar} ); xFunc = new WrapperFunction(f); f = new SimpleFunction( parser.parse(yFuncDef), new Variable[] {xVar,yVar} ); yFunc = new WrapperFunction(f); } String type = (getParameter("VectorStyle", "") + "A").toUpperCase(); int style = 0; switch (type.charAt(0)) { case 'A': style = VectorField.ARROWS; break; case 'L': style = VectorField.LINES; break; case 'S': style = VectorField.SCALED_VECTORS; break; } field = new VectorField(xFunc,yFunc,style); Color color = getColorParam("VectorColor"); if (color != null) field.setColor(color); int space = (style == VectorField.LINES)? 20 : 30; double[] d = getNumericParam("VectorSpacing"); if (d != null && d.length > 0 && d[0] >= 1) space = (int)Math.round(d[0]); field.setPixelSpacing(space); canvas.add(field); // Finally, add the graph to the canvas. curveColor = getColorParam("CurveColor", Color.magenta); // add a mouse listener to the canvas for starting curves. if ("yes".equalsIgnoreCase(getParameter("MouseStartsCurves","yes")) && "yes".equalsIgnoreCase(getParameter("DoCurves","yes"))) canvas.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent evt) { CoordinateRect coords = canvas.getCoordinateRect(); double x = coords.pixelToX(evt.getX()); double y = coords.pixelToY(evt.getY()); if (xStart != null) xStart.setVal(x); if (yStart != null) yStart.setVal(y); startCurve(x,y); } }); } // end setUpCanvas() protected void setUpBottomPanel() { // Override this to make a panel containing controls. This is complicated // because it's possible to turn off a lot of the inputs with applet params. // Check on the value of delta t, which has to be set even if there are no input controls. double[] DT = getNumericParam("DeltaT"); if ( ! (DT == null || DT.length == 0 || DT[0] <= 0) ) dt = DT[0]; boolean doCurves = "yes".equalsIgnoreCase(getParameter("DoCurves","yes")); boolean useInputs = "yes".equalsIgnoreCase(getParameter("UseFunctionInput","yes")); if (!doCurves && !useInputs) // no input controls at all. return; // make the input panel inputPanel = new JCMPanel(); inputPanel.setBackground( getColorParam("PanelBackground", Color.lightGray) ); mainPanel.add(inputPanel,BorderLayout.SOUTH); // Make the function inputs and the compute button, if these are in the configuration. JCMPanel in1 = null, in2 = null; // hold function inputs, if any if (useInputs) { if ( "yes".equalsIgnoreCase(getParameter("UseComputeButton", "yes")) ) { String cname = getParameter("ComputeButtonName", "New Functions"); computeButton = new Button(cname); computeButton.addActionListener(this); } functionInput = new ExpressionInput(getParameter("Function"),parser); in1 = new JCMPanel(); in1.add(functionInput,BorderLayout.CENTER); in1.add(new Label(getParameter("FunctionLabel")), BorderLayout.WEST); functionInput.setOnUserAction(mainController); functionInput2 = new ExpressionInput(getParameter("Function2"),parser); in2 = new JCMPanel(); in2.add(functionInput2,BorderLayout.CENTER); in2.add(new Label(getParameter("FunctionLabel2")), BorderLayout.WEST); functionInput2.setOnUserAction(mainController); } // If we're not doing curves, all we have to do is put the function inputs in the inputPanel if (!doCurves) { Panel p = new JCMPanel(2,1,3); p.add(in1); p.add(in2); inputPanel.add(p, BorderLayout.CENTER); if (computeButton != null) inputPanel.add(computeButton,BorderLayout.EAST); return; } // Now we know that doCurves is true. First, make the animator and clear button animator = new Animator(Animator.STOP_BUTTON); animator.setStopButtonName("Stop Curves"); animator.setOnChange(new Computable() { // animator drives curves public void compute() { extendCurves(); } }); mainController.add(new InputObject() { // curves must stop if main controller is triggered public void checkInput() { curves.setSize(0); animator.stop(); } public void notifyControllerOnChange(Controller c) { } }); clearButton = new Button("Clear"); clearButton.addActionListener(this); // Make a panel to contain the xStart and yStart inputs, if they are in the configuration. Panel bottom = null; if ("yes".equalsIgnoreCase(getParameter("UseStartInputs","yes"))) { xStart = new VariableInput(); xStart.addActionListener(this); yStart = new VariableInput(); yStart.addActionListener(this); bottom = new Panel(); // not a JCMPanel -- I don't want their contents checked automatically startCurveButton = new Button("Start curve at:"); startCurveButton.addActionListener(this); bottom.add(startCurveButton); bottom.add(new Label(xVar.getName() + " =")); bottom.add(xStart); bottom.add(new Label(yVar.getName() + " =")); bottom.add(yStart); } // Now, make a panel to contain the methodChoice and deltaT input if they are in the configuration. // The animator and clear button will be added to this panel if it exists. If not, and if // an xStart/yStart panel exists, then it will be added there. If neither exists, // it goes in its own panel. The variable bottom ends up pointing to a panel that // contains all the curve controls. boolean useChoice = "yes".equalsIgnoreCase(getParameter("UseMethodChoice","yes")); boolean useDelta = "yes".equalsIgnoreCase(getParameter("UseDeltaInput","yes")); if (useChoice || useDelta) { Panel top = new Panel(); // not a JCMPanel! if (useDelta) { top.add(new Label("dt =")); deltaT = new VariableInput(null,""+dt); top.add(deltaT); } if (useChoice) { top.add(new Label("Method:")); methodChoice = new Choice(); methodChoice.add("Runge-Kutta 4"); methodChoice.add("Runge-Kutta 2"); methodChoice.add("Euler"); top.add(methodChoice); } top.add(animator); top.add(clearButton); if (bottom == null) bottom = top; else { Panel p = new Panel(); p.setLayout(new BorderLayout()); p.add(top, BorderLayout.NORTH); p.add(bottom, BorderLayout.CENTER); bottom = p; } } else { if (bottom == null) bottom = new Panel(); bottom.add(animator); bottom.add(clearButton); } // Add the panels "bottom" to the inputPanel, and ruturn // if there are no function inputs. inputPanel.add(bottom, BorderLayout.CENTER); if (in1 == null) return; // Add the function inputs and compute button to the inputPanel Panel in = new JCMPanel(1,2); in.add(in1); in.add(in2); if (computeButton != null) { Panel p = new JCMPanel(); p.add(in,BorderLayout.CENTER); p.add(computeButton,BorderLayout.EAST); in = p; } inputPanel.add(in,BorderLayout.NORTH); } // end setUpBottomPanel() public void actionPerformed(ActionEvent evt) { // React if user presses return in xStart or yStart, or pass evt on to GenericGraphApplet Object src = evt.getSource(); if (src == clearButton) { canvas.clearErrorMessage(); curves.setSize(0); animator.stop(); canvas.compute(); // force recompute of off-screen canvas! } else if (src == xStart || src == yStart || src == startCurveButton) { // Start a curve from x and y values in xStart and yStart canvas.clearErrorMessage(); double x=0, y=0; try { xStart.checkInput(); x = xStart.getVal(); yStart.checkInput(); y = yStart.getVal(); startCurve(x,y); if (deltaT != null) { deltaT.checkInput(); dt = deltaT.getVal(); if (dt <= 0) { deltaT.requestFocus(); throw new JCMError("dt must be positive", deltaT); } } } catch (JCMError e) { curves.setSize(0); animator.stop(); canvas.setErrorMessage(null,"Illegal Data For Curve. " + e.getMessage()); } } else super.actionPerformed(evt); } // end actionPerfromed public void startCurve(double x, double y) { // Start an integral curve at the point (x,y) synchronized (curves) { if (deltaT != null) { try { deltaT.checkInput(); dt = deltaT.getVal(); if (dt <= 0) { deltaT.requestFocus(); throw new JCMError("dt must be positive", deltaT); } } catch (JCMError e) { curves.setSize(0); animator.stop(); canvas.setErrorMessage(null,"Illegal Data For Curve. " + e.getMessage()); return; } } Curve c = new Curve(); c.dt = dt; int method = (methodChoice == null)? RK4 : methodChoice.getSelectedIndex(); c.method = method; c.x = x; c.y = y; curves.addElement(c); animator.start(); } } public void extendCurves() { // Add the next segment to each integral curve. This function // is called repeatedly by the animator. synchronized(curves) { if (canvas == null || canvas.getCoordinateRect() == null) // can happen when frame closes return; while (canvas.getCoordinateRect().getWidth() <= 0) { // need this at startup to make sure that the canvas has appeared on the screen try { Thread.sleep(200); } catch (InterruptedException e) { } } int size = curves.size(); for (int i = 0; i < size; i++) { Curve curve = (Curve)curves.elementAt(i); curve.lastX = curve.x; curve.lastY = curve.y; nextPoint(curve.x, curve.y, curve.dt, curve.method); curve.x = nextPoint[0]; curve.y = nextPoint[1]; } CoordinateRect c = canvas.getCoordinateRect(); double pixelWidthLimit = 100000*c.getPixelWidth(); double pixelHeightLimit = 100000*c.getPixelHeight(); for (int i = size-1; i >= 0; i--) { Curve curve = (Curve)curves.elementAt(i); if (Double.isNaN(curve.x) || Double.isNaN(curve.y) || Math.abs(curve.x) > pixelWidthLimit || Math.abs(curve.y) > pixelWidthLimit) // stop processing this curve curves.removeElementAt(i); } if (curves.size() > 0) canvas.drawTemp(curveDrawer); else { animator.stop(); } } } private void nextPoint(double x, double y, double dt, int method) { // Find next point from (x,y) by applying specified method over time interval dt switch (method) { case EULER: nextEuler(x,y,dt); break; case RK2: nextRK2(x,y,dt); break; case RK4: nextRK4(x,y,dt); break; } } private void nextEuler(double x, double y, double dt) { params[0] = x; params[1] = y; double dx = xFunc.getVal(params); double dy = yFunc.getVal(params); nextPoint[0] = x + dt*dx; nextPoint[1] = y + dt*dy; } private void nextRK2(double x, double y, double dt) { params[0] = x; params[1] = y; double dx1 = xFunc.getVal(params); double dy1 = yFunc.getVal(params); double x2 = x + dt*dx1; double y2 = y + dt*dy1; params[0] = x2; params[1] = y2; double dx2 = xFunc.getVal(params); double dy2 = yFunc.getVal(params); nextPoint[0] = x + 0.5*dt*(dx1+dx2); nextPoint[1] = y + 0.5*dt*(dy1+dy2); } private void nextRK4(double x, double y, double dt) { params[0] = x; params[1] = y; double dx1 = xFunc.getVal(params); double dy1 = yFunc.getVal(params); double x2 = x + 0.5*dt*dx1; double y2 = y + 0.5*dt*dy1; params[0] = x2; params[1] = y2; double dx2 = xFunc.getVal(params); double dy2 = yFunc.getVal(params); double x3 = x + 0.5*dt*dx2; double y3 = y + 0.5*dt*dy2; params[0] = x3; params[1] = y3; double dx3 = xFunc.getVal(params); double dy3 = yFunc.getVal(params); double x4 = x + dt*dx3; double y4 = y + dt*dy3; params[0] = x4; params[1] = y4; double dx4 = xFunc.getVal(params); double dy4 = yFunc.getVal(params); nextPoint[0] = x + (dt / 6) * (dx1 + 2 * dx2 + 2 * dx3 + dx4); nextPoint[1] = y + (dt / 6) * (dy1 + 2 * dy2 + 2 * dy3 + dy4); } protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For the IntegrapCurves applet, the example string should contain // two expression that defines the vector field, separated // by a semicolon. This can optionally // be followed by another semicolon and a list of numbers, separated by spaces and/or commas. // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The next number, if present, // specifies a value for delta t. If there are more numbers, they should come in pairs. // each pair specifies a point where a curve will be started when the // example is loaded. There is a 0.5 second delay between loading and starting the // curves to allow time for the redrawing (although it seems to block the redrawing, at least // on some platforms). if (animator != null) { curves.setSize(0); animator.stop(); } int pos = example.indexOf(";"); if (pos == -1) return; // illegal example -- must have two functions String example2 = example.substring(pos+1); example = example.substring(0,pos); pos = example2.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use StringTokenizer toks = null; if (pos > 0) { // Get limits from example2 text. String nums = example2.substring(pos+1); example2 = example2.substring(0,pos); toks = new StringTokenizer(nums, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } } if (toks.hasMoreTokens()) { double d = Double.NaN; try { d = (new Double(toks.nextToken())).doubleValue(); } catch (NumberFormatException e) { } if (Double.isNaN(d) || d <= 0 || d > 100) d = 0.1; if (deltaT != null) deltaT.setVal(d); else dt = d; } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); functionInput2.setText(example2); } else { // If there is no user input, set the function in the graph directly. try { Function f = new SimpleFunction( parser.parse(example), xVar ); ((WrapperFunction)xFunc).setFunction(f); Function g = new SimpleFunction( parser.parse(example2), xVar ); ((WrapperFunction)yFunc).setFunction(g); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); if (animator != null && toks != null) { // get any extra nums from the tokenizer and use them as starting points for curves int ct = 2*(toks.countTokens()/2); if (ct > 0) { synchronized(curves) { for (int i = 0; i < ct; i++) { try { double x = (new Double(toks.nextToken())).doubleValue(); double y = (new Double(toks.nextToken())).doubleValue(); startCurve(x,y); } catch (Exception e) { } } if (curves.size() > 0) { // start the curves going try { Thread.sleep(500); // wait a bit to give the canvas time to start drawing itself. } catch (InterruptedException e) { } } } } } } // end doLoadExample() public void stop() { // stop animator when applet is stopped if (animator != null) { curves.setSize(0); animator.stop(); } super.stop(); } } // end class IntegralCurves jcm1-source/AnimatedGraph.java0000644000076500011320000003313711741343635015565 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // An applet belonging to the class AnimatedGraph displays a graph // of a function that can depend on a parameter. The value of the // parameter can be "animated" so that it ranges from one value ot // another over a sequence of frames. import java.awt.*; import java.applet.Applet; import java.util.*; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; public class AnimatedGraph extends GenericGraphApplet { // Declare some private variables that are created in one method in // this class and used in a second method. private Function func; // The function that is graphed. private Graph1D graph; // The graph of the function. private Animator animator; // Animates the graph private Variable kVar; // The parameter variable private VariableInput kMin, kMax, kIntervals; // min, max, and number of intervals for the animator. Might be null. protected void setUpParser() { // Override this to create the animator and add its variable to the parser. int options = Animator.START_STOP_BUTTON | Animator.PAUSE_BUTTON | Animator.LOOP_CHOICE; if ( ! "no".equalsIgnoreCase(getParameter("UseNextAndPrev","yes")) ) options |= Animator.PREV_BUTTON | Animator.NEXT_BUTTON; animator = new Animator(options); kVar = animator.getValueAsVariable( getParameter("Parameter","k") ); parser.add(kVar); super.setUpParser(); parameterDefaults = new Hashtable(); String defaultFunction = xVar.getName() + " / (" + kVar.getName() + " - " + xVar.getName() + "^2)"; parameterDefaults.put("Function",defaultFunction); if (! "no".equalsIgnoreCase(getParameter("UseAnimatorInputs"))) parameterDefaults.put("TwoLimitsColumns","yes"); // change default if we need space for animator inputs } // end setUpParser() protected void setUpBottomPanel() { // Overridden to add the sliders at the bottom of the applet. super.setUpBottomPanel(); // Do the default setup. // If there is a functionInput box, then the SOUTH position of the mainPanel already contains // the inputPanel that contains that box. If so, add the animator to the SOUTH position of // the inputPanel. (This is a good place, in general, to put extra input objects.) // If there is no inputPanel, then the SOUTH position of the mainPanel is empty, so put // the animator there. if (inputPanel != null) inputPanel.add(animator, BorderLayout.SOUTH); else mainPanel.add(animator, BorderLayout.SOUTH); } // end setUpBottomPanel() protected void setUpCanvas() { // Overridden to add the graph to the canvas and do other chores. super.setUpCanvas(); // Do the default setup. // When setUpCanvas() is called, the functionInput already exists, if one is // to be used, since it is created in setUpBopttomPanel(), which is called // before setUpCanvas. If functionInput exists, add a graph of the function // from functionInput to the canvas. If not, create a graph of the function // specified by the parameter named "Function" (or use sin(k*x) if none is specified). if (functionInput != null) func = functionInput.getFunction(xVar); else { String def = getParameter("Function"); // default value is set in setUpParser() func = new SimpleFunction( parser.parse(def), xVar ); } // Create a graph of the function and add it to the canvas. graph = new Graph1D(func); graph.setColor(getColorParam("GraphColor", Color.magenta)); canvas.add(graph); // Set up the min, max, and intervals property of the animator if (! "no".equalsIgnoreCase(getParameter("UseAnimatorInputs"))) { kMin = new VariableInput(kVar.getName() + "Start",getParameter("ParameterMin","-2")); kMax = new VariableInput(kVar.getName() + "End",getParameter("ParameterMax","2")); kIntervals = new VariableInput("Intervals", getParameter("Intervals","25")); kIntervals.setInputStyle(VariableInput.INTEGER); kIntervals.setMin(1); kIntervals.setMax(1000); kMin.setOnUserAction(mainController); kMax.setOnUserAction(mainController); kIntervals.setOnUserAction(mainController); animator.setMin(kMin); animator.setMax(kMax); animator.setIntervals(kIntervals); if (limitsPanel != null) { // componets will be added to limitsPanel in setUpLimitsPanel() mainController.add(kMin); // This is not done automatically, since they are in a limits panel mainController.add(kMax); mainController.add(kIntervals); } else { JCMPanel ap = new JCMPanel(9,0); ap.setBackground(getColorParam("PanelBackground", Color.lightGray)); ap.add(new Label(kMin.getName())); ap.add(kMin); ap.add(new Label()); ap.add(new Label(kMax.getName())); ap.add(kMax); ap.add(new Label()); ap.add(new Label(kIntervals.getName())); ap.add(kIntervals); ap.add(new Label()); mainPanel.add(ap,BorderLayout.EAST); } } else { try { animator.setMin( (new Double(getParameter("ParameterMin","-2"))).doubleValue() ); animator.setMax( (new Double(getParameter("ParameterMax","2"))).doubleValue() ); animator.setIntervals( (int)Math.round((new Double(getParameter("Intervals","25"))).doubleValue()) ); } catch (NumberFormatException e) { } } animator.setOnChange(mainController); // Add a DrawString to show the current value of the parameter if ( ! "no".equalsIgnoreCase(getParameter("ShowParameter","yes")) ) { DrawString param = new DrawString(kVar.getName() + " = #", DrawString.BOTTOM_LEFT, new Value[] { kVar }); param.setBackgroundColor(canvas.getBackground()); Color c = getColorParam("ParameterColor",Color.black); param.setColor(c); canvas.add(param); } } // end setUpCanvas protected void setUpLimitsPanel() { super.setUpLimitsPanel(); if (limitsPanel != null && kMin != null) { // add animator inputs to limits panel limitsPanel.addComponentPair(kMin,kMax); limitsPanel.addComponent(kIntervals); } } protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For the AnimatedGraph applet, the example string should contain // an expression that defines the function to be graphed. This can optionally // be followed by a semicolon and a list of four to nine numbers. // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The // next three numbers specify the minimum value for the parameter, the // maximum number, and the number of intervals in the animation. // The eigth number, if present, specifies the starting loop style // for the animation with the following code: 0 for once-through, // 1 for loop, and 2 for back-and-forth. The ninth number, if // present, tells whether to start the animation immediately upon // loading. If it is 1, the animation is started. If it is // not specified or is any value other than 1, the animation is not started. animator.stop(); int pos = example.indexOf(";"); boolean startAnimation = false; double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // Get limits from example text. String nums = example.substring(pos+1); example = example.substring(0,pos); StringTokenizer toks = new StringTokenizer(nums, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } } if (toks.hasMoreTokens()) { try { double d = (new Double(toks.nextToken())).doubleValue(); if (kMin == null) animator.setMin(d); else kMin.setVal(d); } catch (NumberFormatException e) { } } if (toks.hasMoreTokens()) { try { double d = (new Double(toks.nextToken())).doubleValue(); if (kMax == null) animator.setMax(d); else kMax.setVal(d); } catch (NumberFormatException e) { } } if (toks.hasMoreTokens()) { try { int d = (int)Math.round((new Double(toks.nextToken())).doubleValue()); if (kIntervals == null) animator.setIntervals(d); else kIntervals.setVal(d); } catch (NumberFormatException e) { } } if (toks.hasMoreTokens()) { try { int d = (int)Math.round((new Double(toks.nextToken())).doubleValue()); animator.setLoopStyle(d); } catch (NumberFormatException e) { } } if (toks.hasMoreTokens()) { try { int d = (int)Math.round((new Double(toks.nextToken())).doubleValue()); startAnimation = (d == 1); } catch (NumberFormatException e) { } } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); } else { // If there is no user input, set the function in the graph directly. try { func = new SimpleFunction( parser.parse(example), xVar ); graph.setFunction(func); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); if (startAnimation) { try { // insert a small delay before animation starts synchronized(this) { wait(250); } } catch (InterruptedException e) { } animator.start(); } } // end doLoadExample() public void stop() { // stop animator when applet is stopped animator.stop(); super.stop(); } } // end class FamiliesOfGraphs jcm1-source/SecantTangent.java0000644000076500011320000003245511741343635015621 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // The SecantTangent applet shows the graph of a function with two points // marked on the function. The tangent to the graph is drawn at the first // of these points. A second line is drawn between the two points. // Both of the points can be dragged by the user. Alternatively, the // x-coords of the points can be typed into input boxes. import java.awt.*; import java.applet.Applet; import java.util.StringTokenizer; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; public class SecantTangent extends GenericGraphApplet { private Function func; // The function that is graphed. VariableInput x1Input = new VariableInput(); // x-coord where tangent is drawn. VariableInput x2Input = new VariableInput(); // x-coord for other point of secant. protected void setUpParameterDefaults() { parameterDefaults = new java.util.Hashtable(); String varName = getParameter("Variable","x"); parameterDefaults.put("Function", " e ^ " + varName); } protected void setUpCanvas() { // Override this to add more stuff to the canvas. super.setUpCanvas(); // Do the common setup: Add the axes and // When setUpCanvas is called, the functionInput already exists, if one is // to be used, since it is created in setUpBopttomPanel(), which is called // before setUpCanvas. If functionInput exists, add a graph of the function // from functionInput to the canvas. If not, create a graph of the function // specified by the parameter named "Function". if (functionInput != null) func = functionInput.getFunction(xVar); else { String def = getParameter("Function"); Function f = new SimpleFunction( parser.parse(def), xVar ); func = new WrapperFunction(f); } Graph1D graph = new Graph1D(func); Color color = getColorParam("GraphColor",Color.black); graph.setColor(color); // Create two DraggablePoint objects, which will be the points on the canvas // that the user can drag. The x-coordinate of drag1 will be tied later to // x1Input, so that either drag1 or x1Input can be used for setting the // values of the point. Same for drag2 and x2Input. Color tangentColor = getColorParam("TangentColor", Color.red); Color secantColor = getColorParam("SecantColor",new Color(0,200,0)); DraggablePoint drag1 = new DraggablePoint(); // point where tangent is drawn DraggablePoint drag2 = new DraggablePoint(); // other point on secant line drag1.clampY(func); // Both points are clamped to move along the function. drag2.clampY(func); drag1.setColor(tangentColor); drag1.setGhostColor(lighten(tangentColor)); drag2.setColor(secantColor); drag2.setGhostColor(lighten(secantColor)); // Create the tangent line and the secant line. DrawGeometric secant = new DrawGeometric(DrawGeometric.INFINITE_LINE_ABSOLUTE, drag1.getXVar(), drag1.getYVar(), drag2.getXVar(), drag2.getYVar()); secant.setColor(secantColor); TangentLine tangent = new TangentLine(drag1.getXVar(), func); tangent.setColor(tangentColor); canvas.add(drag1); canvas.add(drag2); canvas.add(tangent); canvas.add(secant); canvas.add(graph); // Create a DrawString to display the slopes of the tangent and secant. Value tangentSlope = new ValueMath(func.derivative(1), drag1.getXVar()); Value secantSlope = new ValueMath( new ValueMath(drag2.getYVar(), drag1.getYVar(), '-'), new ValueMath(drag2.getXVar(), drag1.getXVar(), '-'), '/'); DrawString info; if ( "no".equalsIgnoreCase(getParameter("ShowTangentSlope","yes")) ) { info = new DrawString( "Secant Slope = #", DrawString.TOP_LEFT, new Value[] { secantSlope } ); } else { info = new DrawString( "Secant Slope = #\nTangent Slope = #", DrawString.TOP_LEFT, new Value[] { secantSlope, tangentSlope } ); } info.setFont(new Font("Monospaced",Font.PLAIN,10)); info.setNumSize(7); info.setColor(getColorParam("SlopeTextColor", Color.black)); info.setBackgroundColor(getColorParam("SlopeTextBackground",Color.white)); info.setFrameWidth(1); canvas.add(info); // Create input boxes and add them to the bottom of the applet Panel xIn = new Panel(); // not a JCMPanel, since I don't want the input boxes to be controlled // by the main controller anyway. xIn.setBackground(getColorParam("PanelColor",Color.lightGray)); xIn.setLayout(new GridLayout(1,4,3,3)); xIn.add(new Label("Tangent at " + xVar.getName() + " = ", Label.RIGHT)); xIn.add(x1Input); xIn.add(new Label("Secant to " + xVar.getName() + " = ", Label.RIGHT)); xIn.add(x2Input); // Put the inputs at the bottom of the inputPanel, if there is one, otherwise // at the bottom of the mainPanel if (inputPanel == null) mainPanel.add(xIn, BorderLayout.SOUTH); else inputPanel.add(xIn, BorderLayout.SOUTH); // Set up controllers. I want to arrange things so that the controls that position the // two points on the graph do not cause the graph to be recomputted when they are changed. Controller dragControl = new Controller(); // A controller to respond to dragging mainController.remove(canvas); mainController.add(graph); mainController.add(dragControl); // Things in dragController should be recomputed when graph is changed dragControl.add(x1Input); // dragControl checks the contents of the x-inputs dragControl.add(x2Input); // and recomputes everything except the graph. dragControl.add(drag1); dragControl.add(drag2); dragControl.add(tangent); dragControl.add(secant); dragControl.add(info); drag1.setOnUserAction(dragControl); // dragControl.compute() is called when the drag2.setOnUserAction(dragControl); // user drags one of the points or types x1Input.setOnTextChange(dragControl); // in one of the x-input boxes. x2Input.setOnTextChange(dragControl); // By adding Tie's to dragControll, we make sure that the positions of the // draggable points are synchronized with the contents of the x-input boxes. dragControl.add(new Tie((Tieable)drag1.getXVar(), x1Input)); dragControl.add(new Tie((Tieable)drag2.getXVar(), x2Input)); // Get initial values for draggable points double[] d1 = getNumericParam("X1"); double x1 = (d1 != null && d1.length == 1)? d1[0] : 0; x1Input.setVal(x1); drag1.setLocation(x1,0); // y-value will be changed to make the point lie on the curve double[] d2 = getNumericParam("X2"); double x2 = (d2 != null && d2.length == 1)? d2[0] : 1; x2Input.setVal(x2); drag2.setLocation(x2,0); } // end setUpCanvas() private Color lighten(Color c) { // for making "Ghost" color of draggable point int r = c.getRed(); int g = c.getGreen(); int b = c.getBlue(); int nr, ng, nb; if (r <= 200 || g <= 200 || b <= 200) { nb = 255 - (255 - b) / 3; ng = 255 - (255 - g) / 3; nr = 255 - (255 - r) / 3; } else { nb = b / 2; ng = g / 2; nr = r / 2; } return new Color(nr,ng,nb); } protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For the SecantTangent applet, the example string should contain // an expression that defines the function to be graphed. This can optionally // be followed by a semicoloon and a list of fourto six numbers. // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The // fifth number, if present, gives the x-coord where the tangent line // is drawn. The sixth number gives the x-coord of the second point // on the secant line. int pos = example.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // get limits from example text String limitsText = example.substring(pos+1); example = example.substring(0,pos); StringTokenizer toks = new StringTokenizer(limitsText, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } if (toks.countTokens() > 0) { // Get point for tangent line try { Double d = new Double(toks.nextToken()); x1Input.setVal( d.doubleValue() ); } catch (NumberFormatException e) { } } if (toks.countTokens() > 0) { // Get other point for secant line try { Double d = new Double(toks.nextToken()); x2Input.setVal( d.doubleValue() ); } catch (NumberFormatException e) { } } } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); } else { // If there is no user input, set the function in the graph directly. // Also, in this case, func is a "WrapperFunction". Set the // definition of that WrapperFunction to be the same as f try { Function f = new SimpleFunction( parser.parse(example), xVar ); ((WrapperFunction)func).setFunction(f); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); } // end doLoadExample() } // end class SimpleGraph jcm1-source/README.html0000644000076500011320000000201711741343635014032 0ustar djefaculty JCM Source README

About these source files:


This folder contains the source files for the JCM Project. This Version 1.0.

The Java files in this folder are for a set of configurable applets that use the JCM classes. See the Configurable Applet Collection for more information.

The sources for the JCM classes themselves are contained in the directories edu/hws/jcm/awt, edu/hws/jcm/data, edu/hws/jcm/draw, and edu/hws/jcm/functions.

The home page for the project is http://math.hws.edu/javamath/



David Eck, eck@hws.edu, http://math.hws.edu/eck/
jcm1-source/FunctionComposition.java0000644000076500011320000007533511741343635017100 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ import java.awt.*; import java.awt.event.*; import java.util.StringTokenizer; import java.applet.Applet; import edu.hws.jcm.data.*; import edu.hws.jcm.draw.*; import edu.hws.jcm.awt.*; import edu.hws.jcm.functions.*; // A Function composation applet displays the graphs of two functions, f(x) and g(x), and their // coposition, g(f(x)). It marks points (x,f(x)), (f(x),g(f(x)), and (x,g(f(x))) on the respective // graphs. The user controls the value of x by dragging a red square along the x-axis of the first // graph. The functions f and g can be given as table functions instead of expressions. The // user controls this by clicking on check boxes. When a table function is displayed, the user // can modify it by dragging its points. Optionally, the apple can also display tangent lines // to the graphs. The slopes of the tangent line are supposed to illustrate the chain rule. // Note: This inherits from GenericGraphApplet, but a lot of code in that applet is superceded // here. In particular, there is no possibility of having a limit control panel. public class FunctionComposition extends GenericGraphApplet implements ActionListener, ItemListener { Button zoomInButton, zoomOutButton, restoreButton, equalizeButton; // Control buttons for // setting limits on coordinate rects. Button fComputeButton, gComputeButton; // Buttons that the user can press when she enters a new function. // If the displayed function is a table function, the function's values // are all reset to zero. Variable pointX; // The x-coordinate that is marked on the graph of f(x) // There are two possibilities for each function -- an expression or a table. // I need variables for keeping track of both possibilities. They are swapped in // and out when the user clicks on a check box. They can also be swapped when // an example is loaded. The data for the functions: Checkbox fCheck, gCheck; // checkboxes for switching between the two versions of the functions. ExpressionInput fInput, gInput; // Input boxes for the definitions of f and g. Function fFunc, gFunc; // The functions, defined by expressions. Graph1D fGraph, gGraph; // The graphs of the functions defined by expressions TableFunction fTable, gTable; // The functions, defined by tables. TableFunctionGraph fTableGraph, gTableGraph; // The graphs of the functions defined by tables. boolean fTableShown, gTableShown; // keep track of which version of the function is shown. String fSaveText, gSaveText; // Save the text in the input box while a table shown is shown. WrapperFunction fWrapper, gWrapper; // These functions refer to f and g, whichever versions of // f and g are currently in effect. They are used to form // the composition function, g(f(x)). So the composed function // is valid however f and g are represented. public void setUpMainPanel() { // Set up the applet. // basic setup, with three coordinate rects in the canvas mainController = new Controller(); defaultFrameSize = new int[] { 606, 306 }; Color textColor = getColorParam("TextColor", Color.black); Color canvasBackground = getColorParam("CanvasColor", Color.white); boolean useInputs = ! "no".equalsIgnoreCase(getParameter("UseFunctionInput", "yes")); double[] gap = getNumericParam("Insets"); if (gap == null || gap.length == 0 || gap[0] < 0 || gap[0] > 50) mainPanel.setInsetGap(3); else mainPanel.setInsetGap( (int)Math.round(gap[0]) ); parser = new Parser(null,0); setUpParser(); setUpExampleMenu(); setUpTopPanel(); Color color; color = getColorParam("BackgroundColor", Color.gray); mainPanel.setBackground(color); color = getColorParam("ForegroundColor", Color.black); mainPanel.setForeground(color); double[] limits = getNumericParam("Limits"); if (limits == null || limits.length < 4) limits = new double[] { -5, 5, -5 ,5 }; canvas = new DisplayCanvas(); mainPanel.add(canvas,BorderLayout.CENTER); // add canvas to panel canvas.setBackground(canvasBackground); if ( ! "no".equalsIgnoreCase(getParameter("UseMouseZoom", "no")) ) canvas.setHandleMouseZooms(true); if ( ! "no".equalsIgnoreCase(getParameter("UseOffscreenCanvas", "yes")) ) canvas.setUseOffscreenCanvas(true); canvas.addCoordinateRect(new CoordinateRect(limits[0],limits[1],limits[2],limits[3]), 0, 1.0/3.0, 0, 1,null); canvas.addCoordinateRect(new CoordinateRect(limits[0],limits[1],limits[2],limits[3]),1.0/3.0, 2.0/3.0, 0,1,null); canvas.addCoordinateRect(new CoordinateRect(limits[0],limits[1],limits[2],limits[3]),2.0/3.0, 1, 0, 1, null); if ( ! "no".equalsIgnoreCase(getParameter("UseGrid", "no")) ) { color = getColorParam("GridColor"); Grid g = new Grid(); if (color != null) g.setColor(color); canvas.add(g,0); g = new Grid(); if (color != null) g.setColor(color); canvas.add(g,1); g = new Grid(); if (color != null) g.setColor(color); canvas.add(g,2); } canvas.add(makeAxes(), 0); canvas.add(makeAxes(), 1); canvas.add(makeAxes(), 2); // Make the expression functions fSaveText = getParameter("Function"," 3 - " + xVar.getName() + "^2/2"); gSaveText = getParameter("SecondFunction", " sin(" + xVar.getName() + ")"); if (useInputs) { fInput = new ExpressionInput(fSaveText, parser); gInput = new ExpressionInput(gSaveText, parser); fFunc = fInput.getFunction(xVar); gFunc = gInput.getFunction(xVar); } else { fFunc = new SimpleFunction( parser.parse(fSaveText), xVar ); gFunc = new SimpleFunction( parser.parse(gSaveText), xVar ); } fGraph = new Graph1D(fFunc); gGraph = new Graph1D(gFunc); // Create the wrapper functions for f and g and use it to make a compostion function fWrapper = new WrapperFunction(fFunc); fWrapper.setName("f"); gWrapper = new WrapperFunction(gFunc); gWrapper.setName("g"); Parser p1 = new Parser(); // parser for making the composition function p1.add(fWrapper); p1.add(gWrapper); ExpressionFunction comp = new ExpressionFunction("h", new String[] { "x" }, "g(f(" + xVar.getName() + "))", p1); Graph1D compositionGraph = new Graph1D(comp); fTableShown = gTableShown = false; // Make table functions. If a table function is specified in applet params, but no // expression is specified, show the table function. String tf = getParameter("TableFunction"); if (tf != null) { try { fTable = parseTableFuncDef(tf); } catch (Exception e) { tf = null; } } if (tf == null) { fTable = new TableFunction(); fTable.addIntervals(6, -5, 5); } fTableGraph = new TableFunctionGraph(fTable); fTableGraph.setInteractive(true); if (getParameter("Function") == null && tf != null) { // show table function at startup fGraph.setVisible(false); fTableShown = true; fWrapper.setFunction(fTable); if (fInput != null) { fInput.setEnabled(false); fInput.setThrowErrors(false); fInput.setText("Drag points to modify function."); } } else { fTableGraph.setVisible(false); } tf = getParameter("SecondTableFunction"); if (tf != null) { try { gTable = parseTableFuncDef(tf); } catch (Exception e) { tf = null; } } if (tf == null) { gTable = new TableFunction(); gTable.addIntervals(6, -5, 5); } gTableGraph = new TableFunctionGraph(gTable); gTableGraph.setInteractive(true); if (getParameter("SecondFunction") == null && tf != null) { // show table function at startup gGraph.setVisible(false); gTableShown = true; gWrapper.setFunction(gTable); if (gInput != null) { gInput.setEnabled(false); gInput.setThrowErrors(false); gInput.setText("Drag points to modify function."); } } else { gTableGraph.setVisible(false); } // Create the stuff for marking points on each graph DraggablePoint point = new DraggablePoint(DraggablePoint.SQUARE); // gives x-coord Color pointColor1 = getColorParam("PointColor1", Color.red); Color pointColor2 = getColorParam("PointColor2", new Color(0,200,0)); Color pointColor3 = getColorParam("PointColor3", new Color(100,100,255)); point.setColor(pointColor1); point.clampY(0); point.setLocation(1,0); canvas.add(point,0); pointX = point.getXVar(); Value fOfX = new ValueMath(fWrapper, pointX); Value gOfFOfX = new ValueMath(gWrapper, fOfX); DrawGeometric line1; line1 = new DrawGeometric( DrawGeometric.LINE_ABSOLUTE, pointX, new Constant(0), pointX, fOfX ); line1.setColor(pointColor1); canvas.add(line1, 0); DrawGeometric line2; line2 = new DrawGeometric( DrawGeometric.LINE_ABSOLUTE, pointX, fOfX, new Constant(0), fOfX ); line2.setColor(pointColor2); canvas.add(line2, 0); DrawGeometric line3; line3 = new DrawGeometric( DrawGeometric.LINE_ABSOLUTE, fOfX, new Constant(0), fOfX, gOfFOfX ); line3.setColor(pointColor2); canvas.add(line3, 1); DrawGeometric line4; line4 = new DrawGeometric( DrawGeometric.LINE_ABSOLUTE, fOfX, gOfFOfX, new Constant(0), gOfFOfX ); line4.setColor(pointColor3); canvas.add(line4, 1); DrawGeometric line5; line5 = new DrawGeometric( DrawGeometric.LINE_ABSOLUTE, pointX, new Constant(0), pointX, gOfFOfX ); line5.setColor(pointColor1); canvas.add(line5, 2); DrawGeometric line6; line6 = new DrawGeometric( DrawGeometric.LINE_ABSOLUTE, pointX, gOfFOfX, new Constant(0), gOfFOfX ); line6.setColor(pointColor3); canvas.add(line6, 2); line1.setLineWidth(2); line2.setLineWidth(2); line3.setLineWidth(2); line4.setLineWidth(2); line5.setLineWidth(2); line6.setLineWidth(2); // Add the graphs Color gc = getColorParam("GraphColor", Color.magenta); fGraph.setColor(gc); gGraph.setColor(gc); fTableGraph.setColor(gc); gTableGraph.setColor(gc); compositionGraph.setColor(gc); canvas.add(fGraph, 0); canvas.add(fTableGraph, 0); canvas.add(gGraph, 1); canvas.add(gTableGraph, 1); canvas.add(compositionGraph, 2); // Create tangent lines, if they are called for by an applet param TangentLine tangent1=null, tangent2=null, tangent3=null; DrawString ts1=null, ts2=null, ts3=null; if (! "no".equalsIgnoreCase(getParameter("ShowTangents","no"))) { Color tangentColor = getColorParam("TangentColor", Color.gray); tangent1 = new TangentLine(pointX,fWrapper); tangent1.setColor(tangentColor); canvas.add(tangent1,0); tangent2 = new TangentLine(fOfX,gWrapper); tangent2.setColor(tangentColor); canvas.add(tangent2,1); tangent3 = new TangentLine(pointX,comp); tangent3.setColor(tangentColor); canvas.add(tangent3,2); if ("yes".equalsIgnoreCase(getParameter("ShowSlopes","yes"))) { ts1 = new DrawString("slope = #", DrawString.TOP_RIGHT, new Value[] { new ValueMath(fWrapper.derivative(1), pointX) }); ts1.setColor(textColor); ts1.setNumSize(6); canvas.add(ts1,0); ts2 = new DrawString("slope = #", DrawString.TOP_RIGHT, new Value[] { new ValueMath(gWrapper.derivative(1), fOfX) }); ts2.setColor(textColor); ts2.setNumSize(6); canvas.add(ts2,1); ts3 = new DrawString("slope = #", DrawString.TOP_RIGHT, new Value[] { new ValueMath(comp.derivative(1), pointX) }); ts3.setColor(textColor); ts3.setNumSize(6); canvas.add(ts3,2); } } // Create DrawStrings for displaying information over the graphs. if ("yes".equalsIgnoreCase(getParameter("ShowFunctionNames","yes"))) { DrawString d = new DrawString("y=f(" + xVar.getName() + ")"); d.setColor(textColor); canvas.add(d, 0); d = new DrawString("y=g(" + xVar.getName() + ")"); d.setColor(textColor); canvas.add(d, 1); d = new DrawString("y=g(f(" + xVar.getName() + "))"); d.setColor(textColor); canvas.add(d, 2); } DrawString ds1=null, ds2=null, ds3=null; if ("yes".equalsIgnoreCase(getParameter("ShowCoordinates","yes"))) { ds1 = new DrawString("f(#) = #", DrawString.BOTTOM_CENTER, new Value[] { pointX, fOfX }); ds1.setNumSize(6); ds1.setColor(textColor); ds1.setBackgroundColor(canvasBackground); canvas.add(ds1, 0); ds2 = new DrawString("g(#) = #", DrawString.BOTTOM_CENTER, new Value[] { fOfX, gOfFOfX }); ds2.setNumSize(6); ds2.setColor(textColor); ds2.setBackgroundColor(canvasBackground); canvas.add(ds2, 1); ds3 = new DrawString("g(f(#)) = #", DrawString.BOTTOM_CENTER, new Value[] { pointX, gOfFOfX }); ds3.setNumSize(6); ds3.setColor(textColor); ds3.setBackgroundColor(canvasBackground); canvas.add(ds3, 2); } // Add panner and borders if (! "no".equalsIgnoreCase(getParameter("UsePanner", "no")) ) { canvas.add(new Panner(), 0); canvas.add(new Panner(), 1); canvas.add(new Panner(), 2); } int borderWidth; double[] bw = getNumericParam("BorderWidth"); if (bw == null || bw.length == 0 || bw[0] > 25) borderWidth = 1; else borderWidth = (int)Math.round(bw[0]); if (borderWidth > 0) { Color bc = getColorParam("BorderColor", Color.black); canvas.add(new DrawBorder(bc, borderWidth), 0); canvas.add(new DrawBorder(bc, borderWidth), 1); canvas.add(new DrawBorder(bc, borderWidth), 2); } // Set up the bottom panel to hold inputs and control buttons, unless an // applet param has specified that no input panel is desired. if ( useInputs ) { Panel bottom = new Panel(); bottom.setLayout(new BorderLayout(3,3)); bottom.setBackground(getColorParam("PanelBackground", Color.lightGray)); mainPanel.add(bottom,BorderLayout.SOUTH); Panel left = new Panel(); left.setLayout(new GridLayout(0,1)); bottom.add(left, BorderLayout.CENTER); Panel right = new Panel(); right.setLayout(new GridLayout(0,2)); bottom.add(right, BorderLayout.EAST); Panel fPanel = new Panel(); fPanel.setLayout(new BorderLayout()); fPanel.add(new Label(" f(" + xVar.getName() + ") = "), BorderLayout.WEST); fPanel.add(fInput, BorderLayout.CENTER); Panel fp = new Panel(); fp.setLayout(new GridLayout(1,2)); fCheck = new Checkbox("Use Mouse"); if (fTableShown) fCheck.setState(true); fCheck.addItemListener(this); fp.add(fCheck); fComputeButton = new Button("New f(" + xVar.getName() + ")"); fComputeButton.addActionListener(this); fp.add(fComputeButton); fPanel.add(fp,BorderLayout.EAST); left.add(fPanel); Panel gPanel = new Panel(); gPanel.setLayout(new BorderLayout()); gPanel.add(new Label(" g(" + xVar.getName() + ") = "), BorderLayout.WEST); gPanel.add(gInput, BorderLayout.CENTER); Panel gp = new Panel(); gp.setLayout(new GridLayout(1,2)); gCheck = new Checkbox("Use Mouse"); if (gTableShown) gCheck.setState(true); gCheck.addItemListener(this); gp.add(gCheck); gComputeButton = new Button("New g(" + xVar.getName() + ")"); gComputeButton.addActionListener(this); gp.add(gComputeButton); gPanel.add(gp,BorderLayout.EAST); left.add(gPanel); zoomInButton = new Button("Zoom In"); right.add(zoomInButton); zoomInButton.addActionListener(this); zoomOutButton = new Button("Zoom Out"); right.add(zoomOutButton); zoomOutButton.addActionListener(this); equalizeButton = new Button("EqualizeAxes"); equalizeButton.addActionListener(this); right.add(equalizeButton); restoreButton = new Button("Restore Limits"); right.add(restoreButton); restoreButton.addActionListener(this); fInput.setOnUserAction(mainController); gInput.setOnUserAction(mainController); mainController.add(fInput); mainController.add(gInput); } // finish setting up controllers fTableGraph.setOnDrag(mainController); gTableGraph.setOnDrag(mainController); mainController.add(canvas); mainController.setErrorReporter(canvas); Controller lineController = new Controller(); // doesn't recompute the graphs mainController.add(lineController); point.setOnUserAction(lineController); lineController.add(point); lineController.add(line1); lineController.add(line2); lineController.add(line3); lineController.add(line4); lineController.add(line5); lineController.add(line6); if (ds1 != null) { lineController.add(ds1); lineController.add(ds2); lineController.add(ds3); } if (tangent1 != null) { lineController.add(tangent1); lineController.add(tangent2); lineController.add(tangent3); } if (ts1 != null) { lineController.add(ts1); lineController.add(ts2); lineController.add(ts3); } } // end makeMainPanel() public void itemStateChanged(ItemEvent evt) { // Respond when user clicks one of the check boxes. Object src = evt.getSource(); if (src == fCheck) { // Swap the f table function in or out boolean check = fCheck.getState(); if (check == fTableShown) return; fTableShown = check; fGraph.setVisible(!fTableShown); fTableGraph.setVisible(fTableShown); if (fTableShown) { fWrapper.setFunction(fTable); fSaveText = fInput.getText(); fInput.setText("Drag points to modify function."); fInput.setThrowErrors(false); fInput.setEnabled(false); } else { fWrapper.setFunction(fFunc); fInput.setText(fSaveText); fInput.setThrowErrors(true); fInput.setEnabled(true); } mainController.compute(); } else if (src == gCheck) { // Swap the g table function in or out boolean check = gCheck.getState(); if (check == gTableShown) return; gTableShown = check; gGraph.setVisible(!gTableShown); gTableGraph.setVisible(gTableShown); if (gTableShown) { gWrapper.setFunction(gTable); gSaveText = gInput.getText(); gInput.setText("Drag points to modify function."); gInput.setThrowErrors(false); gInput.setEnabled(false); } else { gWrapper.setFunction(gFunc); gInput.setText(gSaveText); gInput.setThrowErrors(true); gInput.setEnabled(true); } mainController.compute(); } } // end itemStateChanged() public void actionPerformed(ActionEvent evt) { // respond when the user clicks one of the control buttons. Object src = evt.getSource(); if (src == zoomInButton) { canvas.getCoordinateRect(0).zoomIn(); canvas.getCoordinateRect(1).zoomIn(); canvas.getCoordinateRect(2).zoomIn(); } else if (src == zoomOutButton) { canvas.getCoordinateRect(0).zoomOut(); canvas.getCoordinateRect(1).zoomOut(); canvas.getCoordinateRect(2).zoomOut(); } else if (src == restoreButton) { canvas.getCoordinateRect(0).restore(); canvas.getCoordinateRect(1).restore(); canvas.getCoordinateRect(2).restore(); } else if (src == equalizeButton) { canvas.getCoordinateRect(0).equalizeAxes(); canvas.getCoordinateRect(1).equalizeAxes(); canvas.getCoordinateRect(2).equalizeAxes(); } else if (src == fComputeButton) { if (fTableShown) { int ct = fTable.getPointCount(); double val; if (0 < canvas.getCoordinateRect(0).getYmin() || 0 > canvas.getCoordinateRect(0).getYmax()) val = canvas.getCoordinateRect(0).getYmin(); else val = 0; for (int i = 0; i < ct; i++) fTable.setY(i,val); } mainController.compute(); } else if (src == gComputeButton) { if (gTableShown) { int ct = gTable.getPointCount(); double val; if (0 < canvas.getCoordinateRect(1).getYmin() || 0 > canvas.getCoordinateRect(1).getYmax()) val = canvas.getCoordinateRect(1).getYmin(); else val = 0; for (int i = 0; i < ct; i++) gTable.setY(i,val); } mainController.compute(); } else { super.actionPerformed(evt); } } // end actionPerformed() protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // The example string should contain two function definitions, // separated by a semicolon. A function definition can be either // an expression or a table function. These espressions can be // followed by a semicolon and four or five numberd. // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The // next number is used as the x-coordinated that is selected on // the graph of the first function. int pos = example.indexOf(";"); if (pos == -1) { System.out.println("Illegal example -- must have two functions"); return; } String example2 = example.substring(pos+1); example = example.substring(0,pos).trim(); pos = example2.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // Get limits from example2 text. String nums = example2.substring(pos+1); example2 = example2.substring(0,pos).trim(); StringTokenizer toks = new StringTokenizer(nums, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } } if (toks.hasMoreTokens()) { // get the x coordinate try { double d = (new Double(toks.nextToken())).doubleValue(); pointX.setVal(d); } catch (NumberFormatException e) { } } } // Set up the example data and recompute everything. if (example.startsWith("table")) { // install table for f(x) from example try { TableFunction tf = parseTableFuncDef(example); fTable = tf; fTableGraph.setFunction(tf); fWrapper.setFunction(tf); if (!fTableShown) { // show table function if (fCheck != null) fCheck.setState(true); fGraph.setVisible(false); fTableGraph.setVisible(true); fTableShown = true; if (fInput != null) { fSaveText = fInput.getText(); fInput.setText("Drag points to modify function."); fInput.setThrowErrors(false); fInput.setEnabled(false); } } } catch (ParseError e) { System.out.println("Illegal table function for f(x) in example."); } } else { // install expression function for f(x) try { if (fInput != null) fInput.setText(example); else { Function f = new SimpleFunction( parser.parse(example), xVar ); fFunc = f; fGraph.setFunction(f); fWrapper.setFunction(f); } if (fTableShown) { // show expression function if (fCheck != null) fCheck.setState(false); fGraph.setVisible(true); fTableGraph.setVisible(false); fTableShown = false; if (fInput != null) { fInput.setThrowErrors(true); fInput.setEnabled(true); } } } catch (ParseError e) { System.out.println("Parse error for f(x) in example."); } } if (example2.startsWith("table")) { // install table for fg(x) from example try { TableFunction tg = parseTableFuncDef(example2); gTable = tg; gTableGraph.setFunction(tg); gWrapper.setFunction(tg); if (!gTableShown) { // show table function if (gCheck != null) gCheck.setState(true); gGraph.setVisible(false); gTableGraph.setVisible(true); gTableShown = true; if (gInput != null) { gSaveText = gInput.getText(); gInput.setText("Drag points to modify function."); gInput.setThrowErrors(false); gInput.setEnabled(false); } } } catch (ParseError e) { System.out.println("Illegal table function for g(x) in example."); } } else { // install expression function for g(x) try { if (gInput != null) gInput.setText(example2); else { Function g = new SimpleFunction( parser.parse(example2), xVar ); gFunc = g; gGraph.setFunction(g); gWrapper.setFunction(g); } if (gTableShown) { // show expression function if (gCheck != null) gCheck.setState(false); gGraph.setVisible(true); gTableGraph.setVisible(false); gTableShown = false; if (gInput != null) { gInput.setThrowErrors(true); gInput.setEnabled(true); } } } catch (ParseError e) { System.out.println("Parse error for g(x) in example."); } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); coords = canvas.getCoordinateRect(1); coords.setLimits(limits); coords.setRestoreBuffer(); coords = canvas.getCoordinateRect(2); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); } // end doLoadExample() } // end class FunctionComposition jcm1-source/SimpleGraph.java0000644000076500011320000003240511741343635015271 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // The SimpleGraph applet is a configurable applet that displays the graph of // a single function of one variable. Optionally, a point can be marked on // the graph. The user can control the location of the point. import java.awt.*; import java.applet.Applet; import java.util.StringTokenizer; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; public class SimpleGraph extends GenericGraphApplet { // Declare some private variables that are created in one method in // this class and used in a second method. private VariableInput xInput; // Contains the x-coordinate of the marked point. private Function func; // The function that is graphed. private Graph1D graph; // The graph of the function. private DrawGeometric point; // An oval that marks the selected point on the graph. private DrawGeometric vLine; // A line from the point to the x-axis. private DrawGeometric hLine; // A line from the point to the y-axis. protected void setUpCanvas() { // Override this to add more stuff to the canvas. super.setUpCanvas(); // Do the common setup: Add the axes and // When setUpCanvas is called, the functionInput already exists, if one is // to be used, since it is created in setUpBopttomPanel(), which is called // before setUpCanvas. If functionInput exists, add a graph of the function // from functionInput to the canvas. If not, create a graph of the function // specified by the parameter named "Function". if (functionInput != null) func = functionInput.getFunction(xVar); else { String def = getParameter("Function", " abs(" + xVar.getName() + ") ^ " + xVar.getName()); Function f = new SimpleFunction( parser.parse(def), xVar ); func = new WrapperFunction(f); } graph = new Graph1D(func); Color color = getColorParam("GraphColor"); if (color != null) graph.setColor(color); // If the applet is configured to mark a point on the graph, create the point and // the lines from the point to the x- and y-axes and add them to the canvas before // the graph. The properties of these objects have to be set later, in setUpMainPanel(), // because the input objects that they depend on don't exist when this method is // called. However, I want to add them to the canvas here so they will lie behind the // graph and behind the border of the canvas (which is added after setUpCanvas() is // executed). if (! "no".equalsIgnoreCase( getParameter("ShowPoint","yes") ) ) { vLine = new DrawGeometric(); hLine = new DrawGeometric(); point = new DrawGeometric(); canvas.add(vLine); canvas.add(hLine); canvas.add(point); } canvas.add(graph); // Finally, add the graph to the canvas. } // end setUpCanvas() protected void setUpMainPanel() { // Override to handle the point marked on the graph super.setUpMainPanel(); // Do the common setup if ( "no".equalsIgnoreCase( getParameter("ShowPoint","yes") ) ) { return; // If the applet is not configured to show a point, there is nothing to do. } // Create two input objects, a VariableInput and a VariableSlider. The values of // the two inputs will be synchronized with each other using a "Tie". The // minimum and maximum values represented on the slider are given by the // the minimum and maximum x-coordinates on the CoordinateRect. This will restrict // the x-coodinate of the point that is marked on the graph to the range of // x-values actually shown on the screen. xInput = new VariableInput(); // An input box for the x-coord of the marked point xInput.setInputStyle(VariableInput.REAL); // Allow only real numbers (not constant expressions) CoordinateRect coords = canvas.getCoordinateRect(); VariableSlider xSlider = new VariableSlider( coords.getValueObject(CoordinateRect.XMIN), coords.getValueObject(CoordinateRect.XMAX) ); Value yValue = new ValueMath(func,xSlider); // Represents the y-value of the marked point. DisplayLabel yDisplay = new DisplayLabel(" y = #", yValue); // Shows the y-value of the point // Create a panel to contain the input objects. JCMPanel panel = new JCMPanel(1,3); panel.setBackground(getColorParam("PanelBackground",Color.lightGray)); JCMPanel subpanel = new JCMPanel(); String varName = getParameter("Variable","x"); subpanel.add(new Label(" " + varName + " = ", Label.CENTER), BorderLayout.WEST); subpanel.add(xInput, BorderLayout.CENTER); panel.add(xSlider); panel.add(subpanel); panel.add(yDisplay); // If there is a functionInput box, then the SOUTH position of the mainPanel already contains // the inputPanel that contains that box. If so, add the new panel to the SOUTH position of // the inputPanel. (This is a good place, in general, to put extra input objects.) // If there is no inputPanel, then the SOUTH position of the mainPanel is empty, so put // the newly created panel there. Also, set the background color for the input panel from // from the PanelBackground applet param. (This is already done for inputPanel, if it exists.) if (inputPanel == null) mainPanel.add(panel, BorderLayout.SOUTH); else { inputPanel.setBackground(getColorParam("PanelBackground",Color.lightGray)); inputPanel.add(panel, BorderLayout.SOUTH); } // Set up all the data for the point and the lines from the point to the axes. // The objects where created in setUpCanvas() and added to the canvas. hLine.setPoints(new Constant(0),yValue,xSlider,yValue); hLine.setPoints(new Constant(0),yValue,xSlider,yValue); point.setShape(DrawGeometric.CROSS); point.setPoints(xSlider,yValue,5,5); point.setLineWidth(3); vLine.setPoints(xSlider,new Constant(0),xSlider,yValue); Color c = getColorParam("LineColor", Color.lightGray); vLine.setColor(c); hLine.setColor(c); c = getColorParam("DotColor", Color.gray); point.setColor(c); // Now, I have to set a Controller to respond to changes in the input objects. // I could just use the mainController, but then the data for the graph would // be recomputed whenever the user changes the x-coordinate of the marked point. // For effieciency, I will use a separate Controller that only recomputes the // data for the point (not the graph) when the inputs change. Controller cc = new Controller(); xInput.setOnTextChange(cc); // cc responds when user types in the input box xSlider.setOnUserAction(cc); // cc responds when the user drags the slider coords.setOnChange(cc); // cc responds when the coordinate limits change; // this is necessary because the minimum and // maximum values on the slider have to be checked. cc.add( xInput ); // Check whether the values have changed. cc.add( xSlider ); cc.add( new Tie(xSlider,xInput) ); // synchronize values of input box and slider cc.add( hLine ); // Recompute the values for the point and lines. cc.add( vLine ); cc.add( point ); cc.add( yDisplay ); // Recompute the value displayed on the yDisplay label. mainController.add(cc); // When the mainController recomputes (because function has // been changed, all the stuff controlled by cc also has // to be checked. mainController.remove(canvas); // The mainController should not recompute the contents // of the canvas (which it would do by default). mainController.add(graph); // But the mainController should recompute the graph. } // end setUpMainPanel() protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For the SimpleGraph applet, the example string should contain // an expression that defines the function to be graphed. This can optionally // be followed by a semicoloon and a list of four or five numbers. // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The // fifth number, if present, gives the x-coord of the marked point // on the graph. int pos = example.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // get limits from example text String limitsText = example.substring(pos+1); example = example.substring(0,pos); StringTokenizer toks = new StringTokenizer(limitsText, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } if (toks.countTokens() > 0 && xInput != null) { // get x-coord of marked point from example text try { Double d = new Double(toks.nextToken()); xInput.setVal( d.doubleValue() ); } catch (NumberFormatException e) { } } } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); } else { // If there is no user input, set the function in the graph directly. // Also, in this case, func is a "WrapperFunction". Set the // definition of that WrapperFunction to be the same as f try { Function f = new SimpleFunction( parser.parse(example), xVar ); ((WrapperFunction)func).setFunction(f); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); } // end doLoadExample() } // end class SimpleGraph jcm1-source/Evaluator.java0000644000076500011320000003032111741343635015013 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ import edu.hws.jcm.awt.*; import edu.hws.jcm.data.*; import edu.hws.jcm.draw.*; import edu.hws.jcm.functions.*; import java.awt.*; import java.awt.event.*; import java.util.StringTokenizer; import java.applet.Applet; /** * An Evaluator applet lets the user enter the values of one or more variables, * and it displayes the values of one or more expressions that can involve those * variables. The expression values are updated continuously as the user types. */ public class Evaluator extends Applet implements ActionListener { private Frame frame; // If non-null, a separate window. private String frameTitle; // Title for the separate window. private Button launchButton; // If non-null, then clicking this buttons opens a separate window. private String launchButtonName; // Name for the launch button. /** * The init() method is called by the system to set up the applet. * If the applet does not appear as a button, then init() creates the main panel of the applet * and calls setUpMainPanel to set it up. */ public void init() { frameTitle = getParameter("FrameTitle"); // Get title to be used for separate window, if any. if (frameTitle == null) { frameTitle = "Calculator"; int pos = frameTitle.lastIndexOf('.'); if (pos > -1) frameTitle = frameTitle.substring(pos+1); } setLayout(new BorderLayout()); int height = getSize().height; launchButtonName = getParameter("LaunchButtonName"); if ( (height > 0 && height <= 35) || launchButtonName != null) { // Use a separater window and only show a button in the applet. if (launchButtonName == null) launchButtonName = "Launch " + frameTitle; launchButton = new Button(launchButtonName); add(launchButton, BorderLayout.CENTER); launchButton.addActionListener(this); } else { // Show the main panel in the applet, not in a separate window. add(makeMainPanel(), BorderLayout.CENTER); } } /* * Create the main panel of the applet. */ public JCMPanel makeMainPanel() { // Get values of color params. Color background = getColorParam("BackgroundColor", Color.gray); Color labelBackground = getColorParam("LabelBackground", new Color(225,225,225)); Color labelForeground = getColorParam("LabelForeground", new Color(0,0,200)); Color answerBackground = getColorParam("AnswerBackground", labelBackground); Color answerForeground = getColorParam("AnswerForeground", Color.red); Color inputBackground = getColorParam("InputBackground", Color.white); Color inputForeground = getColorParam("InputForeground", Color.black); // Create the panel and subpanel. The left subpanel will hold labes for the // variables and expressions. The right subpanel will hold the variable input // boxes and expression values. JCMPanel panel = new JCMPanel(5); panel.setBackground(background); panel.setInsetGap(3); setLayout(new BorderLayout()); add(panel,BorderLayout.CENTER); JCMPanel left = new JCMPanel(0,1,3); panel.add(left, BorderLayout.CENTER); JCMPanel right = new JCMPanel(0,1,3); panel.add(right, BorderLayout.EAST); // Create a parser and configure it to allow factorials and summations. Parser parser = new Parser(); parser.addOptions(Parser.FACTORIAL); parser.add( new SummationParser() ); // Create the variable input boxes, using variable names given by // applet parameters. If no names are provided in applet parameters, // use one variable named "x". Add the Variables from the variable inputs // to the parser so that they can be used in expressions. int ct = 0; String variableName = getParameter("Variable"); if (variableName == null) { variableName = getParameter("Variable1"); if (variableName == null) { variableName = "x"; } else ct = 1; } String firstVar = variableName; while (variableName != null) { String valString = "0"; variableName = variableName.trim(); int pos = variableName.indexOf(" "); if (pos > 0) { // If there is anything in the string after the variable name, use it as the variable value. valString = variableName.substring(pos+1).trim(); variableName = variableName.substring(0,pos); } Label lab = new Label(" Input: " + variableName + " = ", Label.RIGHT); lab.setBackground(labelBackground); lab.setForeground(labelForeground); left.add(lab); VariableInput v = new VariableInput(variableName,valString,parser); v.setBackground(inputBackground); v.setForeground(inputForeground); v.setThrowErrors(false); v.setOnTextChange(panel.getController()); v.setOnUserAction(panel.getController()); right.add(v); ct++; variableName = getParameter("Variable" + ct); } // Get the expressions to be evalueated from applet parameters and add evaluators // to the applet. If not expressions are provided in applet parameters, use // one expression, "log2(x)". ct = 0; String function = getParameter("Expression"); if (function == null) { function = getParameter("Expression1"); if (function == null) function = "log2(" + firstVar + ")"; else ct = 1; } while (function != null) { Label lab = new Label(" " + function + " = ", Label.RIGHT); lab.setBackground(labelBackground); lab.setForeground(labelForeground); left.add(lab); try { DisplayLabel d = new DisplayLabel("#", parser.parse(function)); d.setBackground(answerBackground); d.setForeground(answerForeground); d.setAlignment(Label.CENTER); right.add(d); } catch (ParseError e) { right.add(new Label("invalid function")); } ct++; function = getParameter("Expression" + ct); } return panel; } // end makeMainPanel() /** * Respond when user clicks a button; not meant to be called directly. * This opens and closes the separate window. */ synchronized public void actionPerformed(ActionEvent evt) { Object source = evt.getSource(); if (source == launchButton && launchButton != null) { launchButton.setEnabled(false); if (frame == null) { frame = new Frame(frameTitle); frame.add(makeMainPanel()); frame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { frame.dispose(); } public void windowClosed(WindowEvent evt) { frameClosed(); } } ); frame.pack(); frame.setLocation(50,50); frame.show(); launchButton.setLabel("Close Window"); launchButton.setEnabled(true); } else { frame.dispose(); } } } synchronized private void frameClosed() { // respond when separate window closes. frame = null; launchButton.setLabel(launchButtonName); launchButton.setEnabled(true); } /** * Get The value of an applet parameter that specifies a color. The color can be specfied * as a list of three numbers in the range 0 to 255 or by one of the standard color names * ("black", "red", "blue", "green", "yellow", "cyan", "magenta", "gray", "darkgray", * "lightgray", "pink", "orange", "white"). Color names are not case sensitive. * * @param paramName The name of the applet parameter. * @param defaultColor The value to be returned if getParameter(paramName) is null or is not a valid color. */ protected Color getColorParam(String paramName, Color defaultColor) { String data = getParameter(paramName); if (data == null || data.trim().length() == 0) return defaultColor; data = data.trim(); if (Character.isLetter(data.charAt(0))) { for (int i = 0; i < colorNames.length; i++) if (data.equalsIgnoreCase(colorNames[i])) return colors[i]; return defaultColor; } else { StringTokenizer tokenizer = new StringTokenizer(data," \t,;"); int count = tokenizer.countTokens(); if (count < 3) return defaultColor; double[] nums = new double[3]; for (int i = 0; i < 3; i++) { try { Double d = new Double(tokenizer.nextToken()); nums[i] = d.doubleValue(); } catch (NumberFormatException e) { return defaultColor; } } if (nums[0] < 0 || nums[0] > 255 || nums[1] < 0 || nums[1] > 255 || nums[2] < 0 || nums[2] > 255) return defaultColor; return new Color((int)Math.round(nums[0]), (int)Math.round(nums[1]), (int)Math.round(nums[2])); } } private String[] colorNames = { "black", "red", "blue", "green", "yellow", "cyan", "magenta", "gray", "darkgray", "lightgray", "pink", "orange", "white" }; private Color[] colors = { Color.black, Color.red, Color.blue, Color.green, Color.yellow, Color.cyan, Color.magenta, Color.gray, Color.darkGray, Color.lightGray, Color.pink, Color.orange, Color.white }; } // end class evaluator jcm1-source/RiemannSums.java0000644000076500011320000003220711741343635015317 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // The SimpleGraph applet is a configurable applet that displays the graph of // a single function of one variable and computes a Riemann sum for that // function. import java.awt.*; import java.awt.event.*; import java.applet.Applet; import java.util.StringTokenizer; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; public class RiemannSums extends GenericGraphApplet { // Declare some private variables that are created in one method in // this class and used in a second method. private Variable intervals; // Gives the number of intervals. private VariableInput intCtInput; // For letting the user enter the number of intervals. private Choice methodChoice; // Gives the method to be used: Left Endpoint, ..., Trapezoid. private Function func; // The function that is graphed. private Graph1D graph; // The graph of the function. private RiemannSumRects sums; public void itemStateChanged(ItemEvent evt) { // React when user changes the summation method. if (evt.getSource() == methodChoice) { sums.setMethod(methodChoice.getSelectedIndex()); mainController.compute(); } else super.itemStateChanged(evt); } protected void setUpParameterDefaults() { // Override to give a different default function parameterDefaults = new java.util.Hashtable(); String func = " 3 / (1 + " + getParameter("Variable","x") + "^2)"; parameterDefaults.put("Function",func); parameterDefaults.put("ComputeButtonName","Compute!"); } protected void setUpCanvas() { // Override this to add more stuff to the canvas. // When setUpCanvas is called, the functionInput already exists, if one is // to be used, since it is created in setUpBopttomPanel(), which is called // before setUpCanvas. If functionInput exists, add a graph of the function // from functionInput to the canvas. If not, create a graph of the function // specified by the parameter named "Function". if (functionInput != null) func = functionInput.getFunction(xVar); else { String def = getParameter("Function", " abs(" + xVar.getName() + ") ^ " + xVar.getName()); Function f = new SimpleFunction( parser.parse(def), xVar ); func = new WrapperFunction(f); } graph = new Graph1D(func); Color color = getColorParam("GraphColor"); if (color != null) graph.setColor(color); // Get the data for the RiemannSumRects object. Create it (along with other // associated objects) and add it to the canvas. double[] intCtD = getNumericParam("IntervalCount"); if (intCtD == null || intCtD.length < 1) intCtD = new double[] { 5 }; else if (Double.isNaN(intCtD[0]) || intCtD[0] < 1 || intCtD[0] > 5000) intCtD[0] = 5; int intCt = (int)(intCtD[0]+0.5); if ("yes".equalsIgnoreCase( getParameter("UseIntervalInput", "yes") )){ intCtInput = new VariableInput(null, "" + intCt); intCtInput.setInputStyle(VariableInput.INTEGER); intCtInput.setMin(1); intCtInput.setMax(5000); intervals = intCtInput.getVariable(); } else intervals = new Variable(null,intCt); int method = RiemannSumRects.LEFTENDPOINT; String methodStr = getParameter("Method"); if (methodStr != null && methodStr.trim().length() > 0) { switch (methodStr.trim().charAt(0)) { case 'L': case 'l': method = RiemannSumRects.LEFTENDPOINT; break; case 'R': case 'r': method = RiemannSumRects.RIGHTENDPOINT; break; case 'M': case 'm': method = RiemannSumRects.MIDPOINT; break; case 'C': case 'c': method = RiemannSumRects.CIRCUMSCRIBED; break; case 'I': case 'i': method = RiemannSumRects.INSCRIBED; break; case 'T': case 't': method = RiemannSumRects.TRAPEZOID; break; } } if ("yes".equalsIgnoreCase( getParameter("UseMethodInput", "yes") )) { methodChoice = new Choice(); methodChoice.add("Left Endpoint"); methodChoice.add("Right Endpoint"); methodChoice.add("Midpoint"); methodChoice.add("~Circumscribed"); methodChoice.add("~Inscribed"); methodChoice.add("Trapezoid"); methodChoice.select(method); methodChoice.addItemListener(this); } sums = new RiemannSumRects(func,intervals); sums.setMethod(method); canvas.add(sums); Color c = getColorParam("RectColor"); if (c != null) sums.setColor(c); c = getColorParam("OutlineColor"); if (c != null) sums.setOutlineColor(c); super.setUpCanvas(); // Do the common setup: Add the axes, for example. canvas.getCoordinateRect().setGap(10); // Extra space around edges. canvas.add(graph); // Add the graph to the canvas. // Add a DrawString to the canvas to show the value of the sum. DrawString ds = new DrawString("sum = #", DrawString.TOP_LEFT, new Value[] { sums.getValueObject(RiemannSumRects.CURRENT_METHOD) } ); ds.setBackgroundColor(getColorParam("TextBackground",Color.white)); ds.setColor(getColorParam("TextColor",Color.black)); ds.setFrameWidth(1); canvas.add(ds); mainController.add(ds); mainController.add(sums); if (intCtInput != null) intCtInput.setOnUserAction(mainController); canvas.getCoordinateRect().setOnChange(mainController); } // end setUpCanvas() protected void setUpMainPanel() { // Override this method to add the methodChoice menu and interval count inputs, if any, // to the panel. (They were created in the setUpCanvas method.) super.setUpMainPanel(); // Do the common setup if (methodChoice == null && intCtInput == null) return; JCMPanel panel = new JCMPanel(); panel.setLayout(new FlowLayout()); panel.setBackground(getColorParam("PanelBackground",Color.lightGray)); if (intCtInput != null) { panel.add(new Label("Intervals:")); panel.add(intCtInput); } if (methodChoice != null) { panel.add(new Label("Method:")); panel.add(methodChoice); } if (inputPanel == null) mainPanel.add(panel, BorderLayout.SOUTH); else { inputPanel.setBackground(getColorParam("PanelBackground",Color.lightGray)); inputPanel.add(panel, BorderLayout.SOUTH); } } // end setUpMainPanel() protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For this applet, the example string should contain // an expression that defines the function to be graphed. // This can optionally be followed by a semicoloon and a list of four or five // numbers. The first four numbers give the x- and y- limes to be used for the // example. The fifth number, if present, gives the interval count. // After the numbers, there can be another semicolon // and the name of the summation method to be used. int pos = example.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // get limits, method from example text String limitsText = example.substring(pos+1); example = example.substring(0,pos); pos = limitsText.indexOf(";"); if (pos > 0) { // data includes a method name. String methodStr = limitsText.substring(pos+1).trim(); limitsText = limitsText.substring(0,pos); if (methodStr.length() > 0) { int method; switch (methodStr.charAt(0)) { case 'L': case 'l': method = RiemannSumRects.LEFTENDPOINT; break; case 'R': case 'r': method = RiemannSumRects.RIGHTENDPOINT; break; case 'M': case 'm': method = RiemannSumRects.MIDPOINT; break; case 'C': case 'c': method = RiemannSumRects.CIRCUMSCRIBED; break; case 'I': case 'i': method = RiemannSumRects.INSCRIBED; break; case 'T': case 't': method = RiemannSumRects.TRAPEZOID; break; default: method = -1; } if (method >= 0) { sums.setMethod(method); if (methodChoice != null) methodChoice.select(method); } } } StringTokenizer toks = new StringTokenizer(limitsText, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } if (toks.countTokens() > 0) { // get number of intervals try { Double d = new Double(toks.nextToken()); double intCtD = d.doubleValue(); if (intCtD < 1) intCtD = 1; else if (intCtD > 5000) intCtD = 5000; intervals.setVal((int)(intCtD + 0.5)); } catch (NumberFormatException e) { } } } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); } else { // If there is no user input, set the function in the graph directly. // Also, in this case, func is a "WrapperFunction". Set the // definition of that WrapperFunction to be the same as f try { Function f = new SimpleFunction( parser.parse(example), xVar ); ((WrapperFunction)func).setFunction(f); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); } // end doLoadExample() } // end class RiemannSums jcm1-source/Makefile0000644000076500011320000000025211741343635013646 0ustar djefacultyjar: classes jar cfv jcm.jar `find . -name "*.class"` classes: clean javac -target 1.1 -source 1.3 `find . -name "*.java"` clean: rm -f `find . -name "*.class"` jcm1-source/edu/0000755000076500011320000000000011741343635012764 5ustar djefacultyjcm1-source/edu/hws/0000755000076500011320000000000011741343635013565 5ustar djefacultyjcm1-source/edu/hws/jcm/0000755000076500011320000000000011741343635014336 5ustar djefacultyjcm1-source/edu/hws/jcm/awt/0000755000076500011320000000000011741343635015131 5ustar djefacultyjcm1-source/edu/hws/jcm/awt/ExpressionInput.java0000644000076500011320000004346111741343635021163 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // November 2005: Removed processKeyEvent to get rid of bogus "beep" when shift key is pressed. // (this also lets illegal characters into the input box) package edu.hws.jcm.awt; import edu.hws.jcm.data.*; import java.awt.*; import java.awt.event.*; /** * An ExpressionInput is an input box that allows the user * input a mathematical expression. There is an associated * object that belongs to the class Expression. The * value of this object can change only when checkInput() * is called. The checkInput() method is usually called by a Controller. *

An ExpressionInput will ordinarily be registered with * a Controller in TWO ways: It's added to a Controller * with the Controller's add() method. This makes the Contrller * call the ExpressionInput's checkInput() method during the * Controller's compute() method. Secondly, the Controller * is set as the "onUserAction" property. This causes the * Controller's compute() method to be called when the user * presses return in the ExpressionInput box. This is optional-- * you might, for example, only want the Controller to compute() * when a Compute button is pressed. You can also set the * ExpressionInput's onTextChange property to a Controller * if you want it to compute every time the text in the box * changes. *

Use the function getFunction() if you want to * use an ExpressionInput as a way of inputting a function. * */ public class ExpressionInput extends TextField implements InputObject, Value { /** * The Expression associate with this input box. * Class EI is a private nested class. */ protected EI expr; /** * A parser for parsing the user's input * expression. If this is null, * a default parser will be used and * only constant expressions will * be allowed. */ protected Parser parser; //protected boolean hasChanged; protected String previousContents; /** * True if an error should be thrown * when checkInput() is called, * but the content of the box is not * a legal expression. Otherwise, the * expression will become a constant * expression with value Double.NaN. */ protected boolean throwErrors; private Controller onUserAction; // If this is non-null, the compute() method // of onUserAction is called when the user // presses return in this input-box. private Controller onTextChange; // If this is non-null, the compute() method // of onTextChange is called when the text // in this input box changes /** * Error message from the most recent * time the input was checked by a * call to checkInput(). If this is * null, then no error occurred. */ protected String errorMessage; private long serialNumber; // This goes up by one every time checkInput() // is called and finds a change in the // user's input; /** * Create a new ExpressionFunction with no associated parser. This can only * be used to input constant expressions, unless you set a parser later with setParser(). */ public ExpressionInput() { this("",null); } /** * Create an ExpressionInputBox with initial contents given by initialValue. * (If initialValue is null, the empty string is used.) If p is not null, * then p will be used to parse the contents of the box. * * @param initialValue initial contents of ExpressionInputBox. * @param p if non-null, this parser will be used to parse contents of the ExpressionInputBox. */ public ExpressionInput(String initialValue, Parser p) { super(30); expr = new EI(); if (initialValue == null) initialValue = ""; super.setText(initialValue); setBackground(Color.white); //enableEvents(KeyEvent.KEY_EVENT_MASK); setParser(p); // (Sets previousContents to null, so checkInput() will actually check the input.) checkInput(); // Won't throw an error, since throwErrors is false. throwErrors = true; } /** * Set the parser that is used to parse the user's input strings. * If this is null, then a default parser is used that will * only allow constant expressions. * * @param p parser to register with user's input strings. */ public void setParser(Parser p) { parser = (p == null)? new Parser() : p; //hasChanged = true; // force re-compute when checkInput() is next called. previousContents = null; } /** * Get the Expression associated with this ExpressionInput. * */ public Expression getExpression() { return expr; } /** * Get a function of one variable whose value at a real number * x is computed by assigning x to the variable v and then * returning the value of the expression associated with this * ExpressionInput. Of couse, v should be one of the variables * registered with the Parser for this ExpressionInput, or else * in can never occur in the expression. * Note that the value of the variable v changes TEMPORARILY * when the function is evaluated. (So you should not use * a variable where setting the value has a side effect, * such as the variable associated with a SliderVariable.) * * @param v The function that is returned in a function of this variable. */ public Function getFunction(Variable v) { return new SimpleFunction(expr,v); } /** * Get a function of one or more variables whose value at arguments * x1, x2, ... is computed by assigning the x's to the variables and then * returning the value of the expression associated with this * ExpressionInput. Of couse, each v[i] should be one of the variables * registered with the Parser for this ExpressionInput. * Note that the value of the variables change TEMPORARILY * when the function is evaluated. * * @param v The function that is returned is a function of the variables in this array. */ public Function getFunction(Variable[] v) { return new SimpleFunction(expr,v); } /** * Return the current value of the expression associated with * this ExpressionInput. */ public double getVal() { return expr.getVal(); } /** * If the parameter c is non-null, then its compute method will be called whenever * the user presses the return key while typing in this text-input box. */ public void setOnUserAction(Controller c) { onUserAction = c; enableEvents(AWTEvent.ACTION_EVENT_MASK); } /** * Return the Controller, if any, that is notified when the user * presses return in this text-input box. */ public Controller getOnUserAction() { return onUserAction; } /** * Method required by InputObject interface; in this class, it simply calls * setOnUserAction(c). This is meant to be called by JCMPanel.gatherInputs(). */ public void notifyControllerOnChange(Controller c) { setOnUserAction(c); } /** * If the parameter, c, is non-null, then its compute method will be called whenever * the text in this input box changes. Furthermore, the throwErrors * property will be set to false, to avoid throwing multiple errors * while the user is typing. (You can change it back to true if * you want by calling setThrowErrors(true).) It would only rarely make sense to * use this feature. */ public void setOnTextChange(Controller c) { onTextChange = c; enableEvents(AWTEvent.TEXT_EVENT_MASK); if (c != null) throwErrors = false; } /** * Return the Controller, if any, that is notified whenever the text * in this input box changes */ public Controller getOnTextChange() { return onTextChange; } /** * Set the throwErrors property. When this is true, a JCMError can be thrown * when checkInput() is called an a parse error is found in the contents of the input box. * If throwErrors is false, no error is thrown. Instead, * the expression is set to a constant expression with value Double.NaN. */ public void setThrowErrors(boolean throwErrors) { this.throwErrors = throwErrors; } /** * Return the value of the throwErrors property, which determines whether errors * can be thrown when checkInput() is called. */ public boolean getThrowErrors() { return throwErrors; } /** * Get error message from previous call to checkInput(). * Returns null if and only if there was no error. */ public String getErrorMessage() { return errorMessage; } //---------------- Some implementation details ------------------------------------------------- /** * Get the expression from the box, maybe throw a JBCError * if a ParseError occurs. This is meant to be called by a Controller, in general. * The expression associated with this ExpressionInput can only change when this * method is called; it DOES NOT change continuously as the user types. */ public void checkInput() { boolean hasChanged = previousContents == null || !previousContents.equals(getText()); if (!hasChanged) return; expr.serialNumber++; String contents = getText(); try { expr.exp = parser.parse(contents); errorMessage = null; previousContents = getText(); } catch (ParseError e) { expr.exp = null; if (throwErrors) { errorMessage = "Error in expression: " + e.getMessage(); setCaretPosition(e.context.pos); requestFocus(); throw new JCMError(e.getMessage(),this); } else errorMessage = "Error in expression at position " + e.context.pos + ": " + e.getMessage(); } } /** * Set the text displayed in this input box. This overrides TextField.setText * to make sure that the expression will be recomputed the next time * checkInput() is called. */ public void setText(String str) { super.setText(str); //hasChanged = true; previousContents = null; } /** * Override processKeyEvent to only allow characters * that are legal in expressions. This is not meant to be called directly. public void processKeyEvent(KeyEvent evt) { if (evt.getID() == KeyEvent.KEY_PRESSED) { int ch = evt.getKeyCode(); char chr = evt.getKeyChar(); boolean use = (chr != 0 && (Character.isDigit(chr) || Character.isLetter(chr)) || chr == '.' || chr == '(' || chr == ')' || chr == '-' || chr == '+' || chr == '*' || chr == '/' || chr == '^' || chr == ',' || chr == ':' || chr == '?' || chr == '|' || chr == '&' || chr == '~' || chr == '=' || chr == '<' || chr == '>' || chr == '!') || ch == KeyEvent.VK_DELETE || ch == KeyEvent.VK_SPACE || ch == KeyEvent.VK_BACK_SPACE; boolean useControl = use || ch == KeyEvent.VK_TAB || ch ==KeyEvent.VK_ENTER || chr == 0; if (!useControl) { evt.consume(); Toolkit.getDefaultToolkit().beep(); } else if (use) hasChanged = true; } super.processKeyEvent(evt); } */ /** * Overridden to call onUserAction.compute() if onUserAction is non-null. * This is not meant to be called directly */ public void processActionEvent(ActionEvent evt) { if (onUserAction != null) onUserAction.compute(); super.processActionEvent(evt); } /** * Overridden to call onUserAction.compute() if onUserAction is non-null. * This is not meant to be called directly. */ public void processTextEvent(TextEvent evt) { if (onTextChange != null) onTextChange.compute(); super.processTextEvent(evt); } /** * The expression associated with an ExpressionInput belongs to this class. * So is any derivative of such a function. Note that derivatives * must be recomputed when the expression changes. This is done * via "lazy evaluation", that is, only when necessary. When * a derivative is used, it tests whether it is out of date * by comparing its serialNumber to the serial number of the * expression that it is the derivative of. If they don't match, * then the expression is recomputed and the serial number is updated. * The serial number and defintion of the main expresssion is changed by * checkInput() whenever the user's input has changed. */ protected class EI implements Expression { /** * The actual expression, or null if the * expression is undefined. If this is a * derivative of another EI, this will be * recomputed as necessary when the expression is used * in some way. */ ExpressionProgram exp; /** * This is null for the original expression input by the * user. If this EI was formed by taking the derivative * of anotehr EI, that EI is stored here. */ EI derivativeOf; /** * Which Variable is this a derivative with respect to? * If derivativeOf is null, so is wrt. */ Variable wrt; /** * For the original expression input by the user, this * goes up by one each time checkInput() is called and * finds a change in the user's input. For derivative * EI, this is the serial number of "derivativeOf" at * the time this derivative expression was last computed. */ int serialNumber; EI() { serialNumber = -1; // Forces exp to be computed the first time it is needed. } public double getVal() { checkForChanges(); if (exp == null) return Double.NaN; return exp.getVal(); } public double getValueWithCases(Cases c) { checkForChanges(); if (exp == null) return Double.NaN; return exp.getValueWithCases(c); } public String toString() { checkForChanges(); if (exp == null) return "(undefined)"; return exp.toString(); } public Expression derivative(Variable wrt) { EI deriv = new EI(); deriv.derivativeOf = this; deriv.wrt = wrt; return deriv; } public boolean dependsOn(Variable x) { checkForChanges(); return exp.dependsOn(x); } void checkForChanges() { if (derivativeOf != null) { derivativeOf.checkForChanges(); if (serialNumber != derivativeOf.serialNumber) { serialNumber = derivativeOf.serialNumber; if (errorMessage != null) exp = null; else exp = (ExpressionProgram)derivativeOf.exp.derivative(wrt); } } } } // end nested class EI } // end class ExpressionInput jcm1-source/edu/hws/jcm/awt/ComputeButton.java0000644000076500011320000001000311741343635020576 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; import java.awt.*; import java.awt.event.*; /** * A compute button is a button that can have an associated Controller. * When the user clicks the button, the compute() method of the * Controller is called. This class really just exists for convenience. */ public class ComputeButton extends Button { private Controller onUserAction; // The Controller whose compute() // method is called when the user clicks // the button. /** * Create a Compute button labeled "Compute!". */ public ComputeButton() { this("Compute!"); } /** * Create a Compute button displaying the given text. */ public ComputeButton(String label) { super(label); setBackground(Color.lightGray); enableEvents(AWTEvent.ACTION_EVENT_MASK); } /** * Set the controller whose compute() method is called * when the user clicks this button. */ public void setOnUserAction(Controller c) { onUserAction = c; } /** * Return the controlller whose compute() method is * called when the user clicks this button. */ public Controller getOnUserAction() { return onUserAction; } /** * This is called by the system when the user clicks the * button. Not meant to be called directly. */ public void processActionEvent(ActionEvent evt) { if (onUserAction != null) onUserAction.compute(); super.processActionEvent(evt); } } // end class ComputeButton jcm1-source/edu/hws/jcm/awt/Tieable.java0000644000076500011320000000752111741343635017346 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; /** * A Tieable object has an associated serial number. The value of the serial * should increase when the value of the object changes. A Tieable can "sync" with another * Tieable, presumably by copying its serial number and other information. * A given Tieable might only be able to synchronize with other Tiebles of * certain types. If its sync() method is called with an object of the wrong * type, it should probably thrown an IllegalArguemntException. * * See the "Tie" and "Controller" classes for information about how Tieable * are used. * */ public interface Tieable extends java.io.Serializable { /** * Get the serial number associated with this Tieable. If the * value of this Tieable changes, then the serial number should * increase. */ public long getSerialNumber(); /** * This routine is called to tell this Tieable that the serial * numbers of the Tieables that have been added to the Tie do not * match. newest has a serial number that is at least as * large as the serial number of any other Tieable in the Tie. * This Tieable should synchronize its value and serial number * with the "newest" Tieables. * (Note: So far, I haven't found any reason to use * the Tie parameter in this method! Maybe it should be removed.) */ public void sync(Tie tie, Tieable newest); } jcm1-source/edu/hws/jcm/awt/Controller.java0000644000076500011320000003370611741343635020130 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; import edu.hws.jcm.data.*; import java.awt.*; import java.awt.event.*; import java.util.Vector; /** * Controllers are the focus of all the action in the JCM system. A Controller can be * set to listen for changes (generally changes in user input). This is done by * registering the Controller with (usally) an InputObject. For example, if a Controller, c, * is to respond when the user presses return in a VariableInput, v, then * v.setOnUserAction(c) should be called to arrange to have the Controller listen * for such actions. VariableSliders, ExpressionInputs, MouseTrackers, Animators have a similar * methods. It is also possible to set the Controller to listen for events of * type AdjustmentEvent, ActionEvent, TextEvent, or ItemEvent (but this feature is * left over from an older version of JCM, and I'm not sure whether it's necessary). * Whenever a Controller learns of some change, it will process any InputObjects, * Ties, and Computables that have been registered with it. * *

InputObjects and Computables have to be added to a Controller to be processed, * using the Controller's add method. (If you build your inteface out of JCMPanels, * then this is done automatically.) (Note that an InputObject is added to a Controller * to have its value checked -- This is separate from registering the Controller to * listen for changes in the InputObject. Often, you have to do both.) The gatherInputs() * method in class JCMPanel can be used to do most of this registration automaticaly. * *

A Tie that synchronizes two or more Values, to be effective, has to be added to a Controller. * See the Tie class for inforamtion about what Ties are and how they are used. * *

A Controller can have an associated ErrorReporter, which is used to report any * errors that occur during the processing. Currently, an ErrorReporter is either * a DisplayCanvas or a MessagePopup. * *

A Controller can be added to another Controller, which then becomes a sub-controller. * Whenever the main Controller hears some action, it also notifies all its sub-controllers * about the action. Furthermore, it can report errors that occur in the sub-controllers, * if they don't have their own error reporters. (Usually, you will just set an error * reporter for the top-level Controller.) **/ public class Controller implements java.io.Serializable, Computable, InputObject, AdjustmentListener, ActionListener, TextListener, ItemListener { /** * Computable objects controlled by this controller. Note that Controllers * are Computables, so this list can include sub-controllers. */ protected Vector computables; /** * InputObjects controlled by this controller. Note that Controllers * are InputObjects, so this list can include sub-controllers. */ protected Vector inputs; /** * Ties that have been added to this controller. */ protected Vector ties; /** * Used for reporting errors that occur in the * compute() method of this controller. If the errorReporter * is null and if this controller has a parent, * then the parent will report the error. If * no ancestor has an errorReporter, the error * message is written to standard output. */ protected ErrorReporter errorReporter; /** * The parent of this controller, if any. * This is set automatically when one * controller is added to another. */ protected Controller parent; /** * If non-null, this is an error message * that has been reported and not yet cleared. */ protected String errorMessage; /** * Create a Controller. */ public Controller() { } /** * Set the ErrorReporter used to report errors that occur when the * compute() method of this Controller is executed. */ public void setErrorReporter(ErrorReporter r) { errorReporter = r; } /** * Get the ErrorReporter for this Controller. Return null if there is none. */ public ErrorReporter getErrorReporter() { return errorReporter; } /** * Add an object to be controlled by this controller. It should be of * one or more of the types InputObject, Computable, Tie. If it is * a Controller, then this Controller becomes its parent. */ public void add(Object obj) { if (obj == null) return; if (obj instanceof Controller) { Controller c = (Controller)obj; if (c.parent != null) c.parent.remove(this); c.parent = this; } if (obj instanceof Computable) { if (computables == null) computables = new Vector(); computables.addElement(obj); } if (obj instanceof InputObject) { if (inputs == null) inputs = new Vector(); inputs.addElement(obj); } if (obj instanceof Tie) { if (ties == null) ties = new Vector(); ties.addElement(obj); } } /** * Remove the object from the controller (if present). */ public void remove(Object obj) { if (obj == null) return; if (computables != null) { computables.removeElement(obj); if (computables.size() == 0) computables = null; } if (inputs != null) { inputs.removeElement(obj); if (inputs.size() == 0) inputs = null; } if (ties != null) { ties.removeElement(obj); if (ties.size() == 0) ties = null; } if (obj instanceof Controller && ((Controller)obj).parent == this) ((Controller)obj).parent = null; } /** * If this controller has a parent, remove it from its parent. (Then, a call to the * former parent's compute() method will not call this controller's compute().) */ public void removeFromParent() { if (parent != null) parent.remove(this); } // ----------------- Listening for events ---------------------- /** * Simply calls compute when the Controller hears an ActionEvent. * This is not meant to be called directly. */ public void actionPerformed(ActionEvent evt) { compute(); } /** * Simply calls compute when the Controller hears a TextEvent. * This is not meant to be called directly. */ public void textValueChanged(TextEvent evt) { compute(); } /** * Simply calls compute when the Controller hears an AdjustmantEvent. * This is not meant to be called directly. */ public void adjustmentValueChanged(AdjustmentEvent evt) { compute(); } /** * Simply calls compute when the Controller hears an ItemEvent. * This is not meant to be called directly. */ public void itemStateChanged(ItemEvent evt) { compute(); } // -------------- Implementation and error-handling ---------------------- /** * When an contoller computes, it first calls checkInput() for any * InputOjects that it controls (including those in sub-controllers). * It then handles any Ties. Finally, * it calls the compute() method of any Computables. If an error * occurs, it reports it. JCMErrors (which should represent errors * on the part of the user) will generally only occur during the * checkInput() phase. Internal, programmer errors can occur at * any time and might leave the sytem in an unhappy state. They are * reported as debugging aids for the programmer. When one occurs, * a stack trace is printed to standard output. */ synchronized public void compute() { try { checkInput(); doTies(); clearErrorMessage(); doCompute(); } catch (JCMError e) { if (errorMessage == null || !errorMessage.equals(e.getMessage())) reportError(e.getMessage()); } catch (RuntimeException e) { reportError("Internal programmer's error detected? " + e); e.printStackTrace(); } } /** * Call checkInput() of each InputObject. Can throw a JCMError. * This is mostly meant to be called by Controller.compute(). * Note that this will recurse though any sub-controllers of * this controller, so that when comput() is called, * all the InputObjects in the sub-controllers * are processed before ANY Tie or Computable is processed. * Similarly, the Ties and Computables in the sub-controllers * are processed in separate passes. */ public void checkInput() { if (inputs != null) { int top = inputs.size(); for (int i = 0; i < top; i++) ((InputObject)inputs.elementAt(i)).checkInput(); } } /** * Check the Ties in this controller and its sub-controllers. */ protected void doTies() { if (inputs != null) { int top = inputs.size(); for (int i = 0; i < top; i++) if (inputs.elementAt(i) instanceof Controller) ((Controller)inputs.elementAt(i)).doTies(); } if (ties != null) { int top = ties.size(); for (int i = 0; i < top; i++) ((Tie)ties.elementAt(i)).check(); } } /** * Compute the Computables in this controller and its sub-controllers. */ protected void doCompute() { if (computables != null) { int top = computables.size(); for (int i = 0; i < top; i++) { Object obj = computables.elementAt(i); if (obj instanceof Controller) ((Controller)obj).doCompute(); else ((Computable)obj).compute(); } } } /** * Report the specified error message. */ public void reportError(String message) { if (message == null) clearErrorMessage(); if (errorReporter != null) { errorReporter.setErrorMessage(this,message); errorMessage = message; } else if (parent != null) parent.reportError(errorMessage); else { errorMessage = message; System.out.println("***** Error: " + errorMessage); } } /** * Clear the error message. */ protected void clearErrorMessage() { if (errorReporter != null) errorReporter.clearErrorMessage(); else if (parent != null) parent.clearErrorMessage(); errorMessage = null; } /** * Should be called by the ErrorReporter if the ErrorReporter clears the error itself. * (This is only used to avoid repeatedly setting the same error message, during an * animation for example.) */ public void errorCleared() { errorMessage = null; } /** * Method required by InputObject interface; in this class, calls the same method * recursively on any input objects controlled by this controller. This is meant to * be called by JCMPanel.gatherInputs(). */ public void notifyControllerOnChange(Controller c) { if (inputs != null) { int top = inputs.size(); for (int i = 0; i < top; i++) ((InputObject)inputs.elementAt(i)).notifyControllerOnChange(c); } } /** * Calles notifyControllerOnChange(this). That is, it sets all the InputObjects in * this Controller, and in subcontrollers, to notify this Controller when they change. */ public void gatherInputs() { notifyControllerOnChange(this); } } // end class Controller jcm1-source/edu/hws/jcm/awt/Limits.java0000644000076500011320000000625211741343635017242 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; /** * The Limits interface is implemented by edu.hws.jcm.data.CoordinateRect * and by other objects that can be "Tied" to a CoordinateRect, such as * LimitControlPanel. This will be used to synchronize the (x,y) * limits on the CoordinateRect with limit values obtained elsewhere. * See the Tie class for more information. * * @author David Eck */ public interface Limits extends java.io.Serializable { /** * Return a 4-element array containing xmin, xmax, ymin, and ymax. */ public double[] getLimits(); /** * Set the current limits. * * @param A 4-element array containing the new xmin, xmax, ymin, and ymax. */ public void setLimits(double[] limits); } jcm1-source/edu/hws/jcm/awt/MessagePopup.java0000644000076500011320000002446511741343635020417 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; import java.awt.*; import java.awt.event.*; import java.util.Vector; /** * The class MessagePopup represents a Window that pops up to display an error * message. A MessagePopup object is created by a "source" component. If * that component is contained in a Frame, then the popup will be a modal dialog * box with that Parent. If the component is not in a Frame (or is null), then an * independent Frame is used. The message box is popped up when reportError() is * called. It is closed either when the user clicks the OK button, * or if clearErrorMessage() is called. */ public class MessagePopup implements ActionListener, ErrorReporter { private String errorMessage; private Controller errorSource; private Component source; private Window popup; /** * Create a MessagePopup with the give source component. If source is null, then * an independent window will always be used to show the error message. */ public MessagePopup(Component source) { this.source = source; } /** * Show the given message in a dialog box or independent window, * depending on whether the source component is contained in * a Frame or not. * * @param c The Controller that calls this method, or null if it is not called by a Controller. * (The Controller, if any, will be notified when the error message is cleared.) * @param message The message to display. */ public void setErrorMessage(Controller c, String message) { if (popup != null) clearErrorMessage(); if (message == null) return; errorSource = c; errorMessage = message; Component parent = source; while (parent != null && !(parent instanceof Frame)) parent = parent.getParent(); if (parent != null) popup = new Dialog((Frame)parent,"Error Message",true); // modal dialog else popup = new Frame("Error Message"); // independent window popup.setBackground(Color.white); popup.add(new MC(message), BorderLayout.CENTER); Panel buttonBar = new Panel(); buttonBar.setLayout(new FlowLayout(FlowLayout.RIGHT,10,10)); Button OK = new Button(" OK "); OK.addActionListener(this); buttonBar.add(OK); popup.add(buttonBar, BorderLayout.SOUTH); popup.pack(); if (parent == null) popup.setLocation(100,80); else popup.setLocation(parent.getLocation().x+50,parent.getLocation().y+30); popup.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent evt) { popup.dispose(); } }); popup.show(); // make the dialog visible. } /** * Get the currently displayed error message. The return value is null if no error message is being displayed. */ public String getErrorMessage() { return errorMessage; } /** * Clear the error message and close the window. This can be * called from outside this class. It is called automatically * when the user clicks the OK button or close box of the window * that displays the error message. */ synchronized public void clearErrorMessage() { if (popup == null) return; popup.dispose(); errorMessage = null; if (errorSource != null) errorSource.errorCleared(); errorSource = null; popup = null; } /** * Respond when user clicks OK. This is not meant to be called directly. */ public void actionPerformed(ActionEvent evt) { clearErrorMessage(); } /** * The nested class MC (Message Canvas) displays the message passed * to it in the constructor. Unless the message is very short, * it will be broken into multiple lines. */ private static class MC extends Canvas { private String message; // A copy of the message // The following data is computed in makeStringList() private Vector messageStrings; // The message broken up into lines. private int messageWidth; // The width in pixels of the message display. private int messageHeight; // The height in pixels of the message display. private Font font; // The font that will be used to display the message. private int lineHeight; // The height of one line in that font. private int fontAscent; // The font ascent of the font (disance from the // baseline to the top of a tall character.) /** * Constructor: store the message. * * @param message message to store. */ MC(String message) { if (message == null) this.message = ""; // this.message can't be null. else this.message = message; } /** * Return the message size, as determined by makeStringList(), allowing * space for a border around the message. * * @return the message size. */ public Dimension getPreferredSize() { if (messageStrings == null) makeStringList(); return new Dimension(messageWidth + 20, messageHeight + 20); } /** * Display the message using data stored in instance variables. * * @param g the Graphics context. */ public void paint(Graphics g) { if (messageStrings == null) makeStringList(); int y = (getSize().height - messageHeight)/2 + fontAscent; if (y < fontAscent) y = fontAscent; int x = (getSize().width - messageWidth)/2; if (x < 0) x = 0; g.setFont(font); for (int i = 0; i < messageStrings.size(); i++) { g.drawString( (String)messageStrings.elementAt(i), x, y); y += lineHeight; } } /** * Compute all the instance variables necessary for displaying * the message. If the total width of the message in pixels * would be more than 280, break it up into several lines. */ private void makeStringList() { messageStrings = new Vector(); font = new Font("Dialog", Font.PLAIN, 12); FontMetrics fm = getFontMetrics(font); lineHeight = fm.getHeight() + 3; fontAscent = fm.getAscent(); int totalWidth = fm.stringWidth(message); if (totalWidth <= 280) { messageStrings.addElement(message); messageWidth = 280; messageHeight = lineHeight; } else { if (totalWidth > 1800) messageWidth = Math.min(500, totalWidth/6); else messageWidth = 300; int actualWidth = 0; String line = " "; String word = ""; message += " "; // this forces word == "" after the following for loop ends. for (int i = 0; i < message.length(); i++) { if (message.charAt(i) == ' ') { if (fm.stringWidth(line + word) > messageWidth + 8) { messageStrings.addElement(line); actualWidth = Math.max(actualWidth,fm.stringWidth(line)); line = ""; } line += word; if (line.length() > 0) line += ' '; word = ""; } else { word += message.charAt(i); } } if (line.length() > 0) { messageStrings.addElement(line); actualWidth = Math.max(actualWidth, fm.stringWidth(line)); } messageHeight = lineHeight*messageStrings.size() - fm.getLeading(); messageWidth = Math.max(280,actualWidth); } } } // end nested class MC } // end class MessageDialog jcm1-source/edu/hws/jcm/awt/DisplayLabel.java0000644000076500011320000002271411741343635020347 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; import java.awt.*; import edu.hws.jcm.data.*; /** * A DisplayLabel is a label that can display numbers embedded in * strings. The text for the label can include '#' characters, which * are substituted by values of specified Value objects. (A doubled * ## is interpreted as a single literal # to be displayed rather than * substituted.) You should provide as many Values as there * are #'s in the text. However, no errors are generated if this * is not the case. Extra expressions are ignored; extra #'s are * shown as "undefined" in the display. In fact, DisplayLabels * do not ever generat JCMErrors. Note that Value objects include * objects of type Constant, Variable, and Expression, for example. Value * is just an interface defined in package edu.hws.jcm.data. * *

The values displayed in a DisplayLabel are recomputed when * the Label's compute() method is called. Usually, this is * done by a Controller that the DisplayLabel is registered with. * See the Controller class for more information. */ public class DisplayLabel extends Label implements Computable { /** * Unsubstituted text for display. */ protected String text; /** * Desired maximum number of characters in displayed numbers. */ protected int numSize = 10; /** * Value objects whose values will be * substituted for #'s in text. */ protected Value[] values; /** * Create a label with no expressions set up to display a * single number. Initial value is "undefined"; Use the * setValue() method to set the value to be displayed. */ public DisplayLabel() { this(null,(Value[])null); } /** * Convenience method for making a DisplayLabel with just one value to display. * * @param text Text to display. It shoud contain a single '#', which will be substituted by the value. * @param val a Value object whose value is substituted for the # in the text. */ public DisplayLabel(String text, Value val) { this(text, (val == null)? null : new Value[] { val }); } /** * Create a DisplayLabel to display one or more values. * Text and vals can be null. If not, text should have * as many (single) #'s as there are expressions. The * values of the Value objects are substituted for the * #'s in the display. * * @param text The text to display. If this is null, it is set to "#". * @param vals The Value object(s) whose values are substituted for #'s in the text. If this is null, * the values shoud be set later by calling the setValues() method. */ public DisplayLabel(String text, Value[] vals) { this.text = (text == null)? "#" : text; setValues(vals); } /** * The compute method recalculates the displayed Values * and changes the text of the label to show the new values. * This is usually called by a Controller. */ public void compute() { super.setText(getSubstitutedText()); } /** * Get the array of Value objects whose values are displayed * in this DisplayLabel. */ public Value[] getValues() { return values; } /** * A convenience method that can be used when the display string contains * just a single #. This sets the Value object whose value is substituted * for that #. */ public void setValue(Value val) { if (val == null) values = null; else values = new Value[] { val }; super.setText(getSubstitutedText()); } /** * Set the array of Value objects whose values are displayed * in this DisplayLabel, and change the display to show * the new values. (The contents of the array, vals, are * copied into a newly created array.) */ public void setValues(Value[] vals) { if (vals == null) values = null; else { values = new Value[vals.length]; System.arraycopy(vals,0,values,0,vals.length); } super.setText(getSubstitutedText()); } /** * Set the desired maximum number of characters in displayed numbers. * Actual size might be larger. Value is clamped to the range * 6 to 25. */ public void setNumSize(int size) { numSize = Math.min(Math.max(size,6),25); } /** * Return the desired maximum number of characters in displayed numbers. */ public int getNumSize() { return numSize; } /** * Return the basic text, including the #'s where Values * are inserted in the displayed text. Note that the * getText() method from the Label class will return the actual * displayed text, including the substitited values. */ public String getBaseText() { return text; } /** * Compute the string that is obtained by substituting values for #'s in text. * Will NOT throw any errors. (Any errors that occur when the * Value objects are evaluated are caught and translated * into "undefined" values.) */ private String getSubstitutedText() { StringBuffer b = new StringBuffer(); int valCt = 0; for (int i = 0; i < text.length(); i++) { if (text.charAt(i) == '#') { if (i != text.length() - 1 && text.charAt(i+1) == '#') { b.append('#'); i++; } else if (values == null || valCt >= values.length) b.append("undefined"); else { try { b.append(NumUtils.realToString(values[valCt].getVal(),numSize)); } catch (JCMError e) { b.append("undefined"); } valCt++; } } else b.append(text.charAt(i)); } return b.toString(); } /** * Set text for display -- text should include as many (single) #'s * as there are values to display. */ public void setText(String text) { this.text = text; super.setText(getSubstitutedText()); } /** * Return the preferred size of this DisplayLabel. * Allow space for up to numSize (or 8, whichever is larger) characters for * each (single) # in the text. This is not meant to be called directly. */ public Dimension getPreferredSize() { Dimension size = super.getPreferredSize(); int ct = 0; // Number of (single) #'1 in the text. if (text == null || text.length() == 0) ct = 1; else { for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (ch == '#') if (i < text.length() - 1 && text.charAt(i+1) == '#') i++; else ct++; } } FontMetrics fm = getFontMetrics(getFont()); int perChar = fm.charWidth('0'); int w = 10 + (int)(perChar * Math.max(8,numSize) * ct + fm.stringWidth(text)); // allowing extra space for numbers return new Dimension(w,size.height); } } // end class DisplayLabel jcm1-source/edu/hws/jcm/awt/Tie.java0000644000076500011320000001542711741343635016526 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; import java.util.Vector; import edu.hws.jcm.data.Value; /** * A Tie associates several Tieable objects. When the check() mehtod of the * Tie is called, it determines which of the Tieables has the largest serial number. * It then tells each Tieable to synchronize with that object. Ordinarily, the * Tie is added to a Controller, which is responsible for calling the Tie's * check() method. * *

This is meant to be used, for example, to Tie together two InputObjects to synchronize * the values that they represent. For example, you might want a VariableSlider * and a VariableInput to be alternative ways of inputting the same value. If so, * you can put them in a Tie and add that Tie to any Controller that is set to * respond to changes in the VariableSlider or VariableInput. * The x- and y- variables of a MouseTracker are also Tieable objects, so you * can synchronize the values of two MouseTrackers (in different CoordinateRects, * presumably) and you can synchronize the value of a MouseTracker variable with * a VariableInput or VariableSlider. * *

CoordinateRects and LimitControlPanels are also Tieable (to each other -- not * to Value objects). This is used to allow the LimitControlPanel to synchronize * with the Limits on the CoordinateRects that it controls. It could also * synchronize the Limits on two CoordinateRects, even in the absense of a * LimitControlPanel. * * @author David Eck */ public class Tie { /** * The Tieables in this Tie. */ protected Vector items = new Vector(2); /** * Create a Tie, initially containing no objects. */ public Tie() { } /** * Create a Tie initally containing only the object item. * item should be non-null. * * @param item the only initial item in this tieable. */ public Tie(Tieable item) { add(item); } /** * Create a Tie initially containing item1 and item2. * The items should be non-null. The items will be * synced with each other at the time the Tie is created. */ public Tie(Tieable item1, Tieable item2) { add(item1); add(item2); } /** * Add item to the tie, and sync it with the items that are * already in the Tie. It should be non-null. Note that synchronization * of the objects is forced even if they all have the same serial number, * since the values might not be the same when they are first added to * the Tie. */ public void add(Tieable item) { if (item != null) { items.addElement(item); forcecheck(); } } /** * If this Tie contains more than one item, find the newest * one and sync all the items with that item. If the serial * numbers of all the items are already the same, nothing is * done. */ public void check() { int top = items.size(); if (top < 2) return; long maxSerialNumber = ((Tieable)items.elementAt(0)).getSerialNumber(); int indexOfMax = 0; boolean outOfSync = false; for (int i = 1; i < top; i++) { long sn = ((Tieable)items.elementAt(i)).getSerialNumber(); if (sn != maxSerialNumber) outOfSync = true; if (sn > maxSerialNumber) { maxSerialNumber = sn; indexOfMax = i; } } if (!outOfSync) // if serialnumbers are the same, no sync is necessary. return; Tieable newest = (Tieable)items.elementAt(indexOfMax); for (int i = 0; i < top; i++) ((Tieable)items.elementAt(i)).sync(this, newest); } private void forcecheck() { // Synchronize the items in this Tie, even if serial numbers are the same. int top = items.size(); if (top < 2) return; long maxSerialNumber = ((Tieable)items.elementAt(0)).getSerialNumber(); int indexOfMax = 0; boolean outOfSync = false; for (int i = 1; i < top; i++) { long sn = ((Tieable)items.elementAt(i)).getSerialNumber(); if (sn != maxSerialNumber) outOfSync = true; if (sn > maxSerialNumber) { maxSerialNumber = sn; indexOfMax = i; } } Tieable newest = (Tieable)items.elementAt(indexOfMax); for (int i = 0; i < top; i++) ((Tieable)items.elementAt(i)).sync(this, newest); } } // end class Tie jcm1-source/edu/hws/jcm/awt/Computable.java0000644000076500011320000000560311741343635020073 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; /** * A Computable is an object that performs some sort of computation or * action when its compute() method is called. The compute() method is * meant to be called (usually) by a Controller. See the Controller class for more * information. */ public interface Computable { /** * Perform the computation or action associated with this * Computable object. */ public void compute(); } jcm1-source/edu/hws/jcm/awt/JCMPanel.java0000644000076500011320000002012111741343635017361 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; import edu.hws.jcm.data.*; import java.awt.*; import java.awt.event.*; /** * A JCMPanel is a Panel with an associated Controller. When an InputObject or * Computable is added to the JCMPanel, it is automatically added to the controller. * When a sub-JCMPanel is added, the Controller of the sub-panel is "attatched" to * the controller of the main panel so that objects in the sub-panel will also * be controlled by the Controller of the main panel. So, if you build an * interface entirely from JCMPanels, a lot of the control setup is done * automatically. Note that to make this work, you will need * a "mainPanel" that fills the entire window or applet (or at least the part that * holds JCM components). You should also call the gatherInputs() method of * the main JCMPanel after it is completely set up, so that changes in input objects * will cause the panel's controller to be notified, or, alternatively, you * can register the Controller by hand with InputObjects so that the * Controller will be notified when they change. * *

The disadvantage of this is that all the data used in the interface is recomputed, * even if the input objects that they depend on have not changed. For example. * if the user changes the value in a VarialbleInput, all the points on a graph * will be recomputed even if the function has not changed. The alternative is * to use regular Panels for all or part of the interface and configure some * Controllers by hand. * */ public class JCMPanel extends Panel { private int insetGap; // Size of gap, in pixels, around the edges of the // Panel, where the background color shows through. private Controller controller; // The controller associated with this panel. /** * Create a JCMPanel that uses a given layout manager. * * @param layout layout manager to use. (This could be null.) */ public JCMPanel(LayoutManager layout) { enableEvents(ContainerEvent.CONTAINER_EVENT_MASK); setLayout(layout); } /** * Create a JCMPanel that uses a BorderLayout with horizontal and veritcal * gaps of 3 pixels. */ public JCMPanel() { this(3); } /** * Create a JCMPanel that uses a BorderLayout with horizontal and vertical * gaps of "gap" pixels. * * @param gap inset gap to use. */ public JCMPanel(int gap) { this(new BorderLayout(gap,gap)); } /** * Create a JCMPanel that uses a GridLayout with the specified number of rows * and columns and with horizontal and veritcal gaps of 3 pixels between components. * * @param rows number of rows in the GridLayout. * @param columns number of columns in the GridLayout. */ public JCMPanel(int rows, int columns) { this(rows,columns,3); } /** * Create a JCMPanel that uses a GridLayout with the specified number of rows * and columns and with horizontal and vertical gaps of "gap" pixels. * * @param rows number of rows in the GridLayout. * @param columns number of columns in the GridLayout. * @param gap number of pixels between rows and columns */ public JCMPanel(int rows, int columns, int gap) { this(new GridLayout(rows,columns,gap,gap)); } /** * Set the size of the "Insets" for this JCMPanel. This is the gap, in pixels, around the edges of the * Panel, where the background color shows through. * * @param x inset gap to use. */ public void setInsetGap(int x) { insetGap = x; } /** * Called by the system to determine how much of a gap to leave * on each edge of the panel. Not meant to be called directly */ public Insets getInsets() { return new Insets(insetGap,insetGap,insetGap,insetGap); } /** * Return the controller associated with this JCMPanel. */ public Controller getController() { if (controller == null) controller = new Controller(); return controller; } /** * This method will set all the input objects in this JCMPanel * and in sub-JCMPanels, as well as any other input objects that have been * added to the panels' Controllers, to notify the Controller of this JCMPanel * when they change. It does this by calling setOnUserAction(c) -- or * a corresponding method -- for each input object c. This is meant to * be used, ordinarily, at the end of an applet's init() method, as * an alternative to adding each of the input objects to the controller * by hand. */ public void gatherInputs() { Controller c = getController(); c.notifyControllerOnChange(c); } /** * Called by the system when a component is added to or removed from * this panel. This takes care of automatically adding and removing * things from this Panel's Controller. This is not meant to be called directly */ public void processContainerEvent(ContainerEvent evt) { Component child = evt.getChild(); if (child instanceof JCMPanel) { if (evt.getID() == ContainerEvent.COMPONENT_ADDED) getController().add(((JCMPanel)child).getController()); else if (evt.getID() == ContainerEvent.COMPONENT_REMOVED) getController().remove(((JCMPanel)child).getController()); } else if (child instanceof Computable || child instanceof InputObject) { if (evt.getID() == ContainerEvent.COMPONENT_ADDED) getController().add(child); else if (evt.getID() == ContainerEvent.COMPONENT_REMOVED) getController().remove(child); } } } // end class JCMPanel jcm1-source/edu/hws/jcm/awt/VariableSlider.java0000644000076500011320000005104611741343635020672 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; import edu.hws.jcm.data.*; import java.awt.*; import java.awt.event.*; /** * A VariableSlider is a slider (implemented as a Scrollbar) whose * position represents the value of an associated variable. * The range of values represented by the slider is given by a pair of * Value objects. They can be specified in the constructor or later with the * setMin and setMax methods. A VariableSlider has an associated variable * that represents the value of the slider. Note that the value of the variable * can change only when the setInput() or checkInput() method is called. * If you want the value of the variable to track the position * of the slider as it is is manipulated by the user, add the slider to * a Controller and set the Controller as the Slider's "onUserAction" property. * This allows other objects that depend on the values of the slider to * be recomputed by the controller when the value changes, as long as they * are also registered with the Controller. * *

Some points to note: * * 1) setVal() can set a value outside the range from min to max, * which will persist until the next time checkInput() * or setVal() is called again. * 2) If the value of min or max changes, the value of this variable * will not change EXCEPT that it is clamped to the range between min and max. * 3) Min does not have to be less than max. * 4) The checkInput() routine only sets the needValueCheck flag to true. * (The setVal() and getVal() routines both set this flag to false.) This "lazy evaluation" is used because * checkInput() can't compute the new value itself. (The max and min * might depend on Values that are themselves about to change when some * other object's checkInput() mehtod is called.) * 5) getVal() returns the current value, as stored in the variable, * UNLESS needValueCheck is true. In that case, it recomputes * the value first. getSerialNumber() works similarly. * 6) A VariableSlider never throws JCMErrors. If an error occurs when min * or max is evaluated, the value of the variable associated with this VariableSlider * becomes undefined. (The point is, it doesn't generate any errors of its own. * The error would be caused by other InputObjects which should throw * their own errors when their checkInput() methods are called.) */ public class VariableSlider extends Scrollbar implements InputObject, Tieable, Value { /** * The variable associated with this VariableSlider. * VS is a nested private class, defined below. */ protected VS variable; /** * The Values that specify the range of values represented * by the slider. min does not have to be less than max. */ protected Value min, max; private Controller onUserAction; // If this is non-null, the compute() method // of onUserAction is called when the user // changes the position of the slider. /** * If this is true, then the value of the * variable associated with this slider is * an integer. Furthermore, the number of * intervals on the slider is set to be * the same as the range of possible values * (unless this range is too big). */ protected boolean integerValued; /** * The number of possible value of the scrollbar * (Unless integerValued is true.) */ protected int intervals; /** * This increases every time the value of the variable changes. */ protected long serialNumber; /** * This is set to true when checkInput() is called * to indicate that the min and max values must be * checked the next time getVal() is called. */ protected boolean needsValueCheck; /** * This is the position of the scrollbar the last time * getVal() or setVal() was called. It is used to check * whether the user has repositioned the slider. */ protected int oldPosition; /** * The values found for min and max the last time * checkInput() was called. */ protected double minVal = Double.NaN, maxVal; /** * Create a horizontal variable slider with no name and with a default * value range of -5 to 5. */ public VariableSlider() { this(null,null,null,null); } /** * Create a horizontal variable slider with no name and with the * specified range of values. If min is null, a default * value -5 is used. If max is null, a default value 5 is used. */ public VariableSlider(Value min, Value max) { this(null,min,max,null); } /** * Create a horizontal variable slider with the given name and range of * values, and register it with the given parser (but only if * both name and p are non-null). If min is null, a default * value -5 is used. If max is null, a default value 5 is used. */ public VariableSlider(String name, Value min, Value max, Parser p) { this(name,min,max,p,-1,Scrollbar.HORIZONTAL); } /** * Create a variable slider with the given name and range of * values, and register it with the given parser (but only if * both name and p are non-null). The "intervals" parameter specifes * how many different positions there are on the slider. (The value * of the scrollbar ranges from 0 to intervals.) If intervals is <= 0, * it will be set to 1000. If it is between 1 and 9, it will be set to 10. * The orientation must be either Scrollbar.HORIZONTAL or Scrollbar.VERTICAL. * It specifies whether this is a horizontal or vertical scrollbar. * If min is null, a default value -5 is used. If max is null, a default * value 5 is used. * * @param name name for this VariableSlider. * @param min minimum value for slider. * @param max maximum value for slider. * @param p register VariableSlider with this Parser. * @param intervals discrete positions on slider. * @param orientation Scrollbar.HORIZONTAL or Scrollbar.VERTICAL. */ public VariableSlider(String name, Value min, Value max, Parser p, int intervals, int orientation) { super(orientation); setBackground(Color.lightGray); setMin(min); setMax(max); if (intervals <= 0) intervals = 1000; if (intervals <= 10) intervals = 10; this.intervals = intervals; int visible = (intervals / 50) + 3; if (intervals < 100) setBlockIncrement(1); else setBlockIncrement(intervals/100); setValues(intervals/2,visible,0,intervals+visible); variable = new VS(name); if (name != null) super.setName(name); if (p != null && name != null) p.add(variable); needsValueCheck = true; // Force getVal() to compute a new value for the variable. oldPosition = -1; getVal(); } /** * Set the name of the associated variable. You shouldn't do this * if it has been added to a parser. If name is non-null, then * the name of this Component is also set to the specified name. */ public void setName(String name) { variable.setName(name); if (name != null) super.setName(name); } /** * A convenience method that registers this VariableSlider's variable * with p (but only if both p and the name of the variable are non-null). */ public void addTo(Parser p) { if (p != null && variable.getName() != null) p.add(variable); } /** * Return the variable associated with this VariableSlider. */ public Variable getVariable() { return variable; } /** * If set to true, restrict the values of the variable associated with this * slider to be integers. Furthermore, the number of intervals on the * scrollbar will be set to be the same as the size of the range from * min to max (unless this range is too big). The setVal() * method can still set the value of the variable to be a non-integer. */ public void setIntegerValued(boolean b) { integerValued = b; if (b && !Double.isNaN(minVal) && !Double.isNaN(maxVal)) checkIntegerLimits(minVal,maxVal); needsValueCheck = true; } /** * Return a boolean which is true if the VariableSlider restricts ranges of values to integers, false otherwise. */ public boolean getIntegerValued() { return integerValued; } /** * Set the value that the variable has when the slider is at the left (or * bottom) of the scrollbar. If v is null, -5 is used as the default value. */ public void setMin(Value v) { min = (v == null)? new Constant(-5) : v; } /** * Set the value that the variable has when the slider is at the right (or * top) of the scrollbar. If v is null, 5 is used as the default value. */ public void setMax(Value v) { max = (v == null)? new Constant(5) : v; } /** * Get the Value object that gives the value of the variable when the slider is * at the left (or bottom) of the scrollbar. The Value is always non-null. */ public Value getMin() { return min; } /** * Get the Value object that gives the value of the variable when the slider is * at the right (or top) of the scrollbar. The Value is always non-null. */ public Value getMax() { return max; } /** * If the Controller, c, is non-null, then its compute method will be called whenever * the user adjusts the position of the scroll bar. */ public void setOnUserAction(Controller c) { onUserAction = c; enableEvents(AWTEvent.ADJUSTMENT_EVENT_MASK); } /** * Method required by InputObject interface; in this class, it simply calls * setOnUserAction(c). This is meant to be called by JCMPanel.gatherInputs(). */ public void notifyControllerOnChange(Controller c) { setOnUserAction(c); } /** * Return the Controller, if any, that is notified when the user * adjusts the position of the scroll bar. */ public Controller getOnUserAction() { return onUserAction; } /** * Return this object's serial number, which is increased every time the * value changes. */ public long getSerialNumber() { if (needsValueCheck) getVal(); // Make sure the value/serialNumber data is up-to-date. return serialNumber; } /** * Change the value and serial number of this object to match * those of newest. See the Tie class for more information. * This is not meant to be called directly */ public void sync(Tie tie, Tieable newest) { if (newest != this) { if (! (newest instanceof Value) ) throw new IllegalArgumentException("Internal Error: A VariableSlider can only sync with Value objects."); setVal(((Value)newest).getVal()); serialNumber = newest.getSerialNumber(); } } /** * Get the value of this VariableSlider. (If needsValueCheck is * true, then the value is recomputed. Otherwise, the current * value is returned.) */ public double getVal() { if (needsValueCheck) { double newMinVal = Double.NaN; double newMaxVal = Double.NaN; boolean maxMinChanged = false; double value = variable.getVariableValue(); // Current value of the variable. try { // Compute new max/min values. newMinVal = min.getVal(); newMaxVal = max.getVal(); if (!Double.isNaN(newMinVal) && !Double.isNaN(newMaxVal) && (newMinVal != minVal || newMaxVal != maxVal)) { if (integerValued) checkIntegerLimits(newMinVal,newMaxVal); minVal = newMinVal; maxVal = newMaxVal; maxMinChanged = true; } } catch (JCMError e) { // don't allow error to propagate } if (Double.isNaN(minVal) || Double.isNaN(maxVal) || Double.isInfinite(minVal) || Double.isInfinite(maxVal)) { variable.setVariableValue(Double.NaN); if (!Double.isNaN(value)) serialNumber++; // Value has changed. setValue(0); } else if (oldPosition != getValue()) { // Position of scroll bar has been changed by user, // so compute a new value for the variable. double newVal = minVal + ((maxVal-minVal)*getValue())/intervals; newVal = clamp(newVal, minVal, maxVal); if (integerValued) newVal = Math.round(newVal); if (newVal != value) { variable.setVariableValue(newVal); serialNumber++; } } else if (!Double.isNaN(value) && maxMinChanged) { // Max/min have changed, but user has not changed scroll bar position. // Change the value only if that is necessary to clamp it to the min/max range. // Possibly, we have to change the position of the scroll. double newVal = clamp(value,minVal,maxVal); if (newVal != value) { variable.setVariableValue(newVal); serialNumber++; } if (minVal != maxVal) { int pos = (int)( (value - minVal)/(maxVal - minVal)*intervals ); setValue(pos); } } oldPosition = getValue(); needsValueCheck = false; } return variable.getVariableValue(); } /** * Set the value of the variable to x. If possible, set the * value on the scroll bar to match. */ public void setVal(double x) { try { double minVal = min.getVal(); double maxVal = max.getVal(); if (Double.isNaN(x) || Double.isNaN(minVal) || Double.isNaN(maxVal) || Double.isInfinite(x) || Double.isInfinite(minVal) || Double.isInfinite(maxVal)) { } else { if (integerValued) { minVal = Math.round(minVal); maxVal = Math.round(maxVal); } double xpos = clamp(x,minVal,maxVal); int pos = (int)((xpos-minVal)/(maxVal-minVal)*intervals); setValue(pos); } } catch (JCMError e) { } variable.setVariableValue(x); needsValueCheck = false; oldPosition = getValue(); serialNumber++; } // ------------------ Some implementation details ------------------------- /** * From the InputObject interface. This will force the slider to recompute * its max and min values, and possibly clamp its value between these two * extremes) the next time the value or serial number is checked. This is * ordinarily called by a Controller. */ public void checkInput() { needsValueCheck = true; } /** * Modify getPreferredSize to return a width of * 200, if the scrollbar is horzontal, or a height * of 200, if it is vertical. This is not meant to * be called directly. */ public Dimension getPreferredSize() { Dimension d = super.getPreferredSize(); if (getOrientation() == Scrollbar.HORIZONTAL) return new Dimension(200, d.height); else return new Dimension(d.width,200); } private void checkIntegerLimits(double minVal, double maxVal) { // Called if integerValued is true to set values on scrollbar. int oldpos = getValue(); minVal = Math.round( minVal ); maxVal = Math.round( maxVal ); double value = Math.round(variable.getVariableValue()); double range = Math.abs(minVal-maxVal); if (range > 0 && range != intervals) { intervals = (int)Math.min(range,10000); double v = clamp(value, minVal, maxVal); int pos = (int)((v-minVal)/(maxVal-minVal)*intervals); int visible = (intervals / 50) + 3; if (intervals < 10) setBlockIncrement(1); else if (intervals < 100) setBlockIncrement(intervals/10); else setBlockIncrement(10+intervals/100); setValues(pos,visible,0,intervals+visible); } if (oldpos == oldPosition) oldPosition = getValue(); else oldPosition = -1; } private double clamp(double val, double minVal, double maxVal) { // Utility routine used by setVal and getVal. If val is // between minVal and maxVal, it returns val. Otherwise, // it returns one of the endpoints, minVal or maxVal. // minVal can be greater than maxVal. double newVal = val; if (minVal < maxVal) { if (newVal < minVal) newVal = minVal; else if (newVal > maxVal) newVal = maxVal; } else { if (newVal < maxVal) newVal = maxVal; else if (newVal > minVal) newVal = minVal; } return newVal; } /** * Overridden to call onUserAction.compute() if onUserAction is non-null. * This is not meant to be called directly. */ public void processAdjustmentEvent(AdjustmentEvent evt) { if (onUserAction != null) onUserAction.compute(); super.processAdjustmentEvent(evt); } private class VS extends Variable { // A modified Variable class in which the getVal and // setVal methods are redirected to calls to the // getVal and setVal methods in the VariableSlider // class. The methods getVariableValue and setVariableValue // provide access to the original getVal and setVal of // the variable class. VS(String name) { super(name); } public double getVal() { return VariableSlider.this.getVal(); } public void setVal(double x) { VariableSlider.this.setVal(x); } void setVariableValue(double x) { super.setVal(x); } double getVariableValue() { return super.getVal(); } } } // end class VariableSlider jcm1-source/edu/hws/jcm/awt/VariableInput.java0000644000076500011320000005714011741343635020550 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // November 2005: Removed processKeyEvent to get rid of bogus "beep" when shift key is pressed. // (this also lets illegal characters into the input box) package edu.hws.jcm.awt; import edu.hws.jcm.data.*; import java.awt.*; import java.awt.event.*; /** * A VariableInput is an input box into which the user can type a real * number value, which becomes the value of an associated Variable. * The value of the Variable can change only when the VariableInput's * checkInput() method is called (usually by a Controller). See the Controller * class for more information. * *

Whenever checkInput is called, an error of type JCMError * might be generated. If throwErrors is true, this error is * thrown; if it is false, the error is caught, the value * of the variable is set to Double.NaN, and no error is thrown. * The error message associated with the error can be retrieved by * calling getErrorMessage(), if desired. (This value is null if * no error occurred the last time checkInput was called.) * *

A VariableInput is a Value, so it can be used directly where * a Value object is needed. * *

An VariableInput will ordinarily be registered with * a Controller in TWO ways: It's added to a Controller * with the Controller's add() method. This makes the Controller * call the VariableInput's checkInput() method during the * Controller's compute() method. Secondly, a Controller * is set as the "onUserAction" property. This causes the * Controller's compute() method to be called when the user * presses return in the VariableInput box. This is optional-- * you might, for example, only want the Controller to compute() * when a Compute button is pressed. You can also set the * VariableInput's onTextChange property to a Controller * that you want to compute every time the text in the box * changes. * *

After the VariableInput is created, it is possible to specify the * largest and smallest allowed values for the variable. It is also * possible to specify what sytle of input is allowed. The style * can be to allow any constant expression, constant real numbers only, * or integers only. Set these parameters with setMin(), setMax(), * and setInputStyle(). For setInputStyle(), the legal parameter * values are VariableInput.EXPRESSION, VariableInput.REAL, and * VariableInput.INTEGER. The default input style is EXPRESSION. * */ public class VariableInput extends TextField implements InputObject, Tieable, Value { /** * The Variable that represents * the value of this input box. (VI is * a private nested class inside VariableInput.) */ protected VI variable; /** * True if an error should be thrown * when checkInput() is calles and * the contents do not define a * legal number. True by default. */ protected boolean throwErrors; /** * Error message from the most recent * time checkInput() as called. * Null if there was no error. */ protected String errorMessage; /** * This serial number is increased * each time the value of the variable * changes. */ protected long serialNumber; /** * This is set to true if the text in the * box has been changed since the last time * the value of the variable was checked by checkInput(). */ //protected boolean hasChanged; protected String previousContents; private Controller onUserAction; // If this is non-null, the compute() method // of onUserAction is called when the user // presses return in this input-box. private Controller onTextChange; // If this is non-null, the compute() method // of onTextChange is called when the text // in this input box changes /** * Smallest allowable value. */ protected double minValue = -Double.MAX_VALUE; /** * Largest allowable value. */ protected double maxValue = Double.MAX_VALUE; /** * One of the constant values EXPRESSION, REAL, or * INTEGER, specifying the style of input. */ protected int inputStyle = 0; /** * A constant for use in the setInputStyle() method. Any constant expression is allowed. */ public static final int EXPRESSION = 0; /** * A constant for use in the setInputStyle() method. Only real numbers are allowed. */ public static final int REAL = 1; /** * A constant for use in the setInputStyle() method. Only integers are allowed. */ public static final int INTEGER = 2; /** * Create an unnamed VariableInput with initial contents "0". */ public VariableInput() { this(null,null); } /** * Construct a VariableInput with the given name * and initial String (which can both be null). * If initialString is null, the string "0" is used. * No error occurs in the constructor if the initialString * does not represent a legal value (A string rather than a * double is used for initialization since the initial * content can be an expression such as "pi/2".) * If name is not null, it is used as the name of * the VariableInput component as well as the name * of the associated variable. */ public VariableInput(String name, String initialString) { super((initialString == null)? "0" : initialString, 12); setBackground(Color.white); variable = new VI(name); if (name != null) super.setName(name); //hasChanged = true; previousContents = null; variable.checkInput(); // Won't throw an error, since throwErrors is false. //enableEvents(AWTEvent.KEY_EVENT_MASK); throwErrors = true; } /** * Create a VariableInput just as in the constructor * VariableInput(String,String). Then, if both parser and * name are non-null, register the associated variable * with the parser. */ public VariableInput(String name, String initialString, Parser parser) { this(name, initialString); addTo(parser); } /** * Get the associated variable for the VariableInput box. You will need * this, for example, if you want to register the variable with a Parser. */ public Variable getVariable() { return variable; } /** * Convenience method for creating a component containing * this VariableInput together with a label of the form * " = ". This version uses default colors for the * label, which are inherited from the containing * component. */ public JCMPanel withLabel() { return withLabel(null,null); } /** * Convenience method for creating a component containing * this VariableInput together with a label of the form * "name = ". Uses the given background and foreground * colors for the label and the panel. The colors can be * null to use the defaults, which will be inherited from the * containing Component. */ public JCMPanel withLabel(Color back, Color fore) { Label label = new Label(" " + variable.getName() + " ="); JCMPanel panel = new JCMPanel(); if (back != null) { panel.setBackground(back); label.setBackground(back); } if (fore != null) { panel.setForeground(fore); label.setBackground(fore); } panel.add(label, BorderLayout.WEST); panel.add(this, BorderLayout.CENTER); return panel; } /** * Set the name of the variable. This should not be called * while the variable is registered with a Parser. * The name of the VariableInput Component is also set to name, * if the name is non=null. */ public void setName(String name) { variable.setName(name); if (name != null) super.setName(name); } /** * A convenience method that registers this VariableInput's variable * with Parser p (but only if both p and the name of the variable are non-null). */ public void addTo(Parser p) { if (p != null && variable.getName() != null) p.add(variable); } /** * If the Controller, c, is non-null, then its compute() method will be called whenever * the user presses the return key while typing in this text-input box. */ public void setOnUserAction(Controller c) { onUserAction = c; enableEvents(AWTEvent.ACTION_EVENT_MASK); } /** * Return the Controller, if any, that is notified when the user * presses return in this text-input box. */ public Controller getOnUserAction() { return onUserAction; } /** * Method required by InputObject interface; in this class, it simply calls * setOnUserAction(c). This is meant to be called by JCMPanel.gatherInputs(). */ public void notifyControllerOnChange(Controller c) { setOnUserAction(c); } /** * If the Controller, cm is non-null, then its compute() method will be called whenever * the text in this input box changes. Furthermore, the throwErrors * property will be set to false, to avoid throwing multiple errors * while the user is typing. (You can change it back to true if * you want by calling setThrowErrors(true).) */ public void setOnTextChange(Controller c) { onTextChange = c; enableEvents(AWTEvent.TEXT_EVENT_MASK); if (c != null) throwErrors = false; } /** * Return the Controller, if any, that is notified when the text * in this input box changes */ public Controller getOnTextChange() { return onTextChange; } /** * Return the value of the associated variable, which might not * reflect the value of the contents of the input box. The value * of the variable changes only when the checkInput() method is called, * or when the setVal() method is called. * Call checkInput() first, if you want to be sure of getting the * same value that is currently shown in the box. */ public double getVal() { return variable.getVal(); } /** * Set the value of the associated variable. * Also sets the content of the input box. */ public void setVal(double d) { variable.setVal(d); } /** * Set the throwErrors property. If the value is true, then * an error will be thrown by the checkInput() method when the * contents of the VariableInput box are not legal. Otherwise, * no error is thrown; the value of the variable is just set * to Double.NaN. */ public void setThrowErrors(boolean throwErrors) { this.throwErrors = throwErrors; } /** * Return the value of the throwErrors property. */ public boolean getThrowErrors() { return throwErrors; } /** * Specify the smallest allowed value for the content of this VariableInput box. */ public void setMin(double min) { if (!Double.isNaN(min)) { minValue = min; //hasChanged = true; previousContents = null;// (force recheck of contents) } } /** * Return the minimum value that will be accepted in this VariableInput box. */ public double getMin() { return minValue; } /** * Specify the largest allowed value for the content of this VariableInput box. */ public void setMax(double max) { if (!Double.isNaN(max)) { maxValue = max; //hasChanged = true; previousContents = null;// (force recheck of contents) } } /** * Return the maximum value that will be accepted in this VariableInput box. */ public double getMax() { return maxValue; } /** * Specify what types of things are allowed in the input box. * The value of the parameter, style, must be one of the constants VariableInput.EXPRESSION, * VariableInput.REAL, or VariableInput.INTEGER. If not, the call to setInputStyle is ignored. */ public void setInputStyle(int style) { if (style == EXPRESSION || style == REAL || style == INTEGER) { if (style != inputStyle) { //hasChanged = true; previousContents = null;// (force recheck of contents) inputStyle = style; } } } /** * Return the input style, which determines what types of things * are allowed in the input box. The returned value is one * of the contstants EXPRESSION, REAL, or INTEGER */ public int getInputStyle() { return inputStyle; } /** * Get error message from previous call to checkInput(). * Returns null if there was no error. */ public String getErrorMessage() { return errorMessage; } //--------------------- Implementation Details ------------------------------------------- /** * Check whether the contents are valid, and change the value * of the associated variable if the new contents do not match * the current value. This might throw an error of type JCMError, * if throwErrors is true. This is usually called by a Controller. */ public void checkInput() { variable.checkInput(); } /** * Return this object's serial number, which increases whenever the * value of the associated variable changes. */ public long getSerialNumber() { return serialNumber; } /** * Synchronize serial number and value with newest, unless * this VariableInput is itself newest. This is required by * the Tieable interface, and is usually called by an object of type Tie. */ public void sync(Tie tie, Tieable newest) { if (newest == this) return; if (! (newest instanceof Value) ) throw new IllegalArgumentException("Internal Error: A VariableInput can only sync with Value objects."); variable.setVal(((Value)newest).getVal()); serialNumber = newest.getSerialNumber(); } private class VI extends Variable { // This class is used to define a Variable object associated // with this VariableInput. VI(String name) { super(name); } public void setVal(double d) { // If d is different from the current value of the variable, // set the value of the variable to d, set the displayed text, // and increment the serial number of the variable. double oldVal = this.getVal(); boolean hasChanged = previousContents == null || !previousContents.equals(getText()); if ( !hasChanged && ((Double.isNaN(d) && Double.isNaN(oldVal)) || (d == oldVal)) ) return; // Value is not actually changing. serialNumber++; justSetText(NumUtils.realToString(d)); //hasChanged = false; previousContents = getText(); errorMessage = null; super.setVal(d); } void checkInput() { // If the contents of the input box have changed, change // the value of the variable to match. If this is an actual // change in the value of the variable, then the serialNumber // is incremented. boolean hasChanged = previousContents == null || !previousContents.equals(getText()); if (!hasChanged) return; errorMessage = null; String content = getText(); try { double d = convertInput(content); double oldVal = this.getVal(); if ( (Double.isNaN(d) && Double.isNaN(oldVal)) || (d == oldVal) ) return; // Value is not actually changing. serialNumber++; super.setVal(d); } catch (JCMError e) { if (!Double.isNaN(this.getVal())) serialNumber++; super.setVal(Double.NaN); // Value becomes undefined. if (throwErrors) throw e; } } } private transient Parser constantParser; // To be used to process constant expressions; // will not know about any variables or user functions. /** * Convert a string into a real value. The parameter is taken from the input box when * this method is called by VI.checkInput() * Throw a JCMError if any error is found in the input. * * @param num String to be converted * @return the real value. */ protected double convertInput(String num) { double ans = Double.NaN; // The value. if (inputStyle == EXPRESSION) { if (constantParser == null) constantParser = new Parser(); try { Expression exp = constantParser.parse(num); ans = exp.getVal(); } catch (ParseError e) { errorMessage = "Illegal constant expression: " + e.getMessage(); if (throwErrors) { setCaretPosition(e.context.pos); requestFocus(); } } } else if (inputStyle == REAL) { try { Double d = new Double(num); ans = d.doubleValue(); } catch (NumberFormatException e) { errorMessage = "Value is not a legal real number."; if (throwErrors) { requestFocus(); } } } else { // inputStyle is INTEGER try { ans = Long.parseLong(num); } catch (NumberFormatException e) { errorMessage = "Value is not a legal integer."; if (throwErrors) { requestFocus(); } } } if (errorMessage == null) { if (ans < minValue || ans > maxValue) { errorMessage = "Value outside legal range. It should be "; if (inputStyle == INTEGER) errorMessage += "an integer "; else if (inputStyle == REAL) errorMessage += "a real number "; if (minValue > -Double.MAX_VALUE && maxValue < Double.MAX_VALUE) errorMessage += "between " + NumUtils.realToString(minValue) + " and " + NumUtils.realToString(maxValue); else if (minValue > -Double.MAX_VALUE) errorMessage += "greater than or equal to " + NumUtils.realToString(minValue); else errorMessage += "less than or equal to " + NumUtils.realToString(maxValue); if (throwErrors) { requestFocus(); } } } if (errorMessage != null) throw new JCMError(errorMessage,this); return ans; } /* * Override processKeyEvent to only allow characters * that are legal in this VariableInput. * * @param evt used internally. public void processKeyEvent(KeyEvent evt) { if (evt.getID() == KeyEvent.KEY_PRESSED) { int ch = evt.getKeyCode(); char chr = evt.getKeyChar(); boolean use = (chr != 0 && Character.isDigit(chr) || chr == '-' || chr == '+') || ch == KeyEvent.VK_DELETE || ch == KeyEvent.VK_BACK_SPACE; if (inputStyle != INTEGER) use = use || chr == '.' || chr == 'e' || chr == 'E'; if (inputStyle == EXPRESSION) use = use || Character.isLetter(chr) || chr == '(' || chr == ')' || chr == '*' || chr == '/' || chr == '^' || chr == ':' || chr == '?' || chr == '|' || chr == '&' || chr == '~' || chr == '=' || chr == '<' || chr == '>' || chr == '!' || ch == KeyEvent.VK_SPACE; boolean useControl = use || ch == KeyEvent.VK_TAB || ch ==KeyEvent.VK_ENTER || chr == 0; if (!useControl) { evt.consume(); Toolkit.getDefaultToolkit().beep(); } else if (use) { hasChanged = true; } } super.processKeyEvent(evt); } */ /** * This overrides the setText() method from the TextField class so that * it will also force the contents to be checked the next time * the checkInput() method is called. * * @param text change text to this. */ public void setText(String text) { super.setText(text); //hasChanged = true; previousContents = null; } private void justSetText(String text) { // Call super.setText(). super.setText(text); } /** * Overridden to call onUserAction.compute() if onUserAction is non-null. * This is not meant to be called directly. */ public void processActionEvent(ActionEvent evt) { if (onUserAction != null) onUserAction.compute(); super.processActionEvent(evt); } /** * Overridden to call onUserAction.compute() if onUserAction is non-null. * This is not meant to be called directly. */ public void processTextEvent(TextEvent evt) { //hasChanged = true; previousContents = null; if (onTextChange != null) onTextChange.compute(); super.processTextEvent(evt); } } // end class VariableInput jcm1-source/edu/hws/jcm/awt/JCMError.java0000644000076500011320000000667311741343635017433 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; /** * JCMErrors can be generated by objects belonging to various classes * in edu.hws.jcm.awt and edu.hws.jcm.draw. A JCMError can have an * associated object, which is generally a ParseContext, InputObject, * or Computable. * * @author David Eck */ public class JCMError extends RuntimeException { /** * Object, possibly null, associated with this error. */ public Object object; /** * Create a JCMError with the given error message and no associated object. * * @param message the error message associated with this JCMError. */ public JCMError(String message) { this(message,null); } /** * Create a JCMError with the given error message and associated object. * * @param message the error message associated with this JCMError. * @param object the object associated with this JCMError. */ public JCMError(String message, Object object) { super(message); this.object = object; } } jcm1-source/edu/hws/jcm/awt/InputObject.java0000644000076500011320000000771411741343635020233 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; /** * An InputObject represents some sort of value that can be changed * by, for example, user interaction with a GUI element. The value can * actually change only when the checkInput() method is called. Generally, * an InputObject is a GUI element with an associated MathObject such as * a Variable or Expression. For example, a VariableInput is a text-input * box where the user can enter the value of a Variable. However, the * input is only checked and the value of the variable can only change * when the VariableInput's checkInput() method is called. The checkInput() * method is generally meant to be called by a Controller object. The * checkInput() method should throw a JCMError if an error occurs. * See the Controller class for more information. * * @author David Eck */ public interface InputObject extends java.io.Serializable { /** * Check and possibly change the value associated with this InputObject. */ public void checkInput(); /** * This method was introduced to provide a common interface for setting * a Controller that is to be notified when there is a change in the * InputObject. (This was introduced late in development, to be used * by edu.hws.jcm.awt.JCMPanel.gatherInputs(). In all the standard * classes that implement the InputObject interface, this method * simply calls a setOnChange or setOnUserAction method.) */ public void notifyControllerOnChange(Controller c); } // end interface InputObject jcm1-source/edu/hws/jcm/awt/Animator.java0000644000076500011320000011110011741343635017540 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; import edu.hws.jcm.data.*; import java.awt.*; import java.awt.event.*; /** * An Animator can change a value continuously, without user intervention, by running * a separate Thread. By default, an animator appears as a "Start" button. When the * button is pressed, the value of the animator starts counting 0, 1, 2, ... The button * changes to a "Stop" button. When this is pressed, the value stops changing. A Controller * can be set, by calling the setOnChange() method, to be notified whenever the value is * changed. If this is done, then the value of the Animator will only change when its * checkInput() method is called, so it should be added to a Controller which will call * this method. * *

The getValueAsVariable() method can be called to get a Variable whose value is * the value of the Animator. This variable can then be added to a Parser, so it can * be used in expressions. An Animator is "Tieable", so it can share its value * with another InputObject, such as a VariableSlider or a VariableIput. * *

There are many options: If maximum and minimum values are both specified, then the value * of the Animator ranges between these values. By default, this interval is divided into * 100 sub-intervals, so that there are 101 frames. However, the number of intervals can * also be set. If no min or max is specified but a number of intervals is specified, * then the value is an integer which ranges from 0 up to the specified number of intervals. * If the number of frames is finite, then there are three possibities when the last * frame is reached: The animation can stop; it can loop back to the the starting * frame, or it can reverse direction and cycle back and forth. The behavior is controlled * with the setLoopStyle() method. * *

An Animator is actually a Panel which can contain other controls in addition to or * instead of the Start/Stop button. For example, it can contain a "Next" button or * a pop-up menu to control the speed. * */ public class Animator extends Panel implements Value, Tieable, InputObject, ActionListener, ItemListener, Runnable { /** * Used to add a component to the Animator Panel; can be used in a constructor * or in the addControl() method. Can also be used in the getControl() method * to specify which component is to be retrieved. */ public static final int START_STOP_BUTTON = 1, START_BUTTON = 2, PAUSE_BUTTON = 4, STOP_BUTTON = 8, NEXT_BUTTON = 16, PREV_BUTTON = 32, SPEED_CHOICE = 64, LOOP_CHOICE = 128; /** * Indicates that the components in the Animator panel are to be stacked vertically. * (Can be used in a constructor and in the setOrientation method.) */ public static final int VERTICAL = 1; /** * Indicates that the components in the Animator panel are to be in a horizontal row. * (Can be used in a constructor and in the setOrientation method.) */ public static final int HORIZONTAL = 0; /** * Represents a loop style in which the animation is played once. When the final frame * is reached, the animation ends. Use in the setLoopStyle() method. */ public static final int ONCE = 0; /** * Represents a loop style in which the animation is played repeatedly. When the final frame * is reached, the animation returns to the first frame. Use in the setLoopStyle() method. */ public static final int LOOP = 1; /** * Represents a loop style in which the animation is cycled back and forth. When the final frame * is reached, the animation reverses direction. Use in the setLoopStyle() method. */ public static final int BACK_AND_FORTH = 2; private Button startStopButton, startButton, stopButton, pauseButton, nextButton, prevButton; private Choice speedChoice, loopChoice; private int orientation; private String startButtonName = "Start"; private String stopButtonName = "Stop"; private volatile int loopStyle; private boolean runningBackwards; private volatile int millisPerFrame = 100; private volatile int frame; private int maxFrame; private double value; private volatile long serialNumber = 1; private Computable onChange; private Value min, max; // If both are non-null, give the max and min values of the animator. private Value intervals; // If non-null, gives the number of sub-intervals. Number of frames is this value plus one. private boolean needsValueCheck = true; private double min_val, max_val; // Values of min and max. private int intervals_val; // Value of intervals. private static int START = 0, PAUSE = 1, NEXT = 2, PREV = 3, STOP = 4, RUN = 5; // possible values of thread status private Thread runner; private volatile int status = STOP; private boolean undefinedWhenNotRunning; /** * Create a default Animator. If no changes are made by calling other methods, it will appear as * a Start/Stop button. When Start is pressed, the value will count 0, 1, 2, 3, ..., until the Stop * button is pressed. Restarting the animation starts the value again at zero. */ public Animator() { this(START_STOP_BUTTON,HORIZONTAL); } /** * Create an Animator containing the specified control. The parameter can consist of one or * more of the following constants, or'ed together: START_STOP_BUTTON, START_BUTTON, STOP_BUTTON, * PAUSE_BUTTON, NEXT_BUTTON, PREV_BUTTON, SPEED_CHOICE, LOOP_CHOICE. If no changes are made * by calling other methods, the value of the Animator will be 0, 1, 2, 3, .... The components * are arranged into one horizontal row, using a GridLayout. */ public Animator(int controls) { this(controls,HORIZONTAL,null,null,null); } /** * Create an Animator containing specified controls. (See the one-parameter constructor.) * The second parameter should be one of the constants HORIZONTAL or VERTICAL, to specify * how the components are arranged in the Animator panel. */ public Animator(int controls, int orientation) { this(controls,orientation,null,null,null); } /** Create an Animator with specified controls, orienation, range limits and number of intervals * * @param controls Specify the controls to add to the Animator. Can consist of one or * more of the following constants, or'ed together: START_STOP_BUTTON, START_BUTTON, STOP_BUTTON, * PAUSE_BUTTON, NEXT_BUTTON, PREV_BUTTON, SPEED_CHOICE, LOOP_CHOICE. * @param orientation How the controls are arranged in the panel. One of the constants VERTICAL or HORIZONTAL. * @param min If BOTH min and max are non-null, they specify the range of values of the Animator. * @param max If BOTH min and max are non-null, they specify the range of values of the Animator. * @param intervals If non-null, specifies the number of intervals into which the range of values * is divided. Note that the value will be rounded to the nearest integer and clamped to the * range 0 to 100000. The number of frames is the number of intervals, plus one. If min and max are * non-null and intervals is null, then a default value of 100 is used. If either min or max is * null and intervals is non-null, then the Animator takes on the values 0, 1, 2, ..., intervals. */ public Animator(int controls, int orientation, Value min, Value max, Value intervals) { this.min = min; this.max = max; this.intervals = intervals; this.orientation = orientation; if (orientation == VERTICAL) setLayout(new GridLayout(0,1)); else setLayout(new GridLayout(1,0)); for (int i = 1; i <= LOOP_CHOICE; i <<= 1) if ( (i & controls) != 0 ) addControl(i); } //-------------- Accessor methods for public properties ------------------ /** * Get one of controls associated with the Animator. Usually, these are displayed * in the Animator panel, but you could get a control and add it to another panel if you * want. Even if you do this, the control will still be managed by the Animator (which * will respond to it and enable/disable it, for example). You might also want to get * one of the Animator's buttons so that you can change its label. The value * of the parameter should be one of the constants START_STOP_BUTTON, START_BUTTON, STOP_BUTTON, * PAUSE_BUTTON, NEXT_BUTTON, PREV_BUTTON, SPEED_CHOICE, LOOP_CHOICE. If the parameter is * not one of these values, then null is returned. */ public Component getControl(int controlCode) { switch (controlCode) { case START_STOP_BUTTON: if (startStopButton == null) { startStopButton = new Button(startButtonName); startStopButton.setBackground(Color.lightGray); startStopButton.addActionListener(this); } return startStopButton; case START_BUTTON: if (startButton == null) { startButton = new Button(startButtonName); startButton.setBackground(Color.lightGray); startButton.addActionListener(this); } return startButton; case STOP_BUTTON: if (stopButton == null) { stopButton = new Button(stopButtonName); stopButton.setBackground(Color.lightGray); stopButton.addActionListener(this); stopButton.setEnabled(false); } return stopButton; case PAUSE_BUTTON: if (pauseButton == null) { pauseButton = new Button("Pause"); pauseButton.setBackground(Color.lightGray); pauseButton.addActionListener(this); pauseButton.setEnabled(false); } return pauseButton; case NEXT_BUTTON: if (nextButton == null) { nextButton = new Button("Next"); nextButton.setBackground(Color.lightGray); nextButton.addActionListener(this); } return nextButton; case PREV_BUTTON: if (prevButton == null) { prevButton = new Button("Prev"); prevButton.setBackground(Color.lightGray); prevButton.addActionListener(this); } return prevButton; case SPEED_CHOICE: if (speedChoice == null) { speedChoice = new Choice(); speedChoice.add("Fastest"); speedChoice.add("Fast"); speedChoice.add("Moderate"); speedChoice.add("Slow"); speedChoice.add("Slower"); speedChoice.select(2); speedChoice.addItemListener(this); } return speedChoice; case LOOP_CHOICE: if (loopChoice == null) { loopChoice = new Choice(); loopChoice.add("Play Once"); loopChoice.add("Loop"); loopChoice.add("Back and Forth"); loopChoice.addItemListener(this); } return loopChoice; default: return null; } } /** * Add one of the possible control buttons or pop-up menus to the Animator. The possible values * of the parameter and their meanings are as follows: *

START_STOP_BUTTON: When clicked, animation starts and name of button changes; when clicked again, animation stops. *

START_BUTTON: When clicked, animation starts. *

STOP_BUTTON: When clicked, animaton stops. *

PAUSE_BUTTON: When clicked, animation is paused; this is different from stopping the animation since * a paused animation can be resumed from the same point while a stopped animation can only be restarted * from the beginning. *

NEXT_BUTTON: When clicked, the animation advances one frame; this is disabled when the animation is running. *

PREV_BUTTON: When clicked, the animation is moved back one frame; this is disabled when the animation is running. *

SPEED_CHOICE: A pop-up menu whose value controls the speed at which the animation plays. *

LOOP_CHOICE: A pop-up menu that controls the style of animation, that is, what happens when the * animation reaches its final frame; values are Play Once, Loop, and Back and Forth. *

If the parameter is not one of these constants, then nothing is done. Ordinarily, this * will be called during initialization. (If you call it at some other time, you will have * to validate the panel yourself.) The return value is the component that is added, or null * if the parameter value is not legal. */ public Component addControl(int controlCode) { Component c = getControl(controlCode); if (c == null) return null; else { add(c); return c; } } /** * The name of the Start/Stop button is managed by the Animator, so changing it directly makes * no sense. This method can be used to specify the label displayed by the Start/Stop button * when the animation is NOT running. This name is also used for the Start button. This method * should ordinarily be called during initialization. In any case, it should not be called while * an animation is running, since it changes the name of the Start/Stop button to the specified value. */ public void setStartButtonName(String name) { if (name != null) { startButtonName = name; if (startStopButton != null) startStopButton.setLabel(name); if (startButton != null) startButton.setLabel(name); } } /** * The name of the Start/Stop button is managed by the Animator, so changing it directly makes * no sense. This method can be used to specify the label displayed by the Start/Stop button * when the animation IS running. This name is also used for the Stop button. This method * should ordinarily be called during initialization. In any case, it should not be called while * an animation is running, since it does not change the name of the Start/Stop button. */ public void setStopButtonName(String name) { if (name != null) { stopButtonName = name; if (stopButton != null) stopButton.setLabel(name); } } /** * Get the constant, VERTICAL or HORIZONTAL, that was used to specify whether the components * in the animator are arranged veritcally or horizontally. */ public int getOrientation() { return orientation; } /** * Set the orientation of the components in the Animator panel. The parameter should be one * of the constants HORIZONTAL or VERTICAL. This just sets the layout for the panel to * be a GridLayout with one row or one column and validates the panel. You could also * set the layout to be something else, such as a FlowLayout, using the setLayout() method. */ public void setOrientation(int orientation) { if (orientation != this.orientation && (orientation == HORIZONTAL || orientation == VERTICAL)) { this.orientation = orientation; if (orientation == VERTICAL) setLayout(new GridLayout(0,1)); else setLayout(new GridLayout(1,0)); validate(); } } /** * Get the Value object that specifies the final value of the Animator. This object can be null. */ public Value getMax() { return max; } /** * Set the Value object that gives the final value of the Animator. If both min and max are * non-null, the value of the Animator ranges from min to max as the animation procedes. (It is not required * that max be greater than min. They should probably be calles startVal and endVal.) */ public void setMax(Value max) { this.max = max; needsValueCheck = true; } /** * A convenience method that simply calls setMax(new Constant(d)). */ public void setMax(double d) { setMax(new Constant(d)); } /** * Get the Value object that specifies the starting value of the Animator. This object can be null. */ public Value getMin() { return min; } /** * Set the Value object that gives the starting value of the Animator. If both min and max are * non-null, the value ranges from min to max as the animation procedes. (It is not required * that max be greater than min.) */ public void setMin(Value min) { this.min = min; needsValueCheck = true; } /** * A convenience method that simply calls setMin(new Constant(d)). */ public void setMin(double d) { setMin(new Constant(d)); } /** * Get the Value object that specifies the number of frames in the animation. This can be null. */ public Value getIntervals() { return intervals; } /** * Set the Value object that specifies the number of frames in the animation. If non-null, then * the value is rounded to the nearest integer and clamped to the range 1 to 100000. If it is * null and min and max are non-null, then a default value of 100 is used. If it is null and * min or max is null, then the number of frames is unlimited and the values taken on by the * animator are 0, 1, 2, 3, ..., that is, the value of the animator is the frame number. * If min or max is null and intervals is non-null, then the values taken on by the * animator are 0, 1, 2, ..., intervals. Note that the number of frames is (intervals+1). */ public void setIntervals(Value intervals) { this.intervals = intervals; needsValueCheck = true; } /** * A convenience method that simply calls setIntervals(new Constant(d)). */ public void setIntervals(int intervals) { setIntervals(new Constant(intervals)); } /** * Get the nominal number of milliseconds per frame. The actual time between frames * can be longer because of the work that is done processing the frame or on other tasks. */ public int getMillisPerFrame() { return millisPerFrame; } /** * Set the nominal number of milliseconds per frame. The actual time between frames * can be longer because of the work that is done processing the frame or on other tasks. * Values less than 5 are effectively equivalent to 5. Realistic values are 25 or more, * but it depends on what system the program is running on and how complicated each frame is. */ public void setMillisPerFrame(int millis) { millisPerFrame = millis; } /** * Get the loop style, which determines what happens when the final frame of the animation is reached. */ public int getLoopStyle() { return loopStyle; } /** * Set the loop style, which determines what happens when the final frame of the animation is reached. * The parameter can be one of the constants: ONCE (animation stops when final frame is reached), * LOOP (animation cycles back to the first frame and continues from there); or BACK_AND_FORTH (animation reverses direction * and cycles back and forth). */ public void setLoopStyle(int style) { if (style >= 0 && style <= 2 && style != loopStyle) { loopStyle = style; if (loopChoice != null) loopChoice.select(style); runningBackwards = false; } } /** * Set the value of the animation. Note that the value does not have to be one of * the values that would ordinarily occur in the animation. Of course, if the animation * is running, then the new value won't be around for long since it will change as * soon as the next frame comes up. */ synchronized public void setVal(double val) { if (needsValueCheck) checkValue(); // make sure min,max,intervals are evaluated if necessary value = val; serialNumber++; needsValueCheck = false; // Try to make the frame number match the value as closely as possible if (!Double.isNaN(val)) { if (min == null || max == null) frame = (int)Math.round(val); else if (!Double.isNaN(min_val) && !Double.isNaN(max_val) && min_val != max_val) frame = (int)Math.round( (val - min_val)/(max_val - min_val) * maxFrame ); if (frame < 0) frame = 0; else if (maxFrame > 0 && frame > maxFrame) frame = maxFrame; } } /** * Get the current value of the Animator. */ public double getVal() { if (needsValueCheck) checkValue(); return value; } /** * Get a variable whose value is always equal to the value of the animator. * The name of the variable will be k. */ public Variable getValueAsVariable() { return getValueAsVariable("k"); } /** * Get a variable whose value is always equal to the value of the animator. * The name of the variable is specified by the parameter. */ public Variable getValueAsVariable(String name) { return new Variable(name) { public void setVal(double val) { Animator.this.setVal(val); } public double getVal() { return Animator.this.getVal(); } }; } /** * Get the Controller that is notified (by calling its compute() method) whenever * the frame changes. This can be null. */ public Computable getOnChange() { return onChange; } /** * Set the Controller that is notified (by calling its compute() method) whenever * the frame changes. If null, no Controller is notified. NOTE: Animators are * different from InputObjects in that when onChange is null, the value of animator * and its associated Variable will change without checkInput() being called. * However, if the onChange is not null, then checkInput() must be called for * the value to change. (So the Animation to be added to the Controller by * calling the Controller's add() method. Then, the Controller will call * the checkInput() method.) */ public void setOnChange(Computable onChange) { this.onChange = onChange; } /** * Method required by the InputObject interface. It just calls setOnChange(c). * This is meant to be called by the gatherInputs() method in JCMPanel. */ public void notifyControllerOnChange(Controller c) { setOnChange(c); } /** * Get the value of the undefinedWhenNotRunning property. */ public boolean getUndefinedWhenNotRunning() { return undefinedWhenNotRunning; } /** * Set the value of the undefinedWhenNotRunning property. If this is true, * then the value of the Animator is Double.NaN except when the animation is * running (or paused), (or if it has been set by a call to the setVal() method). * The default value is false. */ public void setUndefinedWhenNotRunning(boolean undefinedWhenNotRunning) { this.undefinedWhenNotRunning = undefinedWhenNotRunning; } //--------------- play control -------------------------------------- /** * Start the animation from the first frame, or continue it if it was paused. * This is called when the Start button or Start/Stop button is pressed, but * it could also be called directly. */ synchronized public void start() { if (runner != null && runner.isAlive() && status == STOP){ // A previous run is stopping. Give it a chance to stop. try { wait(1000); } catch (InterruptedException e) { } if (runner != null && runner.isAlive()) { runner.stop(); // bad form, but what choice do I have? runner = null; } } if (runner == null || !runner.isAlive()) { runner = new Thread(this); status = START; runner.start(); } else if (status != RUN) { status = START; notify(); } } /** * Pause the animation, if it is running. * This is called when the Pause button is pressed, but * it could also be called directly. */ synchronized public void pause() { if (status == RUN) { status = PAUSE; notify(); } } /** * Advance the animation by one frame. This will start the animation from the * first frame if it is stopped. This has no effect unless the animation is * stopped or paused. This is called when the Next button pressed, but * it could also be called directly. */ synchronized public void next() { if (runner == null || !runner.isAlive()) { runner = new Thread(this); status = NEXT; runner.start(); } else if (status == PAUSE) { status = NEXT; notify(); } } /** * Advance the animation BACK one frame. This will start the animation from the * first frame if it is stopped. This has no effect unless the animation is * stopped or paused. This is called when the Prev button pressed, but * it could also be called directly. */ synchronized public void prev() { if (runner == null || !runner.isAlive()) { runner = new Thread(this); status = PREV; runner.start(); } else if (status == PAUSE) { status = PREV; notify(); } } /** * Stop the animation, if it is running or paused. This is called when the Stop button * or the StartStop button is pressed, but it could also be called directly. * NOTE: If the Animator is in an applet, then it is a good idea to call the stop() * method of the Animator from the applet's destroy() method. */ synchronized public void stop() { status = STOP; if (runner == null || !runner.isAlive()) return; notify(); } //-----------------Implementation details -------------------------- /** * Respond to button clicks. This is not meant to be called directly. */ synchronized public void actionPerformed(ActionEvent evt) { Object src = evt.getSource(); if (src == null) return; if (src == startStopButton) { if (status == RUN) stop(); else start(); } else if (src == startButton) { start(); } else if (src == stopButton) { stop(); } else if (src == nextButton) { next(); } else if (src == prevButton) { prev(); } else if (src == pauseButton) { pause(); } } /** * Respond to clicks on pop-up menus. This is not meant to be called directly. */ synchronized public void itemStateChanged(ItemEvent evt) { if (evt.getSource() == loopChoice && loopChoice != null) { setLoopStyle(loopChoice.getSelectedIndex()); } else if (evt.getSource() == speedChoice && speedChoice != null) { switch (speedChoice.getSelectedIndex()) { case 0: millisPerFrame = 0; break; case 1: millisPerFrame = 30; break; case 2: millisPerFrame = 100; break; case 3: millisPerFrame = 500; break; case 4: millisPerFrame = 2000; break; } } } /** * Part of the IputObject interface. This is meant to be called by a Controller. */ public void checkInput() { needsValueCheck = true; } /** * Part of the Tieable interface. This is meant to be called by other Tieable objects * as part of object synchronization. */ public long getSerialNumber() { if (needsValueCheck) checkValue(); return serialNumber; } /** * Part of the Tieable interface. This is meant to be called by Tie objects * as part of object synchronization. */ public void sync(Tie tie, Tieable newest) { if (newest != this) { if (! (newest instanceof Value) ) throw new IllegalArgumentException("Internal Error: An Animator can only sync with Value objects."); setVal(((Value)newest).getVal()); serialNumber = newest.getSerialNumber(); } } synchronized private void checkValue() { // Recompute the value, which might have changed. double newVal; if (min != null) min_val = min.getVal(); if (max != null) max_val = max.getVal(); if (intervals == null) intervals_val = 0; else { double d= intervals.getVal(); if (Double.isNaN(d) || d <= 0.5) intervals_val = 0; else if (d > 100000) intervals_val = 100000; else intervals_val = (int)Math.round(d); } maxFrame = intervals_val; if (min == null || max == null) { // value is frame number newVal = frame; } else if (Double.isNaN(min_val) || Double.isNaN(max_val) || Double.isInfinite(min_val) || Double.isInfinite(max_val)) { newVal = Double.NaN; } else if (intervals_val > 0) { newVal = min_val + (frame*(max_val-min_val)) / intervals_val; } else { // Assume 100 intervals if maxFrame = 100; newVal = min_val + (frame*(max_val-min_val)) / 100; } if (undefinedWhenNotRunning && status == STOP) newVal = Double.NaN; value = newVal; needsValueCheck = false; } private void doControlStatus(int status) { // Enable/disable buttons according to Thread status. // status should be START or STOP or PAUSE if (startStopButton != null) startStopButton.setLabel((status == START)? stopButtonName : startButtonName); if (startButton != null) startButton.setEnabled(status != START); if (stopButton != null) stopButton.setEnabled(status != STOP); if (nextButton != null) nextButton.setEnabled(status != START); if (prevButton != null) prevButton.setEnabled(status != START); if (pauseButton != null) pauseButton.setEnabled(status == START); } private void doAdvanceFrame(int amt) { // Move on to next or previous frame. // amt is +1 or -1 serialNumber++; if (loopStyle == BACK_AND_FORTH && runningBackwards) frame -= amt; else frame += amt; if (frame < 0) { if (loopStyle == LOOP) frame = maxFrame; // might be 0, which is OK I guess else if (loopStyle == BACK_AND_FORTH) { frame = 1; if (amt == 1) runningBackwards = false; else runningBackwards = true; } else frame = 0; } else if (maxFrame > 0 && frame > maxFrame) { if (loopStyle == LOOP) frame = 1; // Don't use 0, because usually frames 0 and maxFrame will be the same else if (loopStyle == ONCE) { frame = maxFrame; status = STOP; return; // exit at once } else { // loopStyle == BACK_AND_FORTH frame = maxFrame - 1; if (amt == 1) runningBackwards = true; else runningBackwards = false; } } if (onChange != null) onChange.compute(); else needsValueCheck = true; } /** * The method that is run by the animation thread. This is not meant to be called directly. */ public void run() { int localstatus = status; long lastFrameTime = 0; runningBackwards = false; if (frame != 0 || undefinedWhenNotRunning) { frame = 0; serialNumber++; lastFrameTime = System.currentTimeMillis(); if (onChange != null) onChange.compute(); else needsValueCheck = true; if (status == PREV || status == NEXT) status = PAUSE; } try { while (true) { synchronized(this) { while (status == PAUSE) { if (localstatus != PAUSE) { doControlStatus(PAUSE); localstatus = PAUSE; } try { wait(); } catch (InterruptedException e) { } } if (status == STOP) break; localstatus = status; if (needsValueCheck) checkValue(); // make sure maxFrame is correct } if (localstatus == START) { doControlStatus(START); localstatus = status = RUN; } if (localstatus == RUN) { long sinceLastFrame = System.currentTimeMillis() - lastFrameTime; long waitTime = millisPerFrame - sinceLastFrame; if (waitTime <= 5) waitTime = 5; try { synchronized(this) { wait(waitTime); } } catch (InterruptedException e) { } lastFrameTime = System.currentTimeMillis(); if (status == RUN) doAdvanceFrame(1); } else if (localstatus == NEXT) { doAdvanceFrame(1); if (status != STOP) // status can become stop in doAdvanceFrame status = localstatus = PAUSE; } else if (localstatus == PREV) { doAdvanceFrame(-1); if (status != STOP) // status can become stop in doAdvanceFrame (but shouldn't happen here) status = localstatus = PAUSE; } } } finally { synchronized(this) { status = STOP; doControlStatus(STOP); frame = 0; serialNumber++; if (onChange != null) onChange.compute(); else needsValueCheck = true; runner = null; notify(); // in case start() method is waiting for thread to end } } } } // class Animator jcm1-source/edu/hws/jcm/awt/DataTableInput.java0000644000076500011320000016566011741343635020653 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // November 2005: Removed "beep" from processKeyEvent package edu.hws.jcm.awt; import edu.hws.jcm.data.*; import java.awt.*; import java.awt.event.*; import java.util.Vector; import java.io.*; /** * A DataTableInput lets the user input a grid of real numbers arranged * in rows and columns. Each column has a name, and rows are numberd * starting from 1. The column names and row numbers can be * displayed, optionally. By default, a new row is added automatically * if the user moves down out of the last row by pressing return * or down-arrow, provided the last row is non-empty. Rows can also be * added programmatically. Columns are usually added in the constructor, * but they can also be added later. If the user leaves a cell * at a time when the content of that cell does not represent a legal * number, then the message "bad input" will be displayed in the cell. *

A DataTableInput can be given a name and can then be added to a parser. * The parser will then recognize the name of the table. In an expression, the table name * can be followed by a ".", then one of the column names from table, then * an expression in parentheses. This represents a reference to the number in * one of the cells in the table. The expression gives the number of * the row, where rows are numbered starting from 1. For example: * "data.A(3)" represents the third number in column "A" in a table * named data. The table name and "." can also be followed by the word "sum" and * an expression in parentheses. An expression used in this way * can include references to the column names in the table and to * the special value "rowNumber". The value of expression is computed * for each row in the table and the sum of the values is computed. In the expression that * is being summed, a column name represents the * number in that column and rowNumber represents the number of * the row. For example: "data.sum(A^2)". Finally, the "." can be * followed by the word "count". "count" can, optionally be followed * by an empty pair of parentheses. This represents the number of * rows. For example: "data.count". Empty rows at the bottom * of the table are ignored in both "sum" and "count". *

Note that rows are numbered starting from 1 because row numbers * can be visible to the user, who shouldn't be subjected to zero-based * numbering. However, columns are always numbered from zero. */ public class DataTableInput extends Panel implements ParserExtension { private String objectName; // The name of the DataTableObject private Vector rows; // Vector of double[]. Each entry holds // the values in a row of a table. An // entry is null for an empty row. private Vector rowStrings; // Row values as Strings. The string "bad input" // means that the string in the cell does not // represent a legal number private String[] columnName; // The name to be used to refer to the column // in expressions. Also used as a label at the // top of the column. private int columnCount; // Number of columns private int currentRow = 1; // The number of the "currentRow" which is // specified in the setCurrentRowNumber() method. private double emptyCellValue = Double.NaN; // An empty cell is considered to have this value. private boolean throwErrors = false; // Should an error be thrown when getValue(col,row) // sees "bad input" in the cell. If not, then the // value is considered to be Double.NaN. private boolean autoAddRows = true; // If this is true, an empty row is added // when the user presses enter or hits // down-arrow while in the last row, if that row // is empty. private boolean showColumnTitles; // If true, column names are shown above the columns private boolean showRowNumbers; // If true, row numbers are shown to left of each row private DisplayPanel canvas; // Where the table appears; nested class DisplayPanel is defined below. private long serialNumber; // This is incremented each time the table changes. private Color labelBackground = new Color(220,220,220); // Colors of various things. private Color cellBackground = new Color(255,255,220); private Color blankBackground = Color.gray; private Color gridColor = Color.blue; /** * Create a DataTableInput with no columns. Columns can be added later * using the addColumn() methods. The table initially has no name. */ public DataTableInput() { this(null,0); } /** * Create a table with the specified column names. If columnNames * is null, the number of columns is zero. The name can be null, if you * don't need a name for the table. The length of the array determines * the number of columns in the table. */ public DataTableInput(String name, String[] columnNames) { this(name,columnNames == null ? 0 : columnNames.length); if (columnNames != null) for (int i = 0; i < columnNames.length; i++) setColumnName(i,columnNames[i]); } /** * Create a table with the specified number of columns, * named "A", "B", etc. The name can be null, if you * don't need a name for the table. The number of columns can be zero. */ public DataTableInput(String name, int columns) { if (columns < 0) columns = 0; setName(name); rowStrings = new Vector(); rows = new Vector(); rowStrings.addElement(null); rows.addElement(null); columnName = new String[columns]; for (int i = 0; i < columns; i++) columnName[i] = "" + (char)( (int)'A' + i ); canvas = new DisplayPanel(); setLayout(new BorderLayout()); setBackground(Color.lightGray); add(canvas,BorderLayout.CENTER); add(canvas.vScroll,BorderLayout.EAST); columnCount = columns; } //------------------------- Data access and variables /** * Required by the ParserExtension interface and not meant to be called directly. * This is called by a parser if it encounters the name of the table in an * expression. It parses the complete table reference, such as "data.A(3)" * or "data.sum(A^2)". */ public void doParse(Parser parser, ParserContext context) { int tok = context.next(); if (tok != ParserContext.OPCHARS || !context.tokenString.equals(".")) throw new ParseError("Expected a '.' after the name of a data table.",context); tok = context.next(); if (tok != ParserContext.IDENTIFIER) throw new ParseError("Expected 'sum', 'count', or the name of a column after data table name.", context); String commandName = context.tokenString; int command = -10; for (int i = 0; i < columnCount; i++) if (commandName.equalsIgnoreCase(getColumnName(i))) { command = i; break; } if (command == -10) { if (commandName.equalsIgnoreCase("sum")) command = -1; else if (commandName.equalsIgnoreCase("count")) command = -2; } if (command == -10) throw new ParseError("Unrecognized table command \""+ commandName + "\".", context); if (command == -2) { if (context.look() == ParserContext.OPCHARS && context.tokenString.equals("(")) { context.next(); if (context.next() != ParserContext.OPCHARS || !context.tokenString.equals(")")) throw new ParseError("Missing right parenthesis; \"count\" does not take a parameter.", context); } context.prog.addCommandObject(new DTEC(-2,null)); return; } if (context.next() != ParserContext.OPCHARS || !context.tokenString.equals("(")) throw new ParseError("Expected a left parentheses after table command \"" + commandName + "\".", context); ExpressionProgram saveProg = context.prog; ExpressionProgram tableProg = new ExpressionProgram(); context.prog = tableProg; if (command == -1) { context.mark(); for (int i = 0; i < columnCount; i++) context.add(getColumnVariable(i)); context.add(getRowNumberVariable()); } parser.parseExpression(context); context.prog = saveProg; if (context.next() != ParserContext.OPCHARS || !context.tokenString.equals(")")) throw new ParseError("Missing right parenthesis.", context); context.prog.addCommandObject(new DTEC(command,tableProg)); if (command == -1) context.revert(); } /** * Return the number of rows in the table, ignoring empty rows at the bottom * of the table. Note that an empty row that precedes some non-empty row * is included in the count. */ public int getNonEmptyRowCount() { int rowCt = rows.size(); while (rowCt > 0 && rows.elementAt(rowCt-1) == null) rowCt--; return rowCt; } /** * Get the number in the specified row and column. Rows are numberd starting * from 1, but columns are numbered starting from zero. If the specified * cell does not exist in the table, Double.NaN is returned. If the cell is empty, * emptyCellValue is returned. If the content of the cell does not define * a legal real number, then the action depends on the value of the missingValueIsError * property: If this property is true, then a JCMError is thrown; if it is * false, then Double.NaN is returned. */ public double getCellContents(int row, int col) { if (row < 1 || row > rows.size() || col < 0 || col > columnCount) return Double.NaN; return canvas.getValue(col,row); } /** * Put the given real number, val, in the cell in the specified row * and column, where rows are numbered starting from 1 and columns are * numbered starting from zero. This is ignored if the specified row * and column do not exist in the table. */ public void setCellContents(int row, int col, double val) { if (row < 1 || row > rows.size() || col < 0 || col > columnCount) return; canvas.setValue(col,row,val); } /** * Set the current row in the table. If the parameter is less than 1, the current * row is set to 1. * (The table keeps track of a "current row number", which is always greater than * or equal to 1. This row number is used when a column variable or row number * variable is evaluated. These variables can be added to a parser using the * addVariablesToParser method, and can then be used in expressions parsed by * that parser. When the row number variable, which is named rowNumber, is * evaluated, its value is the current row number in the table. When a column * variable is evaluated, its value is the number in the cell in the associated * column and in the current row. The setCurrentRowNumber() method, in combination * with the getNonEmptyRowCount() method allow you to iterate through the rows * of the table and evaluate the expression for each row.) */ public void setCurrentRowNumber(int i) { currentRow = (i < 1)? 1 : i; } /** * Return the current row number. */ public int getCurrentRowNumber() { return currentRow; } /** * Return a column variable for the specified column, where columns are * numbered starting from 1. The value of this variable is the number * in the specified column and in the current row of the table (as set * by the setCurrentRowNumber() method.) The name of the variable is * the name of the column. This method is protected since variables are * not meant to be used as regular variables. But they can be added to * a Parser by calling the addVariablesToParser() method.) */ protected Variable getColumnVariable(final int columnNum) { if (columnNum < 0 || columnNum >= columnCount) throw new IllegalArgumentException("Column number out of range."); return new Variable(getColumnName(columnNum),0) { public void setVal(double v) { if (currentRow < rows.size()) canvas.setValue(columnNum,currentRow,v); super.setVal(v); } public double getVal(){ if (currentRow > rows.size()) return Double.NaN; else return canvas.getValue(columnNum,currentRow); } }; } /** * Get a variable that represents the current row number in the table, * as set by the setCurrentRowNumber() method. The name of the * variable is rowNumber. */ protected Variable getRowNumberVariable() { return new Variable("rowNumber",0) { public void setVal(double v) { int val = (int)(v + 0.5); if (val < 1 || val > getNonEmptyRowCount()) val = getNonEmptyRowCount() + 1; currentRow = val; super.setVal(val); } public double getVal(){ return currentRow; } }; } /** * Add a row number variable (from the getRowNumberVariable() method) and * a column variable for each column (from the getColumnVariable() method) * to the parser. The parser will then be able to parse expressions that * refer to these variables. The value of such an expression depends on * the current row number, as set by setCurrentRowNumber(). */ public void addVariablesToParser(Parser p) { p.add(getRowNumberVariable()); for (int i = 0; i < columnCount; i++) { p.add(getColumnVariable(i)); } } /** * Get the serial number of the table. This is incremented each time the * table changes in any way. */ public long getSerialNumber() { return serialNumber; } //------------------------- /** * Set the throwErrors property. If this is true, then a JCMError is thrown when * an attempt is made to use the value of a cell that contains an invalid String. * Note that referring to an empty cell is not an error. The default value is true. */ public void setThrowErrors(boolean throwErr){ throwErrors = throwErr; } /** * Get the value of the throwErrors property, which determines whether an error * is thrown when an attempt is made to refer to the value of a cell that * contains an invalid string. */ public boolean getThrowErrors(){ return throwErrors; } /** * Set the value that should be returned when the value of an empty cell is * requested. The default value is Double.NaN. Another plausible value, in * some circumstances, would be zero. */ public void setEmptyCellValue(double val){ emptyCellValue = val; } /** * Get the value that is represented by an empty cell. */ public double getEmptyCellValue() { return emptyCellValue; } /** * If the value of autoAddRows is true, then an empty row is added to the table * automatically when the user attempts to move down from the last row of * the table, provided that the last row is non-empty (so there can only be * one auto-added row at a time). If the user leaves this row while it is * still empty, it will automatically be deleted. The default value is true. */ public void setAutoAddRows(boolean auto) { // Set the autoAddRows property. autoAddRows = auto; canvas.lastRowAutoAdded = false; } /** * Get the value of the autoAddRows property, which determines whether empty * rows are automatically added to the bottom of the table when needed. */ public boolean getAutoAddRows() { return autoAddRows; } /** * Set the name of this DataTableInput. This is only needed if the table is * to be added to a parser. The name should be a legal identifier. */ public void setName(String name) { objectName = name; } /** * Get the name of the DataInputTable (which might be null). */ public String getName() { return objectName; } /** * Set the name of column number i, where columns are numbered starting * from zero. If column variables are to be used or if * the DataTableInput itself is to be added to a parser, then the name should * be a legal identifier. If the showColumnTitles property is set to true, * then column names are shown at the top of the table. */ public void setColumnName(int i, String name) { if (name != null) columnName[i] = name; } /** * Get the name of column number i, where columns are numbered starting from zero. */ public String getColumnName(int i) { return columnName[i]; } /** * Add the specified number of empty rows at the bottom of the table. If you * want a table with a fixed number of rows, add them with this method and * set the autoAddRows property to false. */ public void addRows(int rowCt) { canvas.addRows(rowCt,rows.size()); } /** * Insert a row before the row that contains the cell that the user is editing. */ public void insertRow() { canvas.addRows(1,canvas.activeRow); } /** * Delete the row that contains the cell that the user is editing. However, * if that is the only row in the table, just make the row empty. */ public void deleteCurrentRow() { if (canvas.activeRow == rows.size() - 1 && rows.size() > 1) { canvas.setActive(canvas.activeRow-1,canvas.activeColumn); rows.removeElementAt(canvas.activeRow + 1); rowStrings.removeElementAt(canvas.activeRow + 1); } else { rows.removeElementAt(canvas.activeRow); rowStrings.removeElementAt(canvas.activeRow); } if (rows.size() == 0) { rows.addElement(null); rowStrings.addElement(null); } String[] vals = (String[])rowStrings.elementAt(canvas.activeRow); if (vals == null || vals[canvas.activeColumn] == null) canvas.input.setText(""); else canvas.input.setText(vals[canvas.activeColumn]); canvas.checkScroll(); canvas.repaint(); if (canvas.rowLabelCanvas != null) canvas.rowLabelCanvas.repaint(); if (canvas.columnLabelCanvas != null) canvas.columnLabelCanvas.repaint(); serialNumber++; } /** * Remove all rows from the table, leaving just one empty row. */ public void clear() { rows = new Vector(); rowStrings = new Vector(); rows.addElement(null); rowStrings.addElement(null); canvas.setActive(0,0); canvas.checkScroll(); canvas.repaint(); if (canvas.rowLabelCanvas != null) canvas.rowLabelCanvas.repaint(); if (canvas.columnLabelCanvas != null) canvas.columnLabelCanvas.repaint(); serialNumber++; } /** * Get the number of columns in the table. */ public int getColumnCount() { return columnName.length; } /** * Add a column at the right side of the table, with all cells initially * empty. The name of the columns will be single letter such as 'A', 'B', ... */ public int addColumn() { return addColumn(null); } /** * Add a column with the specified name at the right side of the table, with all cells initially * empty. This is inefficient if the table already contains a bunch of non-empty rows. */ public int addColumn(String name) { int newSize = columnName.length + 1; String[] newNames = new String[newSize]; for (int i = 0; i < columnName.length; i++) newNames[i] = columnName[i]; if (name == null) newNames[newSize-1] = "" + (char)( (int)'A' + newSize - 1 ); else newNames[newSize-1] = name; columnName = newNames; int rowCt = rows.size(); for (int i = 0; i < rowCt; i++) { if (rows.elementAt(i) != null) { double[] oldRow = (double[])rows.elementAt(i); double[] newRow = new double[newSize]; for (int j = 0; j < oldRow.length; j++) newRow[j] = oldRow[j]; newRow[newSize - 1] = Double.NaN; rows.setElementAt(newRow,i); } if (rowStrings.elementAt(i) != null) { String[] oldRow = (String[])rows.elementAt(i); String[] newRow = new String[newSize]; for (int j = 0; j < oldRow.length; j++) newRow[j] = oldRow[j]; rowStrings.setElementAt(newRow,i); } } if (canvas.hScroll != null) canvas.checkScroll(); canvas.repaint(); if (canvas.columnLabelCanvas != null) canvas.columnLabelCanvas.repaint(); columnCount = columnName.length; serialNumber++; return columnCount - 1; } /** * Test whether the column name is shown at the top of each column. */ public boolean getShowColumnTitles() { return showColumnTitles; } /** * If set to true, then the column name is shown at the top of each column. The * default value is false. This is meant to be called before the table has been * shown on the screen, such as in the init() method of an applet. If you call it * after the table has already been shown, you will have to validate the panel * yourself. */ public void setShowColumnTitles(boolean show) { if (show == showColumnTitles) return; showColumnTitles = show; if (showColumnTitles) { canvas.makeColumnLabelCanvas(); add(canvas.columnLabelCanvas, BorderLayout.NORTH); } else { remove(canvas.columnLabelCanvas); canvas.columnLabelCanvas = null; } } /** * Test whether row numbers are shown. */ public boolean getShowRowNumbers() { return showRowNumbers; } /** * If set to true, then the row number is shown at the left of each row. The * default value is false. This is meant to be called before the table has been * shown on the screen, such as in the init() method of an applet. If you call it * after the table has already been shown, you will have to validate the panel * yourself. */ public void setShowRowNumbers(boolean show) { if (show == showRowNumbers) return; showRowNumbers = show; if (showRowNumbers) { canvas.makeRowLabelCanvas(); add(canvas.rowLabelCanvas, BorderLayout.WEST); } else { remove(canvas.rowLabelCanvas); canvas.rowLabelCanvas = null; } } /** * Returns the color that is used as a background for row numbers and column titles. */ public Color getLabelBackground() { return labelBackground; } /** * Set the color to be used as a background for row numbers and column titles. * The default is a very light gray. */ public void setLabelBackground(Color color) { if (color != null) labelBackground = color; } /** * Returns the color that is used as a background for cells in the table. */ public Color getCellBackground() { return cellBackground; } /** * Set the color to be used as a background for cells in the table. * The default is a light yellow. */ public void setCellBackground(Color color) { if (color != null) cellBackground = color; } /** * Returns the color that is used for blank areas in the table, below the * rows of cells. */ public Color getBlankBackground() { return blankBackground; } /** * Get the color to be used as a background blank areas in the table, below the * rows of cells. The default is a gray. */ public void setBlankBackground(Color color) { if (color != null) blankBackground = color; } /** * Returns the color that is used for the lines between cells in the table. */ public Color getGridColor() { return gridColor; } /** * Get the color to be used for the lines between cells in the table. * The default is a blue. */ public void setGridColor(Color color) { if (color != null) gridColor = color; } /** * Read data for table from the specified Reader. One row is filled * from each non-empty line of input. The line should contain * numbers separated by spaces/tabs/commas. The word "undefined" * can be used to represent an empty cell. Otherwise, if a non-number is * encountered, an error occurs. If not enough numbers are * found on a line, the extra columns are filled with empties. After * filling all columns, extra data on the line is ignored. * Data currently in the table is removed and replaced (if no error * occurs during reading). In the case of an error, if throwErrors is * true, then a JCMError is thrown; if throwErrors is false, no * error is thrown, but the return value is false. If no error occurs, * the return value is true. If an error occurs, the previous data * in the table is left unchanged. */ public boolean readFromStream(Reader in) { Vector newRows = new Vector(); int cols = columnCount; try { StreamTokenizer tokenizer = new StreamTokenizer(in); tokenizer.resetSyntax(); tokenizer.eolIsSignificant(true); tokenizer.whitespaceChars(',', ','); tokenizer.whitespaceChars(' ', ' '); tokenizer.whitespaceChars('\t', '\t'); tokenizer.wordChars('a','z'); tokenizer.wordChars('A','Z'); tokenizer.wordChars('0','9'); tokenizer.wordChars('.','.'); tokenizer.wordChars('+','+'); tokenizer.wordChars('-','-'); int token = tokenizer.nextToken(); while ( true ) { while (token == StreamTokenizer.TT_EOL) // ignore empty lines token = tokenizer.nextToken(); if (token == StreamTokenizer.TT_EOF) break; double[] row = new double[cols]; for (int i = 0; i < cols; i++) { if (token == StreamTokenizer.TT_EOL || token == StreamTokenizer.TT_EOF) row[i] = Double.NaN; else if (token == StreamTokenizer.TT_WORD) { if (tokenizer.sval.equalsIgnoreCase("undefined")) row[i] = Double.NaN; else { try { Double d = new Double(tokenizer.sval); row[i] = d.doubleValue(); } catch (NumberFormatException e) { throw new IOException("Illegal non-numeric data (" + tokenizer.sval + ") encountered."); } } token = tokenizer.nextToken(); } else throw new IOException("Illegal non-numeric data encountered."); } newRows.addElement(row); while (token != StreamTokenizer.TT_EOL && token != StreamTokenizer.TT_EOF) token = tokenizer.nextToken(); } if (rows.size() == 0) throw new IOException("Empty data was found."); } catch (Exception e) { if (throwErrors) throw new JCMError("Error while reading data: " + e, this); return false; } canvas.setActive(0,0); rows = newRows; rowStrings = new Vector(); for (int i = 0; i < rows.size(); i++) { String[] s = new String[cols]; double[] d = (double[])rows.elementAt(i); for (int col = 0; col < cols; col++) if (Double.isNaN(d[col])) s[col] = null; else s[col] = NumUtils.realToString(d[col]); rowStrings.addElement(s); } canvas.input.setText( ((String[])rowStrings.elementAt(0))[0] ); if (canvas.hScroll != null) canvas.hScroll.setValue(0); canvas.vScroll.setValue(0); canvas.checkScroll(); canvas.repaint(); if (canvas.rowLabelCanvas != null) canvas.rowLabelCanvas.repaint(); if (canvas.columnLabelCanvas != null) canvas.columnLabelCanvas.repaint(); serialNumber++; return true; } //------------------------------ private nested classes ------------------------------- private class InputBox extends TextField { // An object of type InputBox is used for user input // of numbers in the table. There is only one input box // and it moves around. InputBox() { super(12); setBackground(Color.white); setForeground(Color.black); enableEvents(AWTEvent.KEY_EVENT_MASK + AWTEvent.MOUSE_EVENT_MASK); } public void processKeyEvent(KeyEvent evt) { if (evt.getID() == KeyEvent.KEY_PRESSED) { int ch = evt.getKeyCode(); char chr = evt.getKeyChar(); boolean use = (chr != 0 && Character.isDigit(chr) || chr == '.' || chr == 'E' || chr == '-' || chr == '+' || chr == 'e') || ch == KeyEvent.VK_DELETE || ch == KeyEvent.VK_BACK_SPACE; boolean useControl = use || chr == 0; if (!useControl || ch == KeyEvent.VK_ENTER || ch == KeyEvent.VK_DOWN || ch == KeyEvent.VK_UP || ch == KeyEvent.VK_TAB) { if (ch == KeyEvent.VK_ENTER || ch == KeyEvent.VK_DOWN) canvas.doRowDown(); else if (ch == KeyEvent.VK_UP) canvas.doRowUp(); else if (ch == KeyEvent.VK_TAB) canvas.doColumnRight(); /* else Toolkit.getDefaultToolkit().beep(); */ evt.consume(); } else if (ch == KeyEvent.VK_LEFT && getCaretPosition() == 0) { canvas.doColumnLeft(); evt.consume(); } else if (ch == KeyEvent.VK_RIGHT && getCaretPosition() == getText().length()) { canvas.doColumnRight(); evt.consume(); } } super.processKeyEvent(evt); } public void processMouseEvent(MouseEvent evt) { if (evt.getID() == MouseEvent.MOUSE_PRESSED) canvas.ensureActiveVisible(); super.processMouseEvent(evt); } } // end nested class InputBox private class DisplayPanel extends Panel implements TextListener,MouseListener,AdjustmentListener,ComponentListener { // An object of this class is the actual table seen by the user. // The panel itself is just the grid of cells. The row and column // labels and the scroll bars are variables in the DisplayPanel // ojbect, but they are added to the containing Panel in the // constructor for DataTableInput above. InputBox input; // Text field for user input, from nested class defined above. int activeRow = 0, activeColumn = 0; // Where the Input box is. int rowHeight=-1, columnWidth; // Size of each cell. rowHeight = -1 indiacates // the size is not yet known. Scrollbar hScroll,vScroll; // for scrolling through the grid of cells. Canvas rowLabelCanvas, columnLabelCanvas; // These canvasses hold the row // and column lables and are displayed // to the left of and above the grid of cells. // They scroll along with the grid. // They are null and are enabled by // methods setShowRowNumbers and setShowColumnTitles boolean lastRowAutoAdded; // True if last row was auto added. // If the user leaves this row while // it is still empty, it will be auto deleted. // Empty rows added with the addRows() method // are not deleted in this way. DisplayPanel() { setBackground(cellBackground); input = new InputBox(); vScroll = new Scrollbar(Scrollbar.VERTICAL); vScroll.setBackground(Color.lightGray); input.addTextListener(this); vScroll.addAdjustmentListener(this); addMouseListener(this); setLayout(null); add(input); addComponentListener(this); } void makeRowLabelCanvas() { rowLabelCanvas = new Canvas() { // canvas for showing row labels public void paint(Graphics g) { int topRow = vScroll.getValue() / rowHeight; int rowCt = getSize().height / rowHeight + 1; int tableRows = rows.size(); FontMetrics fm = g.getFontMetrics(); int textOffset = (rowHeight + fm.getAscent()) / 2; int vScrollVal = vScroll.getValue(); for (int i = topRow; i < rowCt + topRow && i < tableRows; i++) { String rs = "" + (i+1); int os = (getSize().width - fm.stringWidth(rs)) / 2; g.drawString(rs,os,textOffset + rowHeight*i - vScrollVal); } } public Dimension getPreferredSize() { return new Dimension(35,50); } }; rowLabelCanvas.setBackground(labelBackground); } void makeColumnLabelCanvas() { columnLabelCanvas = new Canvas() { // canvas for showing column labels public void paint(Graphics g) { int leftColumn = 0; if (hScroll != null) leftColumn = hScroll.getValue() / columnWidth; int blank = (rowLabelCanvas == null)? 0 : 35; // width of rowLabelCanvas int columnCt = (getSize().width-blank) / columnWidth + 1; FontMetrics fm = g.getFontMetrics(); int textOffset = (getSize().height + fm.getAscent()) / 2; int hScrollVal = hScroll == null ? 0 : hScroll.getValue(); for (int i = leftColumn; i < leftColumn + columnCt && i < columnCount; i++) { String s = getColumnName(i); int os = (columnWidth - fm.stringWidth(s)) / 2; g.drawString(s,blank + i*columnWidth + os - hScrollVal,textOffset); } g.setColor(Color.gray); g.fillRect(0,0,blank,getSize().height); } public Dimension getPreferredSize() { return new Dimension(50,20); } }; columnLabelCanvas.setBackground(labelBackground); } public void addNotify() { // Determine the size of a cell, which is based on the preferred size // of the input box (which can't be determined until after the peer // is added. (I hope this works on all platforms!) super.addNotify(); if (rowHeight != -1) return; Dimension size = input.getPreferredSize(); rowHeight = size.height-1; columnWidth = size.width-1; input.setBounds(1,1,columnWidth+1,rowHeight+1); } public void update(Graphics g) { // Don't fill in with background before painting. paint(g); } public void paint(Graphics g) { // Draw the grid of cells, in position based on scroll bar values. int hScrollVal = (hScroll == null)? 0 : hScroll.getValue(); int vScrollVal = vScroll.getValue(); int width = getSize().width; // width and height of component int height = getSize().height; int tableWidth = columnCount*columnWidth + 2; int tableHeight = rows.size()*rowHeight + 2; int tableRows = rows.size(); // Number of rows in table Rectangle clip = g.getClipBounds(); // Try to avoid uncessary painting by checking clip rect. int topRow, rowCt, leftColumn, columnCt; if (clip != null) { // change data about included rows, columns topRow = (vScrollVal + clip.y) / rowHeight; rowCt = clip.height / rowHeight + 1; leftColumn = (hScrollVal + clip.x) / columnWidth; columnCt = clip.width / columnWidth + 1; } else { topRow = vScrollVal / rowHeight; // top visible row rowCt = height / rowHeight + 1; // num of rows possibly visible leftColumn = hScrollVal / columnWidth;// leftmost visible column columnCt = width / columnWidth + 1; // num of columns visible } FontMetrics fm = g.getFontMetrics(); int textOffset = (rowHeight + fm.getAscent()) / 2; // from top of box to text baseline for (int i = topRow; i < topRow + rowCt && i < tableRows; i++ ) { String[] contents = (String[])rowStrings.elementAt(i); for (int c = 0; c < columnCount; c++) if (c != activeColumn || i != activeRow) { g.setColor(cellBackground); g.fillRect(1+c*columnWidth-hScrollVal,1+i*rowHeight-vScrollVal,columnWidth,rowHeight); g.setColor(getForeground()); if (contents != null && contents[c] != null && contents[c].length() > 0) { String s = contents[c]; g.drawString(s,c*columnWidth + 5 - hScrollVal, textOffset + i*rowHeight - vScrollVal); } } } if (width > tableWidth) { g.setColor(blankBackground); g.fillRect(tableWidth,0,width-tableWidth,height); } if (height > tableHeight) { g.setColor(blankBackground); g.fillRect(0,tableHeight,width,height-tableHeight); } g.setColor(gridColor); g.drawRect(0,0,tableWidth, tableHeight); g.drawRect(1,1, tableWidth - 2, tableHeight - 2); for (int i = - 1; i < topRow + rowCt && i < tableRows; i++) g.drawLine(0, 1 + (i+1)*rowHeight - vScrollVal, tableWidth - 1, 1 + (i+1)*rowHeight - vScrollVal); for (int j = 0; j <= columnCount; j++) g.drawLine(1 + j*columnWidth - hScrollVal, 0, 1 + j*columnWidth - hScrollVal, tableHeight - 1); } void setActive(int row, int column) { // Move the input box to the specified row and column, and // move the focus to the input box. if (row != activeRow || column != columnWidth) { int topOffset = vScroll.getValue(); int leftOffset = (hScroll == null)? 0 : hScroll.getValue(); int y = -topOffset + row*rowHeight + 1; int x = -leftOffset + column*columnWidth + 1; input.setLocation(x,y); activeRow = row; activeColumn = column; String[] contents = (String[])rowStrings.elementAt(activeRow); if (contents == null || contents[activeColumn] == null) input.setText(""); else input.setText(contents[activeColumn]); } ensureActiveVisible(); input.selectAll(); input.requestFocus(); } void doRowDown() { // Move active box down one row. Create a new row if in the // last row, that row is non-empty, and autoAddRows is true. int tableRows = rows.size(); if (activeRow == tableRows - 1 && autoAddRows && rows.elementAt(tableRows-1) != null) { addRows(1,tableRows); lastRowAutoAdded = true; } if (activeRow < rows.size() - 1) setActive(activeRow+1,activeColumn); else { ensureActiveVisible(); input.requestFocus(); } } void doRowUp() { // Move up one row. If leaving an empty row that was autoadded, delete it. if (activeRow == 0) return; setActive(activeRow - 1, activeColumn); if (autoAddRows && lastRowAutoAdded == true && activeRow == rows.size() - 2 && rows.elementAt(activeRow+1) == null) { // delete empty row from bottom of table rows.removeElementAt(rows.size() - 1); rowStrings.removeElementAt(rowStrings.size() - 1); checkScroll(); repaint(); if (rowLabelCanvas != null) rowLabelCanvas.repaint(); input.requestFocus(); } lastRowAutoAdded = false; } void doColumnRight() { // Move active box right to the next column, possibly // wrapping around to the first column. int c = activeColumn + 1; if (c >= columnCount) c = 0; setActive(activeRow,c); } void doColumnLeft() { // Move active box left to the next column, possibly // wrapping around to the last column. int c = activeColumn - 1; if (c < 0) c = columnCount - 1; setActive(activeRow,c); } void ensureActiveVisible() { // Make sure that the entire input box is visible. int x = columnWidth*activeColumn + 1; int y = rowHeight*activeRow + 1; int visibleLeft = (hScroll == null)? 0 : hScroll.getValue(); int visibleTop = vScroll.getValue(); int visibleRight = visibleLeft + getSize().width; int visibleBottom = visibleTop + getSize().height; int offsetX = 0; int offsetY = 0; if (x + columnWidth > visibleRight) offsetX = -(x + columnWidth - visibleRight); if (x < visibleLeft) offsetX = visibleLeft - x; if (y + rowHeight > visibleBottom) offsetY = -(y + rowHeight - visibleBottom); if (y < visibleTop) offsetY = visibleTop - y; if (offsetX == 0 && offsetY == 0) return; if (offsetX != 0) { if (hScroll != null) hScroll.setValue(visibleLeft - offsetX); if (columnLabelCanvas != null) columnLabelCanvas.repaint(); } if (offsetY != 0) { vScroll.setValue(visibleTop - offsetY); if (rowLabelCanvas != null) rowLabelCanvas.repaint(); } input.setLocation(x - ( hScroll == null ? 0 : hScroll.getValue() ),y - vScroll.getValue()); repaint(); } void addRows(int num, int before) { // Add specified number of rows to table, before row // number before. If before is after the last existing // row, the rows are added at the end of the table. serialNumber++; if (num <= 0) return; if (before >= rows.size()) { for (int i = 0; i < num; i++) { rows.addElement(null); rowStrings.addElement(null); lastRowAutoAdded = false; } } else { if (before < 0) before = 0; for (int i = 0; i < num; i++) { rows.insertElementAt(null,before); rowStrings.insertElementAt(null,before); } if (activeRow >= before) { // data in active cell changes String[] vals = (String[])rowStrings.elementAt(activeRow); if (vals == null || vals[activeColumn] == null) input.setText(""); else input.setText(vals[activeColumn]); } } checkScroll(); repaint(); if (rowLabelCanvas != null) rowLabelCanvas.repaint(); } void setValue(int column, int row, double value) { // set the value in the column at given row and column. // Note that row numbers start at 1 in this method!! // Row and column numbers are assumed to be in legal range!! String num = NumUtils.realToString(value); setRowData(row-1,column,num,value); if (column == activeColumn && row-1 == activeRow) input.setText(num); else { repaintItem(row-1,column); } } double getValue(int column, int row) { // Get the value from the table in the given row and column. // Note that row numbers start at 1 in this method!! // Row and column numbers are assumed to be in legal range!! if (rows.elementAt(row-1) == null) return emptyCellValue; else { double d = ((double[])rows.elementAt(row-1))[column]; if ( ! Double.isNaN(d) ) return d; else { String val = ((String[])rowStrings.elementAt(row-1))[column]; if (val == null || val.length() == 0) return emptyCellValue; else // val is "bad input", the only other possibility if d is NaN if (throwErrors) throw new JCMError("Invalid numerical input in data table, column \"" + getColumnName(column) + "\", row " + row + ".", this); else return Double.NaN; } } } public void textValueChanged(TextEvent txt) { // From TextListener interface. When text in input box changes, // change the stored string and stored value for that position to match. String num = input.getText().trim(); if (num.length() == 0) setRowData(activeRow,activeColumn,num,Double.NaN); else { double x; try { Double d = new Double(num); x = d.doubleValue(); } catch (NumberFormatException e) { x = Double.NaN; } if (Double.isNaN(x)) setRowData(activeRow,activeColumn,"bad input",x); else setRowData(activeRow,activeColumn,num,x); } } void setRowData(int row, int col, String num, double val) { // puts num, val into rows, rowStrings vectors at position row,col // Empty rows are always represented by null's in the rowVals // and rowStrings vectores. This requires a bit of care. serialNumber++; double[] rowVals = (double[])rows.elementAt(row); String[] rowStr = (String[])rowStrings.elementAt(row); if (num.length() == 0) { if (rowStr == null || rowStr[col] == null) return; rowStr[col] = null; rowVals[col] = Double.NaN; boolean empty = true; for (int i = 0; i < rowStr.length; i++) if (rowStr[i] != null) { empty = false; break; } if (empty) { rows.setElementAt(null,row); rowStrings.setElementAt(null,row); } } else { if (num.length() > 12) num = NumUtils.realToString(val,12); if (rowStr == null) { int ct = columnCount; rowVals = new double[ct]; rowStr = new String[ct]; for (int i = 0; i < ct; i++) rowVals[i] = Double.NaN; rows.setElementAt(rowVals,row); rowStrings.setElementAt(rowStr,row); } rowStr[col] = num; rowVals[col] = val; } } protected void repaintItem(int row, int column) { // forces a repaint for just the specified cell int y = row*rowHeight - vScroll.getValue(); int x = column*columnWidth; if (hScroll != null) x -= hScroll.getValue(); repaint(x+1,y+1,columnWidth-1,rowHeight-1); } public void adjustmentValueChanged(AdjustmentEvent evt) { // From the AdjustmentListener interface. React to // change in scroll positions. repaint(); if (evt.getSource() == vScroll) { if (rowLabelCanvas != null) rowLabelCanvas.repaint(); } else { if (columnLabelCanvas != null) columnLabelCanvas.repaint(); } int x = columnWidth*activeColumn + 1; if (hScroll != null) x -= hScroll.getValue(); int y = rowHeight*activeRow + 1 - vScroll.getValue(); input.setLocation(x,y); } public void mousePressed(MouseEvent evt) { // From the MouseListener interface. Move the active // cell to an input cell close to mouse click. int hOffset = (hScroll == null)? 0 : hScroll.getValue(); int vOffset = vScroll.getValue(); int row = (evt.getY() + vOffset - 1) / rowHeight; if (row < 0) row = 0; else if (row >= rows.size()) row = rows.size() - 1; int col = (evt.getX() + hOffset - 1) / columnWidth; if (col < 0) col = 0; else if (col >= columnCount) col = columnCount - 1; if (row == activeRow && col == activeColumn) ensureActiveVisible(); // want to avoid doing selectAll if box doesn't move else setActive (row,col); int emptyRow = rows.size() - 1; if (!lastRowAutoAdded || emptyRow == row) return; lastRowAutoAdded = false; if (rows.elementAt(emptyRow) != null) return; rows.removeElementAt(emptyRow); rowStrings.removeElementAt(emptyRow); checkScroll(); repaint(); if (rowLabelCanvas != null) rowLabelCanvas.repaint(); } public void componentResized(ComponentEvent evt) { // From ComponentListener interface. Fix scroll bars after resize. checkScroll(); } void checkScroll() { // Make sure scoll bars are OK after resize, adding rows, etc. int width = DisplayPanel.this.getSize().width; int height = DisplayPanel.this.getSize().height; if (rowHeight == -1 || width <= 1) return; int tableWidth = columnWidth*columnCount + 2; int tableHeight = rowHeight*rows.size() + 2; int oldTop = vScroll.getValue(); int oldLeft = (hScroll == null)? 0 : hScroll.getValue(); boolean revalidate = false; if (width >= tableWidth - 2) { if (hScroll != null) { int scrollHeight = hScroll.getPreferredSize().height; DataTableInput.this.remove(hScroll); hScroll = null; height += scrollHeight; revalidate = true; } } else { if (hScroll == null) { hScroll = new Scrollbar(Scrollbar.HORIZONTAL); hScroll.setBackground(Color.lightGray); int scrollHeight = hScroll.getPreferredSize().height; height -= scrollHeight; DataTableInput.this.add(hScroll, BorderLayout.SOUTH); hScroll.addAdjustmentListener(this); revalidate = true; } if (oldLeft > tableWidth - width) hScroll.setValues(tableWidth-width,width,0,tableWidth); else hScroll.setValues(oldLeft,width,0,tableWidth); hScroll.setUnitIncrement(columnWidth / 4); if (width > 1) hScroll.setBlockIncrement((3*width)/4); } if (height >= tableHeight - 2) { vScroll.setEnabled(false); vScroll.setValues(0,1,0,1); } else { if (oldTop > tableHeight - height) vScroll.setValues(tableHeight-height,height,0,tableHeight); else vScroll.setValues(oldTop,height,0,tableHeight); vScroll.setUnitIncrement(rowHeight); if (height > 1) vScroll.setBlockIncrement((3*height)/4); vScroll.setEnabled(true); } int x = columnWidth*activeColumn + 1; if (hScroll != null) x -= hScroll.getValue(); int y = rowHeight*activeRow + 1 - vScroll.getValue(); input.setLocation(x,y); if (revalidate) DataTableInput.this.validate(); } public Dimension getPreferredSize() { if (rowHeight == -1) return new Dimension(350,200); else if (columnCount >= 4) return new Dimension(4*columnWidth+2,6*rowHeight+2); else return new Dimension(columnCount*columnWidth+2,6*rowHeight+2); } public void mouseEntered(MouseEvent evt) {} // Other methods from listener interfaces. public void mouseExited(MouseEvent evt) {} public void mouseClicked(MouseEvent evt) {} public void mouseReleased(MouseEvent evt) {} public void componentHidden(ComponentEvent evt) {} public void componentShown(ComponentEvent evt) {} public void componentMoved(ComponentEvent evt) {} } // end nested class Display Panel private class DTEC implements ExpressionCommand { // This is used in the doParse() method. When a reference to a DataInputTable // is found by a parser, the doParse() method will add an object of this // type to the ExpressionProgram that the parseris producing. A DTEC represents // a sub-exprssion such as "data.A(3)" or "data.sum(A^2)" ExpressionProgram prog; // The expression inside the parentheses int command; // The column number for an expression such as "data.A(3)" which // represents the value of a cell in column "A". For the sum // function, command is -1. For the count funtion, it is -2. DTEC(int command, ExpressionProgram prog) { this.command = command; this.prog = prog; } public void apply(StackOfDouble stack, Cases cases) { if (command >= 0) { // Reference to a column. Value of prog gives row number. double loc = prog.getVal(); if (Double.isNaN(loc) || loc < 0.5 || loc >= rows.size() + 0.5) stack.push(Double.NaN); else stack.push(canvas.getValue(command,(int)(loc+0.5))); } else if (command == -1) { // sum of the prog expression for all rows in the table. double sum = 0; int top = getNonEmptyRowCount(); for (int row = 1; row <= top; row++) { setCurrentRowNumber(row); sum += prog.getVal(); } stack.push(sum); } else if (command == -2) { // the count of rows in the table stack.push(getNonEmptyRowCount()); } } public void compileDerivative(ExpressionProgram prog, int myIndex, ExpressionProgram deriv, Variable wrt) { if (command != -1) { deriv.addConstant(0); } else { ExpressionProgram d = (ExpressionProgram)this.prog.derivative(wrt); deriv.addCommandObject( new DTEC(command,d) ); } } public int extent(ExpressionProgram prog, int myIndex) { return 1; } public boolean dependsOn(Variable x) { if (command == -2) return false; else return prog.dependsOn(x); } public void appendOutputString(ExpressionProgram prog, int myIndex, StringBuffer buffer) { buffer.append(getName()); buffer.append('.'); if (command == -2) buffer.append("count"); else if (command == -1) buffer.append("sum"); else buffer.append(getColumnName(command)); buffer.append("("); if (command != -2) buffer.append(this.prog.toString()); buffer.append(")"); } } // end nested class DTEC } // end class DataTableInput jcm1-source/edu/hws/jcm/awt/ErrorReporter.java0000644000076500011320000000701411741343635020612 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.awt; /** * To allow different styles of reporting errors, a * Controller uses an ErrorReporter to report any * errors that are thrown during its checkInput/compute * cycle. The DisplayCanvas and MessagePopup classes * implement this interface. * * @author David Eck */ public interface ErrorReporter { /** * Report the specifed message as an error. If source is non-null, * then it is the Controller that called this routine. In that case, * if the error reporter is capable of clearing its own error * condition, it should call source.errorCleared() when it does so. * * @param source Controller that called this method (if non-null). * @param message error message to report. */ public void setErrorMessage(Controller source, String message); /** * Clear the error reprort, if there is one. */ public void clearErrorMessage(); /** * Get the error message that is currently being displayed, or * return null if there is no error message. */ public String getErrorMessage(); } jcm1-source/edu/hws/jcm/draw/0000755000076500011320000000000011741343635015273 5ustar djefacultyjcm1-source/edu/hws/jcm/draw/DisplayCanvas.java0000644000076500011320000011176611741343635020713 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.awt.*; import java.util.StringTokenizer; import java.util.Vector; import java.awt.*; import java.awt.event.*; /** * A DisplayCanvas is a drawing area that can contain one or more CoordinateRects. * Each CoordinateRect can, in turn, contain Drawable items. If you only want * one CoordinateRect that fills the whole canvas, you can for the most part * ignore the CoordinateRect and work only with the canvas. * The option of using an offscreen image for double buffering is provided. * By default, this option is on. *

If a DisplayCanvas is added to a * Controller, then it will take care of calling the checkInput() * and compute() methods of the InputObjects and Computables that * it contains, so there is no need to add them individually to * the Controller. If the DisplayCanvas is added to a JCMPanel, it * is automatically added to the Controller for that JCMPanel. * (On the other hand, if a DisplayCanvas is added to a Controller, * this means that all the * items in the DisplayCanvas will be recomputed, even if only some * of them need to be.) *

The canvas can display an error message that goes away * when the canvas is clicked or when clearErrorMessage() * is called. This allows the Canvas to be used as an * ErrorReporter for a Controller or LimitControlPanel. *

When any changes are made to the contents of the Canvas, * doRedraw() must be called for those changes to take effect. * Normally, doRedraw() is called by the CoordinateRect or by * one of the Drawables in a CoordinateRect. Use doRedraw(CoordinateRect c) * or doRedraw(int index) to redraw a single CoordinateRect. Note that * repainting the canvas is not enough, because this will not automatically * refresh the off-screen image. * */ public class DisplayCanvas extends Canvas implements ErrorReporter, InputObject, Computable { private Vector coordinateRects; // Contains all the CoordinateRects that have been added to this canvas. // The elements are members of the private static nested class CRData. private boolean useOffscreenCanvas = true; // If true, double buffering is used. private boolean handleMouseZooms = false; // If true, clicking on a CoordinateRect // will zoom in on the clicked point, or // will zoom out if the shift key is down. /** * Create a DisplayCanvas with a white background containing no CoordinateRects. */ public DisplayCanvas() { setBackground(Color.white); enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); } /** * Create a DisplayCanvas with a white background and containing the * specified CoordinateRect. The CoordinateRect fills the entire canvas. * * @param c The CoordinateRect that will fill the canvas. If c is null, no CoordinateRect is added to the canvas. */ public DisplayCanvas(CoordinateRect c) { this(); if (c != null) addCoordinateRect(c); } /** * Release the memory used the by the off-screen image, if any, that is used for * double-buffering. It's a good idea to call this if the DisplayCanvas is in * an applet and the applet is stopped. */ public void releaseResources() { OSC = null; OSG = null; } /** * Set the "handleMouseZooms" property of this DisplayCanvas. IF the value is true, * then clicking on the canvas will zoom in on the point that is clicked and shift-clicking * will zoom out from that point. Only the CoordinateRect, if any, that contains the point * is zoomed. Furthermore, if the user clicks-and-drags, a rectangle * is drawn. When the mouse is released, the interior of the rectangle is zoomed to fill * the CoordinateRect. This property is false by default. */ public void setHandleMouseZooms(boolean handle) { handleMouseZooms = handle; } /** * Get the "handleMouseZooms" property of this DisplayCanvas, which determines whether the * canvas reacts to mouse events by zooming the CoordinateRect that is clicked. */ public boolean getHandleMouseZooms() { return handleMouseZooms; } /** * Set the "useOffscreenCanvas" property of this DisplayCanvas. IF the value is true, * an off-screen image is used for double buffering. This property is true by default. */ public boolean getUseOffscreenCanvas() { return useOffscreenCanvas; } /** * Get the "useOffscreenCanvas" property of this DisplayCanvas, which determines whether * double-buffering is used. */ public void setUseOffscreenCanvas(boolean use) { useOffscreenCanvas = use; if (!use) { OSC = null; OSG = null; } } // ----------- For managing CoordinateRects ------------------------ /** * This private subclass of DisplayCanvas holds the data for one CoordinateRect * contained in a DisplayCanvas. */ private static class CRData implements java.io.Serializable { // Data for one coordinate rect CoordinateRect coords; double xmin, xmax, ymin, ymax; // Values between 0 and 1 that // specify the region of the canvas occupied by this // CoordinateRect. Color background; // Color to fill area with before drawing. // If it's null, no fill is done. (The display color // of the Canvas shows through.) } /** * Add the specified Drawable item to the first CoordinateRect in this DisplayCanvas. * If no CoordinateRect is associated with the canvas, one is created to fill the * entire canvas. */ public void add(Drawable d) { if (coordinateRects == null) addCoordinateRect(new CoordinateRect()); CoordinateRect c = ((CRData)coordinateRects.elementAt(0)).coords; c.add(d); } /** * Add a Drawable item to one of the CoordinateRects associated with the Canvas. * * @param d The Drawable item to be added to a CoordinateRect * @param coordRectIndex The index of the CoordinateRect, where the index of the first * CoordinateRect that was added to the cavas is zero, the index of the second is one, * and so on. A CoordinateRect with the specified index must already exist in the * canvas, or an IllegalArgumentException is thrown. */ public void add(Drawable d, int coordRectIndex) { if (coordinateRects == null || coordRectIndex < 0 || coordRectIndex >= coordinateRects.size()) throw new IllegalArgumentException("Internal programming error: CoordinateRect index (" + coordRectIndex + ")out of range."); CoordinateRect c = ((CRData)coordinateRects.elementAt(coordRectIndex)).coords; c.add(d); } /** * Add the specified CoordinateRect to this DisplayCanvas, filling the entire canvas, * and with background color equal to the background color of the canvas. * * @param c the CoordinateRect to be added. If null, an IllegalArgumentException is thrown. */ public void addCoordinateRect(CoordinateRect c) { addCoordinateRect(c,0,1,0,1,null); } /** * Add a CoordinateRect to the canvas, occupying a specified region of the canvas. * * @param coords The CoordinateRect to be added. If this is null, an IllegalArgumentExceptionis thrown. * @param hmin Specifies the left edge of the CoordinateRect in the canvas, as a fraction of the size of the canvas. * This must be in the range form 0 to 1, or an IllegalArgumentException is thrown. * @param hmax Specifies the right edge of the CoordinateRect in the canvas, as a fraction of the size of the canvas. * This must be in the range form 0 to 1 and must be strictly greater than hmin, or an IllegalArgumentException is thrown. * @param vmin Specifies the top edge of the CoordinateRect in the canvas, as a fraction of the size of the canvas. * This must be in the range form 0 to 1, or an IllegalArgumentException is thrown. * @param vmax Specifies the bottom edge of the CoordinateRect in the canvas, as a fraction of the size of the canvas. * This must be in the range form 0 to 1 and must be strictly greater than vmin, or an IllegalArgumentException is thrown. * @param background The background color of the CoordinateRect. The CoordinateRect is filled with this color * before the Drawables that it contains are drawn. If background is null, no filling takes place * and the canvas shows through. */ public void addCoordinateRect(CoordinateRect coords, double hmin, double hmax, double vmin, double vmax, Color background) { if (hmin < 0 || hmin > 1 || hmax < 0 || hmax > 1 || hmin >= hmax || vmin < 0 || vmin > 1 || vmax < 0 || vmax > 1 || vmin >= vmax) throw new IllegalArgumentException("Illegal values for area covered by CoordinateRect."); if (coords == null) throw new IllegalArgumentException("Can't add null CoordinateRect to DisplayCanvas."); CRData c = new CRData(); c.coords = coords; c.xmin = hmin; c.xmax = hmax; c.ymin = vmin; c.ymax = vmax; c.background = background; if (coordinateRects == null) coordinateRects = new Vector(); coordinateRects.addElement(c); coords.setOwner(this); } /** * Add a newly created CoordinateRect covering the specified section of * the canvas. hmin, hmax, vmin, vmax must be in the range 0 to 1. * The index of the new CoordinateRect is returned. */ public int addNewCoordinateRect(double hmin, double hmax, double vmin, double vmax) { CoordinateRect c = new CoordinateRect(); addCoordinateRect(c,hmin,hmax,vmin,vmax,null); return coordinateRects.size() - 1; } /** * Add a newly created CoordinateRect covering the specified section of * the canvas, with the specfied background color. hmin, hmax, vmin, vmax must be in the range 0 to 1. * The index of the new CoordinateRect is returned. */ public int addNewCoordinateRect(double hmin, double hmax, double vmin, double vmax, Color background) { CoordinateRect c = new CoordinateRect(); addCoordinateRect(c,hmin, hmax, vmin, vmax,background); return coordinateRects.size() - 1; } /** * Get the first CoordinateRect in this canvas. (If none exists, one is created and * added to the canvas.) */ public CoordinateRect getCoordinateRect() { return getCoordinateRect(0); } /** * Get the i-th CoordinateRect in this DisplayCanvas. They are numbered staring from zero. * If there is no i-th rect, null is returned, except that if there * are NO coordinate rects and a request for rect 0 is received, * then a new CoordinateRect is added to fill the entire canvas. */ public CoordinateRect getCoordinateRect(int i) { if (i == 0 && (coordinateRects == null || coordinateRects.size() == 0)) addNewCoordinateRect(0,1,0,1); if (coordinateRects == null || i < 0 || i >= coordinateRects.size()) return null; else return ((CRData)coordinateRects.elementAt(i)).coords; } /** * Return CoordinateRect that contains the specified pixel, or * null if there is none. The CoordinateRects are searched in * reverse order, so that the "top" CoordinateRect at that point is * returned. Note that this method only makes sense if the canvas * has already been displayed. * (Mostly, this is for internal use in this class.) */ public CoordinateRect findCoordinateRectAt(int pixelX, int pixelY) { if (coordinateRects == null) return null; for (int i = coordinateRects.size() - 1; i >= 0; i--) { CRData c = (CRData)coordinateRects.elementAt(i); int width = getSize().width; if (width <= 0) return null; int height = getSize().height; int x = (int)(c.xmin*width); int y = (int)(c.ymin*height); int r = (int)(c.xmax*width); int b = (int)(c.ymax*height); if (pixelX >= x && pixelX < r && pixelY >= y && pixelY < b) return c.coords; } return null; } /** * Should be called whenever the contents of the canvas have changed and so * it needs to need to be redrawn. (This causes the off-screen image to be redrawn. * A simple call to repaint() does not do this.) * If only one CoordinateRect needs to be repainted, you can call doRedraw(int i) or * or doRedraw(CoordinateRect c), which can be more efficient than redrawing the whole canvas. * If an error message is displayed, it will be cleared. */ synchronized public void doRedraw() { // Should be called whenever the coordinate rects need to be repainted. // If only one needs to be repainted, you can call doRedraw(int i) or // or doRedraw(CoordinateRect c), which can be more efficient. // If an error message is displayed, this will not take effect until // the error message is cleared. OSCvalid = false; if (errorMessage != null) clearErrorMessage(); // does repaint else repaint(); } /** * To be called when the contents of one of the CordinateRects have changed and so * it needs to need to be redrawn. (This causes the off-screen image to be redrawn. * A simple call to repaint() does not do this.) * If an error message is displayed, it will be cleared. * * @param coordRectIndex The index of the CoordinateRect to be redrawn, where the first CoordinateRect is at index zero. * If there is no such CoordinateRect, then nothing is done. */ synchronized public void doRedraw(int coordRectIndex) { if (coordinateRects != null && coordRectIndex >= 0 && coordRectIndex < coordinateRects.size()) { CRData c = (CRData)coordinateRects.elementAt(coordRectIndex); OSCvalid = false; if (errorMessage != null) clearErrorMessage(); // does repaint else { int width = getSize().width; int height = getSize().height; int x = (int)(c.xmin*width); int y = (int)(c.ymin*height); int w = (int)(c.xmax*width) - x; int h = (int)(c.ymax*height) - y; repaint(x,y,w,h); } } } /** * To be called when the contents of one of the CordinateRects have changed and so * it needs to need to be redrawn. (This causes the off-screen image to be redrawn. * A simple call to repaint() does not do this.) * If an error message is displayed, it will be cleared. * * @param coords The CoordinateRect to be redrawn. If coords is not in this DisplayCanvas, nothing is done. */ synchronized public void doRedraw(CoordinateRect coords) { int size = (coordinateRects == null)? -1 : coordinateRects.size(); for (int i = 0; i < size; i++) if (((CRData)coordinateRects.elementAt(i)).coords == coords) { doRedraw(i); break; } } //------------------- InputObject/Computable interfaces --------------------- /** * This is generally called by a Controller. It calls the checkInput() method of * any InputObject displayed on this Canvas. */ synchronized public void checkInput() { if (coordinateRects != null) { int top = coordinateRects.size(); for (int i = 0; i < top; i++) ((CRData)coordinateRects.elementAt(i)).coords.checkInput(); } } /** * This is generally called by a Controller. It calls the compute() method of * any InputObject displayed on this Canvas. */ synchronized public void compute() { if (coordinateRects != null) { int top = coordinateRects.size(); for (int i = 0; i < top; i++) ((CRData)coordinateRects.elementAt(i)).coords.compute(); } } /** * Method required by InputObject interface; in this class, calls the same method * recursively on any CoordinateRects contained in this DisplayCanvas. This is meant to * be called by JCMPanel.gatherInputs(). */ public void notifyControllerOnChange(Controller c) { if (coordinateRects != null) { int top = coordinateRects.size(); for (int i = 0; i < top; i++) ((CRData)coordinateRects.elementAt(i)).coords.notifyControllerOnChange(c); } } //-------------------- Error Reporter Stuff ------------------------------------ /** * Get color that is used as a background when the canvas displays an error message. */ public Color getErrorBackground() { return errorBackground; } /** * Set color to be used as a background when the canvas displays an error message. * The default is a light green. If the specified Color value is null, nothing is done. */ public void setErrorBackground(Color c) { if (c != null) errorBackground = c; } /** * Get color that is used for the text when the canvas displays an error message. */ public Color getErrorForeground() { return errorForeground; } /** * Set color to be used for the text when the canvas displays an error message. * The default is a dark green. If the specified Color value is null, nothing is done. */ public void setErrorForeground(Color c) { if (c != null) errorForeground = c; } /** * Get the error message that is currently displayed on the canvas. If no error * is displyed, the return value is null. */ synchronized public String getErrorMessage() { return errorMessage; } /** * Set an error message to be displayed on the canvas. This method is generally called by * a Controller or a LimitControlPanel. If you call it directly, use null as the first parameter. * * @param c The Controller, if any, that is calling this routine. This controller will be notified * when the error message is cleared. If the method is not being called by a contrller, this * parameter should be set to null. * @param message The error message to be displayed. If the value is null or is a blank string, * the current message, if any, is cleared. */ synchronized public void setErrorMessage(Controller c, String message) { if (message == null || message.trim().length() == 0) { if (errorMessage != null) { clearErrorMessage(); if (errorSource != c) errorSource.errorCleared(); repaint(); } } else { errorMessage = message.trim(); errorSource = c; OSCvalid = false; repaint(); } } /** * Clear the error message, if any, that is currently displayed on the canvas. */ synchronized public void clearErrorMessage() { if (errorMessage == null) return; errorMessage = null; if (errorSource != null) errorSource.errorCleared(); errorSource = null; repaint(); } // ---------------------- ErrorReporter Implementation ---------------------------- private Color errorBackground = new Color(220,255,220); private Color errorForeground = new Color(0,120,0); private String errorMessage; private Controller errorSource; private Draggable dragged; // The draggable object, if any, that is being dragged. /** * This has been overridden to handle the mouse zoom feature. * Not meant to be called directly. */ public void processMouseEvent(MouseEvent evt) { // If an error message is displayed, get rid of it and // ignore the mouse click. if (evt.getID() == MouseEvent.MOUSE_PRESSED) { dragging = false; // Shouldn't be possible, but some old, buggy versions of Java made it so. dragged = null; if (errorMessage != null) { if (errorSource != null) errorSource.errorCleared(); errorSource = null; errorMessage = null; repaint(); evt.consume(); return; } CoordinateRect c = findCoords(evt); if (c != null) dragged = c.checkDraggables(evt); if (dragged != null) return; if (handleMouseZooms && !(evt.getClickCount() > 1 || evt.isAltDown() || evt.isMetaDown() || evt.isControlDown())) { super.processMouseEvent(evt); if (!evt.isConsumed()) doMouseZoom_pressed(evt); return; } } else if (evt.getID() == MouseEvent.MOUSE_RELEASED && handleMouseZooms && dragged != null) { dragged.finishDrag(evt); dragged = null; return; } else if (evt.getID() == MouseEvent.MOUSE_RELEASED && handleMouseZooms && dragging) { doMouseZoom_released(evt); return; } super.processMouseEvent(evt); } /** * This has been overridden to handle the mouse zoom feature. * Not meant to be called directly. */ public void processMouseMotionEvent(MouseEvent evt) { if (dragged != null && evt.getID() == MouseEvent.MOUSE_DRAGGED) dragged.continueDrag(evt); else if (dragging && evt.getID() == MouseEvent.MOUSE_DRAGGED) doMouseZoom_moved(evt); else super.processMouseMotionEvent(evt); } private void drawErrorMessage(Graphics g) { if (errorMessage == null) return; Font font = new Font("Helvetica",Font.BOLD,12); FontMetrics fm = g.getFontMetrics(font); int width = getSize().width; int height = getSize().height; int lineHeight = fm.getHeight(); int leading = fm.getLeading(); int messageWidth = width - 80; int left = 30; int maxLines = (height - 60 - lineHeight) / lineHeight; if (maxLines <= 0) maxLines = 1; StringTokenizer t = new StringTokenizer(errorMessage, " \t\r\n"); int lineCt = 0; String[] errorMessageList = new String[maxLines]; String line = " "; // indent first line while (t.hasMoreTokens()) { String word = t.nextToken(); if (fm.stringWidth(word) > messageWidth) { String w = ""; int dots = fm.stringWidth("..."); for (int c = 0; c < word.length(); c++) { w += word.charAt(c); if (fm.stringWidth(w) + dots > messageWidth) break; } word = w; } String linePlusWord = line + " " + word; if (fm.stringWidth(linePlusWord) > messageWidth) { errorMessageList[lineCt] = line; lineCt++; if (lineCt == maxLines) break; line = word; } else { line = linePlusWord; } } if (lineCt < maxLines) { errorMessageList[lineCt] = line; lineCt++; } if (lineCt == 1) errorMessageList[0] += " "; // for proper centering int boxWidth = width - 60; int boxHeight = (lineCt+1)*lineHeight + 50; int top = height/2 - boxHeight/2; if (top < 0) top = 0; g.setColor(getBackground()); g.fillRect(0,0,width,height); g.setColor(errorBackground); g.fillRect(left,top,boxWidth,boxHeight); g.setColor(errorForeground); g.drawRect(left,top,boxWidth,boxHeight); g.drawRect(left+1,top+1,boxWidth-2,boxHeight-2); g.drawLine(left,top + 23 + lineHeight, left + boxWidth - 2, top + 23 + lineHeight); g.drawLine(left,top + 24 + lineHeight, left + boxWidth - 2, top + 24 + lineHeight); g.setFont(font); g.drawString("ERROR MESSAGE",width/2 - fm.stringWidth("(Error Message)")/2, top + 10 + lineHeight); if (lineCt == 1) g.drawString(errorMessageList[0], width/2 - fm.stringWidth(errorMessageList[0])/2, top + 35 + 2*lineHeight); else { for (int i = 0; i < lineCt; i++) { g.drawString(errorMessageList[i], left + 10, top + 35 + (i+2)*lineHeight - leading); } } } // end drawErrorMessage(); // ------------ Handle Mouse Zooming ----------------------------------------- private transient boolean dragging, draggingZoomWindow; private transient CRData draggingInRect; private transient int dragXmax, dragXmin, dragYmax, dragYmin; private transient int lastX, lastY, startX, startY; private CoordinateRect findCoords(MouseEvent evt) { // Find coord rect containing the mouse. int xMouse = evt.getX(); int yMouse = evt.getY(); int size = (coordinateRects == null)? -1 : coordinateRects.size(); int width = getSize().width; int height = getSize().height; for (int i = size-1; i >= 0; i--) { CRData c = (CRData)coordinateRects.elementAt(i); double xmin = (int)(c.xmin*width); double ymin = (int)(c.ymin*height); double xmax = (int)(c.xmax*width) - 1; double ymax = (int)(c.ymax*height) - 1; if (xMouse >= xmin && xMouse <= xmax && yMouse >= ymin && yMouse <= ymax) return c.coords; } return null; } private synchronized void doMouseZoom_pressed(MouseEvent evt) { // Called from processMouseEvent, above. // Ignore multiple clicks and clicks with other than button 1 // and clicks modified with any key except shift. if (evt.getClickCount() > 1 || evt.isAltDown() || evt.isMetaDown() || evt.isControlDown()) return; int xMouse = evt.getX(); int yMouse = evt.getY(); int size = (coordinateRects == null)? -1 : coordinateRects.size(); int width = getSize().width; int height = getSize().height; for (int i = size-1; i >= 0; i--) { CRData c = (CRData)coordinateRects.elementAt(i); dragXmin = (int)(c.xmin*width); dragYmin = (int)(c.ymin*height); dragXmax = (int)(c.xmax*width) - 1; dragYmax = (int)(c.ymax*height) - 1; if (xMouse >= dragXmin && xMouse <= dragXmax && yMouse >= dragYmin && yMouse <= dragYmax) { dragging = true; draggingZoomWindow = false; draggingInRect = c; startX = xMouse; startY = yMouse; lastX = xMouse; lastY = yMouse; break; } } } private synchronized void doMouseZoom_released(MouseEvent evt) { Graphics g = getGraphics(); putDragRect(g); g.dispose(); CoordinateRect c = draggingInRect.coords; if ( (Math.abs(lastX - startX) < 4 && Math.abs(lastY - startY) < 4) || Math.abs(startX - lastX) < 2 || Math.abs(startY - lastY) < 2 ) { if (draggingZoomWindow) return; if (evt.isShiftDown()) c.zoomOutFromPixel(startX,startY); else c.zoomInOnPixel(startX,startY); } else { c.setLimits( c.pixelToX(startX), c.pixelToX(lastX), c.pixelToY(startY), c.pixelToY(lastY) ); } dragging = false; } private synchronized void doMouseZoom_moved(MouseEvent evt) { Graphics g = getGraphics(); putDragRect(g); lastX = evt.getX(); lastY = evt.getY(); putDragRect(g); g.dispose(); } private void putDragRect(Graphics g) { // (Assume dragging = true) if (lastX < dragXmin) lastX = dragXmin; if (lastX > dragXmax) lastX = dragXmax; if (lastY < dragYmin) lastY = dragYmin; if (lastY > dragYmax) lastY = dragYmax; if ( (Math.abs(startX - lastX) < 4 && Math.abs(startY - lastY) < 4) || Math.abs(startX - lastX) < 2 || Math.abs(startY - lastY) < 2 ) return; draggingZoomWindow = true; Color bc = draggingInRect.background; if (bc == null) bc = getBackground(); g.setXORMode(bc); if (bc.getRed() <= 100 && bc.getGreen() <= 100 && bc.getBlue() <= 150) g.setColor(Color.white); else g.setColor(Color.black); int x, y, w, h; if (startX < lastX) { x = startX; w = lastX - startX; } else { x = lastX; w = startX - lastX; } if (startY < lastY) { y = startY; h = lastY - startY; } else { y = lastY; h = startY - lastY; } g.drawRect(x,y,w,h); g.setPaintMode(); } //--------- More implementation details... ---------------------------------- /** * This has been overridden to return a default size of 350-by-350 pixels. * Not usually called directly. */ public Dimension getPreferredSize() { return new Dimension(350,350); } private transient Image OSC; private transient Graphics OSG; private transient boolean OSCvalid; private transient int OSCwidth = -1, OSCheight = -1; private void drawCoordinateRects(Graphics g, int width, int height, Rectangle clip) { g.setColor(getBackground()); g.fillRect(0,0,width,height); int count = (coordinateRects == null)? -1 : coordinateRects.size(); try { for (int i = 0; i < count; i++) { CRData c = (CRData)coordinateRects.elementAt(i); Rectangle bounds = new Rectangle(); bounds.x = (int)(c.xmin*width); bounds.y = (int)(c.ymin*height); bounds.width = (int)(c.xmax*width) - bounds.x; bounds.height = (int)(c.ymax*height) - bounds.y; Rectangle clipThisRect = (clip == null)? bounds : bounds.intersection(clip); if (clip == null || !clipThisRect.isEmpty()) { g.setClip(clipThisRect); if (c.background != null) { g.setColor(c.background); g.fillRect(bounds.x,bounds.y,bounds.width,bounds.height); } c.coords.draw(g,bounds.x,bounds.y,bounds.width,bounds.height); } } } finally { g.setClip(clip); } } /** * This has been overridden to implemnt double-buffering. * Not meant to be called directly. */ public void update(Graphics g) { paint(g); } /** * Draw the contents of the DisplayCanvas. * Not usually called directly. */ synchronized public void paint(Graphics g) { if (errorMessage == null) { try { checkOSC(); if (OSC != null) g.drawImage(OSC,0,0,this); else drawCoordinateRects(g,getSize().width,getSize().height,g.getClipBounds()); if (dragging) putDragRect(g); } catch (RuntimeException e) { errorMessage = "Internal Error? (stack trace on System.out): " + e.toString(); e.printStackTrace(); g.setClip(0,0,getSize().width,getSize().height); } } if (errorMessage != null) { drawErrorMessage(g); OSCvalid = false; return; } } /** * Draws the specified item in the first CoordinateRect in this canvas. * It is drawn on screen and on the off-screen canvas, if there is one. * However, no information is kept about this item, so the drawing will * disappear the next time the off-screen canvas is re-drawn (if there * is an off-screen canvas) or the next time the canvas is repainted * (is there is no off-screen canvas). If the canvas contains no * CoordinateRect when this is called, a new one is added. Note that * this method should only be called after the canvas has appeared on * the screen. */ public void drawTemp(DrawTemp drawItem) { if ( coordinateRects == null || coordinateRects.size() == 0) addCoordinateRect(new CoordinateRect()); drawTemp(drawItem,0); } /** * Draws the specified item in the specified CoordinateRect in this canvas. * It is drawn on screen and on the off-screen canvas, if there is one. * However, no information is kept about this item, so the drawing will * disappear the next time the off-screen canvas is re-drawn (if there * is an off-screen canvas) or the next time the canvas is repainted * (is there is no off-screen canvas). Note that * this method should only be called after the canvas has appeared on * the screen. * * @param drawItem The non-null object that is to be drawn * @param coordRectIndex The index of the CoordinateRect in which it * is to be drawn, where the index of the fist CoordinateRect * added to the canvas is zero, and so on. If there is * no CoordinateRect with the specified index, an IllegalArgumentException * is thrown. */ synchronized public void drawTemp(DrawTemp drawItem, int coordRectIndex) { if (coordRectIndex < 0 || coordRectIndex >= coordinateRects.size()) throw new IllegalArgumentException("Invalid CoordinateRect index, " + coordRectIndex); Graphics g = getGraphics(); if (g == null) return; CRData c = (CRData)coordinateRects.elementAt(coordRectIndex); Rectangle bounds = new Rectangle(); bounds.x = (int)(c.xmin*getSize().width); bounds.y = (int)(c.ymin*getSize().height); bounds.width = (int)(c.xmax*getSize().width) - bounds.x; bounds.height = (int)(c.ymax*getSize().height) - bounds.y; g.setClip(bounds); drawItem.draw(g,c.coords); g.dispose(); if (useOffscreenCanvas && OSCvalid && OSC != null) { g = OSC.getGraphics(); g.setClip(bounds); drawItem.draw(g,c.coords); g.dispose(); } } synchronized private void checkOSC() { // make off-screen image, if necessary if (!useOffscreenCanvas || (OSCvalid == true && OSC != null && OSCwidth == getSize().width && OSCheight == getSize().height)) return; int width = getSize().width; int height = getSize().height; if (OSC == null || width != OSCwidth || height != OSCheight) { OSCvalid = false; OSCwidth = width; OSCheight = height; try { OSC = createImage(OSCwidth,OSCheight); OSG = OSC.getGraphics(); } catch (OutOfMemoryError e) { OSC = null; OSG = null; } } if (OSC == null || OSCvalid) return; OSCvalid = true; OSG.setClip(0,0,width,height); drawCoordinateRects(OSG,width,height,null); } } // end class DisplayCanvas jcm1-source/edu/hws/jcm/draw/DrawString.java0000644000076500011320000005746411741343635020242 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.*; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; import java.util.StringTokenizer; /** * A DrawString object displays a string, possibly multi-line, in a DisplayCanvas, * inside the rectangular region of a CoordinateRect. The location of the string * can be specified in two ways. First, by giving the coordinates of a reference * point together with a constant that says how the string is positioned with * respect to that reference point. The coordintes are given as Value objects * and the values are interepreted in the coordinate system of the CoordinateRect. * The positioning object is one of the constants TOP_LEFT, TOP_CENTER, ..., * BOTTOM_RIGHT defined in this class. This says where the REFERENCE POINT * is -- at the top left of the string, at the top center, etc. *

The second way to specify the position of the string is to set the reference * point coordinates to null. In that case, the postioning constant gives * the location of the STRING in the CorrdinateRect. A value of TOP_LEFT * says that the string is in the top left corner of the rect, etc. * *

An array of Value objects can be specified to be displayed in the string. * Their values are substituted for #'s in the string. (A double # in the string, * however, is displayed as a literal single #.) * *

It is possible to set the color, font and justification of the string. * *

A DisplayString implements the Computable interface, so it can be added to * a Controller. The values of the Value objects used by the string are recomputed * only when its compute() method is called. */ public class DrawString extends Drawable implements Computable { /** * Specify string location in rect */ public static final int TOP_LEFT = 4*0 + 0, TOP_CENTER = 4*0 + 1, //value/4 gives vertical position TOP_RIGHT = 4*0 + 2, // value%4 gives horizontal position CENTER_LEFT = 4*1 + 0, CENTER_CENTER = 4*1 + 1, CENTER_RIGHT = 4*1 + 2, BOTTOM_LEFT = 4*2 + 0, BOTTOM_CENTER = 4*2 + 1, BOTTOM_RIGHT = 4*2 + 2; /** * For specifying justification of lines in multiline strings. * (But can also be used as a synonym for CENTER_CENTER to specify the position of the string). */ public static final int CENTER = CENTER_CENTER; /** * For specifying justification of lines in multiline strings. * (But can also be used as a synonym for TOP_LEFT to specify the position of the string). */ public static final int LEFT = TOP_LEFT; /** * For specifying justification of lines in multiline strings. * (But can also be used as a synonym for TOP_RIGHT to specify the position of the string). */ public static final int RIGHT = TOP_RIGHT; /** * one of the constants defined in this class for specifying position */ protected int position; /** * String, possibly with \n and #'s. This is used as a base to get the actual string that is drawn. */ protected String baseString; /** * The actual lines to draw, derived from baseString. */ protected String[] strings; /** * Values to be substituted for #'s in the baseString. */ protected Value[] values; /** * xy-coords for drawing the string. If non-null then relative positioning is used. * If null, then positioning is absolute. */ protected Value xPos,yPos; /** * Color of string. If null, black is used as the default. */ protected Color color; /** * Font for drawing string. If null, get font from graphics context. */ protected Font font; /** * If absolute positioning is used, then this gives a gap between the string and edge of rect. * For relative positioning, this gives an offset from the value of xPos yPos. */ protected int offset = 3; /** * If true, the string is clamped to lie within the CoordinateRect. */ protected boolean clamp = true; /** * Left, right, or center justification of lines in the text. */ protected int justification = LEFT; /** * Maximum number of characters desired in numbers; actual number might actually be larger. */ protected int numSize = 10; /** * If backgroundColor is non-null, then a rectangle of this color is filled * as a background for the string; */ protected Color backgroundColor; /** * If frameWidth is greater than zero, then a frame of this width is drawn around the * string in the color given by frameColor. */ protected int frameWidth; /** * If frameWidth is greate than zero, then a frame is drawn around the string in this * color. If the value is null, then the color will be the same as the color of the string. */ protected Color frameColor; // In the following, note that str can contain \n to break up the // string into multiple lines. private double xRef,yRef; //Coordinates of reference point where string is drawn. private boolean changed = true; // set to true when strings need to be recomputed /** * Create a DrawString object that initially has no string to draw. */ public DrawString() { this(null,TOP_LEFT,(Value[])null); } /** * Create a DrawString for drawing a black string in the top left corner of the coordinate rect. * * @param str The string to draw, which can contain \n's to indicate line breaks. */ public DrawString(String str) { this(str,TOP_LEFT,(Value[])null); } /** * Create a DrawString for drawing a black string in the position specified. * * @param str The string to draw, which can contain \n's to indicate line breaks. * @param pos The positioning of the string in the coordinate rect. One of the positioning constants such as TOP_LEFT or BOTTOM_RIGHT. */ public DrawString(String str, int pos) { this(str,pos,(Value[])null); } /** * Create a DrawString for drawing a black string in the specified position. * The number of #'s in the string should match values.length. The values * are computed and substituted for the #'s. * * @param str The string to draw, which can contain \n's to indicate line breaks and #'s to be replaced by numeric values. * @param pos The positioning of the string in the coordinate rect. One of the positioning constants such as TOP_LEFT or BOTTOM_RIGHT. * @param values Value objects associated with #'s in the string. */ public DrawString(String str, int pos, Value[] values) { position = pos; this.values = values; setString(str); } /** * Create a string that is displayed at the reference point (xPos,yPos); * The positioning constant, pos, gives the positioning relative to this point, if xPos or yPos is non-null. * * @param str The string to draw, which can contain \n's to indicate line breaks and #'s to be replaced by numeric values. * @param pos The positioning of the string. One of the positioning constants such as TOP_LEFT or BOTTOM_RIGHT. If xPos or yPos is non-nul, this is interpreted relative to their values. * @param xPos x-coordinate relative to which the string is drawn (or null for absolute hoizontal positioning). * @param yPos y-coordinate relative to which the string is drawn (or null for absolute vertical positioning). * @param values Value objects associated with #'s in the string. */ public DrawString(String str, int pos, Value xPos, Value yPos, Value[] values) { setReferencePoint(xPos,yPos); position = pos; this.values = values; setString(str); } /** * Set the color for the string. If c is null, Color.black is used. * */ public void setColor(Color c) { color = c; needsRedraw(); } /** * Get the non-null color that is used for drawing the string. * */ public Color getColor() { return (color == null)? Color.black : color; } /** * Set the font that is used for drawing this string. If f is null, * then the font is obtained from the Graphics context in which the * string is drawn. * */ public void setFont(Font f) { font = f; needsRedraw(); } /** * Return the font that is used for drawing the string. If the return * value is null, then the font is taken from the Graphics context. * */ public Font getFont() { return font; } /** * Set the Values that are substituted for (single) #'s in the string. * If the array of Values is null, then no substitution is done. The length of the array should match * the number of #'s, but it is not an error if they do not match. * Extra values will be ignored; extra #'s will be shown as "undefined". * */ public void setValues(Value[] v) { values = v; changed = true; needsRedraw(); } /** * Return the array of values that are substituted for #'s in the string. * */ public Value[] getValues() { return values; } /** * Set the positioning of the string. The parameter should be one of the positioning * contstants defined in this class, such as TOP_LEFT. (If it is not, * TOP_LEFT is used by default.) * */ public void setPositioning(int pos) { position = pos; needsRedraw(); } /** * Return the positioning, as set by setPositioning(). * */ public int getPositioning() { return position; } /** * Set the values of the (x,y) coordinates of the * reference point for the stirng. If a value is null, * absolute positioning is used. If a value is * undefined, the string is not drawn. * */ public void setReferencePoint(Value x, Value y) { xPos = x; yPos = y; try { if (xPos != null) xRef = xPos.getVal(); if (yPos != null) yRef = yPos.getVal(); } catch (RuntimeException e) { } needsRedraw(); } /** * Return the Value object that gives the x-coordinate of the reference * point of this string. * */ public Value getXPos() { return xPos; } /** * Return the Value object that gives the y-coordinate of the reference * point of this string.point of this string. * */ public Value getYPos() { return yPos; } /** * Set the string that is displayed. Note that it can include '\n' to * represent a line break, and it can contain #'s which will be replaced * by computed values. * */ public void setString(String str) { baseString = str; strings = null; changed = true; needsRedraw(); } /** * Get a copy of the display string (with \n's #'s, not with substitued values.) * */ public String getString() { return baseString; } /** * Set the distance of the bounding box of the string from the reference * point where it is drawn. The default value is 3. * */ public void setOffset(int b) { offset = b; needsRedraw(); } /** * Get the distance of the bounding box of the string from the reference * point where it is drawn. * */ public int getOffset() { return offset; } /** * Set the "clamp" property of the DrawString. * If set to true, the string will be clamped to lie entirely within the CoordinateRect * (unless it doens't fit -- then it can stick out on the right and bottom). * The default value is true. * */ public void setClamp(boolean clamp) { this.clamp = clamp; needsRedraw(); } /** * Returns true if the string is set to be clamped to lie within the CoordinateRect. * */ public boolean getClamp() { return clamp; } /** * Set the justification to be used if there are multiple lins in the string. * Possible value are DrawString.LEFT, DrawString.RIGHT, and DrawString.CENTER. * */ public void setJustification(int j) { if (j == RIGHT || j == CENTER) justification = j; else justification = LEFT; needsRedraw(); } /** * Get the justification that is used for a multiple-line string. The value * is one of the constants DrawString.LEFT, DrawString.RIGHT, or DrawString.CENTER */ public int getJustification() { return justification; } /** * Set the desired maximum number of characters in displayed numbers. * Actual size might be larger. Value is clamped to the range * 6 to 25. * */ public void setNumSize(int size) { numSize = Math.min(Math.max(size,6),25); changed = true; needsRedraw(); } /** * Return the desired maximum number of characters in displayed numbers. * */ public int getNumSize() { return numSize; } /** * Get the color that is used to fill a rectangle on which the string is drawn. Null * indicates that no rectangle is filled so the stuff in back of the string shows though. * The default value is null. */ public Color getBackgroundColor() { return backgroundColor; } /** * Set the color that is used to fill a rectangle on which the string is drawn. If the * value is null, no rectangle is filled and the string just overlays whatever is in back * of it on the canvas. */ public void setBackgroundColor(Color color) { backgroundColor = color; needsRedraw(); } /** * Get the color that is used to draw a frame around the string. This is only done if the * frameWidth property is greater than zero. If the value is null, the frame is the same color * as the string. */ public Color getFrameColor() { return frameColor; } /** * Set the color that is used to draw a frame around the string. This is only done if the * frameWidth property is greater than zero. If the value is null, the frame is the same color * as the string. */ public void setFrameColor(Color color) { frameColor = color; needsRedraw(); } /** * Get the width, in pixels, of the frame that is drawn around the string. * The default width is zero. The largest possible value is 25. */ public int getFrameWidth() { return frameWidth; } /** * Set the width, in pixels, of a frame to draw around the string. If the value is zero, * no frame is drawn. The default value is zero. The the value is clamped * to the range 0 to 25. */ public void setFrameWidth(int width) { if (width < 0) frameWidth = 0; else if (width > 25) frameWidth = 25; else frameWidth = width; needsRedraw(); } /** * The compute method sets up the array of strings that is actually displayed. * This is required by the Computable interface and is usually called by a * Controller rather than directly. */ public void compute() { changed = true; needsRedraw(); } private void getSubstitutedText() { //Get the strings obtained by substituting values for #'s in text. changed = false; if (xPos != null) xRef = xPos.getVal(); if (yPos != null) yRef = yPos.getVal(); if (values == null && strings != null) // no need to recompute, since there is no #-substitution to do. return; if (baseString == null || baseString.trim().length() == 0) { strings = null; return; } StringTokenizer tok = new StringTokenizer(baseString, "\n"); int count = tok.countTokens(); strings = new String[count]; if (values == null) { for (int i = 0; i < count; i++) strings[i] = tok.nextToken(); return; } StringBuffer b = new StringBuffer(); int expCt = 0; for (int strNum = 0; strNum < count; strNum++) { String text = tok.nextToken(); for (int i = 0; i < text.length(); i++) { if (text.charAt(i) == '#') { if (i != text.length() - 1 && text.charAt(i+1) == '#') { b.append('#'); i++; } else { if (expCt < values.length) { try { b.append(NumUtils.realToString(values[expCt].getVal(),numSize)); } catch (RuntimeException e) { b.append("(error)"); } expCt++; } else b.append("undefined"); } } else b.append(text.charAt(i)); } strings[strNum] = b.toString(); b.setLength(0); } } /** * Draws the string. */ public void draw(Graphics g, boolean coordsChanged) { if (changed) getSubstitutedText(); if (strings == null) return; if (xPos != null && (Double.isNaN(xRef) || Double.isInfinite(xRef))) return; if (yPos != null && (Double.isNaN(yRef) || Double.isInfinite(yRef))) return; int trueOffset = offset; // offset allowing for frame and background if (backgroundColor != null || frameWidth > 0) trueOffset += 3; trueOffset += frameWidth; Font saveFont = null; FontMetrics fm; if (font != null) { saveFont = g.getFont(); g.setFont(font); fm = g.getFontMetrics(font); } else fm = g.getFontMetrics(g.getFont()); int lineHeight = fm.getHeight(); int xmin = coords.getLeft(); int width = coords.getWidth(); int ymin = coords.getTop(); int height = coords.getHeight(); int xmax = xmin+width; int ymax = ymin+height; int stringWidth = 0; for (int i = 0; i < strings.length; i++) stringWidth = Math.max(stringWidth,fm.stringWidth(strings[i])); int stringHeight = strings.length*lineHeight; if (backgroundColor == null && frameWidth <= 0) stringHeight = stringHeight - fm.getLeading() - fm.getDescent(); int xInt=0,yInt=0; int hPos = position % 4; int vPos = position / 4; if (position < 0 || hPos > 2 || vPos > 2) { // Use TOP_LEFT as default, if position is not a legal value. hPos = 0; vPos = 0; } if (xPos == null) { if (hPos == 0) xInt = xmin + trueOffset; else if (hPos == 1) xInt = (xmin + xmax - stringWidth) / 2; else xInt = xmax - stringWidth - trueOffset; } else { if (hPos == 0) xInt = coords.xToPixel(xRef) + trueOffset; else if (hPos == 1) xInt = coords.xToPixel(xRef) - stringWidth / 2; else xInt = coords.xToPixel(xRef) - stringWidth - trueOffset; } if (yPos == null) { if (vPos == 0) yInt = ymin + trueOffset; else if (vPos == 1) yInt = (ymin + ymax - stringHeight) / 2; else yInt = ymax - stringHeight - trueOffset; } else { if (vPos == 0) yInt = coords.yToPixel(yRef) + trueOffset; else if (vPos == 1) yInt = coords.yToPixel(yRef) - stringHeight / 2; else yInt = coords.yToPixel(yRef) - stringHeight - trueOffset; } if (clamp) { if (xInt + stringWidth > xmax) xInt = xmax - stringWidth; if (xInt < xmin) xInt = xmin; if (yInt + stringHeight > ymax) yInt = ymax - stringHeight; if (yInt < ymin) yInt = ymin; } if (backgroundColor != null) { g.setColor(backgroundColor); g.fillRect(xInt-3, yInt-3, stringWidth+6, stringHeight+6); } if (frameWidth > 0) { if (frameColor != null) g.setColor(frameColor); else if (color != null) g.setColor(color); else g.setColor(Color.black); for (int k = 1; k <= frameWidth; k++) g.drawRect(xInt-3-k,yInt-3-k,stringWidth+5+2*k,stringHeight+5+2*k); } if (color != null) g.setColor(color); else g.setColor(Color.black); yInt += fm.getAscent(); for (int i = 0; i < strings.length; i++) { int x = xInt; if (justification == CENTER) x = x + (stringWidth - fm.stringWidth(strings[i]))/2; else if (justification == RIGHT) x = x + stringWidth - fm.stringWidth(strings[i]); g.drawString(strings[i],x,yInt); yInt += lineHeight; } if (saveFont != null) g.setFont(saveFont); } } // end class DrawString jcm1-source/edu/hws/jcm/draw/Crosshair.java0000644000076500011320000000637711741343635020110 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.data.*; /** * A Crosshair is a small cross, 15 pixels wide and high, that is drawn in * a CoordinateRect at a specified point. * A Crosshair is a Computable object, so should be added to a Controller to be * recomputed when the coordinates of the point change. */ public class Crosshair extends DrawGeometric { /** * Create a cross that appears at the point with coordinates (x,y). */ public Crosshair(Value x, Value y) { super(CROSS, x, y, 7, 7); } /** * Create a cross that appears on the graph of the function y=f(x) * at the point with coordinates (x,f(x)). f should be a function * of one variable. */ public Crosshair(Value x, Function f) { super(CROSS, x, new ValueMath(f,x), 7, 7); } } jcm1-source/edu/hws/jcm/draw/CoordinateRect.java0000644000076500011320000007146311741343635021056 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.awt.*; import edu.hws.jcm.data.Value; import java.awt.*; import java.util.Vector; /** * A CoordinateRect represents a rectagular region in the xy-plane, specified * by values xmin,xmax,ymin,ymax. The conditions ymin < ymax and xmin < xmax * are enforced. (Values are swapped if necessary, and if min==max, they are * reset to -1 and +1. If any of the values are set to an infinite or NaN * value, then the coordinate rect won't display anything except the message * "Error: undefined limits".) *

When the Rect is mapped onto the screen, there can be a gap of a specified * number of pixels between the min,max values and the edges of the rectangle * on the screen. If the gap is non-zero, then the actual range of coordinates * on the rect is larger than the range from the specifed min to max. (This is * done mainly so I could have axes that don't quite reach the edges of the rect.) *

A CoordinateRect maintains a list of Drawable items. When the Rect's * draw() method is called, it calls the draw() method of each of the Drawable * items it contains. When its compute() method is called, it calls the * compute() method of any Drawable that is a Computable. When its checkInput() * method is called, it calls the checkInput() method of any Drawable that is * an InputObject. *

A CoordinateRect represents a rectangular region in a DisplayCanvas. * It has a reference to that Canvas, which is set automatically when it is * added to the canvas. If the size, range, or gap on the CoordinateRect * change, it will ask the Canvas to redraw the area it occupies. * *

The values of xmin, xmax, ymin, ymax are exported as Value objects, * which can be used elsewhere in your program. The Value objects can * be obtained by calling getValueObject(). If you do this, you should * add the objects that depend on those values to a Controller and * register the Controller to listen for changes from this CoordinateRect * by calling the CoordinateRect.setOnChange(Controller) method. */ public class CoordinateRect implements Tieable, Limits, Computable, InputObject { private double xmin,xmax,ymin,ymax; // Range of x and y values on the Rect (not counting the gap). private int gap = 5; //Extra pixels around the edges, outside the specifed range of x,y values. //Note: xmin,xmax,ymin,ymax are the limits on a rectangle that //is inset from the drawing rect by gap pixels on each edge. /** * Drawable items contained in this CoordinateRect */ protected Vector drawItems = new Vector(); /** * Set to true when one of the limits or the gap has changed. */ protected boolean changed; private long serialNumber; // This value is increased whenever xmin,xmax,ymin,ymax,gap change // or when the size of the rectangle in pixels changes. /** * This contains other Limit objects with which the CoordinateRect is * synchronizing. This is ordinarily managed by a LimitControlPanel, * so you don't have to worry about it. (However, you can also sync * several CoordinateRects even in the absense of a LimitControlPanel. * To do so, create the Tie that ties the CoordinateRect and pass it to * the setSyncWith() method of each CoordinateRect. It is NOT necessary * to add the Tie to a Controller. Synchronization is handled by the * CoordinateRects themselves. */ protected Tie syncWith; /** * Create a CoordinateRect with default limits: -5, 5, -5, 5. */ public CoordinateRect() { this(-5,5,-5,5); } /** * Create a CoordinateRect with specified limits. */ public CoordinateRect(double xmin, double xmax, double ymin, double ymax) { setLimits(xmin,xmax,ymin,ymax); serialNumber = 0; setRestoreBuffer(); // Restore buffer holds original limits, util it is reset } //--------- Methods for getting and setting xmin, xmax, ymin, ymax, and gap.------ /** * Get the mimimum x-coordinate. */ public double getXmin() { return xmin; } /** * Get the maximum x-coordinate. */ public double getXmax() { return xmax; } /** * Get the mimimum y-coordinate. */ public double getYmin() { return ymin; } /** * Get the maximum x-coordinate. */ public double getYmax() { return ymax; } /** * Get the gap, in pixels, between the edges of * the CoordinateRect and the limits specified by xmin, xmax, ymin, and ymax. */ public int getGap() { return gap; } /** * Set the gap. This is ignored if g is less than zero. This gap is the number of pixels * between the edges of the CoordinateRect and the limits specified by xmin, xmax, ymin, and ymax. * The default value is 5. * */ public void setGap(int g) { if (g >= 0 && gap!= g) { int oldgap = gap; gap = g; changed = true; serialNumber++; needsRedraw(); } } /** * Get an array containing the limits on the CoordinateRect in the order xmin, xmax, ymin, ymax. */ public double[] getLimits() { return new double[] { xmin, xmax, ymin, ymax }; } /** * Set the limits on the CoordinteRect * * @param xmin the minimum x-coordinate on the CoordinateRect * @param xmax the maximum x-coordinate on the CoordinateRect * @param ymin the minimum y-coordinate on the CoordinateRect * @param ymax the maximum y-coordinate on the CoordinateRect */ public void setLimits(double xmin, double xmax, double ymin, double ymax) { double[] oldLimits = getLimits(); this.xmin = xmin; this.xmax = xmax; this.ymin = ymin; this.ymax = ymax; checkLimits(); double[] newLimits = getLimits(); if (oldLimits[0] == newLimits[0] && oldLimits[1] == newLimits[1] && oldLimits[2] == newLimits[2] && oldLimits[3] == newLimits[3]) return; changed = true; serialNumber++; if (syncWith != null) syncWith.check(); if (onChange != null) onChange.compute(); needsRedraw(); } /** * Set the coordinate limits from array; extra elements in array are ignored. * This is ignored if the array is null or has fewer than 4 members. * The order of values in the array is xmin, xmax, ymin, ymax. * */ public void setLimits(double[] d) { if (d != null && d.length >= 4) setLimits(d[0],d[1],d[2],d[3]); } /** * Specify a controller to be notified when the limits on this * CoordinateRect change. */ public void setOnChange(Controller c) { onChange = c; } /** * Get the controller that is notified when the limits on this * CoordinateRect change. This can be null. */ public Controller getOnChange() { return onChange; } /** * Get a Value object representing one of the limits on this CoordinateRect. * The parameter should be one of the constants CoordinateRect.XMIN, * CoordinateRect.XMAX, CoordinateRect.YMIN, or CoordinateRect.YMAX. * (If not, it is treated the same as YMAX). * */ public Value getValueObject(final int which) { return new Value() { public double getVal() { switch (which) { case XMIN: return getXmin(); case XMAX: return getXmax(); case YMIN: return getYmin(); default: return getYmax(); } } }; } /** * Return the serial number of the CoordinateRect, which is incremented each time the limits change. * Part of the Tieable interface. * Not meant to be called directly. */ public long getSerialNumber() { return serialNumber; } /** * Set the Tie object that is used to synchronize this CoordinareRect with other objects. * This is ordinarily called by a LimitControlPanel, so you don't have to worry about it. */ public void setSyncWith(Tie tie) { syncWith = tie; } /** * Part of the Tieable interface. * Not meant to be called directly. */ public void sync(Tie tie, Tieable newest) { if (newest != this) { if ( !(newest instanceof Limits) ) throw new IllegalArgumentException("Internal programming error: A CoordinateRect can only be tied to a Limits object."); double[] d = ((Limits)newest).getLimits(); if (d != null && d.length >= 4) { double[] oldLimits = getLimits(); if (d[0] == oldLimits[0] && d[1] == oldLimits[1] && d[2] == oldLimits[2] && d[3] == oldLimits[3]) return; xmin = d[0]; xmax = d[1]; ymin = d[2]; ymax = d[3]; checkLimits(); serialNumber = newest.getSerialNumber(); changed = true; if (onChange != null) onChange.compute(); needsRedraw(); } } } private void checkLimits() { //Make sure limits satisfy constraints. if (xmin == xmax) { xmin -= 1; xmax += 1; } else if (xmin > xmax) { double temp = xmin; xmin = xmax; xmax = temp; } if (ymin == ymax) { ymin -= 1; ymax += 1; } if (ymin > ymax) { double temp = ymin; ymin = ymax; ymax = temp; } } // -------------- Value objects corresponding to xmin, xmax, ymin, ymax ------------- /** * A constant for use with the getValueObject() method to specify which Value is to be returned. * XMIN specifies that the Value is the minimum x-coordinate on the CoordinateRect. */ public final static int XMIN = 0; /** * A constant for use with the getValueObject() method to specify which Value is to be returned. * XMAX specifies that the Value is the maximum x-coordinate on the CoordinateRect. */ public final static int XMAX = 1; /** * A constant for use with the getValueObject() method to specify which Value is to be returned. * YMIN specifies that the Value is the minimum y-coordinate on the CoordinateRect. */ public final static int YMIN = 2; /** * A constant for use with the getValueObject() method to specify which Value is to be returned. * YMAX specifies that the Value is the maximum y-coordinate on the CoordinateRect. */ public final static int YMAX = 3; /** * If non-null, this is the Controller that is notified when the limits change. */ protected Controller onChange; // ---------------------- Methods for working with Pixels ---------------------- // Note: This stuff is only valid if the CoordinateRect is // displayed in a Graphics context. I.E., after a call to draw(); // It is meant to be used by Drawables when their draw() methods are called. private int left, top, width = -1, height = -1; // Not setable; these are valid only during drawing and are meant to be used // by the Drawables in this Coorfdinate Rect. /** * Get the left edge of this CoordinateRect in the DisplayCanvas that contains it. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) */ public int getLeft() { return left; } /** * Get the width in pixels of this CoordinateRect in the DisplayCanvas that contains it. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) */ public int getWidth() { return width; } /** * Get the top edge of this CoordinateRect in the DisplayCanvas that contains it. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) */ public int getTop() { return top; } /** * Get the height in pixels of this CoordinateRect in the DisplayCanvas that contains it. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) */ public int getHeight() { return height; } /** * Return the width of one pixel in this coordinate system. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) * */ public double getPixelWidth() { return (xmax - xmin)/(width-2*gap-1); } /** * Return the height of one pixel in this coordinate system. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) * */ public double getPixelHeight() { return (ymax - ymin)/(height-2*gap-1); } /** * Convert an x-coodinate into a horizontal pixel coordinate. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) * */ public int xToPixel(double x) { int xInt = left + gap + (int)((x - xmin)/(xmax - xmin) * (width-2*gap-1)); if (xInt < -32000) return -32000; else if (xInt > 32000) return 32000; else return xInt; } /** * Convert a y-coodinate into a vertical pixel coordinate. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) * */ public int yToPixel(double y) { int yInt = top + gap + (int)((ymax - y)/(ymax - ymin) * (height-2*gap-1)); if (yInt < -32000) return -32000; else if (yInt > 32000) return 32000; else return yInt; } /** * Convert a horizontal pixel coordinate into an x-coordinate. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) * */ public double pixelToX(int h) { return xmin + ((h-left-gap)*(xmax-xmin)) / (width-2*gap-1); } /** * Convert a vertical pixel coordinate into a y-coordinate. * (This is only valid when the CoordinateRect has actually been displayed. It is meant * mainly to be used by Drawables in this CoordinateRect.) * */ public double pixelToY(int y) { return ymax - ((y-top-gap)*(ymax-ymin)) / (height-2*gap-1); } // ---------------------- Save/Restore limits ------------------------- private double restore_xmin = Double.NaN, restore_xmax, restore_ymin, restore_ymax; /** * A CoordinateRect can store its current limits in a buffer. These limits * can be restored by a call to this method. Only one level of * save/restore is provided. If limits have not been saved, then nothing happens. * The original limits on the CoordinateRect are saves automatically when * the CoordinateRect is first created. * * @return an array containing new limits. */ public double[] restore() { if (Double.isNaN(restore_xmin)) return null; setLimits(restore_xmin,restore_xmax,restore_ymin,restore_ymax); return getLimits(); } /** * A CoordinateRect can store its current limits in a buffer. This method * clears that buffer. */ public void clearRestoreBuffer() { restore_xmin = Double.NaN; } /** * Save current limits in buffer. They can be restored later by a call * to the restore() method. Only one level of * save/restore is provided. */ public void setRestoreBuffer() { if (badData()) return; checkLimits(); restore_xmin = xmin; restore_xmax = xmax; restore_ymin = ymin; restore_ymax = ymax; } /** * Used to test if any of the limit data are infinite or NaN. */ private boolean badData() { return Double.isNaN(xmin) || Double.isInfinite(xmin) || Double.isNaN(ymin) || Double.isInfinite(ymin) || Double.isNaN(xmax) || Double.isInfinite(xmax) || Double.isNaN(ymax) || Double.isInfinite(ymax); } // ----------- Zoom in and out --------------- /** * Change limits to zoom in by a factor of 2. A maximal zoom is enforced. * The center of the rectangle does not move. * * @return an array of the new limits, or null if limits don't change. */ public double[] zoomIn() { if (badData()) return getLimits(); double halfwidth = (xmax - xmin)/4.0; double halfheight = (ymax - ymin)/4.0; double centerx = (xmin + xmax)/2.0; double centery = (ymin + ymax)/2.0; if (Math.abs(halfheight) < 1e-100 || Math.abs(halfwidth) < 1e-100) return null; setLimits(centerx - halfwidth, centerx + halfwidth, centery - halfheight, centery + halfheight); return getLimits(); } /** * Change limits to zoom out by a factor of 2. A maximal zoom is enforced. * The center of the rectangle does not move. * * @return an array of the new limits, or null if limits don't change. */ public double[] zoomOut() { if (badData()) return getLimits(); double halfwidth = (xmax - xmin); double halfheight = (ymax - ymin); double centerx = (xmin + xmax)/2.0; double centery = (ymin + ymax)/2.0; if (Math.abs(halfwidth) > 1e100 || Math.abs(halfheight) > 1e100) return null; setLimits(centerx - halfwidth, centerx + halfwidth, centery - halfheight, centery + halfheight); return getLimits(); } /** * Change limits to zoom in by a factor of 2, centered on a specified point. A maximal zoom is enforced. * The point does not move. Only valid when CoordinateRect is * displayed in a rectangle on the screen. * * @param x the horizontal pixel coordinate of the center point of the zoom * @param y the vertical pixel coordinate of the center point of the zoom * * @return an array of the new limits, or null if limits don't change. */ public double[] zoomInOnPixel(int x, int y) { if (badData()) return getLimits(); double halfwidth = (xmax - xmin)/4.0; double halfheight = (ymax - ymin)/4.0; if (Math.abs(halfheight) < 1e-100 || Math.abs(halfwidth) < 1e-100) return null; double xclick = pixelToX(x); double yclick = pixelToY(y); double centerx = (xmin+xmax)/2; double centery = (ymin+ymax)/2; double newCenterx = (centerx+xclick)/2; double newCentery = (centery+yclick)/2; setLimits(newCenterx - halfwidth, newCenterx + halfwidth, newCentery - halfheight, newCentery + halfheight); return getLimits(); } /** * Change limits to zoom out by a factor of 2, centered on a specified point. A maximal zoom is enforced. * The point (x,y) does not move. Valid only if CoordinateRect has been drawn. * * @param x the horizontal pixel coordinate of the center point of the zoom * @param y the vertical pixel coordinate of the center point of the zoom * * @return an array of the new limits, or null if limits don't change. */ public double[] zoomOutFromPixel(int x, int y) { if (badData()) return getLimits(); double halfwidth = (xmax - xmin); double halfheight = (ymax - ymin); if (Math.abs(halfwidth) > 1e100 || Math.abs(halfheight) > 1e100) return null; double xclick = pixelToX(x); double yclick = pixelToY(y); double centerx = (xmin+xmax)/2; double centery = (ymin+ymax)/2; double newCenterx = 2*centerx - xclick; double newCentery = 2*centery - yclick; setLimits(newCenterx - halfwidth, newCenterx + halfwidth, newCentery - halfheight, newCentery + halfheight); return getLimits(); } /** * Reset limits, if necessary, so scales on the axes are the same. * Only valid of the CoordinateRect has been drawn. * * @return an array with the new limits, or null if limits don't change. */ public double[] equalizeAxes() { if (badData()) return getLimits(); double w = xmax - xmin; double h = ymax - ymin; double pixelWidth = w / (width - 2*gap - 1); double pixelHeight = h / (height - 2*gap - 1); double newXmin, newXmax, newYmin, newYmax; if (pixelWidth < pixelHeight) { double centerx = (xmax + xmin) / 2; double halfwidth = w/2 * pixelHeight/pixelWidth; newXmax = centerx + halfwidth; newXmin = centerx - halfwidth; newYmin = ymin; newYmax = ymax; } else if (pixelWidth > pixelHeight) { double centery = (ymax + ymin) / 2; double halfheight = h/2 * pixelWidth/pixelHeight; newYmax = centery + halfheight; newYmin = centery - halfheight; newXmin = xmin; newXmax = xmax; } else return null; setLimits(newXmin, newXmax, newYmin, newYmax); return getLimits(); } // ------------------------------ Drawing ---------------------------- private DisplayCanvas canvas; // The canvas in which this CoordinateRect is displayed. This is set // automatically when the CoordinateRect is added to or removed from // a DisplayCanvas, and it should not be changed. /** * This is meant to be called only by the DisplayCanvas class, * when this CoordinateRect is added to ta DisplayCanvas. * */ void setOwner(DisplayCanvas canvas) { this.canvas = canvas; } private void needsRedraw() { //Notifies the canvas that the area occupied by this CoodinateRect if (canvas != null) //needs to be redrawn. canvas.doRedraw(this); } /** * When this is called, the CoordinateRect will call the * checkInput method of any Drawable it contains that is * also an InputObject. This is ordinarly only called by a DisplayCanvas. */ public void checkInput() { int ct = drawItems.size(); for (int i = 0; i < ct; i++) if (drawItems.elementAt(i) instanceof InputObject) ((InputObject)drawItems.elementAt(i)).checkInput(); } /** * When this is called, the CoordinateRect will call the compute method * of any Drawable it contains that is also a Computable. * This is ordinarly only called by a DisplayCanvas. */ public void compute() { int ct = drawItems.size(); for (int i = 0; i < ct; i++) if (drawItems.elementAt(i) instanceof Computable) ((Computable)drawItems.elementAt(i)).compute(); } /** * Method required by InputObject interface; in this class, it calls the same method * recursively on any input objects containted in this CoordinateRect. This is meant to * be called by JCMPanel.gatherInputs(). */ public void notifyControllerOnChange(Controller c) { int ct = drawItems.size(); for (int i = 0; i < ct; i++) if (drawItems.elementAt(i) instanceof InputObject) ((InputObject)drawItems.elementAt(i)).notifyControllerOnChange(c); } /** * Add a drawable item to the CoordinateRect. * */ synchronized public void add(Drawable d) { if (d != null && !drawItems.contains(d)) { d.setOwnerData(canvas,this); drawItems.addElement(d); } } /** * Remove the given Drawable item, if present in this CoordinateRect. * */ synchronized public void remove(Drawable d) { if (d != null && drawItems.removeElement(d)) d.setOwnerData(null,null); } /** * Returns the number of Drawable items that are in this CoordinateRect. */ public int getDrawableCount() { return (drawItems == null)? 0 : drawItems.size(); } /** * Get the i-th Drawable in this Rect, or null if i is less than zero * or greater than or equal to the number of items. * * @param i The number of the item to be returned, where the first item is number zero. */ public Drawable getDrawable(int i) { if (drawItems != null && i >= 0 && i < drawItems.size()) return (Drawable)drawItems.elementAt(i); else return null; } /** * Check whether a mouse click (as specified in the MouseEvent parameter) is * a click on a Draggable item that wants to be dragged. If so, return that item. If not, return null. * This is meant to be called only by DisplayCanvas. */ Draggable checkDraggables(java.awt.event.MouseEvent evt) { int top = drawItems.size(); for (int i = top-1; i >= 0; i--) if ( drawItems.elementAt(i) instanceof Draggable ) { if ( ((Draggable)drawItems.elementAt(i)).startDrag(evt) ) return (Draggable)drawItems.elementAt(i); } return null; } /** * Draw in rect with upperleft corner (0,0) and specified width,height. * This is not ordinarily called directly. * */ public void draw(Graphics g, int width, int height) { draw(g,0,0,width,height); } /** * Draw in specified rect. This is not ordinarily called directly. */ synchronized public void draw(Graphics g, int left, int top, int width, int height) { if (badData()) { g.setColor(Color.red); g.drawRect(left,top,width-1,height-1); g.drawString("(undefined limits)",left+6,top+15); } if (changed || this.left != left || this.top != top || this.width != width || this.height != height) { this.width = width; this.height = height; this.left = left; this.top = top; checkLimits(); changed = true; } doDraw(g); changed = false; } /** * Draw all the Drawable items. This is called by the draw() method and is not * meant to be called directly. However, it might be overridden in a subclass. * */ protected void doDraw(Graphics g) { int ct = drawItems.size(); for (int i = 0; i < ct; i++) { Drawable d = (Drawable)drawItems.elementAt(i); if (d.getVisible()) d.draw(g,changed); } } } // end class CoordinateRect jcm1-source/edu/hws/jcm/draw/DrawBorder.java0000644000076500011320000001126011741343635020171 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.Graphics; import java.awt.Color; /** * A DrawBorder object is just a simple border around the edges of its CoordinateRect, with * a specified width, in pixels, and a specified color. */ public class DrawBorder extends Drawable { /** * A non-null Color, giving the color of the bortder. */ protected Color color; /** * A non-negative integer giving the width of the border in pixels. */ protected int width; /** * Create a black border that is one pixel thick. */ public DrawBorder() { this(Color.black,1); } /** * Create a border with the spcified color and width. If the color is null, * black is used. If the width is less than zero, a width of 1 is used. * A border of width zero is invisible. * */ public DrawBorder(Color color, int width) { this.color = ((color == null)? Color.black : color); this.width = ((width >= 0)? width : 1); } /** * Get the color of the border. * */ public Color getColor() { return color; } /** * Set the color of the border to the specified color. If the color is null, nothing is done. * */ public void setColor(Color c) { if (c != null && !c.equals(color)) { color = c; needsRedraw(); } } /** * Get the width of the border, in pixels. * */ public int getWidth() { return width; } /** * Set the width of the border to be w pixels. If w is negative, * this is ignored. A border of witdth 0 is invisible. * * @param w the desired width for the border. */ public void setWidth(int w) { if (w >= 0 && width != width) { width = w; } } /** * Draw the border in the given graphics context. This is not ordinarily called directly. * */ public void draw(Graphics g, boolean changed) { if (coords == null || width == 0) return; g.setColor(color); for (int i = 0; i < width; i++) g.drawRect(coords.getLeft() + i, coords.getTop() + i, coords.getWidth() - 2*i - 1, coords.getHeight() - 2*i - 1); } } // end class DrawBorder jcm1-source/edu/hws/jcm/draw/LimitControlPanel.java0000644000076500011320000007142611741343635021547 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.*; import java.util.Vector; import java.awt.event.*; import edu.hws.jcm.awt.*; import edu.hws.jcm.data.*; /** * A LimitControlPanel has four input boxes for specifying the xmin, xmax, ymin, and ymax * of a CoodinateRect. You can actually add more than one CoordinteRect to the LimitControlPanel. * This will synchronize the coordinate systems on the all the CoordinateRects that is controlls. * *

A LimitControlPanel can also contain a number of standard buttons, such as buttons for * zooming the coordinates in and out. The buttons are specfied using constants * defined in this class. It is possible to obtain standard buttons so that they can * be displayed outside the LimitControlPanel. Furthermore, it is also possible to add other components * to the panel, using the addRange(), addComponent(), and addComponentPair() methods. * (The standard add() method from the Component class is overridded to call * addComponent().) Any VariableInput added to the LimitControl Panel will appear with its name * as a label, just above the input box. * *

Ordinarily, all the components are just stacked up vertically. However, if * you set the useTwoColumnsIfPossible property to true, then they will be in two columns, unless * the width of the Panel is too small for two columns. Pairs of items added * with addRange() or addComponentPair() will appear on the same row. An item * added with addComponent() will appear on a row by itself. As for the standard * buttons, the following pairs will appear together, IF they are added at the * same time: SET_LIMITS and EQUALUIZE; ZOOM_IN and ZOOM_OUT; SAVE and RESTORE. * *

A LimitControlPanel can have an error reporter, which is used to report * any errors that are found in the input boxes for xmin, xmax, ymin, ymax * (or other VariableInputs added with addRange()). Except for these * input boxes, other coponents are NOT checked for errors. */ public class LimitControlPanel extends Panel implements InputObject, Tieable, Limits, ActionListener { /** * A constant that can be used in the addButton() method to add a button to the LimitControlPanel. * This represents a button that will set the limits using the values in the input boxes. * (This is also done when the user presses return in one of the boxes.) */ public final static int SET_LIMITS = 1; /** * A constant that can be used in the addButton() method to add a button to the LimitControlPanel. * This represents a button that will equalize the scales on the axes (of the first * CoordinateRect that was added to this panel). */ public final static int EQUALIZE = 2; /** * A constant that can be used in the addButton() method to add a button to the LimitControlPanel. * This represents a button that will zoom in on the center of the coordinate rect. */ public final static int ZOOM_IN = 4; /** * A constant that can be used in the addButton() method to add a button to the LimitControlPanel. * This represents a button that will zoom out from the center of the coordinate rect. */ public final static int ZOOM_OUT = 8; /** * A constant that can be used in the addButton() method to add a button to the LimitControlPanel. * This represents a button that will save the current limits, so they can be * restored later with the restore button. */ public final static int SAVE = 16; /** * A constant that can be used in the addButton() method to add a button to the LimitControlPanel. * This represents a button that will restore previously saved coordinates. * The coords are those that were saved with the save button, * or if none were saved in that way, then the original * coordinates that the CoordinateRect had when it was created. */ public final static int RESTORE = 32; /** * A constant that can be used in the addButton() method to add all possible buttons to the LimitControlPanel. */ public final static int ALL_BUTTONS = 0x3F; private final static String[] buttonNames = { "Set Limits", "Equalize Axes", "Zoom In", "Zoom Out", "Save Limits", "Restore Limits" }; /** * Set of installed buttons. */ protected int buttons; /** * Use two columns for display, if possible. */ protected boolean twoColumn; /** * The input boxes for the x- and y-value ranges. */ protected VariableInput xmin, xmax, ymin, ymax; /** * This is increased when the user changes the limits. * (The -1 will make this LimitControlPanel get its limits * from the first CoordinateRect that is added to it.) * This variable is used to implement syncronization of limits * with the limits on CoordinateRects. */ protected long serialNumber = -1; /** * A Tie holding this panel and the CoordinateRects that it controls. */ protected Tie syncWith; /** * For reporting errors in user input. */ protected ErrorReporter errorReporter; /** * The first CoordinateRect tied to this LimitControlPanel. */ protected CoordinateRect coords; /** * Vector of components and component pairs that have * been added to this panel, including at least the xmin, xmax, ymin, ymax * input boxes. */ protected Vector items = new Vector(); /** * Create a LimitControlPanel containing input boxes labeled * "xmin", "xmax", "ymin", "ymax" and a SET_LIMITS button. The * components are shown in a single column. */ public LimitControlPanel() { this("xmin","xmax","ymin","ymax",SET_LIMITS, false); } /** * Create a LimitControlPanel containing input boxes labeled * "xmin", "xmax", "ymin", "ymax" and whatever buttons are in the * set specified by the first parameter. * * @param buttonsToAdd The set of buttons to be added to the panel. Can consist of one or * more of the constants SET_LIMITS, EQUALIZE, ZOOM_IN, ZOOM_OUT, SAVE, and RESTORE, * or'ed together. * @param useTwoColumnsIfPossible If this is true, then the components in the panel will * be arranged into two columns instead of one (assuming that there is room). */ public LimitControlPanel(int buttonsToAdd, boolean useTwoColumnsIfPossible) { this("xmin","xmax","ymin","ymax",buttonsToAdd,useTwoColumnsIfPossible); } /** * Create a LimitControlPanel containing input boxes labeled * with the given names and containing whatever buttons are in the * set buttonsToAdd. buttonsToAdd should be formed by or-ing together * constants such as SET_LIMITS from this class. The last parameter * specifies whether to show the components in two columns. * @param xminName Name to be used as a label for the xmin input box. * @param xmaxName Name to be used as a label for the xmax input box. * @param yminName Name to be used as a label for the ymin input box. * @param xmaxName Name to be used as a label for the ymax input box. * * @param buttonsToAdd The set of buttons to be added to the panel. Can consist of one or * more of the constants SET_LIMITS, EQUALIZE, ZOOM_IN, ZOOM_OUT, SAVE, and RESTORE, * or'ed together. * @param useTwoColumnsIfPossible If this is true, then the components in the panel will * be arranged into two columns instead of one (assuming that there is room). */ public LimitControlPanel(String xminName, String xmaxName, String yminName, String ymaxName, int buttonsToAdd, boolean useTwoColumnsIfPossible) { setLayout(null); enableEvents(AWTEvent.COMPONENT_EVENT_MASK); xmin = new VariableInput(xminName,"-5"); xmax = new VariableInput(xmaxName,"5"); addRange(xmin,xmax); ymin = new VariableInput(yminName,"-5"); ymax = new VariableInput(ymaxName,"5"); addRange(ymin,ymax); addButtons(buttonsToAdd); twoColumn = useTwoColumnsIfPossible; } /** * Add a CoordinateRect to be controlled by this LimitControlPanel. When the user changes * the limits in this LimitControlPanel, the limits are also changed on the CoordinateRect * to match. If the limits on the CoordinateRect change for some other reason, then * the limits in the panel are changed to match. If multiple CoordinateRects are added, * the limits on all the CoordinateRects will be synchronized with each other and with * the limits in the panel. */ public void addCoords(CoordinateRect coords) { if (syncWith == null) syncWith = new Tie(this); syncWith.add(coords); coords.setSyncWith(syncWith); if (this.coords == null) this.coords = coords; } /** * Add the first CoordinateRect from the canvas to be controlled * by this LimitControlPanel. (Just calls addCoords(canvas.getCoordinateRect()).) */ public void addCoords(DisplayCanvas canvas) { addCoords(canvas.getCoordinateRect()); } /** * Set the ErrorReporter that is used to report errors in the * user's input. Note that only the input boxes for * xmin, xmax, ymin, and ymax and any VariableInputs * added with the addRange() method are checked. * */ public void setErrorReporter(ErrorReporter rep) { errorReporter = rep; } /** * Get the ErrorReporter that is used to report errors in the * user's input. Note that only the input boxes for * xmin, xmax, ymin, and ymax and any VariableInputs * added with the addRange() method are checked. * */ public ErrorReporter getErrorReporter() { return errorReporter; } /** * Set to true if you want the components to be shown in * two columns (provided the panel is wide enough). * */ public void setUseTwoColumnsIfPossible(boolean two) { twoColumn = two; } /** * Get the value of the "useTwoColumnsIfPossible" property of this panel. */ public boolean getUseTwoColumnsIfPossible() { return twoColumn; } /** * Add a component to the panel. If two-column format is used, it will * be shown on a line by itself. Note that the component shouldn't be too * wide, or it will make the Panel stretch. This component * is NOT checked for input errors. If it is an input object * or a computable, it should be added to a Controller. For an * input object, some Controller should be set up to be notified when * the value changes. (You have to do this by hand, even if you use JCMPanels!!) */ public void addComponent(Component c) { super.add(c); items.addElement(c); } /** * Add two components to the panel. If two-column format is used, they will * be shown on the same row. Note that the components shouldn't be too * wide, or they will make the Panel stretch. These components * are NOT checked for input errors. If they are input objects * or computables, they should be added to another Controller. For an * input object, some Controller should be set up to be notified when * the value changes. (You have to do this by hand, even if you use JCMPanels!!) */ public void addComponentPair(Component c1, Component c2) { super.add(c1); super.add(c2); items.addElement( new Component[] { c1, c2 } ); } /** * Add two VariableInputs to the panel. These ARE checked for input when * the user presses return or clicks the SET_LIMITS button. * Furthermore, it is checked that the value in the second input box is greater than * the value in the first, and an error is reported if it is not. * This method is used to add the xmin, xmax, ymin, and ymax * boxes. It could possibly be used to add tmin and tmax boxes * for the limits on the parameter of a parametric curve, * for example. */ public void addRange(final VariableInput v1, final VariableInput v2) { super.add(v1); super.add(v2); v1.addActionListener(this); v2.addActionListener(this); items.addElement( new Component[] { v1, v2, null } ); } /** * Add the buttons in buttonSet to the panel, if they are not * already there. buttonSet should be formed by or-ing * together some of the constants SET_LIMITS, ZOOM_IN, etc. */ public void addButtons(int buttonSet) { if ( (buttonSet & SET_LIMITS) != 0 && (buttons & SET_LIMITS) == 0 && (buttonSet & EQUALIZE) != 0 && (buttons & EQUALIZE) == 0 ) addComponentPair( makeButton(0), makeButton(1) ); else if ( (buttonSet & SET_LIMITS) != 0 && (buttons & SET_LIMITS) == 0 ) addComponent( makeButton(0) ); else if ( (buttonSet & EQUALIZE) != 0 && (buttons & EQUALIZE) == 0 ) addComponent( makeButton(1) ); if ( (buttonSet & ZOOM_IN) != 0 && (buttons & ZOOM_IN) == 0 && (buttonSet & ZOOM_OUT) != 0 && (buttons & ZOOM_OUT) == 0 ) addComponentPair( makeButton(2), makeButton(3) ); else if ( (buttonSet & ZOOM_IN) != 0 && (buttons & ZOOM_IN) == 0 ) addComponent( makeButton(2) ); else if ( (buttonSet & ZOOM_OUT) != 0 && (buttons & ZOOM_OUT) == 0 ) addComponent( makeButton(3) ); if ( (buttonSet & SAVE) != 0 && (buttons & SAVE) == 0 && (buttonSet & RESTORE) != 0 && (buttons & RESTORE) == 0 ) addComponentPair( makeButton(4), makeButton(5) ); else if ( (buttonSet & SAVE) != 0 && (buttons & SAVE) == 0 ) addComponent( makeButton(4) ); else if ( (buttonSet & RESTORE) != 0 && (buttons & RESTORE) == 0 ) addComponent( makeButton(5) ); buttons = buttons | buttonSet; } /** * Get a Button corresponding to one of the six button types defined by the constants * SET_LIMITS, EQUALIZE, ZOOM_IN, ZOOM_OUT, SAVE, and RESTORE in this class. The button * can be added to a different panel, but it will still affect this LimitControlPanel in * the usual way. It is possible to change the name of the button, and it will still * work correctly. Each call to this method creates a new button, even if multiple buttons * of the same type are created. * * @param buttonCode one of the constants from this class (SET_LIMITS, EQUALIZE, etc.) specifying * one of the types of button for controlling limits. If the parameter is not one of * these constants, and IllegalArgumentException will be thrown. */ public Button getButton(int buttonCode) { int buttonNum; if (buttonCode == SET_LIMITS) buttonNum = 0; else if (buttonCode == EQUALIZE) buttonNum = 1; else if (buttonCode == ZOOM_IN) buttonNum = 2; else if (buttonCode == ZOOM_OUT) buttonNum = 3; else if (buttonCode == SAVE) buttonNum = 4; else if (buttonCode == RESTORE) buttonNum = 5; else throw new IllegalArgumentException("Unknown button code passed to getButton()."); Button b = makeButton(buttonNum); b.setActionCommand(buttonNames[buttonNum]); // So command won't change if button name is changed. return b; } // ---------------- Implementation details ------------------------------------------ private Button makeButton(int i) { // Make one of the limit control buttons. Parameter is an index into the buttonNames // array, not one of the constants SET_LIMITS, EQUALIZE, etc. Button b = new Button(buttonNames[i]); b.setBackground(Color.lightGray); b.addActionListener(this); return b; } /** * Redefine this method from the Component class to make it a synonym for addComponent(c); */ public Component add(Component c) { // Redefine this, in case someone uses it. addComponent(c); return c; } /** * Method required by CheckInput interface. In this class, it does nothing because * responses to inputs are handled by the LimitControlPanel itself. */ public void notifyControllerOnChange(Controller c) { } /** * Check the input boxes in this panel. This is generally not meant to be * called from outside this class, except by a Controller. */ public void checkInput() { try { boolean changed = false; for (int i = 0; i < items.size(); i++) { Object obj = items.elementAt(i); if (obj instanceof Component[] && ((Component[])obj).length == 3) { VariableInput v1 = (VariableInput)((Component[])obj)[0]; VariableInput v2 = (VariableInput)((Component[])obj)[1]; double x, x1, x2; x = v1.getVal(); v1.checkInput(); x1 = v1.getVal(); if (x != x1) changed = true; x = v2.getVal(); v2.checkInput(); x2 = v2.getVal(); if (x != x2) changed = true; if (x1 >= x2) throw new JCMError("The value of " + v2.getName() + " must be greater than the value of " + v1.getName() + ".", v2); } } if (errorReporter != null) errorReporter.clearErrorMessage(); if (changed) { serialNumber++; if (syncWith != null) syncWith.check(); } } catch (JCMError e) { if (errorReporter != null) errorReporter.setErrorMessage(null,e.getMessage()); else System.out.println("***** Error: " + e.getMessage()); if (e.object instanceof TextField) { ((TextField)e.object).selectAll(); ((TextField)e.object).requestFocus(); } } catch(RuntimeException e) { if (errorReporter != null) errorReporter.setErrorMessage(null,e.toString()); e.printStackTrace(); } } /** * Part of the Tieable interface, and not meant to be called directly. */ public long getSerialNumber() { return serialNumber; } /** * Part of the Tieable interface, and not meant to be called directly. */ public void sync(Tie t, Tieable newest) { if (newest == this) return; if ( ! (newest instanceof Tieable) ) throw new IllegalArgumentException("Internal Error: A LimitControlPanel can only sync with a Limits object."); setLimits(((Limits)newest).getLimits()); serialNumber = newest.getSerialNumber(); } /** * Get the values in the xmin, xmax, ymin, and ymax input boxes. Note that this can * throw a JCMError. */ public double[] getLimits() { double[] limits = new double[4]; limits[0] = xmin.getVal(); limits[1] = xmax.getVal(); limits[2] = ymin.getVal(); limits[3] = ymax.getVal(); return limits; } /** * Set the values in the xmin, xmax, ymin, and ymax input boxes. */ public void setLimits(double[] limits) { if (limits == null || limits.length < 4) throw new IllegalArgumentException("Internal Error: Not enough values supplied for setLimits."); for (int i = 0; i < 4; i++) if (Double.isNaN(limits[i]) || Double.isInfinite(limits[i])) return; boolean changed = false; if (limits[0] != xmin.getVal()) { changed = true; xmin.setVal(limits[0]); } if (limits[1] != xmax.getVal()) { changed = true; xmax.setVal(limits[1]); } if (limits[2] != ymin.getVal()) { changed = true; ymin.setVal(limits[2]); } if (limits[3] != ymax.getVal()) { changed = true; ymax.setVal(limits[3]); } if (changed) serialNumber++; } /** * Handle a click on one of the standard buttons. * Not meant to be called directly. */ public void actionPerformed(ActionEvent evt) { String cmd = evt.getActionCommand(); if (evt.getSource() instanceof VariableInput || cmd.equals(buttonNames[0])) checkInput(); else if (coords == null) return; else if (cmd.equals(buttonNames[1])) coords.equalizeAxes(); else if (cmd.equals(buttonNames[2])) coords.zoomIn(); else if (cmd.equals(buttonNames[3])) coords.zoomOut(); else if (cmd.equals(buttonNames[4])) coords.setRestoreBuffer(); else if (cmd.equals(buttonNames[5])) coords.restore(); } /** * Draw the input box labels. * Not meant to be called directly. */ public void paint(Graphics g) { int n = getComponentCount(); for (int i = 0; i < n; i++) { Component c = getComponent(i); if (c instanceof VariableInput) { Point topLeft = c.getLocation(); g.drawString( c.getName(), topLeft.x + 4, topLeft.y - 4 ); } } } /** * Compute the preferred size of this panel. * Not meant to be called directly. */ public Dimension getPreferredSize() { int width = 0; int height = 5; FontMetrics fm = getFontMetrics(getFont()); int lineHeight = (fm == null)? 12 : 4 + fm.getAscent(); for (int i = 0; i < items.size(); i++) { Object obj = items.elementAt(i); if (obj instanceof Component) { Component c = (Component)obj; Dimension d = c.getPreferredSize(); height += d.height + 5; if (c instanceof VariableInput) height += lineHeight; if (d.width > width) width = d.width; } else { Component[] pair = (Component[])obj; Dimension d1 = pair[0].getPreferredSize(); if (pair[0] instanceof VariableInput) d1.height += lineHeight; Dimension d2 = pair[1].getPreferredSize(); if (pair[1] instanceof VariableInput) d2.height += lineHeight; if (twoColumn) { width = Math.max(width,d1.width+d2.width); height = height + Math.max(d1.height,d2.height) + 5; } else { height += d1.height + d2.height + 10; width = Math.max(width,d1.width); height = Math.max(height,d2.height) ; } } } return new Dimension(width + (twoColumn? 15 : 10), height); } /** * Recompute component locations when the panel is resized. * Not meant to be called directly. */ public void processComponentEvent(ComponentEvent evt) { if (evt.getID() == ComponentEvent.COMPONENT_RESIZED) { Dimension size = getSize(); Dimension preferredSize = getPreferredSize(); boolean two = twoColumn; // Do we really use two columns? if (two && size.width < preferredSize.width - 20) { two = false; twoColumn = false; preferredSize = getPreferredSize(); twoColumn = true; } int count = items.size(); if (!two) for (int i = 0; i < items.size(); i++) if (items.elementAt(i) instanceof Component[]) count++; double scale, vspace; if (size.height >= preferredSize.height) { scale = 1; vspace = 5 + (size.height - preferredSize.height)/(count+2); if (vspace > 15) vspace = 15; } else if (size.height >= preferredSize.height - 4*(count+2)) { scale = 1; vspace = (size.height - (preferredSize.height - 5*(count+2)))/(count+2); } else { scale = (double)size.height / (preferredSize.height - 4*(count+2)); vspace = 1; } int hspace = (size.width - (preferredSize.width - (two? 15 : 10)))/(two? 3 : 2); if (hspace < 1) hspace = 1; else if (hspace > 10) hspace = 10; double y = vspace; int lineHeight = 4 + (getFontMetrics(getFont())).getAscent(); for (int i = 0; i < items.size(); i++) { Object obj = items.elementAt(i); if (obj instanceof Component) { Component c = (Component)obj; Dimension p = c.getPreferredSize(); if (c instanceof VariableInput) y += lineHeight*scale; if (p.width + 2*hspace < size.width - 10) c.setBounds((size.width - p.width)/2, (int)y, p.width, (int)(p.height*scale)); else c.setBounds(hspace, (int)y, p.width - 2*hspace, (int)(p.height*scale)); y += scale*(p.height) + vspace; } else { Component[] pair = (Component[])obj; Dimension d1 = pair[0].getPreferredSize(); Dimension d2 = pair[1].getPreferredSize(); if (two) { if (pair[0] instanceof VariableInput || pair[1] instanceof VariableInput) y += lineHeight*scale; int h = (int)(scale*Math.max(d1.height,d2.height)); pair[0].setBounds(hspace,(int)y,(size.width-3*hspace)/2,h); pair[1].setBounds(hspace*2+(size.width-3*hspace)/2,(int)y,(size.width-3*hspace)/2,h); y += scale*Math.max(d1.height,d2.height) + vspace; } else { if (pair[0] instanceof VariableInput) y += lineHeight*scale; pair[0].setBounds(hspace,(int)y,size.width-2*hspace,(int)(d1.height*scale)); y += d1.height*scale + vspace; if (pair[1] instanceof VariableInput) y += lineHeight*scale; pair[1].setBounds(hspace,(int)y,size.width-2*hspace,(int)(d2.height*scale)); y += d2.height*scale + vspace; } } } } super.processComponentEvent(evt); } } jcm1-source/edu/hws/jcm/draw/Grid.java0000644000076500011320000001540611741343635017031 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.*; /** * A Grid object draws a graph paper-like grid on a Canvas. The pixel width * and height between adjacent grid lines is specified as a parameter to the * constructer, or through the access methods "setXSP(double)" and * "setYSP(double)". Note that the spacing will be scaled to between PIX_MIN * and PIX_MAX (20 and 80, respectively, by default). The color of the grid * lines can be set, and defaults to (220, 220, 220). * *

This class was written by Gabriel Weinstock (with some modifications by David Eck). */ public class Grid extends Drawable { private Color gcol = new Color(220, 220, 220); private double xsp, ysp; private final int PIX_MAX = 50, PIX_MIN = 20; /** * Create a Grid object with x and y spacing 1.0. This does not mean that * the actual spacing between grid lines will be 1. It will be some reasonable * fraction or multiply of 1, with the value chosen to give a reasonable * spacing between the grid lines. */ public Grid() { this(1.0, 1.0); } /** * Create a Grid object with spacing specified. */ public Grid(double xspace, double yspace) { xsp = xspace; ysp = yspace; } /** * Access method which returns the Color of the grid lines. */ public Color getColor() { return gcol; } /** * Method to set the Color used to draw grid lines. */ public void setColor(Color c) { if(c != null && !c.equals(gcol)) { gcol = c; needsRedraw(); } } /** * Access method to return the x spacing used between grid lines. */ public double getXSP() { return xsp; } /** * Access method to return the y spacing used between grid lines */ public double getYSP() { return ysp; } /** * Method to set the x spacing between grid lines. This does not mean that * the actual spacing between grid lines will be x. It will be some reasonable * fraction or multiply of s, with the value chosen to give a reasonable * spacing between the grid lines. */ public void setXSP(double x) { xsp = x; needsRedraw(); } /** * Method to set the y spacing between grid lines. This does not mean that * the actual spacing between grid lines will be y. It will be some reasonable * fraction or multiply of s, with the value chosen to give a reasonable * spacing between the grid lines. */ public void setYSP(double y) { ysp = y; needsRedraw(); } /** * Draws the grid if an update is required. This is not usually called directly. * * @param g the Graphics context * @param coordsch boolean describing whether coordinates have changed */ public void draw(Graphics g, boolean coordsch) { if(coords == null) return; double pixwidth = coords.getPixelWidth(); double pixheight = coords.getPixelHeight(); if (Double.isNaN(pixwidth) || Double.isNaN(pixheight) || Double.isInfinite(pixheight) || Double.isInfinite(pixwidth) || pixwidth == 0 || pixheight == 0) return; g.setColor(gcol); // start by drawing vertical grid lines (starting with center): if(xsp > 0) { double x = xsp; while(x > (pixwidth * PIX_MAX)) x /= 10; if (x < (pixwidth * PIX_MIN)) x *= 5; if (x > (pixwidth * PIX_MAX)) x /= 2; int j = (int) (Math.ceil(coords.getXmin() / x)); double i = j * x; while(coords.xToPixel(i) < (coords.getWidth() + coords.getLeft())) { g.drawLine(coords.xToPixel(i), coords.getTop(), coords.xToPixel(i), coords.getTop() + coords.getHeight()); i += x; } } // next draw horizontal grid lines if(ysp > 0) { double y = ysp; while(y > (pixheight * PIX_MAX)) y /= 10; if (y < (pixheight * PIX_MIN)) y *= 5; if (y > (pixheight * PIX_MAX)) y /= 2; int j = (int) (Math.ceil(coords.getYmin() / y)); double i = j * y; while(coords.yToPixel(i) > coords.getTop()) { g.drawLine(coords.getLeft(), coords.yToPixel(i), coords.getLeft() + coords.getWidth(), coords.yToPixel(i)); i += y; } } } } // end class Grid jcm1-source/edu/hws/jcm/draw/Axes.java0000644000076500011320000005164611741343635017052 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.data.NumUtils; import java.awt.*; /** * A set of horizontal and vertical axes that look OK and * have reasonable, labeled tick marks. The number and spacing of tick * marks changes depending on the scale on the axes. (The heuristics * for computing this could use some improvement.) */ public class Axes extends Drawable { /** * Creates axes with no names on the axes. */ public Axes() { this(null,null); } /** * Creates axes with given names on the axes. * * @param xlabel Label for x axis. If the value is null, no label is drawn. * @param ylabel Label for y axis. If the value is null, no label is drawn. */ public Axes(String xLabel, String yLabel) { this.xLabel = xLabel; this.yLabel = yLabel; } /** * A constant that can be used in the setYAxisPosition() method to indicate the placement of the y-axis. * The axis is placed at the top of the CoordinateRect. */ public static final int TOP = 0; /** * A constant that can be used in the setYAxisPosition() method to indicate the placement of the y-axs. * The axis is placed at the bottom of the CoordinateRect. */ public static final int BOTTOM = 1; /** * A constant that can be used in the setXAxisPosition() method to indicate the placement of the x-axis. * The axis is placed at the left edge of the CoordinateRect. */ public static final int LEFT = 2; /** * A constant that can be used in the setXAxisPosition() method to indicate the placement of the x-axis. * The axis is placed at the right edge of the CoordinateRect. */ public static final int RIGHT = 3; /** * A constant that can be used in the setXAxisPosition() and setYAxisPosition() methods to indicate the placement of the axes. * The axis is placed in the center of the CoordinateRect. */ public static final int CENTER = 4; /** * A constant that can be used in the setXAxisPosition() and setYAxisPosition() methods to indicate the placement of the axes. * The axis is placed at its true x- or y-position, if that lies within the range of values shown on the CoordinateRect. * Otherwise, it is placed along an edge of the CoordinateRect. This is the default value for axis placement. */ public static final int SMART = 5; private int xAxisPosition = SMART; private int yAxisPosition = SMART; private Color axesColor = new Color(0,0,180); private Color lightAxesColor = new Color(180,180,255); // Used if real axis is outside the draw rect private Color labelColor = Color.black; private String xLabel = null; private String yLabel = null; //------------------ Methods for getting/setting properties ---------------- /** * Get the color that is used for drawing the axes, when they are drawn in their true position. */ public Color getAxesColor() { return axesColor; } /** * Set the color that is used for drawing the axes, when they are drawn in their true position. * The default is blue. */ public void setAxesColor(Color c) { if (c != null && !c.equals(axesColor)) { axesColor = c; needsRedraw(); } } /** * Get the color that is used for drawing an axis, when it is drawn along an edge of the CoordinateRect * instead of in its proper x- or y-position. */ public Color getLightAxesColor() { return lightAxesColor; } /** * Get the color that is used for drawing an axis, when it is drawn along an edge of the CoordinateRect * instead of in its proper x- or y-position. The default is a light blue. */ public void setLightAxesColor(Color c) { if (c != null && !c.equals(lightAxesColor)) { lightAxesColor = c; needsRedraw(); } } /** * Get the color that is used for drawing the labels on the x- and y-axes. */ public Color getLabelColor() { return labelColor; } /** * Set the color that is used for drawing the labels (usually the names of the variables) on the x- and y-axes. * The default is black. */ public void setLabelColor(Color c) { if (c != null && !c.equals(labelColor)) { labelColor = c; if (xLabel != null || yLabel != null) needsRedraw(); } } /** * Get the positioning constant that tells where the x-axis is drawn. This can be LEFT, RIGHT, CENTER, or SMART. */ public int getXAxisPosition() { return xAxisPosition; } /** * Set the positioning constant that tells where the x-axis is drawn. This can be LEFT, RIGHT, CENTER, or SMART. * The default is SMART. */ public void setXAxisPosition(int pos) { if ((pos == TOP || pos == BOTTOM || pos == CENTER || pos == SMART) && pos != xAxisPosition) { xAxisPosition = pos; needsRedraw(); } } /** * Get the positioning constant that tells where the y-axis is drawn. This can be TOP, BOTTOM, CENTER, or SMART. */ public int getYAxisPosition() { return yAxisPosition; } /** * Set the positioning constant that tells where the y-axis is drawn. This can be TOP, BOTTOM, CENTER, or SMART. * The default is SMART. */ public void setYAxisPosition(int pos) { if ((pos == LEFT || pos == RIGHT || pos == CENTER || pos == SMART) && pos != yAxisPosition) { yAxisPosition = pos; needsRedraw(); } } /** * Get the label that appears on the x-axis. If the value is null, no label appears. */ public String getXLabel() { return xLabel; } /** * Set the label that appears on the x-axis. If the value is null, no label appears. This is the default. */ public void setXLabel(String s) { xLabel = s; needsRedraw(); } /** * Get the label that appears on the y-axis. If the value is null, no label appears. */ public String getYLabel() { return yLabel; } /** * Set the label that appears on the y-axis. If the value is null, no label appears. This is the default. */ public void setYLabel(String s) { yLabel = s; needsRedraw(); } //-------------------------------------------------------------------------- /** * Draw the axes. This is not meant to be called directly. * */ public void draw(Graphics g, boolean coordsChanged) { if (coords == null) return; if (coordsChanged || xTicks == null || !g.getFont().equals(font)) { // The second test forces a setup() when the // Axes object has been reloaded after serialization. // The third test accounts for the fact that the // font might have changed since the last time // a setup() was done. font = g.getFont(); FontMetrics fm = g.getFontMetrics(font); setup(fm, coords.getXmin(), coords.getXmax(), coords.getYmin(), coords.getYmax(), coords.getLeft(), coords.getTop(), coords.getWidth(), coords.getHeight(), coords.getGap()); } doDraw(g, coords.getXmin(), coords.getXmax(), coords.getYmin(), coords.getYmax(), coords.getLeft(), coords.getTop(), coords.getWidth(), coords.getHeight(), coords.getGap()); } private void doDraw(Graphics g, double xmin, double xmax, double ymin, double ymax, int left, int top, int width, int height, int gap) { // Draw axes using data computed by setup(). The parameters come from the CoordinateRect. if (xAxisPosition == SMART && (ymax < 0 || ymin > 0)) g.setColor(lightAxesColor); else g.setColor(axesColor); g.drawLine(left + gap, xAxisPixelPosition, left + width - gap - 1, xAxisPixelPosition); for (int i = 0; i < xTicks.length; i++) { int a = (xAxisPixelPosition - 2 < top) ? xAxisPixelPosition : xAxisPixelPosition - 2; int b = (xAxisPixelPosition + 2 >= top + height)? xAxisPixelPosition : xAxisPixelPosition + 2; g.drawLine(xTicks[i], a, xTicks[i], b); } for (int i = 0; i < xTickLabels.length; i++) g.drawString(xTickLabels[i], xTickLabelPos[i][0], xTickLabelPos[i][1]); if (yAxisPosition == SMART && (xmax < 0 || xmin > 0)) g.setColor(lightAxesColor); else g.setColor(axesColor); g.drawLine(yAxisPixelPosition, top + gap, yAxisPixelPosition, top + height - gap - 1); for (int i = 0; i < yTicks.length; i++) { int a = (yAxisPixelPosition - 2 < left) ? yAxisPixelPosition : yAxisPixelPosition - 2; int b = (yAxisPixelPosition + 2 >= left + width)? yAxisPixelPosition : yAxisPixelPosition + 2; g.drawLine(a, yTicks[i], b, yTicks[i]); } for (int i = 0; i < yTickLabels.length; i++) g.drawString(yTickLabels[i], yTickLabelPos[i][0], yTickLabelPos[i][1]); g.setColor(labelColor); if (xLabel != null) g.drawString(xLabel, xLabel_x, xLabel_y); if (yLabel != null) g.drawString(yLabel, yLabel_x, yLabel_y); } private transient int[] xTicks; // Data for drawing axes private transient int[] yTicks; private transient String[] xTickLabels; private transient String[] yTickLabels; private transient int[][] xTickLabelPos; private transient int[][] yTickLabelPos; private transient int xAxisPixelPosition, yAxisPixelPosition; private transient int xLabel_x, xLabel_y, yLabel_x, yLabel_y; private transient Font font; private transient int ascent, descent, digitWidth; void setup(FontMetrics fm, double xmin, double xmax, double ymin, double ymax, int left, int top, int width, int height, int gap) { // Set up all data for drawing the axes. digitWidth = fm.charWidth('0'); ascent = fm.getAscent(); descent = fm.getDescent(); switch (xAxisPosition) { case TOP: xAxisPixelPosition = top + gap; break; case BOTTOM: xAxisPixelPosition = top + height - gap - 1; break; case CENTER: xAxisPixelPosition = top + height/2; break; case SMART: if (ymax < 0) xAxisPixelPosition = top + gap; else if (ymin > 0) xAxisPixelPosition = top + height - gap - 1; else xAxisPixelPosition = top + gap + (int)((height-2*gap - 1) * ymax / (ymax-ymin)); break; } switch (yAxisPosition) { case LEFT: yAxisPixelPosition = left + gap; break; case BOTTOM: yAxisPixelPosition = left + width - gap - 1; break; case CENTER: yAxisPixelPosition = left + width/2; break; case SMART: if (xmax < 0) yAxisPixelPosition = left + width - gap - 1; else if (xmin > 0) yAxisPixelPosition = left + gap; else yAxisPixelPosition = left + gap - (int)((width-2*gap - 1) * xmin / (xmax-xmin)); break; } if (xLabel != null) { int size = fm.stringWidth(xLabel); if (left + width - gap - size <= yAxisPixelPosition) xLabel_x = left + gap; else xLabel_x = left + width - gap - size; if (xAxisPixelPosition + 3 + ascent + descent + gap >= top + height) xLabel_y = xAxisPixelPosition - 4; else xLabel_y = xAxisPixelPosition + 3 + ascent; } if (yLabel != null) { int size = fm.stringWidth(yLabel); if (yAxisPixelPosition + 3 + size + gap > left + width) yLabel_x = yAxisPixelPosition - size - 3; else yLabel_x = yAxisPixelPosition + 3; if (top + ascent + descent + gap > xAxisPixelPosition) yLabel_y = top + height - gap - descent; else yLabel_y = top + ascent + gap; } double start = fudgeStart( ((xmax-xmin)*(yAxisPixelPosition - (left + gap)))/(width - 2*gap) + xmin, 0.05*(xmax-xmin) ); int labelCt = (width - 2*gap) / (10*digitWidth); if (labelCt <= 2) labelCt = 3; else if (labelCt > 20) labelCt = 20; double interval = fudge( (xmax - xmin) / labelCt ); for (double mul = 1.5; mul < 4; mul += 0.5) { if (fm.stringWidth(NumUtils.realToString(interval+start)) + digitWidth > (interval/(xmax-xmin))*(width-2*gap)) // overlapping labels interval = fudge( mul*(xmax - xmin) / labelCt ); else break; } double[] label = new double[50]; labelCt = 0; double x = start + interval; double limit = left + width; if (xLabel != null && left + width - gap - fm.stringWidth(xLabel) > yAxisPixelPosition) // avoid overlap with xLabel limit -= fm.stringWidth(xLabel) + gap + digitWidth; while (labelCt < 50 && x <= xmax) { if (left + gap + (width-2*gap)*(x-xmin)/(xmax-xmin) + fm.stringWidth(NumUtils.realToString(x))/2 > limit) break; label[labelCt] = x; labelCt++; x += interval; } x = start - interval; limit = left; if (xLabel != null && left + width - gap - fm.stringWidth(xLabel) <= yAxisPixelPosition) // avoid overlap with xLabel limit += fm.stringWidth(xLabel) + digitWidth; while (labelCt < 50 && x >= xmin) { if (left + gap + (width-2*gap)*(x-xmin)/(xmax-xmin) - fm.stringWidth(NumUtils.realToString(x))/2 < limit) break; label[labelCt] = x; labelCt++; x -= interval; } xTicks = new int[labelCt]; xTickLabels = new String[labelCt]; xTickLabelPos = new int[labelCt][2]; for (int i = 0; i < labelCt; i++) { xTicks[i] = (int)(left + gap + (width-2*gap)*(label[i]-xmin)/(xmax-xmin)); xTickLabels[i] = NumUtils.realToString(label[i]); xTickLabelPos[i][0] = xTicks[i] - fm.stringWidth(xTickLabels[i])/2; if (xAxisPixelPosition - 4 - ascent >= top) xTickLabelPos[i][1] = xAxisPixelPosition - 4; else xTickLabelPos[i][1] = xAxisPixelPosition + 4 + ascent; } start = fudgeStart( ymax - ((ymax-ymin)*(xAxisPixelPosition - (top + gap)))/(height - 2*gap), 0.05*(ymax-ymin) ); labelCt = (height - 2*gap) / (5*(ascent+descent)); if (labelCt <= 2) labelCt = 3; else if (labelCt > 20) labelCt = 20; interval = fudge( (ymax - ymin) / labelCt ); labelCt = 0; double y = start + interval; limit = top + 8 + gap; if (yLabel != null && top + gap + ascent + descent <= xAxisPixelPosition) // avoid overlap with yLabel limit = top + gap + ascent + descent; while (labelCt < 50 && y <= ymax) { if (top + gap + (height-2*gap)*(ymax-y)/(ymax-ymin) - ascent/2 < limit) break; label[labelCt] = y; labelCt++; y += interval; } y = start - interval; limit = top + height - gap - 8; if (yLabel != null && top + gap + ascent + descent > xAxisPixelPosition) // avoid overlap with yLabel limit = top + height - gap - ascent - descent; while (labelCt < 50 && y >= ymin) { if (top + gap + (height-2*gap)*(ymax-y)/(ymax-ymin) + ascent/2 > limit) break; label[labelCt] = y; labelCt++; y -= interval; } yTicks = new int[labelCt]; yTickLabels = new String[labelCt]; yTickLabelPos = new int[labelCt][2]; int w = 0; // max width of tick mark for (int i = 0; i < labelCt; i++) { yTickLabels[i] = NumUtils.realToString(label[i]); int s = fm.stringWidth(yTickLabels[i]); if (s > w) w = s; } for (int i = 0; i < labelCt; i++) { yTicks[i] = (int)(top + gap + (height-2*gap)*(ymax-label[i])/(ymax-ymin)); yTickLabelPos[i][1] = yTicks[i] + ascent/2; if (yAxisPixelPosition - 4 - w < left) yTickLabelPos[i][0] = yAxisPixelPosition + 4; else yTickLabelPos[i][0] = yAxisPixelPosition - 4 - fm.stringWidth(yTickLabels[i]); } } // end setup() /** * Translated directly from the Pascal version of xFunctions. * Move x to a more "rounded" value; used for labeling axes. * * @param x the x coordinate used for labeling axes * @return the rounded value of x */ double fudge (double x) { int i, digits; double y; if (Math.abs(x) < 0.0005 || Math.abs(x) > 500000) return x; else if (Math.abs(x) < 0.1 || Math.abs(x) > 5000) { y = x; digits = 0; if (Math.abs(y) >= 1) { while (Math.abs(y) >= 8.75) { y = y / 10; digits = digits + 1; } } else { while (Math.abs(y) < 1) { y = y * 10; digits = digits - 1; } } y = Math.round(y * 4) / 4; if (digits > 0) { for (int j = 0; j < digits; j++) y = y * 10; } else if (digits < 0) { for (int j = 0; j < -digits; j++) y = y / 10; } return y; } else if (Math.abs(x) < 0.5) return Math.round(10 * x) / 10.0; else if (Math.abs(x) < 2.5) return Math.round(2 * x) / 2.0; else if (Math.abs(x) < 12) return Math.round(x); else if (Math.abs(x) < 120) return Math.round(x / 10) * 10.0; else if (Math.abs(x) < 1200) return Math.round(x / 100) * 100.0; else return Math.round(x / 1000) * 1000.0; } private double fudgeStart(double a, double diff) { // Adapted from the Pascal version of xFunctions. // Tries to find a "rounded value" within diff of a. if (Math.abs(Math.round(a) - a) < diff) return Math.round(a); for (double x = 10; x <= 100000; x *= 10) { double d = Math.round(a*x) / x; if (Math.abs(d - a) < diff) return d; } return a; } } // end class Axes jcm1-source/edu/hws/jcm/draw/RiemannSumRects.java0000644000076500011320000003511311741343635021220 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.*; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; /** * A RiemannSumRects calculates a Riemann sum for a function. It implements * Computable and InputObject. You can specify and change the number of * intervals in the sum, as well as the method used to calculate the sum. * Functions exist to return Value objects for the sum using different * computations. This class was written by Gabriel Weinstock, with some * modifications by David Eck */ public class RiemannSumRects extends Drawable implements Computable { private double[] rectHeights; private int method; private Color color = new Color(255, 255, 180); private Color outlineColor = new Color(180,180,0); private double []endpointVals, maxVals, minVals, midpointVals; private Value intervalCount; private Function func, deriv; // derivative is used in max/min computations // store sum data here: private double[] sum; private double[] param = new double[1]; private boolean changed = true; /** * Summation method type. */ public static final int LEFTENDPOINT = 0, RIGHTENDPOINT = 1, MIDPOINT = 2, CIRCUMSCRIBED = 3, INSCRIBED = 4, TRAPEZOID = 5; /** * For use in getValueObject(), to indicate whatever summation method is currently set for drawing. */ public static final int CURRENT_METHOD = -1; /** * Get the current color used to draw the rectangles */ public Color getColor() { return color; } /** * Set the color used to draw the rectangles. The default color is a light yellow. */ public void setColor(Color c) { if (c != null) { color = c; needsRedraw(); } } /** * Set the color that will be used to draw outlines around the rects. If this is null, * then no outlines are drawn. The default is a medium-dark red that looks brownish next to the default yellow fill color. */ public void setOutlineColor(Color c) { outlineColor = c; needsRedraw(); } /** * Get the color that is used to draw outlines around the rects. If this is null, then * no outlines are drawn. */ public Color getOutlineColor() { return outlineColor; } /** * Set the function whose Riemann sums are to be computed. If null, nothing is drawn. * The function, if non-null, must have arity 1, or an IllegalArgumentException is thrown. */ public void setFunction(Function func) { if (func != null && func.getArity() != 1) throw new IllegalArgumentException("Function for Riemann sums must have arity 1."); this.func = func; deriv = (func == null)? null : func.derivative(1); changed = true; needsRedraw(); } /** * Returns the function whose Riemann sums are computed. Can be null. */ public Function getFuction() { return func; } /** * Set the method used to calculate the rectangles. * @param m can be: LEFTENDPOINT, RIGHTENDPOINT, MIDPOINT, CIRCUMSCRIBED, * INSCRIBED or TRAPEZOID (these are integers ranging from 0 to 5, * respectively) */ public void setMethod(int m) { method = m; changed = true; needsRedraw(); } /** * Return the current method used to find the rectangle sums */ public int getMethod() { return method; } /** * This is generally called by a Controller. Indicates that all data should be recomputed * because input values that the data depends on might have changed. */ public void compute() { changed = true; needsRedraw(); } /** * Get the number of intervals used. * @return a Value object representing the number of intervals */ public Value getIntervalCount() { return intervalCount; } /** * Set the interval count (the RiemannSumRects will be redrawn after this function * is called). The value will be clamped to be a value between 1 and 5000. * If the value is null, the default number of intervals, five, is used. * @param c a Value object representing the interval count */ public void setIntervalCount(Value c) { changed = true; intervalCount = c; needsRedraw(); } /** * Construct a RiemannSumRects object that initially has nothing to draw and that * is set up to use the default number of intervals, 5. */ public RiemannSumRects() { this(null,null); } /** * Construct a new RiemannSumRects object. * @param i a Value object representing the number of intervals. If null, five intervals are used. * @param f a Function object used to derive the Riemann sum. If null, nothing is drawn. */ public RiemannSumRects(Function f, Value i) { intervalCount = i; func = f; if (f != null) deriv = func.derivative(1); sum = new double[6]; method = LEFTENDPOINT; } /** * Draw the Rieman sum rects. This is generally called by an object of class CoordinateRect */ public void draw(Graphics g, boolean coordsChanged) { if (func == null || coords == null) return; if (changed || rectHeights == null || coordsChanged) setSumData(); int intervals = ((method == 5 || method == 0 || method == 1) ? (rectHeights.length - 1) : rectHeights.length); double x = coords.getXmin(); double dx = (coords.getXmax() - x) / intervals; int zero = coords.yToPixel(0); g.setColor(color); if(method == 5) // trapezoids { int []xp = new int[4]; int []yp = new int[4]; xp[1] = coords.xToPixel(x); yp[0] = yp[1] = zero; yp[2] = coords.yToPixel(rectHeights[0]); for(int i = 0; i < intervals; i++) { x += dx; xp[0] = xp[3] = xp[1]; xp[1] = xp[2] = coords.xToPixel(x); yp[3] = yp[2]; yp[2] = coords.yToPixel(rectHeights[i + 1]); g.fillPolygon(xp, yp, 4); if (outlineColor != null) { g.setColor(outlineColor); g.drawPolygon(xp, yp, 4); g.setColor(color); } } } else { int left = coords.xToPixel(x); for(int i = 0; i < intervals; i++) { int right = coords.xToPixel(x + dx); int width = right - left + 1; int top = coords.yToPixel(rectHeights[(method == 1)? i + 1 : i]); int height = zero - top; if(height > 0) g.fillRect(left, top, width, height); else if(height == 0) g.drawLine(left, zero, left + width - 1, zero); else g.fillRect(left, zero, width, -height); if (outlineColor != null) { g.setColor(outlineColor); if(height > 0) g.drawRect(left, top, width, height); else if(height == 0) g.drawLine(left, zero, left + width - 1, zero); else g.drawRect(left, zero, width, -height); g.setColor(color); } x += dx; left = right; } } } private void setSumData() { // Recompute all data. changed = false; double intCtD = (intervalCount == null)? 5 : (intervalCount.getVal()+0.5); if (Double.isNaN(intCtD) || Double.isInfinite(intCtD)) intCtD = 5; else if (intCtD < 0) intCtD = 1; else if (intCtD > 5000) intCtD = 5000; int intCt = (int)intCtD; endpointVals = new double[intCt + 1]; maxVals = new double[intCt]; minVals = new double[intCt]; midpointVals = new double[intCt]; double x = coords.getXmin(); double dx = (coords.getXmax() - x) / intCt; param[0] = x; endpointVals[0] = func.getVal(param); int ptsPerInterval = 200 / intCt; double smalldx; if(ptsPerInterval < 1) { ptsPerInterval = 1; smalldx = dx; } else smalldx = dx / ptsPerInterval; boolean increasingleft; boolean increasingright = deriv.getVal(param) > 0; for(int i = 1; i <= intCt; i++) { x += dx; param[0] = x; endpointVals[i] = func.getVal(param); param[0] = x - dx / 2; midpointVals[i - 1] = func.getVal(param); // maxmin stuff double max, min; max = min = endpointVals[i - 1]; for(int j = 1; j <= ptsPerInterval; j++) // looking for turning points in the interval { increasingleft = increasingright; double xright = (x - dx) + j * smalldx; param[0] = xright; increasingright = deriv.getVal(param) > 0; if(increasingleft != increasingright) { if(increasingleft) { double z = searchMax(xright - smalldx, xright, 1); if(z > max) max = z; } else { double z = searchMin(xright - smalldx, xright, 1); if (z < min) min = z; } } } if(endpointVals[i] > max) max = endpointVals[i]; else if(endpointVals[i] < min) min = endpointVals[i]; minVals[i - 1] = min; maxVals[i - 1] = max; } double y = endpointVals[0]; double leftsum = 0, midpointsum = 0, rightsum = 0, maxsum = 0, minsum = 0; for(int i = 0; i < intCt; i++) { leftsum += endpointVals[i]; midpointsum += midpointVals[i]; maxsum += maxVals[i]; minsum += minVals[i]; } rightsum = leftsum - endpointVals[0] + endpointVals[intCt]; // calculate sums sum[LEFTENDPOINT] = leftsum * dx; sum[RIGHTENDPOINT] = rightsum * dx; sum[MIDPOINT] = midpointsum * dx; sum[CIRCUMSCRIBED] = maxsum * dx; sum[INSCRIBED] = minsum * dx; sum[TRAPEZOID] = (leftsum + rightsum) / 2 * dx; setRectData(); } private void setRectData() { if (method == 3) setRectHeights(maxVals); else if(method == 4) setRectHeights(minVals); else if (method == 2) setRectHeights(midpointVals); else setRectHeights(endpointVals); } private void setRectHeights(double[] e) { rectHeights = e; changed = true; } private double searchMin(double x1, double x2, int depth) { // find an approximate minimum of func in the interval (x1,x2) double mid = (x1 + x2) / 2; param[0] = mid; if(depth >= 13) return func.getVal(param); double slope = deriv.getVal(param); if(slope < 0) return searchMin(mid, x2, depth + 1); else return searchMin(x1, mid, depth + 1); } private double searchMax(double x1, double x2, int depth) { // find an approximate maximum of func in the interval (x1,x2) double mid = (x1 + x2) / 2; param[0] = mid; if(depth >= 13) return func.getVal(param); double slope = deriv.getVal(param); if(slope > 0) return searchMin(mid, x2, depth + 1); else return searchMin(x1, mid, depth + 1); } /** * Gets a Value object that gives the value of the Riemann sum for the specified method. * @return a Value object representing the sum for the given method * @param which integer stating the method used to derive the sum; one of the * constants LEFTENDPOINT, RIGHTENDPOINT, MIDPOINT, * CIRCUMSCRIBED, INSCRIBED, TRAPEZOID, or CURRENT_METHOD. */ public Value getValueObject(final int which) { return new Value() { public double getVal() { if (func == null || coords == null) return Double.NaN; if (changed) setSumData(); if (which == CURRENT_METHOD) return sum[method]; else return sum[which]; } }; } } // end class RiemannSumRects jcm1-source/edu/hws/jcm/draw/Panner.java0000644000076500011320000002435111741343635017366 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.*; import java.awt.event.*; import edu.hws.jcm.awt.Controller; /** * When a Panner object is added to a CoordinateRect, it becomes possible to * "grab" the coordinate rectangle and pan it (that is, slide it around by * moving it with the mouse). By default, the user must right-click-and-drag * to pan the coordinates, but this can be changed by providing an argument to * the constructor. It is possible to set a Controller to be notified each time * the mouse moves while the user is dragging. Alternatively, or in addition, * you can set a Controller to be notified when the user finishes dragging. * However, for the most part this is unnecessary, since the Drawables in the * CoordinateRect will for the most part redraw themselves properly when the * limits on the CoordinateRect change. However, if you have Computable objects * that depend on the coordinate limits, then they will need to be recomputed. * (This will be the case if you use value objects returned by the * getValueObject() method in the CoordinateRect class.) *

A Panner, p, is inactive if its "visible" property has been set to false. * (This is done by calling p.setVisible(false).) */ public class Panner extends Drawable implements MouseListener, MouseMotionListener { private int modifiers; // Combination of MouseEvent.SHIFT_MASK, MouseEvent.CTRL_MASK, // MouseEvent.META_MASK, and MouseEvent.ALT_MASK that must be // present in the mouse-pressed event for a drag to start. private Controller onUserAction; // notified each time the mouse moves during a drag private Controller onFinishDrag; // notified when the user finishes a drag /** * Create a Panner object that will let the user pan the CoordinateRect * that contains the Panner by * right-clicking and dragging (or, on Macintosh, command-clicking). */ public Panner() { this(MouseEvent.META_MASK); } /** * Create a Panner object that will let the user click-and-drag to pan the CoordinateRect * that contains the Panner. The mouse-pressed event must have the specfied set of * modifiers set. * * @param modifiers If the value is zero, the user drags the CoordinateRect by clicking without * pressing any modifier keys. Otherwise, the value should be a combination of * one or more of the constants MouseEvent.SHIFT_MASK, MouseEvent.CTRL_MASK, * MouseEvent.META_MASK, and MouseEvent.ALT_MASK, or'ed together. (Remember * that right-clicking sets META_MASK and clicking with a middle mouse button * sets ALT_MASK.) * */ public Panner(int modifierSet) { modifiers = modifierSet & (MouseEvent.SHIFT_MASK | MouseEvent.CTRL_MASK | MouseEvent.META_MASK | MouseEvent.ALT_MASK); } /** * Set a Controller that will be notified (by calling its compute method) whenever * the user moves the mouse during a drag. If the value is null, no Controller is * notified. Note that Drawables generally redraw themselvs correctly during the * drag anyway, without any Controller being involved. Even if there are other * things that need to be computed, it's probably better to compute them only once * at the end of the drag. Do this by calling setOnFinishDrag() instead of this method. */ public void setOnUserAction(Controller c) { onUserAction = c; } /** * Get the Controller that is notified when the user moves the mouse during a drag. * Returns null if no Controller is notified. */ public Controller getOnUserAction() { return onUserAction; } /** * Set a Controller that will be notified (by calling its compute method) whenever * the user finishes a drag operation. If the value is null, no Controller is notified. * You only need to do this if you have to recompute some object that depends on the * coordinate limits of the CoordinateRect that contains this Panner object. * Presumably, this will be an object that depends on one if the Value objects returned * by the getValueObject() method in the CoordinatRect class. */ public void setOnFinishDrag(Controller c) { onFinishDrag = c; } /** * Get the Controller that is notified when the user finishs a drag. * Returns null if no Controller is notified. */ public Controller getOnFinishDrag() { return onFinishDrag; } /** * Called when this object is added to a DisplayCanvas. Not meant to be called directly. */ protected void setOwnerData(DisplayCanvas canvas, CoordinateRect coords) { // Called automatically when this object is added to canvas. if (canvas != null) { canvas.removeMouseListener(this); canvas.removeMouseMotionListener(this); } super.setOwnerData(canvas,coords); if (canvas != null) { canvas.addMouseListener(this); canvas.addMouseMotionListener(this); } } /** * Override the abstract draw() method from the Drawable class. This * is defined to be empty since a Panner object has no visible representation. */ public void draw(Graphics g, boolean coordsChanged) { } private boolean dragging; private int prevX, prevY; /** * Responds to a mouse-press. Not meant to be called directly. */ public void mousePressed(MouseEvent evt) { dragging = false; if (evt.isConsumed()) return; if (!getVisible() || canvas == null || coords == null) return; if ( (evt.getModifiers() & modifiers) != modifiers ) return; prevX = evt.getX(); prevY = evt.getY(); if (prevX < coords.getLeft() || prevX >= coords.getLeft() + coords.getWidth() || prevY < coords.getTop() || prevY >= coords.getTop() + coords.getHeight()) return; evt.consume(); dragging = true; } /** * Responds to a mouse-drag. Not meant to be called directly. */ public void mouseDragged(MouseEvent evt) { if (!dragging) return; evt.consume(); if (evt.getX() == prevX && evt.getY() == prevY) return; double[] limits = coords.getLimits(); if (limits == null) return; double xOffset = (evt.getX() - prevX) * coords.getPixelWidth(); double yOffset = (evt.getY() - prevY) * coords.getPixelHeight(); coords.setLimits(limits[0] - xOffset, limits[1] - xOffset, limits[2] + yOffset, limits[3] + yOffset); needsRedraw(); if (onUserAction != null) onUserAction.compute(); prevX = evt.getX(); prevY = evt.getY(); } /** * Responds to a mouse-release. Not meant to be called directly. */ public void mouseReleased(MouseEvent evt) { if (!dragging) return; evt.consume(); mouseDragged(evt); dragging = false; if (onFinishDrag != null) onFinishDrag.compute(); } /** * Responds to a mouse-click. Not meant to be called directly. * Defined to be empty in this class. */ public void mouseClicked(MouseEvent evt) { } /** * Responds when mouse moves. Not meant to be called directly. * Defined to be empty in this class. */ public void mouseMoved(MouseEvent evt) { } /** * Responds to a mouse-enter event. Not meant to be called directly. * Defined to be empty in this class. */ public void mouseEntered(MouseEvent evt) { } /** * Responds to a mouse-exit event. Not meant to be called directly. * Defined to be empty in this class. */ public void mouseExited(MouseEvent evt) { } } // end class Panner jcm1-source/edu/hws/jcm/draw/ParametricCurve.java0000644000076500011320000004322011741343635021233 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; import java.awt.*; import java.util.Vector; /** * A ParametricCurve is defined by two functions, x(t) and y(t) of a variable, t, * for t in a specified interval. The curve is simply defined as a sequence of line * segments connecting points of the form (x(t),y(t)), except where one of the functions * is undefined. Also, in some cases a * discontinuity will be detected and no line will be drawn between two of the points. */ public class ParametricCurve extends Drawable implements Computable { private Function xFunc, yFunc; //The functions of t that are graphed. private Color graphColor = Color.magenta; //Color of the graph. private boolean changed; // Used internally to indicate that data has to be recomputed. private transient int[] xcoord, ycoord; //points on graph; xcoord[i] == Integer.MIN_VALUE //for points where a gap occurs. private Value tmin, tmax; // Value objects giving the minimum and maximum value of t. private Value intervals; // Value object giving the number of intervals into which the // interval (tmin,tmax) is to be divided. private double tmin_val, tmax_val; // The values of tmin and tmax. // (tmin_val is set to Double.NaN if any of the values are bad, and nothing is drawn.) private int intervals_val; // The value of intervals. /** * Create a ParametricCurve with nothing to graph. The functions and other values * can be set later. */ public ParametricCurve() { this(null,null,null,null,null); } /** * Create a parametric curve with x and y coordinates given by the specified functions * of the parameter t. Defaults values are used for tmin, tmax, and the number of intervals. * If either function is null, nothing is drawn. */ public ParametricCurve(Function xFunc, Function yFunc) { this(xFunc,yFunc,null,null,null); } /** * Create a parametric curve with the specified values. * * @param xFunc A Function of one variable giving the x-coordinate of points on the curve. If this * is null, then nothing will be drawn. * @param yFunc A Function of one variable giving the y-coordinate of points on the curve. If this * is null, then nothing will be drawn. * @param tmin A Value object giving one endpoint of the domain of the parameter. If this is null, * the default value -5 is used. * @param tmax A Value object giving the second endpoint of the domain of the parameter. If this is null, * the default value 5 is used. Note that it is not required that tmax be greater than tmin. * @param intervals A Value object giving the number of intervals into which the domain is subdivided. * If this is null, the default value 200 is used. The number of points on the curve will be * the number of intervals plus one (unless a function is undefined at some value of the parameter * or if a discontinuity is detected). The number of intervals is clamped to the range 1 to 10000. * Values outside this range would certainly be unreasonable. */ public ParametricCurve(Function xFunc, Function yFunc, Value tmin, Value tmax, Value intevals) { if ( (xFunc != null && xFunc.getArity() != 1) || (yFunc != null && yFunc.getArity() != 1) ) throw new IllegalArgumentException("Internal Error: The functions that define a parametric curve must be functions of one variable."); this.xFunc = xFunc; this.yFunc = yFunc; this.tmin = tmin; this.tmax = tmax; this.intervals = intervals; changed = true; } /** * Set the color to be used for drawing the graph. */ public void setColor(Color c) { if (c != null & !c.equals(graphColor)) { graphColor = c; needsRedraw(); } } /** * Get the color that is used to draw the graph. */ public Color getColor() { return graphColor; } /** * Sets the functions that gives the coordinates of the curve to be graphed. If either function is * null, then nothing is drawn. If non-null, each function must be a function of one variable. */ synchronized public void setFunctions(Function x, Function y) { setXFunction(x); setYFunction(y); } /** * Set the function that gives the x-coordinate of the curve to be graphed. If this is * null, then nothing is drawn. If non-null, it must be a function of one variable. */ synchronized public void setXFunction(Function x) { if (x != null && x.getArity() != 1) throw new IllegalArgumentException("Internal Error: ParametricCurve can only graph functions of one variable."); if (x != xFunc) { xFunc = x; changed = true; needsRedraw(); } } /** * Set the function that gives the y-coordinate of the curve to be graphed. If this is * null, then nothing is drawn. If non-null, it must be a function of one variable. */ synchronized public void setYFunction(Function y) { if (y != null && y.getArity() != 1) throw new IllegalArgumentException("Internal Error: ParametricCurve can only graph functions of one variable."); if (y != yFunc) { yFunc = y; changed = true; needsRedraw(); } } /** * Get the (possibly null) function that gives the x-coordinate of the curve. */ public Function getXFunction() { return xFunc; } /** * Get the (possibly null) function that gives the y-coordinate of the curve. */ public Function getYFunction() { return yFunc; } /** * Specify the number of subintervals into which the domain of the parametric curve is divided. * The interval (tmin,tmax) is divided into subintervals. X and y coordinates of the parametric curve * are computed at each endpoint of these subintervals, and then the points are connected by lines. * If the parameter of this function is null, or if no interval count is ever specified, then a * default value of 200 is used. */ public void setIntervals(Value intervalCount) { intervals = intervalCount; changed = true; } /** * Get the value object, possibly null, that determines the number of points on the curve. */ public Value getIntervals() { return intervals; } /** * Set the Value objects that specify the domain of the paratmeter. */ public void setLimits(Value tmin, Value tmax) { setTMin(tmin); setTMax(tmax); } /** * Get the Value object, possibly null, that gives the left endpoint of the domain of the parameter. */ public Value getTMin() { return tmin; } /** * Get the Value object, possibly null, that gives the right endpoint of the domain of the parameter. */ public Value getTMax() { return tmax; } /** * Set the Value object that gives the left endpoint of the domain of the parameter. If this is null, * then a default value of -5 is used for the left endpoint. (Note: actually, it's not required that * tmin be less than tmax, so this might really be the "right" endpoint.) */ public void setTMin(Value tmin) { this.tmin = tmin; changed = true; } /** * Set the Value object that gives the right endpoint of the domain of the parameter. If this is null, * then a default value of 5 is used for the right endpoint. (Note: actually, it's not required that * tmin be less than tmax, so this might really be the "left" endpoint.) */ public void setTMax(Value tmax) { this.tmax = tmax; changed = false; } //------------------ Implementation details ----------------------------- /** * Recompute data for the graph and make sure that the area of the display canvas * that shows the graph is redrawn. This method is ordinarily called by a * Controller. */ synchronized public void compute() { setup(); needsRedraw(); changed = false; } /** * Draw the graph (possibly recomputing the data if the CoordinateRect has changed). * This is not usually called directly. * */ synchronized public void draw(Graphics g, boolean coordsChanged) { if (changed || coordsChanged || xcoord == null || ycoord == null) { setup(); changed = false; } if (xcoord == null || xcoord.length == 0) return; g.setColor(graphColor); int x = xcoord[0]; int y = ycoord[0]; for (int i = 1; i < xcoord.length; i++) { if (xcoord[i] == Integer.MIN_VALUE) { do { i++; } while (i < xcoord.length && xcoord[i] == Integer.MIN_VALUE); if (i < xcoord.length) { x = xcoord[i]; y = ycoord[i]; } } else { int x2 = xcoord[i]; int y2 = ycoord[i]; g.drawLine(x,y,x2,y2); x = x2; y = y2; } } } // ------------------------- Computing the points on the graph ----------------------- private double[] v = new double[1]; private Cases case1x = new Cases(); private Cases case2x = new Cases(); private Cases case1y = new Cases(); private Cases case2y = new Cases(); private Cases case3x = new Cases(); private Cases case3y = new Cases(); private Vector points = new Vector(250); private Point eval(double t, Cases xcases, Cases ycases) { v[0] = t; if (xcases != null) xcases.clear(); if (ycases != null) ycases.clear(); double x = xFunc.getValueWithCases(v,xcases); double y = yFunc.getValueWithCases(v,ycases); if (Double.isNaN(x) || Double.isNaN(y)) return null; int xInt = coords.xToPixel(x); int yInt = coords.yToPixel(y); if (Math.abs(xInt) > 10000 || Math.abs(yInt) > 10000) return null; return new Point(xInt,yInt); } private void setup() { if (xFunc == null || yFunc == null || coords == null) { xcoord = ycoord = new int[0]; // Nothing will be drawn return; } double intervals_val_d; if (tmin == null) tmin_val = -5; else tmin_val = tmin.getVal(); if (tmax == null) tmax_val = 5; else tmax_val = tmax.getVal(); if (intervals == null) intervals_val_d = 200; else intervals_val_d = intervals.getVal(); if (Double.isInfinite(tmin_val) || Double.isInfinite(tmax_val) || Double.isInfinite(intervals_val_d) || Double.isNaN(tmax_val) || Double.isNaN(intervals_val_d)) tmin_val = Double.NaN; // Signal that data is bad, so nothing will be drawn. if (intervals_val_d < 1) intervals_val = 1; else if (intervals_val > 10000) intervals_val = 10000; else intervals_val = (int)Math.round(intervals_val_d); if (Double.isNaN(tmin_val)) { // data is bad, don't draw xcoord = ycoord = new int[0]; return; } points.setSize(0); double delta = (tmax_val - tmin_val) / intervals_val; double prevx, prevy, x, y, lastT; Point point, prevpoint; double t = tmin_val; prevpoint = eval(t,case1x,case1y); if (prevpoint != null) points.addElement(prevpoint); for (int i = 1; i <= intervals_val; i++) { t = tmin_val + i * delta; point = eval(t,case2x,case2y); if (point != null && prevpoint != null) { if (!case1x.equals(case2x) || !case1y.equals(case2y)) // A discontinuity between two "onscreen" points. discontinuity(prevpoint,tmin_val+(i-1)*delta,point,t,0); else points.addElement(point); } else if (prevpoint == null && point != null) { becomesDefined(prevpoint,tmin_val+(i-1)*delta,point,t,0); } else if (prevpoint != null && point == null) { becomesUndefined(prevpoint,tmin_val+(i-1)*delta,point,t,0); } prevpoint = point; Cases temp = case1x; case1x = case2x; case2x = temp; temp = case1y; case1y = case2y; case2y = temp; } // end for xcoord = new int[points.size()]; ycoord = new int[points.size()]; for (int i = 0; i < ycoord.length; i++) { Point p = (Point)points.elementAt(i); xcoord[i] = p.x; ycoord[i] = p.y; } } // end setup(); private static int MAXDEPTH = 10; // maximum depth of recursion in the next three methods. void discontinuity(Point p1, double t1, Point p2, double t2, int depth) { // Both p1 and p2 are non-null; "cases" data at these two points does not agree; // Original point p1 (from case depth=1) is in points vector. Case data for p1 and p2 // is contained in case1x,case1y and case2x,case2y respectively. if (depth >= MAXDEPTH || (Math.abs(p1.x - p2.x) < 2 && Math.abs(p1.y - p2.y) < 2)) { if (points.elementAt(points.size()-1) != p1) points.addElement(p1); if (depth >= MAXDEPTH) points.addElement(new Point(Integer.MIN_VALUE,0)); points.addElement(p2); return; } double t = (t1+t2)/2; Point p = eval(t,case3x,case3y); if (p == null) { becomesUndefined(p1,t1,p,t,depth+1); becomesDefined(p,t,p2,t2,depth+1); } else if (case3x.equals(case1x) && case3y.equals(case1y)) { discontinuity(p,t,p2,t2,depth+1); } else if (case3x.equals(case2x) && case3y.equals(case2y)) { discontinuity(p1,t1,p,t,depth+1); } else { discontinuity(p1,t1,p,t,depth+2); discontinuity(p,t,p2,t2,depth+2); } } void becomesUndefined(Point p1, double t1, Point p2, double t2, int depth) { // p1 is non-null; p2 is null. Original point p1 is in points vector. if (depth >= MAXDEPTH) { if (points.elementAt(points.size()-1) != p1) points.addElement(p1); points.addElement(new Point(Integer.MIN_VALUE,0)); return; } double t = (t1+t2)/2; Point p = eval(t,null,null); if (p == null) becomesUndefined(p1,t1,p,t,depth+1); else becomesUndefined(p,t,p2,t2,depth+1); } void becomesDefined(Point p1, double t1, Point p2, double t2, int depth) { // p1 is null; p2 is non-null if (depth >= MAXDEPTH) { if (points.size() > 0) points.addElement(new Point(Integer.MIN_VALUE,0)); points.addElement(p2); return; } double t = (t1+t2)/2; Point p = eval(t,null,null); if (p != null) becomesDefined(p1,t1,p,t,depth+1); else becomesDefined(p,t,p2,t2,depth+1); } } // end class ParametricCurve jcm1-source/edu/hws/jcm/draw/DraggablePoint.java0000644000076500011320000004376511741343635021037 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.*; import java.awt.event.*; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; /** * A DraggablePoint can be added to a DisplayCanvas, where it appears as a small disk, square, or * cross. (The visual style is a settable property.) This object can be dragged with the mouse, * within the limits of the CoordinateRect that contains the DraggablePoint. Either the x- or * y-value of the point can be clamped to a specified Value. Typically, the y-value might * be given by some function of the x-value. In that case, the point is constrained to move * along the graph of the function. Or the x- or y-value can be clamped to a constant to make * the point move along a vertical or horizontal line. Two Variables are associated with * the DraggablePoint. These Variables represent the x- and y- values of the point. Each Variable * implements the Tieable interface, so it can be synchronized with other Tieable values such as * a VariableIput or VariableSlider. */ public class DraggablePoint extends Drawable implements InputObject, Draggable { /** * A style constant that specifies the visual appearance of a DraggablePoint to be a disk. */ public static final int DISK = 0; /** * A style constant that specifies the visual appearance of a DraggablePoint to be a square. */ public static final int SQUARE = 1; /** * A style constant that specifies the visual appearance of a DraggablePoint to be a cross. */ public static final int CROSS = 2; private int radius; // Radius of the point. private Color color; // Color of the point. private Color ghostColor; // Color used for point when it is undefined or outside the CoordinateRect. private int style; // One of the above style constants, DISK by default. private double xLoc, yLoc; // The current x- and y-values of the point. private int xPosition, yPosition; // The pixel position of the point. private boolean useGhost; // This is true if the point is a "ghost" (undefined or outside the CoordinateRect). private DPV xVar, yVar; // The Variables that represent the x- and y-values; DPV is a private nested class, defined below. private Controller onUserAction; // A Controller whose compute method is called when the user drags the point. private Value clampX, clampY; // Values used to clamp the x- and y-values. Only one can be non-null. /** * Create a DraggablePoint with default values for style, radius, color. The point appears as a dark gray disk of radius 4. */ public DraggablePoint() { this(DISK); } /** * Create a DraggablePoint with specified visual style. Radius is 4, color is darkGray, and * ghostColor is lightGray. * * @param style One of the style constants DraggablePoint.DISK, DraggablePoint.SQUARE, or DraggablePoint.CROSS. */ public DraggablePoint(int style) { if (style >= 0 && style <= 2) this.style = style; setColor(Color.darkGray); setGhostColor(Color.lightGray); radius = 4; xPosition = -10000; xLoc = Double.NaN; yLoc = Double.NaN; xVar = new DPV(true); yVar = new DPV(false); } /** * Clamp the x-value of the point to v. That is, if v is not null, then whenever the location of the point * changes, its x-value is modified to v.getVal(). Note that if v is non-null then any clamp Value * specified for y will be cleared since x and y cannot both be clamped. */ public void clampX(Value v) { clampX = v; if (v != null) clampY = null; checkClamp(); needsRedraw(); } /** * Clamp the y-value of the point to v. That is, if v is not null, then whenever the location of the point * changes, its y-value is modified to v.getVal(). Note that if v is non-null then any clamp Value * specified for x will be cleared since x and y cannot both be clamped. */ public void clampY(Value v) { clampY = v; if (v != null) clampX = null; checkClamp(); needsRedraw(); } /** * Clamp the x-value of the point to the constant x, so that the point is constrained to a vertical line. */ public void clampX(double x) { clampX(new Constant(x)); } /** * Clamp the y-value of the point to the constant y, so that the point is constrained to a horizontal line. */ public void clampY(double y) { clampY(new Constant(y)); } /** * Clamp the x-value of the point to the function f, so that the point is constrained to move along the graph of x = f(y). * f must be a function of one variable. */ public void clampX(Function f) { if (f != null) clampX(new ValueMath(f,xVar)); } /** * Clamp the y-value of the point to the function f, so that the point is constrained to move along the graph of y = f(x). * f must be a function of one variable. */ public void clampY(Function f) { if (f != null) clampY(new ValueMath(f,xVar)); } /** * Get the radius used for drawing the point. The point's height and width are given by two times the radius. */ public int getRadius() { return radius; } /** * Set the radius that determines the size of the point when it is drawn. * The point's height and width are given by two times the radius. */ public void setRadius(int r) { if (r > 0) { radius = r; needsRedraw(); } } /** * Set the visual style of the point. The style should be one of the constants * DraggablePoint.DISK, DraggablePoint.SQUARE, or DraggablePoint.CROSS. If it is not, * then nothing is done. */ public void setStyle(int style) { if (style >= 0 && style <= 2) { this.style = style; needsRedraw(); } } /** * Get the visual style of the point, which must be one of the constants * DraggablePoint.DISK, DraggablePoint.SQUARE, or DraggablePoint.CROSS. */ public int getStyle() { return style; } /** * Get the variable that represents the current x-value of the point. (Note that this * variable can be type-cast to type Tieable.) */ public Variable getXVar() { return xVar; } /** * Get the variable that represents the current y-value of the point. (Note that this * variable can be type-cast to type Tieable.) */ public Variable getYVar() { return yVar; } /** * Get the color used for drawing the point. */ public Color getColor() { return color; } /** * Set the color to be used for drawing the point. If the specified Color value is * null, then nothing is done. */ public void setColor(Color c) { if (c != null) { color = c; needsRedraw(); } } /** * Get the "ghostColor" of the point. This color is used for drawing the point when its x-value * or y-value is undefined or outside the range of values on the CoordinateRect that contains * the point. (This can happen because of clamping of values. It can also happen if the limits * on the CoordinateRect are changed.) */ public Color getGhostColor() { return ghostColor; } /** * Set the ghoseColor to be used for drawing the point when it location is undefined or is outside the * proper limits. If the specified Color value is null, then nothing is done. */ public void setGhostColor(Color c) { if (c != null) { ghostColor = c; needsRedraw(); } } /** * Set the Controller that is to be notified when the user drags the point. (The compute() method * of the Controller is called.) If the Controller value is null, then no notification is done. */ public void setOnUserAction(Controller c) { onUserAction = c; } /** * Method required by InputObject interface; in this class, it simply calls * setOnUserAction(c). This is meant to be called by JCMPanel.gatherInputs(). */ public void notifyControllerOnChange(Controller c) { setOnUserAction(c); } /** * Get the Controller that is notified when the user drags the point. A null value means that * no notification is done. */ public Controller getOnUserAction(Controller c) { return onUserAction; } /** * Move the point to (x,y), then "clamp" the value of x or y, if a clamp Value has been set. */ public void setLocation(double x, double y) { xLoc = x; yLoc = y; xVar.setVariableValue(x); yVar.setVariableValue(y); xVar.serialNumber++; yVar.serialNumber++; checkClamp(); needsRedraw(); } private void checkClamp() { // Apply the clamping values. if (clampX != null) { xLoc = clampX.getVal(); xVar.setVariableValue(xLoc); } else if (clampY != null) { yLoc = clampY.getVal(); yVar.setVariableValue(yLoc); } } /** * This method is required by the InputObject interface. In this case, it just applies the * clamping Values if any are specified. */ public void checkInput() { xVar.needsClamp = true; yVar.needsClamp = true; } /** * This method, from the Drawable interface, draws the point. It is not usually called directly. */ public void draw(Graphics g, boolean coordsChanged) { if (coords == null) return; checkPosition(); if (useGhost) g.setColor(getGhostColor()); else g.setColor(color); switch (style) { case DISK: g.fillOval(xPosition-radius,yPosition-radius,2*radius+1,2*radius+1); break; case SQUARE: g.fillRect(xPosition-radius,yPosition-radius,2*radius+1,2*radius+1); break; case CROSS: g.drawLine(xPosition-radius,yPosition,xPosition+radius,yPosition); g.drawLine(xPosition,yPosition-radius,xPosition,yPosition+radius); break; } } private void checkPosition() { // compute (xPosition, yPosition), the position where point is actually drawn useGhost = false; xVar.getVal(); // Forces recompute, if needsClamp yVar.getVal(); if (Double.isNaN(xLoc) || Double.isNaN(yLoc)) { if (xPosition == -10000) { // otherwise, use previous position xPosition = coords.getLeft() + coords.getWidth()/2; yPosition = coords.getTop() + coords.getHeight()/2; } useGhost = true; } else { xPosition = coords.xToPixel(xLoc); yPosition = coords.yToPixel(yLoc); } if (xPosition <= coords.getLeft()) { useGhost = true; xPosition = coords.getLeft() + 1; } else if (xPosition >= coords.getLeft() + coords.getWidth()) { useGhost = true; xPosition = coords.getLeft() + coords.getWidth() - 1; } if (yPosition <= coords.getTop()) { useGhost = true; yPosition = coords.getTop() + 1; } else if (yPosition >= coords.getTop() + coords.getHeight()) { useGhost = true; yPosition = coords.getTop() + coords.getHeight() - 1; } } //------------------ Dragging the point --------------------------- private boolean dragging; // True if the point is being dragged. /** * Check whether a mouse click (as specified in the MouseEvent parameter) is a * click on this DraggablePoint. If so, return true, and start a drag operation. * It is expected that the continueDrag() and finishDrag() will be called to * complete the drag operation. This is only meant to be called from * the checkDraggables() method in class CoordinateRect. */ public boolean startDrag(MouseEvent evt) { dragging = false; if (evt.isConsumed() || !getVisible() || coords == null) return false; checkPosition(); if (evt.getX() < xPosition - radius || evt.getX() >= xPosition + radius || evt.getY() < yPosition - radius || evt.getY() >= yPosition + radius) return false; dragging = true; evt.consume(); return true; } /** * Continue a drag operation begun in startDrag(). This is not meant to be called directly. */ public void continueDrag(MouseEvent evt) { if (!dragging) return; int xInt = evt.getX(); int yInt = evt.getY(); double x = coords.pixelToX(evt.getX()); double y = coords.pixelToY(evt.getY()); if (x < coords.getXmin()) x = coords.getXmin(); else if (x > coords.getXmax()) x = coords.getXmax(); if (y < coords.getYmin()) y = coords.getYmin(); else if (y > coords.getYmax()) y = coords.getYmax(); setLocation(x,y); if (Double.isNaN(xLoc) || Double.isNaN(yLoc)) { xPosition = xInt; yPosition = yInt; } if (onUserAction != null) onUserAction.compute(); } /** * Finish a drag operation begun in startDrag(). This is not meant to be called directly. */ public void finishDrag(MouseEvent evt) { dragging = false; } private class DPV extends Variable implements Tieable { private boolean isXVar; // True for xVar; false for yVar. long serialNumber; // This object's serial number. boolean needsClamp; // Set to true by DraggablePoint().checkInput(). DPV(boolean isXVar) { // Create the variable. super(isXVar? "xDrag" : "yDrag"); this.isXVar = isXVar; super.setVal(Double.NaN); } public double getVal() { // Return the value, after applying clamping, if necessary. // (It's done this way because checkInput() can't use values of // other objects, but after it's called, any call to getVal() // should return the new correct value.) if (needsClamp) { if (isXVar) { if (clampX != null) { xLoc = clampX.getVal(); setVariableValue(xLoc); } } else { if (clampY != null) { yLoc = clampY.getVal(); setVariableValue(yLoc); } } needsClamp = false; } return super.getVal(); } public void setVal(double val) { // Set the value of the variable, and set the point's // location to reflect new value. (setLocation ups serial number // and calls setVariableValue() to set the actual variable value.) if (isXVar) setLocation(val,yVar.getVal()); else setLocation(xVar.getVal(),val); } void setVariableValue(double val) { // Call the setVal() routine from the superclass. super.setVal(val); needsClamp = false; } public long getSerialNumber() { // Return this Tieable object's serial number. return serialNumber; } public void sync(Tie tie, Tieable newest) { // Synchronize values and serial numbers with newest. if ( ! (newest instanceof Value) ) throw new IllegalArgumentException("Internal Error: A MouseTracker variable can only be tied to a Value object."); if (newest != this) { setVal(((Value)newest).getVal()); serialNumber = newest.getSerialNumber(); } } } } // end class DraggablePoint jcm1-source/edu/hws/jcm/draw/DrawTemp.java0000644000076500011320000000622311741343635017664 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.Graphics; /** * An object that implements this interface can draw itself, using information * from a CoordinateRect (in which it presumably appears). This interface is * meant to be used with the method drawTemp() in edu.hws.jcm.draw.DisplayCanvas. */ public interface DrawTemp extends java.io.Serializable { /** * Draw this item in the specified graphics context, possibly using information * from the specified CoordinateRect. Note that the drawTemp() method * in class DisplayCanvas creates a new graphics context every time it * is called, just for drawing this item. */ public void draw(Graphics g, CoordinateRect coords); } jcm1-source/edu/hws/jcm/draw/VectorField.java0000644000076500011320000004225011741343635020347 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; import java.awt.*; import java.util.Vector; /** * A VectorField displays lines or arrows on a grid of points where the direction * and/or lengths are given by two functions (f1(x,y),f2(x,y)). This is probably * more useful as a "direction field" than as a "vector field." * */ public class VectorField extends Drawable implements Computable { /** * One of the possible styles for displaying a VectorField: as a direction field shown as * arrows of equal length. The point where the vector is computed is the tail of the arrow. */ public static final int ARROWS = 0; /** * One of the possible styles for displaying a VectorField: as a direction field shown as * tangent lines. The point where the vector is computed is the center of the line. */ public static final int LINES = 1; /** * One of the possible styles for displaying a VectorField: as a vector field where a vector is shown as * an arrow from (x,y) to (x+xFunc(x,y),y+xFunc(x,y)), except that a maximum length is imposed. */ public static final int CLAMPED_VECTORS = 2; /** * One of the possible styles for displaying a VectorField: as a field of tangent lines where the length * of the line is proportional to the length of the vector. */ public static final int SCALED_LINES = 3; /** * One of the possible styles for displaying a VectorField: as a vector field where a vector is shown as * an arrow with length proportional to the length of the vector. The lengths are scaled so that * the longest arrow has length equal to the grid spacing. */ public static final int SCALED_VECTORS = 4; private int style; // The style in which the vector field is drawn. private Function xFunc, yFunc; // The vector field is (xFunc(x,y),yfunc(x,y)). private Color graphColor = Color.lightGray; //Color of the vectors. private boolean changed; // Used internally to indicate that data has to be recomputed. private transient int[][] data; // Pre-computed data for the vectors. private int pixelSpacing = 30; // Desired number of pixels between grid points, clamped to the range 5 to 200. /** * Create a VectorField object with nothing to graph. The functions and other values * can be set later. The default display style is as a direction field of equal-length arrows. */ public VectorField() { this(null,null,ARROWS); } /** * Create a VectorField that will be displayed using the default style, as a direction field of * equal-length arrows. If either of the functions is null, nothing will be displayed. If non-null, * the functions must be functions of two variables. */ public VectorField(Function xFunc, Function yFunc) { this(xFunc,yFunc,ARROWS); } /** * Create a VectorField with the specified functions and style. * * @param xFunc A Function of two variables giving the x-component of the vector field. If this * is null, then nothing will be drawn. * @param yFunc A Function of two variables giving the y-component of the vector field. If this * is null, then nothing will be drawn. * @param style The style in which the direction field is drawn. This can be one of the constants ARROWS (a direction field of equal-lenth arrows), LINES (equal length lines), CLAMPED_VECTORS (vectors drawn at actual length, unless too long), SCALED_VECTORS (vectors scaled so longest has length equal to the grid spacing), or SCALED_LINES (lines scaled so longest has length equal to the grid spacing). */ public VectorField(Function xFunc, Function yFunc, int style) { if ( (xFunc != null && xFunc.getArity() != 2) || (yFunc != null && yFunc.getArity() != 2) ) throw new IllegalArgumentException("Internal Error: The functions that define a vector must be functions of two variables."); this.xFunc = xFunc; this.yFunc = yFunc; this.style = style; changed = true; } /** * Set the color to be used for drawing the vector field. The default color is light gray. */ public void setColor(Color c) { if (c != null & !c.equals(graphColor)) { graphColor = c; needsRedraw(); } } /** * Get the color that is used to draw the vector field. */ public Color getColor() { return graphColor; } /** * Sets the functions that give the components of the vector field. If either function is * null, then nothing is drawn. If non-null, each function must be a function of two variables. */ synchronized public void setFunctions(Function dx, Function dy) { setXFunction(dx); setYFunction(dy); } /** * Set the function that gives the x-component of the vector field. If this is * null, then nothing is drawn. If non-null, it must be a function of two variables. */ synchronized public void setXFunction(Function dx) { if (dx != null && dx.getArity() != 2) throw new IllegalArgumentException("Internal Error: VectorField can only use functions of two variables."); if (dx != xFunc) { xFunc = dx; changed = true; needsRedraw(); } } /** * Set the function that gives the y-component of the vector field. If this is * null, then nothing is drawn. If non-null, it must be a function of two variables. */ synchronized public void setYFunction(Function dy) { if (dy != null && dy.getArity() != 1) throw new IllegalArgumentException("Internal Error: VectorField can only use functions of two variables."); if (dy != yFunc) { yFunc = dy; changed = true; needsRedraw(); } } /** * Get the (possibly null) function that gives the x-component of the vector field. */ public Function getXFunction() { return xFunc; } /** * Get the (possibly null) function that gives the y-component of the vector field. */ public Function getYFunction() { return yFunc; } /** * Get the style in which the vector field is displayed. */ public int getStyle() { return style; } /** * Set the style in which the vector field is displayed. This should be one of the * constants ARROWS, LINES, CLAMPED_VECTORS, SCALED_LINES, or SCALED_VECTORS. */ public void setStyle(int style) { if (this.style != style) { this.style = style; changed = true; needsRedraw(); } } /** * Get the value of the pixelSpacing property, which determines the grid spacing for the vector field. */ public int getPixelSpacing() { return pixelSpacing; } /** * Set the value of the pixelSpacing property, which determines the grid spacing for the vector field. * The value will be clamped to the range from 5 to 200. The default value is 30. */ public void setPixelSpacing(int spacing) { if (spacing < 5) spacing = 5; else if (spacing > 200) spacing = 200; if (spacing != pixelSpacing) { pixelSpacing = spacing; changed = true; needsRedraw(); } } //------------------ Implementation details ----------------------------- /** * Recompute data for the vector field and make sure that the area of the display canvas * that shows the vector field is redrawn. This method is ordinarily called by a * Controller. */ synchronized public void compute() { setup(); needsRedraw(); changed = false; } /** * Draw the vector field (possibly recomputing the data if the CoordinateRect has changed). * */ synchronized public void draw(Graphics g, boolean coordsChanged) { if (changed || coordsChanged || data == null) { setup(); changed = false; } if (data == null) return; g.setColor(graphColor); boolean arrows = style == ARROWS || style == CLAMPED_VECTORS || style == SCALED_VECTORS; for (int i = 0; i < data.length; i++) { int[] c = data[i]; if (c[0] != Integer.MIN_VALUE) { // Otherwise, vector is undefined g.drawLine(c[0],c[1],c[2],c[3]); if (arrows && c[4] != Integer.MIN_VALUE) { // Otherwise, there is no arrowhead g.drawLine(c[2],c[3],c[4],c[5]); g.drawLine(c[2],c[3],c[6],c[7]); } } } } // ------------------------- Computing the data for the vector field ----------------------- private void setup() { if (xFunc == null || yFunc == null || coords == null) { data = null; // Nothing will be drawn return; } boolean arrows = style == ARROWS || style == CLAMPED_VECTORS || style == SCALED_VECTORS; int xCt, yCt; // number of points in x and y directions. double xStart, yStart; // Starting values for x,y, at lower left corner of grid. double dx, dy; // Change in x and y between grid points. double[] params = new double[2]; xCt = (coords.getWidth()) / pixelSpacing + 2; yCt = (coords.getHeight()) / pixelSpacing + 2; dx = pixelSpacing*coords.getPixelWidth(); dy = pixelSpacing*coords.getPixelHeight(); xStart = (coords.getXmax() + coords.getXmin() - xCt*dx)/2; yStart = (coords.getYmax() + coords.getYmin() - yCt*dy)/2; data = new int[xCt*yCt][arrows? 8 : 4]; double[][] xVec = new double[xCt][yCt]; // Vector field scaled so pixelsize is one unit. double[][] yVec = new double[xCt][yCt]; double pixelWidth = coords.getPixelWidth(); double pixelHeight = coords.getPixelHeight(); double maxLength = 0; for (int i = 0; i < xCt; i++) { double x = xStart + i*dx; params[0] = x; for (int j = 0; j < yCt; j++) { double y = yStart + j*dy; params[1] = y; xVec[i][j] = xFunc.getVal(params); yVec[i][j] = yFunc.getVal(params); if ( ! (Double.isNaN(xVec[i][j]) || Double.isNaN(yVec[i][j]) || Double.isInfinite(xVec[i][j]) || Double.isInfinite(yVec[i][j])) ) { xVec[i][j] = xVec[i][j]/pixelWidth; // size in terms of pixels yVec[i][j] = -yVec[i][j]/pixelHeight; // sign change because pixels are numbered from top down double length = xVec[i][j]*xVec[i][j] + yVec[i][j]*yVec[i][j]; if (length > maxLength) maxLength = length; } } } maxLength = Math.sqrt(maxLength); int ct = 0; // which item of data are we working on? for (int i = 0; i < xCt; i++) { double x = xStart + i*dx; int xInt = coords.xToPixel(x); for (int j = 0; j < yCt; j++) { double y = yStart + j*dy; int yInt = coords.yToPixel(y); int[] d = data[ct]; ct++; if ( Double.isNaN(xVec[i][j]) || Double.isNaN(yVec[i][j]) || Double.isInfinite(xVec[i][j]) || Double.isInfinite(yVec[i][j]) ) { d[i] = Integer.MIN_VALUE; // signal that vector is undefined at this point } else { double length = Math.sqrt(xVec[i][j]*xVec[i][j] + yVec[i][j]*yVec[i][j]); if (length < 1e-15 || (maxLength == 0 && (style == SCALED_LINES || style == SCALED_VECTORS))) { // no arrow. d[0] = d[2] = xInt; d[1] = d[3] = yInt; if (arrows) d[4] = Integer.MIN_VALUE; } else { double sdx, sdy; // dx and dy scaled to a vector of right length double alength; // length of arrow or line boolean clamped = false; switch (style) { case ARROWS: sdx = 0.8 * pixelSpacing * xVec[i][j]/length; sdy = 0.8 * pixelSpacing * yVec[i][j]/length; d[0] = xInt; d[1] = yInt; d[2] = (int)(xInt + sdx); d[3] = (int)(yInt + sdy); break; case LINES: sdx = 0.8 * pixelSpacing * xVec[i][j]/length/2; sdy = 0.8 * pixelSpacing * yVec[i][j]/length/2; d[0] = (int)(xInt - sdx); d[1] = (int)(yInt - sdy); d[2] = (int)(xInt + sdx); d[3] = (int)(yInt + sdy); break; case CLAMPED_VECTORS: alength = length; if (alength > 0.9*pixelSpacing) { alength = 0.9*pixelSpacing; clamped = true; } sdx = xVec[i][j]/length*alength; sdy = yVec[i][j]/length*alength; d[0] = xInt; d[1] = yInt; d[2] = (int)(xInt + sdx); d[3] = (int)(yInt + sdy); break; case SCALED_LINES: alength = (length/maxLength)*pixelSpacing; sdx = xVec[i][j]/length*alength/2; sdy = yVec[i][j]/length*alength/2; d[0] = (int)(xInt - sdx); d[1] = (int)(yInt - sdy); d[2] = (int)(xInt + sdx); d[3] = (int)(yInt + sdy); break; case SCALED_VECTORS: alength = (length/maxLength)*pixelSpacing; sdx = xVec[i][j]/length*alength; sdy = yVec[i][j]/length*alength; d[0] = xInt; d[1] = yInt; d[2] = (int)(xInt + sdx); d[3] = (int)(yInt + sdy); break; } if (arrows) { // add an arrowhead int d1 = (d[2] - d[0])/5; int d2 = (d[3] - d[1])/5; if (clamped || d1 == 0 && d2 == 0) d[4] = Integer.MIN_VALUE; // no arrowhead else { d[4] = d[2] + d2 - d1; d[5] = d[3] - d1 - d2; d[6] = d[2] - d1 - d2; d[7] = d[3] + d1 - d2; } } } } } } } // end setup() } // end class VectorField jcm1-source/edu/hws/jcm/draw/ScatterPlot.java0000644000076500011320000005273011741343635020411 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.*; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; /** * A ScatterPlot graphs data taken from a DataTableInput. The data * in the form of a small plus sign at each (x,y) in the data. * The x and y values can be taken directly from two specified * columns in the table. They can also be computed by expressions * that can use column names from the table as well as the special * variable rowNumber. For example, if column names are X and Y, * then it could plot sqrt(X) versus rowNumber*(X+Y). *

Optionally, a ScatterPlot will also draw a regression line * for the data. Certain statistical values about the data points * are available as Value objects by calling the getValueObject() * method. */ public class ScatterPlot extends Drawable implements Computable { /** * A constant that can be used in the getValueObject() method to * indicate which statistic the object should represent. */ public static final int INTERCEPT = 0, SLOPE = 1, DATACT = 2, MISSINGCT = 3, STANDARDERROR = 4, CORRELATION = 5; private DataTableInput table; // The table from which the data for the plot is taken private long lastTableSN; // serial number from table when getData() was last done private boolean autoChangeLimits = true; // If true, then the limits on the coords will // be changed so that the data pretty much // fills the coordinate rect. private int column1, column2; // Column numbers that specify which columns from // the table will be plotted. These are ignored // if exp1 and exp2 are non-null. private Expression exp1, exp2; // Expressions that give data to be plotted, or // null if column numbers are to be used. private boolean showRegressionLine = true; // If true, a regression is drawn private boolean missingValueIsError = true; // If true and if any of the data values is Double.NaN, // then an error is thrown. private double slope=Double.NaN; // Values of statistics. private double intercept=Double.NaN; private int dataCt; private int missingCt; private double correlation=Double.NaN; private double standardError=Double.NaN; private double[][] data; // The actual data values to be drawn (computed in getData()) private Color lineColor = Color.black; // Color of regression line. private Color dataColor = Color.red; // Color of data points. private static final int crossHalfSize = 2; // Size of one arm of the plus sign that is drawn // to represent a data point. /** * Default constructor. A data table, at least, must be specified before anything can be drawn. * The first two columns of the table will be plotted (once a table is specified). */ public ScatterPlot() { this(null,0,1); } /** * Create a scatter plot to plot data from the specified table. Initially, it is configured * to plot data from the first two columns in the table. */ public ScatterPlot(DataTableInput table) { this(table,0,1); } /** * Create a scatter plot to plot data from two specified columns in a table. * Columns are numbered starting from zero. */ public ScatterPlot(DataTableInput table, int column1, int column2) { this.table = table; this.column1 = column1; this.column2 = column2; } /** * Create scatter plot to plot specified expressions using data from a table. * The expressions should include references to the column names from the table * and can also refer to the special variable "rowNumber". */ public ScatterPlot(DataTableInput table, Expression exp1, Expression exp2) { this.table = table; this.exp1 = exp1; this.exp2 = exp2; column1 = 0; column2 = 1; } /** * Specify the table from which the plotted data is to be taken. The data from the * first two columns of the table will be plotted, unless this is changed by * calling setColumns() or setExpressions(). */ public void setTable(DataTableInput table) { if (table == this.table) return; this.table = table; lastTableSN = 0; column1 = 0; column2 = 1; checkData(); } /** * Get the DataTableInput from which the plotted data is obtained. */ public DataTableInput getTable() { return table; } /** * Specify that the data to be plotted should be taken from the specified * columns in the table. Note that columns are numbered starting from zero. * The parameters must be within the range of column numbers in the table. */ public void setColumns(int c1, int c2) { column1 = c1; column2 = c2; exp1 = exp2 = null; lastTableSN = 0; // force checkData to recompute checkData(); } /** * Specify the data for the the plot is to be obtained by evaluating * the two expressions that are given as parameters. Both expressions * should be non-null. The expressions can only be created by a Parser * to which the variables from the table have been added by calling * the method DataTableInput.addVariablesToParser(). The expressions * are evaluated once for each row in the table to obtain the data to be ploted. They can include * references to the column names from the table and to the special * variable "rowNumber", which represents the number of the current row. */ public void setExpressions(Expression exp1, Expression exp2) { this.exp1 = exp1; this.exp2 = exp2; lastTableSN = 0; // force checkData to recompute checkData(); } /** * If the parameter is true, then a regression line for the data is drawn. * The default value is true. */ public void setShowRegressionLine(boolean line) { if (line != showRegressionLine) { showRegressionLine = line; needsRedraw(); } } /** * The return value tells whether a regression line is drawn. */ public boolean getShowRegressionLine() { return showRegressionLine; } /** * If the parameter is true, then a missing data value (an empty cell or * an undefined value for one of the expressions) is considered to be an * error, and a JCMError is thrown when it is encountered. If the value is * false, missing data are ignored, and the value of MISSINGCT gives the * number of points for which the data was missing. Note that invalid * data (a cell that contains text that does not represent a number) is * always considered to be an error. Also note that completely empty * rows at the bottom of a DataTableInput are always ignored and are * never considered to be an error. * The default value of missingValueIsError is true, so that missing data * is considered to be an error unless you turn off this option. */ public void setMissingValueIsError(boolean isError) { missingValueIsError = isError; } /** * The return value indicates whether missing data is considered to be * an error. */ public boolean getMissingValueIsError() { return missingValueIsError; } /** * If the parameter is true, then the limits on the CoordinateRect that contains * this ScatterPlot are automatically adjusted whenever the data is recomputed. * The default value is true. */ public void setAutoChangeLimits(boolean set) { autoChangeLimits = set; } /** * The return value indicates whether the limits on the CoordinateRect are * automatically adjusted when the data is recomputed. */ public boolean getAutoChangeLimits() { return autoChangeLimits; } /** * Get the color that is used to draw the points in the data. */ public Color getDataColor() { return dataColor; } /** * Set the color that is used to draw the points in the data. * The default value is red. */ public void setDataColor(Color color) { if (color != null) dataColor = color; } /** * Get the color that is used to draw the regression line. */ public Color getLineColor() { return lineColor; } /** * Set the color that is used to draw the regression line. * The default value is black. */ public void setLineColor(Color color) { if (color != null) lineColor = color; } /** * Get a Value that represents a statistic about the data that is shown * in the scatter plot. The parameter specifies which statistic is represented. * It can be one of the constants defined in this class: SLOPE (of regression line), * INTERCEPT (y-intercept of regression line), DATACT (number of data points), * MISSINGCT (number of missing data; alwasy zero if the missingValueIsError property * is true), STANDARDERROR (standard error of regression line), and CORRELATION * (correlation coefficient between first and second coordintes of data points). */ public Value getValueObject(int valueCode) { if (valueCode < 0 || valueCode > 5) throw new IllegalArgumentException("Unknown code (" + valueCode + ") for type of value object."); return new SPV(valueCode); } /** * Check data from table and recompute everything if it has changed. */ private void checkData() { if (table != null && lastTableSN == table.getSerialNumber()) return; try { compute(); } catch (JCMError e) { canvas.setErrorMessage(null,e.getMessage()); } } /** * Recompute the data for the scatter plot. This is generally * not called directly. */ public void compute() { double[] desiredLimits = getData(); if (table != null) lastTableSN = table.getSerialNumber(); if (desiredLimits == null || !needsNewLimits(desiredLimits,coords)) needsRedraw(); else coords.setLimits(desiredLimits); } /** * Draw the data points and regression line. Not meant to be called directly. */ public void draw(Graphics g, boolean coordsChanged) { g.setColor(dataColor); if (table == null) { g.drawString("No table has been specified.",20,27); return; } if (column1 < 0 || column1 >= table.getColumnCount() || column2 < 0 || column2 >= table.getColumnCount()) { g.drawString("Illegal column numbers.",20,27); return; } if (data == null || data.length == 0) { g.drawString("No data available.",20,27); return; } checkData(); for (int i = 0; i < data.length; i++) { int x = coords.xToPixel(data[i][0]); int y = coords.yToPixel(data[i][1]); g.drawLine(x-crossHalfSize,y,x+crossHalfSize,y); g.drawLine(x,y-crossHalfSize,x,y+crossHalfSize); } if (showRegressionLine && !Double.isNaN(slope)) { g.setColor(lineColor); if (Double.isInfinite(slope)) { int x = coords.xToPixel(data[0][0]); g.drawLine(x,coords.getTop(),x,coords.getTop()+coords.getHeight()); } else { double x1 = coords.pixelToX(coords.getLeft()); double x2 = coords.pixelToX(coords.getLeft()+coords.getWidth()); double y1 = slope*x1 + intercept; double y2 = slope*x2 + intercept; g.drawLine(coords.xToPixel(x1), coords.yToPixel(y1)-1, coords.xToPixel(x2), coords.yToPixel(y2)-1); } } } /** * Get the data for the plot, and recompute the statistics. * Also, compute the appropriate limits for the CoordinateRect. * The return value represents these limits. */ private double[] getData() { int rows = (table == null)? 0 : table.getNonEmptyRowCount(); double[] desiredLimits = null; if ( table == null || rows == 0 || ( (exp1 == null || exp2 == null) && (column1 < 0 || column1 >= table.getColumnCount() || column2 < 0 || column2 >= table.getColumnCount()) ) ) { data = new double[0][2]; dataCt = 0; missingCt = 0; slope = Double.NaN; intercept = Double.NaN; correlation = Double.NaN; standardError = Double.NaN; return null; } data = new double[rows][2]; dataCt = 0; missingCt= 0; if (exp1 == null || exp2 == null) { for (int i = 0; i < rows; i++) { double x = table.getCellContents(i+1,column1); double y = table.getCellContents(i+1,column2); if (Double.isNaN(x) || Double.isNaN(y) || Double.isInfinite(x) || Double.isInfinite(y)) { if (missingValueIsError) throw new JCMError("Missing data in row " + table.getCurrentRowNumber() + " of table.", this); missingCt++; } else { data[dataCt][0] = x; data[dataCt][1] = y; dataCt++; } } } else { for (int i = 0; i < rows; i++) { table.setCurrentRowNumber(i+1); double x = exp1.getVal(); double y = exp2.getVal(); if (Double.isNaN(x) || Double.isNaN(y) || Double.isInfinite(x) || Double.isInfinite(y)) { if (missingValueIsError) throw new JCMError("Missing data or undefined expression value for row " + table.getCurrentRowNumber() + " of table.", this); missingCt++; } else { data[dataCt][0] = x; data[dataCt][1] = y; dataCt++; } } } if (dataCt < data.length) { double[][] d = new double[dataCt][2]; for (int i = 0; i < dataCt; i++) d[i] = data[i]; data = d; } getRegressionStats(); if (autoChangeLimits) desiredLimits = computeDesiredLimits(); return desiredLimits; } private void getRegressionStats() { // Compute statistics, based on data in data array. if (dataCt == 0) { slope = intercept = correlation = standardError = Double.NaN; return; } boolean allSameX = true, allSameY = true; double sumx = data[0][0], sumy = data[0][1], sumxy = data[0][0]*data[0][1], sumx2 = data[0][0]*data[0][0], sumy2 = data[0][1]*data[0][1]; for (int i = 1; i < dataCt; i++) { if (data[0][0] != data[i][0]) allSameX = false; if (data[0][1] != data[i][1]) allSameY = false; sumx += data[i][0]; sumy += data[i][1]; sumxy += data[i][0] * data[i][1]; sumx2 += data[i][0] * data[i][0]; sumy2 += data[i][1] * data[i][1]; } double denomx = dataCt * sumx2 - sumx*sumx; double denomy = dataCt * sumy2 - sumy*sumy; double numer = dataCt * sumxy - sumx*sumy; if (allSameX && allSameY) { slope = 0; intercept = data[0][1]; correlation = standardError = Double.NaN; } else if (allSameX) { slope = Double.POSITIVE_INFINITY; intercept = correlation = standardError = Double.NaN; } else if (denomx == 0) { slope = intercept = correlation = standardError = Double.NaN; } else { slope = numer / denomx; intercept = (sumy - slope * sumx) / dataCt; if (denomy == 0) correlation = Double.NaN; else correlation = numer / Math.sqrt(denomx*denomy); if (dataCt <= 2) standardError = Double.NaN; else { double sum = 0; for (int i = 0; i < dataCt; i++) { double x = data[i][1] - (slope*data[i][0] + intercept); sum += x*x; } standardError = Math.sqrt(sum/(dataCt-2)); } } } private double[] computeDesiredLimits() { // Compute desired limits, based on data in data array if (data.length == 0) return null; double xmin=Double.MAX_VALUE, xmax=-Double.MAX_VALUE, ymin=Double.MAX_VALUE, ymax=-Double.MAX_VALUE; for (int i = 0; i < dataCt; i++) { double x = data[i][0]; double y = data[i][1]; if (x > xmax) xmax = x; if (x < xmin) xmin = x; if (y > ymax) ymax = y; if (y < ymin) ymin = y; } if (xmin > 0 && (xmax - xmin) > xmax/2) xmin = 0; if (ymin > 0 && (ymax - ymin) > ymax/2) ymin = 0; if (ymax < 0) ymax = 0; if (xmax < 0) xmax = 0; if (xmax == xmin) { xmax += 1; xmin -= 1; } else { double spread = (xmax - xmin) / 15; xmax += spread; xmin -= spread; } if (ymax == ymin) { ymax += 1; ymin -= 1; } else { double spread = (ymax - ymin) / 15; ymax += spread; ymin -= spread; } return new double[] { xmin, xmax, ymin, ymax }; } private boolean needsNewLimits(double[] desiredLimits, CoordinateRect coords) { // Check if limits should actually be changed; avoid changing them if // they are close to the desired limits. double[] limits = new double[] { coords.getXmin(), coords.getXmax(), coords.getYmin(), coords.getYmax() } ; return (desiredLimits[0] < limits[0] || desiredLimits[1] > limits[1] || desiredLimits[2] < limits[2] || desiredLimits[3] > limits[3] || (limits[1] - limits[0]) > 1.3*(desiredLimits[1] - desiredLimits[0]) || (limits[3] - limits[2]) > 1.3*(desiredLimits[3] - desiredLimits[2]) || (limits[1] - limits[0]) < (desiredLimits[1] - desiredLimits[0]) / 1.3 || (limits[3] - limits[2]) < (desiredLimits[3] - desiredLimits[2]) / 1.3 ); } private class SPV implements Value { // Represents one of the value objects that can // be returned by the getValueObject() method. private int code; // Which statisitic does this Value represent? SPV(int code) { this.code = code; } public double getVal() { checkData(); switch (code) { case INTERCEPT: return intercept; case SLOPE: return slope; case DATACT: return dataCt; case MISSINGCT: return missingCt; case STANDARDERROR: return standardError; default: return correlation; } } } } // end class ScatterPlot jcm1-source/edu/hws/jcm/draw/TangentLine.java0000644000076500011320000000646411741343635020360 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.data.*; /** * A Tangent line is a line that is tangent to the graph of a specified function of one argument * at a specified value of its argument. If added to a CoordinateRect, it will appear * as a line. * A TangentLine is a Computable object, so should be added to a Controller to be * recomputed when the Value or Function changes. * */ public class TangentLine extends DrawGeometric { /** * Create a tangent line to the graph of a function. * * @param x The x-coordinate where the tangent is drawn. * @param f The line is tangent to the graph of this function. This should be a function of one variable. */ public TangentLine(Value x, Function f) { super(INFINITE_LINE_RELATIVE, x, new ValueMath(f,x), new Constant(1), new ValueMath(f.derivative(1), x)); } } jcm1-source/edu/hws/jcm/draw/Graph1D.java0000644000076500011320000004043711741343635017374 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; import java.awt.*; import java.util.Vector; /** * A Graph1D represents the graph of a function of one variable, to be * displayed in a given CoordinateRect. A Graph1D is a Computable. * The data for the graph is recomputed when its compute() method is * called. It will also be recomputed, before it is drawn, if the * coordinate rect has changed in some way. */ public class Graph1D extends Drawable implements Computable { private Function func; //The function that is graphed. private Color graphColor = Color.magenta; //Color of the graph. private boolean changed; //Used internally to indicate that data has to be recomputed. private transient int[] xcoord, ycoord; //points on graph; xcoord[i] == Integer.MIN_VALUE //for points where function is undefined. /** * Create a Graph1D with no function to graph. One can be set * later with setFunction(); */ public Graph1D() {} /** * Create a graph of the specified function. * * @param func The function to be graphed. If func is null, nothing is drawn. * If func is non-null, it must be a function of one variable. */ public Graph1D(Function func) { setFunction(func); } /** * Set the color to be used for drawing the graph. The default color is magenta. */ public void setColor(Color c) { if (c != null & !c.equals(graphColor)) { graphColor = c; needsRedraw(); } } /** * Get the color that is used to draw the graph. */ public Color getColor() { return graphColor; } /** * Set the function to be graphed. If it is null, nothing is drawn. * If it is non-null, it must be a function of one variable, or an error will occur. * */ synchronized public void setFunction(Function f) { if (f != null && f.getArity() != 1) throw new IllegalArgumentException("Internal Error: Graph1D can only graph a function of one variable."); if (f != func) { func = f; changed = true; needsRedraw(); } } /** * Get the (possibly null) function whose graph is drawn. */ public Function getFunction() { return func; } //------------------ Implementation details ----------------------------- /** * Recompute data for the graph and make sure that the area of the display canvas * that shows the graph is redrawn. This method is ordinarily called by a * Controller. */ synchronized public void compute() { setup(coords); needsRedraw(); } /** * Draw the graph (possibly recomputing the data if the CoordinateRect has changed). * This is not usually called directly. * */ synchronized public void draw(Graphics g, boolean coordsChanged) { if (changed || coordsChanged || xcoord == null || ycoord == null) { setup(coords); changed = false; } if (xcoord.length == 0) return; g.setColor(graphColor); int x = xcoord[0]; int y = ycoord[0]; for (int i = 1; i < xcoord.length; i++) { if (xcoord[i] == Integer.MIN_VALUE) { do { i++; } while (i < xcoord.length && xcoord[i] == Integer.MIN_VALUE); if (i < xcoord.length) { x = xcoord[i]; y = ycoord[i]; } } else { int x2 = xcoord[i]; int y2 = ycoord[i]; g.drawLine(x,y,x2,y2); x = x2; y = y2; } } } // ------------------------- Computing the points on the graph ----------------------- private double absoluteYmax, onscreenymax, absoluteYmin, onscreenymin; private final static int UNDEFINED = 0, ABOVE = 1, BELOW = 2, ONSCREEN = 3; private double[] v = new double[1]; private Cases case1 = new Cases(); private Cases case2 = new Cases(); private double eval(double x, Cases c) { v[0] = x; if (c != null) c.clear(); double y = func.getValueWithCases(v,c); if (Double.isInfinite(y) || Double.isNaN(y)) return Double.NaN; else if (y > absoluteYmax) return absoluteYmax; else if (y < absoluteYmin) return absoluteYmin; else return y; } private int getStatus(double y) { if (Double.isNaN(y)) return UNDEFINED; else if (y > onscreenymax) return ABOVE; else if (y < onscreenymin) return BELOW; else return ONSCREEN; } private void setup(CoordinateRect c) { if (func == null || c == null) { xcoord = ycoord = new int[0]; return; } Vector points = new Vector(); double pixelWidth = (c.getXmax() - c.getXmin()) / (c.getWidth()-2*c.getGap()-1); onscreenymax = c.getYmax() + (100+c.getGap())*pixelWidth; onscreenymin = c.getYmin() - (100+c.getGap())*pixelWidth; absoluteYmax = c.getYmax() + 5000*pixelWidth; absoluteYmin = c.getYmin() - 5000*pixelWidth; double prevx, prevy, x, y, lastx; int status, prevstatus; int pixelx = c.getLeft(); int pixely; int xHoldOffscreen = Integer.MIN_VALUE; int yHoldOffscreen = 0; int statusHoldOffscreen = 0; x = c.pixelToX(pixelx); y = eval(x,case1); status = getStatus(y); if (status == ONSCREEN) { points.addElement(new Point(pixelx,c.yToPixel(y))); } else if (status != UNDEFINED) { xHoldOffscreen = pixelx; yHoldOffscreen = c.yToPixel(y); statusHoldOffscreen = status; } int limitx = c.getLeft() + c.getWidth() -1; while (pixelx < limitx) { prevx = x; prevy = y; prevstatus = status; pixelx += 3; if (pixelx > limitx) pixelx = limitx; x = c.pixelToX(pixelx); y = eval(x, case2); status = getStatus(y); if (status == UNDEFINED) { if (prevstatus != UNDEFINED) { if (prevstatus == ONSCREEN) domainEndpoint(c,points,prevx,x,prevy,y,prevstatus,status,1); else if (xHoldOffscreen != Integer.MIN_VALUE) points.addElement(new Point(xHoldOffscreen,yHoldOffscreen)); xHoldOffscreen = Integer.MIN_VALUE; points.addElement(new Point(Integer.MIN_VALUE,0)); } } else if (prevstatus == UNDEFINED) { if (status == ONSCREEN) { domainEndpoint(c,points,prevx,x,prevy,y,prevstatus,status,1); points.addElement(new Point(pixelx,c.yToPixel(y))); xHoldOffscreen = Integer.MIN_VALUE; } else {// note: status != UNDEFINED xHoldOffscreen = pixelx; yHoldOffscreen = c.yToPixel(y); statusHoldOffscreen = status; } // xHoldOffscreen is already Integer.MIN_VALUE } else if (case1.equals(case2)) { if (status == ONSCREEN) { if (xHoldOffscreen != Integer.MIN_VALUE) { points.addElement(new Point(xHoldOffscreen,yHoldOffscreen)); xHoldOffscreen = Integer.MIN_VALUE; } points.addElement(new Point(pixelx, c.yToPixel(y))); } else { pixely = c.yToPixel(y); if (xHoldOffscreen != Integer.MIN_VALUE) { if (status != statusHoldOffscreen) { // one ABOVE, one BELOW points.addElement(new Point(xHoldOffscreen,yHoldOffscreen)); points.addElement(new Point(pixelx,pixely)); points.addElement(new Point(Integer.MIN_VALUE,0)); } } else points.addElement(new Point(pixelx,pixely)); // first jump to offscreen xHoldOffscreen = pixelx; yHoldOffscreen = pixely; statusHoldOffscreen = status; } } else { // discontinuity if (prevstatus == ABOVE || prevstatus == BELOW) { if (status == prevstatus) { if (xHoldOffscreen != Integer.MIN_VALUE) { // should be false points.addElement(new Point(xHoldOffscreen,yHoldOffscreen)); points.addElement(new Point(Integer.MIN_VALUE,0)); } xHoldOffscreen = pixelx; // don't worry about offscreen discontinuity yHoldOffscreen = c.yToPixel(y); statusHoldOffscreen = status; } else if (status == ONSCREEN) { // possible visible discontinuity if (xHoldOffscreen != Integer.MIN_VALUE) { points.addElement(new Point(xHoldOffscreen,yHoldOffscreen)); xHoldOffscreen = Integer.MIN_VALUE; } discontinuity(c,points,prevx,x,prevy,y,prevstatus,status,1); y = eval(x,case2); // reset cases, for next check points.addElement(new Point(pixelx,c.yToPixel(y))); } else { // status == ABOVE or BELOW, opposit to prevstatus; just do a jump if (xHoldOffscreen != Integer.MIN_VALUE) points.addElement(new Point(xHoldOffscreen,yHoldOffscreen)); points.addElement(new Point(Integer.MIN_VALUE,0)); xHoldOffscreen = pixelx; yHoldOffscreen = c.yToPixel(y); statusHoldOffscreen = status; } } else { // prevstatus is ONSCREEN; possible visible discontinuity discontinuity(c,points,prevx,x,prevy,y,prevstatus,status,1); y = eval(x,case2); // reset cases, for next check if (status == ONSCREEN) { points.addElement(new Point(pixelx,c.yToPixel(y))); xHoldOffscreen = Integer.MIN_VALUE; } else { xHoldOffscreen = pixelx; yHoldOffscreen = c.yToPixel(y); statusHoldOffscreen = status; } } } Cases temp = case2; case2 = case1; case1 = temp; } // end while (pixel < limitx) xcoord = new int[points.size()]; ycoord = new int[points.size()]; for (int i = 0; i < ycoord.length; i++) { Point p = (Point)points.elementAt(i); xcoord[i] = p.x; ycoord[i] = p.y; } } private static final int MAX_DEPTH = 10; // Status of one endpoints is ONSCREEN; other is ABOVE,BELOW, or ONSCREEN private void discontinuity(CoordinateRect c, Vector points, double x1, double x2, double y1, double y2, int status1, int status2, int depth) { //System.out.println("In discontinuity, depth = " + depth); if (depth == MAX_DEPTH) { points.addElement(new Point(c.xToPixel(x1),c.yToPixel(y1))); points.addElement(new Point(Integer.MIN_VALUE,0)); points.addElement(new Point(c.xToPixel(x2),c.yToPixel(y2))); } else { double xmid = (x1+x2)/2.0; y1 = eval(x1,case1); double ymid = eval(xmid,case2); boolean cases1 = case1.equals(case2); y2 = eval(x2,case1); boolean cases2 = case1.equals(case2); int statusmid = getStatus(ymid); if (statusmid == UNDEFINED) { // hope it doesn't happen if (status1 == ONSCREEN) domainEndpoint(c,points,x1,xmid,y1,ymid,status1,statusmid,1); points.addElement(new Point(Integer.MIN_VALUE,0)); if (status2 == ONSCREEN) domainEndpoint(c,points,xmid,x2,ymid,y2,statusmid,status2,1); } else if (cases1 == false) { discontinuity(c,points,x1,xmid,y1,ymid,status1,statusmid,depth+1); if (cases2 == false) // double discontinuity discontinuity(c,points,xmid,x2,ymid,y2,statusmid,status2,depth+1); } else if (cases2 == false) discontinuity(c,points,xmid,x2,ymid,y2,statusmid,status2,depth+1); else System.out.println("Impossible error? no discontinuity found in discontinuity for " + x1 + ',' + x2); } } //One of status1 and status2 is UNDEFINED, one is ONSCREEN. //This always adds a point to points. private void domainEndpoint(CoordinateRect c, Vector points, double x1, double x2, double y1, double y2, int status1, int status2, int depth) { //System.out.println("IN domainEndpoint, ......... depth = " + depth); if (depth == MAX_DEPTH*2) { if (status1 == ONSCREEN) points.addElement(new Point(c.xToPixel(x1),c.yToPixel(y1))); else // status2 == ONSCREEN points.addElement(new Point(c.xToPixel(x2),c.yToPixel(y2))); } else { double xmid = (x1+x2)/2.0; double ymid = eval(xmid,null); int statusmid = getStatus(ymid); if (statusmid == ABOVE || statusmid == BELOW) points.addElement(new Point(c.xToPixel(xmid),c.yToPixel(ymid))); else if (statusmid == status1) // statusmid is ONSCREEN or UNDEFINED domainEndpoint(c,points,xmid,x2,ymid,y2,statusmid,status2,depth+1); else domainEndpoint(c,points,x1,xmid,y1,ymid,status1,statusmid,depth+1); } } } // end class Graph1D jcm1-source/edu/hws/jcm/draw/MouseTracker.java0000644000076500011320000003761411741343635020555 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.awt.*; import edu.hws.jcm.data.*; import java.awt.*; import java.awt.event.*; /** * A MouseTracker can be added to a CoordinateRect in a DisplayCanvas to respond to user * mouse actions in the rectangular area occupied by the CoordinateRect. Two * Variable objects, which can be retrieved by calling getXVar() and getYVar(), * represent the location of the most recent mouse action in terms of the coordinates * of the CoordinateRect. Note that these variables are Tieable objects, so they * can be synchronized with other means of inputting the same information. * The default names of the variables, if you don't change them, are "xMouse" * and "yMouse". * *

A MouseTracker is an InputObject. The values of the variables associated with the * MouseTracker can change only when the checkInput() method is called (or when * the setVal() method of the variable is called to set its value explicitely). * If you want the value of the variables to track the mouse, you must * add the MouseTracker (or the DisplayCanvas that contains it) to a Controller * and set that Controller to listen for changes from the MouseTracker object by * passing the Controller to the setOnUserAction() method of this class. * */ public class MouseTracker extends Drawable implements MouseListener, MouseMotionListener, InputObject { /** * If true, the MouseTracker responds to both clicks and drags. * If false, it responds only to clicks. */ protected boolean listenForDrags; /** * If true, the values of the associated variables are * undefined except during the time that the user is * clicking and dragging the mouse. This is ignored * if listenForDrags is false. */ protected boolean undefinedWhenNotDragging; /** * If this is non-null, then its compute() method is called * when the user clicks the mouse and, if listenForDrags is also * true, when the user drags and releases the mouse. */ protected Controller onUserAction; /** * If thie is true, then the value of the variable associated with * the x-ccordinate of the mouse is clamped to lie within the * xmin and xmax of the coordinate rect. */ protected boolean clampX = true; /** * If thie is true, then the value of the variable associated with * the y-ccordinate of the mouse is clamped to lie within the * ymin and ymax of the coordinate rect. */ protected boolean clampY = true; private MTVariable xVar, yVar; //The variables associated with this MouseTracker. The class MTVarible // is a private nested class defined below. private int xClick, yClick; //Pixel where the mose recent user mouse action occured. private boolean inRect; //This is set to true while the user is dragging (if listenForDrags is true). /** * Create a MouseTracker that responds to both clicks and drags. The values of the * associated variables remain defined even after the user stops dragging. */ public MouseTracker() { this(true,false); } /** * Creates a mouse tracker. The first parameter specifies whether the values of * the variables change when the user drags the mouse, or only when the user clicks. * The second parameter is only used if the first is true. It specifies whether * the values of the variables become undefined after the user stops dragging the * mouse. * */ public MouseTracker(boolean listenForDrags, boolean undefinedWhenNotDragging) { this.listenForDrags = listenForDrags; this.undefinedWhenNotDragging = undefinedWhenNotDragging; xVar = new MTVariable(true); yVar = new MTVariable(false); } /** * Get the variable whose value represents the x-coordinate of the MouseTracker. * Note that this variable implements the Tieable interface, so can legally * be type-cast to type Tieable. It can be tied to other objects that * implement the Tieable and Value interfaces to synchronize their values. */ public Variable getXVar() { return xVar; } /** * Get the variable whose value represents the y-coordinate of the MouseTracker. * Note that this variable implements the Tieable interface, so can legally * be type-cast to type Tieable. It can be tied to other objects that * implement the Tieable and Value interfaces to synchronize their values. */ public Variable getYVar() { return yVar; } /** * Sets the "listenForDrags" property of the MouseTracker. * If set to true, then the MouseTracker responds to both clicks and drags if false, * it responds only to clicks. * */ public void setListenForDrags(boolean listen) { if (listen != listenForDrags) { listenForDrags = listen; if (canvas != null) { if (listen) canvas.addMouseMotionListener(this); else canvas.removeMouseMotionListener(this); } } } /** * Gets the "listenForDrags" property of the MouseTracker, which determines * if the MouseTracker responds to both clicks and drags, or only to clicks. */ public boolean getListenForDrags() { return listenForDrags; } /** * Sets the "undefinedWhenNotDragging" property of the MouseTracker. * This is ignored if the MouseTracker is not listening for drags. * If set to true, the values of the variables associated with this * variable become undefined when the user is not dragging. * */ public void setUndefinedWhenNotDragging(boolean b) { undefinedWhenNotDragging = b; } /** * Gets the "undefinedWhenNotDragging" property of the MouseTracker. * */ public boolean getUndefinedWhenNotDragging() { return undefinedWhenNotDragging; } /** * Set a Controller to respond to user mouse actions tracked * by this MouseTracker. The MouseTracker should also be added * to the Controller, so that the values of its variables will * actually change when a user action occurs. * */ public void setOnUserAction(Controller onUserAction) { this.onUserAction = onUserAction; } /** * Method required by InputObject interface; in this class, it simply calls * setOnUserAction(c). This is meant to be called by JCMPanel.gatherInputs(). */ public void notifyControllerOnChange(Controller c) { setOnUserAction(c); } /** * Get the Controller that responds when a user mouse action is detected by this MouseTracker. */ public Controller getOnUserAction() { return onUserAction; } /** * Set the "clampX" property of the MouseTracker. * If set to true, which is the default, the value of * the variable associated with the horizontal position of * the mouse is clamped to lie within the containing * CoordinateRect. * */ public void setClampX(boolean clamp) { clampX = clamp; } /** * Get the "clampX" property of the MouseTracker. */ public boolean getClampX() { return clampX; } /** * Set the "clampY" property of the MouseTracker. * If set to true, which is the default, the value of * the variable associated with the vertical position of * the mouse is clamped to lie within the containing * CoordinateRect. * */ public void setClampY(boolean clamp) { clampY = clamp; } /** * Get the "clampY" property of the MouseTracker. */ public boolean getClampY() { return clampX; } //------------------ Implementation details -------------------------------------------- /** * Set the values of the associated variables. This is part of the InputObject interface, * and it is meant to be called by a Controller. */ public void checkInput() { if (coords == null || (undefinedWhenNotDragging && !inRect)) { ; xVar.setVal(Double.NaN); yVar.setVal(Double.NaN); } else { double newX, newY; // The new values. newX = coords.pixelToX(xClick); if (clampX) { if (newX < coords.getXmin()) newX = coords.getXmin(); else if (newX > coords.getXmax()) newX = coords.getXmax(); } xVar.setVal(newX); newY = coords.pixelToY(yClick); if (clampY) { if (newY < coords.getYmin()) newY = coords.getYmin(); else if (newY > coords.getYmax()) newY = coords.getYmax(); } yVar.setVal(newY); } } /** * A MouseTracker doesn't actually draw anything, but this method is required in * a Drawable object. */ public void draw(Graphics g, boolean coordsChanged) { } /** * This is called automatically by CoordinateRect when the * MouseTracker is added to the CoordinateRect. It is not * meant to be used directly. * */ protected void setOwnerData(DisplayCanvas canvas, CoordinateRect coords) { if (this.canvas != null) { canvas.removeMouseListener(this); canvas.removeMouseMotionListener(this); } this.canvas = canvas; this.coords = coords; canvas.addMouseListener(this); if (listenForDrags) canvas.addMouseMotionListener(this); } /** * Responds when the user clicks the mouse in the rectangular * area occupied by the CoordinateRect that contains this MouseTracker. * Since the MouseTracker listens for clicks on the whole DisplayCanvas * and the CoordinateRect might only occupy part of that, it is necessary * to check whether the user click was in that rect. This is not meant to be called directly. * */ public void mousePressed(MouseEvent evt) { if (evt.isConsumed() || coords == null) return; inRect = (evt.getX() >= coords.getLeft() && evt.getX() <= coords.getLeft() + coords.getWidth() && evt.getY() >= coords.getTop() && evt.getY() <= coords.getTop() + coords.getHeight()); if (!inRect) return; evt.consume(); xClick = evt.getX(); yClick = evt.getY(); xVar.serialNumber++; yVar.serialNumber++; if (onUserAction != null) onUserAction.compute(); } /** * Responds when the user releases the mouse. This is not meant to be called directly. * */ public void mouseReleased(MouseEvent evt) { if (inRect == false) return; inRect = false; if (listenForDrags && undefinedWhenNotDragging) { xVar.serialNumber++; yVar.serialNumber++; if (onUserAction != null) onUserAction.compute(); } } /** * Responds when the user drags the mouse. This is not meant to be called directly. * */ public void mouseDragged(MouseEvent evt) { if (listenForDrags && inRect) { xClick = evt.getX(); yClick = evt.getY(); xVar.serialNumber++; yVar.serialNumber++; if (onUserAction != null) onUserAction.compute(); } } /** * Empty method, required by MouseListener interface. */ public void mouseClicked(MouseEvent evt) { } /** * Empty method, required by MouseMotionListener interface. */ public void mouseEntered(MouseEvent evt) { } /** * Empty method, required by MouseMotionListener interface. */ public void mouseExited(MouseEvent evt) { } /** * Empty method, required by MouseMotionListener interface. */ public void mouseMoved(MouseEvent evt) { } //The class to which the variables associated with this MouseTracker belong. private class MTVariable extends Variable implements Tieable { //True for xVar; false for yVar. private boolean isXVar; //This object's serial number, which can //change in MouseTracker.checkInput() as //well as in the setVal() and sync() methods in this class. long serialNumber; //Create the variable. MTVariable(boolean isXVar) { super(isXVar? "xMouse" : "yMouse"); this.isXVar = isXVar; super.setVal(Double.NaN); } //Set the value of the variable. Note that the //value can be set to lie outside the coordinate rect, //even if clampX and clampY are true. The next checkInput(), //however, will apply the clamp. public void setVal(double val) { if (isXVar) { if (coords != null) // set xClick to match the value. xClick = coords.xToPixel(val); } else { if (coords != null) // set yClick to match the value yClick = coords.yToPixel(val); } super.setVal(val); } // Return this Tieable object's serial number. public long getSerialNumber() { return serialNumber; } //Synchronize values and serial numbers with newest. public void sync(Tie tie, Tieable newest) { if ( ! (newest instanceof Value) ) throw new IllegalArgumentException("Internal Error: A MouseTracker variable can only be tied to a Value object."); if (newest != this) { setVal(((Value)newest).getVal()); serialNumber = newest.getSerialNumber(); } } } // end nested class MTVariable } // end class MouseTracker jcm1-source/edu/hws/jcm/draw/Draggable.java0000644000076500011320000000653111741343635020013 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.event.MouseEvent; /** * An interface that can be implemented by an object that can be dragged * with the mouse. */ public interface Draggable { /** * Tell the object that a drag operation might be beginning. * The Draggable object can decide whether it really wants * to be dragged, based on the MouseEvent. It should return * true to indicate that a drag should really be started, and * false if it wants to ignore the MouseEvent. */ public boolean startDrag(MouseEvent evt); /** * Continue a drag that was started in startDrag(). Presumably * the event is a mouseDragged event. */ public void continueDrag(MouseEvent evt); /** * Finish a draw that was started in startDrag(). Presumably * the event is a mouseReleased event. */ public void finishDrag(MouseEvent evt); } jcm1-source/edu/hws/jcm/draw/DrawGeometric.java0000644000076500011320000005322311741343635020677 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; import java.awt.*; /** * A DrawGeometric object is a geometic figure such as a line or rectangle that can * be drawn in a CoordinateRect. The data for the object always consists of four * numbers, which are interpreted differenetly depending on the object. These numbers * can be specified as Value objects. A DrawGeometric is a Computable, and the * Values will be re-computed when its compute() method is called. It should be * added to a Controller that can respond to any changes in the data that define * the Values. If one of the Value objects has an undefined value, nothing will be drawn. *

The type of object is given as one of the constants defined in this class: * LINE_ABSOLUTE, OVAL_RELATIVE, CROSS, and so on. In the descriptions of these * constants, x1, x2, y1, and y2 refer to the values of Value objects that provide data * for the DrawGeomentric while h and v refer to int's that can be specified in place of * x2 and y2 for certain types of figures. For those figures, h or v is used if * x2 or y2, respectively, is null. * * @author David Eck */ public class DrawGeometric extends Drawable implements Computable { /** * Specifies a line segment from (x1,y1) to (x2,y2). */ public static final int LINE_ABSOLUTE = 0; /** * Specifies a line that extends through the points (x1,y1) and (x2,y2) and beyond. */ public static final int INFINITE_LINE_ABSOLUTE = 1; /** * Specifies a rectangle with corners at (x1,y1) and (x2,y2). */ public static final int RECT_ABSOLUTE = 2; /** * Specifies an oval that just fits in the rectangle with corners at (x1,y1) and (x2,y2). */ public static final int OVAL_ABSOLUTE = 3; /** * Specifies a line segment from (x1,y1) to (x1+x2,y1+y2), or to (x1+h,y1+v) if x2,y2 are null. * (Note that h,v are given in terms of pixels while x1,x2,y1,y2 are given * in terms of the CoordinateRect. If you use h,v, you get a line * of a fixed size and direction.) */ public static final int LINE_RELATIVE = 4; /** * Specifies an infinite line through (x1,y1) and (x1+x2,y1+y2), or through (x1,y1) and (x1+h,y1+v) if x2,y2 are null. */ public static final int INFINITE_LINE_RELATIVE = 5; /** * Specifies a rectangle with one corner at (x1,y1), and with width given by x2, or h if * if x2 is null, and with height given by y2, or by v if y2 is null. */ public static final int RECT_RELATIVE = 6; /** * Specifies an oval that just fits inside the rect specified by RECT_RELATIVE. */ public static final int OVAL_RELATIVE = 7; /** * Specifies a line segment centered on (x1,y1). The amount it extends in each direction * is given by x2,y2 or by h,v */ public static final int LINE_CENTERED = 8; /** * Specifies a Rectangle centered on (x1,y1). The amount it extends in each direction * is given by x2,y2 or by h,v. (Thus, x2 or h is the HALF-width and y2 or v is the HALF-height.) */ public static final int RECT_CENTERED = 9; /** * Specifies an oval that just fits inside the rect specified by RECT_CENTERED. */ public static final int OVAL_CENTERED = 10; /** * Specifies a cross centered on the point (x1,y1). Its arms extend horizontally * by a distance of x2, or h, in each direction. Its vertical * arms extend y2, or v, in each direction. */ public static final int CROSS = 11; /** * One of the constants such as OVAL_CENTERED, specifying the shape to be drawn */ protected int shape; /** * One of the Value objects that determine the shape that is drawn. * The shape is specified by two points, (x1,y1) and (x2,y2). * x1 must be non-null. */ protected Value x1; /** * One of the Value objects that determine the shape that is drawn. * The shape is specified by two points, (x1,y1) and (x2,y2). * x2 must be non-null * for the "ABSOLUTE" shapes. (If not, they revert to * "RELATIVE" shapes and use h,v as the offset values.) */ protected Value x2; /** * One of the Value objects that determine the shape that is drawn. * The shape is specified by two points, (x1,y1) and (x2,y2). * y1 must be non-null. */ protected Value y1; /** * One of the Value objects that determine the shape that is drawn. * The shape is specified by two points, (x1,y1) and (x2,y2). * y2 must be non-null * for the "ABSOLUTE" shapes. (If not, they revert to * "RELATIVE" shapes and use h,v as the offset values.) */ protected Value y2; /** * Integer that gives horizontal pixel offset from x1. * This is only used if x2 is null. */ protected int h = 10; /** * Integer that gives vertical pixel offset fromy1. * This is only used if y2 is null. */ protected int v = 10; /** * Value of x1. This is re-computed when the compute() method is called. */ protected double a = Double.NaN; /** * Value of y1. This is re-computed when the compute() method is called. */ protected double b; /** * Value of x2. This is re-computed when the compute() method is called. */ protected double c; /** * Value of y2. This is re-computed when the compute() method is called. */ protected double d; /** * Color of the shappe. Color will be black if this is null. For shapes that * have "insides", such as rects, this is the color of the outline. */ protected Color color = Color.black; /** * Rects and ovals are filled with this color, if it is non-null. * If this is null, only the outline of the shape is drawn. */ protected Color fillColor; /** * The width, in pixels, of lines, including the outlines * of rects and ovals. It is restricted to being an integer * in the range from 0 to 10. A value of 0 means that lines * won't be drawn at all; this would only be useful for a filled * shape that has a colored interior. */ protected int lineWidth = 1; private boolean changed = true; // set to true when values have to be recomputed. /** * Create a DrawGeometric object. By default, it is a LINE_ABSOLUTE. However, * nothing will be drawn as long as x1,y1,x2,y2 are null. */ public DrawGeometric() {} /** * Create a DrawGeometric with the specified shape and values for x1,x2,y1,y2 * Any of the shapes makes sense in this context. * * @param shape One of the shape constants such as LINE_ABSOLUTE or RECT_RELATIVE. */ public DrawGeometric(int shape, Value x1, Value y1, Value x2, Value y2) { setShape(shape); setPoints(x1,y1,x2,y2); } /** * Create a DrawGeometric with a specified shape and values. The last two parameters * give pixel offsets from x1,y1. The "ABSOLUTE" shapes don't make * sense in this context. (They will be treated as the corresponding * "RELATIVE" shapes.) * * @param shape One of the "RELATIVE" or "CENTERED" shape constants such as LINE_RELATIVE or OVAL_CENTERED or CROSS. */ public DrawGeometric(int shape, Value x1, Value y1, int h, int v) { setShape(shape); setPoints(x1,y1,h,v); } // ---------------- Routines for getting and setting properties -------------------------- /** * Set the shape, which should be given as one of the shape constants such as LINE_ABSOLUTE or CROSS. */ public void setShape(int shape) { if (shape < 0 || shape > CROSS) throw new IllegalArgumentException("Internal error: Illegal value for shape of DrawGeometric object."); this.shape = shape; needsRedraw(); } /** * Set the Value objects that specify the two points that determine the shape. * The first two parameters, x1 and y1, must be non-null. */ public void setPoints(Value x1, Value y1, Value x2, Value y2) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; compute(); } /** * Set the values that specify a point (x1,y1) and an offset (h,v) from that point. * This only makes sense for RELATIVE shapes. The Value objects x1 and y1 must be non-null */ public void setPoints(Value x1, Value y1, int h, int v) { this.x1 = x1; this.y1 = y1; this.x2 = null; this.y2 = null; this.h = h; this.v = v; compute(); } /** * Set the value that gives the x-coordinate of the first point that determines the shape. * This must be non-null, or nothing will be drawn. */ public void setX1(Value x) { x1 = x; compute(); } /** * Get the value that gives the x-coordinate of the first point that determines the shape. */ public Value getX1() { return x1; } /** * Set the value that gives the x-coordinate of the second point that determines the shape. * If this is null, then the value of h is used instead. */ public void setX2(Value x) { x2 = x; compute(); } /** * Get the value that gives the x-coordinate of the second point that determines the shape. */ public Value getX2() { return x2; } /** * Set the value that gives the y-coordinate of the first point that determines the shape. * This must be non-null, or nothing will be drawn. */ public void setY1(Value y) { y1 = y; compute(); } /** * Get the value that gives the y-coordinate of the first point that determines the shape. */ public Value getY1() { return y1; } /** * Set the value that gives the y-coordinate of the second point that determines the shape. * If this is null, then the value of v is used instead. */ public void setY2(Value y) { y2 = y; compute(); } /** * Get the value that gives the y-coordinate of the second point that determines the shape. */ public Value getY2() { return y2; } /** * Set the integer that gives the horizontal offset from (x1,y1). * This only makes sense for RELATIVE shapes. This method also sets x2 to null, * since the h value is only used when x2 is null. */ public void setH(int x) { h = x; x2 = null; compute(); } /** * Get the horizontal offset from (x1,y1). */ public int getH() { return h; } /** * Set the integer that gives the vertical offset from (x1,y1). * This only makes sense for RELATIVE shapes. This method also sets y2 to null, * since the v value is only used when y2 is null. */ public void setV(int y) { v = y; y2 = null; needsRedraw(); } /** * Get the vertical offset from (x1,y1). */ public int getV() { return v; } /** * Set the color that is used for drawing the shape. If the color is null, black is used. * For shapes that have interiors, such as rects, this is only the color of the outline of the shaape. */ public void setColor(Color c) { color = (c == null)? Color.black : c; needsRedraw(); } /** * Get the non-null color that is used for drawing the shape. */ public Color getColor() { return color; } /** * Set the color that is used for filling ovals and rects. If the color is null, only the outline of the shape is drawn. */ public void setFillColor(Color c) { fillColor = c; needsRedraw(); } /** * Get the color that is used for filling ovals and rects. If null, no fill is done. */ public Color getFillColor() { return fillColor; } /** * Set the width, in pixels, of lines that are drawn. This is also used for outlines of rects and ovals. */ public void setLineWidth(int width) { if (width != lineWidth) { lineWidth = width; if (lineWidth > 10) lineWidth = 10; else if (lineWidth < 0) lineWidth = 0; needsRedraw(); } } /** * Get the width, in pixels, of lines that are drawn. This is also used for outlines of rects and ovals. */ public int getLineWidth() { return lineWidth; } // ------------------------- Implementation details ------------------------------------ /** * Recompute the values that define the size/postion of the DrawGeometric. * This is ordinarily only called by a Controller. */ public void compute() { changed = true; needsRedraw(); } private void doValues() { if (x1 != null) a = x1.getVal(); if (y1 != null) b = y1.getVal(); if (x2 != null) c = x2.getVal(); if (y2 != null) d = y2.getVal(); changed = false; } /** * Do the drawing. This is not meant to be called directly. */ public void draw(Graphics g, boolean coordsChanged) { if (changed) doValues(); if (coords == null || x1 == null || y1 == null || Double.isNaN(a) || Double.isNaN(b) || Double.isInfinite(a) || Double.isInfinite(b) ) return; if (x2 != null && (Double.isNaN(c) || Double.isInfinite(c))) return; if (y2 != null && (Double.isNaN(d) || Double.isInfinite(d))) return; // Get the four real numbers that determine the shape, in terms of pixels. double A,B,W,H; A = xToPixelDouble(a); B = yToPixelDouble(b); if (x2 == null) W = h; else if (shape <= OVAL_ABSOLUTE) W = xToPixelDouble(c) - A; else W = c/coords.getPixelWidth(); if (y2 == null) H = -v; else if (shape <= OVAL_ABSOLUTE) H = yToPixelDouble(d) - B; else H = -d/coords.getPixelHeight(); if (shape == INFINITE_LINE_ABSOLUTE || shape == INFINITE_LINE_RELATIVE) drawInfiniteLine(g, A, B, W, H); else if (shape == CROSS) drawCross(g, (int)A, (int)B, (int)(Math.abs(W)+0.5), (int)(Math.abs(H)+0.5)); else if (shape == LINE_RELATIVE || shape == LINE_ABSOLUTE) drawLine(g, (int)A, (int)B, (int)(A+W), (int)(B+H)); else if (shape == LINE_CENTERED) drawLine(g,(int)(A-Math.abs(W)+1),(int)(B-Math.abs(H)+1),(int)(A+Math.abs(W)),(int)(B+Math.abs(H))); else if (shape <= OVAL_RELATIVE) { if (W < 0) { W = -W; A = A - W; } if (H < 0) { H = -H; B = B - H; } drawShape(g, (int)A, (int)B, (int)(W+0.5), (int)(H+0.5)); } else drawShape(g,(int)(A-Math.abs(W)+1),(int)(B-Math.abs(H)+1),(int)(2* Math.abs(W)-0.5),(int)(2*Math.abs(H)-0.5)); } private double xToPixelDouble(double x) { return coords.getLeft() + coords.getGap() + ((x - coords.getXmin())/(coords.getXmax() - coords.getXmin()) * (coords.getWidth()-2*coords.getGap()-1)); } private double yToPixelDouble(double y) { return coords.getTop() + coords.getGap() + ((coords.getYmax() - y)/(coords.getYmax() - coords.getYmin()) * (coords.getHeight()-2*coords.getGap()-1)); } private void drawLine(Graphics g, int x1, int y1, int x2, int y2) { int width = Math.abs(x2 - x1); int height = Math.abs(y2 - y1); g.setColor( color ); if (width == 0 && height == 0) g.drawLine(x1,y1,x1,y1); else if (width > height) { for (int i = 0; i < lineWidth; i++) g.drawLine(x1,y1-lineWidth/2+i,x2,y2-lineWidth/2+i); } else { for (int i = 0; i < lineWidth; i++) g.drawLine(x1-lineWidth/2+i,y1,x2-lineWidth/2+i,y2); } } /** * Draws a rect or oval. * * @param x the top-left x value of the rect or the rect that contains the oval * @param y the top-left y value of the rect or the rect that contains the oval * @param width width of the rect * @param height height of the rect */ private void drawShape(Graphics g, int x, int y, int width, int height) { if (x > coords.getLeft() + coords.getWidth() || y > coords.getTop() + coords.getHeight() || x + width < coords.getLeft() || y + height < coords.getTop()) { return; } if (fillColor != null) { g.setColor(fillColor); if (shape == RECT_ABSOLUTE || shape == RECT_RELATIVE || shape == RECT_CENTERED) g.fillRect(x,y,width,height); else g.fillOval(x,y,width,height); } g.setColor( color ); if (shape == RECT_ABSOLUTE || shape == RECT_RELATIVE || shape == RECT_CENTERED) { for (int i = 0; i < lineWidth; i++) g.drawRect(x+i,y+i,width-2*i,height-2*i); } else { for (int i = 0; i < lineWidth; i++) g.drawOval(x+i,y+i,width-2*i,height-2*i); } } private void drawCross(Graphics g, int x, int y, int width, int height) { if (x - width> coords.getLeft() + coords.getWidth() || y - height > coords.getTop() + coords.getHeight() || x + width < coords.getLeft() || y + height < coords.getTop()) { return; } int left = x - lineWidth/2; int top = y - lineWidth/2; g.setColor( color ); for (int i = 0; i < lineWidth; i++) g.drawLine(x-width,top+i,x+width,top+i); for (int i = 0; i < lineWidth; i++) g.drawLine(left+i,y-height,left+i,y+height); } private void drawInfiniteLine(Graphics g, double x, double y, double dx, double dy) { if (Math.abs(dx) < 1e-10 && Math.abs(dy) < 1e-10) return; g.setColor( color ); if (Math.abs(dy) > Math.abs(dx)) { double islope = dx / dy; int y1 = coords.getTop() - 5; int y2 = coords.getTop() + coords.getHeight() + 5; int x1 = (int)(islope*(y1 - y) + x); int x2 = (int)(islope*(y2 - y) + x); if (Math.abs(x1) < 20000 && Math.abs(x2) < 20000) for (int i = 0; i < lineWidth; i++) g.drawLine(x1-lineWidth/2+i,y1,x2-lineWidth/2+i,y2); } else { double slope = dy / dx; int x1 = coords.getLeft() - 5; int x2 = coords.getLeft() + coords.getWidth() + 5; int y1 = (int)(slope*(x1 - x) + y); int y2 = (int)(slope*(x2 - x) + y); if (Math.abs(y1) < 20000 && Math.abs(y2) < 20000) for (int i = 0; i < lineWidth; i++) g.drawLine(x1,y1-lineWidth/2+i,x2,y2-lineWidth/2+i); } } } // end class DrawGeometric jcm1-source/edu/hws/jcm/draw/Drawable.java0000644000076500011320000001535011741343635017663 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.draw; import java.awt.Graphics; /** * A Drawable object can be added to a CoordinateRect, which is itself in * a DisplayCanvas. Its purpose is, generally, to draw something in the * rectangular area represented by the CoordinateRect. The drawing can * use information in the CoordinateRect, which includes both the real * number coordinates and the pixel coordinates of the rectangular area. */ abstract public class Drawable implements java.io.Serializable { /** * The CoordinateRect for the rectagular area where this * Drawable is drawn. This is set automatically when the * Drawable is added to a CoordingteRect and should not be * changed. (It will be changed automatically if the * Drawable is removed from the CoordinateRect.) */ protected CoordinateRect coords; /** * The canvas on which this Drawable is drawn. This is set * automatically when the Drawable is added to a CoordinateRect * and it should not be changed. (It will be changed automatically * if the Drawable is removed from the CoordinateRect.) */ protected DisplayCanvas canvas; private boolean visible = true; // If visible is false, then the CoordinateRect // that manages this Drawable will ignore it. // It will not call the draw() routine, so when // draw is called, it can be assumed that the // Drawable is visible. /** * Draw this drawable in the graphics context g. This is meant to * be called only by the CoordinateRect, coords, that manages this Drawable. * The coords contains information about the rectangular area in which * this Drawable is displayed, both in terms of pixels and in terms * of real (x,y)-coordinates. * The value of coordsChanged is true if any of the values * coords.getXmin(), coords.getXmax(), coords.getYmin(), coords.getYmax(), * coords.getLeft(), coords.getRight(), coords.getTop(), coords.getBottom(), * or coords.getGap() has changed. Drawables that depend only on this * information can check the value of coordsChanged to see whether they * need to update any previously computed member variables that * depend on these values. This method is meant to be called only by the system. * * @param g The graphics context in which the Drawble is to be drawn. (The drawing * can change the color in g, but should not permanently change font, painting mode, etc. * Thus, every drawable is responsible for setting the color it wants to use.) * @param coordsChanged Indicates whether the CoordinateRect has changed. */ abstract public void draw(Graphics g, boolean coordsChanged); /** * Return true if this Drawable is visible, false if it is hidden. * A hidden Drawable is ignored by the CoordinateRect that manages it. */ public boolean getVisible() { return visible; } /** * Set the visibility of this Drawable. If show is false, then * the Drawable is hidden. If it is true, the Drawable is shown. */ public void setVisible(boolean show) { if (show != visible) { visible = show; needsRedraw(); } } /** * This routine should be called if the appearance of the Drawable changes * so that the rectangular area that it occupies has to be redrawn. * The routine is generally meant to be called by the Drawable itself. * It will notify the DisplayCanvas, canvas, that the CoordinateRect, * coords, needs to be redrawn, where canvas and coords are the member * variables in this class. If canvas is null, nothing happens, since * presumably the Drawable is not displayed anywhere in that case. */ public void needsRedraw() { if (canvas != null) canvas.doRedraw(coords); } /** * Sets the values of member variables canvas and coords. This is * designed to be called only by the CoordinateRect class. */ protected void setOwnerData(DisplayCanvas canvas, CoordinateRect coords) { this.canvas = canvas; this.coords = coords; } } // end class Drawable jcm1-source/edu/hws/jcm/functions/0000755000076500011320000000000011741343635016346 5ustar djefacultyjcm1-source/edu/hws/jcm/functions/TableFunctionInput.java0000644000076500011320000006141011741343635022770 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.functions; import java.awt.*; import java.awt.event.*; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; import edu.hws.jcm.draw.*; /** * A TableInputFunction is a Panel that can be used to define a TableFunction * or to edit an existing TableFunction. To fetch the function currently * displayed in the panel, call copyOfCurrentFunction(). To edit a function, * call startEdit() to install the function. Then call cancelEdit() or * finishEdit() to finish the editing. The panel displays both the graph * of the function and a list of points that define the function. The user * can drag the points on the graph up and down to change the y-values. * Points can be added or modified by typing in their x- and y-coordinates. * A point can be deleted by clicking it in the list of points and then * clicking on the button labeled "Delete Point". */ public class TableFunctionInput extends Panel implements ItemListener, ActionListener, MouseListener, MouseMotionListener { private VariableInput xInput, yInput; // Components of the GUI. private DisplayCanvas canvas; private List pointList; private Button clearButton, deleteButton, addButton; private Checkbox[] styleCheckbox = new Checkbox[5]; private CheckboxGroup styleGroup; private Controller onChange; // If non-null, the compute() method of the controller // is called when the user edits a point. private TableFunction function; // The function displayed. private TableFunction editFunction; // If non-null, the function passed to startEdit. /** * Create a TableFunctionInput panel. Initially, the function in the * panel has no points and so is undefined everywhere. The panel needs * to be fairly large! */ public TableFunctionInput() { xInput = new VariableInput(); xInput.addActionListener(this); yInput = new VariableInput(); yInput.addActionListener(this); pointList = new List(); pointList.setBackground(Color.white); pointList.setFont(new Font("Monospaced",Font.PLAIN,12)); pointList.addItemListener(this); clearButton = new Button("Remove All Points"); clearButton.addActionListener(this); deleteButton = new Button("Delete Point"); deleteButton.setEnabled(false); deleteButton.addActionListener(this); addButton = new Button("Add/Modify Point"); addButton.addActionListener(this); styleGroup = new CheckboxGroup(); styleCheckbox[0] = new Checkbox("Smooth", true, styleGroup); styleCheckbox[1] = new Checkbox("Piecewise Linear", false, styleGroup); styleCheckbox[2] = new Checkbox("Step (nearest value)", false, styleGroup); styleCheckbox[3] = new Checkbox("Step (value from left)", false, styleGroup); styleCheckbox[4] = new Checkbox("Step (value from right)", false, styleGroup); for (int i = 0; i < 5; i++) styleCheckbox[i].addItemListener(this); canvas = new DisplayCanvas( new CoordinateRect(-1,1,-1,1) ); canvas.add(new Axes()); canvas.addMouseListener(this); canvas.addMouseMotionListener(this); function = new TableFunction(); canvas.add(new Draw()); Font labelFont = new Font("Serif", Font.BOLD, 12); Label lab1 = new Label("Input Area"); lab1.setForeground(Color.red); lab1.setFont(labelFont); Label lab2 = new Label("Type of Function", Label.CENTER); lab2.setForeground(Color.red); lab2.setFont(labelFont); Label lab3 = new Label("Table of Values", Label.CENTER); lab3.setForeground(Color.red); lab3.setFont(labelFont); Panel topLeft = new Panel(); topLeft.setLayout(new FlowLayout(FlowLayout.CENTER, 10000,3)); Panel topRight = new Panel(); topRight.setLayout(new GridLayout(6,1,3,3)); Panel bottomLeft = new Panel(); bottomLeft.setLayout(new BorderLayout()); Panel top = new Panel(); top.setLayout(new BorderLayout(3,3)); top.add(topLeft,BorderLayout.CENTER); top.add(topRight,BorderLayout.EAST); setLayout(new BorderLayout(3,3)); add(top,BorderLayout.NORTH); add(bottomLeft, BorderLayout.WEST); add(canvas, BorderLayout.CENTER); setBackground(Color.darkGray); topLeft.setBackground(Color.lightGray); topRight.setBackground(Color.lightGray); bottomLeft.setBackground(Color.lightGray); Panel inputBar = new Panel(); inputBar.add(new Label("x = ")); inputBar.add(xInput); inputBar.add(new Label(" y = ")); inputBar.add(yInput); Panel buttonBar = new Panel(); buttonBar.setLayout(new GridLayout(1,2,3,3)); buttonBar.add(deleteButton); buttonBar.add(clearButton); topLeft.add(lab1); topLeft.add(inputBar); topLeft.add(addButton); topLeft.add(new Label("(Press RETURN in X to move to Y)")); topLeft.add(new Label("(Press RETURN in Y to add/modify point)")); topRight.add(lab2); for (int i = 0; i < 5; i++) topRight.add(styleCheckbox[i]); bottomLeft.add(pointList, BorderLayout.CENTER); bottomLeft.add(lab3, BorderLayout.NORTH); bottomLeft.add(buttonBar, BorderLayout.SOUTH); } // end constructor //------------------- Methods for modifying an existing function -------------- /** * Install a function to be edited. The data from the function is * copied into the panel, and a pointer to the function is retained. * Note that the function itself is not changed by any editing that * the user does. To commit the changes made by the user to the * actual function, you must call finishEdit(). If f is null, * the effect is to start with a new, empty function. */ public void startEdit(TableFunction f) { editFunction = f; revertEditFunction(); // Installs data from function. } /** * If a function has been specified using startEdit(), and neither * finishEdit() nor cancelEdit have been called, then this method * will discard the current data in and replace it with data from * the edit function. If there is no edit function, then the data * is simply discarded. (That is, the data reverts to an empty point list.) */ public void revertEditFunction() { if (editFunction == null) { clearAllPoints(); return; } function.copyDataFrom(editFunction); pointList.removeAll(); int pointCt = function.getPointCount(); for (int i = 0; i < pointCt; i++) pointList.add(makePointString(function.getX(i),function.getY(i))); styleGroup.setSelectedCheckbox(styleCheckbox[function.getStyle()]); checkCanvas(); if (onChange != null) onChange.compute(); } /** * If an edit function has been specified (by startEdit()), this function copies the * data form the TableFunctionInput into that function, and returns a * pointer to that function. This ends the edit session, and the internally stored * pointer to the edit function is discarded. If no edit function has been specified * then a new TableFunction is created with the data from the panel, and a * pointer to the new function is returned. This does not clear the data * in the TableFunctionInput panel. */ public TableFunction finishEdit() { TableFunction func; if (editFunction == null) func = copyOfCurrentFunction(); else { editFunction.copyDataFrom(function); func = editFunction; editFunction = null; } return func; } /** * Discards the internal pointer to the edit function (specified by startEdit()), * if any. This does not clear the data in the TableFunctionInput panel. */ public void cancelEdit() { editFunction = null; } /** * Create a new TableFunction containing the data that is currently * in the TableFunctionInput panel, and return a pointer to that new function. */ public TableFunction copyOfCurrentFunction() { TableFunction copy = new TableFunction(); copy.copyDataFrom(function); copy.setName(function.getName()); return copy; } /** * Specify a controller whose compute() method will be called whenever * the user edits the data in this TableFunctionInput panel. (Note that * when the user edits the function by dragging a point, the Controller * is only called once at the end of the drag.) If the specified * Controller is null, then no notification takes place. */ public void setOnChange(Controller c) { onChange = c; } /** * Get the Controller that is notified when the user edits the data * in this panel. The return value can be null (the default), indicating * that no notification takes place. */ public Controller getOnChange() { return onChange; } private void deletePoint() { // delete point selected in the point list, if any int index = pointList.getSelectedIndex(); if (index >= 0) { pointList.remove(index); function.removePointAt(index); checkCanvas(); if (onChange != null) onChange.compute(); } deleteButton.setEnabled(false); } private void clearAllPoints() { // remove all points, leaving an empty point list function.removeAllPoints(); pointList.removeAll(); deleteButton.setEnabled(false); if (onChange != null) onChange.compute(); checkCanvas(); } private void addPoint() { // add the point whose coords are specified in the x and y input boxes; // if the x-value already exits, then the corresponding y-value is changed. double x,y; try { xInput.checkInput(); x = xInput.getVal(); } catch (JCMError e) { canvas.setErrorMessage(null,"The input for x does is not a legal real number."); xInput.requestFocus(); xInput.selectAll(); return; } try { yInput.checkInput(); y = yInput.getVal(); } catch (JCMError e) { canvas.setErrorMessage(null,"The input for y does is not a legal real number."); yInput.requestFocus(); yInput.selectAll(); return; } String str = makePointString(x,y); int index = function.findPoint(x); if (index >= 0 && y == function.getY(index)) { xInput.requestFocus(); xInput.selectAll(); return; } int newindex = function.addPoint(x,y); if (index >= 0) pointList.replaceItem(str,index); else pointList.addItem(str,newindex); deleteButton.setEnabled(pointList.getSelectedIndex() != -1); checkCanvas(); if (onChange != null) onChange.compute(); xInput.requestFocus(); xInput.selectAll(); } private String makePointString(double x, double y) { // Make a string representing (x,y), using exactly 11 spaces for each number. String X = NumUtils.realToString(x); String Y = NumUtils.realToString(y); if (X.length() < 11) X = " ".substring(0,11 - X.length()) + X; if (Y.length() < 11) Y = " ".substring(0,11 - Y.length()) + Y; return X + " " + Y; } private void selectPoint() { // React when user selects a point in the list of points. int index = pointList.getSelectedIndex(); if (index >= 0) { xInput.setVal(function.getX(index)); yInput.setVal(function.getY(index)); yInput.requestFocus(); yInput.selectAll(); } deleteButton.setEnabled(index >= 0); } private void changeStyle() { // React when user changes style of function. int newstyle = 0; Checkbox selected = styleGroup.getSelectedCheckbox(); for (int i = 1; i < 5; i++) if (selected == styleCheckbox[i]) newstyle = i; if (function.getStyle() == newstyle) return; function.setStyle(newstyle); canvas.doRedraw(); if (onChange != null) onChange.compute(); } private void checkCanvas() { // Adjust limits on canvas, if necessary, and redraw it. int ct = function.getPointCount(); double newXmin = -1, newXmax = 1, newYmin = -1, newYmax = 1; if (ct > 0) { if (ct == 1) { newXmin = function.getX(0); if (Math.abs(newXmin) < 10000) { newXmax = newXmin + 1; newXmin -= 1; } else { newXmax = newXmin - Math.abs(newXmin)/10; newXmin -= Math.abs(newXmin)/10; } } else { newXmin = function.getX(0); newXmax = function.getX(ct - 1); } newYmin = function.getY(0); newYmax = newYmin; for (int i = 1; i < ct; i++) { double y = function.getY(i); if (y < newYmin) newYmin = y; else if (y > newYmax) newYmax = y; } double size = Math.abs(newYmin - newYmax); if (size < 1e-10 && Math.abs(newYmin) < 10000 && Math.abs(newYmax) < 10000) { newYmax += 1; newYmin -= 1; } else { newYmax += size*0.15; newYmin -= size*0.15; } } CoordinateRect coords = canvas.getCoordinateRect(0); double curSize = Math.abs(coords.getYmin() - coords.getYmax()); double newSize = Math.abs(newYmax - newYmin); if (newXmax != coords.getXmax() || newXmin != coords.getXmin() || newSize > 1.3*curSize || newSize < 0.5*curSize || newYmax > coords.getYmax() - 0.1*curSize || newYmin < coords.getYmin() + 0.1*curSize) coords.setLimits(newXmin,newXmax,newYmin,newYmax); canvas.doRedraw(); } /** * Leave a 3-pixel gap around the edges of the panel. Not meant to be called directly. */ public Insets getInsets() { return new Insets(3,3,3,3); } /** * React when user clicks one of the buttons or presses return in one * of the input boxes. Not meant to be called directly. */ public void actionPerformed(ActionEvent evt) { Object source = evt.getSource(); if (source == deleteButton) deletePoint(); else if (source == clearButton) clearAllPoints(); else if (source == xInput) { yInput.requestFocus(); yInput.selectAll(); } else addPoint(); } /** * React when user clicks on a point in the list of points or clicks one * of the radio buttons for specifying the style of the function. * Not meant to be called directly. */ public void itemStateChanged(ItemEvent evt) { if (evt.getSource() == pointList) selectPoint(); else changeStyle(); } //-------------------- Dragging -------------------------- private int dragPoint = -1; // -1 if no point is being dragged; // Otherwise, the index of the point being dragged. private int startX, startY; // Point where mouse was clicked at start of drag. private int prevY; // Previous position of mouse during dragging. private boolean moved; // Becomes true once the clicked point has actually // been dragged a bit. If the mouse is released before // the point is moved at least 3 pixels, then the associated // y-value is not changed. /** * Method required by the MouseListener interface. Defined here to * support dragging of points on the function's graph. Not meant to be called directly. */ public void mousePressed(MouseEvent evt) { dragPoint = -1; moved = false; int ct = function.getPointCount(); CoordinateRect coords = canvas.getCoordinateRect(0); for (int i = 0; i < ct; i++) { int x = coords.xToPixel(function.getX(i)); int y = coords.yToPixel(function.getY(i)); if (evt.getX() >= x-3 && evt.getX() <= x+3 && evt.getY() >= y-3 && evt.getY() <= y+3) { startX = evt.getX(); prevY = startY = evt.getY(); pointList.deselect(pointList.getSelectedIndex()); pointList.select(i); selectPoint(); dragPoint = i; return; } } } /** * Method required by the MouseListener interface. Defined here to * support dragging of points on the function's graph. Not meant to be called directly. */ public void mouseReleased(MouseEvent evt) { if (dragPoint == -1) return; if (!moved) { dragPoint = -1; return; } mouseDragged(evt); pointList.replaceItem(makePointString(function.getX(dragPoint),function.getY(dragPoint)), dragPoint); pointList.select(dragPoint); dragPoint = -1; if (onChange != null) onChange.compute(); } /** * Method required by the MouseListener interface. Defined here to * support dragging of points on the function's graph. Not meant to be called directly. */ public void mouseDragged(MouseEvent evt) { if (dragPoint == -1 || prevY == evt.getY()) return; if (!moved && Math.abs(evt.getY() - startY) < 3) return; moved = true; int y = evt.getY(); CoordinateRect coords = canvas.getCoordinateRect(0); if (y < coords.getTop() + 4) y = coords.getTop() + 4; else if (y > coords.getTop() + coords.getHeight() - 4) y = coords.getTop() + coords.getHeight() - 4; if (Math.abs(evt.getX() - startX) > 72) y = startY; if (y == prevY) return; prevY = y; function.setY(dragPoint, coords.pixelToY(prevY)); yInput.setVal(function.getY(dragPoint)); canvas.doRedraw(); } /** * Empty method, required by the MouseListener interface. */ public void mouseClicked(MouseEvent evt) { } /** * Empty method, required by the MouseMotionListener interface. */ public void mouseEntered(MouseEvent evt) { } /** * Empty method, required by the MouseMotionListener interface. */ public void mouseExited(MouseEvent evt) { } /** * Empty method, required by the MouseMotionListener interface. */ public void mouseMoved(MouseEvent evt) { } //-------------------------------------------------------- private class Draw extends Drawable{ // An object of this nested class is added to the canvas // of the TableFunctionInput panel. It is responsible for // drawing the function. public void draw(Graphics g, boolean coordsChanged) { int ct = function.getPointCount(); if (ct == 0) return; g.setColor(Color.magenta); int xInt,yInt, aInt, bInt; double x,y, a,b; switch (function.getStyle()) { case TableFunction.SMOOTH: { if (ct > 1) { try { x = function.getX(0); y = function.getVal(x); xInt = coords.xToPixel(x); yInt = coords.yToPixel(y); int limit = coords.xToPixel(function.getX(ct-1)); aInt = xInt; while (aInt < limit) { aInt += 3; if (aInt > limit) aInt = limit; a = coords.pixelToX(aInt); b = function.getVal(a); bInt = coords.yToPixel(b); g.drawLine(xInt,yInt,aInt,bInt); xInt = aInt; yInt = bInt; } } catch (Exception e) { e.printStackTrace(); } } break; } case TableFunction. PIECEWISE_LINEAR: { x = function.getX(0); xInt = coords.xToPixel(x); y = function.getY(0); yInt = coords.yToPixel(y); for (int i = 1; i < ct; i++) { a = function.getX(i); aInt = coords.xToPixel(a); b = function.getY(i); bInt = coords.yToPixel(b); g.drawLine(xInt,yInt,aInt,bInt); xInt = aInt; yInt = bInt; } break; } case TableFunction.STEP: { x = function.getX(0); xInt = coords.xToPixel(x); for (int i = 0; i < ct; i++) { if (i < ct - 1) { double nextX = function.getX(i+1); a = (x + nextX)/2; x = nextX; } else a = x; aInt = coords.xToPixel(a); y = function.getY(i); yInt = coords.yToPixel(y); g.drawLine(xInt,yInt,aInt,yInt); xInt = aInt; } break; } case TableFunction.STEP_LEFT: { x = function.getX(0); xInt = coords.xToPixel(x); for (int i = 1; i < ct; i++) { a = function.getX(i); aInt = coords.xToPixel(a); y = function.getY(i-1); yInt = coords.yToPixel(y); g.drawLine(xInt,yInt,aInt,yInt); xInt = aInt; } break; } case TableFunction.STEP_RIGHT: { x = function.getX(0); xInt = coords.xToPixel(x); for (int i = 1; i < ct; i++) { a = function.getX(i); aInt = coords.xToPixel(a); y = function.getY(i); yInt = coords.yToPixel(y); g.drawLine(xInt,yInt,aInt,yInt); xInt = aInt; } break; } } for (int i = 0; i < ct; i++) { x = function.getX(i); y = function.getY(i); xInt = coords.xToPixel(x); yInt = coords.yToPixel(y); g.fillOval(xInt-2,yInt-2,5,5); } } } } jcm1-source/edu/hws/jcm/functions/WrapperFunction.java0000644000076500011320000002051211741343635022337 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.functions; import edu.hws.jcm.data.*; /** * A WrapperFunction contains another function and delegates to it * all calls to methods from the Function interface, except for calls * to setName() and getName(). (It maintains its own name, which can * be different from the name of the wrapped Function.) This has at least two * uses: A WrapperFunction is a FunctionParserExtension, so it can * be added to a parser and then used in expressions parsed in that * parser. Second, the function that is wrapped inside the WrapperFunction * object can be changed by a call to the setFunction() method. * This makes it possible to have a single function that can be used, * for example, in ValueMath objects and Graph1D's, whose definition * can be changed at will. Note that it is NOT legal to change the * arity of the function when calling the setFunction() method. */ public class WrapperFunction extends FunctionParserExtension { private Function func; // The non-null function. private double[] params; // For use in evaluating the function in the apply() method; // has length equal to func.getArity(); private int serialNumber; // serial number goes up when function def is changed; // this is used in the check() method for synchronizing derivatives. private WrapperFunction derivativeOf; // If non-null, then this function is a // derivative of the specified function. private Variable derivativeVar; // If non-null, then the derivative was taken w.r.t. this variable private int derivativeIndex; // If derivativeVar is null, this is the argument // number with respect to which the derivative was taken. /** * Create a WrapperFunction object containing a specified function. * * @param f The non-null function that will be contained in the WrapperFunction. */ public WrapperFunction(Function f) { setFunction(f); serialNumber = 0; } private void check() { // Called if this function is the derivative of another wrapper function to see // if the serial number of the parent function has changed. If so, the derivative is // recomputed. if (derivativeOf == null || derivativeOf.serialNumber == serialNumber) return; serialNumber = derivativeOf.serialNumber; if (derivativeVar != null) func = derivativeOf.derivative(derivativeVar); else func = derivativeOf.derivative(derivativeIndex); } /** * Set the function that is contained in this WrapperFunction. * * @param f The non-null function to be used in this WrapperFunction object. * It must have the same arity as the current function. */ public void setFunction(Function f) { if (f == null) throw new IllegalArgumentException("Function supplied to WrapperFunction object can't be null."); if (func != null && f.getArity() != func.getArity()) throw new IllegalArgumentException("Attempt to change the arity of a WrapperFunction."); if (derivativeOf != null) throw new IllegalArgumentException("Can't change the definition of a function that is a derivative of another function."); func = f; params = new double[f.getArity()]; serialNumber++; } /** * Return the function that is currently wrapped in this WrapperFunction. */ public Function getFunction() { return func; } /** * Return the number of arguments of this function. */ public int getArity() { return func.getArity(); } /** * Find the value of the function at the argument value * argument[0], .... The number of arguments should match * the arity of the function. */ public double getVal( double[] arguments ) { check(); return func.getValueWithCases(arguments,null); } /** * Find the value of the function at the argument values * argument[0],.... Information about "cases" is stored in * the Cases parameter, if it is non-null. See the Cases * class for more information. */ public double getValueWithCases( double[] arguments, Cases cases ) { check(); return func == null? 1 : func.getValueWithCases(arguments,cases); } /** * Return the derivative of the function with repect to * argument number wrt, where arguments are numbered starting from 1. */ public Function derivative(int wrt) { check(); WrapperFunction deriv = new WrapperFunction(func.derivative(wrt)); deriv.derivativeOf = this; deriv.derivativeIndex = wrt; deriv.serialNumber = serialNumber; return deriv; } /** * Return the derivative of the function with respect to the * variable x (where x is NOT one of the parameters of the function). */ public Function derivative(Variable x) { check(); WrapperFunction deriv = new WrapperFunction(func.derivative(x)); deriv.derivativeOf = this; deriv.derivativeVar = x; deriv.serialNumber = serialNumber; return deriv; } /** * Return true if the definition of this function depends * in some way on the variable x. (Note that the function does * NOT depend on the variables that are being used as its parameters!) */ public boolean dependsOn(Variable x) { check(); return func.dependsOn(x); } /** * Evaluate the function applied to argument values popped from the stack, * and leave the result on the stack. This is not meant to be called * directly. */ public void apply(StackOfDouble stack, Cases cases) { check(); for (int i = params.length - 1; i >= 0; i--) params[i] = stack.pop(); stack.push( getValueWithCases(params, cases) ); } } jcm1-source/edu/hws/jcm/functions/CubicSegment.java0000644000076500011320000001245511741343635021570 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.functions; /** * This non-public class is for use with TableFunctions. It defines one segment * of a TableFunction whose style is TableFunction.SMOOTH. A cubic segment is * a segment of a cubic polynomial. It is defined by six numbers: two x-coordinates, * the y-value at each x-coordinate, and the derivative at each x-coordinate. */ class CubicSegment { private double x1,x2, // x-ccords at endpoints with x1 < x2 y1,y2, // y-coords at endpoints d1,d2; // derivatives at endpoints private double a,b,c,d; // coefficients in a(x-x1)^3 + b(x-x1)^2(x-x2) + ... CubicSegment() { } CubicSegment(double x1, double x2, double y1, double y2, double d1, double d2) { setData(x1, x2, y1, y2, d1, d2); } void setData(double nx1, double nx2, double ny1, double ny2, double nd1, double nd2) { double temp; if (nx1 == nx2) throw new IllegalArgumentException("Attempt to make CubicSegment of length 0"); if (nx1 > nx2) { temp = nx1; nx1 = nx2; nx2 = temp; temp = ny1; ny1 = ny2; ny2 = temp; temp = nd1; nd1 = nd2; nd2 = temp; } x1 = nx1; x2 = nx2; y1 = ny1; y2 = ny2; d1 = nd1; d2 = nd2; temp = (x2 - x1); a = y2/(temp*temp*temp); b = d2/(temp*temp) - 3*a; temp = -temp; d = y1/(temp*temp*temp); c = d1/(temp*temp) - 3*d; } void setDerivativesFromNeighbors(double leftX, double leftY, double rightX, double rightY) { double nd1,nd2; if (!Double.isNaN(leftX) && leftX < x1) nd1 = (y2 - leftY) / (x2 - leftX); else nd1 = (y2 - y1) / (x2 - x1); if (!Double.isNaN(rightX) && rightX > x2) nd2 = (rightY - y1) / (rightX - x1); else nd2 = (y2 - y1) / (x2 - x1); setData(x1,x2,y1,y2,nd1,nd2); } double value(double x) { // should have x1 <= x <= x2, but not required return derivativeValue(x,0); } double derivativeValue(double x, int derivativeOrder) { // Assume derivativeOrder >= 0. // This function, unlike value() returns the value that represents the // one-sided limit at the endpoint, even if that endpoint is not // in the domain. switch (derivativeOrder) { case 0: double t1 = x - x1; double t2 = t1*t1; double t3 = t2*t1; double s1 = x - x2; double s2 = s1*s1; double s3 = s2*s1; return a*t3 + b*t2*s1 + c*t1*s2 + d*s3; case 1: return ((3*a+b)*(x-x1)*(x-x1) + 2*(b+c)*(x-x1)*(x-x2) + (3*d+c)*(x-x2)*(x-x2)); case 2: return 2*( (3*a+2*b+c)*(x-x1) + (3*d+2*c+b)*(x-x2) ); case 3: return 6*(2*a+b+c); default: return 0; } } } jcm1-source/edu/hws/jcm/functions/TableFunction.java0000644000076500011320000006024111741343635021751 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.functions; import edu.hws.jcm.data.*; /** * A TableFunction is a function that is specified by a table of (x,y)-points. * Values are interpolated between the specified x-values. This can be done * in several differnt ways; the method that is used is controlled by the * "Style" property. Since a TableFunction extends FunctionParserExtension, * a TableFunction can be added to a Parser (provided it has a name), and * it can then be used in expressions parsed by that parser. Note that this * class is meant to be used for functions that are defined by a fairly * small number of points, since each function evaluation involves a linear * search through the list of x-values of the defining points. */ public class TableFunction extends FunctionParserExtension { /** * If the style of the function is set to SMOOTH, * then cubic interpolation is used to find the value * of the functions for x-values between the points that define the function. */ public static final int SMOOTH = 0; /** * If the style of the function is set to PIECEWISE_LINEAR, * then linear interpolation is used to find the value * of the functions for x-values between the points that define the function. */ public static final int PIECEWISE_LINEAR = 1; /** * If the style of the function is set to STEP, then the function is * piecewise constant, and the value of the function at x is taken * from the nearest point in the list of points that define the function. */ public static final int STEP = 2; /** * If the style of the function is set to STEP_LEFT, then the function is * piecewise constant, and the value of the function at x is taken * from the nearest point to the left in the list of points that define the function. */ public static final int STEP_LEFT = 3; /** * If the style of the function is set to STEP_RIGHT, then the function is * piecewise constant, and the value of the function at x is taken * from the nearest point to the right in the list of points that define the function. */ public static final int STEP_RIGHT = 4; private int style; // Type of function, given by one of the above constants. private double[] xCoords = new double[10]; // x-coordinates, in increasing order private double[] yCoords = new double[10]; // the corresponding y-coordinates private CubicSegment[] segments; // interpolation data for SMOOTH style private int pointCt; // number of points (possibly less than xCoords.length) /** * Create a TableFunction with SMOOTH style and no points. */ public TableFunction() { this(SMOOTH); } /** * Create a TableFunction with specified style and no points. * * @param style The style for the function: SMOOTH, PIECEWISE_LINEAR, STEP, * STEP_LEFT, or STEP_RIGHT. */ public TableFunction(int sytle) { this.style = style; if (style == SMOOTH) segments = new CubicSegment[9]; } /** * Copy data from another TableFunction, except that the name of the funcion is * not duplicated. The new TableFunction is nameless. */ public void copyDataFrom(TableFunction source) { xCoords = (double[])source.xCoords.clone(); yCoords = (double[])source.yCoords.clone(); style = -1; // Force setStyle to compute data for SMOOTH style. setStyle(source.style); } /** * Set the style of this TableFunction, to specify how values are interpolated * between points on the curve. * * @param style One of the style constants SMOOTH, PIECEWISE_LINEAR, STEP, * STEP_LEFT, STEP_RIGHT. Other values are ignored. */ public void setStyle(int style) { if (style == this.style || style < 0 || style > 4) return; this.style = style; if (style == SMOOTH) { segments = new CubicSegment[xCoords.length - 1]; for (int i = 0; i < pointCt-1; i++) segments[i] = new CubicSegment(xCoords[i],xCoords[i+1],yCoords[i],yCoords[i+1],0,0); for (int i = 0; i < pointCt-1; i++) interpolateDerivatives(i); } else { segments = null; } } /** * Get the style of this TableFunction, which specifies how values are * interpolated between points on the curve. * * @return The style of this TableFunction. This is one of the constants * SMOOTH, PIECEWISE_LINEAR, STEP, STEP_LEFT, or STEP_RIGHT. */ public int getStyle() { return style; } /** * Add points to the table. The x-coordinates of the points are taken from * the xCoords array. The y-coordinate for the i-th point is yCoords[i], if * an i-th position exists in this array. Otherwise, the y-coordinate is * is zero. (Note that if xCoords[i] duplicates an x-value already in * the table, then no new point is added but the corresponging y-value is changed.) * * @param xCoords A list of x-coordinates to be added to the table. If this is * null, then nothing is done. * @param yCoords The value of yCoords[i], if it exists, is the y-coordinate * corresponding to xCoords[i]. Otherwise, the y-coordinate is undefined. * This can be null, in which case all y-coordinates are zero. */ public void addPoints(double[] xCoords, double[] yCoords) { if (xCoords == null) return; int ct = xCoords.length; if (yCoords == null) ct = 0; else if (yCoords.length < ct) ct = yCoords.length; for (int i = 0; i < ct; i++) addPoint(xCoords[i], yCoords[i]); for (int i = ct; i < xCoords.length; i++) addPoint(xCoords[i],0); } /** * Add points to the table. The number of points added is intervals + 1. * The x-coordinates are evenly spaced between xmin and xmax. The y-coordinates * are zero. * * @param intervals The number of intervals. The number of points added is intervals + 1. * The value should be at least 1. If not, nothing is done. * @param xmin The minimim x-coordinate for added points. * @param xmax The maximum x-coodinate for added points. Should be greater than * xmin, for efficiency, but no error occurs if it is not. */ public void addIntervals(int intervals, double xmin, double xmax) { if (intervals < 1) return; double dx = (xmax - xmin) / intervals; for (int i = 0; i < intervals; i++) addPoint(xmin + i*dx, 0); addPoint(xmax, 0); } /** * Add a point with the specified x and y coordinates. If a point with the * given x coordinate already exists in the table, then no new point is added, * but the associated y-value is changed. * (If x is Double.NaN, then no change is made and the return value is -1.) * * @param x The x-coordinate of the point to be added or modified. * @param y The y-coordinate of the point. * @return the position of the point in the list of points, where the first point is at position zero. */ public int addPoint(double x, double y) { if (Double.isNaN(x)) return -1; int pos = 0; while (pos < pointCt && xCoords[pos] < x) pos++; if (pos < pointCt && xCoords[pos] == x) { yCoords[pos] = y; } else { if (pointCt == xCoords.length) { double[] temp = new double[2*xCoords.length]; System.arraycopy(xCoords,0,temp,0,xCoords.length); xCoords = temp; temp = new double[2*yCoords.length]; System.arraycopy(yCoords,0,temp,0,yCoords.length); yCoords = temp; if (style == SMOOTH) { CubicSegment[] temps = new CubicSegment[xCoords.length - 1]; System.arraycopy(segments,0,temps,0,pointCt - 1); segments = temps; } } for (int i = pointCt; i > pos; i--) { xCoords[i] = xCoords[i-1]; yCoords[i] = yCoords[i-1]; } xCoords[pos] = x; yCoords[pos] = y; if (style == SMOOTH && pointCt > 0) { if (pos == pointCt) segments[pointCt - 1] = new CubicSegment(); else { for (int i = pointCt-1; i > pos; i--) segments[i] = segments[i-1]; segments[pos] = new CubicSegment(); } } pointCt++; } if (style == SMOOTH && pointCt > 0) { // make sure segment data is OK for segments that depend on (x,y) if (pos > 0) segments[pos-1].setData(xCoords[pos-1],xCoords[pos],yCoords[pos-1],yCoords[pos],0,0); if (pos < pointCt-1) segments[pos].setData(xCoords[pos],xCoords[pos+1],yCoords[pos],yCoords[pos+1],0,0); for (int i = pos - 2; i <= pos + 1; i++) interpolateDerivatives(i); } return pos; } private void interpolateDerivatives(int pos) { // Compute correct derivatives for segments[pos] from data in coordinate arrays. if (pos < 0 || pos > pointCt - 2) return; // pointCt must be >= 2 if (pointCt == 2) // pos must be 0 segments[0].setDerivativesFromNeighbors(Double.NaN,0,Double.NaN,0); else if (pos == 0) segments[0].setDerivativesFromNeighbors(Double.NaN,0,xCoords[2],yCoords[2]); else if (pos == pointCt - 2) segments[pointCt-2].setDerivativesFromNeighbors(xCoords[pointCt-3],yCoords[pointCt-3],Double.NaN,0); else segments[pos].setDerivativesFromNeighbors(xCoords[pos-1],yCoords[pos-1],xCoords[pos+2],yCoords[pos+2]); } /** * Gets the number of points in the table. */ public int getPointCount() { return pointCt; } /** * Get the x-coordinate in the i-th point, where the first point * is number zero. Throws an IllegalArgumentException if i is * less than zero or greater than or equal to the number of points. */ public double getX(int i) { if (i >= 0 && i < pointCt) return xCoords[i]; else throw new IllegalArgumentException("Point index out of range: " + i); } /** * Get the y-coordinate in the i-th point, where the first point * is number zero. Throws an IllegalArgumentException if i is * less than zero or greater than or equal to the number of points. */ public double getY(int i) { if (i >= 0 && i < pointCt) return yCoords[i]; else throw new IllegalArgumentException("Point index out of range: " + i); } /** * Set the y-coordinate in the i-th point to y, where the first point * is number zero. Throws an IllegalArgumentException if i is * less than zero or greater than or equal to the number of points. */ public void setY(int i, double y) { if (i >= 0 && i < pointCt) yCoords[i] = y; else throw new IllegalArgumentException("Point index out of range: " + i); if (style == SMOOTH) { if (i > 0) segments[i-1].setData(xCoords[i-1],xCoords[i],yCoords[i-1],yCoords[i],0,0); if (i < pointCt - 1) segments[i].setData(xCoords[i],xCoords[i+1],yCoords[i],yCoords[i+1],0,0); for (int j = i-2; j <= i+1; j++) interpolateDerivatives(j); } } /** * If there is a point in the list with x-coordinate x, then this function returns * the index of that point in the list (where the index of the first point is zero). * If there is no such point, then -1 is returned. */ public int findPoint(double x) { int i = 0; while (i < pointCt) { if (x == xCoords[i]) return i; else if (x > xCoords[i]) i++; else break; } return -1; } /** * Removes the i-th point from the list of points. Throws an IllegalArgumentException if i is * less than zero or greater than or equal to the number of points. */ public void removePointAt(int i) { if (i < 0 || i >= pointCt) throw new IllegalArgumentException("Point index out of range: " + i); pointCt--; for (int j = i; j < pointCt; j++) { xCoords[j] = xCoords[j+1]; yCoords[j] = yCoords[j+1]; } if (style == SMOOTH) { style = -1; // force recompute of data setStyle(SMOOTH); } } /** * Remove all points. The resulting function is undefined everywhere. */ public void removeAllPoints() { pointCt = 0; xCoords = new double[10]; yCoords = new double[10]; } /** * Get the value of the function at x, using interpolation if x lies between * two x-coordinates in the list of points that define the function. If x is * outside the range of x-coords in the table, the value of the function is Double.NaN. */ public double getVal(double x) { return computeValue(x,null,0); } private double computeValue(double x, Cases cases, int derivativeLevel) { // Find the value of the function or one of its derivatives at x. // If cases is not null, then a value is added to cases to help // with continuity computations. if (Double.isNaN(x)) return Double.NaN; if (pointCt == 0 || x < xCoords[0] || x > xCoords[pointCt-1]) return Double.NaN; if (pointCt == 1) { if (derivativeLevel > 0) return Double.NaN; else { if (cases != null) cases.addCase(0); return yCoords[0]; } } int casenum; double ans; int seg = 0; switch (style) { case STEP: { while (seg < pointCt - 1 && x > (xCoords[seg] + xCoords[seg+1])/2) seg++; casenum = seg; if (derivativeLevel == 0) ans = yCoords[seg]; else if (x < (xCoords[seg] + xCoords[seg+1])/2 || seg == pointCt - 1 || yCoords[seg] == yCoords[seg + 1]) ans = 0; else ans = Double.NaN; break; } case STEP_RIGHT: { while (seg < pointCt-1 && x > xCoords[seg]) seg++; casenum = seg; if (derivativeLevel == 0) ans = yCoords[seg]; else if (x < xCoords[seg] || seg >= pointCt - 1 || yCoords[seg] == yCoords[seg+1]) ans = 0; else ans = Double.NaN; break; } case STEP_LEFT: { while (seg < pointCt-1 && x >= xCoords[seg+1]) seg++; casenum = seg; if (derivativeLevel == 0) ans = yCoords[seg]; else if (x > xCoords[seg] || seg == 0 || yCoords[seg] == yCoords[seg-1]) ans = 0; else ans = Double.NaN; break; } case PIECEWISE_LINEAR: { while (seg < pointCt-1 && x > xCoords[seg]) seg++; casenum = seg; if (x == xCoords[seg]) { if (derivativeLevel == 0) ans = yCoords[seg]; else if (seg == 0) { if (derivativeLevel == 1) ans = (yCoords[1] - yCoords[0]) / (xCoords[1] - xCoords[0]); else ans = 0; } else if (seg == pointCt-1) { if (derivativeLevel == 1) ans = (yCoords[pointCt-1] - yCoords[pointCt-2]) / (xCoords[pointCt-1] - xCoords[pointCt-2]); else ans = 0; } else { double leftslope = (yCoords[seg] - yCoords[seg-1]) / (xCoords[seg] - xCoords[seg-1]); double rightslope = (yCoords[seg] - yCoords[seg+1]) / (xCoords[seg] - xCoords[seg+1]); if (Math.abs(leftslope - rightslope) < 1e-12) { if (derivativeLevel == 1) ans = leftslope; else ans = 0; } else ans = Double.NaN; } } else { // x < xCoords[seg] && x > xCoords[seg-1] if (derivativeLevel == 0) { ans = yCoords[seg-1] + ((yCoords[seg] - yCoords[seg-1]) / (xCoords[seg] - xCoords[seg-1]))*(x - xCoords[seg-1]); } else if (derivativeLevel == 1) { ans = (yCoords[seg] - yCoords[seg-1]) / (xCoords[seg] - xCoords[seg-1]); } else ans = 0; } break; } default: { // SMOOTH while (seg < pointCt-2 && x > xCoords[seg+1]) seg++; casenum = seg; if (x == xCoords[seg+1] && seg < pointCt-2 && seg > 0) { if (derivativeLevel == 0) ans = yCoords[seg+1]; else if (derivativeLevel == 1) ans = segments[seg].derivativeValue(x,1); else { double leftslope = segments[seg-1].derivativeValue(x,2); double rightslope = segments[seg].derivativeValue(x,2); if (Math.abs(leftslope - rightslope) < 1e-12) ans = segments[seg].derivativeValue(x,derivativeLevel); else ans = Double.NaN; } } else { // x < xCoords[seg] && x > xCoords[seg-1] ans = segments[seg].derivativeValue(x,derivativeLevel); } } } if (cases != null) cases.addCase(casenum); return ans; } //---------------- Methods from the Function class ----------------- /** * Get the value of the function at the specified parameter value. * * @params param should be an array of length 1 holding the argument of the function. * However if the length is greater than one, the extra arguments are simply ignored. * @cases if cases is non-null, a case value is stored here, for help in continuity computations. */ public double getValueWithCases(double[] params, Cases cases) { return computeValue(params[0],cases,0); } /** * Get the value of the function at the specified parameter value. * * @partam param should be an array of length 1 holding the argument of the function. * However if the length is greater than one, the extra arguments are simply ignored. */ public double getVal(double[] params) { return computeValue(params[0], null, 0); } /** * Compute the derivative of this function. The value of the parameter, wrt, must be 1 or an * IllegalArguemntException will be thrown. */ public Function derivative(int wrt) { if (wrt != 1) throw new IllegalArgumentException("Attempt to take the derivative of a function of one argument with respect to argument number " + wrt); return new Deriv(this); } /** * Returns null. * It really should be the constant function zero, but I don't expect this ever to be * called. Since dependsOn(wrt) returns false, it will never be called within the JCM system. */ public Function derivative(Variable wrt) { return null; } /** * Returns false. */ public boolean dependsOn(Variable wrt) { return false; } /** * Returns the arity of the function, which is 1. */ public int getArity() { return 1; } /** * Override method apply() from interface FunctionParserExtension, to handle cases properly. * Not meant to be called directly. */ public void apply(StackOfDouble stack, Cases cases) { double x = stack.pop(); stack.push(computeValue(x,cases,0)); } //---------------- For creating derivatives ------------------------ private static class Deriv extends FunctionParserExtension { // An object of this class represents a derivative function // of a TableFunction. TableFunction derivativeOf; // The function from which this function is derived. int derivativeLevel; // The order of the derivative. Deriv(Deriv f) { derivativeLevel = f.derivativeLevel + 1; derivativeOf = f.derivativeOf; } Deriv(TableFunction f) { derivativeLevel = 1; derivativeOf = f; } public String getName() { // Name comes from the name of the function. String name = derivativeOf.getName(); for (int i = 0; i < derivativeLevel; i++) name += "'"; return name; } public void setName(String name) { } public double getValueWithCases(double[] params, Cases cases) { return derivativeOf.computeValue(params[0],cases,derivativeLevel); } public double getVal(double[] params) { return derivativeOf.computeValue(params[0],null,derivativeLevel); } public Function derivative(int wrt) { if (wrt != 1) throw new IllegalArgumentException("Attempt to take the derivative of a function of one argument with respect to argument number " + wrt); return new Deriv(this); } public Function derivative(Variable wrt) { return null; } public boolean dependsOn(Variable wrt) { return false; } public int getArity() { return 1; } public void apply(StackOfDouble stack, Cases cases) { double x = stack.pop(); stack.push(derivativeOf.computeValue(x,cases,derivativeLevel)); } } // end class Deriv } jcm1-source/edu/hws/jcm/functions/FunctionParserExtension.java0000644000076500011320000003150111741343635024050 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.functions; import edu.hws.jcm.data.*; /** * An object belonging to a concrete subclass of FunctionParserExtesion is a * mathematical function that can be registered with a Parser and then used in * strings that are parsed by that parser. (See the Function inteface * for more information.) * *

Since a FunctionParserExtension object implements the ParserExtension interface, it * has a name and can be registered with a Parser. When the parser * encounters the name of a function in a string, it turns control * of the parsing process over to the function. When a Function * occurs in an ExpressionProgram, the Function is responsible for * evaluating itself and for differentiating itself. This functionality * is defined in the abstract class FunctionParserExtension. The * concrete subclasses of FunctionParserExtension represent actual * functions. They must implement the five methods defined * in the Function interface, but most of the parser- and expression-related * behavior can probably be inherited from this class. (This is good, since * the programming is rather tricky.) * *

(The point of all this is that Parsers and Expressions can deal * with Functions, even though there is no reference to Functions in * the Parser or ExpressionProgram classes. Everything is done * through the ParserExtension interface.) */ abstract public class FunctionParserExtension implements Function, ParserExtension, ExpressionCommand { /** * The name of this MathObject, possibly null. */ protected String name; private boolean parensCanBeOptional; // This variable applies only to functions of // arity 1. It affects the parsing of references // to the function, and then only if the // OPTIONAL_PARENS option is set in the parser. // If both this variable and OPTIONAL_PARENS // are set, then parentheses are optional // around the argument of the function. /** * Call this function with b = true if this is a function of one variable * and you want it to behave like a standard function in that parentheses * can be optional around the argument of the function. For the parentheses * to be treated as optional, the option Parser.OPTIONAL_PARENS must ALSO * be set in the parser. As an example, if you wanted to define the function * sinh and allow the syntax "sinh x", you would turn this option on in * the Function and turn OTPTIONAL_PARENS on in the Parser. * * @param b set whether parenthesis are optional in one variable functions. */ public void setParensCanBeOptional( boolean b ) { parensCanBeOptional = b; } //------ Methods from the MathObject interfaces ---------- /** * Set the name of this object. It is not a good idea to do this * if the object has been registered with a Parser. */ public void setName(String name) { this.name = name; } /** * Get the name of this MathObject. */ public String getName() { return name; } //----------------- Method from the ParserExtension interface ----------- /** * If this ParserExtension is registered with a parser and the parser * encounters the name of the function in the string it is parsing, * then the parser will call this routine. This routine parses * the function's parameter list and generates the code for evaluating * the function, applied to those parameters. When this routine is * called, the name of the function has already been read. The code * that is generated consists of code to evalueate each of the parameters, * leaving the results on the stack, followed by an application of the * function. * * @param parser parser that is parsing the string. * @param context the ParseContext in effect at the time this method is called. */ public void doParse(Parser parser, ParserContext context) { int tok = context.next(); String open = context.tokenString; if (tok == ParserContext.OPCHARS && ( open.equals("(") || (open.equals("[") && (context.options & Parser.BRACKETS) != 0) || (open.equals("{") && (context.options & Parser.BRACES) != 0) )) { String close = open.equals("(") ? ")" : (open.equals("[") ? "]" : "}"); for (int i = 0; i < getArity(); i++) { if (parser.parseExpression(context)) throw new ParseError("An argument of a function cannot be a logical-valued expression.", context); tok = context.next(); if (i == getArity()-1) { if (tok == ParserContext.OPCHARS && context.tokenString.equals(",")) throw new ParseError("Too many parameters for function \"" + getName() + "\".", context); if (tok != ParserContext.OPCHARS || !context.tokenString.equals(close)) throw new ParseError("Expected a \"" + close + "\" at the end of the paramter list for function \"" + getName() + "\".", context); } else { if (tok != ParserContext.OPCHARS || ! context.tokenString.equals(",")) throw new ParseError("Exprected a comma followed by another argument for function \"" + getName() + "\".", context); } } } else if (getArity() == 1 && (context.options & Parser.OPTIONAL_PARENS) != 0 && parensCanBeOptional) { if (parser.parseTerm(context)) throw new ParseError("The argument of a function must be a numerical expression.", context); } else throw new ParseError("Parentheses required around parameter list of function \"" + getName() + "\".", context); context.prog.addCommandObject(this); } //--------------- Methods from the ExpressionCommand interface ---------- /** * Evaluate the function applied to argument values popped from the stack, * and leave the result on the stack. The argument values have already been * computed and placed on the stack when this is called. They should be * popped off the stack in reverse order. This general method can't deal * properly with "cases", so it will probably have to be overridden in * subclasses. */ public void apply(StackOfDouble stack, Cases cases) { double[] d = new double[getArity()]; for (int i = getArity() - 1; i >= 0; i--) d[i] = stack.pop(); stack.push( getVal(d) ); } /** * The function object occurs as a command at index myIndex in prog. The commands for computing * its arguments can be found in prog in positions preceding myIndex. Generate commands for computing * the derivative of the function reference with respect to the Variable wrt and add them to the deriv program. The * computation of the derivative uses the chain rule. * * @param prog program this function object occurs in. * @param myIndex index at which this function occurs. * @param deriv commands for computing the derivative are placed here. * @param wrt the derivative is taken with respect to this Variable. */ public void compileDerivative(ExpressionProgram prog, int myIndex, ExpressionProgram deriv, Variable wrt) { int[] opIndex = new int[getArity()]; int size = 1; for (int i = 0; i < getArity(); i++) { // Find the indices in prog of the arguments of the function. opIndex[getArity()-i-1] = myIndex - size; if (i < getArity()-1) size += prog.extent(myIndex-size); } boolean output = false; // Becomes true after fist term is output. if (dependsOn(wrt)) { output = true; for (int i = 0; i < opIndex.length; i++) prog.copyExpression(opIndex[i],deriv); deriv.addCommandObject((FunctionParserExtension)derivative(wrt)); } for (int i = 0; i < getArity(); i++) { if (prog.dependsOn(opIndex[i],wrt)) { for (int j = 0; j < opIndex.length; j++) prog.copyExpression(opIndex[j],deriv); deriv.addCommandObject((FunctionParserExtension)derivative(i+1)); prog.compileDerivative(opIndex[i], deriv, wrt); deriv.addCommand(ExpressionProgram.TIMES); if (output) deriv.addCommand(ExpressionProgram.PLUS); output = true; } } if (!output) prog.addConstant(0); } /** * Return the number of commands in prog that are part of this function reference, * including the space occupied by the commands that compute the values of the * function's arguments. * * @param prog program to check commands against. * @param myIndex index in program. * @return the number of commands in prog that are part of this function reference. */ public int extent(ExpressionProgram prog, int myIndex) { int size = 1; // Allow for the function itself. for (int i = 0; i < getArity(); i++) size = size + prog.extent(myIndex - size); // Add on the size of the next argument. return size; } /** * Append a string representation of the function and its arguments to the buffer * * @param prog program whose string representation is being generated. * @param myIndex index of this ExpressionCommand in prog. * @param buffer string representation is placed here. */ public void appendOutputString(ExpressionProgram prog, int myIndex, StringBuffer buffer) { int[] opIndex = new int[getArity()]; int size = 1; for (int i = 0; i < getArity(); i++) { // Find the locations in prog of the arguemnts of the function. opIndex[getArity()-i-1] = myIndex - size; if (i < getArity()-1) size += prog.extent(myIndex-size); } String name = getName(); buffer.append(name == null ? "(unnamed function)" : name); buffer.append('('); for (int i = 0; i < getArity(); i++) { prog.appendOutputString(opIndex[i],buffer); if (i < getArity()-1) { buffer.append(", "); } } buffer.append(')'); } } // end class FunctionParserExtesion jcm1-source/edu/hws/jcm/functions/TableFunctionGraph.java0000644000076500011320000004125411741343635022736 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.functions; import java.awt.*; import java.awt.event.*; import edu.hws.jcm.data.*; import edu.hws.jcm.awt.*; import edu.hws.jcm.draw.*; /** * A TableFunctionGraph is a Drawable object that can be added to a CoordinateRect (or DisplayCanvas). * It draws the graph of a specified TableFunction. * A TableFunction is a function and can also be graphed by an object of the class edu.hws.jcm.draw.Graph1D. * However, a TableFunctionGraph offers the option of showing the points from the table that defines * the function as small disks (true by default) and the option of making the graph "interactive" so * that the user can drag the points (false by default). */ public class TableFunctionGraph extends Drawable implements MouseListener, MouseMotionListener { private TableFunction function; // The function that is drawn. private boolean showPoints; // Are the points from the table shown as little disks? private boolean interactive; // Can user drag points? private Computable onDrag; // If interactive is true and onDrag is not null, then this // Computable's compute() method is called each time a // point moves as the user drags it. private Computable onFinishDrag; // If interactive is true and onFinishDraw is not null, then this // Computable's compute() method is called when the user // finishes a drag operation (if the point is actually moved). private Color color; // non-null color of the graph. /** * Create a TableFunctionGraph that initially draws no function. A function can be set * later with setFunction. */ public TableFunctionGraph() { this(null); } /** * Create a TableFunctionGraph to draw the specified TableFunction. */ public TableFunctionGraph(TableFunction function) { this.function = function; this.color = Color.magenta; showPoints = true; } /** * Set the function whose graph is drawn by this TableFunctionGraph. If the value is null, * nothing is drawn */ public void setFunction(TableFunction function) { this.function = function; needsRedraw(); } /** * Get the TableFunction whose graph is drawn by this TableFunctionGraph. If the value is null, * then no graph is drawn. */ public TableFunction getFunction() { return function; } /** * Specify a controller whose compute() method will be called repeatedly * as the user drags one of the points from the table function. This only * applies if the "interactive" property is true. */ public void setOnDrag(Computable c) { onDrag = c; } /** * Get the Computable that is notified as the user drags a point. */ public Computable getOnDrag() { return onDrag; } /** * Specify a controller whose compute() method will be called once * when the user finishes dragging one of the points from the table function. * This only applies if the "interactive" property is true. */ public void setOnFinishDrag(Computable c) { onFinishDrag = c; } /** * Get the Computable that is notified when the user finishes dragging a point. */ public Computable getOnFinishDrag() { return onFinishDrag; } /** * Set the value of the interactive property, which is true if the user can * modify the function by dragging the points from the table. The default is false. */ public void setInteractive(boolean interactive) { if (this.interactive == interactive) return; if (this.interactive && canvas != null) { canvas.removeMouseListener(this); canvas.removeMouseMotionListener(this); } this.interactive = interactive; if (this.interactive && canvas != null) { canvas.addMouseListener(this); canvas.addMouseMotionListener(this); } } /** * Get the value of the interactive property, which is true if the user can * modify the function by dragging the points from the table. */ public boolean getInteractive() { return interactive; } /** * Set the showPoints property, which determines whether the points * from the table that defines the function are visible as little * disks. The default is true; */ public void setShowPoints(boolean show) { showPoints = show; needsRedraw(); } /** * Get the showPoints property, which determines whether the points * from the table that defines the function are visible as little * disks. */ public boolean getShowPoints() { return showPoints; } /** * Set the color that is used for drawing the graph. The defalt is magenta. * If the specified Color value is null, the call to setColor is ignored. */ public void setColor(Color c) { if (c != null) { color = c; needsRedraw(); } } /** * Get the non-null color that is used for drawing the graph. */ public Color getColor() { return color; } /** * Sets the values of member variables canvas and coords. This is * designed to be called only by the CoordinateRect class. This overrides * Drawable.setOwnerData(); */ protected void setOwnerData(DisplayCanvas canvas, CoordinateRect coords) { if (interactive && this.canvas != null) { canvas.removeMouseListener(this); canvas.removeMouseMotionListener(this); } super.setOwnerData(canvas,coords); if (interactive && this.canvas != null) { canvas.addMouseListener(this); canvas.addMouseMotionListener(this); } } /** * Provided as a convenience. If the function for this TableFunctionGraph is non-null, * its style is set to the specified style, and the graph is redrawn. The parameter * should be one of the constants TableFunction.SMOOTH, TableFunction.PIECEWISE_LINEAR, * TableFunction.STEP, TableFunction.STEP_LEFT, or TableFunction.STEP_RIGHT. */ public void setFunctionStyle(int style) { if (function != null && function.getStyle() != style) { function.setStyle(style); needsRedraw(); } } /** * Override the draw() method from class Drawable to draw the function. * This is not meant to be called directly. */ public void draw(Graphics g, boolean coordsChanged) { if (function == null || coords == null) return; int ct = function.getPointCount(); if (ct == 0) return; int startPt; // The index of first point from the table that we have to consider int endPt; // The index of the last point from the table that we have to consider double xmin = coords.pixelToX(coords.getLeft()); double xmax = coords.pixelToX(coords.getLeft() + coords.getWidth()); if (function.getX(0) > xmax || function.getX(ct-1) < xmin) return; startPt = 0; while (startPt < ct-1 && function.getX(startPt+1) <= xmin) startPt++; endPt = ct-1; while (endPt > 1 && function.getX(endPt-1) >= xmax) endPt--; double x,y, a,b; // usually, two consecutive points on curve int xInt,yInt, aInt, bInt; // usually, pixel coordinates for the two points. g.setColor(color); switch (function.getStyle()) { case TableFunction.SMOOTH: { if (endPt > startPt) { x = function.getX(startPt); y = function.getVal(x); xInt = coords.xToPixel(x); yInt = coords.yToPixel(y); double limit = xmax; if (function.getX(endPt) < limit) limit = function.getX(endPt); coords.xToPixel(function.getX(ct-1)); aInt = xInt; while (x < limit) { aInt += 3; a = coords.pixelToX(aInt); if (a > limit) a = limit; b = function.getVal(a); bInt = coords.yToPixel(b); g.drawLine(xInt,yInt,aInt,bInt); x = a; xInt = aInt; yInt = bInt; } } break; } case TableFunction. PIECEWISE_LINEAR: { x = function.getX(startPt); xInt = coords.xToPixel(x); y = function.getY(startPt); yInt = coords.yToPixel(y); for (int i = startPt+1; i <= endPt; i++) { a = function.getX(i); aInt = coords.xToPixel(a); b = function.getY(i); bInt = coords.yToPixel(b); g.drawLine(xInt,yInt,aInt,bInt); xInt = aInt; yInt = bInt; } break; } case TableFunction.STEP: { x = function.getX(startPt); xInt = coords.xToPixel(x); for (int i = startPt; i <= endPt; i++) { if (i < endPt) { double nextX = function.getX(i+1); a = (x + nextX)/2; x = nextX; } else a = x; aInt = coords.xToPixel(a); y = function.getY(i); yInt = coords.yToPixel(y); g.drawLine(xInt,yInt,aInt,yInt); xInt = aInt; } break; } case TableFunction.STEP_LEFT: { x = function.getX(startPt); xInt = coords.xToPixel(x); for (int i = startPt+1; i <= endPt; i++) { a = function.getX(i); aInt = coords.xToPixel(a); y = function.getY(i-1); yInt = coords.yToPixel(y); g.drawLine(xInt,yInt,aInt,yInt); xInt = aInt; } break; } case TableFunction.STEP_RIGHT: { x = function.getX(startPt); xInt = coords.xToPixel(x); for (int i = startPt+1; i <= endPt; i++) { a = function.getX(i); aInt = coords.xToPixel(a); y = function.getY(i); yInt = coords.yToPixel(y); g.drawLine(xInt,yInt,aInt,yInt); xInt = aInt; } break; } } if (!showPoints) return; for (int i = startPt; i <= endPt; i++) { x = function.getX(i); y = function.getY(i); xInt = coords.xToPixel(x); yInt = coords.yToPixel(y); g.fillOval(xInt-2,yInt-2,5,5); } } // end draw(); //-------------------- Dragging -------------------------- private int dragPoint = -1; // -1 if no point is being dragged; // Otherwise, the index of the point being dragged. private int startX, startY; // Point where mouse was clicked at start of drag. private int prevY; // Previous position of mouse during dragging. private boolean moved; // Becomes true once the clicked point has actually // been dragged a bit. If the mouse is released before // the point is moved at least 3 pixels, then the associated // y-value is not changed. /** * Method required by the MouseListener interface. Defined here to * support dragging of points on the function's graph. Not meant to be called directly. */ public void mousePressed(MouseEvent evt) { dragPoint = -1; if (function == null || getVisible() == false || canvas == null || coords == null || evt.isConsumed()) return; if (evt.isShiftDown() || evt.isMetaDown() || evt.isControlDown() || evt.isAltDown()) return; moved = false; int ct = function.getPointCount(); for (int i = 0; i < ct; i++) { int x = coords.xToPixel(function.getX(i)); int y = coords.yToPixel(function.getY(i)); if (evt.getX() >= x-3 && evt.getX() <= x+3 && evt.getY() >= y-3 && evt.getY() <= y+3) { startX = evt.getX(); prevY = startY = evt.getY(); dragPoint = i; evt.consume(); return; } } } /** * Method required by the MouseListener interface. Defined here to * support dragging of points on the function's graph. Not meant to be called directly. */ public void mouseReleased(MouseEvent evt) { if (dragPoint == -1) return; evt.consume(); if (!moved) { dragPoint = -1; return; } mouseDragged(evt); dragPoint = -1; if (onFinishDrag != null) onFinishDrag.compute(); } /** * Method required by the MouseListener interface. Defined here to * support dragging of points on the function's graph. Not meant to be called directly. */ public void mouseDragged(MouseEvent evt) { if (dragPoint == -1 || prevY == evt.getY()) return; evt.consume(); if (!moved && Math.abs(evt.getY() - startY) < 3) return; moved = true; int y = evt.getY(); if (y < coords.getTop() + 4) y = coords.getTop() + 4; else if (y > coords.getTop() + coords.getHeight() - 4) y = coords.getTop() + coords.getHeight() - 4; if (Math.abs(evt.getX() - startX) > 72) y = startY; if (y == prevY) return; prevY = y; function.setY(dragPoint, coords.pixelToY(prevY)); needsRedraw(); if (onDrag != null) onDrag.compute(); } /** * Empty method, required by the MouseListener interface. */ public void mouseClicked(MouseEvent evt) { } /** * Empty method, required by the MouseMotionListener interface. */ public void mouseEntered(MouseEvent evt) { } /** * Empty method, required by the MouseMotionListener interface. */ public void mouseExited(MouseEvent evt) { } /** * Empty method, required by the MouseMotionListener interface. */ public void mouseMoved(MouseEvent evt) { } } // end class TableFunctionGraph jcm1-source/edu/hws/jcm/functions/ExpressionFunction.java0000644000076500011320000003003411741343635023056 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.functions; import edu.hws.jcm.data.*; /** * An ExpressionFunction is a Function that is created from an expression and a list * of variables that serve as the parameter(s) of the function. (This is essentially * a lambda operation, forming a function such as "lambda(x,y) (x^2+y^2)") * * Since an ExpressionFunction is a FunctionParserExtension, functions defined * from this class can be added to a Parser and then used in expressions parsed * by that parser. */ public class ExpressionFunction extends FunctionParserExtension { private Expression definition; // Expression that defines the function. private Variable[] params; // Variable(s) that act as the parameters. /** * Constuct a function of one parameter, named "x", by parsing the String, def, * to get the definition of the function. A standard Parser, with * default options and knowledge only of "pi", "e" and the standard functions * is used. The variable "x" is also added to this parser while the function is being parsed. * * @param name Name of function. This should not be null if the function is to be used in a Parser. * @param def contains definition of the function, as a function of "x". */ public ExpressionFunction(String name, String def) { this(name, new String[] { "x" }, def, null); } /** * Constuct a function of one or more parameters by parsing the String, def, * to get the definition of the function. The given parser is used to * parse the definition, so the definition can refer to objects registered * with the parser (such as other variables or functions). Furthermore, if * both name and parser are non-null, then the function is registered with * the parser so that it can then be used in expressions parsed by the * parser. (It's possible to have a function of zero arguements. In that case, the * function serves as a "named expression".) * * @param name Name of function. * @param paramNames Names of the parameters of the function. The lenght of this array determines the arity of the function. * @param def The definition of the function, in terms of the parameters from the paramNames array. * @param parser Used to parse the definition. If this is null, a standard parser is used. The * paramaters are temporarily added onto the parser while the function definition is being parsed. */ public ExpressionFunction(String name, String[] paramNames, String def, Parser parser) { setName(name); if (paramNames == null) params = new Variable[0]; else { params = new Variable[ paramNames.length ]; for (int i = 0; i < paramNames.length; i++) params[i] = new Variable(paramNames[i]); } redefine(def,parser); if (parser != null && name != null) parser.add(this); } /** * Construct a function from a list of variables that serve as parameters and an expression that, * presumably, can include those variables. WARNING: When the function is * evaluated, the values of the parameter variables can change, so you should * probably not use variables that are being used elsewhere in your program. */ public ExpressionFunction(String name, Variable[] params, Expression definition) { setName(name); this.params = (params == null)? new Variable[0] : params; this.definition = definition; } private ExpressionFunction() { // This default constructor is used in the derivative() method, but is // not meant to be used from outside this class, since it doesn't properly // iniitialize the state of the member variables. } /** * Set the definition of this function by parsing the given string, * using a default parser. The definition is in terms of the parameter * names originally provided in the constructor. */ public void redefine(String def) { redefine(def,null); } /** * Set the definition of this function, using the specified parser (or a default * parser if parser is null). The definition is in terms of the parameter * names originally provided in the constructor. (This routine does * not register the function with the parser, but if it was already * registered with the parser, it stays registered with the new * definition.) Note that changing the definition of the function * effectively changes the definition of any other expression that * refers to this function. */ public void redefine(String def, Parser parser) { if (parser == null) parser = new Parser(); else parser = new Parser(parser); for (int i = 0; i < params.length; i++) parser.add(params[i]); definition = parser.parse(def); } /** * Return the expression that defines this function, as a string. */ public String getDefinitionString() { return definition.toString(); } /** * Return a string that describes this function, such as "function f(x,y) given by x^2 - y^2". */ public String toString() { StringBuffer b = new StringBuffer(); b.append( name == null? "unnamed function of (" : "function " + name + "(" ); for (int i = 0; i < params.length; i++) { b.append(params[i].getName()); if (i < params.length - 1) b.append(","); } b.append(") given by "); b.append(definition.toString()); return b.toString(); } //------- Methods from the Function interface -------------------- /** * Return the number of arguments of this function. */ public int getArity() { return params.length; } /** * Find the value of the function at the argument values * given by arguments[0], arguments[1], ... The length * of the array argument should be equal to the arity of * the function. If not, an IllegalArgumentException is * thrown. */ public double getVal( double[] arguments ) { return getValueWithCases(arguments, null); } /** * Find the value of the function at the argument values * given by arguments[0], arguments[1], ... The length * of the array argument should be equal to the arity of * the function. If not, an IllegalArgumentException is * thrown. Store information about "cases" that occur in * the evaluation in the second parameter, if that parameter is non-null. */ public double getValueWithCases( double[] arguments, Cases cases ) { synchronized (params) { if (arguments == null) { if (params.length > 0) throw new IllegalArgumentException("Internal Error: Number of arguments provided to function does not match its arity."); } else if (arguments.length != params.length) throw new IllegalArgumentException("Internal Error: Number of arguments provided to function does not match its arity."); else { for (int i = 0; i < params.length; i++) params[i].setVal(arguments[i]); } return definition.getValueWithCases(cases); } } /** * Return the derivative of the function with repect to * argument number wrt, where the arguments are numbered 1, 2, 3,.... * For a function of one variable, call derivative(1) to find its derivative. * If arity > 1, this is effectively a partial derivative. If wrt is * not in the legal range, an IllegalArgumentException is thrown. */ public Function derivative(int wrt) { if (wrt <= 0 || wrt > getArity()) throw new IllegalArgumentException("Internal Error: Attempt to take the derivative of a function of " + getArity() + " variables with respect to argument number " + wrt + "."); ExpressionFunction deriv = new ExpressionFunction(); if (name != null) { if (getArity() == 1) deriv.setName(getName() + "'"); else deriv.setName("D" + wrt + "[" + getName() + "]"); } deriv.params = params; deriv.definition = (Expression)definition.derivative(params[wrt-1]); return deriv; } /** * Return the derivative of the function with respect to the * variable x. This will be non-zero if x occurs somehow in * the definition of x: For example, f(y) = sin(x*y); */ public Function derivative(Variable x) { ExpressionFunction deriv = new ExpressionFunction(); if (name != null) deriv.setName("D" + x.getName() + "[" + getName() + "]"); deriv.params = params; deriv.definition = (Expression)definition.derivative(x); return deriv; } /** * Return true if the definition of this function depends * in some way on the variable x. (Note that a function does * NOT depend on its parameter variables!) */ public boolean dependsOn(Variable x) { return definition.dependsOn(x); } //----------- A method from ParserExtension class -------------- /** * Find the value of the function applied to arguments popped * from the stack, and push the result back onto the stack. * (Overrides general method inherited from FunctionParserExtension. * This is done for efficiency and because the general method * can't deal properly with "cases".) Not meant to be called directly */ public void apply(StackOfDouble stack, Cases cases) { for (int i = getArity() - 1; i >= 0; i--) params[i].setVal(stack.pop()); stack.push( definition.getValueWithCases(cases) ); } } // end class ExpressionFunction jcm1-source/edu/hws/jcm/functions/SummationParser.java0000644000076500011320000002653111741343635022351 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.functions; import edu.hws.jcm.data.*; /** * The SummationParser class makes it possible to use summations such as sum(i,1,5,x^i) in a Parser. * The summation psedu-function has four parameters: (1) The summation variable, which must be an identifier; * (2) The lower limit for the summation, given as an expression; (3) The upper limit for the * summation, given as an expression; and (4) The expression that is summed. The values of * the lower and upper limit expressions are rounded to the nearest integer. The expression in * the fourth parameter can (and presumably will) use the summation variable (as well as other * identifiers known to the parser). * *

To use summations with a Parser p, just say p.add(new SummationParser()). It's unlikely that * you will ever need to do anything else with SummationParsers. * If you want to use a name other than "sum", you can change the name after creating the * SummationParser object but before adding it to a parser. (Note, by the way, that parsers by default do not do * factorials. If you want a parser that recognizes factorials, you could do something * like p = new Parser(Parser.DEFAULT_OPTIONS | Parser.FACTORIAL).) * */ public class SummationParser implements ParserExtension { /* A name is required by the MathObject interface, which ParserExtension extends. The name is what is used to indicate a summation in an expression. The name should not be changed after the SummationParser is added to a Parser. */ private String name = "sum"; /** * Set the name, which will be used in place of "sum" in expressions. This should not * be done after the SummationParser has been added to a Parser. The default name is "sum". */ public void setName(String name) { this.name = name; } /** * Get the name, which will be used in place of "sum" in expressions. */ public String getName() { return name; } /** * When the name of this ParserExtension is encountered by a parser with which * the extension is registered, the parser calls this routine to parse the * summation subexpression. The subexpression has the form * (,,,). This method is * not meant to be called directly */ public void doParse(Parser parser, ParserContext context) { int tok = context.next(); String open = context.tokenString; if (tok == ParserContext.OPCHARS && ( open.equals("(") || (open.equals("[") && (context.options & Parser.BRACKETS) != 0) || (open.equals("{") && (context.options & Parser.BRACES) != 0) )) { String close = open.equals("(") ? ")" : (open.equals("[") ? "]" : "}"); tok = context.next(); // Must be an identifier. if (tok != ParserContext.IDENTIFIER) throw new ParseError("Expected the summation variable as the first argument of " + name + ".", context); String varName = context.tokenString; tok = context.next(); if (tok != ParserContext.OPCHARS || ! context.tokenString.equals(",")) throw new ParseError("Exprected a comma after the index variable, " + varName +".", context); parser.parseExpression(context); tok = context.next(); if (tok != ParserContext.OPCHARS || ! context.tokenString.equals(",")) throw new ParseError("Exprected a comma after the lower limit expression for " + name + ".", context); parser.parseExpression(context); tok = context.next(); if (tok != ParserContext.OPCHARS || ! context.tokenString.equals(",")) throw new ParseError("Exprected a comma after the upper limit expression for " + name + ".", context); Variable v = new Variable(varName); context.mark(); // Temporoarily add the summation variable to the symbol table. context.add(v); ExpressionProgram saveProg = context.prog; context.prog = new ExpressionProgram(); // Compile the expression into a new program. parser.parseExpression(context); tok = context.next(); if (tok != ParserContext.OPCHARS || ! context.tokenString.equals(close)) throw new ParseError("Expected a \"" + close + "\" at the end of the paramter list for " + name + ".", context); context.revert(); // Restore the state of the ParserContext. saveProg.addCommandObject(new Cmd(v,context.prog)); context.prog = saveProg; } else throw new ParseError("Parentheses required around parameters of summation.", context); } // end doParse() private static class Cmd implements ExpressionCommand { // When a summation occurs in an expression, it is represented in the compiled ExpressionProgram // by an object belonging to this class. private Variable sumVar; // The summation variable. private ExpressionProgram sumExpr; // The expression that is summed Cmd(Variable v, ExpressionProgram e) { // Constructor. sumVar = v; sumExpr = e; } public void apply(StackOfDouble stack, Cases cases) { // This routine is called when an ExpressionCommand object is encountered during // the evaluation of an ExpressionProgram. The stack may contain results of // previous commands in the program. In this case, the command is a summation // and the stack contains the upper and lower limits summation limits. double upper = Math.round(stack.pop()) + 0.1; // Get summation limits. double lower = Math.round(stack.pop()); if (Double.isNaN(upper) && Double.isNaN(lower) || upper - lower > 1000000) stack.push(Double.NaN); double sum = 0; for (double x = lower; x <= upper; x++) { // Compute the sum. sumVar.setVal(x); sum += sumExpr.getVal(); } stack.push(sum); // Leave the sum on the stack. } public void compileDerivative(ExpressionProgram prog, int myIndex, ExpressionProgram deriv, Variable wrt) { // The ExpressionCommand occurs in the program prog at the index indicated by myIndex. // Add commands to deriv that will evaluate the derivative of this command with respect to // the variable wrt. Note that the "Cmd" object is preceded in the program by commands that // compute the lower and upper limits of the summation. if (!sumExpr.dependsOn(wrt)) deriv.addConstant(0); else { int upper = prog.extent(myIndex - 1); // Size of expression giving the upper limit. prog.copyExpression(myIndex - 1 - upper,deriv); // Copy lower limit exression to deriv. prog.copyExpression(myIndex - 1,deriv); // Copy upper limit expression to deriv. deriv.addCommandObject( new Cmd(sumVar, (ExpressionProgram)sumExpr.derivative(wrt)) ); } } public int extent(ExpressionProgram prog, int myIndex) { // The ExpressionCommand occurs in the program prog at the index indicated by myIndex. // Return the total number of indices in prog occupied by this command and the commands // that generate data used by this command. In this case, that means the commands that // compute the upper and lower limits of the summatio, plus this Cmd object. int upper = prog.extent(myIndex - 1); // Extent of upper limit expression in prog. int lower = prog.extent(myIndex - 1 - upper); // Extent of lower limit expression in prog. return upper + lower + 1; // Upper + lower limits + this object. } public boolean dependsOn(Variable x) { // Return true if this command depends on the value of x, false otherwise. // That is, when apply() is called, can the result depend on the value of x? return sumExpr.dependsOn(x); } public void appendOutputString(ExpressionProgram prog, int myIndex, StringBuffer buffer) { // The ExpressionCommand occurs in the program prog at the index indicated by myIndex. // Add a print string representation of the sub-expression represented by this command // (including any previous commands in the program that generate data used by this // command). int upper = prog.extent(myIndex - 1); buffer.append("sum("); buffer.append(sumVar.getName()); buffer.append(", "); prog.appendOutputString(myIndex - 1 - upper,buffer); buffer.append(", "); prog.appendOutputString(myIndex - 1, buffer); buffer.append(", "); buffer.append(sumExpr.toString()); buffer.append(")"); } } // end nested class Cmd } // end SumParserExtension jcm1-source/edu/hws/jcm/data/0000755000076500011320000000000011741343635015247 5ustar djefacultyjcm1-source/edu/hws/jcm/data/Parser.java0000644000076500011320000007141611741343635017357 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A Parser can take a string and compile it into an ExpressionProgram. * MathObjects, such as variables and functions, can be registered with * the Parser. This means that the Parser will recognize them in the * strings that it parses. There are a few options that can be set to * control certain aspects of the parsing. If a string does not have * the correct syntax for an expression, then the Parser will throw a * ParseError when it tries to parse that string. A Parser can have a * parent. It inherits any MathObjects registered with its parent, but * a MathObject registered with a Parser will hide any MathObject of * the same name that is registered with its parent. * Every parser recognizes the constants pi and e and the operators * +, -, *, /, ^, and **. The ** operator is a synonym for ^, the * exponentiation operator. Both unary and binary + and - are recognized. * The exponentiation operator is right associative. The others are * left associative. */ public class Parser implements java.io.Serializable { /** * An option that can be set for this parser. * If enabled, identifiers are case-sensitive. * For example, Sin, sin, and SIN will be * treated as separate identifiers. It really * only makes sense to enable this at the time the * Parser is first constructed. */ public static final int CASE_SENSITIVE = 1; /** * An that can be set for this parser. * If enabled, mutltiplication can be indicated * implicitely, as well as with a "*". For * example, 2x will mean 2*x. */ public static final int OPTIONAL_STARS = 2; /** * An option that can be set for this parser. * If enabled, spaces are not required to separate * identifiers. This only has an effect if one of * OPTIONAL_STARS or OPTIONAL_PARENS is also enabled. * For example, xsin(x) will be read as x*sin(x), * and sine will be read as sin(e). */ public static final int OPTIONAL_SPACES = 4; /** * An option that can be set for this parser. * If enabled, brackets, [ and ], can be used for grouping. */ public static final int BRACKETS = 8; /** * An option that can be set for this parser. * If enabled, braces, { and }, can be used for grouping. */ public static final int BRACES = 16; /** * An option that can be set for this parser. * If enabled, the "?" operator can be used in expressions, along with the * logical operators &, |, ~, =, <, >, <>, <=, >=. * The words "and", "or", and "not" can be used * in place of &, |, and ~. These words are * treated in a case-insensitive way, even if * the CASE_SENSITIVE option is on. When this * option is set, it is legal to call the * parseLogical method to parse a boolean-valued * expression. This option is enabled by default. */ public static final int BOOLEANS = 32; /** * An option that can be set for this parser. * If enabled, the factorial operator, !, is recognized. */ public static final int FACTORIAL = 64; /** * An option that can be set for this parser. * The character "_", which can usually * be used just like a letter, is * not allowed in identifers. */ public static final int NO_UNDERSCORE_IN_IDENTIFIERS = 128; /** * An option that can be set for this parser. * Digits 0 through 9, which can usually be * used in an identifier after the first * character, are not allowed in identifiers. */ public static final int NO_DIGITS_IN_IDENTIFIERS = 256; /** * An option that can be set for this parser. * If enabled, parentheses are optional around * the parameter of a standard function. If the * parentheses are omited, then the argument is * the term that follows the function name. * For example, "sin x + 1" means "sin(x) + 1" * while "sin x * cos x" means "sin( x*cos(x) )". */ public static final int OPTIONAL_PARENS = 512; /** * An option that can be set for this parser. * When enabled, the standard functions are * registered with the parser. This option * is enabled by default. The standard * functions are: sin, cos, tan, cot, sec, * csc, arcsin, arccos, arctan, exp, ln, * log2, log10, sqrt, cubert, abs, round, * floor, ceiling, trunc. */ public static final int STANDARD_FUNCTIONS = 1024; /** * The default options set that is used for * a newly created Parser, if none is specified * in the Constructor. It includes the options BOOLEANS and STANDARD_FUNCTIONS. */ public static final int DEFAULT_OPTIONS = BOOLEANS | STANDARD_FUNCTIONS; /** * The set of options that have been enabled for this parser. */ protected int options; /** * The symbol table that contains the MathObjects * that have been registered with this parser. */ protected SymbolTable symbols; /** * Construct a Parser with no parent and with the default options, * BOOLEANS and STANDARD_FUNCTIONS. */ public Parser() { this(null,DEFAULT_OPTIONS); } /** * Create a Parser with the specified parent. The options for this * parser are inherited from the parent, if parent is non-null. * If parent is null, the option set is empty. */ public Parser(Parser parent) { this(parent,0); } /** * Create a Parser with the spedified option set and with no parent. */ public Parser(int options) { this(null,options); } /** * Create a Parser with the specified parent. The options for this * parser consist of the option set from the parent, together with * any additional options in the specified options set. * * @param parent parent of this Parser, possibly null. * @param options additional options, in addition to ones inherited from parent. */ public Parser(Parser parent, int options) { if (parent == null) { symbols = new SymbolTable(); symbols.add(new Constant("e",Math.E)); symbols.add(new Constant("pi",Math.PI)); } else { symbols = new SymbolTable(parent.symbols); this.options = parent.options; } addOptions(options); } /** * Add the options in the option set newOptions to this Parser's option set. * The value of newOptions can be one of the option constants defined in this * class, such as OPTIONAL_STARS, or it can consist of several option constants * OR-ed together. * */ public void addOptions(int newOptions) { if ( ((newOptions & STANDARD_FUNCTIONS) != 0) && ((options & STANDARD_FUNCTIONS) == 0) ) { for (int opCode = ExpressionProgram.CUBERT; opCode <= ExpressionProgram.SIN; opCode++) symbols.add( new StandardFunction(opCode) ); } options = options | newOptions; } /** * Parse the string str and create the corresponding expression. * The expression must be numeric-valued, not logical. There can't * be any extra characters in str after the expression. If a syntax * error is found, a ParseError will be thrown. * * @param str String to parse. * @return the expression defined by the string. */ public ExpressionProgram parse(String str) { ParserContext context = new ParserContext(str,options,symbols); // The ParserContext holds all the information relevant to the // parsing of str, including str itself and the ExpressionProgram // that is being generated. See the ParserContext class for more info. if (str == null) throw new ParseError("Can't parse a null string.", context); if (context.look() == ParserContext.END_OF_STRING) throw new ParseError("Can't parse an empty string.", context); boolean isBool; if ( (options & BOOLEANS) != 0 ) isBool = parseLogicalExpression(context); else isBool = parseExpression(context); if (context.look() != ParserContext.END_OF_STRING) throw new ParseError("Extra data found after the end of a complete legal expression.",context); if (isBool) throw new ParseError("Found a logical-valued expression instead of a numeric expression.",context); context.prog.trim(); context.prog.sourceString = str; return context.prog; } /** * Parse the String, str, and create a corresponding logical-valued expression. * The expression must be logical-valued, such as "x > 0", not numeric. There can't * be any extra characters in str after the expression. If a syntax * error is found, a ParseError will be thrown. It is not legal to call this * method if the BOOLEANS option is not set. * * @param str String to parse. * @return the logical-valued expression defined by str. */ public ExpressionProgram parseLogical(String str) { if ( (options & BOOLEANS) == 0 ) throw new IllegalArgumentException("Internal Error: Attempt to parse a logical-valued expression, but BOOLEANS option is not turned on."); ParserContext context = new ParserContext(str,options,symbols); if (str == null) throw new ParseError("Can't parse a null string.", context); if (context.look() == ParserContext.END_OF_STRING) throw new ParseError("Can't parse an empty string.", context); boolean isBool = parseLogicalExpression(context); if (context.look() != ParserContext.END_OF_STRING) throw new ParseError("Extra data found after the end of a complete legal expression.",context); if (!isBool) throw new ParseError("Found a numeric-valued expression instead of a logical expression.",context); context.prog.trim(); return context.prog; } //---------- Wrapper functions for accessing the symbol table -------------- /** * Get the MathObject that has been registered with the parser * under the given name. If the CASE_SENSITIVE option is not set, * names are converted to lower case for the purpose of * registering and retrieving registered objects. */ public MathObject get(String name) { if ( (options & Parser.CASE_SENSITIVE) != 0 ) return symbols.get(name); else return symbols.get(name.toLowerCase()); } /** * Register the MathObject with the Parser, associating it with its * name. An error will occur if the name is null. If the CASE_SENSITIVE * option is not set, names are converted to lower case for the purpose of * registering and retrieving registered objects. */ public void add(MathObject sym) { if ( (options & Parser.CASE_SENSITIVE) != 0 ) symbols.add(sym); else symbols.add(sym.getName().toLowerCase(), sym); } /** * Deregister the MathObject with the given name, if there is one * registered with the Parser. If the name is not registered, nothing * happens and no error occurs. * * @param name MathObject to deregister. */ public void remove(String name) { if (name == null) return; else if ( (options & Parser.CASE_SENSITIVE) != 0 ) symbols.remove(name); else symbols.remove(name.toLowerCase()); } // ------------------------- The parsing code ------------------------- // The remaining routines in this class implement a recursive descent parser // for expressions. These routines would be private, except that it might // be necessary for a ParserExtension to call them. The ParserContext parameter // holds information such as the string that is being parsed and the ExpressionProgram // that is being generated. See the ParseContext class for more information. /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ public boolean parseLogicalExpression(ParserContext context) { boolean isBool = parseLogicalTerm(context); int tok = context.look(); if (tok == ParserContext.OPCHARS && context.tokenString.equals("&") && !isBool) throw new ParseError("The AND operator can only be used with logical expressions.",context); while (tok == ParserContext.OPCHARS && context.tokenString.equals("&")) { context.next(); if (!parseLogicalTerm(context)) throw new ParseError("The AND operator can only be used with logical expressions.",context); context.prog.addCommand(ExpressionProgram.AND); tok = context.look(); } if (tok == ParserContext.OPCHARS && context.tokenString.equals("?")) { if (!isBool) throw new ParseError("The conditional operator, ?, can only be applied to a logical-valued expression.",context); ExpressionProgram trueCase, falseCase; ExpressionProgram saveProg = context.prog; context.next(); trueCase = new ExpressionProgram(); context.prog = trueCase; if (parseLogicalExpression(context)) throw new ParseError("The cases in a conditional expression cannot be logical-valued expressions.",context); tok = context.look(); if (tok == ParserContext.OPCHARS && context.tokenString.equals(":")) { context.next(); falseCase = new ExpressionProgram(); context.prog = falseCase; if (parseLogicalExpression(context)) throw new ParseError("The cases in a conditional expression cannot be logical-valued expressions.",context); } else falseCase = null; context.prog = saveProg; context.prog.addCommandObject( new ConditionalExpression(trueCase, falseCase) ); return false; } else return isBool; } /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ public boolean parseLogicalTerm(ParserContext context) { boolean isBool = parseLogicalFactor(context); int tok = context.look(); if (tok == ParserContext.OPCHARS && context.tokenString.equals("|") && !isBool) throw new ParseError("The OR operator can only be used with logical expressions.",context); while (tok == ParserContext.OPCHARS && context.tokenString.equals("|")) { context.next(); if (!parseLogicalFactor(context)) throw new ParseError("The OR operator can only be used with logical expressions.",context); context.prog.addCommand(ExpressionProgram.OR); tok = context.look(); } return isBool; } /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ public boolean parseLogicalFactor(ParserContext context) { int tok = context.look(); int notCt = 0; while (tok == ParserContext.OPCHARS && context.tokenString.equals("~")) { context.next(); tok = context.look(); notCt++; } boolean isBool = parseRelation(context); if (notCt > 0 && !isBool) throw new ParseError("The NOT operator can only be used with logical expressions.",context); if (notCt % 2 == 1) context.prog.addCommand(ExpressionProgram.NOT); return isBool; } /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ public boolean parseRelation(ParserContext context) { boolean isBool = parseExpression(context); int tok = context.look(); if (tok != ParserContext.OPCHARS) return isBool; int rel = 0; if (context.tokenString.equals("=")) rel = ExpressionProgram.EQ; else if (context.tokenString.equals("<")) rel = ExpressionProgram.LT; else if (context.tokenString.equals(">")) rel = ExpressionProgram.GT; else if (context.tokenString.equals("<=")) rel = ExpressionProgram.LE; else if (context.tokenString.equals(">=")) rel = ExpressionProgram.GE; else if (context.tokenString.equals("<>")) rel = ExpressionProgram.NE; if (rel == 0) return isBool; if (isBool) throw new ParseError("A relational operator can only be used with numerical expressions.",context); context.next(); if (parseExpression(context)) throw new ParseError("A relational operator can only be used with numerical expressions.",context); tok = context.look(); if (tok == ParserContext.OPCHARS && ( context.tokenString.equals("=") || context.tokenString.equals("<") || context.tokenString.equals(">") || context.tokenString.equals("<=") || context.tokenString.equals(">=") || context.tokenString.equals("<>")) ) throw new ParseError("It is illegal to string together relations operators; use \"AND\" instead.",context); context.prog.addCommand(rel); return true; } /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ public boolean parseExpression(ParserContext context) { boolean neg = false; int tok = context.look(); if (tok == ParserContext.OPCHARS && (context.tokenString.equals("+") || context.tokenString.equals("-"))) { neg = (context.tokenString.equals("-")); context.next(); } boolean isBool = parseTerm(context); if (neg) { if (isBool) throw new ParseError("A unary + or - cannot be applied to a logical expression.",context); context.prog.addCommand(ExpressionProgram.UNARY_MINUS); } tok = context.look(); if (tok == ParserContext.OPCHARS && (context.tokenString.equals("+") || context.tokenString.equals("-")) && isBool) throw new ParseError("A + or - operator cannot be applied to logical operands.",context); while (tok == ParserContext.OPCHARS && (context.tokenString.equals("+") || context.tokenString.equals("-"))) { context.next(); int opcode = (context.tokenString.equals("+")? ExpressionProgram.PLUS : ExpressionProgram.MINUS); if (parseTerm(context)) throw new ParseError("A + or - operator cannot be applied to logical operands.",context); context.prog.addCommand(opcode); tok = context.look(); } return isBool; } /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ public boolean parseTerm(ParserContext context) { boolean implicitStar = false; boolean isBool = parsePrimary(context); int tok = context.look(); String ts = context.tokenString; implicitStar = !isBool && (options & OPTIONAL_STARS) != 0 && (tok == ParserContext.NUMBER || tok == ParserContext.IDENTIFIER || (tok == ParserContext.OPCHARS && (ts.equals("(") || ts.equals("[") || ts.equals("{")))); if (tok == ParserContext.OPCHARS && (ts.equals("*") || ts.equals("/")) && isBool) throw new ParseError("A * or / operator cannot be applied to logical operands.",context); while (implicitStar || tok == ParserContext.OPCHARS && (ts.equals("*") || ts.equals("/"))) { if (!implicitStar) context.next(); int opcode = (implicitStar || ts.equals("*")? ExpressionProgram.TIMES : ExpressionProgram.DIVIDE); if (parsePrimary(context)) throw new ParseError("A * or / operator cannot be applied to logical operands.",context); context.prog.addCommand(opcode); tok = context.look(); ts = context.tokenString; implicitStar = !isBool && (options & OPTIONAL_STARS) != 0 && (tok == ParserContext.NUMBER || tok == ParserContext.IDENTIFIER || (tok == ParserContext.OPCHARS && (ts.equals("(") || ts.equals("[") || ts.equals("{")))); } return isBool; } /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ public boolean parsePrimary(ParserContext context) { boolean isBool = parseFactor(context); int tok = context.look(); if (tok == ParserContext.OPCHARS && context.tokenString.equals("^")) { if (isBool) throw new ParseError("The exponentiation operator cannot be applied to logical operands.",context); context.next(); if (parsePrimary(context)) throw new ParseError("The exponentiation operator cannot be applied to logical operands.",context); context.prog.addCommand(ExpressionProgram.POWER); } return isBool; } /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ public boolean parseFactor(ParserContext context) { boolean isBool = false; int tok = context.next(); if (tok == ParserContext.NUMBER) context.prog.addConstant(context.tokenValue); else if (tok == ParserContext.IDENTIFIER) parseWord(context); else if (tok == ParserContext.END_OF_STRING) throw new ParseError("Data ended in the middle of an incomplete expression.",context); else if (tok != ParserContext.OPCHARS) throw new ParseError("Internal error: Unknown token type.",context); else if (context.tokenString.equals("(")) isBool = parseGroup( '(', ')' , context ); else if (context.tokenString.equals("[") && ((options & Parser.BRACKETS) != 0)) isBool = parseGroup( '[', ']' , context ); else if (context.tokenString.equals("{") && ((options & Parser.BRACES) != 0)) isBool = parseGroup( '{', '}' , context ); else if (context.tokenString.equals("}") && ((options & Parser.BRACES) != 0)) throw new ParseError("Misplaced right brace with no matching left brace.",context); else if (context.tokenString.equals("]") && ((options & Parser.BRACKETS) != 0)) throw new ParseError("Misplaced right bracket with no matching left bracket.",context); else if (context.tokenString.equals(")")) throw new ParseError("Misplaced right parenthesis with no matching left parenthesis.",context); else throw new ParseError("Illegal or misplaced character \"" + context.tokenString.charAt(0) + "\"",context); if ( (options & FACTORIAL) != 0 ) { tok = context.look(); while (tok == ParserContext.OPCHARS && context.tokenString.equals("!")) { if (isBool) throw new ParseError("The factorial operator cannot be applied to a logical value.",context); context.next(); context.prog.addCommand(ExpressionProgram.FACTORIAL); tok = context.look(); } } return isBool; } // Helper routine for parsePrimary. If a ParserExtension needs to call this, // it should look ahead to make sure there is a (, [, or { and then call // parsePrimary. /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ private boolean parseGroup(char open, char close, ParserContext context) { boolean isBool = (options & Parser.BOOLEANS) == 0 ? parseExpression(context) : parseLogicalExpression(context); int tok = context.look(); if (tok == ParserContext.OPCHARS && context.tokenString.equals("" + close)) context.next(); else throw new ParseError("Missing \"" + close + "\" to match a previous \"" + open + "\".",context); return isBool; } // Helper routine for parsePrimary. If a ParserExtension needs to call this, // it should look ahead to make sure there is an identifier and then call // parsePrimary. /** * Called as part of the parsing process. From outside this class, this would * probably be called only by a ParserExtension. */ private void parseWord(ParserContext context) { if (context.tokenObject == null) throw new ParseError("Unknown word \"" + context.tokenString + "\" encountered in an expression.", context); if (context.tokenObject instanceof Variable || context.tokenObject instanceof Constant) context.prog.addCommandObject((ExpressionCommand)context.tokenObject); else if (context.tokenObject instanceof StandardFunction) { StandardFunction f = (StandardFunction)context.tokenObject; int tok = context.look(); if (tok == ParserContext.OPCHARS && ( context.tokenString.equals("(") || ( context.tokenString.equals("[") && ((options & BRACKETS) != 0) ) || ( context.tokenString.equals("{") && ((options & BRACES) != 0) ) ) ) { context.next(); boolean isBool; if (context.tokenString.equals("(")) isBool = parseGroup('(',')',context); else if ( context.tokenString.equals("[") ) isBool = parseGroup('[',']',context); else isBool = parseGroup('{','}',context); if (isBool) throw new ParseError("The argument of a function must be a numerical expression.", context); } else { if ( (options & OPTIONAL_PARENS) == 0 ) throw new ParseError("Parentheses required around argument of standard function \"" + f.getName() + "\".", context); if (parseTerm(context)) throw new ParseError("The argument of a function must be a numerical expression.", context); } context.prog.addCommand(f.getOpCode()); } else if ( context.tokenObject instanceof ParserExtension) ((ParserExtension)context.tokenObject).doParse(this, context); else if (! ( context.tokenObject instanceof ExpressionCommand )) throw new ParseError("Unexpected word \"" + context.tokenObject.getName() + "\" encountered in an expression.", context); else throw new ParseError("Unimplemented word \"" + context.tokenObject.getName() + "\" encountered in an expression.", context); } } // end class Parser jcm1-source/edu/hws/jcm/data/StandardFunction.java0000644000076500011320000001457611741343635021375 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * This class exists to associate standard functions, such as sin and abs, with * their names. Note that the functions are actually implemented in the * ExprsssionProgram class, where they are only represented by numerical operation * codes. An object of type StandardFunction contains a name and the operation * code of the associated standard function. A static routine, standardFunctionName, * gives the name associated with each operation code. */ public class StandardFunction implements MathObject { private String name; // The name of this standard function. private int code; // The operation code for this function, from class ExpressionProgram. /** * Create a StandardFunction object to represent the standard * function with the given operation code, where opCode is one * of the codes for standard functions defined in class ExpressionProgram. * The name is the one associated with the opCode by the * static function standardFunctionName() in this class. An error * will occur if opCode is not one of the valid standard function * operation codes. */ public StandardFunction(int opCode) { this(standardFunctionName(opCode),opCode); } /** * Create a StandardFunction object to represent the standard * function with the given operation code, where opCode is one * of the codes for stadard functions defined in class ExpressionProgram. * Use the specified name for the standard function. This allows you * to make alternative names, such as "log" instead of "log10". An error * will occur if opCode is not one of the valid standard function * operation codes. */ public StandardFunction(String name, int opCode) { setName(name); code = opCode; } /** * Return the operation code for this standard function. */ public int getOpCode() { return code; } //----------------------- Methods from interface MathObject -------------------- /** * Return the name of this StandardFunction oject. */ public String getName() { return name; } /** * Change the name of this StandardFunction. This shouldn't be done * if this object is registered with a Parser. */ public void setName(String name) { this.name = name; } //------------------------------------------------------------------------------- /** * Return the usual name for the standard function with the * specified opCode. The opcodes are defined in the ExpressionProgram class. * Will throw an IllegalArgumentException if the specified oPcode is * not the opcode for any standard function. */ public static String standardFunctionName(int opCode) { switch (opCode) { case ExpressionProgram.SIN: return "sin"; case ExpressionProgram.COS: return "cos"; case ExpressionProgram.TAN: return "tan"; case ExpressionProgram.COT: return "cot"; case ExpressionProgram.SEC: return "sec"; case ExpressionProgram.CSC: return "csc"; case ExpressionProgram.ARCSIN: return "arcsin"; case ExpressionProgram.ARCCOS: return "arccos"; case ExpressionProgram.ARCTAN: return "arctan"; case ExpressionProgram.ABS: return "abs"; case ExpressionProgram.SQRT: return "sqrt"; case ExpressionProgram.EXP: return "exp"; case ExpressionProgram.LN: return "ln"; case ExpressionProgram.LOG2: return "log2"; case ExpressionProgram.LOG10: return "log10"; case ExpressionProgram.TRUNC: return "trunc"; case ExpressionProgram.ROUND: return "round"; case ExpressionProgram.FLOOR: return "floor"; case ExpressionProgram.CEILING: return "ceiling"; case ExpressionProgram.CUBERT: return "cubert"; default: throw new IllegalArgumentException("Internal Error: Unknown standard function code."); } } } jcm1-source/edu/hws/jcm/data/NumUtils.java0000644000076500011320000001611011741343635017671 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * This class provides a few static functions for converting real numbers * to strings and strings to real numbers. It should probalby be reimplemented * to use the standard NumberFormat class for converting real to string. */ public class NumUtils { /** * Return the real number represented by the String s, * or return Double.NaN if s does not represent a legal * real number. */ public static double stringToReal(String s) { try { Double d = new Double(s); return d.doubleValue(); } catch (NumberFormatException e) { return Double.NaN; } } /** * Return a string representation of the real number * x occupying, if possible, at most 10 spaces. */ public static String realToString(double x) { return realToString(x,10); } /** * Goal is to return a reasonable string representation * of x, using at most width spaces. (If the parameter width is * unreasonably big or small, its value is adjusted to * lie in the range 6 to 25.) * * @param x value to create string representation of. * @param width maximum number of spaces used in string representation, if possible. * @return a string representation for x. If x is Double.NaN, "undefined" is returned. * If x is infinite, "INF" or "-INF" is returned. */ public static String realToString(double x, int width) { width = Math.min(25, Math.max(6,width)); if (Double.isNaN(x)) return "undefined"; if (Double.isInfinite(x)) if (x < 0) return "-INF"; else return "INF"; String s = String.valueOf(x); if (Math.rint(x) == x && Math.abs(x) < 5e15 && s.length() <= (width+2)) return String.valueOf( (long)x ); // return string without trailing ".0" if (s.length() <= width) return s; boolean neg = false; if (x < 0) { neg = true; x = -x; width--; s = String.valueOf(x); } long maxForNonExp = 5*(long)Math.pow(10,width-2); if (x >= 0.0005 && x <= maxForNonExp && (s.indexOf('E') == -1 && s.indexOf('e') == -1)) { s = round(s,width); s = trimZeros(s); } else if (x > 1) { // construct exponential form with positive exponent long power = (long)Math.floor(Math.log(x)/Math.log(10)); String exp = "E" + power; int numlength = width - exp.length(); x = x / Math.pow(10,power); s = String.valueOf(x); s = round(s,numlength); s = trimZeros(s); s += exp; } else { // constuct exponential form with negative argument long power = (long)Math.ceil(-Math.log(x)/Math.log(10)); String exp = "E-" + power; int numlength = width - exp.length(); x = x * Math.pow(10,power); s = String.valueOf(x); s = round(s,numlength); s = trimZeros(s); s += exp; } if (neg) return "-" + s; else return s; } private static String trimZeros(String num) { // Helper function for realToString. // Remove trailing zeros if num contains a decimal point, and // remove the decimal point as well if all following digits are zero if (num.indexOf('.') >= 0 && num.charAt(num.length() - 1) == '0') { int i = num.length() - 1; while (num.charAt(i) == '0') i--; if (num.charAt(i) == '.') num = num.substring(0,i); else num = num.substring(0,i+1); } return num; } private static String round(String num, int length) { // Helper function for realToString. // Round off num to the given field width if (num.indexOf('.') < 0) return num; if (num.length() <= length) return num; if (num.charAt(length) >= '5' && num.charAt(length) != '.') { char[] temp = new char[length+1]; int ct = length; boolean rounding = true; for (int i = length-1; i >= 0; i--) { temp[ct] = num.charAt(i); if (rounding && temp[ct] != '.') { if (temp[ct] < '9') { temp[ct]++; rounding = false; } else temp[ct] = '0'; } ct--; } if (rounding) { temp[ct] = '1'; ct--; } // ct is -1 or 0 return new String(temp,ct+1,length-ct); } else return num.substring(0,length); } } // end class NumUtils jcm1-source/edu/hws/jcm/data/ExpressionCommand.java0000644000076500011320000001445711741343635021563 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * An ExpressionCommand can occur as a command in an ExpressionProgram. ExpressionCommands * exists so that ExprssionPrograms can be extened to include new types of operations beyond * the basic operations (such as PLUS and SIN) which are represented by constants in the * ExpressionProgram class. Examples include ConditionalExpressions and user-defined functions. * This interface is not meant for casual programmers. It is for programmers who want to * extend the notion of Expression in an orginal way. * */ public interface ExpressionCommand extends java.io.Serializable { /** * This routine is called when an ExpressionCommand object is encountered during * the evaluation of an ExpressionProgram. The stack may contain results of * previous commands in the program. For example, for a ConditionalExpression, it * contains the value of the boolean condition, and for a user-defined function, * it contains the values of the arguments of the function. When apply() is called, * the ExpressionCommand should perform any stack operations that are necessary * to evaluate itself. For example, a user-defined function would remove its arguments * from the stack and replace them with the value of the function at those arguments. * If cases is non-null, then any case information generated during the evaluation * should be recorded in cases. (See the Cases class for more information.) * * @param stack contains results of previous commands in the program. * @param cases if non-null, any case information generated during evaluation should be recorded here. */ public void apply(StackOfDouble stack, Cases cases); /** * The ExpressionCommand occurs in the program prog at the index indicated by myIndex. * Add commands to deriv that will evaluate the derivative of this command with respect to * the variable wrt. prog and myIndex are provided so that this routine will have access * to any commands in prog that generate data used by this command (for example, the commands * that evaluate the arguments of a user-defined function). * * @param prog program in which ExpressionCommand occurs. * @param myIndex point at which ExpressionCommand occurs in the ExpressionProgram. * @param deriv the derivative of the ExpressionPorgram prog, which is in the process of being computed. Commands should added to deriv that will compute the derivative of this ExpressionCommand. * @param wrt commands are added to deriv with respect to this Variable. */ public void compileDerivative(ExpressionProgram prog, int myIndex, ExpressionProgram deriv, Variable wrt); /** * The ExpressionCommand occurs in the program prog at the index indicated by myIndex. * Return the total number of indices in prog occupied by this command and the commands * that generate data used by this command. * * @param prog ExpressionProgram in which this ExpressionCommand occurs. * @param myIndex index at which ExpressionCommand occurs in prog. * @return total number of indices in prog occupied by this command and commands that generate data used by this command. */ public int extent(ExpressionProgram prog, int myIndex); /** * Return true if this command depends on the value of x, false otherwise. * That is, when apply() is called, can the result depend on the value of x? */ public boolean dependsOn(Variable x); /** * The ExpressionCommand occurs in the program prog at the index indicated by myIndex. * Add a print string representation of the sub-expression represented by this command * (including any previous commands in the program that generate data used by this * command). */ public void appendOutputString(ExpressionProgram prog, int myIndex, StringBuffer buffer); } jcm1-source/edu/hws/jcm/data/ExpressionProgram.java0000644000076500011320000011567111741343635021614 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * An ExprssionProgram represents a mathematical expression such as "3" or "sin(x^2)", stored * in the form of a program for a stack machine. The program consists of a sequence of * commands that, when executed, will compute the value of the expression. * *

Each command is encoded as an integer. There are three types of commands that can * occur: (1) A negative integer must be one of the 36 constant values PLUS, MINUS,..., CUBERT. * These constants represent unary and binary operators and standard functions. (2) An integer in the range * 0 <= n < 0x3FFFFFFF encodes an operation of the form "push a constant onto the stack". * The constant that is being pushed is encoded as an index in the array "constant", * which is a private member of this class that holds all the constants that * occur in this ExpressionProgram. (3) An integer >= 0x3FFFFFFF represents * an ExpressionCommand object. When 0x3FFFFFFF is subtracted from the integer, the * result is an index into the array "command", which is a private member of this * class that holds all the ExpressionCommands that occur in this * ExpressionProgram. */ public class ExpressionProgram implements Expression { /** * Code for a unary or binary operator or a standard function. */ public static final int PLUS = -1, MINUS = -2, TIMES = -3, DIVIDE = -4, POWER = -5, EQ = -6, NE = -7, LT = -8, GT = -9, LE = -10, GE = -11, AND = -12, OR = -13, NOT = -14, UNARY_MINUS = -15, FACTORIAL = -16, SIN = -17, COS = -18, TAN = -19, COT = -20, SEC = -21, CSC = -22, ARCSIN = -23, ARCCOS = -24, ARCTAN = -25, ABS = -26, SQRT = -27, EXP = -28, LN = -29, LOG2 = -30, LOG10 = -31, TRUNC = -32, ROUND = -33, FLOOR = -34, CEILING = -35, CUBERT = -36; /** * If this is non-null, it is used as the print string * for this expression in the toString() method. (When an * expression is created by a Parser by parsing a string, * the parse stores that string in this variable.) */ public String sourceString; private int[] prog = new int[1]; // Contains the commands that make up this program. // (Constants and ExpressionCommands are encoded as // positive numbers, while negative numbers are // opCodes from the above list of constants.) // This array expands as necessary. private int progCt; // The number of commands in prog. //--------------------- Methods for creating the program ------------------------------ /** * Default constructor creates an initially empty program. */ public ExpressionProgram() { } /** * Adds com as the next command in the program. Among other things, for example, com can * be a Variable or Constant. In that case, the meaning of the command is the stack * operation "push (value of com)". * * @param com added as next command in the program. */ public void addCommandObject(ExpressionCommand com) { int loc = findCommand(com); addCommandCode(loc + 0x3FFFFFFF); } /** * Add the number d as the next command in the program. The meaning of this command is * actually the stack operation "push d". * * @param d added as next command in program. */ public void addConstant(double d) { int loc = findConstant(d); addCommandCode(loc); } /** * Add a command code to the program, where code is one of the opCode constants * that are public final members of this class, from CUBERT to PLUS. Each code * represents either a binary or unary operator or a standard function that operates on the stack by * poping its argument(s) from the stack, perfroming the operation, and pushing * the result back onto the stack. */ public void addCommand(int code) { if (code >= 0 || code < CUBERT) throw new IllegalArgumentException("Internal Error. Illegal command code."); addCommandCode(code); } /** * To save space, cut the arrays that holds the program data down to the actual * amount of data that they contain. This should be called after the complete * program has been generated. */ public void trim() { if (progCt != prog.length) { int[] temp = new int[progCt]; System.arraycopy(prog,0,temp,0,progCt); prog = temp; } if (commandCt != command.length) { ExpressionCommand[] temp = new ExpressionCommand[commandCt]; System.arraycopy(command,0,temp,0,commandCt); command = temp; } if (constantCt != constant.length) { double[] temp = new double[constantCt]; System.arraycopy(constant,0,temp,0,constantCt); constant = temp; } } private void addCommandCode(int code) { // System.out.println("Add code " + code + " " + progCt); if (progCt == prog.length) { int[] temp = new int[ prog.length * 2 ]; System.arraycopy(prog,0,temp,0,progCt); prog = temp; } prog[progCt++] = code; } //------------------- Methods for evaluating the program ----------------------- private Cases cases; // If this is non-null while the expression is being evaluated, then // information about "cases" is recorded in it as the evaluation is // done. See the Cases class for more information. private StackOfDouble stack = new StackOfDouble(); // An ExpressionProgram is a program for a stack machien. This is the // stack that is used when the program is executed. /** * Run the ExprssionProgram and return the value that it computes. */ synchronized public double getVal() { cases = null; return basicGetValue(); } /** * Run the ExprssionProgram and return the value that it computes. * If the Cases object, c, is non-null, then information about "cases" is recorded in c. * This information can be used to help detect possible "discontinuities" * between two evaluations. See the Cases class for more information. */ synchronized public double getValueWithCases(Cases c) { cases = c; double d = basicGetValue(); cases = null; return d; } private double basicGetValue() { // Run the program and return its value. stack.makeEmpty(); for (int pc = 0; pc < progCt; pc++) { int code = prog[pc]; if (code < 0) { double ans = applyCommandCode(code); if (Double.isNaN(ans) || Double.isInfinite(ans)) { if (cases != null) cases.addCase(0); return Double.NaN; } stack.push(ans); } else if (code < 0x3FFFFFFF) { stack.push(constant[code]); } else { command[code-0x3FFFFFFF].apply(stack,cases); } } if (stack.size() != 1) throw new IllegalArgumentException("Internal Error: Improper stack state after expression evaluation."); double val = stack.pop(); if (cases != null) cases.addCase( Double.isNaN(val) ? 0 : 1 ); return val; } private void addCase(int c) { // If the member variable cases is not null, record c as the next item of case information in cases. if (cases != null) cases.addCase(c); } /** * Apply the stack operation represented by code (a number < 0) to the stack. */ protected double applyCommandCode(int code) { double ans; if (code < OR) ans = eval(code,stack.pop()); else ans = eval(code,stack.pop(),stack.pop()); return ans; } private double eval(int commandCode, double x) { // Compute the value of the unary operator represented by commandCode, when // applied to the value x. switch (commandCode) { case NOT: return (x == 0)? 1 : 0; case UNARY_MINUS: return -x; case FACTORIAL: return factorial(x); case SIN: return Math.sin(x); case COS: return Math.cos(x); case TAN: addCase((int)Math.floor((x-Math.PI/2.0)/Math.PI)); return Math.tan(x); case COT: addCase((int)Math.floor(x/Math.PI)); return Math.cos(x)/Math.sin(x); case SEC: addCase((int)Math.floor((x-Math.PI/2.0)/Math.PI)); return 1/Math.cos(x); case CSC: addCase((int)Math.floor(x/Math.PI)); return 1/Math.sin(x); case ARCSIN: return Math.asin(x); case ARCCOS: return Math.acos(x); case ARCTAN: return Math.atan(x); case ABS: addCase((x > 0)? 1 : ( (x < 0)? -1 : 0 )); return Math.abs(x); case SQRT: return (x < 0)? Double.NaN : Math.sqrt(x); case EXP: return Math.exp(x); case LN: return (x <= 0)? Double.NaN : Math.log(x); case LOG2: return (x <= 0)? Double.NaN : Math.log(x)/Math.log(2); case LOG10: return (x <= 0)? Double.NaN : Math.log(x)/Math.log(10); case TRUNC: addCase((int)x); return (long)x; case ROUND: addCase((int)Math.floor(x+0.5)); return Math.floor(x+0.5); case FLOOR: addCase((int)Math.floor(x)); return Math.floor(x); case CEILING: addCase((int)Math.floor(x)); return Math.ceil(x); case CUBERT: addCase((x > 0)? 1 : -1); return (x >= 0)? Math.pow(x,1.0/3.0) : -Math.pow(-x,1.0/3.0); default: return Double.NaN; } } private double factorial(double x) { // Compute x!. x is rounded to the nearest integer. If x > 170, then the // answer is too big to represent in a value of type double, so the value // is given as Double.NaN. if (x <= -0.5 || x > 170.5) { addCase(-1); return Double.NaN; } int n = (int)x; addCase(n); double ans = 1; for (int i = 1; i <= n; i++) ans *= i; return ans; } private double eval(int commandCode, double y, double x) { // Compute the value of the unary operator represented by commandCode, when // applied to the values x and y. (Note that the second operand comes // first in the parameter list, since that is the way they were popped // off the stack.) switch (commandCode) { case PLUS: return x+y; case MINUS: return x-y; case TIMES: return x*y; case DIVIDE: addCase( (y > 0)? 1 : ( (y < 0)? -1 : 0 ) ); return x/y; case POWER: return Math.pow(x,y); case EQ: return (x == y) ? 1 : 0; case NE: return (x != y) ? 1 : 0; case GT: return (x > y) ? 1 : 0; case LT: return (x < y) ? 1 : 0; case GE: return (x >= y) ? 1 : 0; case LE: return (x <= y) ? 1 : 0; case AND: return ((x != 0) && (y != 0)) ? 1 : 0; case OR: return ((x != 0) || (y != 0)) ? 1 : 0; default: return Double.NaN; } } //------------- Getting the print string of this expression ------------------------ /** * If a source string has been saved, use it as the print string. (When a Parser creates * an expression by parsing a string, it saves the source string in the ExpressionProgram.) * Otherwise, construct the print string based on the commands in the program. */ public String toString() { if (sourceString != null) return sourceString; else { StringBuffer buffer = new StringBuffer(); appendOutputString(progCt-1,buffer); return buffer.toString(); } } /** * Add a string representing part of the expression to the output buffer. * You probably are only interested in this if you write a ParserExtension * or ExpressionCommand. * (The command at position index in the program represents a * subexpression. It could be a constant or a variable, for example, * which is complete subexpression in itself. Or it could be the * final operator in a larger subexpression. In that case, the operands, * which are located in lower positions in the program, are considered to * be part of the expression. This routine appends a print string * for the entire subexpression to the buffer. When this is called * with index = progCt-1 (the last command in the program), it processes * the entire program. * Note that the hard part here is deciding when to put in parentheses. * This is done based on the precedence of the operators. The result is not always pretty. */ public void appendOutputString(int index, StringBuffer buffer) { if (prog[index] >= 0x3FFFFFFF) { command[prog[index] - 0x3FFFFFFF].appendOutputString(this,index,buffer); } else if (prog[index] >= 0) { buffer.append(NumUtils.realToString(constant[prog[index]])); } else if (prog[index] >= OR) { int op2 = index-1; // Position of command representing the second operand. int op1 = op2 - extent(op2); // Position of command representing the first operand. if (precedence(prog[op1]) < precedence(prog[index]) || (prog[index] == POWER && precedence(prog[op1]) == precedence(prog[index]))) { buffer.append('('); appendOutputString(op1,buffer); buffer.append(')'); } else appendOutputString(op1,buffer); switch (prog[index]) { case PLUS: buffer.append(" + "); break; case MINUS: buffer.append(" - "); break; case TIMES: buffer.append("*"); break; case DIVIDE: buffer.append("/"); break; case POWER: buffer.append("^"); break; case AND: buffer.append(" AND "); break; case OR: buffer.append(" OR "); break; case EQ: buffer.append(" = "); break; case NE: buffer.append(" <> "); break; case GE: buffer.append(" >= "); break; case LE: buffer.append(" <= "); break; case GT: buffer.append(" > "); break; case LT: buffer.append(" < "); break; } if (prog[op2] == UNARY_MINUS || precedence(prog[op2]) < precedence(prog[index]) || ((prog[index] == MINUS || prog[index] == DIVIDE) && precedence(prog[op2]) == precedence(prog[index]))) { buffer.append('('); appendOutputString(op2,buffer); buffer.append(')'); } else appendOutputString(op2,buffer); } else if (prog[index] <= SIN) { buffer.append(StandardFunction.standardFunctionName(prog[index])); buffer.append('('); appendOutputString(index-1,buffer); buffer.append(')'); } else if (prog[index] == UNARY_MINUS) { buffer.append('-'); if (precedence(prog[index-1]) <= precedence(UNARY_MINUS)) { buffer.append('('); appendOutputString(index-1,buffer); buffer.append(')'); } else appendOutputString(index-1,buffer); } else if (prog[index] == NOT) { buffer.append("NOT ("); appendOutputString(index-1,buffer); buffer.append(')'); } else if (prog[index] == FACTORIAL) { if (prog[index-1] >= 0 && (prog[index-1] < 0x3FFFFFFF || command[prog[index-1] - 0x3FFFFFFF] instanceof Variable || command[prog[index-1] - 0x3FFFFFFF] instanceof Constant)) { appendOutputString(index-1,buffer); } else { buffer.append('('); appendOutputString(index-1,buffer); buffer.append(')'); } buffer.append('!'); } } private int precedence(int opcode) { // Return the precedence of the operator. This is used // by the prceding method to decide when parentheses are needed. if (opcode >= 0) { if (opcode >= 0x3FFFFFFF && (command[opcode-0x3FFFFFFF] instanceof ConditionalExpression)) return 0; else return 7; } else switch (opcode) { case FACTORIAL: case OR: case AND: return 1; case GE: case LE: case GT: case EQ: case NE: case LT: return 2; case PLUS: case MINUS: case UNARY_MINUS: return 3; case TIMES: case DIVIDE: return 4; case POWER: return 6; default: return 7; } } //----------------------- Methods for computing the derivative ------------------------- /** * Compute the derivative of this expression with respect to the Variable wrt. * The value returned is actually an ExpressionProgram. */ public Expression derivative(Variable wrt) { ExpressionProgram deriv = new ExpressionProgram(); compileDerivative(progCt-1,deriv,wrt); deriv.trim(); return deriv; } /** * The command at position index in the program represents a subexpression of * the whole expression. This routine adds commands to deriv for computing the * derivative of that subexpression with respect to the variable wrt. * You probably are not interested in this unless you write a ParserExtension * or an ExpressionCommand. */ public void compileDerivative(int index, ExpressionProgram deriv, Variable wrt) { if (!dependsOn(index,wrt)) deriv.addConstant(0); else if (prog[index] >= 0x3FFFFFFF) command[prog[index]-0x3FFFFFFF].compileDerivative(this,index,deriv,wrt); else if (prog[index] >= 0) deriv.addConstant(0); else if (prog[index] >= POWER) { int indexOp2 = index - 1; int indexOp1 = indexOp2 - extent(indexOp2); doBinDeriv(prog[index],indexOp1,indexOp2,deriv,wrt); } else if (prog[index] <= SIN) { doFuncDeriv(prog[index],index-1,deriv,wrt); } else if (prog[index] == UNARY_MINUS) { compileDerivative(index-1,deriv,wrt); deriv.addCommand(UNARY_MINUS); } else if (prog[index] == FACTORIAL) { deriv.addConstant(Double.NaN); } else if (prog[index] >= NOT) throw new IllegalArgumentException("Internal Error: Attempt to take the derivative of a logical-valued expression."); else throw new IllegalArgumentException("Internal Error: Unknown opcode."); } /** * The command at position index in the program represents a subexpression of * the whole expression. This routine finds and returns the number of commands * in the program that are part of that subexpression. That is, the subexpresssion * occupies the part of the program between index - extent + 1 and index. * You probably are not interested in this unless you write a ParserExtension * or an ExpressionCommand. */ public int extent(int index) { if (prog[index] <= NOT) return 1 + extent(index-1); else if (prog[index] < 0) { int extentOp1 = extent(index - 1); // Extent of second operand (which is on top in the program). int extentOp2 = extent(index - 1 - extentOp1); // Extent of second operand. return extentOp1 + extentOp2 + 1; } else if (prog[index] < 0x3FFFFFFF) return 1; else return command[prog[index]-0x3FFFFFFF].extent(this,index); } /** * The command at position index in the program represents a subexpression of * the whole expression. This routine copies the commands for the entire * subexpression to the destination program. * You probably are not interested in this unless you write a ParserExtension * or an ExpressionCommand. */ public void copyExpression(int index, ExpressionProgram destination) { int size = extent(index); for (int i = index-size+1; i <= index; i++) { if (prog[i] < 0) destination.addCommand(prog[i]); else if (prog[i] >= 0x3FFFFFFF) destination.addCommandObject(command[prog[i]-0x3FFFFFFF]); else destination.addConstant(constant[prog[i]]); } } /** * The command at position index in the program represents a subexpression of * the whole expression. If that subexpression includes some dependence on * the variable x, then true is returned. If the subexpression is constant * with respect to the variable x, then false is returned. * You probably are not interested in this unless you write a ParserExtension * or an ExpressionCommand. */ public boolean dependsOn(int index, Variable x) { int size = extent(index); for (int i = index-size+1; i <= index; i++) { if (prog[i] >= 0x3FFFFFFF) { ExpressionCommand c = command[prog[i]-0x3FFFFFFF]; if (c == x || c.dependsOn(x)) return true; } } return false; } /** * Checks whether the expression as a whole has any dependence on the variable x. */ public boolean dependsOn(Variable x) { return dependsOn(progCt - 1, x); } private void doBinDeriv(int opCode, int op1, int op2, ExpressionProgram deriv, Variable wrt) { // Add commands to deriv to compute the derivative of a subexpression of this program // with respect to the variable wrt, where the main operation in the subexpression is // the binary operator opCode, the position of the first operand of the operator is // at the index op1 in the program, and the position of second operand is op2. switch (opCode) { case PLUS: if (!dependsOn(op1,wrt)) compileDerivative(op2,deriv,wrt); else if (!dependsOn(op2,wrt)) compileDerivative(op1,deriv,wrt); else { compileDerivative(op1,deriv,wrt); compileDerivative(op2,deriv,wrt); deriv.addCommand(PLUS); } break; case MINUS: if (!dependsOn(op1,wrt)) { compileDerivative(op2,deriv,wrt); deriv.addCommand(UNARY_MINUS); } else if (!dependsOn(op2,wrt)) compileDerivative(op1,deriv,wrt); else { compileDerivative(op1,deriv,wrt); compileDerivative(op2,deriv,wrt); deriv.addCommand(MINUS); } break; case TIMES: { int cases = 0; if (dependsOn(op2,wrt)) { copyExpression(op1,deriv); if (prog[op2] < 0x3FFFFFFF || command[prog[op2]-0x3FFFFFFF] != wrt) { compileDerivative(op2,deriv,wrt); deriv.addCommand(TIMES); } cases++; } if (dependsOn(op1,wrt)) { copyExpression(op2,deriv); if (prog[op1] < 0x3FFFFFFF || command[prog[op1]-0x3FFFFFFF] != wrt) { compileDerivative(op1,deriv,wrt); deriv.addCommand(TIMES); } cases++; } if (cases == 2) deriv.addCommand(PLUS); } break; case DIVIDE: if (!dependsOn(op2,wrt)) { compileDerivative(op1,deriv,wrt); copyExpression(op2,deriv); deriv.addCommand(DIVIDE); } else if (!dependsOn(op1,wrt)) { copyExpression(op1,deriv); deriv.addCommand(UNARY_MINUS); copyExpression(op2,deriv); deriv.addConstant(2); deriv.addCommand(POWER); deriv.addCommand(DIVIDE); if (prog[op2] < 0x3FFFFFFF || command[prog[op2]-0x3FFFFFFF] != wrt) { compileDerivative(op2,deriv,wrt); deriv.addCommand(TIMES); } } else { copyExpression(op2,deriv); if (prog[op1] < 0x3FFFFFFF || command[prog[op1]-0x3FFFFFFF] != wrt) { compileDerivative(op1,deriv,wrt); deriv.addCommand(TIMES); } copyExpression(op1,deriv); if (prog[op2] < 0x3FFFFFFF || command[prog[op2]-0x3FFFFFFF] != wrt) { compileDerivative(op2,deriv,wrt); deriv.addCommand(TIMES); } deriv.addCommand(MINUS); copyExpression(op2,deriv); deriv.addConstant(2); deriv.addCommand(POWER); deriv.addCommand(DIVIDE); } break; case POWER: if (!dependsOn(op2,wrt)) { copyExpression(op2,deriv); copyExpression(op1,deriv); if ( prog[op2] >= 0 && prog[op2] < 0x3FFFFFFF) { if (constant[prog[op2]] != 2) { deriv.addConstant(constant[prog[op2]] - 1); deriv.addCommand(POWER); } } else if (prog[op2] == UNARY_MINUS && prog[op2-1] >= 0 && prog[op2-1] < 0x3FFFFFFF) { deriv.addConstant( constant[prog[op2-1]] + 1 ); deriv.addCommand(UNARY_MINUS); deriv.addCommand(POWER); } else { copyExpression(op2,deriv); deriv.addConstant(1); deriv.addCommand(MINUS); deriv.addCommand(POWER); } deriv.addCommand(TIMES); if (prog[op1] < 0x3FFFFFFF || command[prog[op1]-0x3FFFFFFF] != wrt) { compileDerivative(op1,deriv,wrt); deriv.addCommand(TIMES); } } else if (!dependsOn(op1,wrt)) { copyExpression(op1,deriv); copyExpression(op2,deriv); deriv.addCommand(POWER); if ( ! (prog[op1] >= 0x3FFFFFFF && command[prog[op1]-0x3FFFFFFF] instanceof Constant && ((Constant)command[prog[op1]-0x3FFFFFFF]).getVal() == Math.E) ) { copyExpression(op1,deriv); deriv.addCommand(LN); deriv.addCommand(TIMES); } if (prog[op2] < 0x3FFFFFFF || command[prog[op2]-0x3FFFFFFF] != wrt) { compileDerivative(op2,deriv,wrt); deriv.addCommand(TIMES); } } else { copyExpression(op1,deriv); copyExpression(op2,deriv); deriv.addCommand(POWER); boolean eq = true; int ext1 = extent(op1); int ext2 = extent(op2); if (ext1 != ext2) eq = false; else for (int i = 0; i < ext1; i++) if (prog[op1-i] != prog[op2-i]) { eq = false; break; } if (eq) deriv.addConstant(1); else { copyExpression(op2,deriv); copyExpression(op1,deriv); deriv.addCommand(DIVIDE); } if (prog[op1] < 0x3FFFFFFF || command[prog[op1]-0x3FFFFFFF] != wrt) { compileDerivative(op1,deriv,wrt); deriv.addCommand(TIMES); } copyExpression(op1,deriv); deriv.addCommand(LN); if (prog[op2] < 0x3FFFFFFF || command[prog[op2]-0x3FFFFFFF] != wrt) { compileDerivative(op2,deriv,wrt); deriv.addCommand(TIMES); } deriv.addCommand(PLUS); deriv.addCommand(TIMES); } break; } } private void doFuncDeriv(int opCode, int op, ExpressionProgram deriv, Variable wrt) { // Add commands to deriv to compute the derivative of a subexpression of this program // with respect to the variable wrt, where the main operation in the subexpression is // the standard function represented by opCode, and the position of the operand of the // standard function is at the index op in the program. /* if (opCode == TRUNC || opCode == ROUND || opCode == FLOOR || opCode == CEILING) { // I'm pretending that the derivative is zero, even though it's undefined at // certain values. deriv.addConstant(0); return; } */ switch (opCode) { case FLOOR: case CEILING: case TRUNC: case ROUND: copyExpression(op,deriv); if (opCode == ROUND) { deriv.addConstant(0.5); deriv.addCommand(PLUS); } deriv.addCommand(ROUND); copyExpression(op,deriv); if (opCode == ROUND) { deriv.addConstant(0.5); deriv.addCommand(PLUS); } deriv.addCommand(NE); if (opCode == TRUNC) { copyExpression(op,deriv); deriv.addConstant(0); deriv.addCommand(EQ); deriv.addCommand(OR); } ExpressionProgram zero = new ExpressionProgram(); zero.addConstant(0); deriv.addCommandObject( new ConditionalExpression(zero , null) ); return; case SIN: copyExpression(op,deriv); deriv.addCommand(COS); break; case COS: copyExpression(op,deriv); deriv.addCommand(SIN); deriv.addCommand(UNARY_MINUS); break; case TAN: copyExpression(op,deriv); deriv.addCommand(SEC); deriv.addConstant(2); deriv.addCommand(POWER); break; case COT: copyExpression(op,deriv); deriv.addCommand(CSC); deriv.addConstant(2); deriv.addCommand(POWER); deriv.addCommand(UNARY_MINUS); break; case SEC: copyExpression(op,deriv); deriv.addCommand(SEC); copyExpression(op,deriv); deriv.addCommand(TAN); deriv.addCommand(TIMES); break; case CSC: copyExpression(op,deriv); deriv.addCommand(CSC); copyExpression(op,deriv); deriv.addCommand(COT); deriv.addCommand(TIMES); deriv.addCommand(UNARY_MINUS); break; case ARCSIN: case ARCCOS: deriv.addConstant(1); if (opCode == ARCCOS) deriv.addCommand(UNARY_MINUS); deriv.addConstant(1); copyExpression(op,deriv); deriv.addConstant(2); deriv.addCommand(POWER); deriv.addCommand(MINUS); deriv.addCommand(SQRT); deriv.addCommand(DIVIDE); break; case ARCTAN: deriv.addConstant(1); deriv.addConstant(1); copyExpression(op,deriv); deriv.addConstant(2); deriv.addCommand(POWER); deriv.addCommand(PLUS); deriv.addCommand(DIVIDE); break; case ABS: { ExpressionProgram pos = new ExpressionProgram(); ExpressionProgram neg = new ExpressionProgram(); compileDerivative(op,pos,wrt); compileDerivative(op,neg,wrt); neg.addCommand(UNARY_MINUS); ExpressionProgram negTest = new ExpressionProgram(); copyExpression(op,negTest); negTest.addConstant(0); negTest.addCommand(LT); negTest.addCommandObject( new ConditionalExpression(neg,null) ); copyExpression(op,deriv); deriv.addConstant(0); deriv.addCommand(GT); deriv.addCommandObject( new ConditionalExpression( pos, negTest ) ); } return; case SQRT: deriv.addConstant(1); deriv.addConstant(2); copyExpression(op,deriv); deriv.addCommand(SQRT); deriv.addCommand(TIMES); deriv.addCommand(DIVIDE); break; case EXP: copyExpression(op,deriv); deriv.addCommand(EXP); break; case LN: case LOG2: case LOG10: ExpressionProgram d = new ExpressionProgram(); d.addConstant(1); copyExpression(op,d); d.addCommand(DIVIDE); if (opCode != LN) { d.addConstant( (opCode == LOG2)? 2 : 10); d.addCommand(LN); d.addCommand(DIVIDE); } copyExpression(op,deriv); deriv.addConstant(0); deriv.addCommand(GT); deriv.addCommandObject( new ConditionalExpression(d,null) ); break; case CUBERT: deriv.addConstant(1); deriv.addConstant(3); copyExpression(op,deriv); deriv.addConstant(2); deriv.addCommand(POWER); deriv.addCommand(CUBERT); deriv.addCommand(TIMES); deriv.addCommand(DIVIDE); break; } if (prog[op] < 0x3FFFFFFF || command[prog[op]-0x3FFFFFFF] != wrt) { compileDerivative(op,deriv,wrt); deriv.addCommand(TIMES); } } //--------- private stuff for storing constants and ExpressionCommands ---------- private double[] constant = new double[1]; // Holds all the constants that have been added // to this ExpressionProgram. In a program, a // constant is represented by an index into this // array. private int constantCt; // The number of values in the constant array. private ExpressionCommand[] command = new ExpressionCommand[1]; // Holds all the ExpressionCommands that have been added // to this ExpressionProgram. In a program, an // ExpressionCommand is represented by an index into this // array, with 0x3FFFFFFF added to the index to give // a number >= 0x3FFFFFFF. private int commandCt; // The number of items in the command array. private int findConstant(double d) { // Find the index of d in the constant array, adding it if it is not already there. // The array will be expanded if necessary. for (int i = 0; i < constantCt; i++) if (constant[i] == d) { return i;} if (constantCt == constant.length) { double[] temp = new double[ constant.length * 2 ]; System.arraycopy(constant,0,temp,0,constantCt); constant = temp; } constant[constantCt++] = d; return constantCt - 1; } private int findCommand(ExpressionCommand com) { // Find the index of com in the command array, adding it if it is not already there. // The array will be expanded if necessary. for (int i = 0; i < commandCt; i++) if (command[i] == com) return i; if (commandCt == command.length) { ExpressionCommand[] temp = new ExpressionCommand[ command.length * 2 ]; System.arraycopy(command,0,temp,0,commandCt); command = temp; } command[commandCt++] = com; return commandCt - 1; } } // end class ExpressionProgram jcm1-source/edu/hws/jcm/data/ParserExtension.java0000644000076500011320000000754611741343635021257 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A ParserExtension can be defined to add new capabilities to a * standard Parser. Examples include user-defined functions and * summations (using a notation of the form "sum(i, 0, n, x^n/i!)"). * A ParserExtension is a MathObject, so it has a name and can be * registered with a Parser. When the Parser encounters the name * in a string, it turns control of the parsing process over to * the ParserExtension, which must parse any necessary arguments * and generate any ExpressionProgram commands. * */ public interface ParserExtension extends MathObject { /** * Parses the part of an expression string associated with this ParserExtension. * This method must add commands to context.prog that will generate exactly ONE * number on the stack when they are executed. Parsing routines from the Parse class, * such as parseFactor and parseExpression, can be called * to parse sub-parts of the string. The name of the command * has already been read from the ParseContext when doParse() is called. * (At the time this is called, context.tokenString is the * name under which this ParserExtension was registered with the * Parser. This makes it possible to register the same ParserExtension * under several names, with each name represnting a different * meaning.) */ public void doParse(Parser parser, ParserContext context); } jcm1-source/edu/hws/jcm/data/Constant.java0000644000076500011320000001626511741343635017715 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A Constant is a Value that represents a constant real number. (The value doesn't have to * be constant in sub-classes, since the member that stores the value is protected, not private.) * A Constant doesn't necessarily need a name. If the name is null, then the print string for the * Constant is the value of the constant. If it has a non-null name, then the print string * is the name. (Note that, as for any MathObject, if the name is null, than the Constant can't * be added to a Parser.) Constant objects are used to represent the mathematical constants * pi and e. * A Constant is both an Expression and an ExpressionCommand. Since it is an ExpressionCommand, * it can occur as a command in an ExpressionProgram. In that case, it simply represens a named constant * occurs in an expression. */ public class Constant implements Expression, ExpressionCommand, MathObject { // Also implements Value, which is a subinterface of Expression. private String name; // This Constant's name, possibly null. /** * The value of this Constant. */ protected double value; /** * Create an unnamed Constant with the given value and null name. */ public Constant(double value) { this.value = value; } /** * Create a Constant with the given name and value. * The name can be null. */ public Constant(String name, double value) { setName(name); this.value = value; } // -------------------- Methods from the MathObject interface ------------------------- /** * Return the name of this Constant. It can be null. */ public String getName() { return name; } /** * Set the name of this Constant. (Note that this should not be done * if the Constant has been registered with a Parser.) */ public void setName(String name) { this.name = name; } // -------------- Method from the Value interface (inherited through Expression) ------ /** * Return the value of this Constant. */ public double getVal() { return value; } // ----------------------- Methods from the Expression interface --------------------- /** * Return the value of the Constant. Since a constant is continuous function, * there is only one "case", so no case information needs to be recorded in cases. */ public double getValueWithCases(Cases cases) { return value; } /** * Return the derivative of this Constant with respect to the variable wrt. * The derivative is another Constant with value zero. */ public Expression derivative(Variable wrt) { return new Constant(0); } /** * Return the print string representing this Constant. The string is the * name of the constant, if that is non-null. Otherwise, it is the value * of the constant. */ public String toString() { if (name == null) return NumUtils.realToString(value); else return name; } // -------------------- Methods from the ExpressionCommand interface ----------------- /** * Apply the Constant to the stack. This is done by pushing the value of * the constant onto the stack. The evaluation of a constant doesn't have any * "cases", so there is no need to record any information in cases. */ public void apply(StackOfDouble stack, Cases cases) { stack.push(getVal()); // Feb 3, 2001 -- changed this from "stack.push(value)", which caused problems with sub-classes! } /** * Add a commands to deriv to evaluate the derivative of this Constant with respect to the * variable. The derivative is 0, so the only command is the constant 0 (which really * represents the stack operation "push 0"). The program and the position of the Constant * in that program are irrelevant. */ public void compileDerivative(ExpressionProgram prog, int myIndex, ExpressionProgram deriv, Variable wrt) { deriv.addConstant(0); } /** * Return the number of locations that this Constant uses in the program. * The value is always 1, since the constant is a complete sub-expression * in itself. */ public int extent(ExpressionProgram prog, int myIndex) { return 1; } /** * Retrun false, since the value of this Constant is independent of the value of x. */ public boolean dependsOn(Variable x) { return false; } /** * Append the print string for this Constant to the buffer. (The values of prog and * myIndex are irrelevant.) */ public void appendOutputString(ExpressionProgram prog, int myIndex, StringBuffer buffer) { buffer.append(toString()); } } // end class Constant jcm1-source/edu/hws/jcm/data/MathObject.java0000644000076500011320000000605011741343635020133 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A MathObject is just an object that has setName and getName methods. * MathObjects can be registered with a Parser (meaning that they are * stored in the SymbolTable associated with the Parser, and can * be used in expressions parsed by the Parser). */ public interface MathObject extends java.io.Serializable { /** * Get the name of this object. */ public String getName(); /** * Set the name of this object. This should not be done if * the MathObject is registered with a Parser. */ public void setName(String name); } jcm1-source/edu/hws/jcm/data/ParseError.java0000644000076500011320000000617311741343635020205 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * Represents a syntax error that is found while a string is being parsed. */ public class ParseError extends RuntimeException { /** * The parsing context that was in effect * at the time the error occurred. This includes * the string that was being processed and the * position in the string where the error occured. * These values are context.data and context.pos. */ public ParserContext context; /** * Create a new ParseError with a given error message and parsing context. */ public ParseError(String message, ParserContext context) { super(message); this.context = context; } } jcm1-source/edu/hws/jcm/data/SymbolTable.java0000644000076500011320000001360411741343635020333 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; import java.util.Hashtable; /** * A symbol table contains MathObjects, associating them * with their names. To support scoping (for example), a symbol * table can have a parent symbol table. If a symbol is not found * in the table itself, the search procedes to its parent. * MathObjects in the parent are hidden by MathObjects of * the same name in a SymbolTable. * Note that a NullPointerException error will occur if an * attempt is made to add a MathObject with a null name to a * SymbolTable. * A MathObject should not be renamed while it is registered * in a SymbolTable. * Note that a Parser has an associated SymbolTable. I expect * SymbolTables to be used only through Parsers. */ public class SymbolTable implements java.io.Serializable { private Hashtable symbols; // Objects are stored here and in the parent. private SymbolTable parent; // Parent symbol table, possibly null. /** * Construct a symbol table with null parent. */ SymbolTable() { this(null); } /** * Construct a symbol table with specified parent. */ SymbolTable(SymbolTable parent) { this.parent = parent; symbols = new Hashtable(); } /** * Returns the parent symbol table of this symbol table. */ SymbolTable getParent() { return parent; } /** * Look up the object with the given name, if any. * If not found, return null. (If the name is not found * in the HashTable, symbols, the search is delegated to * the parent.) If the name is null, then * null is returned. */ synchronized public MathObject get(String name) { if (name == null) return null; Object sym = symbols.get(name); if (sym != null) return (MathObject)sym; else if (parent != null) return parent.get(name); else return null; } /** * Adds sym to the SymbolTable, associating it with its name. */ synchronized public void add(MathObject sym) { if (sym == null) throw new NullPointerException("Can't put a null symbol in SymbolTable."); add(sym.getName(), sym); } /** * Adds the given MathObject, sym, to the symbol table, * associating it with the given name (which is probably * the name of the symbol or that name transformed to lower * case, but it doesn't have to be). * If the same name is already in use in the HashTable * then the new object replaces the current object. * Note that if the name is defined in the parent * symbol table, then the old object is hidden, not * removed from the parent. If sym is null or if * sym's name is null, than a NullPointerException is * thrown. */ synchronized public void add(String name, MathObject sym) { if (sym == null) throw new NullPointerException("Can't put a null symbol in SymbolTable."); else if (name == null) throw new NullPointerException("Can't put unnamed MathObject in SymbolTable."); symbols.put(name,sym); } /** * Remove the object with the given name from the symbol table, * but NOT from the parent symbol table. No error occurs * if the name is not in the table or if name is null. * If an object of the same name occurs in the parent, * this routine will un-hide it. */ synchronized public void remove(String name) { if (name != null) symbols.remove(name); } } // end class SymbolTable jcm1-source/edu/hws/jcm/data/Function.java0000644000076500011320000001114511741343635017701 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A Function is a mathematical real-valued function of zero or more * real-valued arguments. The number of arguments is called the arity * of the function. */ abstract public interface Function extends java.io.Serializable { /** * Return the number of arguments of this function. This must * be a non-negative integer. */ public int getArity(); /** * Find the value of the function at the argument values * given by arguments[0], arguments[1], ... The length * of the array, arguments, should be equal to the arity of * the function. */ public double getVal( double[] arguments ); /** * Find the value of the function at the argument values * given by arguments[0], arguments[1], ... The length * of the array argument should be equal to the arity of * the function. Information about "cases" is stored in * the Cases parameter, if it is non-null. See the Cases * class for more information. */ public double getValueWithCases( double[] arguments, Cases cases ); /** * Return the derivative of the function with repect to * argument number wrt. For example, derivative(1) returns * the derivative function with respedt to the first argument. * Note that argements are numbered starting from 1. */ public Function derivative(int wrt); /** * Return the derivative of the function with respect to the * variable x. This will be non-zero only if x occurs somehow in * the definition of x: For example, f(y) = sin(x*y); * (This routine is required for the general function-differentiating * code in the class FunctionParserExtension.) */ public Function derivative(Variable x); /** * Return true if the defintion of this function depends * in some way on the variable x. If not, it's assumed * that the derivative w.r.t. x of the function, applied to any * arguments that do not themselves depend on x, is zero. * (This routine is required for the general function-differentiating * code in the class FunctionParserExtension.) */ public boolean dependsOn(Variable x); } jcm1-source/edu/hws/jcm/data/SimpleFunction.java0000644000076500011320000001670011741343635021055 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * An object belonging to this class is a function of one or more variables. * It is defined by an expression, e, and a list of variables, which presumably * can occur in e. The value of the function at arguments x1,x2,... is obtained by * assigning the x's as the values of the variables and then evaluating * the expression e. * *

Note: When the function is evaluated, the values of the variables that act * as its parameters are saved and the variables are set to the values of the actual parameters * provided for the function. After the evaluation, the saved values are restored. * Usually, this is OK, but if setting the value of the variable has a side effect * (such as changing the position of a VariableSlider), it could be a problem. * So, don't use the variables associated with VariableSliders or VariableInputs * in SimpleFunctions. * *

Note: A SimpleFunction is not a MathObject and does not have a name. * It cannot be added to a Parser. If you want to do somethign like that, * use an ExpressionFunction (defined in package edu.hws.jcm.functions). */ public class SimpleFunction implements Function { private Expression e; // The expression that defines the function. private Variable[] v; // The variables, presumably used in the expression, // which acts as the parameter of the function. private String name; // The name of the function. private double[] save; // For saving values of variables when the function // is evaluated. /** * Create the function of one variable such that the value * of the function at x is obtained by temporarily assigning x as the * value of the variable v and then evaluating e. e and v * should be non-null (or errors will result when the function * is used. Note that evaluating the function will temporarily * change the value of the variable. */ public SimpleFunction(Expression e, Variable v) { this(e, new Variable[] { v }); } /** * Create a function. The arity of the function is * the length of the array v. The value of the function * at a given list of arguments is obtained by temporarily * assigning the arguement values to the variables, and then * returning the value of the expression. e and v should * be non-null. Note that evaluating the function will * temporarily change the values of the variables. */ public SimpleFunction(Expression e, Variable[] v) { this.e = e; this.v = v; save = new double[v.length]; } /** * Return the number of arguments of this function. */ public int getArity() { return v.length; } /** * Find the value of the function at the argument values * argument[0], argument[1].... (If not enough arguments are supplied, * an ArrayIndexOutOfBoundsException will be thrown. Extra * arguments are ignored.) */ public double getVal( double[] arguments ) { return getValueWithCases(arguments,null); } /** * Find the value of the function at the argument values * argument[0], argument[1].... Information about "cases" is stored in * the Cases parameter, if it is non-null. See the Cases * class for more information. */ public double getValueWithCases( double[] arguments, Cases cases ) { for (int i = 0; i < v.length; i++) { save[i] = v[i].getVal(); v[i].setVal(arguments[i]); } double val = e.getValueWithCases(cases); for (int i = 0; i < v.length; i++) v[i].setVal(save[i]); return val; } /** * Return the derivative of the function with repect to * argument number wrt, where arguments are numbered starting from 1. */ public Function derivative(int wrt) { if (wrt < 1 || wrt > v.length) throw new IllegalArgumentException("Internal Error. Function does not have an argument number " + wrt); return new SimpleFunction(e.derivative(v[wrt-1]),v); } /** * Return the derivative of the function with respect to the * variable x. (Note that the derivative w.r.t one of the variables * that are being used as the parameter of this function is ZERO!) * To get the derivative with respect to the i-th parameter variable, * call derivative(i). */ public Function derivative(Variable x) { for (int i = 0; i < v.length; i++) if (x == v[i]) return new SimpleFunction(new Constant(0),v); return new SimpleFunction(e.derivative(x),v); } /** * Return true if the definition of this function depends * in some way on the variable x. (Note that the function does * NOT depend on the variables that are being used as its parameters!) */ public boolean dependsOn(Variable x) { for (int i = 0; i < v.length; i++) if (x == v[i]) return false; return e.dependsOn(x); } } // end class SimpleFunction jcm1-source/edu/hws/jcm/data/Value.java0000644000076500011320000000555311741343635017176 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * An object of type Value has a real-number value that can be retrieved by * calling the getVal() method. * This is a central interface, since Value objects are used throughout the * JCM system where a real number is needed. */ public interface Value extends java.io.Serializable { /** * Gets the current value of this object. */ public double getVal(); } jcm1-source/edu/hws/jcm/data/StackOfDouble.java0000644000076500011320000001007711741343635020604 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A standard stack of values of type double, which can grow to arbitrary size. */ public class StackOfDouble implements java.io.Serializable { private double[] data; // Contents of stack. private int top; // Number of items on stack. /** * Create an initially empty stack. It initially has space allocated for one item. */ public StackOfDouble() { data = new double[1]; } /** * Create an empty stack that initially has space for initialSize items pre-allocated. * If initialSize <= 0, an initialSize of 1 is used. */ public StackOfDouble(int initialSize) { data = new double[initialSize > 0 ? initialSize : 1]; } /** * Add x to top of stack. */ public void push(double x) { if (top >= data.length) { double[] temp = new double[2*data.length]; System.arraycopy(data,0,temp,0,data.length); data = temp; } data[top++] = x; } /** * Remove and return the top item on the stack. * Will throw an exception of type java.util.EmptyStackException * if the stack is empty when pop() is called. */ public double pop() { if (top == 0) throw new java.util.EmptyStackException(); return data[--top]; } /** * Return true if and only if the stack contains no items. */ public boolean isEmpty() { return (top == 0); } /** * Clear all items from the stack. */ public void makeEmpty() { top = 0; } /** * Return the number of items on the stack. */ public int size() { return top; } } // end class StackOfDouble jcm1-source/edu/hws/jcm/data/Variable.java0000644000076500011320000001312411741343635017640 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A Variable is a Value object whose value can be changed. Usually, a Variable will have * a name, although that is not required unless tha Variable is going to be * registered with a Parser. A Variable can be used as a Value, an Expression, * or an ExpressionCommand. Since it is an ExpressionCommand, it can occur * as a command in an ExpressionProgram. In that case, it simply represents a variable * that occurs as part of an expression. *

* This class implements the Expression, ExpressionCommand, MathObject, * and Value interfaces (since Constant implements them). *

* Most methods in interfaces Value, Exprssion, ExpressionCommand, and MathObject * are inherited from class Constant. The following four methods override * methods inherited from that class: * public Expression derivative(Variable wrt); * public void compileDerivative(ExpressionProgram prog, int myIndex, ExpressionProgram deriv, Variable wrt); * public boolean dependsOn(Variable x); and * public String toString(). */ public class Variable extends Constant { /** * Create an unnamed Variable with initial value 0. */ public Variable() { super(0); } /** * Create a Variable with the given name and with initial value zero. * (The name can be null.) */ public Variable(String name) { super(name,0); } /** * Create a Variable with the given name and given initial value. * (The name can be null.) */ public Variable(String name, double value) { super(name,value); } /** * Set the value of this Variable to the specified value. */ public void setVal(double value) { this.value = value; } /** * Return the derivative of this Variable with respect to the * Variable wrt. The answer is 1 if wrt is this Variable. * Otherwise, the answer is 0. * @param wrt "with respect to", i.e., the variable with respect to which to * take the derivative. * @return a constant: 1 if wrt is this Variable, 0 otherwise. */ public Expression derivative(Variable wrt) { return new Constant( (wrt == this)? 1 : 0 ); } /** * Add a command to deriv to evaluate the derivative of this Variable with respect to the * Variable wrt. The derivative is a command for pushing either 1 or 0, depending on whether * wrt is this Variable or some other Variable. This is not meant to be called directly. */ public void compileDerivative(ExpressionProgram prog, int myIndex, ExpressionProgram deriv, Variable wrt) { deriv.addConstant( (wrt == this)? 1 : 0 ); } /** * Check whether the value of this variable depends on the value of x. This * is true if x is this Variable, false otherwise. */ public boolean dependsOn(Variable x) { return this == x; } /** * Return a print string representing this variable. The string is the * name of the variable, if it has one. If not, the string "(unnamed variable)" */ public String toString() { String s = getName(); return (s == null)? "(unnamed variable)" : s; } } // end class Variable jcm1-source/edu/hws/jcm/data/ValueMath.java0000644000076500011320000001061611741343635020004 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A ValueMath object is an easy way to create Value objects that are computed * from other Value objects. For example, "new ValueMath(a,b,'+')" is an * object whose value is obtained by adding the values of a and b. */ public class ValueMath implements Value { private Function f; // If non-null, this is a value of the form f(params); // If null, it's of the form x + y, x - y, ... private double[] param; private Value x,y; private char op; /** * Create a ValueMath object whose value is computed by applying an arithmetic * operator the values of x and y. * @param op The arithmetic operator that is to be applied to x and y. This should * be one of the characters '+', '-', '*', '/', or '^'. (No error is * thrown if another character is provided. It will be treated as a '/'). */ public ValueMath(Value x, Value y, char op) { this.x = x; this.y = y; this.op = op; } /** * Create a ValueMath object whose value is computed as f(x). */ public ValueMath(Function f, Value x) { if (f.getArity() != 1) throw new IllegalArgumentException("Internal Error: The function in a ValueMath object must have arity 1."); this.f = f; this.x = x; param = new double[1]; } /** * Get the value of this object. */ public double getVal() { if (f != null) { param[0] = x.getVal(); return f.getVal(param); } else { double a = x.getVal(); double b = y.getVal(); switch (op) { case '+': return a+b; case '-': return a-b; case '*': return a*b; case '/': return a/b; case '^': return Math.pow(a,b); default: throw new IllegalArgumentException("Internal Error: Unknown math operator."); } } } } // end class ValueMath jcm1-source/edu/hws/jcm/data/Expression.java0000644000076500011320000001052611741343635020255 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * An Expression represents a mathematical expression such as "x+1" or * "3" or "sin(x*ln(x)-3*abs(x/4))". An expression has a value, which * can depend on the values of variables that occur in the expression. * An expression can be differenetiated with respect to a variable. It has * a print string representation. This interface is implemented by * the classes Constant, Variable, and ExpressionProgram, for example. * The Expression interface * represents all the properties of expressions that you are likely to need * to know about, unless you want to write a new kind of ExpressionCommand. */ public interface Expression extends Value { // The method "public double getVal()" is inherited from the Value interface. // It returns the current value of this expression. /** * Compute and return the value of this expression. If cases is non-null, * then data is stored in cases that can be used to check for possible * discontinuities between one evaluation and the next. See the class * Cases for more information. */ public double getValueWithCases(Cases cases); /** * Return an Expression that represents the derivative of * this expression with respect to the variable wrt. * (Note that if the expression contains other variables * besides wrt, this is actually a partial derivative.) */ public Expression derivative(Variable wrt); /** * Checks whether the expression has any dependence on the variable x. */ public boolean dependsOn(Variable x); /** * Get a print string representation of this expression. (Of course, * every object defines toString(). It is included here to remind you * that classes that implement the Expression interface should have * a print string representation that looks like a mathematical * expression.) */ public String toString(); } jcm1-source/edu/hws/jcm/data/ParserContext.java0000644000076500011320000003756311741343635020731 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A ParserContext holds all the state data for a parsing operation, including the * string that is being parsed, a pointer to the current position in that string, * and the most recently parsed token from the string. The ParserContext object * does the tokenization. Token types are retrieved by calling look() and * next(). Attributes of the token are then available in the member variables * tokenString, tokenObject, and tokenValue. You will probably only use this * if you write a ParserExtension. */ public class ParserContext implements java.io.Serializable { /** * One of the possible token types returned by look() and next(). * Represents the end of the string * that is being parsed. */ public static final int END_OF_STRING = 1; /** * One of the possible token types returned by look() and next(). * Indicates aht the token is a number. The numerical value * of the token is in the tokenValue member variable. */ public static final int NUMBER = 2; /** * One of the possible token types returned by look() and next(). * The token is a word. If there is a * MathObject in the symbol table associated * with this word, then that object is in the * tokenObject member variable. If not, tokenObject is null. */ public static final int IDENTIFIER = 3; /** * One of the possible token types returned by look() and next(). * Any other token besides end-of-string, number, or word. * The only information about * the token is the tokenString member variable. For some special operators * (<> <= <=), the tokenString has two characters, but * generally it has only one. Note that ** is translated * to ^. Also, the special tokens "and", "or", and "not" * are translated to type OPCHARS with tokenString * equal to "&", "|", or "~" (but only if options & BOOLEANS is != 0). */ public static final int OPCHARS = 4; private static final int NONE = 0; // A special value for token that is used internally to // mean that the current token has been consumed by next() // so that when look() or next() is called again, a new // token has to be read. (Note that this is the initial value // of token.) /** * The string that is being parsed. */ public String data; /** * Current position in that string, indicating how many * characters have been consumed. */ public int pos; /** * The ExpressionProgram that is being generated as the string * is parsed. Note that while parsing a ConditionalExpression, the * value of prog is temporarily changed. ParserExtensions might * want to do something similar. */ public ExpressionProgram prog; /** * The most recently read token type, or NONE if that token * has been consumed by a call to next(). The value NONE is never * returned by look() or next(). */ public int token; /** * The substring of the parse string that corresponds to the most recently * read token. This can change when look() or next() is called. */ public String tokenString; /** * If the most recently read token was of type IDENTIFIER, then * this is the corresponding MathObject from the symbol table, * or null if the identifier is not in the symbol table. */ public MathObject tokenObject; /** * If the most recently read token was of type NUMBER, then * this is its numerical value. */ public double tokenValue; /** * The options from the Parser. Some of these options * affect tokenization, such as whether BOOLEANS is enabled. */ public int options; /** * The Parser's symbol table, which is used for looking up * tokens of type IDENTIFIER. */ protected SymbolTable symbols; private StringBuffer tokBuf = new StringBuffer(); // Used in the readToken method. (Created // once for efficiency.) /** * Create a ParserContext for parsing the data String, using the * specified options and symbol table. A new ExpressionProgram * is created to hold the program that will be generated from * the string. */ public ParserContext(String data, int options, SymbolTable symbols) { this.data = data; this.options = options; this.symbols = symbols; prog = new ExpressionProgram(); } //---------- Wrapper functions for accessing the symbol table -------------- /** MathObjects added to the symbol table after a call to mark() will * be removed by a later, matching call to revert(). In the meantime, * older symbols of the same name will only be hidden, not replaced, * so they will still be there after the revert. It is important that * a call to this routine is followed by a later call to revert! No * error checking is done to make sure that this is true. */ public void mark() { // ADDED SEPTEMBER 23, 2000 symbols = new SymbolTable(symbols); } /* After a call to mark(), revert() must be called to restore the * state of the symbol table. */ public void revert() { // ADDED SEPTEMBER 23, 2000 symbols = symbols.getParent(); } /** * Get the MathObject associated with name in the symbol table. */ public MathObject get(String name) { if ( (options & Parser.CASE_SENSITIVE) != 0 ) return symbols.get(name); else return symbols.get(name.toLowerCase()); } /** * Add a new MathObject to the symbol table. */ public void add(MathObject sym) { if ( (options & Parser.CASE_SENSITIVE) != 0 ) symbols.add(sym); else symbols.add(sym.getName().toLowerCase(), sym); } //---------------------------- Tokenization --------------------------------- /** * Consume one token from the string. The token type is returned. * After this is called, attributes of the token can be obtained * from the public member variables tokenString, tokenObject, * and tokenValue. Note that the END_OF_STRING token is never * really consumed and can be returned multiple times. Can * throw a ParseError in the case of an illegal numeric token. */ public int next() { int tok = look(); if (token != END_OF_STRING) token = NONE; return tok; } /** * Look ahead at the next token in the data string, without consuming it. * Successive calls to look() will return the same token. (The token * must be consumed by a call to next().) The token type is returned. * After a call to look(), attributes of the token can be obtained * from the public member variables tokenString, tokenObject, * and tokenValue. Can throw a ParseError in the case of an illegal * numeric token. */ public int look() { if (token == NONE) { // Token has been consumed. Read a new token. while (pos < data.length() && (data.charAt(pos) == ' ' || data.charAt(pos) == '\t')) pos++; if (pos >= data.length()) { token = END_OF_STRING; tokenString = null; } else readToken(); } return token; } /** * Read the next token from the data string, and set the values of token, tokenString, * tokenNumber, and tokenObject appropriately. * When this is called, we know that pos < data.length and data.charAt(pos) * is not blank or tab. */ private void readToken() { char ch = data.charAt(pos); // The first character of the token. This determines the token type. int savePosition = pos; // The starting position in data string. tokBuf.setLength(0); if (Character.isLetter(ch) || ( ch == '_' && ( (options & Parser.NO_UNDERSCORE_IN_IDENTIFIERS) == 0) )) { token = IDENTIFIER; while ( Character.isLetter(ch) || ( ch == '_' && ((options & Parser.NO_UNDERSCORE_IN_IDENTIFIERS) == 0) ) || ( Character.isDigit(ch) && ((options & Parser.NO_DIGITS_IN_IDENTIFIERS) == 0) ) ) { tokBuf.append(ch); pos++; if (pos >= data.length()) break; ch = data.charAt(pos); } tokenString = tokBuf.toString(); tokenObject = null; for (int i = tokenString.length(); i > 0; i--) { // Tricky programming: If the OPTIONAL_SPACES option is not set, // then this for loop is executed only once, because of the break // at the end. Therefor, only the complete string is tested as // being a known identifier. If the option is off, then // each prefix of the string is tested, the process ending with // the longest prefix that is a known identifier. However, if // no prefix is a known word, then the entire string is reported // as an unknown identifier. String str = tokenString.substring(0,i); if ( ((options & Parser.BOOLEANS) != 0) ) { // If BOOLEANS is enabled, the special words "and", "or", "not" are // converted to operators. Case is ignored. Note that when BOOLEANS // is enabled, it is impossible to have MathObjects named "and", "or", // "And", "AND", etc. (That is, such MathObjects are hidden if they exist.) if (str.equalsIgnoreCase("and")) { token = OPCHARS; tokenString = "&"; pos = savePosition + 3; return; } else if (str.equalsIgnoreCase("or")) { token = OPCHARS; tokenString = "|"; pos = savePosition + 2; return; } else if (str.equalsIgnoreCase("not")) { token = OPCHARS; tokenString = "~"; pos = savePosition + 3; return; } } if ( get(str) != null ) { tokenString = str; tokenObject = get(tokenString); pos = savePosition + i; break; } if ( ((options & Parser.OPTIONAL_SPACES) == 0) ) break; } } else if (Character.isDigit(ch) || (ch == '.' && pos < data.length()-1 && Character.isDigit(data.charAt(pos+1)))) { token = NUMBER; while (pos < data.length() && Character.isDigit(data.charAt(pos))) tokBuf.append(data.charAt(pos++)); if (pos < data.length() && data.charAt(pos) == '.') { tokBuf.append(data.charAt(pos++)); while (pos < data.length() && Character.isDigit(data.charAt(pos))) tokBuf.append(data.charAt(pos++)); } if (pos < data.length() && (data.charAt(pos) == 'e' || data.charAt(pos) == 'E')) { savePosition = pos; tokBuf.append(data.charAt(pos++)); if (pos < data.length() && (data.charAt(pos) == '+' || data.charAt(pos) == '-')) tokBuf.append(data.charAt(pos++)); if ( pos >= data.length() || (!Character.isDigit(data.charAt(pos))) ) { if ( (options & Parser.OPTIONAL_STARS) == 0 ) throw new ParseError("Illegal number, '" + tokBuf.toString() + "'. No digits in exponential part.",this); else pos = savePosition; } else { while (pos < data.length() && Character.isDigit(data.charAt(pos))) tokBuf.append(data.charAt(pos++)); } } tokenString = tokBuf.toString(); double d = NumUtils.stringToReal(tokenString); if (Double.isInfinite(d)) throw new ParseError("The number '" + tokBuf.toString() + "' is outside the range of legal numbers.",this); if (Double.isNaN(d)) throw new ParseError("The string '" + tokBuf.toString() + "' is not a legal number.",this); tokenValue = d; } else { token = OPCHARS; tokenString = "" + ch; pos++; if (pos < data.length()) { // Check for two-character operators. char nextch = data.charAt(pos); switch (ch) { case '*': if (nextch == '*') { // "**" is an alternative to "^". tokenString = "^"; pos++; } break; case '=': if (nextch == '<' || nextch == '>') tokenString = data.charAt(pos++) + tokenString; break; case '<': if (nextch == '=' || nextch == '>') tokenString += data.charAt(pos++); break; case '>': if (nextch == '=') tokenString += data.charAt(pos++); else if (nextch == '<') tokenString = data.charAt(pos++) + tokenString; break; } } } } } // end class ParserContext jcm1-source/edu/hws/jcm/data/Cases.java0000644000076500011320000001114511741343635017152 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * An object of type Cases stores a list of "case values" that is generated * while an expression is being evaluated using the routine Expression.getValuesWithCases(). * This information can be used as a heuristic (i.e. a fudge) to help detect * a possible discontinuity between two evaluations of the expression. Suppose * that the expression is evaluated twice, with some change of variable values * between the two evaluations. If the variables' values are not changed too much, * and if the Cases objects generated by the two evaluations are equal (as determined * by the "equals" method defined in this class), then it is likely that * there is no discontinuity. (However, this is not perfect. The discontinuity * in 1/x^2 won't be detected since the case value generated by 1/f(x) only * checks the sign of f(x), and the denominator of 1/x^2 is positive on both * sides of x=0. If you want to be more paranoid, check both the expression * and its derivative.) (I really don't like this very much, but it can be used to draw * pretty good graphs in general.) */ public class Cases { private int[] cases = new int[1]; // Array of values that have been added with addCase(value). private int caseCt; // Number of items stored in cases array. /** * Remove all the cases that have been added with addCase(). * This makes it possible to reuse this object in another * call to Expression.getValueWithCases(). */ public void clear() { caseCt = 0; } /** * Add a new case value to the list stored in this object. */ public void addCase(int value) { if (caseCt == cases.length) { int[] temp = new int[2*caseCt]; System.arraycopy(cases,0,temp,0,caseCt); cases = temp; } cases[caseCt++] = value; } /** * Test whether c contains exactly the same list of case * values as this Cases object does. */ public boolean equals(Cases c) { if (c.caseCt != caseCt) return false; for (int i = 0; i < caseCt; i++) if (c.cases[i] != cases[i]) return false; return true; } } // end class Cases jcm1-source/edu/hws/jcm/data/ConditionalExpression.java0000644000076500011320000001753111741343635022444 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ package edu.hws.jcm.data; /** * A "conditional expression" is an expression using the "?" operator, such as "(x > 0)? x : -x" * or "x <> 3 ? 1 / (x - 3)". Note that the second case, which follows the ":", is optional. * If not present, the expression has the value Double.NaN when the boolean condition evaluates * to false. A ConditionalExpression object is generated by a parser when it encounters a * "?" operator in the string it is parsing (provided the BOOLEANS option is turned on in that parser). *

A ConditionalExpression object holds the two expressions that are the cases in a conditional expression. * Note that the boolean condition is NOT stored in the ConditionalExpression object; it is part of * the ExpressionProgram for the expression in which the conditional expression occurs. * A ConditionalExpression is an ExpressionCommand, meaning that it can occur in an ExpressionProgram. * When the apply() method in this class is called, the boolean condition has already been evaluated, * and the result is on the top of the stack. The ConditionalExpression will look at this result * and replace it with the value of one of the two expressions that it contains. *

It is unlikely that there will be any reason for anyone to use or understand this class, * except possibly as an example of an ExpressionCommand. */ public class ConditionalExpression implements ExpressionCommand { private ExpressionProgram trueCase; // Expression to be evaluated if condition is true. private ExpressionProgram falseCase; // Expression to be evaluated if condition is false. // This can be null, representing the value Double.NaN. /** * Create a ConditionalExpression object containing the two given expressions. * trueCase must not be null, but falseCase can be null. */ public ConditionalExpression(ExpressionProgram trueCase, ExpressionProgram falseCase) { this.trueCase = trueCase; this.falseCase = falseCase; } // ---------------- Methods from the ExpressionCommand interface ------------------------- /** * Apply this ConditionalExpression to the stack. * (Get the top item from the stack. If it is non-zero (representing a boolean * value of true), evaluate trueCase and put it's value on the stack. Otherwise, * evaluate falseCase and put its value on the stack. If cases is non-null, record * case values for the boolean condition and for the expression that is evaluated.) */ public void apply(StackOfDouble stack, Cases cases) { double test = stack.pop(); if (cases != null) cases.addCase((int)test); if (test != 0) stack.push(trueCase.getValueWithCases(cases)); else if (falseCase != null) stack.push(falseCase.getValueWithCases(cases)); else stack.push(Double.NaN); } /** * Add commands to deriv that evaluate the derivative of this conditional expression with * respect to the variable wrt. Assume that the ConditionalExpression Object * occurs in the program prog at index myIndex. * (The derivative of a conditional expression is another conditional expression. * The boolean test for the derivative is the same as the test for this expression, * so copy that test onto deriv. Than add a new ConditionalExpression object to deriv * whose trueCase is the derivative of this.trueCase and whose falseCase is the derivative of * this.falseCase.) */ public void compileDerivative(ExpressionProgram prog, int myIndex, ExpressionProgram deriv, Variable wrt) { prog.copyExpression(myIndex-1,deriv); ExpressionProgram trueDeriv = (ExpressionProgram)trueCase.derivative(wrt); ExpressionProgram falseDeriv = (falseCase == null)? null : (ExpressionProgram)falseCase.derivative(wrt); deriv.addCommandObject( new ConditionalExpression( trueDeriv, falseDeriv ) ); } /** * Assume that this ConditionalExpression object occurs in prog at index myIndex. * Compute the total number of commands in prog used by the conditional expression, * including the boolean test, which occurs in prog at position myIndex-1. * (The number of commands in prog used by the conditional expression is 1 (for the * ConditionalExpression object itself) plus the number of commands in the * boolean condition.) */ public int extent(ExpressionProgram prog, int myIndex) { return 1 + prog.extent(myIndex-1); } /** * Returns true if x occurs in either the trueCase or the falseCase expression. */ public boolean dependsOn(Variable x) { return trueCase.dependsOn(x) || (falseCase != null && falseCase.dependsOn(x)); } /** * Append the string representation of the expression (including the boolean * condition) to the buffer. Assume that this ConditionalExpression occurs as * a command in prog at index myIndex (so the boolean condition starts at index myIndex-1). */ public void appendOutputString(ExpressionProgram prog, int myIndex, StringBuffer buffer) { buffer.append('('); prog.appendOutputString(myIndex-1,buffer); buffer.append(") ? ("); buffer.append(trueCase.toString()); buffer.append(')'); if (falseCase != null) { buffer.append(" : ("); buffer.append(falseCase.toString()); buffer.append(')'); } } } // end class ConditionalExpression jcm1-source/Parametric.java0000644000076500011320000004123111741343635015142 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // The ParametricCurve applet is a configurable applet that displays a parametric // curve defined by two functions of a parameter t. There can be a "Tracer" button // on the applet. When the user clicks this button, a crosshair is moved along // the curve from beginning to end. import java.awt.*; import java.applet.Applet; import java.util.StringTokenizer; import java.util.Hashtable; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; public class Parametric extends GenericGraphApplet { // Declare some private variables that are created in one method in // this class and used in a second method. private Function xFunc,yFunc; // The functions that are graphed. private ParametricCurve graph; // The graph of the function. private Animator tracer; // for tracing the curve by moving a crosshair along it private Crosshair crosshair; // Crosshair used for tracing the graph private VariableInput tMin,tMax; // for inputting limits on t private VariableInput tIntervals; // for inutting the number of intervals into which the t-range is divided private ExpressionInput functionInput2; // for inputting yFunc; xFunc is input in functionInput protected void setUpParameterDefaults() { parameterDefaults = new Hashtable(); parameterDefaults.put("TwoLimitsColumns", "yes"); parameterDefaults.put("Variable","t"); parameterDefaults.put("XName","x"); // we need this so that xmin and xmax boxes are labeled correctly; // without it, the name would come from the variable name, t, instead of x parameterDefaults.put("FunctionLabel", " " + getParameter("XName") + "(" + getParameter("Variable") + ") = "); parameterDefaults.put("FunctionLabel2", " " + getParameter("YName","y") + "(" + getParameter("Variable") + ") = "); } protected void setUpCanvas() { // Override this to add more stuff to the canvas. super.setUpCanvas(); // Do the common setup: Add the axes and // When setUpCanvas is called, the function inputs already exist, if they are // to be used, since they are created in setUpBopttomPanel(), which is called // before setUpCanvas(). If functionInput exists, add a graph of the functions // from functionInput and functionInput2 to the canvas. If not, create a graph // of the functions specified by the parameters named "Function" and "Function2". if (functionInput != null) { xFunc = functionInput.getFunction(xVar); yFunc = functionInput2.getFunction(xVar); } else { String xFuncDef = " cos(" + xVar.getName() + ") + cos(3*" + xVar.getName() + ")"; String yFuncDef = " sin(4*" + xVar.getName() + ") - sin(2*" + xVar.getName() + ")"; xFuncDef = getParameter("Function", xFuncDef); yFuncDef = getParameter("Function2", yFuncDef); Function f = new SimpleFunction( parser.parse(xFuncDef), xVar ); xFunc = new WrapperFunction(f); f = new SimpleFunction( parser.parse(yFuncDef), xVar ); yFunc = new WrapperFunction(f); } graph = new ParametricCurve(xFunc,yFunc); Color color = getColorParam("CurveColor"); if (color != null) graph.setColor(color); // if inputs are desired to control the parameter on the curve, set them up here if ("no".equalsIgnoreCase(getParameter("UseParamInputs","yes"))) { tMin = new VariableInput(xVar.getName() + "Start",getParameter("ParameterMin","-2")); tMax = new VariableInput(xVar.getName() + "End",getParameter("ParameterMax","2")); tIntervals = new VariableInput("Intervals", getParameter("Intervals","200")); tIntervals.setInputStyle(VariableInput.INTEGER); tIntervals.setMin(1); tIntervals.setMax(5000); tMin.setOnUserAction(mainController); tMax.setOnUserAction(mainController); tIntervals.setOnUserAction(mainController); graph.setTMin(tMin); graph.setTMax(tMax); graph.setIntervals(tIntervals); if (limitsPanel != null) { // componets will be added to limitsPanel in setUpLimitsPanel() mainController.add(tMin); // This is not done automatically, since they are in a limits panel mainController.add(tMax); mainController.add(tIntervals); } else { JCMPanel ap = new JCMPanel(9,0); ap.setBackground(getColorParam("PanelBackground", Color.lightGray)); ap.add(new Label(tMin.getName())); ap.add(tMin); ap.add(new Label()); ap.add(new Label(tMax.getName())); ap.add(tMax); ap.add(new Label()); ap.add(new Label(tIntervals.getName())); ap.add(tIntervals); ap.add(new Label()); mainPanel.add(ap,BorderLayout.EAST); } } else { try { graph.setTMin( new Constant((new Double(getParameter("ParameterMin","-2"))).doubleValue()) ); graph.setTMax( new Constant((new Double(getParameter("ParameterMax","2"))).doubleValue()) ); graph.setIntervals( new Constant((new Double(getParameter("Intervals","25"))).doubleValue()) ); } catch (NumberFormatException e) { } } // If the applet is configured to have a tracer button, create it and add the crosshair to the canvas if (! "no".equalsIgnoreCase( getParameter("UseTracer","yes") ) ) { tracer = new Animator(); tracer.setMin(graph.getTMin()); tracer.setMax(graph.getTMax()); tracer.setUndefinedWhenNotRunning(true); tracer.setStartButtonName("Trace Curve!"); double[] d = getNumericParam("TracerIntervals"); int ints; if (d == null || d.length != 1) ints = 100; else ints = (int)Math.round(d[0]); if (ints <= 0) tracer.setIntervals(graph.getIntervals()); else tracer.setIntervals(ints); Variable v = tracer.getValueAsVariable(); crosshair = new Crosshair( new ValueMath(xFunc,v), new ValueMath(yFunc,v) ); crosshair.setLineWidth(3); crosshair.setColor(getColorParam("CrosshairColor",Color.gray)); canvas.add(crosshair); if (inputPanel != null) { inputPanel.add(tracer,BorderLayout.WEST); } else if (limitsPanel == null) { Panel p = new Panel(); p.add(tracer); mainPanel.add(p,BorderLayout.SOUTH); } // if inputPanel is null but limitsPanel is not null, the tracer will be // added to the limit control panel in setUpLimitsPanel() } canvas.add(graph); // Finally, add the graph to the canvas. } // end setUpCanvas() protected void setUpLimitsPanel() { super.setUpLimitsPanel(); if (limitsPanel != null && tMin != null) { // add parameter inputs to limits panel limitsPanel.addComponentPair(tMin,tMax); limitsPanel.addComponent(tIntervals); } if (inputPanel == null && tracer != null && limitsPanel != null) { limitsPanel.addComponent(tracer); } } protected void setUpBottomPanel() { // override this to make a panel containing inputs for two functions if ( ! "no".equalsIgnoreCase(getParameter("UseFunctionInput", "yes")) ) { inputPanel = new JCMPanel(); inputPanel.setBackground( getColorParam("PanelBackground", Color.lightGray) ); Panel in = new JCMPanel(2,1); inputPanel.add(in,BorderLayout.CENTER); if ( ! "no".equalsIgnoreCase(getParameter("UseComputeButton", "yes")) ) { String cname = getParameter("ComputeButtonName", "New Functions"); computeButton = new Button(cname); inputPanel.add(computeButton, BorderLayout.EAST); computeButton.addActionListener(this); } String varName = getParameter("Variable"); String def = getParameter("Function"); if (def == null) def = "cos(" + varName + ") + cos(3*" + varName + ")"; functionInput = new ExpressionInput(def,parser); String label = getParameter("FunctionLabel"); if ("none".equalsIgnoreCase(label)) in.add(functionInput); else { Panel p = new JCMPanel(); p.add(functionInput,BorderLayout.CENTER); p.add( new Label(label), BorderLayout.WEST ); in.add(p); } def = getParameter("Function2"); if (def == null) def = "sin(4*" + varName + ") - sin(2*" + varName + ")"; functionInput2 = new ExpressionInput(def,parser); label = getParameter("FunctionLabel2"); if ("none".equalsIgnoreCase(label)) in.add(functionInput2); else { Panel p = new JCMPanel(); p.add(functionInput2,BorderLayout.CENTER); p.add( new Label(label), BorderLayout.WEST ); in.add(p); } mainPanel.add(inputPanel, BorderLayout.SOUTH); functionInput.setOnUserAction(mainController); functionInput2.setOnUserAction(mainController); } } protected void setUpMainPanel() { // Override to set up controller for tracer, if there is one super.setUpMainPanel(); // Do the common setup if ( tracer == null ) { return; // If the applet is not configured to use a trace button, there is nothing to do. } Controller traceController = new Controller(); // A controler that will only recompute the crosshair position traceController.add(tracer); traceController.add(crosshair); tracer.setOnChange(traceController); } // end setUpMainPanel() protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For the Parametric applet, the example string should contain // two expression that defines the curve to be graphed, separated // by a semicolon. This can optionally // be followed by another semicolon and a list of four to seven numbers. // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The // next three numbers specify the minimum value for the parameter, the // maximum value, and the number of intervals into which the range of // parameter values is divided. if (tracer != null) tracer.stop(); int pos = example.indexOf(";"); if (pos == -1) return; // illegal example -- must have two functions String example2 = example.substring(pos+1); example = example.substring(0,pos); pos = example2.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // Get limits from example2 text. String nums = example2.substring(pos+1); example2 = example2.substring(0,pos); StringTokenizer toks = new StringTokenizer(nums, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } } if (toks.hasMoreTokens()) { try { double d = (new Double(toks.nextToken())).doubleValue(); if (tMin == null) { graph.setTMin(new Constant(d)); if (tracer != null) tracer.setMin(d); } else tMin.setVal(d); } catch (NumberFormatException e) { } } if (toks.hasMoreTokens()) { try { double d = (new Double(toks.nextToken())).doubleValue(); if (tMax == null) { graph.setTMax(new Constant(d)); if (tracer != null) tracer.setMax(d); } else tMax.setVal(d); } catch (NumberFormatException e) { } } if (toks.hasMoreTokens()) { try { int d = (int)Math.round((new Double(toks.nextToken())).doubleValue()); if (tIntervals == null) { if (tracer != null && tracer.getIntervals() == graph.getIntervals()) tracer.setIntervals(d); graph.setIntervals(new Constant(d)); } else tIntervals.setVal(d); } catch (NumberFormatException e) { } } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); functionInput2.setText(example2); } else { // If there is no user input, set the function in the graph directly. try { Function f = new SimpleFunction( parser.parse(example), xVar ); ((WrapperFunction)xFunc).setFunction(f); Function g = new SimpleFunction( parser.parse(example2), xVar ); ((WrapperFunction)yFunc).setFunction(g); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); } // end doLoadExample() public void stop() { // stop animator when applet is stopped if (tracer != null) tracer.stop(); super.stop(); } } // end class Parametric jcm1-source/MultiGraph.java0000644000076500011320000004536411741343635015142 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ import java.awt.*; import java.applet.Applet; import java.util.*; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; // The MultiApplet can display the graphs of several functions, in different colors. // By default, there is only one function, but you can configure the applet to // use more than one function with applet params. // The definitions of these functions can, optionally, use parameters whose // values are controled by sliders at the bottom of the applet. public class MultiGraph extends GenericGraphApplet { private Vector sliders; // Elements of this vector are the VariableSlider // objects that represent the parameter values. // The sliders are created in the setUpParser() method. private ExprIn[] inputs; // The function input boxes (or null if inputs aren't used) private Graph1D[] graphs; // The graphs of the functions, in the case function input boxes are NOT used private int functionCt; // Number of functions -- size of inputs or graphs array private Color[] graphColors = { Color.magenta, new Color(0,180,0), Color.red, new Color(0,200,200), Color.orange, Color.gray, Color.blue, Color.pink }; private static class ColorPatch extends Canvas { // a canvas with a preferred size ColorPatch(Color c) { setBackground(c); } public Dimension getPreferredSize() { return new Dimension(25,10); } public void paint(Graphics g) { g.drawRect(0,0,getSize().width-1,getSize().height-1); } } private static class ExprIn extends ExpressionInput { // Doesn't throw an error if empty, just sets function in graph to null Graph1D graph; // Graph associated with this function input. Function func; // The function of x defined by this graph. ExprIn(String definition, Parser p, Graph1D g, Variable v) { super(definition,p); graph = g; func = getFunction(v); if (definition.trim().length() > 0) graph.setFunction(func); } public void checkInput() { // (will be called during constructor -- hence the funny bit with checking if graphe is null) boolean hasChanged = previousContents == null || !previousContents.equals(getText()); if (!hasChanged) return; String text = getText().trim(); if (text.length() == 0) { // set graph's function to null so it doesn't have to do any computations. if (graph != null) graph.setFunction(null); previousContents = getText(); } else { super.checkInput(); if (graph != null) graph.setFunction(func); } } } protected void setUpParser() { // Override this to add VariableSliders to parser. // Get the data for any sliders from applet params named "Parameter", "Parameter1", ... // The sliders are created and the variables are added to the parser by the // addParameter() method, which is defined below. sliders = new Vector(); int ct = 0; String param = getParameter("Parameter"); if (param == null) { ct++; param = getParameter("Parameter" + ct); } while (true) { if (param == null) break; addParameter(param); ct++; param = getParameter("Parameter" + ct); } super.setUpParser(); // Call this last so function definitions // in applet params can use the parameter names } // end setUpParser() private void addParameter(String data) { // Create a VariableSlider from the information in name and add it to the // Vector of sliders. The data must contain the name of the variable // associated with the slider. The name can be followed by a ";" and up to // three numbers. (If there is no ";", a space after the name will do.) // The numbers can be separated by commas, spaces, or tabs. The first // number gives the minimum value on the slider, the second gives the maximum, // and the third gives the initial value of the slider variable. double min = -5, max = 5, val = 0; // min, max, and value for slider data = data.trim(); int pos = data.indexOf(';'); if (pos < 0) pos = data.indexOf(' '); String name; // The name of the parameter if (pos < 0) { // If there is no space or ";", the data is just the name of the variable. name = data; } else { // Get the name from the front of the data, then look for min, max, and val. String nums = data.substring(pos+1); name = data.substring(0,pos).trim(); StringTokenizer toks = new StringTokenizer(nums," ,\t"); try { if (toks.hasMoreElements()) min = (new Double(toks.nextToken())).doubleValue(); if (toks.hasMoreElements()) max = (new Double(toks.nextToken())).doubleValue(); if (toks.hasMoreElements()) val = (new Double(toks.nextToken())).doubleValue(); } catch (NumberFormatException e) { min = -5; max = 5; val = 0; } } // Create the slider, adding the associated variable to the parser, and set its value. VariableSlider slide = new VariableSlider(name, new Constant(min), new Constant(max), parser); slide.setVal(val); sliders.addElement(slide); // Save the slider in the array of sliders for later use. } // end setUpParser(); private void getColors() { // get graph colors from color parameters, if any. Vector vec = new Vector(); int ct = 0; Color c = getColorParam("GraphColor"); if (c == null) { ct++; c = getColorParam("GraphColor" + ct); } while (true) { if (c == null) break; vec.addElement(c); ct++; c = getColorParam("GraphColor" + ct); } if (vec.size() > 0) { graphColors = new Color[vec.size()]; for (int i = 0; i < vec.size(); i++) graphColors[i] = (Color)vec.elementAt(i); } } private Vector getFunctions() { // Read applet parms "Function", "Funcion1", ... // Return a vector containing the function definition strings Vector functions = new Vector(); int ct = 0; String c = getParameter("Function"); if (c == null) { ct++; c = getParameter("Function" + ct); } while (true) { if (c == null) break; functions.addElement(c); ct++; c = getParameter("Function" + ct); } if (functions.size() == 0) functions.addElement( " abs( " + xVar.getName() + ") ^ " + xVar.getName() ); double[] d = getNumericParam("FunctionCount"); if (d == null || d.length == 0 || d[0] <= 0.5) functionCt = functions.size(); else { functionCt = (int)Math.round(d[0]); if (functionCt < functions.size()) { // use number of functions specified as functionCt functionCt = functions.size(); } else { // make extra empty functions to bring total up to functionCt int extra = functionCt - functions.size(); for (int i = 0; i < extra; i++) functions.addElement(""); } } return functions; } private Panel makeFunctionInput(Vector functions, int funcNum) { // make input box for specified function // also adds the input box to the inputs[] array Graph1D graph = new Graph1D(); graph.setColor(graphColors[funcNum % graphColors.length]); ExprIn in = new ExprIn((String)functions.elementAt(funcNum),parser,graph,xVar); in.setOnUserAction(mainController); JCMPanel p = new JCMPanel(); p.add(in,BorderLayout.CENTER); String name; if (functions.size() > 1) name = " " + getParameter("FunctionName","f") + (funcNum+1) + "(" + xVar.getName() + ") = "; else name = " " + getParameter("FunctionName","f") + "(" + xVar.getName() + ") = "; p.add(new Label(name), BorderLayout.WEST); if (graphColors.length > 1 && functions.size() > 1) p.add(new ColorPatch( graphColors[funcNum % graphColors.length] ), BorderLayout.EAST); inputs[funcNum] = in; return p; } protected void setUpBottomPanel() { // Overridden to create an appropriate input panel // Create a panel holding all the function inputs and // sliders, with a display label for each slider to show its value. boolean funcInput = "yes".equalsIgnoreCase(getParameter("UseFunctionInput","yes")); if ( funcInput && "yes".equalsIgnoreCase(getParameter("UseComputeButton", "yes")) ) { // make the compute button String cname = getParameter("ComputeButtonName", "New Functions"); computeButton = new Button(cname); computeButton.addActionListener(this); } Panel firstPanel = null; // To help find a place for the compute button getColors(); Vector functions = getFunctions(); if (!funcInput && sliders.size() == 0) // nothing to put in the input panel return; JCMPanel panel = new JCMPanel(); if (! "no".equalsIgnoreCase(getParameter("TwoInputColumns","no"))) panel.setLayout(new GridLayout(0,2,12,3)); else panel.setLayout(new GridLayout(0,1,3,3)); panel.setBackground(getColorParam("PanelBackground", Color.lightGray)); if (funcInput) { // make an input box for each function and add it to the panel inputs = new ExprIn[functions.size()]; for (int i = 0; i < functions.size(); i++) { Panel p = makeFunctionInput(functions,i); if (firstPanel == null) firstPanel = p; panel.add(p); } } else { // just make graphs from the function definition strings. graphs = new Graph1D[functions.size()]; for (int i = 0; i < functions.size(); i++) { graphs[i] = new Graph1D(); graphs[i].setColor(graphColors[ i % graphColors.length ]); String def = ((String)functions.elementAt(i)).trim(); if (def.length() > 0) { // if the definition string is empty, leave graph's function undefined Function f = new SimpleFunction( parser.parse(def), xVar ); graphs[i].setFunction(f); } } } for (int i = 0; i < sliders.size(); i++) { // add sliders to the input panel JCMPanel p = new JCMPanel(); VariableSlider slide = (VariableSlider)sliders.elementAt(i); p.add(slide, BorderLayout.CENTER); p.add(new DisplayLabel(" " + slide.getName() + " = # ", new Value[] { slide.getVariable() } ), BorderLayout.EAST); panel.add(p); slide.setOnUserAction(mainController); } if (computeButton != null) { // find a place for the compute button! if (functions.size() == 1) firstPanel.add(computeButton, BorderLayout.EAST); else if (limitsPanel == null) { Panel p = new Panel(); p.add(computeButton); panel.add(p); } // otherwise, add it at the end of setUpLimitPanel(); } mainPanel.add(panel, BorderLayout.SOUTH); } // end setUpBottomPanel() protected void setUpLimitsPanel() { // add compute button if it hasn't been put somewhere else super.setUpLimitsPanel(); if (limitsPanel != null && computeButton != null && functionCt != 1) limitsPanel.addComponent(computeButton); } protected void setUpCanvas() { // Overridden to add the graph to the canvas. super.setUpCanvas(); // Do the default setup. // set up bottom panel has already been defined // add the graphs to the canvas if (graphs != null) { for (int i = 0; i < graphs.length; i++) canvas.add(graphs[i]); } else { for (int i = 0; i < inputs.length; i++) canvas.add(inputs[i].graph); } } // end setUpCanvas protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For the FamiliesOfGraphs applet, the example string should contain // an expression that defines the function to be graphed. This must // be followed by a semicolon and list of zero or more numbers. // Then there is another semicolon and one or more function definitions, // separated by semicolons. You can have as many function // definitions as you have functions in your applet setup. // (Note that having the numbers before the // functions is different from the format of examples in all the // other configurable applets. This is to allow more than one function.) Note that even if you leave // out the numbers, you still need two semicolons. The list of numbers has the following meaning: // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The // remaining numbers occur in groups of three. Each group give the maximum, minimum, and value of a parameters that was defined // with the "Parameter", "Parameter1", ... applet params. int pos = example.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // Get limits from example text. String nums = example.substring(0,pos); example = example.substring(pos+1); StringTokenizer toks = new StringTokenizer(nums, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } } int i = 0; while (i < sliders.size() && toks.hasMoreElements()) { // Look for a value for the i-th slider. try { double min = (new Double(toks.nextToken())).doubleValue(); double max = (new Double(toks.nextToken())).doubleValue(); double d = (new Double(toks.nextToken())).doubleValue(); VariableSlider slider = ((VariableSlider)sliders.elementAt(i)); slider.setMin(new Constant(min)); slider.setMax(new Constant(max)); slider.setVal(d); } catch (Exception e) { } i++; } } // Set up the example data and recompute everything. StringTokenizer toks = new StringTokenizer(example,";"); int funcNum = 0; while (funcNum < functionCt) { if (toks.hasMoreElements()) { // define the function using definition from example text String def = toks.nextToken(); if (graphs != null) { try { graphs[funcNum].setFunction(new SimpleFunction( parser.parse(def), xVar )); } catch (ParseError e) { graphs[funcNum].setFunction(null); } } else inputs[funcNum].setText(def); } else { // function is undefined if (graphs != null) graphs[funcNum].setFunction(null); else inputs[funcNum].setText(""); } funcNum++; } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); mainController.compute(); } // end doLoadExample() } // end class MultiGraph jcm1-source/Derivatives.java0000644000076500011320000004323111741343635015342 0ustar djefaculty/************************************************************************** * Copyright (c) 2001, 2005 David J. Eck * * * * Permission is hereby granted, free of charge, to any person obtaining * * a copy of this software and associated documentation files (the * * "Software"), to deal in the Software without restriction, including * * without limitation the rights to use, copy, modify, merge, publish, * * distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to * * the following conditions: * * * * The above copyright notice and this permission notice shall be included * * in all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * * * ---- * * (Released under new license, April 2012.) * * * * David J. Eck * * Department of Mathematics and Computer Science * * Hobart and William Smith Colleges * * 300 Pulteney Street * * Geneva, NY 14456 * * eck@hws.edu * * http://math.hws.edu/eck * **************************************************************************/ // Draws the graph of a function and its first derivative (and optionally // its second derivative). It shows the tangent line to the graph and // marks the corresponding point on the graph of the derivative. The // user controls the position of the tangent line with a slider and/or // a number-input box. A formula for the derivative can be displayed // at the bototm of the applet. import java.awt.*; import java.awt.event.*; import java.applet.Applet; import java.util.StringTokenizer; import edu.hws.jcm.draw.*; import edu.hws.jcm.data.*; import edu.hws.jcm.functions.*; import edu.hws.jcm.awt.*; public class Derivatives extends GenericGraphApplet { private String functionName; // name of the fuction beging graphed, 'f' by default; used in labels etc private Function func; // The function that is graphed. private Function deriv; // derivative of func private Expression derivExpression; // The Expression that defines the derivative private Function deriv2; // if non-null, second derivative of func private Controller subController = new Controller(); // Respond to changes in x-coord input; won't redraw graph private VariableInput xInput; // x-coord of point of tangency private class ExprLbl extends Label implements Computable { // A class for displaying the formula for deriv String label; ExprLbl(String label) { this.label = label; compute(); } public void compute() { setText(label + derivExpression.toString()); } } protected void setUpParameterDefaults() { // I don't want to use abs(x)^x as the default function, since it's derivative is so funny parameterDefaults = new java.util.Hashtable(); parameterDefaults.put("Function", " tan(" + getParameter("Variable","x") + ")"); } protected void setUpMainPanel() { // add a bunch of extra components at the end super.setUpMainPanel(); // now that limitsPanel has been set up, add the two extra coordinate rects to it if (limitsPanel != null) { limitsPanel.addCoords(canvas.getCoordinateRect(1)); if (deriv2 != null) limitsPanel.addCoords(canvas.getCoordinateRect(2)); } else { // CoordinateRects must synchronize with each other Tie coordTie = new Tie(canvas.getCoordinateRect(0),canvas.getCoordinateRect(1)); if (deriv2 != null) coordTie.add(canvas.getCoordinateRect(2)); canvas.getCoordinateRect(0).setSyncWith(coordTie); canvas.getCoordinateRect(1).setSyncWith(coordTie); if (deriv2 != null) canvas.getCoordinateRect(2).setSyncWith(coordTie); } // Add controls at the bottom of the panel for setting the value of x. // Also add the derivative formula, if it's supposed to be displayed Value xMin = canvas.getCoordinateRect().getValueObject(CoordinateRect.XMIN); Value xMax = canvas.getCoordinateRect().getValueObject(CoordinateRect.XMAX); canvas.getCoordinateRect().setOnChange(subController); VariableSlider xSlider = new VariableSlider(xMin,xMax); xSlider.setOnUserAction(subController); xInput.setOnTextChange(subController); subController.add(xSlider); subController.add(xInput); subController.add( new Tie(xSlider,xInput) ); Panel p = new Panel(); p.setLayout(new BorderLayout(5,5)); p.add(xInput.withLabel(), BorderLayout.WEST); p.add(xSlider, BorderLayout.CENTER); // If there is no limits panel, make it possible to add a RestoreLimits button to the input panel if (limitsPanel == null && ! "no".equalsIgnoreCase(getParameter("UseRestoreButton","no"))) { // add button to left of slider Button res = new Button("Restore Limits"); p.add(res, BorderLayout.EAST); res.setBackground(Color.lightGray); res.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { canvas.getCoordinateRect(0).restore(); canvas.getCoordinateRect(1).restore(); if (deriv2 != null) canvas.getCoordinateRect(2).restore(); } }); } if ("yes".equalsIgnoreCase(getParameter("ShowFormula", "yes"))) { // add derivative formula Panel s = new Panel(); s.setLayout(new GridLayout(2,1,3,3)); s.add(p); ExprLbl lbl = new ExprLbl(" " + functionName + "'(" + xVar.getName() + ") = "); mainController.add(lbl); s.add(lbl); p = s; } if (inputPanel == null) { // Add the control panel directly to the main panel p.setBackground(getColorParam("PanelBackground",Color.lightGray)); mainPanel.add(p,BorderLayout.SOUTH); } else { // Add control panel to bottom of input panel. inputPanel.add(p,BorderLayout.SOUTH); } } // end setUpMainPanel protected void setUpCanvas() { // Override this to add more stuff to the canvas. // I don't call super.setUpCanvas(), since // the canvas in this case is quite a bit different // from the standard one. boolean showSecond = ! "no".equalsIgnoreCase(getParameter("SecondDerivative","no")); xInput = new VariableInput(xVar.getName(), getParameter("X","1")); if (functionInput != null) { func = functionInput.getFunction(xVar); derivExpression = functionInput.getExpression().derivative(xVar); } else { String def = getParameter("Function"); Expression exp = parser.parse(def); Function f = new SimpleFunction( exp, xVar ); derivExpression = exp.derivative(xVar); func = new WrapperFunction(f); } Graph1D graph = new Graph1D(func); Color color = getColorParam("GraphColor",Color.black); graph.setColor(color); deriv = func.derivative(1); Graph1D derivGraph = new Graph1D(deriv); derivGraph.setColor(color); Graph1D deriv2Graph = null; if (showSecond) { deriv2 = deriv.derivative(1); deriv2Graph = new Graph1D(deriv2); deriv2Graph.setColor(color); } // Set up 2 or 3 coordinate retcs if (showSecond) { canvas.addNewCoordinateRect(0, 1.0/3.0, 0, 1); canvas.addNewCoordinateRect(1.0/3.0, 2.0/3.0, 0, 1); canvas.addNewCoordinateRect(2.0/3.0, 1, 0, 1); } else { canvas.addNewCoordinateRect(0, 0.5, 0, 1); canvas.addNewCoordinateRect(0.5, 1, 0, 1); } // do the type of stuff that's usually done in super.setUpCanvas color = getColorParam("CanvasColor"); if (color != null) canvas.setBackground(color); if (! "no".equalsIgnoreCase(getParameter("UsePanner", "no")) ) { canvas.add(new Panner(),0); canvas.add(new Panner(),1); if (showSecond) canvas.add(new Panner(),2); } if ( ! "no".equalsIgnoreCase(getParameter("UseGrid", "no")) ) { Grid g = new Grid(); color = getColorParam("GridColor"); if (color != null) g.setColor(color); canvas.add(g,0); g = new Grid(); color = getColorParam("GridColor"); if (color != null) g.setColor(color); canvas.add(g,1); if (showSecond) { g = new Grid(); color = getColorParam("GridColor"); if (color != null) g.setColor(color); canvas.add(g,2); } } canvas.add(makeAxes(),0); canvas.add(makeAxes(),1); if (showSecond) canvas.add(makeAxes(),2); if ( ! "no".equalsIgnoreCase(getParameter("UseMouseZoom", "no")) ) canvas.setHandleMouseZooms(true); if ( "yes".equalsIgnoreCase(getParameter("UseOffscreenCanvas", "yes")) ) canvas.setUseOffscreenCanvas(true); mainController.setErrorReporter(canvas); mainPanel.add(canvas, BorderLayout.CENTER); // add graphs, tangent lines etc. canvas.add(graph,0); canvas.add(derivGraph,1); if (showSecond) canvas.add(deriv2Graph,2); Color tangentColor = getColorParam("TangentColor", Color.red); Color tangentColor2 = getColorParam("TangentColor2", new Color(0, 180, 0)); mainController.remove(canvas); mainController.add(graph); mainController.add(derivGraph); if (showSecond) mainController.add(deriv2Graph); subController = new Controller(); mainController.add(subController); TangentLine tan = new TangentLine(xInput, func); Crosshair cross = new Crosshair(xInput,deriv); tan.setColor(tangentColor); cross.setColor(tangentColor); canvas.add(tan, 0); canvas.add(cross, 1); subController.add(tan); subController.add(cross); if (showSecond) { tan = new TangentLine(xInput, deriv); cross = new Crosshair(xInput, deriv2); tan.setColor(tangentColor2); cross.setColor(tangentColor2); canvas.add(tan, 1); canvas.add(cross, 2); subController.add(tan); subController.add(cross); } functionName = getParameter("FunctionName", "f"); String yName = getParameter("YName","y"); Color textColor = getColorParam("TextColor",Color.black); Color bgColor = getColorParam("TextBackground",Color.white); DrawString str; if ("yes".equalsIgnoreCase(getParameter("ShowGraphLabels","yes"))) { str = new DrawString(yName + " = " + functionName + "(" + xVar.getName() + ")"); str.setColor(textColor); str.setBackgroundColor(bgColor); str.setFrameWidth(1); canvas.add(str,0); str = new DrawString(yName + " = " + functionName + " ' (" + xVar.getName() + ")"); str.setColor(textColor); str.setBackgroundColor(bgColor); str.setFrameWidth(1); canvas.add(str,1); if (showSecond) { str = new DrawString(yName + " = " + functionName + " ' ' (" + xVar.getName() + ")"); str.setColor(textColor); str.setBackgroundColor(bgColor); str.setFrameWidth(1); canvas.add(str,2); } } if ("yes".equalsIgnoreCase(getParameter("ShowValues","yes"))) { str = new DrawString(functionName + "(#) = #", DrawString.BOTTOM_LEFT, new Value[] { xInput, new ValueMath(func,xInput) }); str.setColor(textColor); str.setBackgroundColor(bgColor); str.setFrameWidth(1); str.setNumSize(7); canvas.add(str,0); subController.add(str); str = new DrawString(functionName + " ' (#) = #", DrawString.BOTTOM_LEFT, new Value[] { xInput, new ValueMath(deriv,xInput) }); str.setColor(textColor); str.setBackgroundColor(bgColor); str.setFrameWidth(1); str.setNumSize(7); canvas.add(str,1); subController.add(str); if (showSecond) { str = new DrawString(functionName + " ' ' (#) = #", DrawString.BOTTOM_LEFT, new Value[] { xInput, new ValueMath(deriv2,xInput) }); str.setColor(textColor); str.setBackgroundColor(bgColor); str.setFrameWidth(1); str.setNumSize(7); canvas.add(str,2); subController.add(str); } } } // end setUpCanvas() protected void addCanvasBorder() { // override to add the border to each coordinate rect, and make default width equal to 1 int borderWidth; double[] bw = getNumericParam("BorderWidth"); if (bw == null || bw.length == 0 || bw[0] > 25) borderWidth = 2; else borderWidth = (int)Math.round(bw[0]); if (borderWidth > 0) { canvas.add( new DrawBorder( getColorParam("BorderColor", Color.black), borderWidth ), 0 ); canvas.add( new DrawBorder( getColorParam("BorderColor", Color.black), borderWidth ), 1 ); if (deriv2 != null) canvas.add( new DrawBorder( getColorParam("BorderColor", Color.black), borderWidth ), 2 ); } } protected void doLoadExample(String example) { // This method is called when the user loads an example from the // example menu (if there is one). It overrides an empty method // in GenericGraphApplet. // For the SecantTangent applet, the example string should contain // an expression that defines the function to be graphed. This can optionally // be followed by a semicoloon and a list of four or five numbers. // The first four numbers give the x- and y-limits to be used for the // example. If they are not present, then -5,5,-5,5 is used. The // fifth number, if present, gives the x-coord where the tangent line // is drawn initially. int pos = example.indexOf(";"); double[] limits = { -5,5,-5,5 }; // x- and y-limits to use if (pos > 0) { // get limits from example text String limitsText = example.substring(pos+1); example = example.substring(0,pos); StringTokenizer toks = new StringTokenizer(limitsText, " ,"); if (toks.countTokens() >= 4) { for (int i = 0; i < 4; i++) { try { Double d = new Double(toks.nextToken()); limits[i] = d.doubleValue(); } catch (NumberFormatException e) { } } if (toks.countTokens() > 0) { // Get point for tangent line try { Double d = new Double(toks.nextToken()); xInput.setVal( d.doubleValue() ); } catch (NumberFormatException e) { } } } } // Set up the example data and recompute everything. if (functionInput != null) { // If there is a function input box, put the example text in it. functionInput.setText(example); } else { // If there is no user input, set the function in the graph directly. // Also, in this case, func is a "WrapperFunction". Set the // definition of that WrapperFunction to be the same as f try { Expression exp = parser.parse(example); derivExpression = exp.derivative(xVar); Function f = new SimpleFunction( exp, xVar ); ((WrapperFunction)func).setFunction(f); } catch (ParseError e) { // There should't be parse error's in the Web-page // author's examples! If there are, the function // just won't change. } } CoordinateRect coords = canvas.getCoordinateRect(0); coords.setLimits(limits); coords.setRestoreBuffer(); canvas.getCoordinateRect(1).setRestoreBuffer(); if (deriv2 != null) canvas.getCoordinateRect(0).setRestoreBuffer(); mainController.compute(); } // end doLoadExample() } // end class SimpleGraph