/* Tablet.java Digitizing Tablet Simulator Copyright (C) 1999-2006 Robert Burkhardt, P.O. Box 426164, Cambridge, MA 02142-0021 This program is free software; you can redistribute it and/or modify it under the terms Version 2 of the GNU General Public License as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. You can also find a copy at the Free Software Foundation's website, http://www.gnu.org. */ package com.angelfire.bobwb.tablet; import java.io.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import java.util.*; /** Encode a 2D reference point whose floating-point values in conjunction with another reference point allow the conversion of integer-valued Points to floating-point format. */ class Reference extends Point { static final int CROSSHAIR_RADIUS = 8; static final int CIRCLE_RADIUS = 4; /** Read-only class to store floating-point coordinates corresponding to a Point. */ static class FloatingValues { protected final double m_xd, m_yd; FloatingValues(double xd, double yd) { m_xd = xd; m_yd = yd; } double getX() { return m_xd; } double getY() { return m_yd; } public String toString() { return "(" + java.lang.Double.toString(m_xd) + ", " + java.lang.Double.toString(m_yd) + ")"; } } /** Floating-point values corresponding to the integer coordinates of this Point. */ FloatingValues m_values; Reference(FloatingValues values, int xi, int yi) { super(xi, yi); // integer center point of the Reference m_values = values; } /** Draw this Reference at the integer center point. */ void draw(Graphics g) { draw(g, x, y); } /** Draw a Reference with the center as specified. */ static void draw(Graphics g, int xc, int yc) { g.drawOval(xc - (CIRCLE_RADIUS + 1), yc - (CIRCLE_RADIUS + 1), 2*CIRCLE_RADIUS + 2, 2*CIRCLE_RADIUS + 2); g.drawLine(xc - CROSSHAIR_RADIUS, yc, xc + CROSSHAIR_RADIUS, yc); g.drawLine(xc, yc - CROSSHAIR_RADIUS, xc, yc + CROSSHAIR_RADIUS); } /** Get floating-point values corresponding to the integer coordinates of this Point. */ FloatingValues getValues() { return m_values; } } /** JComponent to serve as a digitizing surface. It also allows labels to be added to the list of digitized points. It takes care of storing the Point values and saving them to a file when requested. */ class DigitizingSurface extends JComponent implements MouseMotionListener { final static int GRID_DEFAULT_SPACING = 200; final static int GRID_MIN_SPACING = 2; final static int GRID_TICK_RADIUS = Reference.CROSSHAIR_RADIUS; final static Point GRID_DEFAULT_ORIGIN = new Point(0, 0); class Grid { protected boolean m_settingOrigin = false; protected Dimension m_spacing = new Dimension(GRID_DEFAULT_SPACING, GRID_DEFAULT_SPACING); /** Origin of grid. Both coordinates >= 0. */ protected Point m_origin = GRID_DEFAULT_ORIGIN; boolean getEnabled() { if (m_items == null) throw new IllegalStateException("Grid.getEnabled: m_items null"); return m_items.contains(this); } void setEnabled(boolean enabled) { if (m_items == null) throw new IllegalStateException("Grid.setEnabled: m_items null"); m_items.remove(this); if (enabled) addToList(this); m_client.setToggleGridButtonText(); } boolean setSpacing(int width, int height) { if (width >= GRID_MIN_SPACING && height >= GRID_MIN_SPACING) { m_spacing.width = width; m_spacing.height = height; if (!m_settingOrigin) setEnabled(true); repaint(); return true; } else return false; } /** Set the setting-the-origin mode. Also enables or disables the grid display according to whether setting the origin was enabled or disabled. */ void settingOrigin(boolean value) { m_settingOrigin = value; setEnabled(!value); } boolean settingOrigin() { return m_settingOrigin; } void setOrigin(int x, int y) { m_origin = new Point(x, y); settingOrigin(false); } void draw(Graphics g, Dimension bounds) { draw(g, m_origin.x, m_origin.y, bounds); } void draw(Graphics g, int xorigin, int yorigin, Dimension bounds) { int x0 = xorigin; while (x0 + GRID_TICK_RADIUS >= m_spacing.width) x0 -= m_spacing.width; int y0 = yorigin; while (y0 + GRID_TICK_RADIUS >= m_spacing.height) y0 -= m_spacing.height; for (int x = x0; x < bounds.width + GRID_TICK_RADIUS; x += m_spacing.width) { for (int y = y0; y < bounds.height + GRID_TICK_RADIUS; y += m_spacing.height) drawTick(g, x, y); } } void drawTick(Graphics g, int x, int y) { g.drawLine(x - GRID_TICK_RADIUS, y, x + GRID_TICK_RADIUS, y); g.drawLine(x, y - GRID_TICK_RADIUS, x, y + GRID_TICK_RADIUS); } } /** Default bound on one dimension's size for the DigitizingSurface (power of 10). Used to initialize m_resolutionX and m_resolutionY. Used to print out current cursor X and Y coords neatly. @see #m_resolutionX @see #m_resolutionY */ final static int DEFAULT_RESOLUTION = 10; /** Bound on width of DigitizingSurface (power of 10). Used to print out current cursor X coord neatly. @see #makePad */ protected int m_resolutionX = DEFAULT_RESOLUTION; /** Bound on height of DigitizingSurface (power of 10). Used to print out current cursor Y coord neatly. @see #makePad */ protected int m_resolutionY = DEFAULT_RESOLUTION; /** Radius of x's drawn to mark pts. */ final static int MARKER_RADIUS = 4; /** Format for printing the label. @see #printLabel */ final static String LABEL_FORMAT = " /* ! */\n"; /** Format for printing X coordinate. @see #printIntCoord @see #printFloatCoord */ final static String X_FORMAT = " { !,"; /** Format for printing Y coordinate. @see #printIntCoord @see #printFloatCoord */ final static String Y_FORMAT = " ! },\n"; /** Default color for drawing markers. @see #printIntCoord @see #printFloatCoord */ final static Color DEFAULT_COLOR = Color.black; /** Currently selected color for drawing markers. */ Color m_color = DEFAULT_COLOR; /** During next paint, redraw the background. */ protected boolean m_redrawBackground = true; /** Flag indicating Image is loading and it should be checked to see if it is good and to ascertain its dimensions. */ protected boolean m_checkImage = false; /** Have we seen something that shows us the base Image (if any) is good? */ protected boolean m_goodImage = false; /** Optional Image to be displayed as base for digitizing. */ protected Image m_baseImage; /** Color for drawing markers. */ protected Tablet m_client; /** File name for optional Image to be displayed as base for digitizing. */ protected String m_imageFilename; /** List for storing Points and labels. */ protected LinkedList m_items; // LinkedList m_items; /** True if no new points or labels have been added since clearing or the Points were last saved; otherwise false. */ protected int m_unsavedItemCount = 0; /** Two Reference points are possible. */ final static int REFERENCE_MAX_COUNT = 2; /** Two Reference points are possible. */ protected int m_referenceCount = 0; /** Reference points (if any) for transforming the integer values to floating-point coordinates. */ protected Reference m_references[] = new Reference[REFERENCE_MAX_COUNT]; /** Scale factors for scaling point values. Only used when there is a full set of References. */ protected Reference.FloatingValues m_scaleFactors; /** Flag to indicate DigitizingSurface mode where a Reference is being selected. */ protected boolean m_selectReferences = false; /** Last observed mouse position. */ protected Point m_mouse = new Point(0, 0); /** Grid to aid digitizing. */ protected Grid m_grid; /** A place to click the mouse and designate points. */ DigitizingSurface(Tablet client) { m_items = new LinkedList(); // m_items = new LinkedList(); m_client = client; addMouseListener(new MouseAdapter() { /** When the user presses the left mouse button, add a Point corresponding to the current mouse position to the list. */ public void mousePressed(MouseEvent evt) { if (evt.getButton() == MouseEvent.BUTTON1) { if (m_grid.settingOrigin()) { m_grid.setOrigin(evt.getX(), evt.getY()); } else if (m_selectReferences) { Reference.FloatingValues values = m_client.getReferenceValues(); if (values != null) { m_references[m_referenceCount++] = new Reference(values, evt.getX(), evt.getY()); addToList(m_references[m_referenceCount - 1]); if (m_referenceCount >= REFERENCE_MAX_COUNT) { m_selectReferences = false; double xscale = (m_references[1].getValues().getX() - m_references[0].getValues().getX())/ (m_references[1].x - m_references[0].x); double yscale = (m_references[1].getValues().getY() - m_references[0].getValues().getY())/ (m_references[1].y - m_references[0].y); m_scaleFactors = new Reference.FloatingValues(xscale, yscale); } } else return; } else { addToList(new Point(evt.getX(), evt.getY())); m_unsavedItemCount++; } repaint(); } } /** Record the mouse position whenever the user enters the digitizing window. This is to catch the position of the mouse when it drops into the DigitizingSurface after selecting from a menu. */ public void mouseEnter(MouseEvent evt) { mouseMoved(evt); } }); addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent evt) { if (evt.getID() != ComponentEvent.COMPONENT_RESIZED) return; int dim = getWidth(); m_resolutionX = DEFAULT_RESOLUTION; while (dim > m_resolutionX) m_resolutionX *= 10; dim = getHeight(); m_resolutionY = DEFAULT_RESOLUTION; while (dim > m_resolutionY) m_resolutionY *= 10; // update position JLabel using new resolution if (m_mouse.x >= 0 && m_mouse.y >= 0) setPositionLabel(); } }); addMouseMotionListener(this); m_grid = new Grid(); } /** Allow client access to grid settings. */ Grid getGrid() { return m_grid; } /** Set the optional base Image to the graphic contained in the designated file. */ void setImage(String filename) { int flags; m_goodImage = false; m_baseImage = getToolkit().getImage(filename); flags = checkImage(m_baseImage, this); m_imageFilename = filename; m_redrawBackground = true; if ((flags & ERROR) != 0) { // second or later attempt to load a bad file m_client.notify("Can't load " + "\"" + m_imageFilename + "\""); repaint(); } else if ((flags & ALLBITS) == 0) { m_checkImage = true; prepareImage(m_baseImage, this); } else // we must have loaded this before { m_goodImage = true; setPreferredSize(m_baseImage.getWidth(this), m_baseImage.getHeight(this)); repaint(); } } /** Set the preferred size for the DigitizingSurface and notify the JScrollPane. */ void setPreferredSize(int width, int height) { if (width < DEFAULT_RESOLUTION) width = DEFAULT_RESOLUTION; if (height < DEFAULT_RESOLUTION) height = DEFAULT_RESOLUTION; setPreferredSize(new Dimension(width, height)); m_client.revalidate(); } /** Returns true if the Tablet is digitizing on an Image. */ boolean hasImage() { return m_goodImage; } /** Create padding String for printed unsigned integer value based on screen resolution. */ protected static String makePad(int value, char padChar, int resolution) { StringBuffer pad = new StringBuffer(); if (value < 0) throw new IllegalArgumentException("makePad: value < 0"); if (value == 0) value = 1; // get padding right for zero for (long j = resolution/10; value < j; j /= 10) pad.append(padChar); return pad.toString(); } /** Print integer coordinate value according to format: the coordinate value is printed instead of the exclamation point. A newline (\n or \d\n according to the environment) is printed where \n appears. */ protected void printIntCoord(PrintStream ps, int coord, String format, int resolution) { for (int i = 0; i < format.length(); i++) switch(format.charAt(i)) { case '!': String pad = makePad(coord, ' ', resolution); ps.print(pad); ps.print(coord); break; case '\n': ps.println(); break; default: ps.print(format.charAt(i)); break; } } /** Print floating-point coordinate value according to format: the coordinate value is printed instead of the exclamation point. A newline (\n or \d\n according to the environment) is printed where \n appears. */ protected void printFloatCoord(PrintStream ps, double coord, String format) { for (int i = 0; i < format.length(); i++) switch(format.charAt(i)) { case '!': ps.print(toExpString(coord)); break; case '\n': ps.println(); break; default: ps.print(format.charAt(i)); break; } } /** Print label according to format: the label text is printed instead of the exclamation point. A newline (\n or \d\n according to the environment) is printed where \n appears. */ protected void printLabel(PrintStream ps, String label, String format) { for (int i = 0; i < format.length(); i++) switch(format.charAt(i)) { case '!': ps.print(label); break; case '\n': ps.println(); break; default: ps.print(format.charAt(i)); } } /** Scale the X coordinate of a point according to reference points. If the reference points aren't both there, it just converts to double. */ double scaleX(int x) { return (m_referenceCount == REFERENCE_MAX_COUNT) ? m_references[0].getValues().getX() + m_scaleFactors.getX()*(x - m_references[0].x) : x; } /** Scale the Y coordinate of a point according to reference points. If the reference points aren't both there, it just converts to double. */ double scaleY(int y) { return (m_referenceCount == REFERENCE_MAX_COUNT) ? m_references[0].getValues().getY() + m_scaleFactors.getY()*(y - m_references[0].y) : y; } final static double log10 = Math.log(10); final static int MANTISSA_LENGTH = 8; final static int EXPONENT_RESOLUTION = 100; final static String ZERO_STRING = " 0.000000E+00"; /** Format a double as an exponential in a string. */ static String toExpString(double value) { boolean negative = false; if (value < 0) { value = -value; negative = true; } double baseElog = Math.log(value); double base10log = baseElog/log10; int exp = (int)Math.floor(base10log); double mantissa = Math.pow(10, base10log - exp); boolean negativeExp = false; if (exp < 0) { exp = -exp; negativeExp = true; } String mantissaString = Double.toString(mantissa); if (MANTISSA_LENGTH > mantissaString.length()) mantissaString += "00000000".substring(0, MANTISSA_LENGTH - mantissaString.length()); else mantissaString = mantissaString.substring(0, MANTISSA_LENGTH); String pad = ""; try { pad = makePad(exp, '0', EXPONENT_RESOLUTION); } catch (Exception e) { // value == 0.0 probably return ZERO_STRING; } return (negative ? "-" : " ") + mantissaString + "E" + (negativeExp ? "-" : "+") + pad + exp; } /** Save the list of Points to the designated file including labels but ignoring Colors. */ void savePoints(String filename) throws FileNotFoundException { PrintStream ps = new PrintStream(new FileOutputStream(filename)); Iterator items = m_items.iterator(); Object item; while (items.hasNext()) { item = items.next(); // catch Reference first before it gets caught as a Point if (item instanceof Reference) continue; else if (item instanceof Point) { Point point = (Point)item; if (m_referenceCount == REFERENCE_MAX_COUNT) { // scale the values printFloatCoord(ps, scaleX(point.x), X_FORMAT); printFloatCoord(ps, scaleY(point.y), Y_FORMAT); } else { printIntCoord(ps, point.x, X_FORMAT, m_resolutionX); printIntCoord(ps, point.y, Y_FORMAT, m_resolutionY); } } else if (item instanceof String) { printLabel(ps, (String)item, LABEL_FORMAT); } } m_unsavedItemCount = 0; } /** Remove the last item from the list. Returns true if removal succeeds, false if the list is empty and therefore nothing was removed. */ boolean undo() { /* there are two undo situations which don't require deleting an item from the list */ if (m_grid.settingOrigin()) { m_grid.settingOrigin(false); repaint(); return true; // undo succeeds } else if (m_selectReferences && m_referenceCount == 0) { m_selectReferences = false; repaint(); return true; // undo succeeds } if (m_items.isEmpty()) return false; // undo fails Object item = m_items.removeLast(); if (item instanceof Reference) { // check for derived class first if (m_referenceCount <= 0) { clear(); throw new IllegalStateException("deleteLastItem: referenceCount = " + m_referenceCount); } else { m_referenceCount--; m_selectReferences = true; } repaint(); } else if (item instanceof Point) repaint(); /* update unsavedItemCount except when Object type is not one that gets saved to a file */ if (!(item instanceof Color || item instanceof Reference)) if (m_unsavedItemCount > 0) m_unsavedItemCount--; return true; // delete succeeds } /** Monitor loading of optional base Image. Overrides Component implementation of ImageObserver. */ public boolean imageUpdate (Image img, int flags, int x, int y, int w, int h) { if ((flags & ERROR) != 0) // first attempt to load a bad file m_client.notify("Can't load image " + "\"" + m_imageFilename + "\""); else if (m_checkImage && (flags & WIDTH) != 0 && (flags & HEIGHT) != 0) { m_goodImage = true; // must be something good there setPreferredSize(w, h); m_checkImage = false; } m_redrawBackground = true; repaint(); return (flags & (ALLBITS|ABORT)) == 0; } /** Put the DigitizingSurface into selectReferences mode. */ boolean selectReferences() { if (m_referenceCount >= REFERENCE_MAX_COUNT) return false; m_selectReferences = true; return true; } /** Indicates when DigitizingSurface is in selectReferences mode, and therefore reference point specification has been started but not completed. */ boolean referencesIncomplete() { return m_selectReferences; } /** Clear the digitizing surface and start fresh (no points and default color). The base Image (if any) is retained. */ void clear() { m_items.clear(); m_unsavedItemCount = 0; m_selectReferences = false; m_referenceCount = 0; m_color = DEFAULT_COLOR; m_grid.setEnabled(true); m_grid.setOrigin(GRID_DEFAULT_ORIGIN.x, GRID_DEFAULT_ORIGIN.y); m_grid.settingOrigin(false); m_redrawBackground = true; repaint(); } /** Set the color used to draw markers. */ void setColor(Color color) { m_color = color; addToList(color); } /** Get the color used to draw markers. */ Color getColor() { return m_color; } /** Returns the number of items added since the surface was last cleared or the Points last saved. Color items aren't included in the count. */ int unsavedItemCount() { return m_unsavedItemCount; } /** Add an item to internal multi-purpose list of objects. */ protected void addToList(Object object) { m_items.addLast(object); } /** Add a label to the list of points. */ void addLabel(String label) { addToList(label); m_unsavedItemCount++; } /** Draw a marker on the surface. */ protected void drawMarker (Graphics g, int xval, int yval, int radius) { g.drawLine(xval - radius, yval - radius, xval + radius, yval + radius); g.drawLine(xval + radius, yval - radius, xval - radius, yval + radius); } /** Redraw the surface. Overrides JComponent.update(). */ public void update(Graphics g) { g.setColor(DEFAULT_COLOR); if (m_redrawBackground) { Dimension size = getSize(); g.clearRect(0, 0, size.width, size.height); if (m_goodImage) g.drawImage(m_baseImage, 0, 0, this); m_redrawBackground = false; } // draw digitized Points (including References) Iterator items = m_items.iterator(); Object item; while (items.hasNext()) { item = items.next(); if (item instanceof Reference) { Reference ref = (Reference)item; ref.draw(g); } else if (item instanceof Point) { Point point = (Point)item; drawMarker(g, point.x, point.y, MARKER_RADIUS); } else if (item == m_grid) { m_grid.draw(g, getSize()); } else if (item instanceof Color) { g.setColor((Color)item); } } // draw potential Reference using current color if (m_grid.settingOrigin()) m_grid.draw(g, m_mouse.x, m_mouse.y, getSize()); else if (m_selectReferences) Reference.draw(g, m_mouse.x, m_mouse.y); } /** Completely redraws the surface. Overrides JComponent.paint(). */ public void paint(Graphics g) { m_redrawBackground = true; update(g); } /** Set the text of the client's position indicator to the current integer coordinates of the mouse. */ void setPositionLabel() { if (m_referenceCount == REFERENCE_MAX_COUNT) { m_client.setPositionLabel ("X: " + toExpString(scaleX(m_mouse.x)) + " " + "Y: " + toExpString(scaleY(m_mouse.y))); } else { String xpad = makePad(m_mouse.x, '0', m_resolutionX); String ypad = makePad(m_mouse.y, '0', m_resolutionY); m_client.setPositionLabel ("X: " + xpad + Integer.toString(m_mouse.x) + " " + "Y: " + ypad + Integer.toString(m_mouse.y)); } } /** Watch for mouse movements and display position accordingly (part of MouseMotionListener interface). */ public void mouseMoved(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); if (x == m_mouse.x && y == m_mouse.y) return; m_mouse.x = x; m_mouse.y = y; if (m_mouse.x >= 0 && m_mouse.y >= 0) { if (m_selectReferences || m_grid.settingOrigin()) { m_redrawBackground = true; repaint(); // show new position of prospective reference } setPositionLabel(); } } /** Implement this for the MouseMotionListener interface, but just refer the events to mouseMoved(). */ public void mouseDragged(MouseEvent evt) { mouseMoved(evt); } } /** GUI manager for the Tablet application. */ public class Tablet extends JFrame implements ActionListener { final static int INITIAL_DIM = 500; JMenuBar m_menuBar; JMenu m_fileMenu; JMenuItem m_loadImageButton; JFileChooser m_loadImageDialog; JMenuItem m_savePointsButton; JFileChooser m_savePointsDialog; JMenuItem m_doneButton; JMenu m_editMenu; JMenuItem m_undoButton; JMenuItem m_insertLabelButton; JMenuItem m_clearButton; JMenu m_optionsMenu; JMenuItem m_setSizeButton; JMenuItem m_setRefsButton; final static String GRID_ON_TEXT = "Grid On"; final static String GRID_OFF_TEXT = "Grid Off"; JMenuItem m_toggleGridButton; JMenuItem m_gridSpacingButton; JMenuItem m_gridOriginButton; JMenuItem m_colorButton; JLabel m_showPositionLabel; JScrollPane m_scrollPane; DigitizingSurface m_digitizingSurface; public Tablet() { super("Tablet"); // initialize reusable dialogs m_loadImageDialog = new JFileChooser(); m_loadImageDialog.setName("Load Image"); m_savePointsDialog = new JFileChooser(); m_savePointsDialog.setSelectedFile(new File("points.dat")); m_savePointsDialog.setName("Save Points"); // create the JMenuBar and stock it m_menuBar = new JMenuBar(); m_fileMenu = new JMenu("File"); m_menuBar.add(m_fileMenu); m_loadImageButton = new JMenuItem("Load Image"); m_loadImageButton.addActionListener(this); m_fileMenu.add(m_loadImageButton); m_savePointsButton = new JMenuItem("Save Points"); m_savePointsButton.addActionListener(this); m_fileMenu.add(m_savePointsButton); m_fileMenu.addSeparator(); m_doneButton = new JMenuItem("Exit"); m_doneButton.addActionListener(this); m_fileMenu.add(m_doneButton); m_editMenu = new JMenu("Edit"); m_menuBar.add(m_editMenu); m_undoButton = new JMenuItem("Undo"); m_undoButton.addActionListener(this); m_editMenu.add(m_undoButton); m_clearButton = new JMenuItem("Clear"); m_clearButton.addActionListener(this); m_editMenu.add(m_clearButton); m_insertLabelButton = new JMenuItem("Insert Label"); m_insertLabelButton.addActionListener(this); m_editMenu.add(m_insertLabelButton); m_optionsMenu = new JMenu("Options"); m_menuBar.add(m_optionsMenu); m_setRefsButton = new JMenuItem("Set References"); m_setRefsButton.addActionListener(this); m_optionsMenu.add(m_setRefsButton); m_setSizeButton = new JMenuItem("Set Size"); m_setSizeButton.addActionListener(this); m_optionsMenu.add(m_setSizeButton); // wait until DigitizingSurface // constructed to set text for toggleGridButton m_toggleGridButton = new JMenuItem(); m_toggleGridButton.addActionListener(this); m_optionsMenu.add(m_toggleGridButton); m_gridSpacingButton = new JMenuItem("Grid Spacing"); m_gridSpacingButton.addActionListener(this); m_optionsMenu.add(m_gridSpacingButton); m_gridOriginButton = new JMenuItem("Grid Origin"); m_gridOriginButton.addActionListener(this); m_optionsMenu.add(m_gridOriginButton); m_colorButton = new JMenuItem("Color"); m_colorButton.addActionListener(this); m_optionsMenu.add(m_colorButton); // layout the screen getContentPane().setLayout(new BorderLayout()); getContentPane().add("North", m_menuBar); m_digitizingSurface = new DigitizingSurface(this); m_digitizingSurface.getGrid().setEnabled(true); m_scrollPane = new JScrollPane(m_digitizingSurface); getContentPane().add("Center", m_scrollPane); m_showPositionLabel = new JLabel("X: ? Y: ?"); m_showPositionLabel.setHorizontalAlignment(JLabel.CENTER); getContentPane().add("South", m_showPositionLabel); } /** Set the initial width and height of the Tablet as specified. If necessary adjust the width to the minimum necessary to display the menu. */ protected void setSize(int minDim) { /* Originally getPreferredSize().width was used instead of minDim to set the width, and this code represented a bit of a kludge since I would have expected getPreferredSize() to provide the right number from the start, or getInsets() to provide non-zero values, but when testing in Windows 98 (JRE 1.5.8) they didn't. */ setSize(minDim, minDim); setVisible(true); int diff = m_menuBar.getPreferredSize().width - m_menuBar.getSize().width; if (diff > 0) setSize(minDim + diff, minDim); validate(); } /** Revalidate the JScrollPane (allows controlled access to DigitizingTablet) */ void revalidate() { m_scrollPane.revalidate(); } /** Set the text for the JLabel which indicates the current mouse position. For use by DigitizingSurface. */ void setPositionLabel(String value) { m_showPositionLabel.setText(value); } /** Set the text for the JLabel which indicates the current mouse position. For use by DigitizingSurface. */ void setToggleGridButtonText() { m_toggleGridButton.setText(m_digitizingSurface.getGrid().getEnabled() ? GRID_OFF_TEXT : GRID_ON_TEXT); } /** Handle Choice and Button events. Implements ActionListener. */ public void actionPerformed(ActionEvent evt) { if (evt.getSource() == m_loadImageButton) { if (m_digitizingSurface.unsavedItemCount() != 0) { int choice = JOptionPane.showConfirmDialog (m_scrollPane, "Load new image and clear old without saving unsaved data?", "Confirm Load Image", JOptionPane.YES_NO_OPTION); if (choice != JOptionPane.YES_OPTION) return; } int option = m_loadImageDialog.showOpenDialog(this); if (option == JFileChooser.APPROVE_OPTION) { File file = m_loadImageDialog.getSelectedFile(); if (file == null) return; try { m_digitizingSurface.setImage(file.getCanonicalPath()); m_digitizingSurface.clear(); } catch (IOException e) { notify(e.toString()); } } } else if (evt.getSource() == m_savePointsButton) { int option = m_savePointsDialog.showSaveDialog(this); if (option == JFileChooser.APPROVE_OPTION) { File file = m_savePointsDialog.getSelectedFile(); if (file == null) return; try { String filename = file.getCanonicalPath(); if (m_digitizingSurface.referencesIncomplete()) { int choice = JOptionPane.showConfirmDialog (m_scrollPane, "References incompletely specified. Continue saving points?", "Confirm Save", JOptionPane.YES_NO_OPTION); if (choice != JOptionPane.YES_OPTION) return; } if (file.exists()) { int choice = JOptionPane.showConfirmDialog (m_scrollPane, "Overwrite " + filename + "?", "Confirm Overwrite", JOptionPane.YES_NO_OPTION); if (choice != JOptionPane.YES_OPTION) return; } m_digitizingSurface.savePoints(filename); } catch (IOException e) { notify(e.toString()); } } } else if (evt.getSource() == m_doneButton) verifyDone(); else if (evt.getSource() == m_undoButton) { if (!m_digitizingSurface.undo()) notify("Nothing to undo"); } else if (evt.getSource() == m_insertLabelButton) { String labelText = JOptionPane.showInputDialog(m_scrollPane, "Label Text:", "Insert Label", JOptionPane.QUESTION_MESSAGE); if (labelText != null) m_digitizingSurface.addLabel(labelText); } else if (evt.getSource() == m_clearButton) { if (m_digitizingSurface.unsavedItemCount() != 0) { int choice = JOptionPane.showConfirmDialog (m_scrollPane, "Clear without saving unsaved data?", "Confirm Clear", JOptionPane.YES_NO_OPTION); if (choice != JOptionPane.YES_OPTION) return; } m_digitizingSurface.clear(); } else if (evt.getSource() == m_setRefsButton) { if (!m_digitizingSurface.selectReferences()) notify("References already specified"); } else if (evt.getSource() == m_setSizeButton) { if (m_digitizingSurface.hasImage()) { notify("Set Size not available with image"); return; } else if (m_digitizingSurface.unsavedItemCount() != 0) { int choice = JOptionPane.showConfirmDialog (m_scrollPane, "Resize without saving unsaved data?", "Confirm Resize", JOptionPane.YES_NO_OPTION); if (choice != JOptionPane.YES_OPTION) return; } while (true) { String coordString = JOptionPane.showInputDialog(m_scrollPane, "Width Height:", "Get Tablet Size", JOptionPane.QUESTION_MESSAGE); if (coordString == null) return; StringTokenizer tokenizer = new StringTokenizer(coordString); if (tokenizer.countTokens() != 2) { notify("Bad values"); continue; } String coordToken = tokenizer.nextToken(); int w, h; try { w = Integer.parseInt(coordToken); } catch (Exception e) { notify("Bad values"); continue; } coordToken = tokenizer.nextToken(); try { h = Integer.parseInt(coordToken); } catch (Exception e) { notify("Bad values"); continue; } m_digitizingSurface.clear(); m_digitizingSurface.setPreferredSize(w, h); return; } } else if (evt.getSource() == m_toggleGridButton) { DigitizingSurface.Grid grid = m_digitizingSurface.getGrid(); grid.setEnabled(!grid.getEnabled()); repaint(); } else if (evt.getSource() == m_gridSpacingButton) { getGridSpacing(); } else if (evt.getSource() == m_gridOriginButton) { m_digitizingSurface.getGrid().settingOrigin(true); } else if (evt.getSource() == m_colorButton) { Color color = JColorChooser.showDialog(m_scrollPane, "Color", m_digitizingSurface.getColor()); if (color != null) m_digitizingSurface.setColor(color); } } /** Confirm that the user really wants to exit. */ protected void verifyDone() { if (m_digitizingSurface.unsavedItemCount() != 0) { int choice = JOptionPane.showConfirmDialog (m_scrollPane, "Exit without saving unsaved data?", "Confirm Exit", JOptionPane.YES_NO_OPTION); if (choice != JOptionPane.YES_OPTION) return; } System.exit(0); } /** Get Grid spacing values. */ void getGridSpacing() { while (true) { String coordString = JOptionPane.showInputDialog(m_scrollPane, "X Y Spacing:", "Get Grid Spacing", JOptionPane.QUESTION_MESSAGE); if (coordString == null) return; StringTokenizer tokenizer = new StringTokenizer(coordString); if (tokenizer.countTokens() != 2) { notify("Bad values"); continue; } String coordToken = tokenizer.nextToken(); int x, y; try { x = Integer.parseInt(coordToken); } catch (Exception e) { notify("Bad values"); continue; } coordToken = tokenizer.nextToken(); try { y = Integer.parseInt(coordToken); } catch (Exception e) { notify("Bad values"); continue; } if (!m_digitizingSurface.getGrid().setSpacing(x, y)) { notify("Bad values"); continue; } else return; } } /** Get FloatingValues from user corresponding to a Reference. */ Reference.FloatingValues getReferenceValues() { while (true) { String coordString = JOptionPane.showInputDialog(m_scrollPane, "X Y Coordinates:", "Get Reference Values", JOptionPane.QUESTION_MESSAGE); if (coordString == null) return null; StringTokenizer tokenizer = new StringTokenizer(coordString); if (tokenizer.countTokens() != 2) { notify("Bad values"); continue; } String coordToken = tokenizer.nextToken(); double x, y; try { x = Double.parseDouble(coordToken); } catch (Exception e) { notify("Bad values"); continue; } coordToken = tokenizer.nextToken(); try { y = Double.parseDouble(coordToken); } catch (Exception e) { notify("Bad values"); continue; } return new Reference.FloatingValues(x, y); } } void notify(String msg) { JOptionPane.showMessageDialog(m_scrollPane, msg, "Error", JOptionPane.ERROR_MESSAGE); } public static void main(String args[]) { final Tablet tablet = new Tablet(); tablet.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); tablet.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { tablet.verifyDone(); } }); tablet.setSize(INITIAL_DIM); } } /* History: 29 Nov 06 R. Burkhardt Use getContentPane() before trying to do setLayout() on a JFrame. This should make Linux users happier. 27 Nov 06 R. Burkhardt Move history and to do to bottom of file. Rename TickMarks as Grid. Draw grid so partial ticks are always shown on margins even if their centers are out of bounds. Draw entire grid when setting the origin. Adjust the API for setting the origin to make it more transparent. Increase initial size and make it less oblong. 22 Nov 06 R. Burkhardt Add tick marks. Use Point for recording mouse position. Don't draw frame when Image not being used. 15 Nov 06 R. Burkhardt Use scrolling when image is bigger than current display -- just add JScrollPane and use setPreferredSize(). Set resolution dynamically using the tablet size. When digitizing without an image, allow user to set the surface size. 14 Nov 06 R. Burkhardt Use a fixed-width exponential format to save the points and echo the cursor location when reference points have been specified. Move "Set References" to "Options" menu. 13 Nov 06 R. Burkhardt Finish implementing reference points. Change deleteLastItem() to undo(). 09 Nov 06 R. Burkhardt Implement getReferenceValues(). 08 Nov 06 R. Burkhardt Start implementing infrastructure for designating reference points. 01 Nov 06 R. Burkhardt Use JMenuBar instead of JPanel and JButtons. Add rudimentary undo option. Keep a count of the unsaved items rather than just a flag indicating that they exist. Prioritize todo items. 27 Oct 06 R. Burkhardt Add comments. Encapsulate color operations in DigitizingSurface. Add utility function DigitizingSurface.makePad(). Rename m_resize to more descriptive m_checkImage. Move m_points_saved flag to DigitizingSurface. Use MouseAdapter to avoid all those stubs needed to implement MouseListener. Consolidate m_clear and m_redrawBaseImage into m_redrawBackground. Better indicate interface by designating protected where appropriate. 03 Oct 06 R. Burkhardt Replace VerifyDoneClass with anonymous class. Reduce Tablet-DigitizingTablet dependencies by having Tablet set m_points_saved flag in most cases. Don't catch FileNotFoundException inside of DigitizingTablet.savePoints(). DrawMarker -> drawMarker. Rename insertLabel() as addToList() and make its argument an Object rather than a String. Add m_colorButton. Add Tablet.setSize(int). 13 Sep 06 R. Burkhardt Replace AWT FileDialog with Swing JFileChooser. 07 Sep 06 R. Burkhardt Use java.util.LinkedList instead of custom version. (Don't bother with generics since they require 1.4 and the list is heterogenous anyway.) Fix interpretation of printing formats. 02 Sep 06 R. Burkhardt Use instanceof instead of getClass() etc. 31 Aug 06 R. Burkhardt Use JOptionPane.showMessageDialog() instead of creating custom NotifyDialog and VerifyDialog. Use JOptionPane.showInputDialog() instead of InsertLabelDialog. 28 Aug 06 R. Burkhardt Convert (first pass -- excludes FileDialog) GUI from AWT to Swing. Make window closes work for dialogs and Tablet. Call it "unsaved data" rather than "unsaved points" because it could just be a label. 25 Aug 06 R. Burkhardt Add package designation. Use FSF URL rather than street address. Convert VerifyLoadDialog, VerifyClearDialog, VerifyDoneDialog and InsertLabelDialog into nested classes of Tablet obviating m_client data members. 26 Nov 00 R. Burkhardt Clear points (with verify) on load. 22 Oct 99 R. Burkhardt Eliminate resizing when image loaded. Verify done and clear when there are unsaved points. 20 Oct 99 R. Burkhardt First draft completed. To do: KeyPress->action mappings. Update mouse position when scrolling. Enable/disable menu buttons appropriately. Display "loading..." or some such thing when Image is loading. Don't allow digitizing while Image is loading. I18n. Allow user to set formats for printing point coordinates to file. Allow snapping to tick marks when references being set and points being digitized. */