Esercitazione 8 – scientific calculator Progetto: Una calcolatrice scientifica Utilizzando il Parser reperibile all’indirizzo https://github.com/uklimaschewski/EvalEx creare un’applicazione Java con interfaccia grafica che permetta di emulare la calcolatrice di Windows in modalità standard e in modalità scientifica. La calcolatrice avrà anche la possibilità di tracciare il grafico di una funzione f(x) di cui si conosce l’espressione f e l’intervallo di valori a<=x<=b. La calcolatrice farà l’autoscaling sia sull’asse delle ascisse che sull’asse delle ordinate. Inoltre passando il mouse sopra il grafico della funzione verranno visualizzati i valori di x e di f(x). Nel progetto software l’interfaccia grafica dovrà richiamare i metodi di una classe CalcEngine che gestisce la macchina a stati e il parser. In altri termini l’applicazione deve essere realizzata in modo da poter disaccoppiare l’interfaccia utente dal CalcEngine. In seguito la classe CalcEngine sarà utilizzata in un’applicazione Android. Ad esempio, supponendo di avere i bottoni: private javax.swing.JButton C; private javax.swing.JButton CE; private javax.swing.JButton MC; private javax.swing.JButton MR; private javax.swing.JButton MS; private javax.swing.JButton Mminus; private javax.swing.JButton Mplus; private javax.swing.JButton back; private javax.swing.JButton one; private javax.swing.JButton percentage; private javax.swing.JButton plus; private javax.swing.JButton plusMinus; private javax.swing.JButton reciprocal; private javax.swing.JButton seven; private javax.swing.JButton six; private javax.swing.JButton sqrt; //..e altri.. I gestori degli eventi di ciascun bottone dell’interfaccia grafica richiameranno i metodi di un oggetto “calc” di tipo CalcEngine, il quale a sua volta utilizzerà un oggetto di classe Expression… //A titolo esemplificativo…. private void zeroActionPerformed(java.awt.event.ActionEvent evt) { setNumber(evt); redisplay(); } private void oneActionPerformed(java.awt.event.ActionEvent evt) { setNumber(evt); redisplay(); } private void CEActionPerformed(java.awt.event.ActionEvent evt) { calc.clearCurrentDisplayNumber(); redisplay(); } private void MCActionPerformed(java.awt.event.ActionEvent evt) { calc.memoryClearValue(); redisplay(); } private void MSActionPerformed(java.awt.event.ActionEvent evt) { calc.memoryStoreValue(calc.getDisplayValue()); redisplay(); } private void MRActionPerformed(java.awt.event.ActionEvent evt) { calc.memoryRecallValue(); redisplay(); } private void MplusActionPerformed(java.awt.event.ActionEvent evt) { calc.addToMemory(calc.getDisplayValue()); } Le funzioni di base della calcolatrice di Windows What the Buttons Do! Backspace --> Removes the last digit of the displayed number. CE --> Clears the number displayed at that time. C --> Clears the entire calculation. MC --> Clears the numbers in the memory. MR --> Recalls a number from the memory. MS --> Stores numbers in the memory. M+ --> Adds the displayed number to the memory. M- --> Subtracts the displayed number from the memory. Sqrt --> This calculates the square root of the number on the screen. 1/x --> This calculates the reciprocal of the displayed number. Per il calcolo percentuale: osservare che la percentuale è rispetto all’espressione già inserita. Ad esempio: 50 + 5% 50 + 2,5 50 *5% 50 * 2,5 Per il calcolo della radice si può usare l’elevamento a potenza usando come esponente (0.5). Il grafico di funzione Ci sono diversi metodi per effettuare il grafico di una funzione di una variabile. Metodo diretto Si utilizzano le classi Polygon, Graphics, etc. di Java come nel seguente esempio: http://stackoverflow.com/questions/19914476/plot-the-sine-and-cosine-functions import javax.swing.*; import java.awt.*; public class Exercise extends JFrame { public Exercise() { setLayout(new BorderLayout()); add(new DrawSine(), BorderLayout.CENTER); } public static void main(String[] args) { Exercise frame = new Exercise(); frame.setSize(400, 300); frame.setTitle("Exercise"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setVisible(true); } class DrawSine extends JPanel { double f(double x) { return Math.sin(x); } double gCos(double y) { return Math.cos(y); } protected void paintComponent(Graphics g) { super.paintComponent(g); g.drawLine(10, 100, 380, 100); g.drawLine(200, 30, 200, 190); g.drawLine(380, g.drawLine(380, g.drawLine(200, g.drawLine(200, 100, 370, 90); 100, 370, 110); 30, 190, 40); 30, 210, 40); g.drawString("X", 360, 80); g.drawString("Y", 220, 40); Polygon p = new Polygon(); Polygon p2 = new Polygon(); for (int x = -170; x <= 170; x++) { p.addPoint(x + 200, 100 - (int) (50 * f((x / 100.0) * 2 * Math.PI))); } for (int x = -170; x <= 170; x++) { p2.addPoint(x + 200, 100 - (int) (50 * gCos((x / 100.0) * 2 * Math.PI))); } g.setColor(Color.red); g.drawPolyline(p.xpoints, p.ypoints, p.npoints); g.drawString("-2\u03c0", 95, 115); g.drawString("-\u03c0", 147, 115); g.drawString("\u03c0", 253, 115); g.drawString("2\u03c0", 305, 115); g.drawString("0", 200, 115); g.setColor(Color.blue); g.drawPolyline(p2.xpoints, p2.ypoints, p2.npoints); } } } Utilizzo della classe Draw di Sedgewick e Wayne Usando le classi e interfacce: http://introcs.cs.princeton.edu/java/stdlib/Draw.java http://introcs.cs.princeton.edu/java/stdlib/DrawListener.java Si tratta di classi presenti nella libreria standard del corso universitario di introduzione alla programmazione dell’università di Princeton: http://introcs.cs.princeton.edu/java/stdlib/ http://introcs.cs.princeton.edu/java/15inout/ La libreria in formato jar può essere scaricata direttamente da qui: http://introcs.cs.princeton.edu/java/stdlib/stdlib.jar Tuttavia per quello che bisogna fare in questo progetto basta scaricare il codice della classe Draw.java e dell’interfaccia DrawListener.java e fare qualche lieve adattamento/modifica: Interfaccia DrawListener.java Aggiungere il metodo: /** * * @param x the x-coordinate of the mouse * @param y the y-coordinate of the mouse */ void mouseMoved(double x, double y); Classe Draw.java Aggiungere il metodo public void plot(double[] x, double[] y) { int N = x.length; GeneralPath path = new GeneralPath(); path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0])); for (int i = 0; i < N - 1; i++) { path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i])); } offscreen.draw(path); draw(); } Ridefinire il metodo mouseMoved come di seguito riportato a titolo esemplificativo: /** * This method cannot be called directly. */ @Override public void mouseMoved(MouseEvent e) { synchronized (mouseLock) { mouseX = userX(e.getX()); mouseY = userY(e.getY()); for (DrawListener listener : listeners) { listener.mouseMoved(userX(e.getX()), userY(e.getY())); } } } Aggiungere la nested static class (dentro la classe Draw): private static class DrawListenerImpl implements DrawListener { private final Draw draw; private final double[] x; private final double[] y; public DrawListenerImpl(Draw draw, double[] x, double[] y) { this.draw = draw; this.x = x; this.y = y; } @Override public void mousePressed(double x, double y) { System.out.println("mouse pressed x= " + x + " y= " + y); draw.clear(); double deltaX = draw.xmax - draw.xmin; double deltaY = draw.ymax - draw.ymin; draw.setXscale(x - deltaX / 4, x + deltaX / 4); draw.setYscale(y - deltaY / 4, y + deltaY / 4); draw.plot(this.x, this.y); } @Override public void mouseDragged(double x, double y) { System.out.println("mouse dragged x= " + x + " y= " + y); } @Override public void mouseReleased(double x, double y) { } @Override public void keyTyped(char c) { } @Override public void keyPressed(int keycode) { } @Override public void keyReleased(int keycode) { } @Override public void mouseMoved(double x, double y) { System.out.println("mouse moved x= " + x + " y= " + y); } } Riscrivere il main come nel seguente esempio per testare le funzionalità della classe Draw: public static void main(String[] args) { // number of line segments to plot int N = Integer.parseInt("20000");//args[0] // the function y = sin(4x) + sin(20x), sampled at N points // between x = 0 and x = pi double[] x2 = new double[N + 1]; double[] y2 = new double[N + 1]; for (int i = 0; i <= N; i++) { x2[i] = Math.PI * i / N; y2[i] = Math.sin(4 * x2[i]) + Math.sin(2 * x2[i]); } // rescale the coordinate system Draw draw3 = new Draw("Test client 3"); draw3.setXscale(0, Math.PI); draw3.setYscale(-2.0, +2.0); draw3.plot(x2, y2); /* // plot the approximation to the function for (int i = 0; i < N; i++) { draw.line(x[i], y[i], x[i+1], y[i+1]); } */ draw3.addListener(new DrawListenerImpl(draw3, x2, y2)); } Gestione dei tasti della tastiera Per associare i tasti della tastiera (inclusi i tasti del numpad e le combinazioni di tasti come SHIFT+altro tasto…) Links di riferimento http://stackoverflow.com/questions/14318191/shift-enter-enter-in-the-same-component-in-java Dont use KeyListener/KeyAdapter with Swing. Always use KeyBindings https://tips4java.wordpress.com/2008/10/10/key-bindings/ http://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html https://docs.oracle.com/javase/7/docs/api/javax/swing/KeyStroke.html http://stackoverflow.com/questions/16069502/how-to-define-the-keystroke-for-the-numpad-enter-key http://docs.oracle.com/javase/7/docs/api/java/awt/event/KeyEvent.html http://stackoverflow.com/questions/2419608/java-swing-keystrokes-how-to-make-ctrl-modifier-work https://docs.oracle.com/javase/7/docs/api/java/awt/KeyEventPostProcessor.html http://docs.oracle.com/javase/7/docs/api/java/awt/KeyboardFocusManager.html http://docs.oracle.com/javase/7/docs/api/java/awt/KeyEventDispatcher.html http://www.simplesoft.it/java/tips/implementare-un-keylistener-in-java-swing.html http://stackoverflow.com/questions/100123/application-wide-keyboard-shortcut-java-swing#100754 http://stackoverflow.com/questions/286727/java-keylistener-for-jframe-is-being-unresponsive Esempio applicativo import import import import import import java.awt.event.ActionEvent; java.awt.event.InputEvent; java.awt.event.KeyEvent; javax.swing.AbstractAction; javax.swing.JComponent; javax.swing.KeyStroke; /** * * @author Prof.G */ public class UserInterface extends javax.swing.JFrame { private static final long serialVersionUID = 1L; private final CalcEngine calc; public UserInterface(CalcEngine calc) { this.calc = calc; initComponents(); notificationText.setVisible(false); //GESTISCE I KEYSTROKE //moltiplicazione //KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, InputEvent.SHIFT_DOWN_MASK) -> SHIFT + (+) this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, InputEvent.SHIFT_DOWN_MASK, false), "moltiplica"); //KeyStroke.getKeyStroke(KeyEvent.VK_MULTIPLY, 0) --> * on numpad this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_MULTIPLY , 0, false), "moltiplica"); this.getRootPane().getActionMap().put("moltiplica", new AbstractAction() { //multiplication by SHIFT and + on the keyboard private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { calc.multiply(); redisplay(); } }); //somma //KeyStroke.getKeyStroke(KeyEvent.VK_PLUS,0, true) --> + on keyboard //KeyStroke.getKeyStroke(KeyEvent.VK_ADD,0, true) --> + on numpad of keyboard this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS,0, false), "somma"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD,0, false), "somma"); this.getRootPane().getActionMap().put("somma", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { calc.plus(); redisplay(); } }); //differenza //KeyStroke.getKeyStroke(KeyEvent.VK_MINUS,0) --> - on keyboard //KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT,0) --> - on numpad of keyboard this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS,0, false), "differenza"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT ,0, false), "differenza"); this.getRootPane().getActionMap().put("differenza", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { calc.minus(); redisplay(); } }); //divisione //KeyStroke.getKeyStroke(KeyEvent.VK_7,InputEvent.SHIFT_DOWN_MASK) --> / on keyboard //KeyStroke.getKeyStroke(KeyEvent.VK_DIVIDE,0) --> / on numpad of keyboard this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_7,InputE vent.SHIFT_DOWN_MASK, false), "divisione"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DIVIDE,0 , false), "divisione"); this.getRootPane().getActionMap().put("divisione", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { calc.divide(); redisplay(); } }); //uguale - invio this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0, false), "uguale"); this.getRootPane().getActionMap().put("uguale", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { calc.equals(); redisplay(); } }); //inserimento numeri // this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_0,0, false), "zero"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0, 0, false), "zero"); this.getRootPane().getActionMap().put("zero", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(0); } }); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_1,0, false), "uno"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD1, 0, false), "uno"); this.getRootPane().getActionMap().put("uno", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(1); } }); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_2,0, false), "due"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD2, 0, false), "due"); this.getRootPane().getActionMap().put("due", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(2); } }); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_3,0, false), "tre"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD3, 0, false), "tre"); this.getRootPane().getActionMap().put("tre", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(3); } }); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_4,0, false), "quattro"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD4, 0, false), "quattro"); this.getRootPane().getActionMap().put("quattro", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(4); } }); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_5,0, false), "cinque"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD5, 0, false), "cinque"); this.getRootPane().getActionMap().put("cinque", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(5); } }); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_6,0, false), "sei"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD6, 0, false), "sei"); this.getRootPane().getActionMap().put("sei", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(6); } }); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_7,0, false), "sette"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD7, 0, false), "sette"); this.getRootPane().getActionMap().put("sette", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(7); } }); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_8,0, false), "otto"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD8, 0, false), "otto"); this.getRootPane().getActionMap().put("otto", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(8); } }); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_9,0, false), "nove"); this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD9, 0, false), "nove"); this.getRootPane().getActionMap().put("nove", new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { setCifraAndRedisplay(9); } }); } /** * Update the interface display to show the current value of the calculator. */ private void redisplay() { //check if the "M" on the dispaly has to be shown if (calc.getMemoryValue() != 0) {//if a value diffeerent than 0 is in memory notificationText.setVisible(true); } else { notificationText.setVisible(false); } display.setText(calc.getDisplayValueString()); } /** * intercetta l'evento corrispondente alla pressione di un tasto * corrispondente a un numero; il numero è passato all'oggetto CalcEngine. * prerequisito: il tasto ha un nome corrispondente al numero, ad esempio * "2" per il tasto corrispondente a 2. * * @param evt evento corrispondente alla pressione del numero */ private void setNumber(java.awt.event.ActionEvent evt) { String command = evt.getActionCommand(); int number = Integer.parseInt(command); calc.numberPressed(number); } private void setCifraAndRedisplay(int numero) { calc.numberPressed(numero); redisplay(); } @SuppressWarnings("unchecked") // <editor-fold defaultstate="collapsed" desc="Generated Code"> private void initComponents() { // da fare} private void zeroActionPerformed(java.awt.event.ActionEvent evt) { /* setNumber(evt); redisplay(); */ setCifraAndRedisplay(0); } private void oneActionPerformed(java.awt.event.ActionEvent evt) { /* setNumber(evt); redisplay(); */ setCifraAndRedisplay(1); } private void twoActionPerformed(java.awt.event.ActionEvent evt) { /* setNumber(evt); redisplay(); */ setCifraAndRedisplay(2); } //e altri metodi da implementare } Valutazione Livello sufficiente-discreto (voti da 6 a 7): calcolatrice standard di Windows con interfaccia grafica, usando il parser della classe ReversePolishNotation e l’algoritmo di valutazione di RPNCalc, senza l’uso della classe BigDecimal. Livello buono (voti da 7 a 8): uso del parser https://github.com/uklimaschewski/EvalEx, uso della classe BigDecimal, e doppia modalità della calcolatrice (base e scientifica). Come nella calcolatrice di Windows bisogna avere un pulsante per mostrare lo storico dei calcoli effettuati. Tale storico potrà essere scartato oppure salvato su file. Livello eccellente-ottimo (voti da 8 al 10): 1. Tutto quanto previsto dal livello eccellente con in aggiunta la possibilità di eseguire il grafico di una funzione di una variabile di cui si inserisce in input l’espressione. 2. Il grafico di funzione dovrà essere interattivo (click sinistro si fa uno zoom in avanti, SHIFT+ click sinistro si fa uno zoom indietro). 3. Il pannello che mostra il grafico di funzione dovrà permettere di definire anche il numero di campioni della funzione (con valore di default se non impostato dall’utente). 4. La calcolatrice dovrà avere la possibilità di associare almeno 5 funzioni, programmabili da utente, ad altrettanti pulsati f0, f1,…,f4. 5. La calcolatrice dovrà avere la possibilità di mostrare, con colori diversi, almeno 5 grafici di funzione differenti sullo stesso pannello, con legenda dei colori (ad es. f0 rosso, f1 blu, etc.). Inoltre l’utente dovrà avere la possibilità di deselezionare un grafico, ad esempio, mediante una checkbox posta sopra il pannello del grafico. 6. Sugli assi cartesiani dovrà essere riportata una scala graduata con i valori numerici. 7. La scala utilizzata userà impostazioni di default (calcolo automatico della scala), ma l’utente dovrà avere, sul frame che contiene il pannello che rappresenta il grafico, la possibilità di impostare manualmente i valori massimi e minimi degli assi cartesiani. 8. L’utente dovrà avere la possibilità di salvare il grafico di funzione. 9. L’utente dovrà avere la possibilità di esportare i valori della funzione (x e f(x)) in un file csv. 10. Quando l’utente porta il mouse sul grafico di una funzione viene visualizzato un tooltip che riporta il valore della funzione e della coordinata x. 11. Il grafico di funzione deve escludere le singolarità: i punti per i quali il calcolo con i double fornirebbe NaN, POSITIVE_INFINITY, NEGATIVE_INFINITY oppure con i BigDecimal fornirebbe NumberFormatException http://stackoverflow.com/questions/10080084/how-to-convert-double-positive-infinity-tobigdecimal Suggerimento implementare una versione di plot che esclude dal path i punti di singolarità.