From e416acb9b012b17d1efe49ad2199ea7132d874d1 Mon Sep 17 00:00:00 2001 From: Normand Briere <nbriere@noware.ca> Date: Sat, 07 Jul 2018 11:09:43 -0400 Subject: [PATCH] timeline --- timeflow/data/db/filter/ConstFilter.java | 24 timeflow/app/ui/ColorLegendPanel.java | 63 timeflow/vis/TimeScale.java | 95 timeflow/data/db/filter/OrFilter.java | 38 timeflow/views/HtmlControls.java | 21 timeflow/vis/VisualActFactory.java | 119 timeflow/app/actions/DeleteUnselectedAction.java | 40 timeflow/format/field/FormatURL.java | 41 timeflow/model/TFListener.java | 5 timeflow/app/ui/filter/BabyHistogram.java | 274 + timeflow/views/AbstractVisualizationView.java | 244 + timeflow/data/analysis/FieldAnalysis.java | 13 timeflow/format/file/DelimitedText.java | 214 timeflow/views/TableView.java | 267 + timeflow/app/actions/TimeflowAction.java | 36 timeflow/app/actions/ImportFromPasteAction.java | 53 timeflow/app/ui/filter/FilterCategoryPanel.java | 164 timeflow/format/field/DateTimeGuesser.java | 115 timeflow/app/ui/WaitingDialog.java | 56 timeflow/data/analysis/DBAnalysis.java | 14 timeflow/data/db/filter/StringMatchFilter.java | 69 timeflow/app/ui/StatusPanel.java | 98 timeflow/views/ListView.java | 266 + timeflow/views/SummaryView.java | 181 timeflow/app/ui/filter/FilterNumberPanel.java | 171 timeflow/app/actions/DeleteFieldAction.java | 43 timeflow/data/analysis/MissingValueAnalysis.java | 44 timeflow/format/file/Export.java | 10 timeflow/app/ui/filter/SearchPanel.java | 52 timeflow/data/db/filter/NotFilter.java | 24 timeflow/util/ColorUtils.java | 31 timeflow/util/TimeIt.java | 34 timeflow/data/time/RoughTime.java | 128 timeflow/vis/Mouseover.java | 94 timeflow/views/TimelineView.java | 465 + timeflow/vis/GroupVisualAct.java | 98 timeflow/format/field/FormatDouble.java | 103 timeflow/app/actions/WebDocAction.java | 25 timeflow/model/ModelPanel.java | 36 timeflow/data/analysis/RangeDateAnalysis.java | 62 timeflow/vis/calendar/CalendarVisuals.java | 113 timeflow/app/actions/NewDataAction.java | 27 timeflow/format/file/FileExtensionCatalog.java | 28 timeflow/vis/timeline/AxisRenderer.java | 88 timeflow/app/actions/RenameFieldAction.java | 100 timeflow/data/db/Field.java | 50 timeflow/vis/timeline/TimelineSlider.java | 239 + timeflow/data/time/TimeUtils.java | 21 timeflow/util/IO.java | 54 timeflow/vis/TagVisualAct.java | 52 timeflow/format/field/FieldFormatGuesser.java | 50 timeflow/views/CalendarView.java | 304 + timeflow/vis/timeline/TimelineRenderer.java | 184 timeflow/app/actions/DateFieldAction.java | 24 timeflow/views/AbstractView.java | 85 timeflow/app/ui/GlobalDisplayPanel.java | 174 timeflow/app/ui/AddFieldPanel.java | 24 timeflow/vis/VisualEncoder.java | 138 timeflow/app/ui/LinkTabPane.java | 208 timeflow/model/Display.java | 373 + timeflow/data/db/DBUtils.java | 234 timeflow/model/TFModel.java | 303 + timeflow/app/ui/ImportDelimitedPanel.java | 310 + timeflow/app/actions/QuitAction.java | 32 timeflow/data/db/BasicAct.java | 89 timeflow/data/time/Interval.java | 114 timeflow/format/field/FieldFormat.java | 64 timeflow/util/Pad.java | 19 timeflow/app/ui/EditValuePanel.java | 111 timeflow/data/db/filter/FieldValueSetFilter.java | 40 timeflow/format/field/FieldFormatCatalog.java | 54 timeflow/vis/calendar/Grid.java | 375 + timeflow/app/ui/MassDeletePanel.java | 80 timeflow/app/ui/filter/FilterDefinitionPanel.java | 10 timeflow/vis/VisualAct.java | 314 + timeflow/data/analysis/FrequencyAnalysis.java | 76 timeflow/app/TimeflowApp.java | 709 ++ timeflow/model/VirtualField.java | 39 timeflow/data/db/filter/AndFilter.java | 50 timeflow/app/actions/ReorderFieldsAction.java | 38 timeflow/format/field/FormatString.java | 44 timeflow/format/field/FormatStringArray.java | 59 timeflow/app/ui/ComponentCluster.java | 37 timeflow/app/actions/DeleteSelectedAction.java | 51 timeflow/format/file/HtmlFormat.java | 143 timeflow/format/file/Import.java | 9 timeflow/data/db/Schema.java | 130 timeflow/data/db/ActDB.java | 30 timeflow/data/time/TimeUnit.java | 243 + timeflow/format/file/TimeflowFormat.java | 163 timeflow/app/ui/DottedLine.java | 34 timeflow/app/ui/SizeLegendPanel.java | 113 timeflow/vis/timeline/AxisTicMarks.java | 117 timeflow/data/db/filter/ActFilter.java | 14 timeflow/data/analysis/RangeNumberAnalysis.java | 85 timeflow/app/TimeflowAppLauncher.java | 52 timeflow/data/db/filter/ValueFilter.java | 5 timeflow/format/file/DelimitedFormat.java | 217 timeflow/app/ui/ReorderFieldsPanel.java | 68 timeflow/app/ui/DateFieldPanel.java | 178 timeflow/views/BarGraphView.java | 363 + timeflow/app/ui/filter/FilterDatePanel.java | 173 timeflow/data/db/BasicDB.java | 129 timeflow/app/AppState.java | 107 timeflow/vis/calendar/GridCell.java | 20 timeflow/data/db/ArrayDB.java | 348 + timeflow/app/actions/AddRecordAction.java | 26 timeflow/vis/timeline/TimelineVisuals.java | 345 + timeflow/format/field/DateTimeParser.java | 39 timeflow/data/db/filter/MissingValueFilter.java | 27 timeflow/data/db/filter/TimeIntervalFilter.java | 35 timeflow/model/TFEvent.java | 43 timeflow/data/db/filter/FieldValueFilter.java | 37 timeflow/data/db/ActList.java | 23 timeflow/util/DoubleBag.java | 145 timeflow/app/actions/AddFieldAction.java | 49 timeflow/app/AboutWindow.java | 53 timeflow/app/actions/CopySchemaAction.java | 29 timeflow/data/db/filter/NumericRangeFilter.java | 28 timeflow/vis/MouseoverLabel.java | 32 timeflow/views/IntroView.java | 105 timeflow/data/db/ActComparator.java | 118 timeflow/views/DescriptionView.java | 71 timeflow/util/Bag.java | 136 timeflow/app/ui/EditRecordPanel.java | 134 timeflow/app/ui/HtmlDisplay.java | 24 timeflow/app/ui/filter/FilterControlPanel.java | 208 timeflow/vis/timeline/TimelineTrack.java | 129 timeflow/data/db/Act.java | 25 timeflow/app/actions/EditSourceAction.java | 37 timeflow/format/field/FormatDateTime.java | 79 timeflow/app/ui/filter/FilterTitle.java | 52 132 files changed, 14,288 insertions(+), 0 deletions(-) diff --git a/timeflow/app/AboutWindow.java b/timeflow/app/AboutWindow.java new file mode 100755 index 0000000..d3bbb8b --- /dev/null +++ b/timeflow/app/AboutWindow.java @@ -0,0 +1,53 @@ +package timeflow.app; + +import timeflow.model.*; + +import java.awt.*; +import java.io.File; +import java.util.*; +import javax.imageio.*; + +public class AboutWindow extends Window { + + Image image; + Display display; + int w=640, h=380; + + public AboutWindow(Frame owner, Display display) { + super(owner); + this.display=display; + + try + { + // image=ImageIO.read(getClass().getClassLoader().getResourceAsStream("images/splash.jpg")); + image=ImageIO.read(new File("images/splash.jpg")); + w=image.getWidth(null); + h=image.getHeight(null); + } + catch (Exception e) + { + e.printStackTrace(System.out); + } + Dimension size = Toolkit.getDefaultToolkit().getScreenSize(); + setBounds((size.width-w)/2, (size.height-h)/2, w,h); + + } + + public void paint(Graphics g) + { + if (image!=null) + { + g.drawImage(image,0,0,null); + return; + } + int lx=15; + ((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(display.getColor("splash.background")); + g.fillRect(0,0,w,h); + g.setFont(display.huge()); + g.setColor(display.getColor("splash.text")); + g.drawString(Display.version(), lx, 35); + g.setFont(display.plain()); + g.drawString("Prototype version",lx,60); + } +} diff --git a/timeflow/app/AppState.java b/timeflow/app/AppState.java new file mode 100755 index 0000000..c1d7fd0 --- /dev/null +++ b/timeflow/app/AppState.java @@ -0,0 +1,107 @@ +package timeflow.app; + +import timeflow.util.*; + +import java.io.*; +import java.util.*; + +public class AppState +{ + + private static final String FILE = "settings/info.txt"; + private File currentFile, currentDir; + private LinkedList<File> recentFiles = new LinkedList<File>(); + + public AppState() + { + if (!new File(FILE).exists()) + { + System.err.println("No existing settings file found."); + return; + } + try + { + for (String line : IO.lines(FILE)) + { + String[] t = line.split("\t"); + String command = t[0]; + String arg = t[1]; + if ("CURRENT_FILE".equals(command)) + { + currentFile = new File(arg); + } else if ("RECENT_FILE".equals(command)) + { + recentFiles.add(new File(arg).getAbsoluteFile()); + } else if ("CURRENT_DIR".equals(command)) + { + currentDir = new File(arg); + } + } + } catch (Exception e) + { + e.printStackTrace(System.out); + } + } + + public List<File> getRecentFiles() + { + return (List<File>) recentFiles.clone(); + } + + public File getCurrentFile() + { + return currentFile; + } + + public void setCurrentFile(File currentFile) + { + this.currentFile = currentFile.getAbsoluteFile(); + + // if list is big, remove one at end. + if (recentFiles.size() > 10) + { + recentFiles.removeLast(); + } + + // put at front of list + if (recentFiles.contains(this.currentFile)) + { + recentFiles.remove(this.currentFile); + } + recentFiles.addFirst(this.currentFile); + + // set current dir, too. + this.currentDir = currentDir; + } + + public File getCurrentDir() + { + return currentDir; + } + + public void setCurrentDir(File currentDir) + { + this.currentDir = currentDir; + } + + public void save() + { + try + { + FileOutputStream fos = new FileOutputStream(FILE); + PrintStream out = new PrintStream(fos); + out.println("CURRENT_FILE\t" + currentFile); + out.println("CURRENT_DIR\t" + currentDir); + for (File f : recentFiles) + { + out.println("RECENT_FILE\t" + f); + } + out.flush(); + out.close(); + fos.close(); + } catch (Exception e) + { + e.printStackTrace(System.out); + } + } +} diff --git a/timeflow/app/TimeflowApp.java b/timeflow/app/TimeflowApp.java new file mode 100755 index 0000000..b7b8442 --- /dev/null +++ b/timeflow/app/TimeflowApp.java @@ -0,0 +1,709 @@ +package timeflow.app; + +import timeflow.app.ui.*; +import timeflow.app.actions.*; +import timeflow.app.ui.filter.*; +import timeflow.data.db.*; +import timeflow.data.time.RoughTime; +import timeflow.format.field.*; +import timeflow.format.file.*; +import timeflow.model.*; +import timeflow.views.*; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import timeflow.util.Pad; + +import java.awt.*; +import java.awt.event.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.*; +import java.util.ArrayList; + +public class TimeflowApp extends JFrame +{ + + public TFModel model = new TFModel(); + public JFileChooser fileChooser; + AboutWindow splash; + String[][] examples; + String[] templates; + AppState state = new AppState(); + JMenu openRecent = new JMenu("Open Recent"); + public JMenu filterMenu; + JMenuItem save = new JMenuItem("Save"); + FilterControlPanel filterControlPanel; + LinkTabPane leftPanel; + TFListener filterMenuMaker = new TFListener() + { + + @Override + public void note(TFEvent e) + { + if (e.affectsSchema()) + { + filterMenu.removeAll(); + for (Field f : model.getDB().getFields()) + { + if (f.getType() == String.class || f.getType() == String[].class + || f.getType() == Double.class || f.getType() == RoughTime.class) + { + final JCheckBoxMenuItem item = new JCheckBoxMenuItem(f.getName()); + final Field field = f; + filterMenu.add(item); + item.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + filterControlPanel.setFacet(field, item.getState()); + leftPanel.setSelectedIndex(1); + } + }); + } + } + } + } + }; + + void splash(boolean visible) + { + splash.setVisible(visible); + } + + public void init() throws Exception + { + Dimension d = Toolkit.getDefaultToolkit().getScreenSize(); + setBounds(0, 0, Math.min(d.width, 1200), Math.min(d.height, 900)); + setTitle(Display.version()); + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + final QuitAction quitAction = new QuitAction(this, model); + addWindowListener(new WindowAdapter() + { + + @Override + public void windowClosing(WindowEvent e) + { + quitAction.quit(); + } + + public void windowStateChanged(WindowEvent e) + { + repaint(); + } + }); + Image icon = Toolkit.getDefaultToolkit().getImage("images/icon.gif"); + setIconImage(icon); + + // read example directory + String[] ex = getVisibleFiles("settings/examples"); + int n = ex.length; + examples = new String[n][2]; + for (int i = 0; i < n; i++) + { + String s = ex[i]; + int dot = s.lastIndexOf('.'); + if (dot >= 0 && dot < s.length() - 1); + s = s.substring(0, dot); + examples[i][0] = s; + examples[i][1] = "settings/examples/" + ex[i]; + } + templates = getVisibleFiles("settings/templates"); + fileChooser = new JFileChooser(state.getCurrentFile()); + + getContentPane().setLayout(new BorderLayout()); + + // left tab area, with vertical gray divider. + JPanel leftHolder = new JPanel(); + getContentPane().add(leftHolder, BorderLayout.WEST); + + leftHolder.setLayout(new BorderLayout()); + JPanel pad = new Pad(3, 3); + pad.setBackground(Color.gray); + leftHolder.add(pad, BorderLayout.EAST); + + leftPanel = new LinkTabPane();//JTabbedPane(); + leftHolder.add(leftPanel, BorderLayout.CENTER); + + JPanel configPanel = new JPanel(); + configPanel.setLayout(new BorderLayout()); + filterMenu = new JMenu("Filters"); + filterControlPanel = new FilterControlPanel(model, filterMenu); + final GlobalDisplayPanel displayPanel = new GlobalDisplayPanel(model, filterControlPanel); + configPanel.add(displayPanel, BorderLayout.NORTH); + + JPanel legend = new JPanel(); + legend.setLayout(new BorderLayout()); + configPanel.add(legend, BorderLayout.CENTER); + legend.add(new SizeLegendPanel(model), BorderLayout.NORTH); + legend.add(new ColorLegendPanel(model), BorderLayout.CENTER); + leftPanel.addTab(configPanel, "Display", true); + + leftPanel.addTab(filterControlPanel, "Filter", true); + + // center tab area + + final LinkTabPane center = new LinkTabPane(); + getContentPane().add(center, BorderLayout.CENTER); + + center.addPropertyChangeListener(new PropertyChangeListener() + { + + @Override + public void propertyChange(PropertyChangeEvent evt) + { + displayPanel.showLocalControl(center.getCurrentName()); + } + }); + + final IntroView intro = new IntroView(model); // we refer to this a bit later. + final TimelineView timeline = new TimelineView(model); + AbstractView[] views = + { + timeline, + new CalendarView(model), + new ListView(model), + new TableView(model), + new BarGraphView(model), + intro, + new DescriptionView(model), + new SummaryView(model), + }; + + for (int i = 0; i < views.length; i++) + { + center.addTab(views[i], views[i].getName(), i < 5); + displayPanel.addLocalControl(views[i].getName(), views[i].getControls()); + } + + // start off with intro screen + center.setCurrentName(intro.getName()); + displayPanel.showLocalControl(intro.getName()); + + // but then, once data is loaded, switch directly to the timeline view. + model.addListener(new TFListener() + { + + @Override + public void note(TFEvent e) + { + if (e.type == e.type.DATABASE_CHANGE) + { + if (center.getCurrentName().equals(intro.getName())) + { + center.setCurrentName(timeline.getName()); + displayPanel.showLocalControl(timeline.getName()); + } + } + } + }); + + JMenuBar menubar = new JMenuBar(); + setJMenuBar(menubar); + + JMenu fileMenu = new JMenu("File"); + menubar.add(fileMenu); + + fileMenu.add(new NewDataAction(this)); + fileMenu.add(new CopySchemaAction(this)); + + JMenu templateMenu = new JMenu("New From Template"); + fileMenu.add(templateMenu); + for (int i = 0; i < templates.length; i++) + { + JMenuItem t = new JMenuItem(templates[i]); + final String fileName = "settings/templates/" + templates[i]; + templateMenu.add(t); + t.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + load(fileName, FileExtensionCatalog.get(fileName), true); + } + }); + } + + fileMenu.addSeparator(); + + + JMenuItem open = new JMenuItem("Open..."); + fileMenu.add(open); + open.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + load(new TimeflowFormat(), false); + } + }); + + + fileMenu.add(openRecent); + makeRecentFileMenu(); + fileMenu.addSeparator(); + fileMenu.add(new ImportFromPasteAction(this)); + + JMenuItem impDel = new JMenuItem("Import CSV/TSV..."); + fileMenu.add(impDel); + impDel.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + if (checkSaveStatus()) + { + importDelimited(); + } + } + }); + + fileMenu.addSeparator(); + + fileMenu.add(save); + save.setAccelerator(KeyStroke.getKeyStroke('S', + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + save.setEnabled(false); + save.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + save(model.getDbFile()); + + } + }); + model.addListener(new TFListener() + { + + @Override + public void note(TFEvent e) + { + save.setEnabled(!model.getReadOnly()); + } + }); + + JMenuItem saveAs = new JMenuItem("Save As..."); + fileMenu.add(saveAs); + saveAs.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + saveAs(); + } + }); + + fileMenu.addSeparator(); + + JMenuItem exportTSV = new JMenuItem("Export TSV..."); + fileMenu.add(exportTSV); + exportTSV.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + exportDelimited('\t'); + } + }); + JMenuItem exportCSV = new JMenuItem("Export CSV..."); + fileMenu.add(exportCSV); + exportCSV.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + exportDelimited(','); + } + }); + JMenuItem exportHTML = new JMenuItem("Export HTML..."); + fileMenu.add(exportHTML); + exportHTML.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + exportHtml(); + } + }); + fileMenu.addSeparator(); + fileMenu.add(quitAction); + + JMenu editMenu = new JMenu("Edit"); + menubar.add(editMenu); + editMenu.add(new AddRecordAction(this)); + editMenu.addSeparator(); + editMenu.add(new DateFieldAction(this)); + editMenu.add(new AddFieldAction(this)); + editMenu.add(new RenameFieldAction(this)); + editMenu.add(new DeleteFieldAction(this)); + editMenu.add(new ReorderFieldsAction(this)); + editMenu.addSeparator(); + editMenu.add(new EditSourceAction(this)); + editMenu.addSeparator(); + editMenu.add(new DeleteSelectedAction(this)); + editMenu.add(new DeleteUnselectedAction(this)); + + menubar.add(filterMenu); + model.addListener(filterMenuMaker); + + + JMenu exampleMenu = new JMenu("Examples"); + menubar.add(exampleMenu); + + for (int i = 0; i < examples.length; i++) + { + JMenuItem example = new JMenuItem(examples[i][0]); + exampleMenu.add(example); + final String file = examples[i][1]; + example.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + load(file, FileExtensionCatalog.get(file), true); + } + }); + } + + JMenu helpMenu = new JMenu("Help"); + menubar.add(helpMenu); + + helpMenu.add(new WebDocAction(this)); + + JMenuItem about = new JMenuItem("About TimeFlow"); + helpMenu.add(about); + about.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + splash(true); + } + }); + + model.addListener(new TFListener() + { + + @Override + public void note(TFEvent e) + { + if (e.type == TFEvent.Type.DATABASE_CHANGE) + { + String name = model.getDbFile(); + int n = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); + if (n > 0) + { + name = name.substring(n + 1); + } + setTitle(name); + } + } + }); + } + + void makeRecentFileMenu() + { + openRecent.removeAll(); + try + { + for (File f : state.getRecentFiles()) + { + final String file = f.getAbsolutePath(); + JMenuItem m = new JMenuItem(f.getName()); + openRecent.add(m); + m.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + load(file, FileExtensionCatalog.get(file), false); + } + }); + } + } catch (Exception e) + { + e.printStackTrace(System.out); + } + } + + void exportHtml() + { + int retval = fileChooser.showSaveDialog(this); + if (retval == fileChooser.APPROVE_OPTION) + { + String fileName = fileChooser.getSelectedFile().getAbsolutePath(); + try + { + FileWriter fw = new FileWriter(fileName); + BufferedWriter out = new BufferedWriter(fw); + new HtmlFormat().export(model, out); + out.close(); + fw.close(); + } catch (Exception e) + { + System.out.println(e); + showUserError("Couldn't save file: " + e); + } + } + } + + void exportDelimited(char delimiter) + { + int retval = fileChooser.showSaveDialog(this); + if (retval == fileChooser.APPROVE_OPTION) + { + String fileName = fileChooser.getSelectedFile().getAbsolutePath(); + try + { + new DelimitedFormat(delimiter).write(model.getDB(), new File(fileName)); + } catch (Exception e) + { + System.out.println(e); + showUserError("Couldn't save file: " + e); + } + } + } + + void load(Import importer, boolean readOnly) + { + if (!checkSaveStatus()) + { + return; + } + try + { + int retval = fileChooser.showOpenDialog(this); + if (retval == fileChooser.APPROVE_OPTION) + { + load(fileChooser.getSelectedFile().getAbsolutePath(), importer, readOnly); + noteFileUse(fileChooser.getSelectedFile().getAbsolutePath()); + } + } catch (Exception e) + { + showUserError("Couldn't read file."); + System.out.println(e); + } + } + + public void showImportEditor(String fileName, String[][] data) + { + final ImportDelimitedPanel editor = new ImportDelimitedPanel(model); + editor.setFileName(fileName); + editor.setData(data); + editor.setBounds(0, 0, 1024, 768); + editor.setVisible(true); + SwingUtilities.invokeLater(new Runnable() + { + + public void run() + { + editor.scrollToTop(); + } + }); + + } + + void importDelimited() + { + if (!checkSaveStatus()) + { + return; + } + try + { + int result = fileChooser.showOpenDialog(this); + + if (result == JFileChooser.APPROVE_OPTION) + { + File file = fileChooser.getSelectedFile(); + String fileName = file.getAbsolutePath(); + noteFileUse(fileName); + String[][] data = DelimitedFormat.readArrayGuessDelim(fileName, System.out); + showImportEditor(fileName, data); + + } else + { + System.out.println("OK, canceling import."); + } + } catch (Exception e) + { + showUserError("Couldn't read file format."); + e.printStackTrace(System.out); + } + } + + void load(final String fileName, final Import importer, boolean readOnly) + { + if (!checkSaveStatus()) + { + return; + } + try + { + final File f = new File(fileName); + ActDB db = importer.importFile(f); + model.setDB(db, fileName, readOnly, TimeflowApp.this); + if (!readOnly) + { + noteFileUse(fileName); + } + } catch (Exception e) + { + e.printStackTrace(System.out); + showUserError("Couldn't read file."); + model.noteError(this); + } + } + + public boolean save(String fileName) + { + try + { + FileWriter fw = new FileWriter(fileName); + BufferedWriter out = new BufferedWriter(fw); + new TimeflowFormat().export(model, out); + out.close(); + fw.close(); + noteFileUse(fileName); + if (!fileName.equals(model.getDbFile())) + { + model.setDbFile(fileName, false, this); + } + model.setChangedSinceSave(false); + model.setReadOnly(false); + save.setEnabled(true); + return true; + } catch (Exception e) + { + e.printStackTrace(System.out); + showUserError("Couldn't save file: " + e); + return false; + } + } + + public boolean checkSaveStatus() + { + boolean needSave = model.isChangedSinceSave(); + if (!needSave) + { + return true; + } + + Object[] options = null; + if (model.isReadOnly()) + { + options = new Object[] + { + "Save As", "Discard Changes", "Cancel" + }; + } else + { + options = new Object[] + { + "Save", "Save As", "Discard Changes", "Cancel" + }; + } + int n = JOptionPane.showOptionDialog( + this, + "The current data set has unsaved changes that will be lost.\n" + + "Would you like to save them before continuing?", + "Save Before Closing?", + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + null, + options, + model.isReadOnly() ? "Save As" : "Save"); + Object result = options[n]; + if ("Discard Changes".equals(result)) + { + return true; + } + if ("Cancel".equals(result)) + { + return false; + } + if ("Save".equals(result)) + { + return save(model.getDbFile()); + } + + // we are now at "save as..." + return saveAs(); + } + + public boolean saveAs() + { + File current = fileChooser.getSelectedFile(); + if (current != null) + { + fileChooser.setSelectedFile(new File(current.getAbsolutePath() + " (copy)")); + } + int retval = fileChooser.showSaveDialog(this); + if (retval == fileChooser.APPROVE_OPTION) + { + String fileName = fileChooser.getSelectedFile().getAbsolutePath(); + model.setReadOnly(false); + save.setEnabled(true); + return save(fileName); + } else + { + return false; + } + } + + public void showUserError(Object o) + { + JOptionPane.showMessageDialog(this, + o, + "A problem occurred", + JOptionPane.ERROR_MESSAGE); + if (o instanceof Exception) + { + ((Exception) o).printStackTrace(System.out); + } + } + + public void noteFileUse(String file) + { + + state.setCurrentFile(new File(file)); + state.save(); + makeRecentFileMenu(); + + } + + public void clearFilters() + { + filterControlPanel.clearFilters(); + } + + static String[] getVisibleFiles(String dir) + { + String[] s = new File(dir).list(); + ArrayList<String> real = new ArrayList<String>(); + for (int i = 0; i < s.length; i++) + { + if (!s[i].startsWith(".")) + { + real.add(s[i]); + } + } + return (String[]) real.toArray(new String[0]); + } +} diff --git a/timeflow/app/TimeflowAppLauncher.java b/timeflow/app/TimeflowAppLauncher.java new file mode 100755 index 0000000..f034666 --- /dev/null +++ b/timeflow/app/TimeflowAppLauncher.java @@ -0,0 +1,52 @@ +package timeflow.app; + +import timeflow.model.*; + +import javax.swing.*; +import java.awt.event.*; + +// For some reason we have to do this in a separate class in order to +// get the menubar working right on the Mac. + +public class TimeflowAppLauncher { + public static void main(String[] args) throws Exception + { + System.setProperty("apple.laf.useScreenMenuBar", "true"); + System.setProperty("com.apple.mrj.application.apple.menu.about.name", "TimeFlow"); + System.out.println("Running "+Display.version()); + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch (Exception e) { + System.out.println("Can't set system look & feel"); + } + + final TimeflowApp t=new TimeflowApp(); + t.splash=new AboutWindow(t, t.model.getDisplay()); + t.splash(true); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + try + { + t.init(); + t.setVisible(true); + } + catch (Exception e) + { + e.printStackTrace(System.out); + } + t.splash.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent e) { + t.splash.setVisible(false); + }} + ); + t.splash(false); + //t.splash.message=t.model.getDisplay().version(); + }}); + + } +} diff --git a/timeflow/app/actions/AddFieldAction.java b/timeflow/app/actions/AddFieldAction.java new file mode 100755 index 0000000..5af658e --- /dev/null +++ b/timeflow/app/actions/AddFieldAction.java @@ -0,0 +1,49 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.TimeflowApp; +import timeflow.app.ui.*; +import timeflow.data.db.*; +import timeflow.format.field.FieldFormatCatalog; + +import java.awt.event.*; +import javax.swing.*; +import java.util.*; + +public class AddFieldAction extends TimeflowAction { + + public AddFieldAction(TimeflowApp app) + { + super(app, "Add Field...", null, "Add a field to this database"); + } + + @Override + public void actionPerformed(ActionEvent e) { + AddFieldPanel p=new AddFieldPanel(); + Object[] options = {"Cancel", "Add Field"}; + int n = JOptionPane.showOptionDialog(app, + p, + "Add New Field To Database", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE, + null, + options, + "Add Field"); + if (n==1) + { + String fieldName=p.name.getText(); + TFModel model=getModel(); + if (fieldName.trim().length()==0) + app.showUserError("Field names can't be all spaces!"); + else if (model.getDB().getField(fieldName)!=null) + app.showUserError("That name is already taken!"); + else + { + model.getDB().addField(fieldName, FieldFormatCatalog.javaClass((String)p.typeChoices.getSelectedItem())); + model.noteAddField(this); + } + } + else + System.out.println("Canceled!"); + } +} diff --git a/timeflow/app/actions/AddRecordAction.java b/timeflow/app/actions/AddRecordAction.java new file mode 100755 index 0000000..c295e6b --- /dev/null +++ b/timeflow/app/actions/AddRecordAction.java @@ -0,0 +1,26 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.TimeflowApp; +import timeflow.app.ui.*; +import timeflow.data.db.*; +import timeflow.format.field.FieldFormatCatalog; + +import java.awt.Toolkit; +import java.awt.event.*; +import javax.swing.*; +import java.util.*; + +public class AddRecordAction extends TimeflowAction { + + public AddRecordAction(TimeflowApp app) + { + super(app, "Add Record...", null, "Add a record to this database"); + accelerate('A'); + } + + @Override + public void actionPerformed(ActionEvent e) { + EditRecordPanel.add(getModel()); + } +} diff --git a/timeflow/app/actions/CopySchemaAction.java b/timeflow/app/actions/CopySchemaAction.java new file mode 100755 index 0000000..30023b7 --- /dev/null +++ b/timeflow/app/actions/CopySchemaAction.java @@ -0,0 +1,29 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.TimeflowApp; +import timeflow.app.ui.*; +import timeflow.data.db.*; +import timeflow.format.field.FieldFormatCatalog; + +import java.awt.event.*; +import javax.swing.*; +import java.util.*; + +public class CopySchemaAction extends TimeflowAction { + + public CopySchemaAction(TimeflowApp app) + { + super(app, "New With Same Fields", null, + "Create a new, blank database with same fields as the current one."); + } + + public void actionPerformed(ActionEvent e) + { + java.util.List<Field> fields=getModel().getDB().getFields(); + ActDB db=new BasicDB("Unspecified"); + for (Field f: fields) + db.addField(f.getName(), f.getType()); + getModel().setDB(db, "[new data]", true, this); + } +} diff --git a/timeflow/app/actions/DateFieldAction.java b/timeflow/app/actions/DateFieldAction.java new file mode 100755 index 0000000..b7e8496 --- /dev/null +++ b/timeflow/app/actions/DateFieldAction.java @@ -0,0 +1,24 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.TimeflowApp; +import timeflow.app.ui.*; +import timeflow.data.db.*; +import timeflow.format.field.FieldFormatCatalog; + +import java.awt.event.*; +import javax.swing.*; +import java.util.*; + +public class DateFieldAction extends TimeflowAction { + + public DateFieldAction(TimeflowApp app) + { + super(app, "Set Date Fields...", null, "Set date fields corresponding to start, end."); + } + + @Override + public void actionPerformed(ActionEvent e) { + DateFieldPanel.popWindow(app.model); + } +} diff --git a/timeflow/app/actions/DeleteFieldAction.java b/timeflow/app/actions/DeleteFieldAction.java new file mode 100755 index 0000000..c7e60cc --- /dev/null +++ b/timeflow/app/actions/DeleteFieldAction.java @@ -0,0 +1,43 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.TimeflowApp; +import timeflow.app.ui.*; +import timeflow.data.db.*; + +import java.awt.event.*; +import javax.swing.*; +import java.util.*; + +public class DeleteFieldAction extends TimeflowAction { + + public DeleteFieldAction(TimeflowApp app) + { + super(app, "Delete Field...", null, "Delete a field from this database"); + } + + @Override + public void actionPerformed(ActionEvent e) { + ArrayList<String> options=new ArrayList<String>(); + for (Field f: getModel().getDB().getFields()) + options.add(f.getName()); + String[] o=(String[])options.toArray(new String[0]); + String fieldToDelete = (String)JOptionPane.showInputDialog( + app, + "Field to delete:", + "Delete Field", + JOptionPane.PLAIN_MESSAGE, + null, + o, + o[0]); + + if (fieldToDelete!=null) + { + TFModel model=getModel(); + Field f=model.getDB().getField(fieldToDelete); + model.getDB().deleteField(f); + model.noteSchemaChange(this); + return; + } + } +} diff --git a/timeflow/app/actions/DeleteSelectedAction.java b/timeflow/app/actions/DeleteSelectedAction.java new file mode 100755 index 0000000..824ac9b --- /dev/null +++ b/timeflow/app/actions/DeleteSelectedAction.java @@ -0,0 +1,51 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.ui.*; +import timeflow.app.*; +import timeflow.data.db.*; + +import java.awt.event.*; +import javax.swing.*; +import java.util.*; + + +public class DeleteSelectedAction extends TimeflowAction { + + public DeleteSelectedAction(TimeflowApp app) + { + super(app, "Delete Selected Items...", null, "Delete the currently visible events"); + } + + @Override + public void actionPerformed(ActionEvent e) { + + HashSet<Act> keepers=new HashSet<Act>(); // switching between sets and lists + keepers.addAll(getModel().getDB().all()); // for efficiency. maybe silly? + ActList selected=getModel().getActs(); + for (Act a: selected) + keepers.remove(a); + ActList keepList=new ActList(getModel().getDB()); + keepList.addAll(keepers); + + MassDeletePanel panel=new MassDeletePanel(getModel(), keepList, + "Delete all selected items."); + Object[] options = {"Cancel", "Proceed"}; + int n = JOptionPane.showOptionDialog(app, + panel, + "Delete Selected", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + options, + "Proceed"); + panel.detachFromModel(); + if (n==1) + { + panel.applyAction(); + app.clearFilters(); + getModel().noteSchemaChange(this); + } + } + +} diff --git a/timeflow/app/actions/DeleteUnselectedAction.java b/timeflow/app/actions/DeleteUnselectedAction.java new file mode 100755 index 0000000..638f3f6 --- /dev/null +++ b/timeflow/app/actions/DeleteUnselectedAction.java @@ -0,0 +1,40 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.ui.*; +import timeflow.app.*; + +import java.awt.event.*; +import javax.swing.*; + + +public class DeleteUnselectedAction extends TimeflowAction { + + public DeleteUnselectedAction(TimeflowApp app) + { + super(app, "Delete Unselected Items...", null, "Delete all but the currently visible events"); + } + + @Override + public void actionPerformed(ActionEvent e) { + MassDeletePanel panel=new MassDeletePanel(getModel(), getModel().getActs(), + "Delete unselected items."); + Object[] options = {"Cancel", "Proceed"}; + int n = JOptionPane.showOptionDialog(app, + panel, + "Delete Unselected", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + options, + "Proceed"); + panel.detachFromModel(); + if (n==1) + { + panel.applyAction(); + app.clearFilters(); + getModel().noteSchemaChange(this); + } + } + +} diff --git a/timeflow/app/actions/EditSourceAction.java b/timeflow/app/actions/EditSourceAction.java new file mode 100755 index 0000000..eec0280 --- /dev/null +++ b/timeflow/app/actions/EditSourceAction.java @@ -0,0 +1,37 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.TimeflowApp; +import timeflow.app.ui.*; + +import java.awt.event.*; +import javax.swing.*; + + +public class EditSourceAction extends TimeflowAction { + + public EditSourceAction(TimeflowApp app) + { + super(app, "Edit Source/Credit Line...", null, "Edit credit line for this database"); + } + + @Override + public void actionPerformed(ActionEvent e) { + TFModel model=getModel(); + String source = (String)JOptionPane.showInputDialog( + app, + null, + "Edit Source/Credit Line", + JOptionPane.PLAIN_MESSAGE, + null, + null, + model.getDB().getSource()); + + if (source!=null) { + model.getDB().setSource(source); + model.noteNewSource(this); + return; + } + } + +} diff --git a/timeflow/app/actions/ImportFromPasteAction.java b/timeflow/app/actions/ImportFromPasteAction.java new file mode 100755 index 0000000..9b6d249 --- /dev/null +++ b/timeflow/app/actions/ImportFromPasteAction.java @@ -0,0 +1,53 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.TimeflowApp; +import timeflow.app.ui.*; +import timeflow.data.db.*; +import timeflow.format.field.FieldFormatCatalog; +import timeflow.format.file.DelimitedFormat; + +import java.awt.event.*; +import javax.swing.*; +import java.util.*; + +public class ImportFromPasteAction extends TimeflowAction { + + public ImportFromPasteAction(TimeflowApp app) + { + super(app, "Paste From Spreadsheet / HTML...", null, "Import from copy-and-pasted data."); + } + + public void actionPerformed(ActionEvent event) + { + if (!app.checkSaveStatus()) + return; + JTextArea text=new JTextArea(10,40); + JScrollPane scroll=new JScrollPane(text); + text.setText("Paste here! (replacing this :-)"); + text.setSelectionStart(0); + text.setSelectionEnd(text.getText().length()); + Object[] options = {"Cancel", "Import"}; + int n = JOptionPane.showOptionDialog(app, + scroll, + "Import From Paste", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE, + null, + options, + "Import"); + if (n==1) + { + try + { + String pasted=text.getText(); + String[][] data=DelimitedFormat.readArrayFromString(pasted, System.out); + app.showImportEditor("Paste", data); + } + catch (Exception e) + { + app.showUserError(e); + } + } + } +} diff --git a/timeflow/app/actions/NewDataAction.java b/timeflow/app/actions/NewDataAction.java new file mode 100755 index 0000000..357fb60 --- /dev/null +++ b/timeflow/app/actions/NewDataAction.java @@ -0,0 +1,27 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.TimeflowApp; +import timeflow.app.ui.*; +import timeflow.data.db.*; +import timeflow.format.field.FieldFormatCatalog; + +import java.awt.event.*; +import javax.swing.*; +import java.util.*; + +public class NewDataAction extends TimeflowAction { + + public NewDataAction(TimeflowApp app) + { + super(app, "New", null, "Create a new, blank database"); + accelerate('N'); + + } + + public void actionPerformed(ActionEvent e) + { + if (app.checkSaveStatus()) + getModel().setDB(new BasicDB("Unspecified"), "[new data]", true, this); + } +} diff --git a/timeflow/app/actions/QuitAction.java b/timeflow/app/actions/QuitAction.java new file mode 100755 index 0000000..bdd6726 --- /dev/null +++ b/timeflow/app/actions/QuitAction.java @@ -0,0 +1,32 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.TimeflowApp; +import timeflow.app.ui.*; +import timeflow.data.db.*; +import timeflow.format.field.FieldFormatCatalog; + +import java.awt.event.*; +import javax.swing.*; +import java.util.*; + +public class QuitAction extends TimeflowAction { + + public QuitAction(TimeflowApp app, TFModel model) + { + super(app, "Quit", null, "Quit the program"); + } + + @Override + public void actionPerformed(ActionEvent e) { + quit(); + } + + public void quit() + { + if (app.checkSaveStatus()) + { + System.exit(0); + } + } +} diff --git a/timeflow/app/actions/RenameFieldAction.java b/timeflow/app/actions/RenameFieldAction.java new file mode 100755 index 0000000..1fcc073 --- /dev/null +++ b/timeflow/app/actions/RenameFieldAction.java @@ -0,0 +1,100 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.ui.*; +import timeflow.app.*; +import timeflow.data.db.*; + +import java.awt.*; +import java.awt.event.*; + +import javax.swing.*; + +import java.util.*; + +public class RenameFieldAction extends TimeflowAction { + + public RenameFieldAction(TimeflowApp app) + { + super(app, "Rename Field...", null, "Rename a field from this database"); + } + + @Override + public void actionPerformed(ActionEvent e) { + JPanel panel=new JPanel(); + panel.setLayout(new GridLayout(4,1)); + panel.add(new JLabel("Choose a field and type a new name.")); + final JComboBox fieldChoices=new JComboBox(); + panel.add(fieldChoices); + ArrayList<String> options=new ArrayList<String>(); + for (Field f: getModel().getDB().getFields()) + fieldChoices.addItem(f.getName()); + JPanel inputPanel=new JPanel(); + inputPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + inputPanel.add(new JLabel("New Name:")); + final JTextField nameField=new JTextField(20); + inputPanel.add(nameField); + nameField.requestFocus(); + final JLabel feedback=new JLabel("(No name entered)"); + + nameField.addKeyListener(new KeyListener() { + @Override + public void keyPressed(KeyEvent e) { + // TODO Auto-generated method stub + + } + + @Override + public void keyReleased(KeyEvent e) { + String name=nameField.getText(); + Field other=getModel().getDB().getField(name); + //System.out.println("name="+name); + if (name.trim().length()==0) + { + feedback.setText("(No name entered)"); + } else if (other!=null && !other.getName().equals(fieldChoices.getSelectedItem())) + { + feedback.setText("A field named '"+name+"' already exists."); + } else + feedback.setText(""); + } + + @Override + public void keyTyped(KeyEvent e) { + // TODO Auto-generated method stub + + }}); + + panel.add(inputPanel); + feedback.setForeground(Color.gray); + panel.add(feedback); + + String[] o={"OK", "Cancel"}; + int n = JOptionPane.showOptionDialog( + app, + panel, + "Rename Field", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + o, + o[0]); + + if (n==0) + { + Field old=getModel().getDB().getField((String)fieldChoices.getSelectedItem()); + String newName=nameField.getText(); + Field conflict=getModel().getDB().getField(newName); + boolean tooSpacey=newName.trim().length()==0; + if (tooSpacey) + app.showUserError("Can't change the field name to be empty."); + else if (conflict!=null && conflict!=old) + app.showUserError("A field named '"+newName+"' already exists."); + else + { + getModel().getDB().renameField(old, nameField.getText()); + getModel().noteSchemaChange(this); + } + } + } +} diff --git a/timeflow/app/actions/ReorderFieldsAction.java b/timeflow/app/actions/ReorderFieldsAction.java new file mode 100755 index 0000000..e291a2a --- /dev/null +++ b/timeflow/app/actions/ReorderFieldsAction.java @@ -0,0 +1,38 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.ui.*; +import timeflow.app.*; + +import java.awt.event.*; +import javax.swing.*; + + +public class ReorderFieldsAction extends TimeflowAction { + + public ReorderFieldsAction(TimeflowApp app) + { + super(app, "Reorder Fields...", null, "Edit the order of fields"); + } + + @Override + public void actionPerformed(ActionEvent e) { + ReorderFieldsPanel panel=new ReorderFieldsPanel(getModel()); + Object[] options = {"Cancel", "Apply"}; + int n = JOptionPane.showOptionDialog(app, + panel, + "Reorder Fields", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + options, + "Apply"); + panel.detachFromModel(); + if (n==1) + { + panel.applyReordering(); + getModel().noteSchemaChange(this); + } + } + +} diff --git a/timeflow/app/actions/TimeflowAction.java b/timeflow/app/actions/TimeflowAction.java new file mode 100755 index 0000000..aae5d23 --- /dev/null +++ b/timeflow/app/actions/TimeflowAction.java @@ -0,0 +1,36 @@ +package timeflow.app.actions; + +import timeflow.model.*; +import timeflow.app.*; +import timeflow.format.file.*; + +import javax.swing.*; + +import java.awt.Toolkit; +import java.io.*; + +public abstract class TimeflowAction extends AbstractAction { + TimeflowApp app; + + public TimeflowAction(TimeflowApp app, String text, ImageIcon icon, String desc) + { + super(text, icon); + this.app=app; + putValue(SHORT_DESCRIPTION, desc); + } + + + protected void accelerate(char c) + { + putValue(Action.ACCELERATOR_KEY,KeyStroke.getKeyStroke(c, + Toolkit.getDefaultToolkit( ).getMenuShortcutKeyMask( ), false)); + } + + + protected TFModel getModel() + { + return app.model; + } + + +} diff --git a/timeflow/app/actions/WebDocAction.java b/timeflow/app/actions/WebDocAction.java new file mode 100755 index 0000000..08d8c84 --- /dev/null +++ b/timeflow/app/actions/WebDocAction.java @@ -0,0 +1,25 @@ +package timeflow.app.actions; + +import java.awt.Toolkit; +import java.awt.event.ActionEvent; + +import javax.swing.Action; +import javax.swing.JOptionPane; +import javax.swing.KeyStroke; + +import timeflow.app.TimeflowApp; +import timeflow.app.ui.ReorderFieldsPanel; +import timeflow.model.Display; + +public class WebDocAction extends TimeflowAction { + public WebDocAction(TimeflowApp app) + { + super(app, "Documentation & License Info...", null, "Read web documentation."); + } + + @Override + public void actionPerformed(ActionEvent e) { + Display.launchBrowser("http://wiki.github.com/FlowingMedia/TimeFlow/"); + } + +} diff --git a/timeflow/app/ui/AddFieldPanel.java b/timeflow/app/ui/AddFieldPanel.java new file mode 100755 index 0000000..fc0d4df --- /dev/null +++ b/timeflow/app/ui/AddFieldPanel.java @@ -0,0 +1,24 @@ +package timeflow.app.ui; + +import timeflow.app.*; +import timeflow.format.field.FieldFormatCatalog; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + + +public class AddFieldPanel extends JPanel { + public JTextField name=new JTextField(12); + public JComboBox typeChoices=new JComboBox(); + public AddFieldPanel() + { + for (String choice: FieldFormatCatalog.classNames()) + typeChoices.addItem(choice); + setLayout(new GridLayout(2,2)); + add(new JLabel("Field Name")); + add(name); + add(new JLabel("Field Type")); + add(typeChoices); + } +} diff --git a/timeflow/app/ui/ColorLegendPanel.java b/timeflow/app/ui/ColorLegendPanel.java new file mode 100755 index 0000000..e8552a0 --- /dev/null +++ b/timeflow/app/ui/ColorLegendPanel.java @@ -0,0 +1,63 @@ +package timeflow.app.ui; + +import timeflow.model.*; +import timeflow.app.ui.filter.FilterCategoryPanel; +import timeflow.data.db.*; +import timeflow.data.db.filter.FieldValueFilter; +import timeflow.data.db.filter.ValueFilter; +import timeflow.data.time.*; + +import timeflow.util.*; + +import java.awt.*; + +import javax.swing.JLabel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +public class ColorLegendPanel extends ModelPanel { + + + Field oldColor; + + public ColorLegendPanel(TFModel model) + { + super(model); + setBackground(Color.white); + setLayout(new GridLayout(1,1)); + } + + @Override + public void note(TFEvent e) { + Field color=getModel().getColorField(); + if (color!=null && color!=oldColor) + { + removeAll(); + final FilterCategoryPanel p=new FilterCategoryPanel("Color Legend: '"+color.getName()+"'", + color, this); + add(p); + Bag<String> data=DBUtils.countValues(getModel().getDB().all(), color); + p.setData(data); + p.dataList.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + ValueFilter f=(ValueFilter)p.defineFilter(); + getModel().setGrayFilter(f, this); + } + }); + + oldColor=color; + revalidate(); + return; + } else if (color==null) + { + removeAll(); + } + repaint(); + } + + public Dimension getPreferredSize() + { + return new Dimension(200,400); + } +} diff --git a/timeflow/app/ui/ComponentCluster.java b/timeflow/app/ui/ComponentCluster.java new file mode 100755 index 0000000..d32b3b6 --- /dev/null +++ b/timeflow/app/ui/ComponentCluster.java @@ -0,0 +1,37 @@ +package timeflow.app.ui; + +import javax.swing.*; +import java.awt.*; + +public class ComponentCluster extends JPanel +{ + int numComps=0; + int x1=80; + int width=200; + int compH=30; + DottedLine line=new DottedLine(); + + public ComponentCluster(String name) + { + setBackground(Color.white); + setLayout(null); + JLabel label=new JLabel(name); + add(label); + label.setBounds(3,3,50,30); + add(line); + } + + public void addContent(JComponent c) + { + add(c); + c.setBorder(null); + c.setBounds(x1,10+numComps*compH, c.getPreferredSize().width, c.getPreferredSize().height); + numComps++; + line.setBounds(x1-10,10,1,numComps*compH-5); + } + + public Dimension getPreferredSize() + { + return new Dimension(width, 20+compH*numComps); + } +} diff --git a/timeflow/app/ui/DateFieldPanel.java b/timeflow/app/ui/DateFieldPanel.java new file mode 100755 index 0000000..f36e1f3 --- /dev/null +++ b/timeflow/app/ui/DateFieldPanel.java @@ -0,0 +1,178 @@ +package timeflow.app.ui; + +import timeflow.model.*; +import timeflow.data.time.*; +import timeflow.data.db.*; +import timeflow.data.db.filter.*; +import timeflow.format.field.*; +import timeflow.format.file.TimeflowFormat; + +import javax.swing.*; +import java.util.*; +import java.awt.*; +import java.awt.event.*; + + +public class DateFieldPanel extends JPanel +{ + TFModel model; + int numRows; + HashMap<String, Integer> numBad=new HashMap<String, Integer>(); + private static String[] mappable={VirtualField.START, VirtualField.END}; + JLabel status=new JLabel(""); + FieldMap[] panels=new FieldMap[mappable.length]; + JButton submit, cancel; + + public DateFieldPanel(TFModel model, boolean hasButtons) + { + this.model=model; + + ActDB db=model.getDB(); + numRows=db.size(); + ActList all=db.all(); + + + // calculate stats. + for (Field f: db.getFields(RoughTime.class)) + { + int bad=DBUtils.count(all, new MissingValueFilter(f)); + numBad.put(f.getName(),bad); + } + + setLayout(new BorderLayout()); + JPanel top=new JPanel(); + if (hasButtons) + { + submit=new JButton("Submit"); + top.add(submit); + cancel=new JButton("Cancel"); + top.add(cancel); + } + else + { + JLabel about=new JLabel("Dates"); + top.add(about); + } + top.add(status); + status.setForeground(Color.red); + add(top, BorderLayout.SOUTH); + JPanel bottom=new JPanel(); + add(bottom, BorderLayout.CENTER); + bottom.setLayout(new GridLayout(mappable.length,1)); + + // add panels. + for (int i=0; i<mappable.length; i++) + { + panels[i]=new FieldMap(mappable[i],i==0); + bottom.add(panels[i]); + } + + + // to do: add a status field or something + // note inconsistencies, like: + // * no start defined. + // * ends after starts + } + + public void map() + { + ActDB db=model.getDB(); + for (int i=0; i<panels.length; i++) + { + String choice=(String)panels[i].choices.getSelectedItem(); + db.setAlias("None".equals(choice) ? null : db.getField(choice), panels[i].name); + } + model.noteSchemaChange(this); + } + + class FieldMap extends JPanel + { + String name; + int bad; + JComboBox choices; + JLabel definedLabel=new JLabel("# def goes here"); + boolean important; + + FieldMap(String name, boolean important) + { + this.name=name; + this.important=important; + setBackground(Color.white); + setLayout(new GridLayout(1,3)); + + add(new JLabel(" "+(important? "* ":"")+VirtualField.humanName(name)));//, BorderLayout.NORTH); + + choices=new JComboBox(); + choices.addItem("None"); + for (Field f: model.getDB().getFields(RoughTime.class)) + { + choices.addItem(f.getName());//+" "+percentDef+"% defined"); + } + add(choices); + choices.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + showDefined(); + }}); + + add(definedLabel); + definedLabel.setForeground(Color.gray); + + Field current=model.getDB().getField(name); + if (current!=null) + choices.setSelectedItem(current.getName()); + + showDefined(); + } + + void showDefined() + { + String choice=(String)choices.getSelectedItem(); + String val=""; + boolean none="None".equals(choice); + int percentDef=0; + if (!none) + { + percentDef=(int)(100*(1-numBad.get(choice)/(double)numRows)); + val=" "+percentDef+"% defined"; + } + definedLabel.setText(val); + if (important) + { + if (none) + status.setText(" Need \"Start\" for timeline/calendar."); + else if (percentDef==0) + status.setText(" No dates defined in "+choice+"."); + else + status.setText(""); + } + } + } + + public Dimension getPreferredSize() + { + return new Dimension(400,80+mappable.length*25); + } + + public static void popWindow(TFModel model) + { + final JFrame window=new JFrame("Date Fields"); + window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + window.getContentPane().setLayout(new GridLayout(1,1)); + final DateFieldPanel p=new DateFieldPanel(model, true); + p.submit.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + p.map(); + window.setVisible(false); + }}); + p.cancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + window.setVisible(false); + }}); + window.getContentPane().add(p); + window.setBounds(50,50,window.getPreferredSize().width,window.getPreferredSize().height); + window.setVisible(true); + } +} diff --git a/timeflow/app/ui/DottedLine.java b/timeflow/app/ui/DottedLine.java new file mode 100755 index 0000000..8713d40 --- /dev/null +++ b/timeflow/app/ui/DottedLine.java @@ -0,0 +1,34 @@ +package timeflow.app.ui; + +import javax.swing.*; +import java.awt.*; + +public class DottedLine extends JPanel +{ + public void paintComponent(Graphics g) + { + int w=getSize().width, h=getSize().height; + g.setColor(Color.white); + g.fillRect(0,0,w,h); + g.setColor(Color.lightGray); + if (w>h) + { + for (int x=0; x<w; x+=4) + { + g.drawLine(x,0,x+1,0); + } + } + else + { + for (int y=0; y<h; y+=4) + { + g.drawLine(0,y,0,y+1); + } + } + } + + public Dimension getPreferredSize() + { + return new Dimension(1,1); + } +} diff --git a/timeflow/app/ui/EditRecordPanel.java b/timeflow/app/ui/EditRecordPanel.java new file mode 100755 index 0000000..08d7aec --- /dev/null +++ b/timeflow/app/ui/EditRecordPanel.java @@ -0,0 +1,134 @@ +package timeflow.app.ui; + +import timeflow.model.*; +import timeflow.app.ui.ImportDelimitedPanel.SchemaPanel; +import timeflow.data.time.*; +import timeflow.data.db.*; + +import javax.swing.*; + +import java.util.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +// panel with form for editing a given database entry +public class EditRecordPanel extends JPanel +{ + + Act act; + HashMap<Field, EditValuePanel> fieldUI = new HashMap<Field, EditValuePanel>(); + JButton submit, cancel; + Dimension idealSize = new Dimension(); + TFModel model; + + private static void edit(final TFModel model, final Act act, final boolean isAdd) + { + final JFrame window = new JFrame(isAdd ? "Add Record" : "Edit Record"); + window.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + final EditRecordPanel editor = new EditRecordPanel(model, act); + window.getContentPane().setLayout(new GridLayout(1, 1)); + window.getContentPane().add(editor); + editor.submit.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + window.setVisible(false); + editor.submitValues(); + model.noteAdd(this); + } + }); + editor.cancel.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + window.setVisible(false); + if (isAdd) + { + model.getDB().delete(act); + } + } + }); + window.setBounds(50, 50, 700, 500); + window.pack(); + window.setVisible(true); + } + + public static void edit(TFModel model, Act act) + { + edit(model, act, false); + } + + public static void add(TFModel model) + { + Act act = model.getDB().createAct(); + edit(model, act, true); + } + + public static void add(TFModel model, RoughTime r) + { + Act act = model.getDB().createAct(); + act.set(act.getDB().getField(VirtualField.START), r); + edit(model, act, true); + } + + public EditRecordPanel(TFModel model, Act act) + { + this.model = model; + this.act = act; + + setBackground(Color.white); + setLayout(new BorderLayout()); + + JPanel buttons = new JPanel(); + add(buttons, BorderLayout.SOUTH); + buttons.setBackground(Color.lightGray); + submit = new JButton("OK"); + buttons.add(submit); + cancel = new JButton("Cancel"); + buttons.add(cancel); + + JPanel entryPanel = new JPanel(); + JScrollPane scroller = new JScrollPane(entryPanel); + add(scroller, BorderLayout.CENTER); + + java.util.List<Field> fields = act.getDB().getFields(); + int n = fields.size(); + entryPanel.setLayout(null); + + DBUtils.setRecSizesFromCurrent(act.getDB()); + int top = 0; + + for (Field f : fields) + { + EditValuePanel p = new EditValuePanel(f.getName(), act.get(f), + f.getType(), f.getRecommendedSize() > 100); + entryPanel.add(p); + Dimension d = p.getPreferredSize(); + p.setBounds(0, top, d.width, d.height); + top += d.height; + idealSize.width = Math.max(d.width + 5, idealSize.width); + idealSize.height = Math.max(top + 45, idealSize.height); + fieldUI.put(f, p); + } + + } + + public Dimension getPreferredSize() + { + return idealSize; + } + + public void submitValues() + { + for (Field f : fieldUI.keySet()) + { + act.set(f, fieldUI.get(f).getInputValue()); + } + model.noteRecordChange(this); + } +} diff --git a/timeflow/app/ui/EditValuePanel.java b/timeflow/app/ui/EditValuePanel.java new file mode 100755 index 0000000..2f148d8 --- /dev/null +++ b/timeflow/app/ui/EditValuePanel.java @@ -0,0 +1,111 @@ +package timeflow.app.ui; + +import timeflow.format.field.*; + +import javax.swing.*; +import javax.swing.text.JTextComponent; + +import java.awt.*; +import java.awt.event.*; + +public class EditValuePanel extends JPanel +{ + + FieldFormat parser; + boolean longField; + JLabel feedback = new JLabel() + { + + public Dimension getPreferredSize() + { + Dimension d = super.getPreferredSize(); + return new Dimension(200, d.height); + } + }; + static final String space = " "; + JTextComponent input; + + public EditValuePanel(String name, Object startValue, Class type, boolean longField) + { + parser = FieldFormatCatalog.getFormat(type); + + if (longField) + { + setLayout(new BorderLayout()); + JPanel top = new JPanel(); + top.setLayout(new GridLayout(2, 2)); + top.add(new JPanel()); + top.add(new JPanel()); + JLabel fieldLabel = new JLabel(space + name + " (long)"); + top.add(fieldLabel); + top.add(feedback); + add(top, BorderLayout.NORTH); + input = new JTextArea(5, 60); + ((JTextArea) input).setLineWrap(true); + ((JTextArea) input).setWrapStyleWord(true); + JScrollPane scroller = new JScrollPane(input); + scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + add(scroller, BorderLayout.CENTER); + add(new JPanel(), BorderLayout.WEST); + add(new JPanel(), BorderLayout.SOUTH); + } else + { + setLayout(new GridLayout(1, 4)); + JLabel fieldLabel = new JLabel(space + name); + add(fieldLabel); + JLabel typeLabel = new JLabel(FieldFormatCatalog.humanName(type)); + add(typeLabel); + typeLabel.setForeground(Color.gray); + input = new JTextField(8); + add(input); + // enough room for "couldn't understand" + add(feedback); + } + input.setText(startValue == null ? "" : parser.format(startValue)); + input.addKeyListener(new KeyAdapter() + { + + @Override + public void keyReleased(KeyEvent e) + { + parse(); + } + }); + parse(); + } + + void parse() + { + try + { + parser.parse(input.getText()); + } catch (Exception e) + { + } + feedback.setText(" " + parser.feedback()); + feedback.setForeground(parser.isUnderstood() ? Color.gray : Color.red); + } + + public Object getInputValue() + { + try + { + return parser.parse(input.getText()); + } catch (Exception e) + { + return null; + } + } + + public boolean isOK() + { + return parser.isUnderstood(); + } + + public Dimension getPreferredSize() + { + Dimension d = super.getPreferredSize(); + int w = Math.max(300, d.width); + return new Dimension(w, d.height); + } +} diff --git a/timeflow/app/ui/GlobalDisplayPanel.java b/timeflow/app/ui/GlobalDisplayPanel.java new file mode 100755 index 0000000..59f4c8a --- /dev/null +++ b/timeflow/app/ui/GlobalDisplayPanel.java @@ -0,0 +1,174 @@ +package timeflow.app.ui; + +import timeflow.app.ui.filter.FilterControlPanel; +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.model.*; + +import javax.swing.*; + +import timeflow.util.*; + +import java.awt.*; +import java.awt.event.*; +import java.util.List; + +public class GlobalDisplayPanel extends ModelPanel +{ + + JPanel encodings = new JPanel(); + JPanel localControls = new JPanel(); + JPanel globalControls = new JPanel(); + CardLayout localCards = new CardLayout(); + + public GlobalDisplayPanel(TFModel model, FilterControlPanel filterControls) + { + super(model); + setBackground(Color.white); + setLayout(new BorderLayout()); + + add(localControls, BorderLayout.CENTER); + localControls.setBackground(Color.white); + localControls.setLayout(localCards); + + JPanel p = new JPanel(); + p.setBackground(Color.white); + p.setLayout(new BorderLayout()); + + JPanel globalLabel = new JPanel(); + globalLabel.setLayout(new BorderLayout()); + + JPanel topLine = new Pad(2, 3); + topLine.setBackground(Color.gray); + globalLabel.add(topLine, BorderLayout.NORTH); + + JPanel bottomLine = new Pad(2, 3); + bottomLine.setBackground(Color.gray); + globalLabel.add(bottomLine, BorderLayout.SOUTH); + + JLabel label = new JLabel(" Global Controls", JLabel.LEFT) + { + + public Dimension getPreferredSize() + { + return new Dimension(30, 30); + } + }; + label.setBackground(Color.lightGray); + label.setForeground(Color.darkGray); + globalLabel.add(label, BorderLayout.CENTER); + p.add(globalLabel, BorderLayout.NORTH); + + JPanel global = new JPanel(); + global.setLayout(new BorderLayout()); + global.add(new StatusPanel(model, filterControls), BorderLayout.NORTH); + + encodings.setLayout(new GridLayout(4, 1)); + encodings.setBackground(Color.white); + global.add(encodings, BorderLayout.CENTER); + p.add(global, BorderLayout.CENTER); + add(p, BorderLayout.SOUTH); + + makeEncodingPanel(); + } + + public void showLocalControl(String name) + { + localCards.show(localControls, name); + } + + public void addLocalControl(String name, JComponent control) + { + localControls.add(control, name); + } + + void makeEncodingPanel() + { + encodings.removeAll(); + ActDB db = getModel().getDB(); + if (db == null) + { + return; + } + + java.util.List<Field> dimensions = DBUtils.categoryFields(db); + java.util.List<Field> measures = db.getFields(Double.class); + java.util.List<Field> fields = db.getFields(); + + makeChooser(VirtualField.LABEL, "Label", "None", fields); //db.getFields()); // String.class)); + makeChooser(VirtualField.TRACK, "Groups", "None", dimensions); + makeChooser(VirtualField.COLOR, "Color", "Same As Groups", dimensions); + + makeChooser(VirtualField.SIZE, "Dot Size", "None", measures); + } + + private JComboBox makeChooser(final String alias, String title, String nothingLabel, List<Field> fields) + { + if (fields.size() == 0) + { + return null; + } + JPanel panel = new JPanel(); + panel.setBackground(Color.white); + panel.setLayout(new BorderLayout()); + JPanel topPad = new Pad(10, 7); + topPad.setBackground(Color.white); + panel.add(topPad, BorderLayout.NORTH); + + JPanel rightPad = new Pad(10, 10); + panel.add(rightPad, BorderLayout.EAST); + rightPad.setBackground(Color.white); + + panel.add(new JLabel(" " + title) + { + + public Dimension getPreferredSize() + { + return new Dimension(60, 25); + } + }, + BorderLayout.WEST); + final JComboBox c = new JComboBox(); + + if (nothingLabel != null) + { + c.addItem(nothingLabel); + } + for (Field f : fields) + { + c.addItem(f.getName()); + } + + Field current = getModel().getDB().getField(alias); + if (current != null) + { + c.setSelectedItem(current.getName()); + } + c.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + Field realField = c.getSelectedIndex() == 0 + ? null : getModel().getDB().getField((String) c.getSelectedItem()); + getModel().setFieldAlias(realField, alias, GlobalDisplayPanel.this); + } + }); + c.setBackground(Color.white); + c.setBorder(null); + panel.add(c, BorderLayout.CENTER); + encodings.add(panel); + c.setBorder(null); + return c; + } + + @Override + public void note(TFEvent e) + { + if (e.affectsSchema()) + { + makeEncodingPanel(); + } + } +} diff --git a/timeflow/app/ui/HtmlDisplay.java b/timeflow/app/ui/HtmlDisplay.java new file mode 100755 index 0000000..c7f0f98 --- /dev/null +++ b/timeflow/app/ui/HtmlDisplay.java @@ -0,0 +1,24 @@ +package timeflow.app.ui; + +import java.awt.Font; + +import javax.swing.JEditorPane; +import javax.swing.UIManager; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.StyleSheet; + +public class HtmlDisplay { + public static JEditorPane create() + { + JEditorPane p = new JEditorPane(); + p.setEditable(false); + p.setContentType("text/html"); + + Font font = UIManager.getFont("Label.font"); + String bodyRule = "body { font-family: "+font.getFamily()+"; "+ + "font-size: " + font.getSize() + "pt; }"; + StyleSheet styles=((HTMLDocument)p.getDocument()).getStyleSheet(); + styles.addRule(bodyRule); + return p; + } +} diff --git a/timeflow/app/ui/ImportDelimitedPanel.java b/timeflow/app/ui/ImportDelimitedPanel.java new file mode 100755 index 0000000..c626219 --- /dev/null +++ b/timeflow/app/ui/ImportDelimitedPanel.java @@ -0,0 +1,310 @@ +package timeflow.app.ui; + +import timeflow.data.time.*; +import timeflow.data.db.*; +import timeflow.format.field.*; +import timeflow.format.file.DelimitedFormat; +import timeflow.model.*; + +import timeflow.util.*; + +import javax.swing.*; + +import java.awt.*; +import java.awt.event.*; +import java.text.ParseException; +import java.util.*; +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; + +public class ImportDelimitedPanel extends JFrame +{ + String fileName; + SchemaPanel schemaPanel; + boolean exitOnClose=false; // for testing! + JScrollPane scroller; + TFModel model; + JLabel numLinesLabel=new JLabel(); + + // for testing: + public static void main(String[] args) throws Exception + { + System.out.println("Starting test of ImportEditor"); + String file="data/probate.tsv"; + String[][] data=DelimitedFormat.readArrayGuessDelim(file, System.out); + ImportDelimitedPanel editor=new ImportDelimitedPanel(new TFModel()); + editor.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + editor.setFileName(file); + editor.setData(data); + editor.setBounds(50,50,900,800); + editor.setVisible(true); + editor.exitOnClose=true; + } + + public ImportDelimitedPanel(final TFModel model) + { + super("Import File"); + this.model=model; + setBackground(Color.white); + + setLayout(new BorderLayout()); + JPanel top=new JPanel(); + add(top, BorderLayout.NORTH); + top.setLayout(new FlowLayout(FlowLayout.LEFT)); + top.setBackground(Color.lightGray); + top.add(numLinesLabel); + final JTextField source=new JTextField(12); + + JButton done=new JButton("Import This"); + top.add(done); + done.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + model.setDB(schemaPanel.makeDB(source.getText()), fileName, true, this); + setVisible(false); + if (exitOnClose) + System.exit(0); + }}); + + JButton cancel=new JButton("Cancel"); + top.add(cancel); + cancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setVisible(false); + if (exitOnClose) + System.exit(0); + }}); + + top.add(new JLabel(" Enter A Source:")); + top.add(source); + schemaPanel=new SchemaPanel(); + schemaPanel.setBackground(Color.white); + scroller=new JScrollPane(schemaPanel); + add(scroller, BorderLayout.CENTER); + } + + public void scrollToTop() + { + scroller.getViewport().setViewPosition(new Point(0,0)); + } + + public void setFileName(String fileName) + { + this.fileName=fileName; + } + + public void setData(String[][] data) + { + numLinesLabel.setText((data.length-1)+" records read. "); + schemaPanel.display(data); + } + + class SchemaPanel extends JPanel + { + int numFields, rows; + String[][] data; + ArrayList<FieldPanel> panels=new ArrayList<FieldPanel>(); + + ActDB makeDB(String source) + { + // count number of fields that are not ignored. + int n=0; + for (FieldPanel fp: panels) + if (!fp.ignore.isSelected()) + n++; + + Class[] types=new Class[n]; + String[] fieldNames=new String[n]; + int[] index=new int[n]; + if (source.trim().length()==0) + source="[source unspecified]"; + int i=0, j=0; + for (FieldPanel fp: panels) + { + if (!fp.ignore.isSelected()) + { + fieldNames[i]=fp.fieldName; + String typeChoice=(String)fp.typeChoices.getSelectedItem(); + Class type=FieldFormatCatalog.javaClass(typeChoice); + System.out.println("Type: "+type+" for: "+typeChoice+" from "+fp.fieldName); + types[i]=type; + index[i]=j; + i++; + } + j++; + } + + ActDB db= new ArrayDB(fieldNames, types, source); + HashMap<Integer, StringBuffer> errors=new HashMap<Integer, StringBuffer>(); + for (i=1; i<data.length; i++) + { + Act act=db.createAct(); + for (int k=0; k<n; k++) + { + j=index[k]; + String s=data[i][j]; + Field f=db.getField(fieldNames[k]); + FieldFormat format=FieldFormatCatalog.getFormat(types[k]); + try + { + Object o=format.parse(s); + act.set(f,o); + } + catch (Exception e) + { + StringBuffer b=errors.get(i-1); + if (b==null) + { + b=new StringBuffer(); + errors.put(i-1,b); + } + else + b.append("; "); + b.append(f.getName()+":"+s); + } + } + } + + if (errors.size()>0) + { + Field error=db.addField("UNPARSED FIELDS", String.class); + for (int row:errors.keySet()) + { + db.get(row).set(error, errors.get(row).toString()); + } + } + + for (j=0; j<n; j++) + { + System.out.println(db.getField(fieldNames[j])); + } + + return db; + } + + void display(String[][] data) + { + removeAll(); + + this.data=data; + + // analyze data. + Class[] guesses=FieldFormatGuesser.analyze(data, 1, 100); + + // go through first row, which is headers. + String[] headers=data[0]; + + // if there are duplicate headers, add indicators. + HashSet<String> h=new HashSet<String>(); + for (int i=0; i<headers.length; i++) + { + String base=headers[i]; + int j=2; + String name=base; + while (h.contains(name)) + { + name=base+" "+j; + j++; + } + headers[i]=name; + h.add(name); + } + + numFields=headers.length; + int cols=2; + rows=(int)Math.ceil(numFields/2.0); + setLayout(new GridLayout(rows, cols)); + for (int i=0; i<numFields; i++) + { + Bag<String> vals=new Bag<String>(); + + for (int j=1; j<data.length; j++) + { + vals.add(data[j][i]); + } + java.util.List<String> top=vals.listTop(5); + int n=top.size(); + String[] samples=new String[n]; + for (int j=0; j<n; j++) + { + String s=top.get(j); + samples[j]=(s.length()==0 ? "*MISSING*" : s)+" ("+vals.num(s)+" times)"; + } + + JPanel p=new JPanel(); + add(p); + p.setLayout(new BorderLayout()); + FieldPanel f=new FieldPanel(headers[i], samples, guesses[i]); + panels.add(f); + p.add(f, BorderLayout.CENTER); + JPanel hr=new JPanel(); + hr.setPreferredSize(new Dimension(20,20)); + p.add(hr, BorderLayout.SOUTH); + } + } + + public Dimension getPreferredSize() + { + return new Dimension(400,150*rows); + } + } + + class FieldPanel extends JPanel + { + JComboBox typeChoices; + String fieldName; + JCheckBox ignore; + JLabel fieldLabel; + int x1=5, y1=20, y3=150,x2=150, x3=150, x4=375, dh=2; + + + FieldPanel(String fieldName, String[] sampleValues, Class typeGuess) + { + // just going with a null layout here, because it's a lot simpler! + + setLayout(null); + setBackground(Color.white); + this.fieldName=fieldName; + + fieldLabel=new JLabel(" \""+fieldName+'"'); + fieldLabel.setFont(model.getDisplay().big()); + add(fieldLabel); + fieldLabel.setBounds(x1,y1,fieldLabel.getPreferredSize().width, fieldLabel.getPreferredSize().height); + + typeChoices=new JComboBox(); + for (String choice: FieldFormatCatalog.classNames()) + typeChoices.addItem(choice); + typeChoices.setSelectedItem(FieldFormatCatalog.humanName(typeGuess)); + add(typeChoices); + int y2=fieldLabel.getY()+fieldLabel.getHeight()+dh+5; + typeChoices.setBounds(x1,y2, + typeChoices.getPreferredSize().width, typeChoices.getPreferredSize().height); + + ignore=new JCheckBox("Ignore Field"); + add(ignore); + ignore.setBounds(x1,typeChoices.getY()+typeChoices.getHeight()+dh, + ignore.getPreferredSize().width, ignore.getPreferredSize().height); + ignore.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Color c=ignore.isSelected() ? Color.gray : Color.black; + fieldLabel.setForeground(c); + typeChoices.setForeground(c); + }}); + + JTextArea values=new JTextArea(); + values.setForeground(Color.gray); + for (int i=0; i<sampleValues.length; i++) + values.append(sampleValues[i]+"\n"); + add(values); + values.setBounds(x3,y2,x4-x3,y3-y2); + } + + public Dimension getPreferredSize() + { + return new Dimension(500,150); + } + } +} diff --git a/timeflow/app/ui/LinkTabPane.java b/timeflow/app/ui/LinkTabPane.java new file mode 100755 index 0000000..1f4ee01 --- /dev/null +++ b/timeflow/app/ui/LinkTabPane.java @@ -0,0 +1,208 @@ +package timeflow.app.ui; + +import javax.swing.*; +import javax.swing.event.ChangeListener; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +// custom JTabbedPane-like thing. +public class LinkTabPane extends JPanel { + + ArrayList<String> tabNames=new ArrayList<String>(); + HashMap<String, JComponent> tabMap=new HashMap<String, JComponent>(); + String currentName; + CardLayout cards=new CardLayout(); + JPanel center=new JPanel(); + LinkTop top=new LinkTop(); + + public LinkTabPane() + { + setBackground(Color.white); + setLayout(new BorderLayout()); + add(top, BorderLayout.NORTH); + add(center, BorderLayout.CENTER); + center.setLayout(cards); + top.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + String s=top.getName(e.getX()); + if (s!=null) + { + String old=currentName; + setCurrentName(s); + firePropertyChange("tab", old, s); + } + }}); + } + + public String getTitleAt(int i) + { + return tabNames.get(i); + } + + public void setSelectedIndex(int i) + { + setCurrentName(getTitleAt(i)); + } + + public void addTab(JComponent component, String name, boolean left) + { + tabNames.add(name); + tabMap.put(name, component); + center.add(component, name); + top.addName(name, left); + repaint(); + if (currentName==null) + currentName=name; + } + + public String getCurrentName() + { + return currentName; + } + + public void setCurrentName(final String currentName) + { + this.currentName=currentName; + top.repaint(); + SwingUtilities.invokeLater(new Runnable() {public void run() {cards.show(center, currentName);}}); + + } + + class LinkTop extends JPanel + { + int left, right; + ArrayList<HotLink> leftHots=new ArrayList<HotLink>(); + ArrayList<HotLink> rightHots=new ArrayList<HotLink>(); + Font font=new Font("Verdana", Font.PLAIN, 14); + FontMetrics fm=getFontMetrics(font); + + LinkTop() + { + setLayout(new FlowLayout(FlowLayout.LEFT)); + setBackground(new Color(220,220,220)); + } + + public void paintComponent(Graphics g1) + { + int w=getSize().width, h=getSize().height; + Graphics2D g=(Graphics2D)g1; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(getBackground()); + g.fillRect(0,0,w,h); + g.setColor(Color.gray); + for (int i=0; i<2; i++) + { + g.drawLine(0,i,w,i); + g.drawLine(0,h-1-i,w,h-1-i); + } + + for (HotLink hot: leftHots) + { + draw(g, hot, h, 0); + } + + for (HotLink hot: rightHots) + { + draw(g, hot, h, w); + } + + for (int i=0; i<leftHots.size(); i++) + { + + if (i<leftHots.size()-1) + { + HotLink hot=leftHots.get(i); + for (int j=0; j<1; j++) + g.drawLine(hot.x+hot.width-1-j, 7, hot.x+hot.width-1-j, h-7); + } + } + + for (int i=0; i<rightHots.size(); i++) + { + + if (i<rightHots.size()-1) + { + HotLink hot=rightHots.get(i); + for (int j=0; j<1; j++) + g.drawLine(hot.x+w-1-j, 7, hot.x+w-1-j, h-7); + } + } + } + + void draw(Graphics g, HotLink hot, int h, int dx) + { + int x=hot.x+dx; + if (hot.s.equals(currentName)) + { + g.setColor(Color.lightGray); + g.fillRect(x,2,hot.width,h-4); + g.setColor(Color.gray); + g.drawLine(x-1, 0, x-1, h); + g.drawLine(x+hot.width-1, 0, x+hot.width-1, h); + } + g.setColor(Color.darkGray); + g.setFont(font); + int sw=fm.stringWidth(hot.s); + g.drawString(hot.s, x+(hot.width-sw)/2, h-10); + + } + + String getName(int x) + { + for (HotLink h: leftHots) + { + if (h.x<=x && h.x+h.width>x) + return h.s; + } + for (HotLink h: rightHots) + { + int w=getSize().width; + if (h.x+w<=x && h.x+h.width+w>x) + return h.s; + } + + if (leftHots.size()>0) + return leftHots.get(leftHots.size()-1).s; + if (rightHots.size()>0) + return rightHots.get(0).s; + return null; + } + + void addName(String name, boolean leftward) + { + if (leftward) + { + int x=right; + int w=fm.stringWidth(name)+24; + leftHots.add(new HotLink(name, x, 0, w, 30)); + right+=w; + } + else + { + int x=left; + int w=fm.stringWidth(name)+24; + rightHots.add(new HotLink(name, x-w, 0, w, 30)); + left-=w; + } + } + + class HotLink extends Rectangle + { + String s; + HotLink(String s, int x, int y, int w, int h) + { + super(x,y,w,h); + this.s=s; + } + } + + public Dimension getPreferredSize() + { + return new Dimension(30,30); + } + } + +} diff --git a/timeflow/app/ui/MassDeletePanel.java b/timeflow/app/ui/MassDeletePanel.java new file mode 100755 index 0000000..10acc13 --- /dev/null +++ b/timeflow/app/ui/MassDeletePanel.java @@ -0,0 +1,80 @@ +package timeflow.app.ui; + +import timeflow.data.db.*; +import timeflow.model.*; +import timeflow.views.*; + +import javax.swing.*; +import javax.swing.table.*; + +import java.awt.*; +import java.util.*; + +public class MassDeletePanel extends ModelPanel +{ + TableView table; + ActList keepers; + + public MassDeletePanel(TFModel model, ActList keepers, String title) + { + super(model); + this.keepers=keepers; + setLayout(new BorderLayout()); + + JPanel top=new JPanel(); + top.setLayout(new GridLayout(4,1)); + top.add(new JPanel()); + top.add(new JLabel(title)); + int n=keepers.size(); + String message=null; + if (n>1) + message="These are the "+n+" items that will remain."; + else if (n==1) + message="This in the only item that will remain."; + else + message="No items will remain!"; + + JLabel instructions=new JLabel(message); + top.add(instructions); + top.add(new JPanel()); + add(top, BorderLayout.NORTH); + + table=new TableView(model); + model.removeListener(table); + add(table, BorderLayout.CENTER); + table.setEditable(false); + table.setActs(keepers); + } + + public void applyAction() + { + ActDB db=getModel().getDB(); + HashSet<Act> keepSet=new HashSet<Act>(); + keepSet.addAll(keepers); + + for (Act a: db.all()) + if (!keepSet.contains(a)) + db.delete(a); + + // we assume the caller will decide what event to fire. + } + + public void detachFromModel() + { + TFModel model=getModel(); + model.removeListener(table); + model.removeListener(this); + } + + public Dimension getPreferredSize() + { + Dimension d=super.getPreferredSize(); + return new Dimension(Math.max(700, d.width), 250); + } + + @Override + public void note(TFEvent e) { + // TODO Auto-generated method stub + + } +} diff --git a/timeflow/app/ui/ReorderFieldsPanel.java b/timeflow/app/ui/ReorderFieldsPanel.java new file mode 100755 index 0000000..1db474d --- /dev/null +++ b/timeflow/app/ui/ReorderFieldsPanel.java @@ -0,0 +1,68 @@ +package timeflow.app.ui; + +import timeflow.data.db.*; +import timeflow.model.*; +import timeflow.views.*; + +import javax.swing.*; +import javax.swing.table.*; + +import java.awt.*; +import java.util.*; + +public class ReorderFieldsPanel extends ModelPanel +{ + TableView table; + + public ReorderFieldsPanel(TFModel model) + { + super(model); + setLayout(new BorderLayout()); + + JPanel top=new JPanel(); + top.setLayout(new GridLayout(3,1)); + top.add(new JPanel()); + JLabel instructions=new JLabel("Drag and drop the column headers to reorder fields."); + top.add(instructions); + top.add(new JPanel()); + add(top, BorderLayout.NORTH); + + table=new TableView(model); + add(table, BorderLayout.CENTER); + table.setEditable(false); + table.setReorderable(true); + table.onscreen(true); + } + + public void applyReordering() + { + Enumeration<TableColumn> columns=table.getTable().getTableHeader().getColumnModel().getColumns(); + ArrayList<Field> newOrder=new ArrayList<Field>(); + while (columns.hasMoreElements()) + { + TableColumn col=columns.nextElement(); + String name=col.getHeaderValue().toString(); + newOrder.add(getModel().getDB().getField(name)); + } + getModel().getDB().setNewFieldOrder(newOrder); + } + + public void detachFromModel() + { + TFModel model=getModel(); + model.removeListener(table); + model.removeListener(this); + } + + public Dimension getPreferredSize() + { + Dimension d=super.getPreferredSize(); + return new Dimension(Math.max(700, d.width), 250); + } + + @Override + public void note(TFEvent e) { + // TODO Auto-generated method stub + + } +} diff --git a/timeflow/app/ui/SizeLegendPanel.java b/timeflow/app/ui/SizeLegendPanel.java new file mode 100755 index 0000000..a689d4a --- /dev/null +++ b/timeflow/app/ui/SizeLegendPanel.java @@ -0,0 +1,113 @@ +package timeflow.app.ui; + +import timeflow.model.*; +import timeflow.data.db.*; +import timeflow.data.time.*; + +import timeflow.util.*; + +import java.awt.*; +import java.awt.geom.AffineTransform; + +public class SizeLegendPanel extends ModelPanel { + Field sizeField; + double min, max; + + public SizeLegendPanel(TFModel model) + { + super(model); + setBackground(Color.white); + } + + @Override + public void note(TFEvent e) { + Field size=getModel().getDB().getField(VirtualField.SIZE); + + if (size!=null && (size!=sizeField || e.affectsData())) + { + double[] minmax=DBUtils.minmax(getModel().getActs(), size); + min=minmax[0]; + max=minmax[1]; + } + sizeField=size; + repaint(); + } + + public Dimension getPreferredSize() + { + return new Dimension(200,40); + } + + public void paintComponent(Graphics g1) + { + Graphics2D g=(Graphics2D)g1; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int w=getSize().width; + int h=getSize().height; + TFModel model=getModel(); + Display display=model.getDisplay(); + g.setColor(getBackground()); + g.setFont(display.plain()); + g.fillRect(0,0,w,h); + g.setColor(Color.gray); + + if (sizeField==null) + { + return; + } + else if (Double.isNaN(min)) + { + g.drawString("All values missing.",3,20); + return; + } + else + { + AffineTransform old=g.getTransform(); + g.setTransform(AffineTransform.getTranslateInstance(20, 0)); + if (min==max) + { + g.setColor(Color.gray); + g.fillOval(3,h/2-3,6,6); + g.setColor(Color.black); + g.setFont(display.tiny()); + g.drawString(format(min),12,h/2+5); + } + else + { + String leftLabel=format(min); + String rightLabel=format(max); + g.setFont(display.tiny()); + int lw=display.tinyFontMetrics().stringWidth(leftLabel); + int rw=display.tinyFontMetrics().stringWidth(rightLabel); + g.setColor(Color.black); + int ty=h/2+5;; + g.drawString(leftLabel,2,ty); + g.setColor(Color.lightGray); + double maxAbs=Math.max(Math.abs(min), Math.abs(max)); + int dx=8+lw; + for (int i=0; i<5; i++) + { + double z=(i*max+(4-i)*min)/4; + int r=(int)(Math.sqrt(Math.abs(z/maxAbs))*Display.MAX_DOT_SIZE); + if (r<1) + r=1; + if (z>0) + g.fillOval(dx,h/2-r,2*r,2*r); + else + g.drawOval(dx,h/2-r,2*r,2*r); + dx+=5+2*r; + } + g.setColor(Color.black); + g.drawString(rightLabel,dx+4,ty); + } + g.setTransform(old); + } + } + + String format(double x) + { + if (Math.abs(x)>10 && (max-min)>10) + return Display.format(Math.round(x)); + return Display.format(x); + } +} diff --git a/timeflow/app/ui/StatusPanel.java b/timeflow/app/ui/StatusPanel.java new file mode 100755 index 0000000..0cdf2fb --- /dev/null +++ b/timeflow/app/ui/StatusPanel.java @@ -0,0 +1,98 @@ +package timeflow.app.ui; + +import timeflow.model.*; +import timeflow.app.ui.filter.FilterControlPanel; +import timeflow.data.db.*; +import timeflow.data.db.filter.ActFilter; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.*; + +public class StatusPanel extends ModelPanel +{ + JLabel numLabel=new JLabel("") + { + public Dimension getPreferredSize() + { + return new Dimension(30,25); + } + }; + JLabel filterLabel=new JLabel("") + { + public Dimension getPreferredSize() + { + return new Dimension(30,25); + } + }; + + static final DecimalFormat niceFormat=new DecimalFormat("###,###"); + + public StatusPanel(TFModel model, final FilterControlPanel filterControls) { + super(model); + setLayout(new BorderLayout()); + setBackground(new Color(245, 245, 245)); + setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 15)); + JPanel center=new JPanel(); + center.setBackground(getBackground()); + center.setLayout(new GridLayout(2,1)); + add(center, BorderLayout.CENTER); + + center.add(numLabel); + numLabel.setFont(model.getDisplay().plain()); + numLabel.setBackground(new Color(245, 245, 245)); + + JPanel bottom=new JPanel(); + center.add(bottom); + bottom.setLayout(new BorderLayout()); + bottom.add(filterLabel, BorderLayout.CENTER); + bottom.setBackground(new Color(245, 245, 245)); + filterLabel.setFont(model.getDisplay().plain()); + filterLabel.setBackground(new Color(245, 245, 245)); + filterLabel.setForeground(Color.red); + + JPanel clearPanel=new JPanel(); + clearPanel.setBackground(new Color(245, 245, 245)); + clearPanel.setLayout(new GridLayout(1,1)); + JButton clear=new JButton(new ImageIcon("images/button_clear_all.gif")); + clear.setBorder(null); + clear.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + filterControls.clearFilters(); + } + }); + clearPanel.add(clear); + bottom.add(clearPanel, BorderLayout.EAST); + + add(new DottedLine(), BorderLayout.SOUTH); + } + + @Override + public void note(TFEvent e) { + + ActDB db=getModel().getDB(); + if (db==null || db.size()==0) + { + numLabel.setForeground(new Color(245,245,245));//Color.gray); + numLabel.setText("No data"); + return; + } + int numTotal=db.size(); + ActList acts=getModel().getActs(); + int numShown=acts.size(); + filterLabel.setText(numShown<numTotal ? " Filters applied" : " Not Filtering"); + filterLabel.setForeground(numShown==numTotal ? Color.lightGray : Color.red); + numLabel.setForeground(numShown==0 ? Color.red : Color.darkGray); + String plural=(numTotal==1 ? "" : "s"); + if (numShown==numTotal) + numLabel.setText(" Showing All "+niceFormat.format(numTotal)+" Event"+plural); + else + numLabel.setText(" Showing "+niceFormat.format(numShown) + +" / "+niceFormat.format(numTotal)+" Event"+ plural); + repaint(); + } + +} diff --git a/timeflow/app/ui/WaitingDialog.java b/timeflow/app/ui/WaitingDialog.java new file mode 100755 index 0000000..9ac2736 --- /dev/null +++ b/timeflow/app/ui/WaitingDialog.java @@ -0,0 +1,56 @@ +package timeflow.app.ui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + +public class WaitingDialog extends JFrame { + + Timer timer; + + public static void main(String[] args) + { + new WaitingDialog("Testing", "Hello, world!"); + } + + public WaitingDialog(String title, String message) + { + super(title); + Throbber throbber=new Throbber(); + timer=new Timer(50, throbber); + getContentPane().setLayout(new FlowLayout(FlowLayout.LEFT)); + getContentPane().add(throbber); + getContentPane().add(new JLabel(message)); + setBounds(400,400,300,150); + setVisible(true); + timer.start(); + } + + public void stop() + { + timer.stop(); + setVisible(false); + } + + class Throbber extends JPanel implements ActionListener + { + int count=0; + public void paintComponent(Graphics g) + { + int w=getSize().width, h=getSize().height; + int c=count%256; + g.setColor(new Color(c,c,c)); + g.fillRect(0,0,w,h); + } + @Override + public void actionPerformed(ActionEvent e) { + repaint(); + count+=10; + } + + public Dimension getPreferredSize() + { + return new Dimension(30,100); + } + } +} diff --git a/timeflow/app/ui/filter/BabyHistogram.java b/timeflow/app/ui/filter/BabyHistogram.java new file mode 100755 index 0000000..fb23aac --- /dev/null +++ b/timeflow/app/ui/filter/BabyHistogram.java @@ -0,0 +1,274 @@ +package timeflow.app.ui.filter; + +import javax.swing.*; + +import timeflow.data.time.Interval; +import timeflow.model.Display; + + +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.text.*; + + +public class BabyHistogram extends JPanel { + private int[] buckets; + private double[] x; + private double min, max; + private int numDefined; + private int maxBucket; + private double value; + private static final DecimalFormat df=new DecimalFormat("###,###,###,###.##"); + + + Point mouseHit=new Point(); + Point mouse=new Point(-1,0); + enum Modify {START, END, POSITION, NONE}; + Modify change=Modify.NONE; + Rectangle startRect=new Rectangle(-1,-1,0,0); + Rectangle endRect=new Rectangle(-1,-1,0,0); + Rectangle positionRect=new Rectangle(-1,-1,0,0); + Color sidePlain=Color.orange; + Color sideMouse=new Color(230,100,0); + double relLow=0, relHigh=1, originalLow, originalHigh; + + public void setRelRange(double relLow, double relHigh) + { + this.relLow=Math.max(0,relLow); + this.relHigh=Math.min(1,relHigh); + repaint(); + } + + public void setTrueRange(double low, double high) + { + double span=max-min; + if (span<=0) // nothing much to do... + return; + + setRelRange((low-min)/span, (high-min)/span); + } + + public boolean isEverything() + { + return relLow==0 && relHigh==1; + } + + public double getLow() + { + return abs(relLow); + } + + public double getHigh() + { + return abs(relHigh); + } + + private double abs(double x) + { + return x*max+(1-x)*min; + } + + public BabyHistogram(final Runnable changeAction) + { + + addMouseListener(new MouseAdapter() { + + @Override + public void mousePressed(MouseEvent e) { + int mx=e.getX(); + int my=e.getY(); + int lx=lowX(), hx=highX(); + + int ox=0; + if (Math.abs(mx-lx)<Math.abs(mx-hx)) + { + change=Modify.START; + ox=lx; + } + else + { + change=Modify.END; + ox=hx; + } + mouseHit.setLocation(ox,my); + mouse.setLocation(mx,my); + originalLow=relLow; + originalHigh=relHigh; + repaint(); + } + + @Override + public void mouseReleased(MouseEvent e) { + change=Modify.NONE; + repaint(); + }}); + addMouseMotionListener(new MouseMotionAdapter() { + + @Override + public void mouseDragged(MouseEvent e) { + + if (change==Modify.NONE) + return; + mouse.setLocation(e.getX(), e.getY()); + int mouseDiff=mouse.x-mouseHit.x; + double relDiff=mouseDiff/(double)getSize().width; + switch (change) + { + case POSITION: + relLow=originalLow+relDiff; + relHigh=originalHigh+relDiff; + + break; + case START: relLow=originalLow+relDiff; + break; + case END: relHigh=originalHigh+relDiff; + } + relLow=Math.max(0, relLow); + relHigh=Math.min(1, relHigh); + changeAction.run(); + repaint(); + } + }); + + } + + public void setData(double[] x) + { + relLow=0; + relHigh=1; + + this.x=x; + int n=x.length; + + // do some quick checks on the data. + boolean positive=true; + min=Double.NaN; + max=Double.NaN; + numDefined=0; + for (int i=0; i<n; i++) + { + double m=x[i]; + if (!Double.isNaN(m)) + { + numDefined++; + positive &= m>0; + if (Double.isNaN(min)) + { + min=m; + max=m; + value=m; + } + else + { + min=Math.min(m, min); + max=Math.max(m, max); + } + } + } + + if (numDefined==0) + return; + if (min==max) + { + buckets=new int[1]; + buckets[0]=numDefined; + maxBucket=numDefined; + return; + } + int numBuckets=(int)Math.min(50, 2*Math.sqrt(numDefined)); + buckets=new int[numBuckets]; + maxBucket=0; + for (int i=0; i<n; i++) + { + if (!Double.isNaN(x[i])) + { + int b=(int)((numBuckets-1)*(x[i]-min)/(max-min)); + buckets[b]++; + maxBucket=Math.max(maxBucket, buckets[b]); + } + } + } + + public void paintComponent(Graphics g) + { + int w=getSize().width; + int h=getSize().height; + g.setColor(Color.white); + g.fillRect(0,0,w,h); + + if (x==null) + { + say(g, "No data"); + return; + } + + if (x.length==0) + { + say(g, "No values"); + return; + } + + if (numDefined==0) + { + say(g, "No defined values"); + return; + } + + int n=buckets.length; + if (n==1) + { + say(g, "All defined vals = "+df.format(value)); + return; + } + + // wow, if we got here we really have a histogram and not a degenerate mess! + + Color bar=Display.barColor; + g.setColor(bar); + for (int i=0; i<n; i++) + { + int x1=(i*w)/n; + int x2=((i+1)*w)/n; + int y1=h-(buckets[i]*h)/maxBucket; + if (buckets[i]>0 && y1>h-2) + y1=h-2; + g.fillRect(x1,y1,x2-x1-1,h-y1); + } + + // now draw thumb. + + int thumb1=lowX(); + int thumb2=highX(); + g.setColor(Color.black); + g.drawLine(thumb1,0,thumb1,h); + g.drawLine(thumb2,0,thumb2,h); + g.setColor(new Color(235,235,235,160)); + g.fillRect(0,0,thumb1,h); + g.fillRect(thumb2+1,0,w-thumb2-1,h); + g.setColor(Color.lightGray); + g.drawRect(0,0,w-1,h-1); + } + + int lowX() + { + return (int)((getSize().width-1)*relLow); + } + + int highX() + { + return (int)((getSize().width-1)*relHigh); + } + + void say(Graphics g, String s) + { + g.setColor(Color.gray); + g.drawString(s,5,getSize().height-5); + } + + public Dimension getPreferredSize() + { + return new Dimension(200,60); + } +} diff --git a/timeflow/app/ui/filter/FilterCategoryPanel.java b/timeflow/app/ui/filter/FilterCategoryPanel.java new file mode 100755 index 0000000..9207469 --- /dev/null +++ b/timeflow/app/ui/filter/FilterCategoryPanel.java @@ -0,0 +1,164 @@ +package timeflow.app.ui.filter; + +import timeflow.util.*; + +import javax.swing.*; + +import timeflow.data.db.*; +import timeflow.data.db.filter.*; +import timeflow.model.ModelPanel; + +import java.awt.*; +import java.awt.event.*; + +public class FilterCategoryPanel extends FilterDefinitionPanel +{ + public JList dataList=new JList(); + Field field; + + public FilterCategoryPanel(final Field field, final ModelPanel parent) + { + this(field.getName(), field, parent); + } + + public FilterCategoryPanel(String title, final Field field, final ModelPanel parent) + { + this.field=field; + setLayout(new BorderLayout()); + setBackground(Color.white); + setBorder(BorderFactory.createEmptyBorder(0,5,0,5)); + + add(new FilterTitle(title, field, parent, true), BorderLayout.NORTH); + + + JScrollPane scroller=new JScrollPane(dataList); + scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scroller.setBorder(null); + add(scroller, BorderLayout.CENTER); + dataList.setForeground(Color.darkGray); + dataList.setSelectionForeground(Color.black); + dataList.setSelectionBackground(new Color(220,235,255)); + dataList.setFont(parent.getModel().getDisplay().small()); + scroller.getVerticalScrollBar().setBackground(Color.white); + + + // ok, the following is ugly code to insert a new mouselistener + // that lets the user deselect items when they are clicked. + // i tried a bunch of stuff but this is all that would work-- + // and searching the web yielded only solutions similar to this. + // also, there's a weird dance with consuming/not consuming events + // that is designed to allow a certain kind of multi-selection behavior + // with the mouse, while letting you scroll through items one at a time + // with the keyboard. this was the product of a long series of + // conversations with target users. + MouseListener[] old = dataList.getMouseListeners(); + for (MouseListener m: old) + dataList.removeMouseListener(m); + + dataList.addMouseListener(new MouseAdapter() + { + public void mousePressed(MouseEvent e) + { + if (e.isControlDown() || e.isMetaDown() || e.isShiftDown()) + return; + final int index = dataList.locationToIndex(e.getPoint()); + if (dataList.isSelectedIndex(index)) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + dataList.removeSelectionInterval(index, index); + + } + }); + e.consume(); + } + else + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + dataList.addSelectionInterval(index, index); + + } + }); + e.consume(); + } + } + }); + + for (MouseListener m: old) + dataList.addMouseListener(m); + + dataList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, + Object value, int index, boolean isSelected, + boolean cellHasFocus) { + Component c=super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (field==parent.getModel().getColorField()) + { + String text=value.toString(); + int n=text.lastIndexOf('-'); + if (n>1) + text=text.substring(0,n-1); + c.setForeground(parent.getModel().getDisplay().makeColor(text)); + } + return c; + }}); + + } + + public void setData(Bag<String> data) + { + dataList.removeAll(); + java.util.List<String> items=data.list(); + String[] s=(String[])items.toArray(new String[0]); + for (int i=0; i<s.length; i++) + { + int num=data.num(s[i]); + if (s[i]==null || s[i].length()==0) + s[i]="(missing)"; + s[i]+=" - "+num; + } + dataList.setListData(s); + } + + public Dimension getPreferredSize() + { + return new Dimension(200,200); + } + + @Override + public ActFilter defineFilter() { + Object[] o=dataList.getSelectedValues(); + if (o==null || o.length==0) + return null; + + int n=o.length; + String[] s=new String[n]; + for (int i=0; i<n; i++) + { + String w=(String)o[i]; + int m=w.lastIndexOf('-'); + s[i]=w.substring(0, m-1); + if ("(missing)".equals(s[i])) + s[i]=""; + } + + if (s.length==1) + return new FieldValueFilter(field, s[0]); + FieldValueSetFilter f=new FieldValueSetFilter(field); + for (int i=0; i<s.length; i++) + f.addValue(s[i]); + return f; + } + + @Override + public void clearFilter() { + dataList.clearSelection(); + } + +} diff --git a/timeflow/app/ui/filter/FilterControlPanel.java b/timeflow/app/ui/filter/FilterControlPanel.java new file mode 100755 index 0000000..fafdb7e --- /dev/null +++ b/timeflow/app/ui/filter/FilterControlPanel.java @@ -0,0 +1,208 @@ +package timeflow.app.ui.filter; + +import timeflow.model.*; +import timeflow.app.ui.StatusPanel; +import timeflow.data.db.*; +import timeflow.data.db.filter.*; +import timeflow.data.time.RoughTime; + +import timeflow.util.*; + +import java.util.*; +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import java.awt.*; + +public class FilterControlPanel extends ModelPanel +{ + FacetSubpanel inside=new FacetSubpanel(); + SearchPanel searchPanel; + boolean inverted=false; + JMenu menuToSyncWith; + + + public FilterControlPanel(TFModel model, JMenu menuToSyncWith) + { + super(model); + this.menuToSyncWith=menuToSyncWith; + searchPanel=new SearchPanel(model, this); + setLayout(new BorderLayout()); + setBackground(Color.white); + JPanel top=new JPanel(); + top.setBackground(Color.white); + top.setLayout(new BorderLayout()); + top.setBackground(Color.white); + + top.add(new StatusPanel(model, this), BorderLayout.NORTH); + top.add(searchPanel, BorderLayout.CENTER); + + add(top, BorderLayout.NORTH); + add(inside, BorderLayout.CENTER); + } + + void setInverted(boolean inverted) + { + this.inverted=inverted; + makeFilter(); + } + + void makeFilter() + { + AndFilter filter=new AndFilter(); + String s=searchPanel.entry.getText(); + if (s.length()>0) + filter.and(new StringMatchFilter(getModel().getDB(), s, true)); + for (FilterDefinitionPanel f: inside.facetTable.values()) + filter.and(f.defineFilter()); + getModel().setFilter(inverted ? new NotFilter(filter) : filter, this); + } + + public void clearFilters() + { + + searchPanel.entry.setText(""); + for (FilterDefinitionPanel d: inside.facetTable.values()) + d.clearFilter(); + inverted=false; + searchPanel.invert.setSelected(false); + for (Field f:getModel().getDB().getFields()) + { + inside.setFacet(f, false); + } + makeFilter(); + } + + @Override + public void note(TFEvent e) { + if (e.affectsSchema()) + { + inside.clearFacets(); + searchPanel.entry.setText(""); + } + } + + public void setFacet(Field field, boolean on) + { + inside.setFacet(field, on); + makeFilter(); + } + + class FacetSubpanel extends JPanel + { + + ArrayList<Field> facets=new ArrayList<Field>(); + HashMap<Field, FilterDefinitionPanel> facetTable=new HashMap<Field, FilterDefinitionPanel>(); + + FacetSubpanel() + { + setLayout(null); + setBackground(Color.white); + } + + FilterDefinitionPanel makePanel(Field field) + { + if (field.getType()==Double.class) + { + final FilterNumberPanel num=new FilterNumberPanel(field, new Runnable() { + @Override + public void run() { + makeFilter(); + }}, FilterControlPanel.this); + num.setData(DBUtils.getValues(getModel().getDB(), field)); + return num; + } + + if (field.getType()==RoughTime.class) + { + final FilterDatePanel date=new FilterDatePanel(field, new Runnable() { + @Override + public void run() { + makeFilter(); + }}, FilterControlPanel.this); + date.setData(DBUtils.getValues(getModel().getDB(), field)); + return date; + } + + final FilterCategoryPanel p= new FilterCategoryPanel(field, FilterControlPanel.this); + p.dataList.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + makeFilter(); + } + }); + Bag<String> data=DBUtils.countValues(getModel().getDB().all(), field); + p.setData(data); + return p; + } + + public void clearFacets() + { + removeAll(); + facets.clear(); + facetTable.clear(); + revalidate(); + repaint(); + } + + public void setFacet(Field field, boolean on) + { + FilterDefinitionPanel panel=facetTable.get(field); + if (on == (panel!=null)) + return; + if (on) + { + panel=makePanel(field); + add(panel); + facets.add(field); + facetTable.put(field,panel); + } + else + { + remove(panel); + facets.remove(field); + facetTable.remove(field); + } + + doFacetLayout(); + if (menuToSyncWith!=null) + for (int i=0; i<menuToSyncWith.getItemCount(); i++) + { + JCheckBoxMenuItem item=(JCheckBoxMenuItem)menuToSyncWith.getItem(i); + if (item.getText().equals(field.getName())) + { + item.setSelected(on); + } + } + revalidate(); + repaint(); + } + + public void setBounds(int x, int y, int w, int h) + { + super.setBounds(x,y,w,h); + doFacetLayout(); + } + + void doFacetLayout() + { + int w=getSize().width, h=getSize().height; + int goodSize=0; + for (Field f: facets) + { + FilterDefinitionPanel p=facetTable.get(f); + goodSize+=p.getPreferredSize().height; + } + int top=0; + for (Field f: facets) + { + FilterDefinitionPanel p=facetTable.get(f); + int pref=p.getPreferredSize().height; + int panelHeight=(goodSize<= h ? pref : (pref*h)/goodSize); + p.setBounds(0,top,w,panelHeight); + top+=panelHeight; + } + } + } +} diff --git a/timeflow/app/ui/filter/FilterDatePanel.java b/timeflow/app/ui/filter/FilterDatePanel.java new file mode 100755 index 0000000..17c1801 --- /dev/null +++ b/timeflow/app/ui/filter/FilterDatePanel.java @@ -0,0 +1,173 @@ +package timeflow.app.ui.filter; + +import timeflow.data.db.*; +import timeflow.data.db.filter.*; +import timeflow.model.*; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.*; +import java.util.Date; + +import javax.swing.*; + +// in theory it should be easy to refactor this to share code with +// NumberFilterPanel. +// but, i'm not sure how to do it in a way that doesn't make the code +// seem too complicated. + +public class FilterDatePanel extends FilterDefinitionPanel +{ + BabyHistogram histogram; + Field field; + JTextField startEntry; + JTextField endEntry; + JCheckBox nullBox; + Runnable action; + SimpleDateFormat df=new SimpleDateFormat("MMM dd yyyy"); + + public FilterDatePanel(final Field field, final Runnable action, final FilterControlPanel parent) + { + this.field=field; + this.action=action; + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(0,5,0,5)); + setBackground(Color.white); + add(new FilterTitle(field, parent, false), BorderLayout.NORTH); + + Runnable fullAction=new Runnable() + { + public void run() + { + startEntry.setText(format(histogram.getLow())); + endEntry.setText(format(histogram.getHigh())); + action.run(); + } + }; + + histogram=new BabyHistogram(fullAction); + + add(histogram, BorderLayout.CENTER); + + JPanel bottomStuff=new JPanel(); + bottomStuff.setLayout(new GridLayout(2,1)); + add(bottomStuff, BorderLayout.SOUTH); + + JPanel lowHighPanel=new JPanel(); + bottomStuff.add(lowHighPanel); + lowHighPanel.setBackground(Color.white); + lowHighPanel.setLayout(new BorderLayout()); + Font small=parent.getModel().getDisplay().small(); + + startEntry=new JTextField(7); + startEntry.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setLowFromText(); + action.run(); + }}); + lowHighPanel.add(startEntry, BorderLayout.WEST); + startEntry.setFont(small); + + JLabel rangeLabel=new JLabel("to", JLabel.CENTER); + rangeLabel.setForeground(Color.gray); + rangeLabel.setFont(small); + lowHighPanel.add(rangeLabel, BorderLayout.CENTER); + endEntry=new JTextField(7); + lowHighPanel.add(endEntry, BorderLayout.EAST); + endEntry.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setHighFromText(); + action.run(); + }}); + endEntry.setFont(small); + + nullBox=new JCheckBox("Include Missing Values"); + nullBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + action.run(); + }}); + bottomStuff.add(nullBox); + bottomStuff.setBackground(Color.white); + nullBox.setBackground(Color.white); + nullBox.setForeground(Color.gray); + nullBox.setFont(small); + + } + + String format(double x) + { + Date date=new Date((long)x); + return df.format(date); + } + + void setLowFromText() + { + try + { + long low=df.parse(startEntry.getText()).getTime(); + long high=(long)histogram.getHigh(); + if (low>high) + { + high=low; + endEntry.setText(startEntry.getText()); + } + histogram.setTrueRange(low,high); + + } + catch (Exception e) + { + + } + } + + + void setHighFromText() + { + try + { + long high=df.parse(endEntry.getText()).getTime(); + double low=(long)histogram.getLow(); + if (low>high) + { + low=high; + startEntry.setText(endEntry.getText()); + } + histogram.setTrueRange(low,high); + + } + catch (Exception e) + { + + } + } + + public void setData(double[] data) + { + histogram.setData(data); + startEntry.setText(format(histogram.getLow())); + endEntry.setText(format(histogram.getHigh())); + repaint(); + } + + public Dimension getPreferredSize() + { + return new Dimension(200,160); + } + + @Override + public ActFilter defineFilter() { + long low=(long)histogram.getLow(); + long high=(long)histogram.getHigh(); + boolean acceptNull=nullBox.isSelected(); + return new TimeIntervalFilter(low, high, acceptNull, field); + } + + @Override + public void clearFilter() { + histogram.setRelRange(0, 1); + } +} \ No newline at end of file diff --git a/timeflow/app/ui/filter/FilterDefinitionPanel.java b/timeflow/app/ui/filter/FilterDefinitionPanel.java new file mode 100755 index 0000000..0823209 --- /dev/null +++ b/timeflow/app/ui/filter/FilterDefinitionPanel.java @@ -0,0 +1,10 @@ +package timeflow.app.ui.filter; + +import timeflow.data.db.filter.ActFilter; + +import javax.swing.*; + +public abstract class FilterDefinitionPanel extends JPanel { + public abstract ActFilter defineFilter(); + public abstract void clearFilter(); +} diff --git a/timeflow/app/ui/filter/FilterNumberPanel.java b/timeflow/app/ui/filter/FilterNumberPanel.java new file mode 100755 index 0000000..e950bb1 --- /dev/null +++ b/timeflow/app/ui/filter/FilterNumberPanel.java @@ -0,0 +1,171 @@ +package timeflow.app.ui.filter; + +import timeflow.data.db.*; +import timeflow.data.db.filter.*; +import timeflow.model.*; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; + +import timeflow.util.*; + +public class FilterNumberPanel extends FilterDefinitionPanel +{ + BabyHistogram histogram; + Field field; + JTextField lowEntry; + JTextField highEntry; + JCheckBox nullBox; + Runnable action; + + public FilterNumberPanel(final Field field, final Runnable action, final FilterControlPanel parent) + { + this.field=field; + this.action=action; + setLayout(new BorderLayout()); + setBackground(Color.white); + setBorder(BorderFactory.createEmptyBorder(0,5,0,5)); + + + setBackground(Color.white); + add(new FilterTitle(field, parent, false), BorderLayout.NORTH); + + Runnable fullAction=new Runnable() + { + public void run() + { + lowEntry.setText(format(histogram.getLow())); + highEntry.setText(format(histogram.getHigh())); + action.run(); + } + }; + + histogram=new BabyHistogram(fullAction); + + add(histogram, BorderLayout.CENTER); + + JPanel bottomStuff=new JPanel(); + bottomStuff.setLayout(new GridLayout(2,1)); + add(bottomStuff, BorderLayout.SOUTH); + bottomStuff.setBackground(Color.white); + + JPanel lowHighPanel=new JPanel(); + bottomStuff.add(lowHighPanel); + lowHighPanel.setBackground(Color.white); + + lowHighPanel.setLayout(new BorderLayout()); + + Font small=parent.getModel().getDisplay().small(); + lowEntry=new JTextField(7); + lowEntry.setFont(small); + lowEntry.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setLowFromText(); + action.run(); + }}); + lowHighPanel.add(lowEntry, BorderLayout.WEST); + JLabel rangeLabel=new JLabel("to", JLabel.CENTER); + + rangeLabel.setFont(small); + rangeLabel.setForeground(Color.gray); + lowHighPanel.add(rangeLabel, BorderLayout.CENTER); + highEntry=new JTextField(7); + lowHighPanel.add(highEntry, BorderLayout.EAST); + highEntry.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setHighFromText(); + action.run(); + }}); + highEntry.setFont(small); + + nullBox=new JCheckBox("Include Missing Values"); + nullBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + action.run(); + }}); + bottomStuff.add(nullBox); + nullBox.setBackground(Color.white); + nullBox.setForeground(Color.gray); + nullBox.setFont(small); + } + + String format(double x) + { + if (Math.abs(x)>10) + return Display.format(Math.round(x)); + return Display.format(x); + } + + void setLowFromText() + { + try + { + double low=Double.parseDouble(lowEntry.getText()); + double high=histogram.getHigh(); + if (low>high) + { + high=low; + highEntry.setText(lowEntry.getText()); + } + histogram.setTrueRange(low,high); + + } + catch (Exception e) + { + + } + } + + + void setHighFromText() + { + try + { + double high=Double.parseDouble(highEntry.getText()); + double low=histogram.getLow(); + if (low>high) + { + low=high; + lowEntry.setText(highEntry.getText()); + } + histogram.setTrueRange(low,high); + + } + catch (Exception e) + { + + } + } + + public void setData(double[] data) + { + histogram.setData(data); + lowEntry.setText(Display.format(histogram.getLow())); + highEntry.setText(Display.format(histogram.getHigh())); + repaint(); + } + + public Dimension getPreferredSize() + { + return new Dimension(200,160); + } + + @Override + public ActFilter defineFilter() { + double low=histogram.getLow(); + double high=histogram.getHigh(); + boolean acceptNull=nullBox.isSelected(); + return new NumericRangeFilter(field, low, high, acceptNull); + } + + @Override + public void clearFilter() { + histogram.setRelRange(0, 1); + } +} \ No newline at end of file diff --git a/timeflow/app/ui/filter/FilterTitle.java b/timeflow/app/ui/filter/FilterTitle.java new file mode 100755 index 0000000..57b1dcc --- /dev/null +++ b/timeflow/app/ui/filter/FilterTitle.java @@ -0,0 +1,52 @@ +package timeflow.app.ui.filter; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.*; + +import timeflow.app.ui.DottedLine; +import timeflow.data.db.Field; +import timeflow.model.ModelPanel; + +import timeflow.util.*; + +public class FilterTitle extends JPanel { + public FilterTitle(final Field field, final ModelPanel parent, boolean dots) + { + this(field.getName(), field, parent, dots); + } + public FilterTitle(String title, final Field field, final ModelPanel parent, boolean dots) + { + JPanel top=new JPanel(); + top.setBackground(Color.white); + top.setLayout(new BorderLayout()); + JLabel label=new JLabel(title); + JPanel pad=new Pad(30,30); + pad.setBackground(Color.white); + top.add(pad, BorderLayout.NORTH); + top.add(label, BorderLayout.CENTER); + label.setBackground(Color.white); + + if (parent instanceof FilterControlPanel) + { + ImageIcon redX=new ImageIcon("images/red_circle.gif"); + JLabel close=new JLabel(redX); + close.setBackground(Color.white); + top.add(close, BorderLayout.EAST); + close.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + ((FilterControlPanel)parent).setFacet(field, false); + } + }); + } + setLayout(new BorderLayout()); + add(top, BorderLayout.CENTER); + + if (dots) + add(new DottedLine(), BorderLayout.SOUTH); + } +} diff --git a/timeflow/app/ui/filter/SearchPanel.java b/timeflow/app/ui/filter/SearchPanel.java new file mode 100755 index 0000000..eca2702 --- /dev/null +++ b/timeflow/app/ui/filter/SearchPanel.java @@ -0,0 +1,52 @@ +package timeflow.app.ui.filter; + +import javax.swing.*; +import java.awt.event.*; + +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.model.*; + +import java.awt.*; + +public class SearchPanel extends ModelPanel { + + JTextField entry; + JCheckBox invert; + + public SearchPanel(TFModel model, final FilterControlPanel f) { + super(model); + setBackground(Color.white); + setBorder(BorderFactory.createEmptyBorder(15, 5,0,0)); + setLayout(new GridLayout(1,1)); + JPanel top=new JPanel(); + top.setLayout(new BorderLayout()); + add(top); + top.setBackground(Color.white); + JLabel label=model.getDisplay().label("Search"); + top.add(label, BorderLayout.WEST); + entry=new JTextField(8); + top.add(entry, BorderLayout.CENTER); + entry.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + f.makeFilter(); + }}); + + invert=new JCheckBox("Invert", false); + top.add(invert, BorderLayout.EAST); + invert.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + f.setInverted(invert.isSelected()); + }}); + invert.setFont(f.getModel().getDisplay().small()); + invert.setForeground(Color.gray); + invert.setBackground(Color.white); + } + + @Override + public void note(TFEvent e) { + } + +} diff --git a/timeflow/data/analysis/DBAnalysis.java b/timeflow/data/analysis/DBAnalysis.java new file mode 100755 index 0000000..ff571b9 --- /dev/null +++ b/timeflow/data/analysis/DBAnalysis.java @@ -0,0 +1,14 @@ +package timeflow.data.analysis; + +import timeflow.data.db.*; + +public interface DBAnalysis { + + public enum InterestLevel {IGNORE, BORING, INTERESTING, VERY_INTERESTING}; + + public String getName(); + public InterestLevel perform(ActList acts); + public String[] getResultDescription(); + + +} diff --git a/timeflow/data/analysis/FieldAnalysis.java b/timeflow/data/analysis/FieldAnalysis.java new file mode 100755 index 0000000..5ecfd8e --- /dev/null +++ b/timeflow/data/analysis/FieldAnalysis.java @@ -0,0 +1,13 @@ +package timeflow.data.analysis; + +import timeflow.data.db.*; + +public interface FieldAnalysis { + + public String getName(); + public boolean canHandleType(Class type); + public DBAnalysis.InterestLevel perform(ActList acts, Field field); + public String[] getResultDescription(); + + +} diff --git a/timeflow/data/analysis/FrequencyAnalysis.java b/timeflow/data/analysis/FrequencyAnalysis.java new file mode 100755 index 0000000..cad3089 --- /dev/null +++ b/timeflow/data/analysis/FrequencyAnalysis.java @@ -0,0 +1,76 @@ +package timeflow.data.analysis; + +import timeflow.util.*; +import timeflow.data.time.*; +import timeflow.data.analysis.DBAnalysis.InterestLevel; +import timeflow.data.db.*; +import java.util.*; + +public class FrequencyAnalysis implements FieldAnalysis { + String[] description; + + @Override + public String getName() { + return "Frequency Of Values"; + } + + @Override + public String[] getResultDescription() { + return description; + } + + @Override + public InterestLevel perform(ActList acts, Field field) { + Bag<Object> bag=new Bag<Object>(); + if (field.getType()==String[].class) + { + for (Act a: acts) + { + String[] tags=a.getTextList(field); + if (tags!=null) + for (String tag:tags) + bag.add(tag); + } + } + else + for (Act a: acts) + bag.add(a.get(field)); + + int numItems=acts.size(); + int numDistinctVals=bag.size(); + int numNullVals=bag.num(null)+bag.num(""); + + if (numItems==numDistinctVals) + description=new String[] {"All values are defined and unique."}; + else if (numItems==numDistinctVals+numNullVals-1) + description=new String[] {"All defined values are unique."}; + else if (numDistinctVals==1) + description=new String[] {"This field is always equal to "+string(bag.list().get(0))}; + else if (numDistinctVals<4) + { + List<Object> all=bag.list(); + description=new String[] {"This field takes only "+all.size()+" values.", + "which are: "+all}; + } + else + { + List<Object> all=bag.list(); + description=new String[] {"There are "+numDistinctVals+" distinct values.", + "Most common: \""+string(all.get(0))+"\", occurring "+bag.num(all.get(0))+" times."}; + } + return InterestLevel.INTERESTING; + } + + private static String[] empty=new String[0]; + static String string(Object o) + { + if (o==null || "".equals(o) || empty.equals(o)) + return "[missing]"; + return o.toString(); + } + + @Override + public boolean canHandleType(Class type) { + return type==Double.class || type==String.class || type==String[].class; + } +} diff --git a/timeflow/data/analysis/MissingValueAnalysis.java b/timeflow/data/analysis/MissingValueAnalysis.java new file mode 100755 index 0000000..abbaa51 --- /dev/null +++ b/timeflow/data/analysis/MissingValueAnalysis.java @@ -0,0 +1,44 @@ +package timeflow.data.analysis; + +import timeflow.data.analysis.DBAnalysis.*; +import timeflow.data.db.*; +import timeflow.data.db.filter.*; + +public class MissingValueAnalysis implements FieldAnalysis { + + int numNull; + int percent; + + @Override + public String getName() { + return "Missing/Blank Values"; + } + + @Override + public String[] getResultDescription() { + String s; + if (numNull==0) + s="No missing values"; + else if (numNull==1) + s= "One missing value"; + else + s=numNull+" missing values: "+percent+"%"; + return new String[] {s}; + } + + @Override + public InterestLevel perform(ActList acts, Field field) { + numNull=DBUtils.count(acts, new MissingValueFilter(field)); + percent=(int)Math.round(100*numNull/(double)acts.size()); + if (numNull==0) + return InterestLevel.IGNORE; + if (numNull<5) + return InterestLevel.VERY_INTERESTING; + return InterestLevel.INTERESTING; + } + + @Override + public boolean canHandleType(Class type) { + return true; + } +} diff --git a/timeflow/data/analysis/RangeDateAnalysis.java b/timeflow/data/analysis/RangeDateAnalysis.java new file mode 100755 index 0000000..955a62b --- /dev/null +++ b/timeflow/data/analysis/RangeDateAnalysis.java @@ -0,0 +1,62 @@ +package timeflow.data.analysis; + +import java.sql.Date; + +import timeflow.data.analysis.DBAnalysis.*; +import timeflow.data.db.*; +import timeflow.data.db.filter.*; +import timeflow.data.time.RoughTime; + +public class RangeDateAnalysis implements FieldAnalysis { + + String[] description; + + @Override + public String getName() { + return "Date Range"; + } + + @Override + public String[] getResultDescription() { + return description; + } + + @Override + public InterestLevel perform(ActList acts, Field field) { + long low=0; + long high=0; + + boolean defined=false; + for (Act a: acts) + { + if (a.get(field)==null) + continue; + long x=a.getTime(field).getTime(); + if (defined) + { + low=Math.min(low,x); + high=Math.max(high, x); + } else + { + defined=true; + low=x; + high=low; + } + } + if (defined) + description= new String[] + { + "Lowest value: "+new Date(low), + "Highest value: "+new Date(high), + }; + else + description=new String[] {"No values defined."}; + + return InterestLevel.INTERESTING; + } + + @Override + public boolean canHandleType(Class type) { + return type==RoughTime.class; + } +} diff --git a/timeflow/data/analysis/RangeNumberAnalysis.java b/timeflow/data/analysis/RangeNumberAnalysis.java new file mode 100755 index 0000000..9148110 --- /dev/null +++ b/timeflow/data/analysis/RangeNumberAnalysis.java @@ -0,0 +1,85 @@ +package timeflow.data.analysis; + +import timeflow.data.analysis.DBAnalysis.*; +import timeflow.data.db.*; +import timeflow.data.db.filter.*; +import java.text.*; + +public class RangeNumberAnalysis implements FieldAnalysis { + + String[] description; + static DecimalFormat intFormat=new DecimalFormat("###,###,###,###"); + static DecimalFormat df=new DecimalFormat("###,###,###,###.##"); + + @Override + public String getName() { + return "Value Range"; + } + + @Override + public String[] getResultDescription() { + return description; + } + + @Override + public InterestLevel perform(ActList acts, Field field) { + double low=0; + double high=0; + int numZero=0; + double sum=0; + int numDefined=0; + + boolean defined=false; + for (Act a: acts) + { + if (a.get(field)==null) + continue; + + double x=a.getValue(field); + numDefined++; + sum+=x; + + if (x==0) + numZero++; + if (defined) + { + low=Math.min(low,x); + high=Math.max(high, x); + } else + { + defined=true; + low=x; + high=low; + } + } + if (defined) + description= new String[] + { + "Average: "+df.format((sum/numDefined)), + "Lowest: "+df.format(low), + "Highest: "+df.format(high), + "Number of zero values: "+df.format(numZero) + }; + else + description=new String[] {"No values defined."}; + + return InterestLevel.INTERESTING; + } + + static String format(double x) + { + + if (Math.abs(x)>.1) + { + if (Math.round(x)-x<.01) + return intFormat.format(x); + return df.format(x); + } + return ""+x; + } + + @Override + public boolean canHandleType(Class type) { + return type==Double.class; + } +} diff --git a/timeflow/data/db/Act.java b/timeflow/data/db/Act.java new file mode 100755 index 0000000..66eb753 --- /dev/null +++ b/timeflow/data/db/Act.java @@ -0,0 +1,25 @@ +package timeflow.data.db; + +import timeflow.data.time.*; + +import java.net.URL; +import java.util.*; + +public interface Act { + + public ActDB getDB(); + + public Object get(Field field); + public double getValue(Field field); + public String getString(Field field); + public String[] getTextList(Field field); + public RoughTime getTime(Field field); + public URL getURL(Field field); + + public void set(Field field, Object value); + public void setText(Field field, String text); + public void setTextList(Field field, String[] list); + public void setValue(Field field, double value); + public void setTime(Field field, RoughTime time); + public void setURL(Field field, URL url); +} diff --git a/timeflow/data/db/ActComparator.java b/timeflow/data/db/ActComparator.java new file mode 100755 index 0000000..7632689 --- /dev/null +++ b/timeflow/data/db/ActComparator.java @@ -0,0 +1,118 @@ +package timeflow.data.db; + +import java.util.*; + +import timeflow.data.time.RoughTime; + +public abstract class ActComparator implements Comparator<Act> { + + protected Field field; + protected boolean ascending=true; + protected String description; + + + private ActComparator(Field field, String description) + { + this.field=field; + this.description=description; + } + + public String getDescription() + { + return description + (ascending ? "" : " (descending)"); + } + + public static ActComparator by(Field field) + { + Class type=field.getType(); + if (type==Double.class) + return new NumberComparator(field); + if (type==String[].class) + return new ArrayComparator(field); + if (type==RoughTime.class) + return new TimeComparator(field); + return new StringComparator(field); + } + + static class TimeComparator extends ActComparator + { + + TimeComparator(Field field) + { + super(field, "by time"); + } + + @Override + public int compare(Act o1, Act o2) { + RoughTime a1=o1.getTime(field); + RoughTime a2=o2.getTime(field); + if (a1==a2) + return 0; + if (a1==null) + return ascending ? 1 : -1; + if (a2==null) + return ascending ? -1 : 1; + int n=a1.compareTo(a2); + return ascending ? n : -n; + } + } + + + static class ArrayComparator extends ActComparator + { + + ArrayComparator(Field field) + { + super(field, "by length of "+field.getName()); + } + + @Override + public int compare(Act o1, Act o2) { + int n=length(o1.getTextList(field))-length(o2.getTextList(field)); + return ascending ? n : -n; + } + + static int length(String[] s) + { + return s==null ? 0 : s.length; + } + } + + static class StringComparator extends ActComparator + { + + StringComparator(Field field) + { + super(field, "by "+field.getName()); + } + + @Override + public int compare(Act o1, Act o2) { + int n=val(o1.getString(field)).toString().compareTo(val(o2.getString(field)).toString()); + return ascending ? n : -n; + } + + String val(String s) + { + return s==null ? "" : s; + } + } + + static class NumberComparator extends ActComparator + { + + NumberComparator(Field field) + { + super(field, "by "+field.getName()); + } + + @Override + public int compare(Act o1, Act o2) { + double x=o1.getValue(field)-o2.getValue(field); + int n=x>0 ? 1 : x<0 ? -1 : 0; + return ascending ? n : -n; + } + + + } +} diff --git a/timeflow/data/db/ActDB.java b/timeflow/data/db/ActDB.java new file mode 100755 index 0000000..46e4bb1 --- /dev/null +++ b/timeflow/data/db/ActDB.java @@ -0,0 +1,30 @@ +package timeflow.data.db; + +import java.util.*; + +import timeflow.data.db.filter.ActFilter; + +public interface ActDB extends Iterable<Act> { + + public String getSource(); + public String getDescription(); + public void setSource(String source); + public void setDescription(String description); + + public List<String> getFieldKeys(); + public List<Field> getFields(); + public List<Field> getFields(Class type); + public Field addField(String name, Class type); + public Field getField(String name); + public void deleteField(Field field); + public void setAlias(Field field, String name); + public void setNewFieldOrder(List<Field> newOrder); + public void renameField(Field field, String name); + + public void delete(Act act); + public Act createAct(); + public ActList select(ActFilter filter); + public ActList all(); + public int size(); + public Act get(int i); +} diff --git a/timeflow/data/db/ActList.java b/timeflow/data/db/ActList.java new file mode 100755 index 0000000..53b9a54 --- /dev/null +++ b/timeflow/data/db/ActList.java @@ -0,0 +1,23 @@ +package timeflow.data.db; + +import java.util.*; + +public class ActList extends ArrayList<Act> { + + private ActDB db; + + public ActList(ActDB db) + { + this.db=db; + } + + public ActDB getDB() + { + return db; + } + + public ActList copy() + { + return (ActList)clone(); + } +} diff --git a/timeflow/data/db/ArrayDB.java b/timeflow/data/db/ArrayDB.java new file mode 100755 index 0000000..b8b570a --- /dev/null +++ b/timeflow/data/db/ArrayDB.java @@ -0,0 +1,348 @@ +package timeflow.data.db; + +import java.net.URL; +import java.util.*; + +import timeflow.data.db.filter.*; +import timeflow.data.time.*; + +public class ArrayDB implements ActDB +{ + + private Schema schema; + private List<Act> data = new ArrayList<Act>(); + private Field[] fields; + private String source = "[unknown]"; + private String description = ""; + + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + } + + public String getSource() + { + return source; + } + + public void setSource(String source) + { + this.source = source; + } + + @Override + public void setAlias(Field field, String name) + { + schema.addAlias(field, name); + } + + public ArrayDB(String[] fieldNames, Class[] types, String source) + { + this.schema = new Schema(); + this.source = source; + int n = fieldNames.length; + fields = new Field[n]; + for (int i = 0; i < n; i++) + { + fields[i] = schema.add(fieldNames[i], types[i]); + fields[i].index = i; + } + } + + public Field[] getFieldArray() + { + return fields; + } + + @Override + public Field addField(String name, Class type) + { + + int n = fields.length; + + // make new Field. + Field field = new Field(name, type); + field.index = n; + + // make new array of fields. + Field[] moreFields = new Field[n + 1]; + System.arraycopy(fields, 0, moreFields, 0, n); + moreFields[n] = field; + this.fields = moreFields; + + // go through all the data items and expand their arrays, too. + for (Act d : data) + { + IndexedAct item = (IndexedAct) d; + Object[] old = item.data; + item.data = new Object[n + 1]; + System.arraycopy(old, 0, item.data, 0, n); + } + + //System.out.println("Field added: "+field); + schema.add(field); + + return field; + } + + public Field getField(String name) + { + return schema.getField(name); + } + + @Override + public ActList all() + { + return select(null); + } + + @Override + public Act createAct() + { + IndexedAct act = new IndexedAct(this, fields.length); + data.add(act); + return act; + } + + @Override + public void delete(Act act) + { + data.remove(act); + } + + @Override + public void deleteField(Field deadField) + { + + System.out.println("Deleting: " + deadField); + + schema.delete(deadField); + int n = fields.length; + int m = deadField.index; + + // make new array of fields. + Field[] fewerFields = new Field[n - 1]; + removeItem(fields, fewerFields, m); + fields = fewerFields; + + // go through all the data items and contract their arrays, too. + for (Act d : data) + { + IndexedAct item = (IndexedAct) d; + Object[] old = item.data; + item.data = new Object[n - 1]; + removeItem(old, item.data, m); + } + + // change field indices + for (int i = 0; i < fields.length; i++) + { + System.out.println("fields[" + i + "]=" + fields[i]); + if (fields[i].index > deadField.index) + { + fields[i].index--; + } + } + } + + private static void removeItem(Object[] a, Object[] b, int m) + { + int n = a.length; + if (m > 0) + { + System.arraycopy(a, 0, b, 0, m); + } + if (m < n - 1) + { + System.arraycopy(a, m + 1, b, m, n - m - 1); + } + } + + @Override + public List<Field> getFields(Class type) + { + return schema.getFields(type); + } + + @Override + public ActList select(ActFilter filter) + { + ActList set = new ActList(this); + for (Act a : data) + { + if (filter == null || filter.accept(a)) + { + set.add(a); + } + } + return set; + } + + @Override + public List<Field> getFields() + { + return schema.getFields(); + } + + @Override + public Act get(int i) + { + return data.get(i); + } + + @Override + public int size() + { + return data.size(); + } + + @Override + public Iterator<Act> iterator() + { + return data.iterator(); + } + + @Override + public List<String> getFieldKeys() + { + return schema.getKeys(); + } + + @Override + public void setNewFieldOrder(List<Field> newOrder) + { + schema.setNewFieldOrder(newOrder); + } + + class IndexedAct implements Act + { + + Object[] data; + ActDB db; + + IndexedAct(ActDB db, int numFields) + { + this.db = db; + data = new Object[numFields]; + } + + @Override + public String getString(Field field) + { + + Object obj = data[field.index]; + + if (obj == null) + return null; + + if (obj instanceof String[]) + { + String[] strings = (String[]) obj; + String string = ""; + for (String s : strings) + { + string += s + ", "; + } + return string; + } else + { + return obj.toString(); + } + } + + public void setText(Field field, String text) + { + data[field.index] = text; + } + + @Override + public String[] getTextList(Field field) + { + Object obj = data[field.index]; + + if (obj == null) + return null; + + if (obj instanceof String[]) + { + return (String[]) obj; + } else + { + return new String[] + { + obj.toString() + }; + } + } + + public void setTextList(Field field, String[] list) + { + data[field.index] = list; + } + + @Override + public double getValue(Field field) + { + Double d = (Double) data[field.index]; + return d == null ? Double.NaN : d.doubleValue(); + } + + public void setValue(Field field, double value) + { + data[field.index] = value; + } + + @Override + public Object get(Field field) + { + return data[field.index]; + } + + @Override + public ActDB getDB() + { + return db; + } + + @Override + public void set(Field field, Object value) + { + data[field.index] = value; + } + + @Override + public RoughTime getTime(Field field) + { + return (RoughTime) data[field.index]; + } + + @Override + public void setTime(Field field, RoughTime time) + { + data[field.index] = time; + } + + @Override + public URL getURL(Field field) + { + return (URL) data[field.index]; + } + + @Override + public void setURL(Field field, URL url) + { + data[field.index] = url; + } + } + + @Override + public void renameField(Field field, String name) + { + schema.renameField(field, name); + } +} diff --git a/timeflow/data/db/BasicAct.java b/timeflow/data/db/BasicAct.java new file mode 100755 index 0000000..48e3eb9 --- /dev/null +++ b/timeflow/data/db/BasicAct.java @@ -0,0 +1,89 @@ +package timeflow.data.db; + +import timeflow.data.db.*; +import timeflow.data.time.*; + +import java.net.URL; +import java.util.*; + +public class BasicAct implements Act { + + private HashMap data=new HashMap(); + private ActDB db; + + public BasicAct(ActDB db) + { + this.db=db; + } + + + @Override + public String getString(Field field) { + return (String)data.get(field.getName()); + } + + public void setText(Field field, String text) + { + data.put(field.getName(), text); + } + + @Override + public String[] getTextList(Field field) { + return (String[])data.get(field.getName()); + } + + public void setTextList(Field field, String[] list){ + data.put(field.getName(), list); + } + + @Override + public double getValue(Field field) { + return (Double)data.get(field.getName()); + } + + public void setValue(Field field, double value) + { + data.put(field.getName(), value); + } + + @Override + public Object get(Field field) { + return data.get(field.getName()); + } + + @Override + public ActDB getDB() { + return db; + } + + @Override + public void set(Field field, Object value) { + data.put(field.getName(), value); + } + + + @Override + public RoughTime getTime(Field field) { + return (RoughTime)data.get(field.getName()); + } + + + @Override + public void setTime(Field field, RoughTime time) { + data.put(field.getName(), time); + + } + + + @Override + public URL getURL(Field field) { + return (URL)data.get(field.getName()); + } + + + @Override + public void setURL(Field field, URL url) { + data.put(field.getName(), url); + } + +} diff --git a/timeflow/data/db/BasicDB.java b/timeflow/data/db/BasicDB.java new file mode 100755 index 0000000..bcd3ba2 --- /dev/null +++ b/timeflow/data/db/BasicDB.java @@ -0,0 +1,129 @@ +package timeflow.data.db; + +import java.util.*; + +import timeflow.data.db.*; +import timeflow.data.db.filter.ActFilter; + +public class BasicDB implements ActDB { + + private Schema schema; + private List<Act> data=new ArrayList<Act>(); + private String source="[unknown]"; + private String description=""; + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public BasicDB(String source) + { + this(new Schema(), source); + } + + public BasicDB(Schema schema, String source) + { + this.schema=schema; + this.source=source; + } + + @Override + public Field addField(String name, Class type) { + Field field=new Field(name, type); + schema.add(field); + return field; + } + + public Field getField(String name) + { + return schema.getField(name); + } + + @Override + public ActList all() { + return select(null); + } + + @Override + public Act createAct() { + BasicAct act=new BasicAct(this); + data.add(act); + return act; + } + + @Override + public void delete(Act act) { + data.remove(act); + } + + @Override + public void deleteField(Field field) { + schema.delete(field); + } + + @Override + public List<Field> getFields(Class type) { + return schema.getFields(type); + } + + @Override + public ActList select(ActFilter filter) { + ActList set=new ActList(this); + for (Act a: data) + if (filter==null || filter.accept(a)) + set.add(a); + return set; + } + + @Override + public List<Field> getFields() { + return schema.getFields(); + } + + @Override + public Act get(int i) { + return data.get(i); + } + + @Override + public int size() { + return data.size(); + } + + @Override + public Iterator<Act> iterator() { + return data.iterator(); + } + + @Override + public void setAlias(Field field, String name) { + schema.addAlias(field,name); + } + + @Override + public List<String> getFieldKeys() { + return schema.getKeys(); + } + + @Override + public void setNewFieldOrder(List<Field> newOrder) { + schema.setNewFieldOrder(newOrder); + } + + @Override + public void renameField(Field field, String name) { + schema.renameField(field, name); + } + +} diff --git a/timeflow/data/db/DBUtils.java b/timeflow/data/db/DBUtils.java new file mode 100755 index 0000000..3a9712e --- /dev/null +++ b/timeflow/data/db/DBUtils.java @@ -0,0 +1,234 @@ +package timeflow.data.db; + +import timeflow.data.db.filter.*; +import timeflow.data.time.*; +import timeflow.util.*; + +import java.util.*; + +public class DBUtils +{ + + public static void dump(Act act) + { + List<Field> fields = act.getDB().getFields(); + for (Field f : fields) + { + System.out.println(f.getName() + " = " + act.get(f)); + } + } + + public static Object get(Act act, String field) + { + return act.get(act.getDB().getField(field)); + } + + public static List<String> getFieldAliases(ActDB db) + { + ArrayList<String> list = new ArrayList<String>(); + for (String s : db.getFieldKeys()) + { + if (!db.getField(s).getName().equals(s)) + { + list.add(s); + } + } + return list; + } + + public static Interval range(ActList a, Field[] fields) + { + if (fields == null || fields.length == 0) + { + return new Interval(0, 0); + } + Interval t = null; + for (Act act : a) + { + for (Field f : fields) + { + RoughTime d = act.getTime(f); + if (d != null && d.isDefined()) + { + if (t == null) + { + t = new Interval(d.getTime(), d.getTime()); + } else + { + t.include(d.getTime()); + } + } + } + } + return t != null ? t : new Interval(RoughTime.UNKNOWN, RoughTime.UNKNOWN); + } + + public static Interval range(ActList a, String fieldName) + { + + Field field = a.getDB().getField(fieldName); + if (field == null || a.size() == 0) + { + return new Interval(0, 0); + } + Interval t = null; + for (Act act : a) + { + RoughTime d = act.getTime(field); + if (d != null && d.isDefined()) + { + if (t == null) + { + t = new Interval(d.getTime(), d.getTime()); + } else + { + t.include(d.getTime()); + } + } + } + return t != null ? t : new Interval(RoughTime.UNKNOWN, RoughTime.UNKNOWN); + } + + public static List<Field> categoryFields(ActDB db) + { + List<Field> list = new ArrayList<Field>(); + list.addAll(db.getFields()); +// list.addAll(db.getFields(String.class)); +// list.addAll(db.getFields(String[].class)); + return list; + } + + public static int count(Iterable<Act> acts, Interval i, Field field)//String fieldName) + { + return count(acts, new TimeIntervalFilter(i, field)); + } + + public static int count(Iterable<Act> acts, ActFilter filter) + { + int num = 0; + for (Act a : acts) + { + if (filter.accept(a)) + { + num++; + } + } + return num; + } + + public static double[] minmax(Iterable<Act> acts, Field field) + { + double min = Double.NaN; + double max = min; + for (Act a : acts) + { + double x = a.getValue(field); + if (Double.isNaN(min)) + { + min = x; + max = x; + } else if (!Double.isNaN(x)) + { + min = Math.min(x, min); + max = Math.max(x, max); + } + } + return new double[] + { + min, max + }; + } + + public static double[] getValues(Iterable<Act> acts, Field field) + { + ArrayList<Double> list = new ArrayList<Double>(); + if (field.getType() == Double.class) + { + for (Act a : acts) + { + list.add(a.getValue(field)); + } + } else if (field.getType() == RoughTime.class) + { + for (Act a : acts) + { + RoughTime r = a.getTime(field); + if (r != null) + { + list.add(new Double(r.getTime())); + } + } + } + int n = list.size(); + double[] x = new double[n]; + for (int i = 0; i < n; i++) + { + x[i] = list.get(i); + } + return x; + } + + public static Bag<String> countValues(Iterable<Act> acts, Field field) + { + Bag<String> bag = new Bag<String>(); + if (field.getType() != String[].class) + { + for (Act a : acts) + { + bag.add(a.getString(field)); + } + } + else // if (field.getType() == String[].class) + { + for (Act a : acts) + { + String[] s = a.getTextList(field); + if (s != null) + { + for (int i = 0; i < s.length; i++) + { + bag.add(s[i]); + } + } + } +// } else +// { +// throw new IllegalArgumentException("Asked to count values for non-text field: " + field); + } + return bag; + } + + public static void setRecSizesFromCurrent(ActDB db) + { + // for String fields. + for (Field f : db.getFields(String.class)) + { + int max = 0; + for (Act a : db) + { + String s = a.getString(f); + if (s != null) + { + max = Math.max(s.length(), max); + } + } + f.setRecommendedSize(max); + } + } + + public static Field ensureField(ActDB db, String name, Class type) + { + Field f = db.getField(name); + if (f == null) + { + return db.addField(name, type); + } else + { + if (f.getType() != type) + { + throw new IllegalArgumentException("Mismatched types: got " + type + ", expected " + f.getType()); + } + } + return f; + } +} diff --git a/timeflow/data/db/Field.java b/timeflow/data/db/Field.java new file mode 100755 index 0000000..9fac035 --- /dev/null +++ b/timeflow/data/db/Field.java @@ -0,0 +1,50 @@ +package timeflow.data.db; + + +public class Field { + private String name; + private Class type; + int index; + private int recommendedSize=-1; + + public Field(String name, Class type) + { + this.name=name; + this.type=type; + } + + public Field(String name, Class type, int recommendedSize) + { + this.name=name; + this.type=type; + this.recommendedSize=recommendedSize; + } + + public int getRecommendedSize() { + return recommendedSize; + } + + public void setRecommendedSize(int recommendedSize) { + this.recommendedSize = recommendedSize; + } + + void setName(String name) + { + this.name=name; + } + + public String getName() + { + return name; + } + + public Class getType() + { + return type; + } + + public String toString() + { + return "[Field: name='"+name+"', type="+type+", index="+index+"]"; + } +} diff --git a/timeflow/data/db/Schema.java b/timeflow/data/db/Schema.java new file mode 100755 index 0000000..9fdcc87 --- /dev/null +++ b/timeflow/data/db/Schema.java @@ -0,0 +1,130 @@ +package timeflow.data.db; + +import java.util.*; + +// methods are public for testing purposes. +public class Schema implements Iterable<Field> +{ + + private Map<String, Field> schema = new HashMap<String, Field>(); + private List<Field> fieldList = new ArrayList<Field>(); // so we preserve field order. + + public Iterator<Field> iterator() + { + return fieldList.iterator(); + } + + public Field getField(String key) + { + return schema.get(key); + } + + public List<String> getKeys() + { + return new ArrayList(schema.keySet()); + } + + public List<Field> getFields(Class type) + { + List<Field> a = new ArrayList<Field>(); + for (Field s : fieldList) + { + if (type == null || s.getType() == type) + { + a.add(s); + } + } + return a; + } + + public List<Field> getFields() + { + return getFields(null); + } + + // not sure this actually works! removing things while iterating? to-do: test! + public void delete(Field field) + { + if (schema.get(field.getName()) == null) + { + throw new IllegalArgumentException("No field exists: " + field); + } + + Set<String> keys = new HashSet<String>(schema.keySet()); + for (String s : keys) + { + Field f = schema.get(s); + if (f == field) + { + schema.remove(s); + } + } + + fieldList.remove(field); + } + + public void addAlias(Field field, String name) + { + if (field == null) + { + schema.remove(name); + return; + } + if (!schema.values().contains(field)) + { + throw new IllegalArgumentException("Field does not exist in schema: " + field); + } + schema.put(name, field); + } + + public Field add(String name, Class type) + { + return add(new Field(name, type)); + } + + public Field add(Field field) + { + if (schema.get(field.getName()) != null) + { + throw new IllegalArgumentException("Schema already has field named '" + field.getName() + + "', type=" + field.getType()); + } + schema.put(field.getName(), field); + fieldList.add(field); + return field; + } + + public void setNewFieldOrder(List<Field> newOrder) + { + // first, we go through and check that this really is a new ordering! + if (newOrder.size() != fieldList.size()) + { + throw new IllegalArgumentException("Field lists have different sizes"); + } + for (Field f : newOrder) + { + if (!fieldList.contains(f)) + { + throw new IllegalArgumentException("New field list has unexpected field: " + f); + } + } + fieldList = newOrder; + } + + public void print() + { + System.out.println(schema); + } + + public void renameField(Field field, String name) + { + Field old = schema.get(name); + if (old != null && old != field) + { + throw new IllegalArgumentException("Can't rename a field to a name that already exists: " + name); + } + schema.remove(field); + field.setName(name); + schema.put(name, field); + } +} diff --git a/timeflow/data/db/filter/ActFilter.java b/timeflow/data/db/filter/ActFilter.java new file mode 100755 index 0000000..c23978e --- /dev/null +++ b/timeflow/data/db/filter/ActFilter.java @@ -0,0 +1,14 @@ +package timeflow.data.db.filter; + +import timeflow.data.db.Act; + +public abstract class ActFilter { + public abstract boolean accept(Act act); + + // in earlier versions we've wanted the UI to count the number of filters applied. + // because of the hierarchical way filters are defined, we need this method. + public int countFilters() + { + return 1; + } +} diff --git a/timeflow/data/db/filter/AndFilter.java b/timeflow/data/db/filter/AndFilter.java new file mode 100755 index 0000000..ecdef4b --- /dev/null +++ b/timeflow/data/db/filter/AndFilter.java @@ -0,0 +1,50 @@ +package timeflow.data.db.filter; + +import java.util.*; + +import timeflow.data.db.Act; + +public class AndFilter extends ActFilter { + private List<ActFilter> filters; + + public AndFilter() + { + } + + public AndFilter(ActFilter a, ActFilter b) + { + filters=new ArrayList<ActFilter>(); + and(a); + and(b); + } + + public void and(ActFilter a) + { + if (a==null) + return; + if (filters==null) + filters=new ArrayList<ActFilter>(); + filters.add(a); + } + + @Override + public boolean accept(Act act) { + if (filters!=null) + for (ActFilter f: filters) + if (!f.accept(act)) + return false; + return true; + } + + public int countFilters() + { + int sum=0; + if (filters!=null) + for (ActFilter f: filters) + if (f!=null) + sum+=f.countFilters(); + return sum; + } + + +} diff --git a/timeflow/data/db/filter/ConstFilter.java b/timeflow/data/db/filter/ConstFilter.java new file mode 100755 index 0000000..fd8d821 --- /dev/null +++ b/timeflow/data/db/filter/ConstFilter.java @@ -0,0 +1,24 @@ +package timeflow.data.db.filter; + +import timeflow.data.db.Act; + +public class ConstFilter extends ActFilter { + + boolean result; + + public ConstFilter(boolean result) + { + this.result=result; + } + + @Override + public boolean accept(Act act) { + return result; + } + + public int countFilters() + { + return result ? 0 : 1; + } + +} diff --git a/timeflow/data/db/filter/FieldValueFilter.java b/timeflow/data/db/filter/FieldValueFilter.java new file mode 100755 index 0000000..28c8af9 --- /dev/null +++ b/timeflow/data/db/filter/FieldValueFilter.java @@ -0,0 +1,37 @@ +package timeflow.data.db.filter; + +import timeflow.data.db.*; + +public class FieldValueFilter extends ActFilter implements ValueFilter { + + private Field field; + private Object value; + + public FieldValueFilter(Field field, Object value) + { + this.field=field; + this.value=value; + } + + public boolean ok(Object o) + { + if (o==null) + return value==null; + if (o.equals(value)) + return true; + if (o instanceof Object[]) + { + Object[] s=(Object[] )o; + for (int i=0; i<s.length; i++) + if (s[i].equals(value)) + return true; + } + return false; + } + + @Override + public boolean accept(Act act) { + return ok(act.get(field)); + } + +} diff --git a/timeflow/data/db/filter/FieldValueSetFilter.java b/timeflow/data/db/filter/FieldValueSetFilter.java new file mode 100755 index 0000000..6168213 --- /dev/null +++ b/timeflow/data/db/filter/FieldValueSetFilter.java @@ -0,0 +1,40 @@ +package timeflow.data.db.filter; + +import timeflow.data.db.*; + +import java.util.*; + +public class FieldValueSetFilter extends ActFilter implements ValueFilter { + + private Field field; + private Set valueSet; + + public FieldValueSetFilter(Field field) + { + this.field=field; + valueSet=new HashSet(); + } + + public void addValue(Object value) + { + valueSet.add(value); + } + + @Override + public boolean ok(Object o) { + if (o instanceof Object[]) + { + Object[] s=(Object[] )o; + for (int i=0; i<s.length; i++) + if (valueSet.contains(s[i])) + return true; + } + return valueSet.contains(o); + } + + @Override + public boolean accept(Act act) { + return ok(act.get(field)); + } + +} diff --git a/timeflow/data/db/filter/MissingValueFilter.java b/timeflow/data/db/filter/MissingValueFilter.java new file mode 100755 index 0000000..f4a4c7d --- /dev/null +++ b/timeflow/data/db/filter/MissingValueFilter.java @@ -0,0 +1,27 @@ +package timeflow.data.db.filter; + +import timeflow.data.db.Act; +import timeflow.data.db.Field; + +public class MissingValueFilter extends ActFilter { + private Field field; + private boolean text, array, number; + + public MissingValueFilter(Field field) + { + this.field=field; + text=field.getType()==String.class; + array=field.getType()==String[].class; + number=field.getType()==Double.class; + } + + @Override + public boolean accept(Act act) { + Object o=act.get(field); + return o==null || + number && Double.isNaN(((Double)o).doubleValue()) || + text && "".equals(o) || + array && ((String[])o).length==0; + } + +} diff --git a/timeflow/data/db/filter/NotFilter.java b/timeflow/data/db/filter/NotFilter.java new file mode 100755 index 0000000..406430b --- /dev/null +++ b/timeflow/data/db/filter/NotFilter.java @@ -0,0 +1,24 @@ +package timeflow.data.db.filter; + +import java.util.*; + +import timeflow.data.db.Act; + +public class NotFilter extends ActFilter { + private ActFilter f; + + public NotFilter(ActFilter f) + { + this.f=f; + } + + @Override + public boolean accept(Act act) { + return f!=null && !f.accept(act); + } + + public int countFilters() + { + return 1+f.countFilters(); + } +} diff --git a/timeflow/data/db/filter/NumericRangeFilter.java b/timeflow/data/db/filter/NumericRangeFilter.java new file mode 100755 index 0000000..b7f74f9 --- /dev/null +++ b/timeflow/data/db/filter/NumericRangeFilter.java @@ -0,0 +1,28 @@ +package timeflow.data.db.filter; + +import timeflow.data.db.*; +import timeflow.data.time.*; + +public class NumericRangeFilter extends ActFilter { + + double low, high; + Field field; + boolean acceptNull; + + public NumericRangeFilter(Field field, double low, double high, boolean acceptNull) + { + this.low=low; + this.high=high; + this.field=field; + this.acceptNull=acceptNull; + } + + @Override + public boolean accept(Act act) { + if (field==null) + return false; + double x=act.getValue(field); + return Double.isNaN(x) && acceptNull || x>=low && x<=high; + } + +} diff --git a/timeflow/data/db/filter/OrFilter.java b/timeflow/data/db/filter/OrFilter.java new file mode 100755 index 0000000..6161138 --- /dev/null +++ b/timeflow/data/db/filter/OrFilter.java @@ -0,0 +1,38 @@ +package timeflow.data.db.filter; + +import java.util.*; + +import timeflow.data.db.Act; + +public class OrFilter extends ActFilter { + private List<ActFilter> filters=new ArrayList<ActFilter>(); + + public OrFilter(ActFilter a, ActFilter b) + { + or(a); + or(b); + } + + public void or(ActFilter a) + { + filters.add(a); + } + + @Override + public boolean accept(Act act) { + for (ActFilter f: filters) + if (f.accept(act)) + return true; + return false; + } + public int countFilters() + { + int sum=0; + if (filters!=null) + for (ActFilter f: filters) + if (f!=null) + sum+=f.countFilters(); + return sum; + } + +} diff --git a/timeflow/data/db/filter/StringMatchFilter.java b/timeflow/data/db/filter/StringMatchFilter.java new file mode 100755 index 0000000..6b9d650 --- /dev/null +++ b/timeflow/data/db/filter/StringMatchFilter.java @@ -0,0 +1,69 @@ +package timeflow.data.db.filter; + +import timeflow.data.db.*; +import timeflow.data.time.*; + +import java.util.regex.*; + +public class StringMatchFilter extends ActFilter { + + private Field[] textFields; + private Field[] listFields; + private String query=""; + private boolean isRegex=false; + private Pattern pattern; + + public StringMatchFilter(ActDB db, boolean isRegex) + { + this(db,"", isRegex); + } + + public StringMatchFilter(ActDB db, String query, boolean isRegex) + { + textFields=(Field[])db.getFields(String.class).toArray(new Field[0]); + listFields=(Field[])db.getFields(String[].class).toArray(new Field[0]); + this.isRegex=isRegex; + setQuery(query); + } + + public String getQuery() + { + return query; + } + + public void setQuery(String query) + { + this.query=query; + if (isRegex) + { + pattern=Pattern.compile(query, Pattern.CASE_INSENSITIVE+Pattern.MULTILINE+Pattern.DOTALL); + } + else + this.query=query.toLowerCase(); + } + + @Override + public boolean accept(Act act) { + // check text fields + for (int i=0; i<textFields.length; i++) + { + String s=act.getString(textFields[i]); + if (s==null) continue; + if (isRegex ? pattern.matcher(s).find() : s.toLowerCase().contains(query)) + return true; + } + // check list fields + for (int j=0; j<listFields.length; j++) + { + String[] m=act.getTextList(listFields[j]); + if (m!=null) + for (int i=0; i<m.length; i++) + { + String s=m[i]; + if (isRegex ? pattern.matcher(s).find() : s.toLowerCase().contains(query)) + return true; + } + } + return false; + } +} diff --git a/timeflow/data/db/filter/TimeIntervalFilter.java b/timeflow/data/db/filter/TimeIntervalFilter.java new file mode 100755 index 0000000..c2a275d --- /dev/null +++ b/timeflow/data/db/filter/TimeIntervalFilter.java @@ -0,0 +1,35 @@ +package timeflow.data.db.filter; + +import timeflow.data.db.*; +import timeflow.data.time.*; + +public class TimeIntervalFilter extends ActFilter { + + Interval interval; + Field timeField; + boolean acceptNull; + + public TimeIntervalFilter(long start, long end, boolean acceptNull, Field timeField) + { + this.interval=new Interval(start, end); + this.acceptNull=acceptNull; + this.timeField=timeField; + } + + public TimeIntervalFilter(Interval interval, Field timeField) + { + this.interval=interval; + this.timeField=timeField; + } + + @Override + public boolean accept(Act act) { + if (timeField==null) + return false; + RoughTime t=act.getTime(timeField); + if (t==null) + return acceptNull; + return interval.contains(t.getTime()); + } + +} diff --git a/timeflow/data/db/filter/ValueFilter.java b/timeflow/data/db/filter/ValueFilter.java new file mode 100755 index 0000000..9e09b0c --- /dev/null +++ b/timeflow/data/db/filter/ValueFilter.java @@ -0,0 +1,5 @@ +package timeflow.data.db.filter; + +public interface ValueFilter { + public boolean ok(Object o); +} diff --git a/timeflow/data/time/Interval.java b/timeflow/data/time/Interval.java new file mode 100755 index 0000000..49a53d5 --- /dev/null +++ b/timeflow/data/time/Interval.java @@ -0,0 +1,114 @@ +package timeflow.data.time; + +import java.util.*; + +public class Interval +{ + + public long start; + public long end; + + public Interval(long start, long end) + { + this.start = start; + this.end = end; + } + + public Interval copy() + { + return new Interval(start, end); + } + + public boolean contains(long x) + { + return x >= start && x <= end; + } + + public boolean intersects(Interval x) + { + return intersects(x.start, x.end); + } + + public boolean intersects(long start1, long end1) + { + return start1 <= end && end1 >= start; + } + + public Interval subinterval(double startFraction, double endFraction) + { + return new Interval((long) (start + startFraction * length()), + (long) (start + endFraction * length())); + } + + public void setTo(long start, long end) + { + this.start = start; + this.end = end; + } + + public void setTo(Interval t) + { + start = t.start; + end = t.end; + } + + public void include(long time) + { + start = Math.min(start, time); + end = Math.max(end, time); + } + + public void include(Interval t) + { + include(t.start); + include(t.end); + } + + public void expand(long amount) + { + start -= amount; + end += amount; + } + + public void add(long amount) + { + start += amount; + end += amount; + } + + public long length() + { + return end - start; + } + + public void translateTo(long newStart) + { + add(newStart - start); + } + + public Interval intersection(Interval i) + { + start = Math.max(i.start, start); + end = Math.min(i.end, end); + return this; + } + + public void clampInside(Interval container) + { + if (length() > container.length()) + { + throw new IllegalArgumentException("Containing interval too small: " + container + " < " + this); + } + if (start >= container.start && end <= container.end) + { + return; + } + add(Math.max(0, container.start - start)); + add(Math.min(0, container.end - end)); + } + + public String toString() + { + return "[Interval: From " + new Date(start) + " to " + new Date(end) + "]"; + } +} diff --git a/timeflow/data/time/RoughTime.java b/timeflow/data/time/RoughTime.java new file mode 100755 index 0000000..9cadc5a --- /dev/null +++ b/timeflow/data/time/RoughTime.java @@ -0,0 +1,128 @@ +package timeflow.data.time; + +import java.util.Calendar; +import java.util.Date; + +public class RoughTime implements Comparable { + + public static final long UNKNOWN=Long.MIN_VALUE; + private TimeUnit units; + private long time; + + public RoughTime(TimeUnit units) + { + time=UNKNOWN; + this.units=units; + } + + public RoughTime(long time, TimeUnit units) + { + this.time=time; + this.units=units; + } + + public boolean isDefined() + { + return time!=UNKNOWN; + } + + public long getTime() + { + return time; + } + + public void setTime(long time) + { + this.time=time; + } + + public Date toDate() + { + return new Date(time); + } + + public boolean after(RoughTime t) + { + return t.time<time; + } + + public boolean before(RoughTime t) + { + return t.time>time; + } + + public RoughTime plus(int numUnits) + { + return plus(units, numUnits); + } + + public RoughTime plus(TimeUnit unit, int times) + { + RoughTime r=copy(); + unit.addTo(r,times); + return r; + } + + public String toString() + { + if (isKnown()) + return new Date(time).toString(); + return "unknown"; + } + + public boolean isKnown() + { + return time!=UNKNOWN; + } + + public boolean equals(Object o) + { + if (!(o instanceof RoughTime)) + return false; + RoughTime t=(RoughTime)o; + return t.units==units && t.time==time; + } + + public RoughTime copy() + { + RoughTime t=new RoughTime(time, units); + return t; + } + + public void setUnits(TimeUnit units) + { + this.units=units; + } + + public TimeUnit getUnits() { + return units; + } + + public String format() + { + return units.formatFull(time); + } + + public static int compare(RoughTime t1, RoughTime t2) + { + if (t1==t2) + return 0; + if (t1==null) + return -1; + if (t2==null) + return 1; + long dt= t1.time-t2.time; + if (dt==0) + return 0; + if (dt>0) + return 1; + return -1; + } + + @Override + public int compareTo(Object o) { + return compare(this, (RoughTime)o); + } + + +} diff --git a/timeflow/data/time/TimeUnit.java b/timeflow/data/time/TimeUnit.java new file mode 100755 index 0000000..760b3be --- /dev/null +++ b/timeflow/data/time/TimeUnit.java @@ -0,0 +1,243 @@ +package timeflow.data.time; + +import java.util.*; +import java.text.*; + +public class TimeUnit { + + public static final TimeUnit YEAR=new TimeUnit("Years", Calendar.YEAR, 365*24*60*60*1000L, "yyyy", "yyyy"); + public static final TimeUnit MONTH=new TimeUnit("Months", Calendar.MONTH, 30*24*60*60*1000L, "MMM", "MMM yyyy"); + public static final TimeUnit WEEK=new TimeUnit("Weeks", Calendar.WEEK_OF_YEAR, 7*24*60*60*1000L, "d", "MMM d yyyy"); + public static final TimeUnit DAY=new TimeUnit("Days", Calendar.DAY_OF_MONTH, 24*60*60*1000L, "d", "MMM d yyyy"); + public static final TimeUnit DAY_OF_WEEK=new TimeUnit("Days", Calendar.DAY_OF_WEEK, 24*60*60*1000L, "d", "MMM d yyyy"); + public static final TimeUnit HOUR=new TimeUnit("Hours", Calendar.HOUR_OF_DAY, 60*60*1000L, "kk:mm", "MMM d yyyy kk:mm"); + public static final TimeUnit MINUTE=new TimeUnit("Minutes", Calendar.MINUTE, 60*1000L, ":mm", "MMM d yyyy kk:mm"); + public static final TimeUnit SECOND=new TimeUnit("Seconds", Calendar.SECOND, 1000L, ":ss", "MMM d yyyy kk:mm:ss"); + public static final TimeUnit DECADE=multipleYears(10); + public static final TimeUnit CENTURY=multipleYears(100); + + private static final double DAY_SIZE=24*60*60*1000L; + + private int quantity; + private long roughSize; + private SimpleDateFormat format, fullFormat; + private String name; + private int calendarCode; + + private TimeUnit() + { + } + + private TimeUnit(String name, int calendarCode, long roughSize, String formatPattern, String fullFormatPattern) + { + this.name=name; + this.calendarCode=calendarCode; + this.roughSize=roughSize; + format=new SimpleDateFormat(formatPattern); + fullFormat=new SimpleDateFormat(fullFormatPattern); + quantity=1; + } + + public String toString() + { + return "[TimeUnit: "+name+"]"; + } + + public static TimeUnit multipleYears(int numYears) + { + TimeUnit t=new TimeUnit(); + t.name=numYears+" Years"; + t.calendarCode=Calendar.YEAR; + t.roughSize=YEAR.roughSize*numYears; + t.format=YEAR.format; + t.fullFormat=YEAR.fullFormat; + t.quantity=numYears; + return t; + } + + public static TimeUnit multipleWeeks(int num) + { + TimeUnit t=new TimeUnit(); + t.name=num+" Weeks"; + t.calendarCode=Calendar.WEEK_OF_YEAR; + t.roughSize=WEEK.roughSize*num; + t.format=WEEK.format; + t.fullFormat=WEEK.fullFormat; + t.quantity=num; + return t; + } + + public TimeUnit times(int quantity) + { + TimeUnit t=new TimeUnit(); + t.name=quantity+" "+this.name; + t.calendarCode=this.calendarCode; + t.roughSize=this.roughSize*quantity; + t.format=this.format; + t.fullFormat=this.fullFormat; + t.quantity=quantity; + return t; + + } + + + public int numUnitsIn(TimeUnit u) + { + return (int)Math.round(u.getRoughSize()/(double)getRoughSize()); + } + + public boolean isDayOrLess() + { + return roughSize <= 24*60*60*1000L; + } + + public RoughTime roundDown(long timestamp) + { + return round(timestamp, false); + } + + public RoughTime roundUp(long timestamp) + { + return round(timestamp, true); + } + + private static final int[] calendarUnits={Calendar.SECOND, Calendar.MINUTE, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_MONTH, Calendar.MONTH, Calendar.YEAR}; + public RoughTime round(long timestamp, boolean up) + { + Calendar c=TimeUtils.cal(timestamp); + + if (calendarCode==Calendar.WEEK_OF_YEAR ) + { + c.set(Calendar.DAY_OF_WEEK, c.getMinimum(Calendar.DAY_OF_WEEK)); + } + else + { + + // set to minimum all fields of finer granularity. + int roundingCode=calendarCode; + if (calendarCode==Calendar.WEEK_OF_YEAR || calendarCode==Calendar.DAY_OF_WEEK) + roundingCode=Calendar.DAY_OF_MONTH; + for (int i=0; i<calendarUnits.length; i++) + { + if (calendarUnits[i]==roundingCode) + break; + if (i==calendarUnits.length-1) + throw new IllegalArgumentException("Unsupported Calendar Unit: "+calendarCode); + c.set(calendarUnits[i], c.getMinimum(calendarUnits[i])); + } + if (quantity>1) + { + c.set(calendarCode, quantity*(c.get(calendarCode)/quantity)); + } + } + + // if rounding up, then add a unit at current granularity. + if (up) + c.add(calendarCode, quantity); + + return new RoughTime(c.getTimeInMillis(), this); + } + + public int get(long timestamp) + { + Calendar c= TimeUtils.cal(timestamp); + int n=c.get(calendarCode); + return quantity==1 ? n : n%quantity; + } + + public void addTo(RoughTime r) + { + addTo(r,1); + } + + public void addTo(RoughTime r, int times) + { + Calendar c=TimeUtils.cal(r.getTime()); + c.add(calendarCode, quantity*times); + r.setTime(c.getTimeInMillis()); + } + + // Finding the difference between two dates, in a given unit of time, + // is much subtler than you'd think! And annoyingly, the Calendar class does not do + // this for you, even though it actually "knows" how to do so since it + // can add fields. + // + // The most vexing problem is dealing with daylight savings time, + // which means that one day a year has 23 hours and one day has 25 hours. + // We also have to handle the fact that months and years aren't constant lengths. + // + // Rather than write all this ourselves, in this code we + // use the Calendar class to do the heavy lifting. + public long difference(long x, long y) + { + // If this is not one of the hard cases, + // just divide the timespan by the length of time unit. + // Note that we're not worrying about hours and daylight savings time. + if (calendarCode!=Calendar.YEAR && calendarCode!=Calendar.MONTH && + calendarCode!=Calendar.DAY_OF_MONTH && calendarCode!=Calendar.DAY_OF_WEEK && + calendarCode!=Calendar.WEEK_OF_YEAR) + { + return (x-y)/roughSize; + } + + Calendar c1=TimeUtils.cal(x), c2=TimeUtils.cal(y); + int diff=0; + switch (calendarCode) + { + case Calendar.YEAR: + return (c1.get(Calendar.YEAR)-c2.get(Calendar.YEAR))/quantity; + + case Calendar.MONTH: + diff= 12*(c1.get(Calendar.YEAR)-c2.get(Calendar.YEAR))+ + c1.get(Calendar.MONTH)-c2.get(Calendar.MONTH); + return diff/quantity; + + case Calendar.DAY_OF_MONTH: + case Calendar.DAY_OF_WEEK: + case Calendar.DAY_OF_YEAR: + case Calendar.WEEK_OF_MONTH: + case Calendar.WEEK_OF_YEAR: + // This is ugly, but believe me, it beats the alternative methods :-) + // We use the Calendar class's knowledge of daylight savings time. + // and also the fact that if we calculate this naively, then we aren't going + // to be off by more than one in either direction. + int naive=(int)Math.round((x-y)/(double)roughSize); + c2.add(calendarCode, naive*quantity); + if (c1.get(calendarCode)==c2.get(calendarCode)) + return naive/quantity; + c2.add(calendarCode, quantity); + if (c1.get(calendarCode)==c2.get(calendarCode)) + return naive/quantity+1; + return naive/quantity-1; + } + throw new IllegalArgumentException("Unexpected calendar code: "+calendarCode); + } + + public long approxNumInRange(long start, long end) + { + return 1+(end-start)/roughSize; + } + + public long getRoughSize() { + return roughSize; + } + + public String format(Date date) + { + return format.format(date); + } + + public String formatFull(Date date) + { + return fullFormat.format(date); + } + + public String formatFull(long timestamp) + { + return fullFormat.format(new Date(timestamp)); + } + + public String getName() { + return name; + } +} diff --git a/timeflow/data/time/TimeUtils.java b/timeflow/data/time/TimeUtils.java new file mode 100755 index 0000000..32d4f6d --- /dev/null +++ b/timeflow/data/time/TimeUtils.java @@ -0,0 +1,21 @@ +package timeflow.data.time; + +import java.util.*; + +public class TimeUtils { + + public static Calendar cal(long time) + { + Calendar c=new GregorianCalendar(); + c.setTimeInMillis(time); + return c; + } + + public static Calendar cal(Date date) + { + Calendar c=new GregorianCalendar(); + c.setTime(date); + return c; + } + +} diff --git a/timeflow/format/field/DateTimeGuesser.java b/timeflow/format/field/DateTimeGuesser.java new file mode 100755 index 0000000..48d05c9 --- /dev/null +++ b/timeflow/format/field/DateTimeGuesser.java @@ -0,0 +1,115 @@ +package timeflow.format.field; + +import java.text.*; +import java.util.*; + +import timeflow.data.time.*; + +public class DateTimeGuesser { + + private DateTimeParser lastGoodFormat; + + private static List<DateTimeParser> parsers=new ArrayList<DateTimeParser>(); + + /* + + HANDY REFERENCE FOR SIMPLEDATEFORMAT: + (quoted from Java documentation) + + Letter Date or Time Component Presentation Examples + G Era designator Text AD + y Year Year 1996; 96 + M Month in year Month July; Jul; 07 + w Week in year Number 27 + W Week in month Number 2 + D Day in year Number 189 + d Day in month Number 10 + F Day of week in month Number 2 + E Day in week Text Tuesday; Tue + a Am/pm marker Text PM + H Hour in day (0-23) Number 0 + k Hour in day (1-24) Number 24 + K Hour in am/pm (0-11) Number 0 + h Hour in am/pm (1-12) Number 12 + m Minute in hour Number 30 + s Second in minute Number 55 + S Millisecond Number 978 + z Time zone General time zone Pacific Standard Time; PST; GMT-08:00 + Z Time zone RFC 822 time zone -0800 + + */ + + + // the order of the list below matters--better not to put the year-only ones at the top, + // because then the guesser succeeds before it has a chance to try parsing days. + static + { + parsers.add(new DateTimeParser("yyyy-MM-ddzzzzzzzzzz", TimeUnit.DAY)); + parsers.add(new DateTimeParser("MMM dd yyyy HH:mm", TimeUnit.SECOND)); + parsers.add(new DateTimeParser("MMM/dd/yyyy HH:mm", TimeUnit.SECOND)); + parsers.add(new DateTimeParser("MM/dd/yy HH:mm", TimeUnit.SECOND)); + parsers.add(new DateTimeParser("MMM dd yyyy HH:mm:ss", TimeUnit.SECOND)); + parsers.add(new DateTimeParser("MM/dd/yyyy HH:mm:ss", TimeUnit.SECOND)); + parsers.add(new DateTimeParser("MMM dd yyyy HH:mm:ss zzzzzzzz", TimeUnit.SECOND)); + parsers.add(new DateTimeParser("EEE MMM dd HH:mm:ss zzzzzzzz yyyy", TimeUnit.SECOND)); + parsers.add(new DateTimeParser("EEE MMM dd HH:mm:ss zzzzzzzz yyyy", TimeUnit.SECOND)); + parsers.add(new DateTimeParser("MM-dd-yyyy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("yyyy-MM-dd", TimeUnit.DAY)); + parsers.add(new DateTimeParser("yyyyMMdd", TimeUnit.DAY)); + parsers.add(new DateTimeParser("MM-dd-yy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("dd-MMM-yy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("MM-dd-yyyy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("MM/dd/yy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("MM/dd/yyyy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("dd MMM yyyy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("dd MMM, yyyy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("MMM dd yyyy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("MMM dd, yyyy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("EEE MMM dd zzzzzzzz yyyy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("EEE MMM dd yyyy", TimeUnit.DAY)); + parsers.add(new DateTimeParser("MMM-yy", TimeUnit.MONTH)); + parsers.add(new DateTimeParser("MMM yy", TimeUnit.MONTH)); + parsers.add(new DateTimeParser("MMM/yy", TimeUnit.MONTH)); + parsers.add(new DateTimeParser("yyyy", TimeUnit.YEAR)); + parsers.add(new DateTimeParser("yyyy GG", TimeUnit.YEAR)); + } + + public DateTimeParser getLastGoodFormat() + { + return lastGoodFormat; + } + + public RoughTime guess(String s) + { + // old code for trying the last good parser. + // we took this out because if the last good one was for a single year, + // but a new one is for years and days, + // all you get is the year. + + //if (lastGoodFormat!=null) + //try { return lastGoodFormat.parse(s); } + //catch (ParseException e) {} + if (s==null || s.trim().length()==0) + return null; + for (DateTimeParser d: parsers) + { + try + { + RoughTime date= d.parse(s); + lastGoodFormat=d; + return date; + } + catch (ParseException e) {} + } + throw new IllegalArgumentException("Couldn't guess date: '"+s+"'"); + } + + public static void main(String[] args) + { + DateTimeGuesser g=new DateTimeGuesser(); + System.out.println(g.guess("2009-03-04")); + System.out.println(g.guess("June 10, 2010")); + System.out.println(g.guess("2010")); + System.out.println(g.guess("3/17/10")); + } +} diff --git a/timeflow/format/field/DateTimeParser.java b/timeflow/format/field/DateTimeParser.java new file mode 100755 index 0000000..e0e45d9 --- /dev/null +++ b/timeflow/format/field/DateTimeParser.java @@ -0,0 +1,39 @@ +package timeflow.format.field; + +import timeflow.data.time.*; + +import java.util.*; +import java.text.*; + + +// Guesses date format and returns result +public class DateTimeParser { + + private DateFormat format; + private TimeUnit units; + private String pattern; + + public DateTimeParser(String pattern, TimeUnit units) + { + this.pattern=pattern; + format=new SimpleDateFormat(pattern); + this.units=units; + } + + public RoughTime parse(String s) throws ParseException + { + RoughTime f= new RoughTime(format.parse(s).getTime(), units); + return f; + } + + public TimeUnit getUnits() + { + return units; + } + + public String toString() + { + return "DateParser: pattern="+pattern+", units="+units; + } + +} diff --git a/timeflow/format/field/FieldFormat.java b/timeflow/format/field/FieldFormat.java new file mode 100755 index 0000000..a8c7223 --- /dev/null +++ b/timeflow/format/field/FieldFormat.java @@ -0,0 +1,64 @@ +package timeflow.format.field; + +import java.net.URL; + +import timeflow.data.time.*; + +public abstract class FieldFormat { + protected String lastInput; + protected Object lastValue; + protected boolean understood=true; + + double value; + + void add(double x) + { + value+=x; + } + + void note(String s) + { + add(scoreFormatMatch(s)); + } + + + protected abstract Object _parse(String s) throws Exception; + public abstract String format(Object o); + public abstract Class getType(); + public abstract double scoreFormatMatch(String s); + public abstract String getHumanName(); + + + public void setValue(Object o) + { + lastValue=o; + lastInput=o==null ? "" : format(o); + } + + public Object parse(String s) throws Exception + { + lastInput=s; + lastValue=null; + understood=false; + lastValue=_parse(s); + understood=true; + return lastValue; + } + + public Object getLastValue() + { + return lastValue; + } + + public String feedback() + { + if (!understood) + return "Couldn't understand"; + return lastValue==null ? "(missing)" : "Read: "+format(lastValue); + } + + public boolean isUnderstood() + { + return understood; + } +} diff --git a/timeflow/format/field/FieldFormatCatalog.java b/timeflow/format/field/FieldFormatCatalog.java new file mode 100755 index 0000000..d65092c --- /dev/null +++ b/timeflow/format/field/FieldFormatCatalog.java @@ -0,0 +1,54 @@ +package timeflow.format.field; + +import timeflow.data.time.*; +import timeflow.util.*; + +import java.net.URL; +import java.util.*; + +public class FieldFormatCatalog { + + private static Map<String, FieldFormat> formatTable=new HashMap<String, FieldFormat>(); + private static Map<Class, FieldFormat> classTable=new HashMap<Class, FieldFormat>(); + + static + { + for (FieldFormat f: listFormats()) + { + formatTable.put(f.getHumanName(), f); + classTable.put(f.getType(), f); + } + } + + static FieldFormat[] listFormats() + { + return new FieldFormat[] {new FormatDateTime(), new FormatString(), + new FormatStringArray(), new FormatDouble(), new FormatURL()}; + } + + public static Iterable<String> classNames() + { + return formatTable.keySet(); + } + + public static String humanName(Class c){ + return getFormat(c).getHumanName(); + } + + + public static FieldFormat getFormat(Class c) { + FieldFormat f= classTable.get(c); + if (f==null) + System.out.println("Warning: no FieldFormat for "+c); + return f; + } + + + public static Class javaClass(String humanName) + { + Class c=formatTable.get(humanName).getType(); + if (c==null) + System.out.println("Warning: no class for "+humanName); + return c; + } +} diff --git a/timeflow/format/field/FieldFormatGuesser.java b/timeflow/format/field/FieldFormatGuesser.java new file mode 100755 index 0000000..168f759 --- /dev/null +++ b/timeflow/format/field/FieldFormatGuesser.java @@ -0,0 +1,50 @@ +package timeflow.format.field; + +import timeflow.util.*; + +public class FieldFormatGuesser { + + FieldFormat[] scores; + + private FieldFormatGuesser() + { + scores=FieldFormatCatalog.listFormats(); + } + public static Class[] analyze(String[][] data, int startRow, int numRows) + { + int n=data[0].length; + FieldFormatGuesser[] g=new FieldFormatGuesser[n]; + for (int i=0; i<n; i++) + g[i]=new FieldFormatGuesser(); + for (int i=startRow; i<startRow+numRows && i<data.length; i++) + { + for (int j=0; j<n; j++) + g[j].add(data[i][j]); + } + Class[] c=new Class[n]; + for (int i=0; i<n; i++) + c[i]=g[i].best(); + return c; + } + + private void add(String s) + { + for (int i=0; i<scores.length; i++) + scores[i].note(s); + } + + private Class best() + { + double max=scores[0].value; + Class best=scores[0].getType(); + for (int i=1; i<scores.length; i++) + { + if (scores[i].value>max) + { + max=scores[i].value; + best=scores[i].getType(); + } + } + return best; + } +} diff --git a/timeflow/format/field/FormatDateTime.java b/timeflow/format/field/FormatDateTime.java new file mode 100755 index 0000000..d16ea32 --- /dev/null +++ b/timeflow/format/field/FormatDateTime.java @@ -0,0 +1,79 @@ +/** + * + */ +package timeflow.format.field; + +import java.text.ParseException; +import java.util.Calendar; + +import timeflow.data.time.RoughTime; +import timeflow.data.time.TimeUnit; +import timeflow.data.time.TimeUtils; + +public class FormatDateTime extends FieldFormat +{ + DateTimeGuesser dateGuesser=new DateTimeGuesser(); + + @Override + public String format(Object o) { + return ((RoughTime)o).format(); + } + + @Override + public Object _parse(String s) throws Exception { + if (s.length()==0) + return null; + Object o= readTime(s); + if (o==null) + throw new IllegalArgumentException(); + return o; + } + + + public RoughTime readTime(Object o) throws ParseException + { + if (!(o instanceof String)) + throw new IllegalArgumentException("Expected String, got: "+o); + return dateGuesser.guess((String)o); + } + DateTimeGuesser g=new DateTimeGuesser(); + + @Override + public double scoreFormatMatch(String s) { + if (s==null || s.length()==0) + return -.05; + try + { + RoughTime f=g.guess(s); + if (f==null) + return -1; + if (g.getLastGoodFormat().getUnits()==TimeUnit.YEAR) + { + int year=TimeUtils.cal(f.getTime()).get(Calendar.YEAR); + if (year>2100) + return -1; + if (year>1900 && year<2050) + return 1; + if (year>2050 || year<1600) + return .1; + return .5; + } + return 2; + } + catch (Exception e) + { + return -1; + } + } + + + @Override + public Class getType() { + return RoughTime.class; + } + + @Override + public String getHumanName() { + return "Date/Time"; + } +} \ No newline at end of file diff --git a/timeflow/format/field/FormatDouble.java b/timeflow/format/field/FormatDouble.java new file mode 100755 index 0000000..e57922d --- /dev/null +++ b/timeflow/format/field/FormatDouble.java @@ -0,0 +1,103 @@ +/** + * + */ +package timeflow.format.field; + +public class FormatDouble extends FieldFormat +{ + @Override + public String format(Object o) { + return o.toString(); + } + + @Override + public Object _parse(String s) { + if (s==null || s.trim().length()==0) + return null; + return parseDouble(s); + } + + public static double parseDouble(String s) + { + int n=s.length(); + if (n>3) + { + if (s.charAt(0)=='(' && s.charAt(n-1)==')') + { + s='-'+s.substring(1,n-1); + n--; + } + } + // remove $,%, etc. + StringBuffer b=new StringBuffer(); + for (int i=0; i<n; i++) + { + char c=s.charAt(i); + if (Character.isDigit(c) || c=='-' || c=='.') + b.append(c); + } + + try + { + return Double.parseDouble(b.toString()); + } + catch (RuntimeException e) + { + System.out.println("b="+b); + throw e; + } + } + + + @Override + public Class getType() { + return Double.class; + } + @Override + public double scoreFormatMatch(String s) { + s=s.trim(); + int n=s.length(); + if (n==5) // possible zip code + { + if (s.charAt(0)=='0') + return -3; // gotta be a zip code! + return 0; + } + if (n==9) // possible zip+4, but really, who knows... + return 0; + + if (n==4) // possible date. + { + try + { + int x=Integer.parseInt(s); + if (x>1900 && x<2030) + return -1; // that's very likely a date. + if (x>1700 && x<2100) + return 0; // you don't know. + + } + catch (Exception e) {} // evidently not a date :-) + } + + if (n==0) + return -.1; + int ok=0; + int bad=0; + for (int i=0; i<n; i++) + { + char c=s.charAt(i); + if (Character.isDigit(c) || c=='.' || c==',' || c=='-' || c=='$' || c=='%') + ok++; + else + bad++; + } + return 4-5*bad; + } + + @Override + public String getHumanName() { + return "Number"; + } + +} \ No newline at end of file diff --git a/timeflow/format/field/FormatString.java b/timeflow/format/field/FormatString.java new file mode 100755 index 0000000..ed784d3 --- /dev/null +++ b/timeflow/format/field/FormatString.java @@ -0,0 +1,44 @@ +/** + * + */ +package timeflow.format.field; + +public class FormatString extends FieldFormat +{ + @Override + public String format(Object o) { + return o.toString(); + } + + @Override + public Object _parse(String s) { + return s; + } + + + public String feedback() + { + if (lastValue==null) + return "Couldn't understand"; + if (((String)lastValue).length()==0) + return "Blank"; + return ""; + } + + @Override + public Class getType() { + return String.class; + } + + @Override + public double scoreFormatMatch(String s) { + return s!=null && s.length()>0 ? .1 : 0; + } + + @Override + public String getHumanName() { + return "Text"; + } + + +} \ No newline at end of file diff --git a/timeflow/format/field/FormatStringArray.java b/timeflow/format/field/FormatStringArray.java new file mode 100755 index 0000000..a2a8245 --- /dev/null +++ b/timeflow/format/field/FormatStringArray.java @@ -0,0 +1,59 @@ +/** + * + */ +package timeflow.format.field; + +import timeflow.model.Display; + +public class FormatStringArray extends FieldFormat +{ + @Override + public String format(Object o) { + return Display.arrayToString((String[])o); + } + + @Override + public Object _parse(String s) { + return parseList(s); + } + + public static String[] parseList(String s) + { + String[] t= s.length()==0 ? new String[0] : s.split(","); + for (int i=0; i<t.length; i++) + t[i]=t[i].trim(); + return t; + } + + public String feedback() + { + if (lastValue==null) + return "Couldn't understand"; + String[] s=(String[])lastValue; + if (s.length==0) + return "Empty list"; + if (s.length==1) + return "One item"; + return s.length+" items"; + } + + @Override + public Class getType() { + return String[].class; + } + + @Override + public double scoreFormatMatch(String s) { + double commas=-1; + for (int i=s.length()-1; i>=0; i--) + if (s.charAt(i)==',') + commas++; + return commas/s.length(); + } + + @Override + public String getHumanName() { + return "List"; + } + +} \ No newline at end of file diff --git a/timeflow/format/field/FormatURL.java b/timeflow/format/field/FormatURL.java new file mode 100755 index 0000000..a20922f --- /dev/null +++ b/timeflow/format/field/FormatURL.java @@ -0,0 +1,41 @@ +/** + * + */ +package timeflow.format.field; + +import java.net.URL; + +public class FormatURL extends FieldFormat +{ + @Override + public String format(Object o) { + return o.toString(); + } + + @Override + public Object _parse(String s) throws Exception { + if (s.length()==0) + return null; + return new URL(s); + } + + @Override + public Class getType() { + return URL.class; + } + + @Override + public double scoreFormatMatch(String s) { + if (s==null || s.length()==0) + return 0; + if (s.startsWith("http") || s.startsWith("file://")) + return 5; + return -1; + } + + @Override + public String getHumanName() { + return "URL"; + } + +} \ No newline at end of file diff --git a/timeflow/format/file/DelimitedFormat.java b/timeflow/format/file/DelimitedFormat.java new file mode 100755 index 0000000..2b9f1ac --- /dev/null +++ b/timeflow/format/file/DelimitedFormat.java @@ -0,0 +1,217 @@ +package timeflow.format.file; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import timeflow.data.db.Act; +import timeflow.data.db.ActDB; +import timeflow.data.db.DBUtils; +import timeflow.data.db.Field; +import timeflow.data.time.RoughTime; +import timeflow.model.Display; + +import timeflow.util.*; + +public class DelimitedFormat { + + char delimiter; + DelimitedText delimitedText; + + public DelimitedFormat(char delimiter) + { + this.delimiter=delimiter; + delimitedText=new DelimitedText(delimiter); + } + + public static String[][] readArrayGuessDelim(String fileName, PrintStream messages) throws Exception + { + //messages.println("DelimitedFormat: reading "+fileName); + String text=IO.read(fileName); + return readArrayFromString(text, messages); + } + + public static String[][] readArrayFromString(String text, PrintStream messages) throws Exception + { + //messages.println("DelimitedFormat: reading string, length="+text.length()); + int n=Math.min(text.length(), 1000); + String beginning=text.substring(0,n); + char c=count(beginning, '\t')>count(beginning, ',') ? '\t' : ','; + return new DelimitedFormat(c).readTokensFromString(text, messages); + } + + private static String[] removeBlankLines(String[] lines) + { + List<String> good=new ArrayList<String>(); + for (int i=0; i<lines.length; i++) + { + if (!(lines[i]==null || lines[i].trim().length()==0)) + good.add(lines[i]); + } + return (String[])good.toArray(new String[0]); + } + + private static int count(String s, char c) + { + int n=0; + for (int i=0; i<s.length(); i++) + { + if (s.charAt(i)==c) + n++; + } + return n; + } + + public String[][] readTokensFromString(String text, PrintStream messages) throws Exception + { + + ArrayList<String[]> resultList=new ArrayList<String[]>(); + Iterator<String[]> lines=delimitedText.read(text).iterator(); + int numCols=-1; + while(lines.hasNext()) + { + String[] r=lines.next(); + int ri=r.length; + if (numCols==-1) + numCols=r.length; + else + { + if (ri>numCols) + { + messages.println("Line too long: "+ri+" > "+numCols); + messages.println("line="+Display.arrayToString(r)); + } + else if (ri<numCols) + { + String[] old=r; + r=new String[numCols]; + System.arraycopy(old,0,r,0,ri); + for (int j=ri; j<numCols; j++) + r[j]=""; + } + } + resultList.add(r); + } + //messages.println("# lines read: "+resultList.size()); + return (String[][]) resultList.toArray(new String[0][]); + } + + public void write(ActDB db, File file) throws IOException + { + write(db, db, file); + } + + public void write(ActDB db, Iterable<Act> acts, File file) throws IOException + { + FileOutputStream fos=new FileOutputStream(file); + BufferedOutputStream b=new BufferedOutputStream(fos); + PrintStream out=new PrintStream(b); + + // Write data! + writeDelimited(db, acts, out); + + out.flush(); + out.close(); + b.close(); + fos.close(); + } + + + void writeDelimited(ActDB db, PrintStream out) + { + writeDelimited(db, db, out); + } + + public void writeDelimited(ActDB db, Iterable<Act> acts, PrintWriter out) + { + // Write headers + List<String> names=new ArrayList<String>(); + List<Field> fields=db.getFields(); + for (Field f: fields) + names.add(f.getName()); + print(names, out); + + // Write data + for (Act a: acts) + { + List<String> data=new ArrayList<String>(); + for (Field f: fields) + data.add(format(a.get(f))); + print(data, out); + } + } + + + public void writeDelimited(ActDB db, Iterable<Act> acts, PrintStream out) + { + // Write headers + List<String> names=new ArrayList<String>(); + List<Field> fields=db.getFields(); + for (Field f: fields) + names.add(f.getName()); + print(names, out); + + // Write data + for (Act a: acts) + { + List<String> data=new ArrayList<String>(); + for (Field f: fields) + data.add(format(a.get(f))); + print(data, out); + } + } + + + static String format(Object o) + { + if (o==null) + return ""; + if (o instanceof String) + return (String)o; + if (o instanceof RoughTime) + return ((RoughTime)o).format(); + if (o instanceof Number) + return o.toString(); + if (o instanceof String[]) + { + return writeArray((String[])o); + } + return o.toString(); + } + + public static String writeArray(Object[] s) + { + if (s==null || s.length==0) + { + return ""; + } + StringBuffer b=new StringBuffer(); + for (int i=0; i<s.length; i++) + { + if (i>0) + b.append(","); + b.append(s[i]); + + } + return b.toString(); + } + + void print(List<String> list, PrintStream out) + { + out.println(delimitedText.write((String[])list.toArray(new String[0]))); + } + + void print(List<String> list, PrintWriter out) + { + out.println(delimitedText.write((String[])list.toArray(new String[0]))); + } + +} + diff --git a/timeflow/format/file/DelimitedText.java b/timeflow/format/file/DelimitedText.java new file mode 100755 index 0000000..cbc6dce --- /dev/null +++ b/timeflow/format/file/DelimitedText.java @@ -0,0 +1,214 @@ +package timeflow.format.file; + +import java.util.*; + +import timeflow.util.*; + +import timeflow.model.Display; + +public class DelimitedText { + private char delimiter; + + public DelimitedText(char delimiter) + { + if (delimiter=='"') + throw new IllegalArgumentException("Can't use quote as delimiter."); + this.delimiter=delimiter; + } + + private static boolean isBreak(char c) + { + return c=='\n' || c=='\r'; + } + + public List<String[]> read(String text) + { + ArrayList<String[]> results=new ArrayList<String[]>(); + int n=text.length(); + StringBuffer currentToken=new StringBuffer(); + ArrayList<String> currentList=new ArrayList<String>(); + + boolean quoted=false; + for (int i=0; i<n; i++) + { + char c=text.charAt(i); + if (quoted) + { + if (c=='"') + { + if (i==n-1) // end of file, ignore quote. + { + quoted=false; + continue; + } + char next=text.charAt(i+1); + if (next=='"') // a quoted quote. + { + currentToken.append('"'); + i++; + + // Alas, there is a weird special case here + // if the user has pasted from Excel. + // If a field starts with a quote, and ends with two quotes, + // it turns out to be ambiguous! + // Excel doesn't do any escaping on: "blah blah"" + // But, it does escape: blah "\n + // turning it into: "blah blah""\n + // So if "blah blah"" occurs at the end of the line, + // you actually do not know which it is! + // In practice, our first bug report was for a literal of "blah blah"" + // so that is what we will choose. + + //System.out.println("next++: '"+text.charAt(i+1)+"'="+(int)text.charAt(i+1)); + if (i<n-1 && isBreak(text.charAt(i+1))) + { + quoted=false; + } + + continue; + } + if (isBreak(next)) // end of line + { + quoted=false; + currentList.add(currentToken.toString()); + currentToken.setLength(0); + results.add((String[])currentList.toArray(new String[0])); + currentList=new ArrayList<String>(); + i++; + if (i<n-1 && isBreak(text.charAt(i+1))) + i++; + continue; + } + if (next==delimiter) + { + quoted=false; + continue; + } + System.out.println("a bad quote from excel: next char="+(int)next); + quoted=false; + } + currentToken.append(c); + continue; + } + + // ok, not quoted. + if (c==delimiter) + { + currentList.add(currentToken.toString()); + currentToken.setLength(0); + quoted=false; + continue; + } + + // not delimiter, not in the middle of a quote. + if (c=='"') + { + if (currentToken.length()==0) // we are at beginning of a token, so this is a quote. + { + quoted=true; + continue; + } + } + + // is it a line feed? we're not in the middle of a quote, so this means a new line. + if (c=='\n' || c=='\r' || c=='\f') + { + currentList.add(currentToken.toString()); + currentToken.setLength(0); + results.add((String[])currentList.toArray(new String[0])); + currentList=new ArrayList<String>(); + if (i<n-1 && (text.charAt(i+1)=='\n' || text.charAt(i+1)=='\r')) + i++; + continue; + } + + // by golly, just a normal character! + currentToken.append(c); + } + + // did it just end in a blank line? + + if (currentList.size()>0 || currentToken.toString().trim().length()>0) + { + currentList.add(currentToken.toString()); + results.add((String[])currentList.toArray(new String[0])); + } + return results; + } + + public String write(String s) + { + return write(new String[] {s}); + } + + public String write(String[] data) + { + StringBuffer b=new StringBuffer(); + for (int i=0; i<data.length; i++) + { + // add a delimiter if necessary. + if (i>0) + b.append(delimiter); + + // if null, just don't write anything. + if (data[i]==null) + continue; + + // does it have weird characters in it? + boolean weird=false; + int n=data[i].length(); + for (int j=0; j<n; j++) + { + char c=data[i].charAt(j); + if (c==delimiter || isBreak(c)) + { + weird=true; + break; + } + } + + if (weird) + { + b.append('"'); + for (int j=0; j<n; j++) + { + char c=data[i].charAt(j); + if (c=='"') + b.append('"'); + b.append(c); + } + b.append('"'); + } + else + b.append(data[i]); + } + return b.toString(); + } + + public static String[] split(String s, char delimiter) + { + DelimitedText t= new DelimitedText(delimiter); + List<String[]> lines=t.read(s); + return lines.get(0); + } + + public static void main(String[] args) throws Exception + { + String bad=IO.read("test/bad-all.txt"); + String[][] s=DelimitedFormat.readArrayFromString(bad, System.out); + System.out.println("len="+s.length); + + /* + //DelimitedText c=new DelimitedText(';'); + //List<String[]> arrays=c.read(IO.read("test/bad.txt")); + //List<String[]> arrays=c.read("a;b;\"x;y\";c"); + //List<String[]> arrays=c.read("a;\"a\n\rq\";b;\"x;y\";c"); + //List<String[]> arrays=c.read("a;b;\"with a \"\"blah\";c\nd;e;f\ng;h;i"); + //List<String[]> arrays=c.read("a,\"b\",\"c\r\nd\"\r\ne,f,g\nh,i,j"); + for (String[] s:arrays) + { + System.out.println("["+Display.arrayToString(s)+"]"); + } + */ + } +} diff --git a/timeflow/format/file/Export.java b/timeflow/format/file/Export.java new file mode 100755 index 0000000..24361e8 --- /dev/null +++ b/timeflow/format/file/Export.java @@ -0,0 +1,10 @@ +package timeflow.format.file; + +import timeflow.model.*; + +import java.io.BufferedWriter; + +public interface Export { + public String getName(); + public void export(TFModel model, BufferedWriter out) throws Exception; +} diff --git a/timeflow/format/file/FileExtensionCatalog.java b/timeflow/format/file/FileExtensionCatalog.java new file mode 100755 index 0000000..f7b8d50 --- /dev/null +++ b/timeflow/format/file/FileExtensionCatalog.java @@ -0,0 +1,28 @@ +package timeflow.format.file; + +// This is meant to be a repository for different +// types of import functions, arranged by file extension. + +// We currently do not import anything but the standard file type. +// There actually is some code that will import from JSON/XML SIMILE +// timelines, but we have removed it from this release to simplify +// both the application and because it would mean redistributing additional +// third-party libraries. +public class FileExtensionCatalog { + + public static Import get(String fileName) + { + /* + // not in this release... + // but contact us if you'd like to see this. + // we took out the SIMILE import material as too "techie" + // for the first release! + + if (fileName.endsWith("xml")) + return new SimileXMLFormat(); + if (fileName.endsWith("json")) + return new SimileJSONFormat(); + */ + return new TimeflowFormat(); + } +} diff --git a/timeflow/format/file/HtmlFormat.java b/timeflow/format/file/HtmlFormat.java new file mode 100755 index 0000000..cf50499 --- /dev/null +++ b/timeflow/format/file/HtmlFormat.java @@ -0,0 +1,143 @@ +package timeflow.format.file; + +import java.awt.Color; +import java.io.BufferedWriter; +import java.net.URL; + +import timeflow.model.*; +import timeflow.data.db.*; + +public class HtmlFormat implements Export +{ + TFModel model; + java.util.List<Field> fields; + Field title; + + public HtmlFormat() {} + + public HtmlFormat(TFModel model) + { + setModel(model); + } + + public void setModel(TFModel model) + { + this.model=model; + fields=model.getDB().getFields(); + title=model.getDB().getField(VirtualField.LABEL); + } + + @Override + public void export(TFModel model, BufferedWriter out) throws Exception { + setModel(model); + out.write(makeHeader()); + for (Act a: model.getDB()) + out.write(makeItem(a)); + out.write(makeFooter()); + out.flush(); + } + + public void append(ActList acts, int start, int end, StringBuffer b) + { + for (int i=start; i<end; i++) + { + Act a=acts.get(i); + b.append(makeItem(a,i)); + } + } + + private String makeItem(Act act) + { + return makeItem(act, -1); + } + + public String makeItem(Act act, int id) + { + StringBuffer page=new StringBuffer(); + + + page.append("<tr><td valign=top align=left width=200><b>"); + if (title!=null) + { + Field f=model.getColorField(); + Color c=Color.black; + if (f!=null) + { + if (f.getType()==String.class) + c=model.getDisplay().makeColor(act.getString(f)); + else + { + String[] tags=act.getTextList(f); + if (tags.length==0) + c=Color.gray; + else + c=model.getDisplay().makeColor(tags[0]); + } + } + + page.append("<font size=+1 color="+htmlColor(c)+">"+act.getString(title)+"</font><br>"); + } + + Field startField=model.getDB().getField(VirtualField.START); + + if (startField!=null) + { + page.append("<font color=#999999>"+model.getDisplay().format( + act.getTime(startField))+"</font>"); + } + page.append("</b><br>"); + if (id>=0) + page.append("<a href=\"e"+id+"\">EDIT</a>"); + page.append("<br></td><td valign=top>"); + for (Field f: fields) + { + page.append("<b><font color=#003399>"+f.getName()+"</font></b> "); + Object val=act.get(f); + if (val instanceof URL) + { + page.append("<a href=\""+val+"\">"+val+"</a>"); + } + else + page.append(model.getDisplay().toString(val)); + page.append("<br>"); + + } + page.append("<br></td></tr>"); + + return page.toString(); + } + + public String makeHeader() + { + StringBuffer page=new StringBuffer(); + page.append("<html><body><blockquote>"); + page.append("<br>File: "+model.getDbFile()+"<br>"); + page.append("Source: "+model.getDB().getSource()+"<br><br>"); + page.append("<br><br>"); + page.append("<table border=0>"); + + return page.toString(); + } + + public String makeFooter() + { + return "</table></blockquote></body></html>"; + } + + + static String htmlColor(Color c) + { + return '#'+hex2(c.getRed())+hex2(c.getGreen())+hex2(c.getBlue()); + } + + private static final String hexDigits="0123456789ABCDEF"; + private static String hex2(int n) + { + return hexDigits.charAt((n/16)%16)+""+hexDigits.charAt(n%16); + } + @Override + public String getName() { + return "HTML List"; + } + +} diff --git a/timeflow/format/file/Import.java b/timeflow/format/file/Import.java new file mode 100755 index 0000000..36df67e --- /dev/null +++ b/timeflow/format/file/Import.java @@ -0,0 +1,9 @@ +package timeflow.format.file; + +import java.io.File; +import timeflow.data.db.ActDB; + +public interface Import { + public String getName(); + public ActDB importFile(File file) throws Exception; +} diff --git a/timeflow/format/file/TimeflowFormat.java b/timeflow/format/file/TimeflowFormat.java new file mode 100755 index 0000000..3778235 --- /dev/null +++ b/timeflow/format/file/TimeflowFormat.java @@ -0,0 +1,163 @@ +package timeflow.format.file; + +import timeflow.model.*; +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.format.field.*; + +import timeflow.util.*; + +import java.io.*; +import java.net.URL; +import java.util.*; + +public class TimeflowFormat implements Import, Export +{ + private static final String END_OF_SCHEMA="#TIMEFLOW\tend-metadata"; + private static final String END_OF_METADATA="#TIMEFLOW\t====== End of Header. Data below is in tab-delimited format. ====="; + public ActDB readFile(String fileName, PrintStream messages) throws Exception + { + return read(new File(fileName), messages); + } + + public static ActDB read(File file, PrintStream out) throws Exception + { + String text=IO.read(file.getAbsolutePath()); + DelimitedText quote=new DelimitedText('\t'); + Iterator<String[]> lines=quote.read(text).iterator(); + + + ActDB db=null; + List<String> fieldNames=new ArrayList<String>(); + List<Class> fieldTypes=new ArrayList<Class>(); + List<Integer> fieldSizes=new ArrayList<Integer>(); + String source="[unknown]", description=""; + for (;;) + { + String[] t=lines.next(); + + if (t[1].equals("field")) + { + fieldNames.add(t[2]); + fieldTypes.add(FieldFormatCatalog.javaClass(t[3])); + if (t.length>4) + { + fieldSizes.add(Integer.parseInt(t[4])); + } + else + fieldSizes.add(-1); + } + else if (t[1].equals("source")) + { + source=t[2]; + } + else if (t[1].equals("description")) + { + description=t[2]; + + } + else if (t[1].equals("end-metadata")) + break; + } + db=new ArrayDB((String[])fieldNames.toArray(new String[0]), + (Class[])fieldTypes.toArray(new Class[0]), source); + db.setDescription(description); + for (int i=0; i<fieldNames.size(); i++) + if (fieldSizes.get(i)>0) + db.getField(fieldNames.get(i)).setRecommendedSize(fieldSizes.get(i)); + for (;;) + { + String[] t=lines.next(); + if (t[1].startsWith("===")) + break; + if (t[1].equals("alias")) + db.setAlias(db.getField(t[3]), t[2]); + } + + // note: in some cases headers may be in a different order than in + // metadata section, so we will read these. + String[] headers=lines.next(); + if (headers.length!=fieldNames.size()) + throw new IllegalArgumentException("Different number of headers than fields!"); + + + while (lines.hasNext()) + { + String[] t=lines.next(); + Act a=db.createAct(); + for (int i=0; i<t.length; i++) + { + Field f=db.getField(headers[i]); + FieldFormat format=FieldFormatCatalog.getFormat(f.getType()); + a.set(f, format.parse(t[i])); + } + } + + return db; + } + + public static void write(ActList acts, BufferedWriter bw) throws IOException + { + ActDB db=acts.getDB(); + + PrintWriter out=new PrintWriter(bw); + + DelimitedText tab=new DelimitedText('\t'); + + // Write version + out.println("#TIMEFLOW\tformat version\t1"); + + // Write source of data. + out.println("#TIMEFLOW\tsource\t"+tab.write(db.getSource())); + + // Write description of data. + out.println("#TIMEFLOW\tdescription\t"+tab.write(db.getDescription())); + + // Write schema. + List<Field> fields=db.getFields(); + for (Field f: fields) + { + String recSize=f.getRecommendedSize()<=0 ? "" : "\t"+f.getRecommendedSize(); + out.println("#TIMEFLOW\tfield\t"+tab.write(f.getName())+ + "\t"+FieldFormatCatalog.humanName(f.getType())+recSize); + } + + out.println(END_OF_SCHEMA); + + // Write column mappings. + List<String> aliases=DBUtils.getFieldAliases(db); + for (String a:aliases) + out.println("#TIMEFLOW\talias\t"+a+"\t"+tab.write(db.getField(a).getName())); + + // Write end of header indicator + out.println(END_OF_METADATA); + + // Write data! + new DelimitedFormat('\t').writeDelimited(db, acts, out); + + out.flush(); + out.close(); + } + + public static void main(String[] args) throws Exception + { + System.out.println("Reading"); + ActDB db=read(new File("test/monet.txt"), System.out); + System.out.println("# lines: "+db.size()); + } + + @Override + public String getName() { + return "TimeFlow Format"; + } + + @Override + public ActDB importFile(File file) throws Exception { + return read(file, System.out); + } + + @Override + public void export(TFModel model, BufferedWriter out) throws Exception { + write(model.getDB().all(), out); + } +} diff --git a/timeflow/model/Display.java b/timeflow/model/Display.java new file mode 100755 index 0000000..b56c86b --- /dev/null +++ b/timeflow/model/Display.java @@ -0,0 +1,373 @@ +package timeflow.model; + +import timeflow.data.db.filter.ValueFilter; +import timeflow.data.time.*; + +import java.awt.*; +import java.util.*; +import java.net.URI; +import java.text.*; +import javax.swing.*; + +// to do: read from a properties file! +public class Display +{ + + HashMap<String, String> strings = new HashMap<String, String>(); + HashMap<String, Integer> ints = new HashMap<String, Integer>(); + HashMap<String, Color> colors = new HashMap<String, Color>(); + HashMap<Class, String> classLabel = new HashMap<Class, String>(); + Color fallback = new Color(0, 53, 153, 128); + String fontName = "Verdana"; + Font tinyFont = new Font(fontName, Font.BOLD, 9); + Font smallFont = new Font(fontName, Font.PLAIN, 11); + Font boldFont = new Font(fontName, Font.BOLD, 12); + Font plainFont = UIManager.getFont("Label.font"); + Font bigFont = plainFont.deriveFont(Font.BOLD); + Font hugeFont = new Font(fontName, Font.BOLD, 16); + Font timeLabelFont = tinyFont; + FontMetrics hugeFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(hugeFont); + FontMetrics bigFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(bigFont); + FontMetrics plainFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(plainFont); + FontMetrics boldFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(boldFont); + FontMetrics tinyFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(tinyFont); + FontMetrics timeLabelFontMetrics = Toolkit.getDefaultToolkit().getFontMetrics(timeLabelFont); + static DecimalFormat df = new DecimalFormat("###,###,###,###.##"); + static DecimalFormat roundFormat = new DecimalFormat("###,###,###,###"); + public static final String MISC_CODE = "Misc. "; + public static final double MAX_DOT_SIZE = 10; + public static final int CALENDAR_CELL_HEIGHT = 80; + public static Color barColor = new Color(150, 170, 200); + private static final double PHI = (1 + Math.sqrt(5)) / 2; + HashMap<String, Color> topColors = new HashMap<String, Color>(); + static Color[] handPalette = + { + new Color(203, 31, 23), + new Color(237, 131, 0), + new Color(71, 175, 13), + new Color(6, 119, 207), + new Color(0, 188, 184), + new Color(209, 80, 174), + new Color(146, 6, 0), + new Color(175, 103, 0), + new Color(76, 124, 0), + new Color(0, 80, 143), + new Color(0, 128, 124), + new Color(153, 56, 126) + }; + ValueFilter grayFilter; + + public Display() + { + + strings.put("other.label", "Other"); + strings.put("null.label", "[none]"); + + classLabel.put(RoughTime.class, "Date/Time"); + classLabel.put(String.class, "Text"); + classLabel.put(Number.class, "Number"); + classLabel.put(Double.class, "Number"); + classLabel.put(Integer.class, "Number"); + classLabel.put(String[].class, "List"); + + colors.put("chart.background", Color.white); + Color ui = new Color(240, 240, 240); + colors.put("splash.background", Color.white); + colors.put("splash.text", new Color(50, 50, 50)); + colors.put("filterpanel.background", ui); + colors.put("visualpanel.background", ui); + colors.put("text.prominent", Color.black); + colors.put("text.normal", Color.gray); + colors.put("timeline.label", new Color(0, 53, 153)); + colors.put("timeline.label.lesser", new Color(110, 153, 200)); + colors.put("timeline.grid", new Color(240, 240, 240)); + colors.put("timeline.grid.vertical", new Color(240, 240, 240)); + colors.put("timeline.zebra", new Color(245, 245, 245)); + colors.put("null.color", new Color(230, 230, 230)); + colors.put("timeline.unspecified.color", new Color(0, 53, 153)); + colors.put("highlight.color", new Color(0, 53, 153)); + + ints.put("timeline.datelabel.height", 20); + ints.put("timeline.item.height.min", 16); + } + + public static void launchBrowser(String urlString) + { + if (Desktop.isDesktopSupported()) + { + Desktop desktop = Desktop.getDesktop(); + try + { + desktop.browse(new URI(urlString)); + } catch (Exception e2) + { + e2.printStackTrace(); + } + } else + { + System.out.println("Desktop not supported!"); + } + } + + public static String arrayToString(Object[] s) + { + if (s == null || s.length == 0) + { + return ""; + } + StringBuffer b = new StringBuffer(); + for (int i = 0; i < s.length; i++) + { + if (i > 0) + { + b.append(", "); + } + b.append(s[i]); + + } + return b.toString(); + } + + public static String version() + { + return "TimeFlow 0.5"; + } + + public Font bold() + { + return boldFont; + } + + public Font plain() + { + return plainFont; + } + + public Font small() + { + return smallFont; + } + + public FontMetrics hugeFontMetrics() + { + return hugeFontMetrics; + } + + public FontMetrics plainFontMetrics() + { + return plainFontMetrics; + } + + public FontMetrics boldFontMetrics() + { + return boldFontMetrics; + } + + public FontMetrics tinyFontMetrics() + { + return tinyFontMetrics; + } + + public FontMetrics timeLabelFontMetrics() + { + return timeLabelFontMetrics; + } + + public Font huge() + { + return hugeFont; + } + + public Font big() + { + return bigFont; + } + + public Font tiny() + { + return tinyFont; + } + + public Font timeLabel() + { + return timeLabelFont; + } + + public String getString(String s) + { + return strings.get(s); + } + + public int getInt(String s) + { + return ints.get(s); + } + + public Color getColor(String key) + { + if (colors.get(key) == null) + { + throw new IllegalArgumentException("No color for " + key); + } + return colors.get(key); + } + + public JLabel label(String s) + { + return new JLabel(s); + } + + public String format(String s, int maxLength, boolean tryNoDots) + { + if (s == null) + { + return ""; + } + int n = s.length(); + if (n <= maxLength) + { + return s; + } + if (maxLength < 4) + { + return "..."; + } + if (!tryNoDots) + { + return s.substring(0, maxLength - 3) + "..."; + } + // find last space before maxLength and after maxLength/2 + for (int j = maxLength - 1; j > maxLength / 2; j--) + { + if (s.charAt(j) == ' ') + { + return s.substring(0, j); + } + } + return s.length() <= maxLength ? s : (maxLength < 6 ? "" : s.substring(0, maxLength - 3) + "..."); + } + + public void refreshColors(Iterable<String> list) + { + topColors = new HashMap<String, Color>(); + double x = .1; + int i = 0; + for (String s : list) + { + topColors.put(s, i < handPalette.length ? handPalette[i] : palette(x)); + i++; + x += PHI; + } + } + + public Color makeColor(String text) + { + if (grayFilter != null && !grayFilter.ok(text)) + { + return new Color(200, 200, 200);//"null.color"); + } + Color c = topColors.get(text); + return c == null ? _makeColor(text) : c; + } + + private Color _makeColor(String text) + { + if (text == null) + { + return getColor("null.color"); + } + + int c = Math.abs(text.hashCode()); + double h = ((c >> 8) % 255) / 255.; + return palette(h); + } + + public static Color palette(double x) + { + float h = (float) (Math.abs(x) % 1); + float s = .8f - .25f * h; + float b = .8f - .25f * h; + return new Color(Color.HSBtoRGB(h, s, b)); + } + + public String getMiscLabel() + { + return getString("other.label"); + } + + public String getNullLabel() + { + return getString("null.label"); + } + + public String toString(Object o) + { + if (o == null) + { + return getNullLabel(); + } + if (o instanceof Object[]) + { + return arrayToString((Object[]) o); + } + if (o instanceof RoughTime) + { + return ((RoughTime) o).format();//UnitOfTime.format((RoughTime)o); + } + if (o instanceof Double) + { + return df.format((Double) o); + } + return o.toString(); + } + + public static String format(double x) + { + return Math.abs(x) > 999 ? roundFormat.format(x) : df.format(x); + } + + public String format(RoughTime time) + { + if (time == null) + { + return getString("null.label"); + } + return time.format();//UnitOfTime.format(time); + } + + public static ArrayList<String> breakLines(String s, int lineChars, int firstOffset) + { + ArrayList<String> lines = new ArrayList<String>(); + String[] words = s.split(" "); + String line = ""; + + for (int i = 0; i < words.length; i++) + { + // is the word just too, too long? + int n = words[i].length(); + if (n > lineChars - 5) + { + words[i] = words[i].substring(0, lineChars / 2 - 2) + "..." + words[i].substring(n - lineChars / 2 + 2, n); + } + if (line.length() + words[i].length() > (lines.size() == 0 ? lineChars - firstOffset : lineChars)) + { + lines.add(line); + line = ""; + } + line += " " + words[i]; + } + lines.add(line); + return lines; + } + + public boolean emptyMessage(Graphics g, TFModel model) + { + if (model.getActs() == null || model.getActs().size() == 0) + { + g.setColor(getColor("text.prominent")); + g.drawString(model.getDB() == null || model.getDB().size() == 0 ? "Empty Database" : "No items found.", 10, 25); + return true; + } + return false; + } +} diff --git a/timeflow/model/ModelPanel.java b/timeflow/model/ModelPanel.java new file mode 100755 index 0000000..4326f4e --- /dev/null +++ b/timeflow/model/ModelPanel.java @@ -0,0 +1,36 @@ +package timeflow.model; + +import javax.swing.*; + +public abstract class ModelPanel extends JPanel implements TFListener { + + TFModel model; + + public ModelPanel(TFModel model) + { + this.model=model; + } + + @Override + public void addNotify() + { + super.addNotify(); + model.addListener(this); + } + + + @Override + public void removeNotify() + { + super.removeNotify(); + model.removeListener(this); + } + + public TFModel getModel() + { + return model; + } + + @Override + public abstract void note(TFEvent e); +} diff --git a/timeflow/model/TFEvent.java b/timeflow/model/TFEvent.java new file mode 100755 index 0000000..6c22b87 --- /dev/null +++ b/timeflow/model/TFEvent.java @@ -0,0 +1,43 @@ +package timeflow.model; + +public class TFEvent { + public enum Type {DATABASE_CHANGE, ACT_ADD, ACT_DELETE, ACT_CHANGE, ERROR, SOURCE_CHANGE, DESCRIPTION_CHANGE, + FIELD_ADD, FIELD_DELETE, FIELD_CHANGE, SELECTION_CHANGE, FILTER_CHANGE, VIEW_CHANGE}; + public Type type; + public String message="[]"; + public Object info; + public Object origin; + + public TFEvent(Type type, Object origin) + { + this.type=type; + this.origin=origin; + } + + public String toString() + { + return "[TimelineEvent: type="+type+", info="+info+", message="+message+", origin="+origin+"]"; + } + + public boolean affectsSchema() + { + switch (type){ + case DATABASE_CHANGE: + case FIELD_ADD: + case FIELD_DELETE: + case FIELD_CHANGE: return true; + } + return false; + } + + public boolean affectsRowSet() + { + return affectsSchema() || type==Type.ACT_CHANGE || type== Type.ACT_ADD || type== Type.ACT_DELETE + || type==Type.FILTER_CHANGE; + } + + public boolean affectsData() + { + return type!=Type.SELECTION_CHANGE && type!=Type.VIEW_CHANGE && type!=Type.ERROR; + } +} diff --git a/timeflow/model/TFListener.java b/timeflow/model/TFListener.java new file mode 100755 index 0000000..3268d05 --- /dev/null +++ b/timeflow/model/TFListener.java @@ -0,0 +1,5 @@ +package timeflow.model; + +public interface TFListener { + public void note(TFEvent e); +} diff --git a/timeflow/model/TFModel.java b/timeflow/model/TFModel.java new file mode 100755 index 0000000..f4e30e4 --- /dev/null +++ b/timeflow/model/TFModel.java @@ -0,0 +1,303 @@ +package timeflow.model; + +import timeflow.data.db.*; +import timeflow.data.db.filter.*; +import timeflow.data.time.*; + + +import java.util.*; + +// encapsulates all properties of a timeline model: +// data, display properties, etc. +// also does listening, etc. +public class TFModel +{ + + private ActDB db; + private ActList acts; + private ActFilter filter = new ConstFilter(true); + private ArrayList<TFListener> listeners = new ArrayList<TFListener>(); + private Display display = new Display(); + private String[] labelGuesses = + { + "label", "LABEL", "Label", "title", "TITLE", "Title", + "name", "Name", "NAME" + }; + private String[] startGuesses = + { + "start", "Start", "START" + }; + private String dbFile = "[unknown source]"; + private boolean changedSinceSave; + private boolean readOnly; + private double minSize, maxSize; + private Interval viewInterval; + + public ValueFilter getGrayFilter() + { + return display.grayFilter; + } + + public void setGrayFilter(ValueFilter grayFilter, Object origin) + { + display.grayFilter = grayFilter; + fireEvent(new TFEvent(TFEvent.Type.FILTER_CHANGE, origin)); + } + + public ActFilter getFilter() + { + return filter; + } + + public Interval getViewInterval() + { + return viewInterval; + } + + public void setViewInterval(Interval viewInterval) + { + this.viewInterval = viewInterval; + } + + public double getMinSize() + { + return minSize; + } + + public void setMinSize(double minSize) + { + this.minSize = minSize; + } + + public double getMaxSize() + { + return maxSize; + } + + public void setMaxSize(double maxSize) + { + this.maxSize = maxSize; + } + + public String getDbFile() + { + return dbFile; + } + + public boolean getReadOnly() + { + return readOnly; + } + + public void setDbFile(String dbFile, boolean readOnly, Object origin) + { + this.dbFile = dbFile; + fireEvent(new TFEvent(TFEvent.Type.SOURCE_CHANGE, origin)); + } + + public boolean isChangedSinceSave() + { + return changedSinceSave; + } + + public void setChangedSinceSave(boolean changedSinceSave) + { + this.changedSinceSave = changedSinceSave; + } + + public Display getDisplay() + { + return display; + } + + public ActDB getDB() + { + return db; + } + + public ActList getActs() + { + return acts; + } + + public void addListener(TFListener t) + { + listeners.add(t); + } + + public void removeListener(TFListener t) + { + listeners.remove(t); + } + + public void noteNewDescription(Object origin) + { + setChangedSinceSave(true); + fireEvent(new TFEvent(TFEvent.Type.DESCRIPTION_CHANGE, origin)); + } + + public void noteNewSource(Object origin) + { + setChangedSinceSave(true); + fireEvent(new TFEvent(TFEvent.Type.SOURCE_CHANGE, origin)); + } + + public void noteRecordChange(Object origin) + { + setChangedSinceSave(true); + fireEvent(new TFEvent(TFEvent.Type.ACT_CHANGE, origin)); + } + + public void noteAddField(Object origin) + { + setChangedSinceSave(true); + fireEvent(new TFEvent(TFEvent.Type.FIELD_ADD, origin)); + } + + public void noteError(Object origin) + { + fireEvent(new TFEvent(TFEvent.Type.ERROR, origin)); + } + + public void noteDelete(Object origin) + { + setChangedSinceSave(true); + updateActs(); + fireEvent(new TFEvent(TFEvent.Type.ACT_DELETE, origin)); + } + + public void noteSchemaChange(Object origin) + { + setChangedSinceSave(true); + updateActs(); + fireEvent(new TFEvent(TFEvent.Type.DATABASE_CHANGE, origin)); // @TODO: make schema change? + } + + public void noteAdd(Object origin) + { + setChangedSinceSave(true); + updateActs(); + fireEvent(new TFEvent(TFEvent.Type.ACT_ADD, origin)); + } + + public void setFilter(ActFilter filter, Object origin) + { + this.filter = filter; + updateActs(); + fireEvent(new TFEvent(TFEvent.Type.FILTER_CHANGE, origin)); + } + + private void updateActs() + { + acts = db.select(filter); + } + + public void setDB(ActDB db, String dbFile, boolean readOnly, Object origin) + { + this.db = db; + this.dbFile = dbFile; + this.readOnly = readOnly; + setChangedSinceSave(false); + this.filter = new ConstFilter(true); + this.acts = db.all(); + initVisualEncodings(); + refreshColors(); + fireEvent(new TFEvent(TFEvent.Type.DATABASE_CHANGE, origin)); + } + + private void initVisualEncodings() + { + guessField(VirtualField.LABEL, labelGuesses, String.class); + guessField(VirtualField.START, startGuesses, RoughTime.class); + viewInterval = null; + } + + public void refreshColors() + { + display.grayFilter = null; + Field colorField = getColorField(); + if (colorField == null) + { + return; + } + List<String> top25 = DBUtils.countValues(db, colorField).listTop(25); + display.refreshColors(top25); + } + + private void guessField(String name, String[] guesses, Class type) + { + Field field = db.getField(name); + if (field == null) + { + for (int i = 0; i < guesses.length; i++) + { + Field f = db.getField(guesses[i]); + if (f != null && f.getType() == type) + { + field = f; + break; + } + } + if (field == null) + { + List<Field> f = db.getFields(type); + if (f.size() > 0) + { + field = f.get(0); + } + } + if (field != null) + { + db.setAlias(field, name); + } + } + } + + private void fireEvent(TFEvent e) + { + // clone list before going through it, because some events can cause + // listeners to be added or removed. + + for (TFListener t : (List<TFListener>) listeners.clone()) + { + if (t != e.origin) + { + t.note(e); + } + } + } + + public void setFieldAlias(Field field, String alias, Object origin) + { + db.setAlias(field, alias); + if (db.size() > 0 && field == getColorField()) + { + refreshColors(); + } + fireEvent(new TFEvent(TFEvent.Type.VIEW_CHANGE, origin)); + } + + public Field getColorField() + { + if (db == null) + { + return null; + } + Field f = db.getField(VirtualField.COLOR); + if (f != null) + { + return f; + } + return db.getField(VirtualField.TRACK); + } + + public void setReadOnly(boolean readOnly) + { + this.readOnly = readOnly; + } + + public boolean isReadOnly() + { + return readOnly; + } +} diff --git a/timeflow/model/VirtualField.java b/timeflow/model/VirtualField.java new file mode 100755 index 0000000..f046653 --- /dev/null +++ b/timeflow/model/VirtualField.java @@ -0,0 +1,39 @@ +package timeflow.model; + +import java.util.*; + +public class VirtualField { + public static final String LABEL="TIMEFLOW_LABEL"; + public static final String COLOR="TIMEFLOW_COLOR"; + public static final String SIZE="TIMEFLOW_SIZE"; + public static final String TRACK="TIMEFLOW_TRACK"; + public static final String START="TIMEFLOW_START"; + public static final String LATEST_START="TIMEFLOW_LATEST_START"; + public static final String END="TIMEFLOW_END"; + public static final String EARLIEST_END="TIMEFLOW_EARLIEST_END"; + + private static HashMap<String, String> humanNames=new HashMap<String, String>(); + private static void tie(String a, String b) {humanNames.put(a,b);} + + static + { + tie(LABEL, "Label"); + tie(COLOR, "Color"); + tie(SIZE, "Size"); + tie(TRACK, "Track"); + tie(START, "Start"); + tie(LATEST_START, "Latest Start"); + tie(END, "End"); + tie(EARLIEST_END, "Earliest End"); + } + + public static String humanName(String s) + { + return humanNames.get(s); + } + + public static Iterable<String> list() + { + return humanNames.keySet(); + } +} diff --git a/timeflow/util/Bag.java b/timeflow/util/Bag.java new file mode 100755 index 0000000..a575b55 --- /dev/null +++ b/timeflow/util/Bag.java @@ -0,0 +1,136 @@ +package timeflow.util; + +import java.util.*; + +public class Bag<T> implements Iterable<T> { + HashMap<T, Count> table; + int max; + + public Bag() + { + table=new HashMap<T, Count>(); + } + + public Bag(Iterable<T> i) + { + for (T x:i) + add(x); + } + + public Bag(T[] array) + { + for (int i=0; i<array.length; i++) + add(array[i]); + } + + public int getMax() + { + return max; + } + + public List<T> listTop(int n) + { + int count=0; + Iterator<T> i=list().iterator(); + List<T> top=new ArrayList<T>(); + while (count<n && i.hasNext()) + { + top.add(i.next()); + count++; + } + return top; + } + + public List<T> unordered() + { + List<T> result=new ArrayList<T>(); + result.addAll(table.keySet()); + return result; + } + + public List<T> list() + { + List<T> result=new ArrayList<T>(); + result.addAll(table.keySet()); + + Collections.sort(result, new Comparator<T>() + { + public int compare(T x, T y) + { + return num(y)-num(x); + } + }); + return result; + } + + public int num(T x) + { + Count c=table.get(x); + if (c!=null) + return c.num; + else + return 0; + } + + public int add(T x) + { + Count c=table.get(x); + int n=0; + if (c!=null) + n=++c.num; + else + { + table.put(x, new Count(1)); + n=1; + } + max=Math.max(n,max); + return n; + } + + class Count + { + int num; + public Count(int num) + { + this.num=num; + } + } + + public int size() + { + return table.size(); + } + + public int removeLessThan(int cut) + { + + Set<T> small=new HashSet<T>(); + for (T x: table.keySet()) + { + if (num(x)<cut) + small.add(x); + } + for (T x:small) + table.remove(x); + return small.size(); + } + + public static void main(String[] args) + { + Bag<String> b=new Bag<String>(); + b.add("a"); + b.add("b"); + b.add("a"); + System.out.println(b.num("a")); + System.out.println(b.num("b")); + System.out.println(b.num("c")); + List<String> s=b.list(); + for (int i=0; i<s.size(); i++) + System.out.println(s.get(i)+": "+b.num(s.get(i))); + } + + @Override + public Iterator<T> iterator() { + return table.keySet().iterator(); + } +} diff --git a/timeflow/util/ColorUtils.java b/timeflow/util/ColorUtils.java new file mode 100755 index 0000000..dffd8ec --- /dev/null +++ b/timeflow/util/ColorUtils.java @@ -0,0 +1,31 @@ +package timeflow.util; + +import java.awt.*; +import java.awt.image.*; + +public class ColorUtils +{ + + public static Color alpha(Color c, int a) + { + return new Color(c.getRed(), c.getGreen(), c.getBlue(),a); + } + + public static Color interpolate(Color x, Color y, double u) + { + return new Color(interp(x.getRed(), y.getRed(), u), + interp(x.getGreen(), y.getGreen(), u), + interp(x.getBlue(), y.getBlue(), u)); + } + + private static int interp(int x, int y, double u) + { + return (int)(y*u+x*(1-u)); + } + + public static float[] hsb(Color c) + { + return Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), new float[3]); + } + +} diff --git a/timeflow/util/DoubleBag.java b/timeflow/util/DoubleBag.java new file mode 100755 index 0000000..088b36d --- /dev/null +++ b/timeflow/util/DoubleBag.java @@ -0,0 +1,145 @@ +package timeflow.util; + +import java.util.*; + +public class DoubleBag<T> implements Iterable<T> { + private HashMap<T, Count> table; + private double max; + + public DoubleBag() + { + table=new HashMap<T, Count>(); + } + + public double getMax() + { + return max; + } + + public List<T> listTop(int n, boolean useSum) + { + int count=0; + Iterator<T> i=list(useSum).iterator(); + List<T> top=new ArrayList<T>(); + while (count<n && i.hasNext()) + { + top.add(i.next()); + count++; + } + return top; + } + + public List<T> list(final boolean useSum) + { + List<T> result=new ArrayList<T>(); + result.addAll(table.keySet()); + + Collections.sort(result, new Comparator<T>() + { + public int compare(T x, T y) + { + double d= useSum ? num(y)-num(x) : average(y)-average(x); + return d>0 ? 1 : (d<0 ? -1 : 0); + } + }); + return result; + } + + public double num(T x) + { + Count c=table.get(x); + if (c!=null) + return c.num; + else + return 0; + } + + public double average(T x) + { + Count c=table.get(x); + return c.num/c.vals; + } + + public void add(T x, double z) + { + if (Double.isNaN(z)) + return; + Count c=table.get(x); + double sum=z; + if (c!=null) + { + c.add(z); + sum=c.num; + } + else + { + table.put(x, new Count(z)); + } + max=Math.max(sum, max); + } + + class Count + { + double num; + int vals; + + public Count(double num) + { + this.num=num; + vals=1; + } + + public double add(double x) + { + vals++; + return num+=x; + } + } + + public int size() + { + return table.size(); + } + + + public List<T> unordered() + { + List<T> result=new ArrayList<T>(); + result.addAll(table.keySet()); + return result; + } + + + public int removeLessThan(int cut) + { + + Set<T> small=new HashSet<T>(); + for (T x: table.keySet()) + { + if (num(x)<cut) + small.add(x); + } + for (T x:small) + table.remove(x); + return small.size(); + } + + public static void main(String[] args) + { + DoubleBag<String> b=new DoubleBag<String>(); + b.add("a",1); + b.add("b",2); + b.add("a",3); + System.out.println(b.num("a")); + System.out.println(b.num("b")); + System.out.println(b.num("c")); + List<String> s=b.list(true); + for (int i=0; i<s.size(); i++) + System.out.println(s.get(i)+": "+b.num(s.get(i))); + } + + @Override + public Iterator<T> iterator() { + return table.keySet().iterator(); + } +} diff --git a/timeflow/util/IO.java b/timeflow/util/IO.java new file mode 100755 index 0000000..888299b --- /dev/null +++ b/timeflow/util/IO.java @@ -0,0 +1,54 @@ +package timeflow.util; + +import java.io.*; +import java.util.*; + +public class IO { + + public static ArrayList<String> lines(String fileName) throws IOException + { + ArrayList<String> a=new ArrayList<String>(); + String line=null; + FileReader fr=new FileReader(fileName); + BufferedReader in=new BufferedReader(fr); + while (null != (line=in.readLine())) + a.add(line); + in.close(); + fr.close(); + return a; + } + + public static String[] lineArray(String fileName) throws IOException + { + ArrayList<String> a=lines(fileName); + return (String[])a.toArray(new String[0]); + } + + public static String read(File file) throws IOException + { + char[] buffer = new char[1024]; + int n = 0; + StringBuilder builder = new StringBuilder(); + FileReader reader = new FileReader(file); + BufferedReader b = new BufferedReader(reader); + while ((n = b.read(buffer, 0, buffer.length)) != -1) + builder.append(buffer, 0, n); + b.close(); + reader.close(); + return builder.toString(); + } + + public static String read(String fileName) throws IOException + { + char[] buffer = new char[1024]; + int n = 0; + StringBuilder builder = new StringBuilder(); + FileReader reader = new FileReader(fileName); + BufferedReader b = new BufferedReader(reader); + while ((n = b.read(buffer, 0, buffer.length)) != -1) + builder.append(buffer, 0, n); + b.close(); + reader.close(); + return builder.toString(); + } +} diff --git a/timeflow/util/Pad.java b/timeflow/util/Pad.java new file mode 100755 index 0000000..ce9b3d9 --- /dev/null +++ b/timeflow/util/Pad.java @@ -0,0 +1,19 @@ +package timeflow.util; + +import java.awt.*; +import javax.swing.*; + +public class Pad extends JPanel { + Dimension pref; + + public Pad(int w, int h) + { + pref=new Dimension(w,h); + setBackground(Color.white); + } + + public Dimension getPreferredSize() + { + return pref; + } +} diff --git a/timeflow/util/TimeIt.java b/timeflow/util/TimeIt.java new file mode 100755 index 0000000..f78ccb0 --- /dev/null +++ b/timeflow/util/TimeIt.java @@ -0,0 +1,34 @@ +package timeflow.util; + +import java.util.*; + +public class TimeIt { + public static long last; + static HashMap<Object, Long> marks=new HashMap<Object, Long>(); + + public static void mark() + { + last=System.currentTimeMillis(); + } + + public static void sinceLast() + { + long now=System.currentTimeMillis(); + System.out.println("TimeIt: "+(now-last)); + last=now; + } + + public static void since(Object o) + { + long now=System.currentTimeMillis(); + System.out.println("TimeIt: "+o+": "+(now-last)); + last=now; + } + + public static void mark(Object o) + { + long now=System.currentTimeMillis(); + marks.put(o, System.currentTimeMillis()); + last=now; + } +} diff --git a/timeflow/views/AbstractView.java b/timeflow/views/AbstractView.java new file mode 100755 index 0000000..a56a933 --- /dev/null +++ b/timeflow/views/AbstractView.java @@ -0,0 +1,85 @@ +package timeflow.views; + +import javax.swing.*; + +import timeflow.data.db.ActDB; +import timeflow.model.*; + +import java.awt.*; + +// superclass of all timeline views +public abstract class AbstractView extends ModelPanel +{ + + protected boolean ignoreEventsWhenInvisible = true; + JPanel panel; + ActDB lastDrawn, lastNotified; + + public AbstractView(TFModel model) + { + super(model); + } + + public void paintComponent(Graphics g) + { + g.drawString(getName(), 10, 30); + } + + public final JComponent getControls() + { + if (panel != null) + { + return panel; + } + + panel = new JPanel(); + panel.setLayout(new BorderLayout()); + panel.add(_getControls(), BorderLayout.CENTER); + JLabel controlLabel = new JLabel(" " + getName() + " Controls") + { + + public Dimension getPreferredSize() + { + return new Dimension(30, 30); + } + }; + controlLabel.setBackground(Color.lightGray); + controlLabel.setForeground(Color.darkGray); + panel.add(controlLabel, BorderLayout.NORTH); + + return panel; + } + + protected JComponent _getControls() + { + return new JLabel("local: " + getName()); + } + + public abstract String getName(); + + protected abstract void _note(TFEvent e); + + protected abstract void onscreen(boolean majorChangeHappened); + + @Override + public final void note(TFEvent e) + { + lastNotified = getModel().getDB(); + if (isVisible() || !ignoreEventsWhenInvisible) + { + _note(e); + lastDrawn = lastNotified; + } + } + + @Override + public void setVisible(boolean visible) + { + super.setVisible(visible); + if (visible && getModel().getDB() != null) + { + onscreen(lastDrawn != lastNotified); + lastDrawn = lastNotified; + } + } +} diff --git a/timeflow/views/AbstractVisualizationView.java b/timeflow/views/AbstractVisualizationView.java new file mode 100755 index 0000000..51026d6 --- /dev/null +++ b/timeflow/views/AbstractVisualizationView.java @@ -0,0 +1,244 @@ +package timeflow.views; + +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.model.*; +import timeflow.vis.*; +import timeflow.app.ui.*; + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.BufferedImage; +import java.net.URL; +import java.util.*; + +import javax.swing.*; + +public abstract class AbstractVisualizationView extends JPanel +{ + + Image buffer; + Graphics2D graphics; + Point mouse = new Point(-10000, 0), firstMouse = new Point(); + boolean mouseIsDown; + ArrayList<Mouseover> objectLocations = new ArrayList<Mouseover>(); + TFModel model; + Act selectedAct; + RoughTime selectedTime; + Set<JMenuItem> urlItems = new HashSet<JMenuItem>(); + + public AbstractVisualizationView(TFModel model) + { + this.model = model; + + // deal with mouseovers. + addMouseMotionListener(new MouseMotionListener() + { + + @Override + public void mouseDragged(MouseEvent e) + { + mouse.setLocation(e.getX(), e.getY()); + repaint(); + } + + @Override + public void mouseMoved(MouseEvent e) + { + mouse.setLocation(e.getX(), e.getY()); + repaint(); + } + }); + + + final JPopupMenu popup = new JPopupMenu(); + final JMenuItem edit = new JMenuItem("Edit"); + edit.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + EditRecordPanel.edit(getModel(), selectedAct); + } + }); + popup.add(edit); + + final JMenuItem delete = new JMenuItem("Delete"); + popup.add(delete); + delete.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + getModel().getDB().delete(selectedAct); + getModel().noteDelete(this); + } + }); + + final JMenuItem add = new JMenuItem("New..."); + popup.add(add); + add.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + EditRecordPanel.add(getModel(), selectedTime); + } + }); + + // deal with right-click. + addMouseListener(new MouseAdapter() + { + + public void mousePressed(MouseEvent e) + { + pop(e); + } + + public void mouseReleased(MouseEvent e) + { + pop(e); + } + + private void pop(MouseEvent e) + { + if (e.isPopupTrigger()) + { + Point p = new Point(e.getX(), e.getY()); + Mouseover o = find(p); + boolean onAct = o != null && o.thing instanceof VisualAct; + if (onAct) + { + VisualAct v = (VisualAct) o.thing; + selectedAct = v.getAct(); + String name = " '" + v.getLabel() + "'"; + edit.setText("Edit" + name + "..."); + delete.setText("Delete" + name); + edit.setEnabled(true); + delete.setEnabled(true); + } else + { + edit.setEnabled(false); + edit.setText("Edit Event"); + delete.setEnabled(false); + delete.setText("Delete Event"); + } + selectedTime = getTime(p); + if (selectedTime != null || onAct) + { + add.setEnabled(selectedTime != null); + add.setText(selectedTime == null ? "Add" : "Add Event At " + selectedTime.format() + "..."); + + java.util.List<Field> urlFields = getModel().getDB().getFields(URL.class); + if (urlFields.size() > 0) + { + // remove any old items. + for (JMenuItem m : urlItems) + { + popup.remove(m); + } + urlItems.clear(); + + if (onAct) + { + Act a = ((VisualAct) o.thing).getAct(); + for (Field f : urlFields) + { + final URL url = a.getURL(f); + JMenuItem go = new JMenuItem("Go to " + url); + go.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + Display.launchBrowser(url.toString()); + } + }); + popup.add(go); + urlItems.add(go); + } + } + } + + popup.show(e.getComponent(), p.x, p.y); + } + } + } + }); + } + + public RoughTime getTime(Point p) + { + return null; + } + + public TFModel getModel() + { + return model; + } + + @Override + public void setBounds(int x, int y, int w, int h) + { + super.setBounds(x, y, w, h); + if (w > 0 && h > 0) + { + if (graphics != null) + { + graphics.dispose(); + } + buffer = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + graphics = (Graphics2D) buffer.getGraphics(); + drawVisualization(); + repaint(); + } + + } + + void drawVisualization() + { + drawVisualization(graphics); + } + + protected abstract void drawVisualization(Graphics2D g); + + protected boolean paintOnTop(Graphics2D g, int w, int h) + { + return false; + } + + protected Mouseover find(Point p) + { + for (Mouseover o : objectLocations) + { + if (o.contains(mouse)) + { + return o; + } + } + return null; + } + + public final void paintComponent(Graphics g1) + { + Graphics2D g = (Graphics2D) g1; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.drawImage(buffer, 0, 0, null); + + int w = getSize().width, h = getSize().height; + if (paintOnTop(g, w, h)) + { + return; + } + + Mouseover highlight = find(mouse); + if (highlight != null) + { + highlight.draw(g, w, h, getModel().getDisplay()); + } + } +} diff --git a/timeflow/views/BarGraphView.java b/timeflow/views/BarGraphView.java new file mode 100755 index 0000000..a55ad05 --- /dev/null +++ b/timeflow/views/BarGraphView.java @@ -0,0 +1,363 @@ +package timeflow.views; + +import timeflow.model.*; +import timeflow.views.ListView.LinkIt; +import timeflow.data.db.*; +import timeflow.data.time.*; + +import javax.swing.*; + +import timeflow.util.*; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +public class BarGraphView extends AbstractView +{ + + BarGraph graph = new BarGraph(); + JPanel controls; + ArrayList<BarData> bars; + + enum Aggregate + { + + TOTAL, AVERAGE, COUNT + }; + Aggregate agg; + JComboBox splitFieldChoice, numFieldChoice; + + public BarGraphView(TFModel model) + { + super(model); + + setLayout(new BorderLayout()); + controls = new JPanel(); + add(controls, BorderLayout.NORTH); + controls.setLayout(null); + controls.setBackground(Color.white); + + JScrollPane scrollPane = new JScrollPane(graph); + add(scrollPane, BorderLayout.CENTER); + + makeTop(); + } + + protected JComponent _getControls() + { + return controls; + } + + void makeTop() + { + int x = 10, y = 10; + int ch = 25, pad = 5, cw = 160; + + controls.removeAll(); + TFModel model = getModel(); + if (model.getDB() == null || model.getDB().size() == 0) + { + JLabel empty = new JLabel("Empty database"); + controls.add(empty); + empty.setBounds(x, y, cw, ch); + return; + } + + JLabel top = new JLabel("For each value of"); + controls.add(top); + top.setBounds(x, y, cw, ch); + y += ch + pad; + + splitFieldChoice = new JComboBox(); + String splitSelection = null; + for (Field f : DBUtils.categoryFields(model.getDB())) + { + splitFieldChoice.addItem(f.getName()); + if (f == graph.splitField) + { + splitSelection = f.getName(); + } + } + controls.add(splitFieldChoice); + splitFieldChoice.setBounds(x, y, cw, ch); + y += ch + 3 * pad; + + if (splitSelection != null) + { + splitFieldChoice.setSelectedItem(splitSelection); + } else if (getModel().getColorField() != null) + { + splitFieldChoice.setSelectedItem(getModel().getColorField().getName()); + } + splitFieldChoice.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + graph.redo(); + } + }); + + JLabel showLabel = new JLabel("show"); + controls.add(showLabel); + showLabel.setBounds(x, y, cw, ch); + y += ch + pad; + + numFieldChoice = new JComboBox(); + numFieldChoice.addItem("Number of events"); + final ArrayList<Field> valueFields = new ArrayList<Field>(); + for (Field f : model.getDB().getFields(Double.class)) + { + numFieldChoice.addItem("Total: " + f.getName()); + numFieldChoice.addItem("Average: " + f.getName()); + valueFields.add(f); + } + controls.add(numFieldChoice); + numFieldChoice.setBounds(x, y, cw, ch); + + boolean chosen = false; + for (int i = 0; i < numFieldChoice.getItemCount(); i++) + { + if (numFieldChoice.getItemAt(i).equals(graph.lastValueMenuChoice)) + { + numFieldChoice.setSelectedIndex(i); + chosen = true; + } + } + if (!chosen) + { + Field size = getModel().getDB().getField(VirtualField.SIZE); + if (size != null) + { + numFieldChoice.setSelectedItem("Total: " + size.getName()); + } + } + numFieldChoice.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + + graph.redo(); + } + }); + revalidate(); + repaint(); + } + + void reset() + { + makeTop(); + graph.redo(); + revalidate(); + repaint(); + } + + @Override + protected void _note(TFEvent e) + { + if (e.affectsSchema()) + { + reset(); + } else + { + graph.redo(); + } + repaint(); + } + + @Override + public String getName() + { + return "Bar Graph"; + } + + @Override + protected void onscreen(boolean majorChange) + { + reset(); + } + + class BarData + { + + Object thing; + double num; + + BarData(Object thing, double num) + { + this.thing = thing; + this.num = num; + } + } + + class BarGraph extends JPanel + { + + int numVals = 0; + int rowHeight = 30; + int barHeight = 20; + int labelX = 10, barLeft = 300, barRight; + int topY = 45; + int numX = 210; + Field splitField, valueField; + String lastValueMenuChoice; + double min, max; + + void redo() + { + bars = new ArrayList<BarData>(); + splitField = getModel().getDB().getField((String) splitFieldChoice.getSelectedItem()); + if (splitField != null) + { + int n = numFieldChoice.getSelectedIndex(); + + if (n == 0) + { + agg = Aggregate.COUNT; + } else + { + agg = n % 2 == 1 ? Aggregate.TOTAL : Aggregate.AVERAGE; + } + + if (agg == Aggregate.COUNT) + { + Bag<String> bag = DBUtils.countValues(getModel().getActs(), splitField); + for (String s : bag.list()) + { + bars.add(new BarData(s, bag.num(s))); + } + } else + { + lastValueMenuChoice = (String) numFieldChoice.getSelectedItem(); + int colon = lastValueMenuChoice.indexOf(':'); + valueField = getModel().getDB().getField(lastValueMenuChoice.substring(colon + 2)); + DoubleBag<String> bag = new DoubleBag<String>(); + for (Act a : getModel().getActs()) + { + if (splitField.getType() == String.class) + { + bag.add(a.getString(splitField), a.getValue(valueField)); + } else + { + String[] tags = a.getTextList(splitField); + for (String tag : tags) + { + bag.add(tag, a.getValue(valueField)); + } + } + } + boolean isSum = agg == Aggregate.TOTAL; + for (String s : bag.list(isSum)) + { + bars.add(new BarData(s, isSum ? bag.num(s) : bag.average(s))); + } + } + } + revalidate(); + repaint(); + } + + public void paintComponent(Graphics g1) + { + Graphics2D g = (Graphics2D) g1; + int w = getSize().width, h = getSize().height; + g.setColor(Color.white); + g.fillRect(0, 0, w, h); + TFModel model = getModel(); + Display display = model.getDisplay(); + + if (display.emptyMessage(g, model)) + { + return; + } + + if (bars == null) + { + return; + } + + if (bars.size() == 0) + { + g.setColor(Color.gray); + g.drawString("(No data selected.)", 10, 30); + return; + } + + int n = bars.size(); + max = bars.get(0).num; + min = Math.min(0, bars.get(n - 1).num); + + + barRight = w - 30; + + int zero = scaleX(0); + boolean isInColor = (splitField != null && getModel().getColorField() == splitField); + + // draw header + int titleY = topY - 15; + g.setColor(Color.black); + g.setFont(display.big()); + g.drawString(splitField.getName().toUpperCase(), labelX, titleY); + String aggLabel = agg.toString(); + if (agg != Aggregate.COUNT) + { + aggLabel += " " + valueField.getName().toUpperCase(); + } + g.drawString(aggLabel, barLeft, titleY); + g.setFont(display.plain()); + FontMetrics fm = display.plainFontMetrics(); + // draw bars + + for (int i = 0; i < n; i++) + { + int y = topY + i * rowHeight; + int ty = y + barHeight; + BarData data = bars.get(i); + + Color c = null; + + g.setColor(Color.gray); + + // label value + boolean missing = data.thing == null || (data.thing.toString().length() == 0); + String label = missing ? "[missing]" + : display.format(display.toString(data.thing), 25, false); + if (isInColor) + { + g.setColor(missing ? Color.gray : display.makeColor(data.thing.toString())); + display.makeColor(label); + } + g.drawString(label, labelX, ty); + + // label number + String numLabel = display.format(data.num); + g.drawString(numLabel, numX + 70 - fm.stringWidth(numLabel), ty); + + // draw bar. + g.setColor(missing ? Color.lightGray : (isInColor ? c : Display.barColor)); + int x = scaleX(data.num); + int a = Math.min(x, zero); + int b = Math.max(x, zero); + g.fillRect(a, y + 5, b - a, barHeight); + } + } + + int scaleX(double x) + { + if (max == min) + { + return barLeft; + } + return (int) (barLeft + (barRight - barLeft) * (x - min) / (max - min)); + } + + public Dimension getPreferredSize() + { + return new Dimension(400, 100 + rowHeight * (bars == null ? 0 : bars.size())); + } + } +} diff --git a/timeflow/views/CalendarView.java b/timeflow/views/CalendarView.java new file mode 100755 index 0000000..354b939 --- /dev/null +++ b/timeflow/views/CalendarView.java @@ -0,0 +1,304 @@ +package timeflow.views; + +import timeflow.app.ui.ComponentCluster; +import timeflow.data.db.*; +import timeflow.data.time.Interval; +import timeflow.data.time.RoughTime; +import timeflow.model.*; +import timeflow.vis.*; +import timeflow.vis.calendar.*; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Date; + +public class CalendarView extends AbstractView { + + CalendarPanel calendarPanel; + ScrollingCalendar scroller; + CalendarVisuals visuals; + ActDB lastDB; + JPanel controls; + + @Override + public JComponent _getControls() + { + return controls; + } + + public CalendarView(TFModel model) + { + super(model); + calendarPanel=new CalendarPanel(model); + scroller=new ScrollingCalendar(); + setLayout(new GridLayout(1,1)); + add(scroller); + + controls=new JPanel(); + controls.setLayout(new BorderLayout()); + + ComponentCluster units=new ComponentCluster("Grid"); + controls.add(units, BorderLayout.NORTH); + + ButtonGroup unitGroup=new ButtonGroup(); + + JRadioButton days=new JRadioButton(new ImageIcon("images/button_days.gif"),true); + days.setSelectedIcon(new ImageIcon("images/button_days_selected.gif")); + units.addContent(days); + days.addActionListener(new LayoutSetter(CalendarVisuals.Layout.DAY)); + unitGroup.add(days); + + JRadioButton months=new JRadioButton(new ImageIcon("images/button_months.gif"),false); + months.setSelectedIcon(new ImageIcon("images/button_months_selected.gif")); + units.addContent(months); + months.addActionListener(new LayoutSetter(CalendarVisuals.Layout.MONTH)); + unitGroup.add(months); + + JRadioButton years=new JRadioButton(new ImageIcon("images/button_years.gif"),false); + years.setSelectedIcon(new ImageIcon("images/button_years_selected.gif")); + units.addContent(years); + years.addActionListener(new LayoutSetter(CalendarVisuals.Layout.YEAR)); + unitGroup.add(years); + + + ComponentCluster showCluster=new ComponentCluster("Show"); + controls.add(showCluster, BorderLayout.CENTER); + + ButtonGroup group=new ButtonGroup(); + JRadioButton icon=new JRadioButton(new ImageIcon("images/button_dots.gif"),true); + icon.setSelectedIcon(new ImageIcon("images/button_dots_selected.gif")); + showCluster.addContent(icon); + icon.addActionListener(new DrawStyleSetter(CalendarVisuals.DrawStyle.ICON)); + group.add(icon); + + JRadioButton label=new JRadioButton(new ImageIcon("images/button_labels.gif"),false); + label.setSelectedIcon(new ImageIcon("images/button_labels_selected.gif")); + showCluster.addContent(label); + label.addActionListener(new DrawStyleSetter(CalendarVisuals.DrawStyle.LABEL)); + group.add(label); + + ComponentCluster layout=new ComponentCluster("Layout"); + controls.add(layout, BorderLayout.SOUTH); + + ButtonGroup layoutGroup=new ButtonGroup(); + JRadioButton loose=new JRadioButton(new ImageIcon("images/button_expanded.gif"),true); + loose.setSelectedIcon(new ImageIcon("images/button_expanded_selected.gif")); + layout.addContent(loose); + loose.addActionListener(new FitStyleSetter(CalendarVisuals.FitStyle.LOOSE)); + layoutGroup.add(loose); + + JRadioButton tight=new JRadioButton(new ImageIcon("images/button_compressed.gif"),false); + tight.setSelectedIcon(new ImageIcon("images/button_compressed_selected.gif")); + layout.addContent(tight); + tight.addActionListener(new FitStyleSetter(CalendarVisuals.FitStyle.TIGHT)); + layoutGroup.add(tight); + } + + class LayoutSetter implements ActionListener + { + CalendarVisuals.Layout layout; + LayoutSetter(CalendarVisuals.Layout layout) + { + this.layout=layout; + } + @Override + public void actionPerformed(ActionEvent e) { + setLayoutStyle(layout); + } + } + + class DrawStyleSetter implements ActionListener + { + CalendarVisuals.DrawStyle style; + DrawStyleSetter(CalendarVisuals.DrawStyle style) + { + this.style=style; + } + @Override + public void actionPerformed(ActionEvent e) { + setDrawStyle(style); + } + } + + class FitStyleSetter implements ActionListener + { + CalendarVisuals.FitStyle style; + FitStyleSetter(CalendarVisuals.FitStyle style) + { + this.style=style; + } + @Override + public void actionPerformed(ActionEvent e) { + setFitStyle(style); + } + } + + @Override + public String getName() { + return "Calendar"; + } + + private void redraw(boolean fresh) + { + visuals.makeGrid(fresh); + calendarPanel.drawVisualization(); + repaint(); + } + + void setLayoutStyle(CalendarVisuals.Layout layout) + { + visuals.setLayoutStyle(layout); + calendarPanel.drawVisualization(); + revalidate(); + repaint(); + } + + void setDrawStyle(CalendarVisuals.DrawStyle style) + { + visuals.setDrawStyle(style); + calendarPanel.drawVisualization(); + revalidate(); + repaint(); + } + + void setFitStyle(CalendarVisuals.FitStyle style) + { + visuals.setFitStyle(style); + calendarPanel.drawVisualization(); + revalidate(); + repaint(); + } + + @Override + protected void onscreen(boolean majorChange) + { + visuals.initAllButGrid(); + scroller.calibrate(true); + revalidate(); + ActDB db=getModel().getDB(); + redraw(majorChange); + scroller.calibrate(majorChange); + lastDB=db; + } + + @Override + protected void _note(TFEvent e) { + int oldHeight=calendarPanel.getPreferredSize().height; + visuals.note(e); + + calendarPanel.drawVisualization(); + calendarPanel.repaint(); + if (e.affectsData() || oldHeight!=calendarPanel.getPreferredSize().height) + { + SwingUtilities.invokeLater(new Runnable() { + public void run() {scroller.calibrate(false); revalidate();} + }); + } + revalidate(); + } + + public void setBounds(int x, int y, int w, int h) + { + super.setBounds(x,y,w,h); + if (visuals==null || visuals.grid==null) + return; + calendarPanel.drawVisualization(); + calendarPanel.repaint(); + } + + class ScrollingCalendar extends JPanel + { + JScrollBar bar; + public ScrollingCalendar() + { + setLayout(new BorderLayout()); + add(calendarPanel, BorderLayout.CENTER); + bar=new JScrollBar(JScrollBar.VERTICAL); + add(bar, BorderLayout.EAST); + bar.addAdjustmentListener(new AdjustmentListener() { + @Override + public void adjustmentValueChanged(AdjustmentEvent e) { + visuals.grid.setDY(bar.getValue()); + + // set time in model. + RoughTime startTime=visuals.grid.getFirstDrawnTime(); + Interval viewInterval=getModel().getViewInterval(); + if (viewInterval!=null) + { + viewInterval.translateTo(startTime.getTime()); + } + + calendarPanel.drawVisualization(); + calendarPanel.repaint(); + } + }); + } + + public void setBounds(int x, int y, int w, int h) + { + if (x==getX() && y==getY() && w==getWidth() && h==getHeight()) + return; + super.setBounds(x,y,w,h); + calibrate(false); + } + + void calibrate(boolean forceValue) + { + if (visuals==null || visuals.grid==null) + return; + int height=getSize().height; + int desired=visuals.grid.getCalendarHeight(); + bar.setVisible(desired>height); + if (desired>height) + { + bar.setMinimum(0); + bar.setMaximum(desired); + bar.setVisibleAmount(height); + Interval view=getModel().getViewInterval(); + if (view!=null && forceValue) + { + double s=visuals.grid.getScrollFraction(); + double maxFraction=(desired-height)/(double)desired; + int value=(int)((s/maxFraction)*desired); + bar.setValue(value); + } + + } + } + } + + class CalendarPanel extends AbstractVisualizationView + { + + CalendarPanel(TFModel model) + { + super(model); + setBackground(Color.white); + visuals=new CalendarVisuals(getModel()); + } + + public RoughTime getTime(Point p) + { + return visuals.grid.getTime(p.x, p.y); + } + + protected void drawVisualization(Graphics2D g) + { + g.setBackground(Color.white); + g.fillRect(0,0,getSize().width, getSize().height); + + getModel().getDisplay().emptyMessage(g, model); + if (model.getDB()==null) + return; + visuals.setBounds(0,0,getSize().width,getSize().height); + objectLocations=new ArrayList<Mouseover>(); + visuals.render(g, objectLocations); + } + } +} diff --git a/timeflow/views/DescriptionView.java b/timeflow/views/DescriptionView.java new file mode 100755 index 0000000..45d66c7 --- /dev/null +++ b/timeflow/views/DescriptionView.java @@ -0,0 +1,71 @@ +package timeflow.views; + +import timeflow.model.*; +import timeflow.util.Pad; +import timeflow.data.db.*; + +import java.awt.*; +import java.awt.event.*; + +import javax.swing.*; + +public class DescriptionView extends AbstractView { + + JTextArea content; + JComponent controls; + + public DescriptionView(TFModel model) { + super(model); + setLayout(new BorderLayout()); + JPanel left=new Pad(5,5); + left.setBackground(Color.white); + add(left, BorderLayout.WEST); + JPanel right=new Pad(5,5); + right.setBackground(Color.white); + add(right, BorderLayout.EAST); + JPanel top=new JPanel(); + add(top, BorderLayout.NORTH); + top.setLayout(new FlowLayout(FlowLayout.LEFT)); + top.add(new JLabel("Notes & Comments on This Data:")); + content=new JTextArea(); + content.setLineWrap(true); + content.setWrapStyleWord(true); + add(content, BorderLayout.CENTER); + content.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + getModel().getDB().setDescription(content.getText()); + getModel().noteNewDescription(DescriptionView.this); + }}); + controls=new HtmlControls("Each TimeFlow data set<br> comes with a free-form <br> "+ + "description area. <p>This is a good place to write<br> notes "+ + "about sources, how the data<br>was cleaned, etc."); + } + + @Override + public JComponent _getControls() + { + return controls; + } + + @Override + protected void _note(TFEvent e) { + if (e.type==TFEvent.Type.DESCRIPTION_CHANGE || e.type==TFEvent.Type.DATABASE_CHANGE) + { + content.setText(getModel().getDB().getDescription()); + repaint(); + } + } + + @Override + public String getName() { + return "Notes"; + } + + @Override + protected void onscreen(boolean majorChange) { + ActDB db=getModel().getDB(); + content.setText(db.getDescription()); + content.requestFocus(); + } +} diff --git a/timeflow/views/HtmlControls.java b/timeflow/views/HtmlControls.java new file mode 100755 index 0000000..534e5ba --- /dev/null +++ b/timeflow/views/HtmlControls.java @@ -0,0 +1,21 @@ +package timeflow.views; + +import java.awt.Color; +import java.awt.GridLayout; + +import javax.swing.*; + +import timeflow.app.ui.HtmlDisplay; + +public class HtmlControls extends JPanel { + + public HtmlControls(String text) + { + JEditorPane html=HtmlDisplay.create(); + html.setText(text); + setBackground(Color.white); + setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); + setLayout(new GridLayout(1,1)); + add(html); + } +} diff --git a/timeflow/views/IntroView.java b/timeflow/views/IntroView.java new file mode 100755 index 0000000..8540401 --- /dev/null +++ b/timeflow/views/IntroView.java @@ -0,0 +1,105 @@ +package timeflow.views; + +import timeflow.app.ui.EditRecordPanel; +import timeflow.data.analysis.*; +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.format.field.FieldFormatCatalog; +import timeflow.model.*; +import timeflow.views.*; +import timeflow.views.ListView.LinkIt; + +import java.awt.*; +import java.io.File; +import java.net.URL; +import java.util.Date; + +import javax.imageio.ImageIO; +import javax.swing.JComponent; +import javax.swing.JEditorPane; +import javax.swing.JScrollPane; +import javax.swing.UIManager; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkListener; +import javax.swing.table.TableModel; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.StyleSheet; + +import timeflow.util.*; + +public class IntroView extends AbstractView +{ + + private JComponent controls; + Image image; + Image repeat; + + public IntroView(TFModel model) + { + super(model); + setBackground(Color.white); + try + { + image = ImageIO.read(new File("images/intro.gif")); + repeat = ImageIO.read(new File("images/repeat.gif")); + } catch (Exception e) + { + System.out.println("Couldn't load images."); + e.printStackTrace(System.out); + } + makeHtml(); + } + + public void paintComponent(Graphics g) + { + g.setColor(Color.white); + int w = getSize().width, h = getSize().height; + g.fillRect(0, 0, w, h); + // draw image and extensible background, so it looks cool on a big screen. + if (image != null && repeat != null) + { + int ih = image.getHeight(null); + int iw = image.getWidth(null); + int rw = repeat.getWidth(null); + g.drawImage(image, 0, 0, null); + for (int x = iw; x < w; x += rw) + { + g.drawImage(repeat, x, 0, null); + } + } + } + + void makeHtml() + { + try + { + String sidebar = IO.read("settings/sidebar.html"); + controls = new HtmlControls(sidebar); + } catch (Exception e) + { + e.printStackTrace(System.out); + } + } + + @Override + public JComponent _getControls() + { + return controls; + } + + @Override + protected void onscreen(boolean majorChange) + { + } + + protected void _note(TFEvent e) + { + // do nothing. + } + + @Override + public String getName() + { + return "About"; + } +} diff --git a/timeflow/views/ListView.java b/timeflow/views/ListView.java new file mode 100755 index 0000000..ec87f1c --- /dev/null +++ b/timeflow/views/ListView.java @@ -0,0 +1,266 @@ +package timeflow.views; + +import timeflow.app.ui.EditRecordPanel; +import timeflow.app.ui.HtmlDisplay; +import timeflow.data.db.*; +import timeflow.format.file.HtmlFormat; +import timeflow.model.*; + +import java.awt.*; +import java.awt.event.*; + +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.table.*; +import javax.swing.text.html.*; + +import timeflow.util.*; + +import java.net.URI; +import java.net.URL; +import java.util.*; + +public class ListView extends AbstractView { + + private JEditorPane listDisplay; + private JComboBox sortMenu=new JComboBox(); + private ActComparator sort;//=ActComparator.byTime(); + private int maxPerPage=50; + private int pageStart=0; + private int lastSize=0; + private ActList acts; + private Field sortField; + + private JLabel pageLabel=new JLabel("Page", JLabel.LEFT); + private JComboBox pageMenu=new JComboBox(); + private boolean changing=false; + + private JPanel controls; + + public ListView(TFModel model) + { + super(model); + + listDisplay=HtmlDisplay.create(); + listDisplay.addHyperlinkListener(new LinkIt()); + JScrollPane scrollPane = new JScrollPane(listDisplay); + setLayout(new BorderLayout()); + add(scrollPane, BorderLayout.CENTER); + + + controls=new JPanel(); + controls.setLayout(null); + controls.setBackground(Color.white); + + int x=10, y=10; + int ch=25, pad=5, cw=160; + JLabel sortLabel=new JLabel("Sort Order", JLabel.LEFT); + controls.add(sortLabel); + sortLabel.setBounds(x,y,cw,ch); + y+=ch+pad; + + controls.add(sortMenu); + sortMenu.setBounds(x,y,cw,ch); + y+=ch+3*pad; + + controls.add(pageLabel); + pageLabel.setBounds(x,y,cw,ch); + y+=ch+pad; + controls.add(pageMenu); + pageMenu.setBounds(x,y,cw,ch); + + showPageMenu(false); + pageMenu.addActionListener(pageListener); + sortMenu.addActionListener(sortListener); + } + + protected JComponent _getControls() + { + return controls; + } + + ActionListener sortListener=new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (changing || sortMenu.getItemCount()<=0) // this means the action was fired after all items removed. + return; + sortField=getModel().getDB().getField((String)sortMenu.getSelectedItem()); + sort=sortField==null ? null : ActComparator.by(sortField); + setToFirstPage(); + makeList(); + }}; + + ActionListener pageListener=new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (changing) + return; + pageStart=maxPerPage*pageMenu.getSelectedIndex(); + System.out.println(e.getActionCommand()); + makeList(); + }}; + + @Override + protected void onscreen(boolean majorChange) + { + _note(null); + } + + public void _note(TFEvent e) { + changing=true; + if (e==null || e.affectsSchema() || e.affectsRowSet()) + { + sortMenu.removeActionListener(sortListener); + sortMenu.removeAllItems(); + pageStart=0; + java.util.List<Field> fields=getModel().getDB().getFields(); + Field firstField=null; + if (fields.size()>0) + firstField=fields.get(0); + for (Field f: fields) + { + sortMenu.addItem(f.getName()); + } + sortField=getModel().getDB().getField(VirtualField.START); + if (sortField!=null) + sortMenu.setSelectedItem(sortField.getName()); + else + sortField=firstField; + sortMenu.addActionListener(sortListener); + sort=null; + } + if (e!=null && e.affectsData()) + { + setToFirstPage(); + } + changing=false; + makeList(); + } + + private void setToFirstPage() + { + pageStart=0; + if (pageMenu.isVisible()) + { + pageMenu.removeActionListener(pageListener); + pageMenu.setSelectedIndex(0); + pageMenu.addActionListener(pageListener); + } + } + + void showPageMenu(boolean visible) + { + pageLabel.setVisible(visible); + pageMenu.setVisible(visible); + if (visible) + { + pageMenu.removeActionListener(pageListener); + pageMenu.setSelectedIndex(pageStart/maxPerPage); + pageMenu.addActionListener(pageListener); + } + } + + + void makeList() + { + HtmlFormat html=new HtmlFormat(); + html.setModel(getModel()); + StringBuffer page=new StringBuffer(); + + page.append(html.makeHeader()); + + + ActList as=getModel().getActs(); + if (as==null || as.size()==0 && getModel().getDB().size()==0) + { + page.append("<tr><td><h1><font color=#003399>Empty Database</font></h1></td></tr>"); + showPageMenu(false); + } + else + { + + if (sort==null) + { + Field timeField=getModel().getDB().getField(VirtualField.START); + if (timeField!=null) + sort=ActComparator.by(timeField); + } + + acts=as.copy(); + if (sort!=null) + Collections.sort(acts, sort); + + boolean pages=acts.size()>maxPerPage; + int last=Math.min(acts.size(), pageStart+maxPerPage); + if (pages) + { + int n=acts.size(); + if (lastSize!=n) + { + pageMenu.removeActionListener(pageListener); + pageMenu.removeAllItems(); + for (int i=0; i*maxPerPage<n;i++) + { + pageMenu.addItem("Items "+((i*maxPerPage)+1)+" to "+ + Math.min(n, (i+1)*maxPerPage)); + } + pageMenu.addActionListener(pageListener); + lastSize=n; + } + } + showPageMenu(pages); + + page.append("<tr><td><h1><font color=#003399>"+(pages? (pageStart+1)+"-"+(last) +" of ": "")+acts.size()+" Events</font></h1>"); + page.append("<br><br></td></tr>"); + + for (int i=pageStart; i<last; i++) + { + Act a=acts.get(i); + page.append(html.makeItem(a,i)); + } + } + page.append(html.makeFooter()); + listDisplay.setText(page.toString()); + listDisplay.setCaretPosition(0); + repaint(); + } + + + + + @Override + public String getName() { + return "List"; + } + + static class ArrayRenderer extends DefaultTableCellRenderer { + public void setValue(Object value) { + setText(Display.arrayToString((Object[])value)); + } + } + + public class LinkIt implements HyperlinkListener + { + public void hyperlinkUpdate(HyperlinkEvent e) + { + if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) + return; + + String s=e.getDescription(); + System.out.println(s); + if (s.length()>0) + { + char c=s.charAt(0); + if (c=='e') // code for "edit" + { + int i=Integer.parseInt(s.substring(1)); + EditRecordPanel.edit(getModel(), acts.get(i)); + return; + } + + } + Display.launchBrowser(e.getURL().toString()); + + } + } +} diff --git a/timeflow/views/SummaryView.java b/timeflow/views/SummaryView.java new file mode 100755 index 0000000..b51b638 --- /dev/null +++ b/timeflow/views/SummaryView.java @@ -0,0 +1,181 @@ +package timeflow.views; + +import timeflow.app.ui.HtmlDisplay; +import timeflow.data.analysis.*; +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.format.field.FieldFormatCatalog; +import timeflow.model.*; + + +import java.awt.*; +import java.util.Date; + +import javax.swing.*; + + +public class SummaryView extends AbstractView { + + private JEditorPane analysisDisplay; + private FieldAnalysis[] fieldAnalyzers=new FieldAnalysis[] + { + new MissingValueAnalysis(), new RangeDateAnalysis(), + new RangeNumberAnalysis(), new FrequencyAnalysis() + }; + private int numItems; + private int numFiltered; + private Interval range; + private JComponent controls; + + public SummaryView(TFModel model) + { + super(model); + analysisDisplay=HtmlDisplay.create(); + JScrollPane scrollPane = new JScrollPane(analysisDisplay); + setLayout(new GridLayout(1,1)); + add(scrollPane); + + controls=new HtmlControls("This report gives a <br> statistical breakdown<br> "+ + "of your data. <p> Reading the summary often helps<br> you find "+ + "data errors."); + } + + @Override + public JComponent _getControls() + { + return controls; + } + + void makeHtml() + { + Display d=getModel().getDisplay(); + ActDB db=getModel().getDB(); + ActList acts=getModel().getActs(); + StringBuffer page=new StringBuffer(); + page.append("<blockquote>"); + if (getModel().getDB()==null) + { + page.append("<h1><font color=#003399>No data loaded.</font></h1>"); + } + else + { + page.append("<BR><BR><BR>File: "+getModel().getDbFile()+"<br>"); + page.append("Source: "+getModel().getDB().getSource()+"<br><br>"); + page.append("Description: "+getModel().getDB().getDescription()+"<br><br>"); + page.append("<br><br>"); + page.append("<table border=0>"); + + + page.append("<tr><td valign=top align=left width=100><b>"); + page.append("<font size=+1>Data</font><br>"); + + page.append("</td><td align=top>"); + append(page, "Total events", ""+numItems); + + if (numItems>0) + { + append(page, "Total selected", ""+numFiltered); + if (numFiltered>0) + { + append(page, "Earliest", new Date(range.start).toString()); + append(page, "Latest", new Date(range.end).toString()); + } + } + page.append("<br></td></tr>"); + + page.append("<tr><td valign=top align=left width=100><b>"); + page.append("<font size=+1>Fields</font><br>"); + + page.append("</td><td align=top>"); + for (Field f: getModel().getDB().getFields()) + { + append(page, f.getName(),FieldFormatCatalog.humanName(f.getType())+fieldLabel(f)); + } + page.append("<br></td></tr>"); + + + page.append("</table>"); + + if (numFiltered>0) + { + page.append("<h1>Statistics (for "+acts.size()+" items)</h1>"); + for (Field field: db.getFields()) + { + page.append("<h2>"+field.getName()+"</h2>"); + page.append("<ul>"); + for (int i=0; i<fieldAnalyzers.length; i++) + { + FieldAnalysis fa=fieldAnalyzers[i]; + if (fa.canHandleType(field.getType())) + { + page.append("<li>"); + page.append("<b><font color=#808080>"+fa.getName()+"</font></b><br>"); + fa.perform(acts, field); + String[] s=fa.getResultDescription(); + for (int j=0; j<s.length; j++) + { + page.append(s[j]); + page.append("<br>"); + } + page.append("</li>"); + } + } + page.append("</ul>"); + } + } + } + page.append("</blockquote>"); + analysisDisplay.setText(page.toString()); + analysisDisplay.setCaretPosition(0); + } + + static void append(StringBuffer page, String label, String value) + { + page.append("<b><font color=#808080>"+label+"</font></b> "); + page.append(value); + page.append("<br>"); + } + + @Override + protected void onscreen(boolean majorChange) + { + _note(null); + } + + protected void _note(TFEvent e) { + recalculate(); + makeHtml(); + repaint(); + } + + String fieldLabel(Field f) + { + StringBuffer b=new StringBuffer("<b>"); + ActDB db=getModel().getDB(); + for (String v: VirtualField.list()) + { + if (db.getField(v)!=null && db.getField(v).getName().equals(f.getName())) + b.append(" (Shown in visualization as "+VirtualField.humanName(v)+")"); + } + b.append("</b>"); + return b.toString(); + } + + void recalculate() + { + ActList acts=getModel().getActs(); + if (acts==null) + { + numItems=0; + return; + } + numFiltered=acts.size(); + numItems=getModel().getDB().size(); + range=DBUtils.range(acts, VirtualField.START); + } + + @Override + public String getName() { + return "Summary"; + } +} diff --git a/timeflow/views/TableView.java b/timeflow/views/TableView.java new file mode 100755 index 0000000..4ce1ff1 --- /dev/null +++ b/timeflow/views/TableView.java @@ -0,0 +1,267 @@ +package timeflow.views; + +import timeflow.app.ui.HtmlDisplay; +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.format.field.DateTimeGuesser; +import timeflow.model.*; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.*; +import javax.swing.event.TableModelListener; +import javax.swing.table.*; +import java.util.*; + +public class TableView extends AbstractView { + + private JTable table=new JTable(); + private int colorColumn=-1, labelColumn=-1; + private Font font, bold; + private boolean editable=true; + private JPanel controls; + + public TableView(TFModel model) + { + super(model); + + JScrollPane scrollPane = new JScrollPane(table); + table.setFillsViewportHeight(true); + table.setAutoCreateRowSorter(true); + font=model.getDisplay().plain(); + bold=model.getDisplay().bold(); + table.setFont(font); + final int fh=getFontMetrics(font).getHeight(); + table.setRowHeight(fh+12); + table.setShowGrid(false); + table.getTableHeader().setPreferredSize(new Dimension(10, fh+15)); + table.getTableHeader().setFont(font); + setReorderable(false); + setLayout(new GridLayout(1,1)); + add(scrollPane); + + controls=new HtmlControls("Use the table view for<br> a rapid overview<br> "+ + "of your data. <p>You can click<br> on the headers to sort the columns,<br> "+ + "and you can edit data<br> directly in the table cells."); + } + + @Override + public JComponent _getControls() + { + return controls; + } + + public void setEditable(boolean editable) + { + this.editable=editable; + } + + public JTable getTable() + { + return table; + } + + @Override + public void onscreen(boolean majorChange) + { + _note(null); + } + + @Override + protected void _note(TFEvent e) { + setActs(getModel().getActs()); + } + + public void setActs(ActList acts) + { + TableModel t=new TimelineTableModel(acts); + table.setModel(t); + ActTableRenderer r=new ActTableRenderer(acts); + table.setDefaultRenderer(Object.class, r); + table.setDefaultRenderer(Double.class, r); + table.setDefaultEditor(String[].class, new StringArrayEditor()); + table.setDefaultEditor(RoughTime.class, new RoughTimeEditor()); + repaint(); + } + + @Override + public String getName() { + return "Table"; + } + + + class ActTableRenderer extends DefaultTableCellRenderer { + + ActList acts; + Color zebra=new Color(240,240,240); + boolean color=false; + Color dataColor; + + ActTableRenderer(ActList acts) + { + this.acts=acts; + } + + public void setValue(Object value) { + if (value==null) + { + super.setValue(""); + return; + } + setHorizontalAlignment(value instanceof Double ? SwingConstants.RIGHT : SwingConstants.LEFT); + super.setValue(getModel().getDisplay().toString(value)); + } + + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int rowIndex, int vColIndex) { + + if (vColIndex==labelColumn || vColIndex==colorColumn) + setFont(bold); + else + setFont(font); + setBackground(rowIndex%2==0 ? Color.white : zebra); + color=vColIndex==colorColumn || vColIndex==labelColumn; + Field colorField=null; + if (color) + { + colorField=getModel().getColorField(); + color &= colorField!=null; + } + if (color) + { + int actIndex=table.convertRowIndexToModel(rowIndex); + Act act=acts.get(actIndex); + + if (colorField==null || colorField.getType()!=String.class) + dataColor=getModel().getDisplay().getColor("timeline.unspecified.color"); + else + dataColor=getModel().getDisplay().makeColor(act.getString(colorField)); + setForeground(dataColor); + setValue(value); + } + else + { + setForeground(Color.black); + setValue(value); + } + return this; + } + } + + class TimelineTableModel implements TableModel + { + ActList acts; + Field[] fields; + + TimelineTableModel(ActList acts) + { + this.acts=acts; + ArrayList<Field> a=new ArrayList<Field>(); + int i=0; + Field colorField=getModel().getColorField(); + Field labelField=getModel().getDB().getField(VirtualField.LABEL); + for (Field f:acts.getDB().getFields()) + { + a.add(f); + if (f==colorField) + colorColumn=i; + if (f==labelField) + labelColumn=i; + i++; + } + fields=(Field[])a.toArray(new Field[0]); + } + + @Override + public void addTableModelListener(TableModelListener l) { + } + + @Override + public Class<?> getColumnClass(int columnIndex) { + return fields[columnIndex].getType(); + } + + @Override + public int getColumnCount() { + return fields.length; + } + + @Override + public String getColumnName(int columnIndex) { + return fields[columnIndex].getName(); + } + + @Override + public int getRowCount() { + return acts.size(); + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + return acts.get(rowIndex).get(fields[columnIndex]); + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return editable; + } + + @Override + public void removeTableModelListener(TableModelListener l) { + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + acts.get(rowIndex).set(fields[columnIndex], aValue); + getModel().noteRecordChange(TableView.this); + } + } + + public class StringArrayEditor extends AbstractCellEditor implements TableCellEditor + { + JComponent component = new JTextField(); + + public Component getTableCellEditorComponent(JTable table, Object value, + boolean isSelected, int rowIndex, int vColIndex) { + ((JTextField)component).setText(getModel().getDisplay().toString(value)); + return component; + } + + public Object getCellEditorValue() + { + String s= ((JTextField)component).getText(); + String[] tags=s.split(","); + for (int i=0; i<tags.length; i++) + tags[i]=tags[i].trim(); + return tags; + } + } + + public class RoughTimeEditor extends AbstractCellEditor implements TableCellEditor + { + JComponent component = new JTextField(); + DateTimeGuesser guesser=new DateTimeGuesser(); + + public Component getTableCellEditorComponent(JTable table, Object value, + boolean isSelected, int rowIndex, int vColIndex) { + + RoughTime r=(RoughTime)value; + JTextField t=((JTextField)component); + t.setText(r==null ? "" : r.format()); + return component; + } + + public Object getCellEditorValue() + { + String s= ((JTextField)component).getText(); + return s.trim().length()==0 ? null : guesser.guess(s); + } + } + + public void setReorderable(boolean allow) { + table.getTableHeader().setReorderingAllowed(allow); + } + +} diff --git a/timeflow/views/TimelineView.java b/timeflow/views/TimelineView.java new file mode 100755 index 0000000..778ece2 --- /dev/null +++ b/timeflow/views/TimelineView.java @@ -0,0 +1,465 @@ +package timeflow.views; + +import timeflow.app.ui.ComponentCluster; +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.model.*; +import timeflow.views.CalendarView.CalendarPanel; +import timeflow.views.CalendarView.ScrollingCalendar; +import timeflow.vis.*; +import timeflow.vis.timeline.*; + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; + +import javax.swing.*; + +import java.util.*; + +public class TimelineView extends AbstractView +{ + AxisRenderer grid; + TimelineRenderer timeline; + TimelineVisuals visuals; + TimelinePanel timelinePanel; + JButton fit; + ScrollingTimeline scroller; + JPanel controls; + JScrollBar bar; + + //JScrollPane scrollpane; + public JComponent _getControls() + { + return controls; + } + + public TimelineView(TFModel model) + { + super(model); + visuals = new TimelineVisuals(model); + grid = new AxisRenderer(visuals); + timeline = new TimelineRenderer(visuals); + + timelinePanel = new TimelinePanel(model); + scroller = new ScrollingTimeline(); + setLayout(new BorderLayout()); + add(scroller, BorderLayout.CENTER); + + JPanel bottom = new JPanel(); + bottom.setLayout(new BorderLayout()); + add(bottom, BorderLayout.SOUTH); + + TimelineSlider slider = new TimelineSlider(visuals, 24 * 60 * 60 * 1000L, new Runnable() + { + + @Override + public void run() + { + redraw(); + } + }); + bottom.add(slider, BorderLayout.CENTER); + + controls = new JPanel(); + controls.setBackground(Color.white); + controls.setLayout(new BorderLayout());//new GridLayout(2,1)); + + // top part of grid: zoom buttons. + ComponentCluster buttons = new ComponentCluster("Zoom"); + ImageIcon zoomOutIcon = new ImageIcon("images/zoom_out.gif"); + JButton zoomOut = new JButton(zoomOutIcon); + buttons.addContent(zoomOut); + zoomOut.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + Interval zoom = visuals.getViewInterval().subinterval(-1, 2).intersection(visuals.getGlobalInterval()); + moveTime(zoom); + } + }); + + ImageIcon zoomOut100Icon = new ImageIcon("images/zoom_out_100.gif"); + JButton zoomOutAll = new JButton(zoomOut100Icon); + buttons.addContent(zoomOutAll); + zoomOutAll.addActionListener(new ActionListener() + { + + @Override + public void actionPerformed(ActionEvent e) + { + moveTime(visuals.getGlobalInterval()); + } + }); + + /* + // UI for zooming to precisely fit the visible selection. + // No one seemed to think this was so important, but we may want it again some day. + // if you uncomment this, then also uncomment the line in reset(). + ImageIcon zoomSelection=new ImageIcon("images/zoom_selection.gif"); + fit=new JButton(zoomSelection); + fit.setBackground(Color.white); + buttons.addContent(fit); + fit.setEnabled(false); + fit.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + moveTime(visuals.getFitToVisibleRange()); + }}); + */ + controls.add(buttons, BorderLayout.NORTH); + + // ok, now do second part of grid: layout style buttons. + ComponentCluster layoutPanel = new ComponentCluster("Layout"); + + ButtonGroup layoutGroup = new ButtonGroup(); + + ImageIcon diagonalIcon = new ImageIcon("images/layout_diagonal.gif"); + JRadioButton diagonal = new JRadioButton(diagonalIcon, true); + diagonal.setSelectedIcon(new ImageIcon("images/layout_diagonal_selected.gif")); + layoutPanel.addContent(diagonal); + diagonal.addActionListener(new LayoutSetter(TimelineVisuals.Layout.TIGHT)); + layoutGroup.add(diagonal); + + ImageIcon looseIcon = new ImageIcon("images/layout_loose.gif"); + JRadioButton loose = new JRadioButton(looseIcon, false); + loose.setSelectedIcon(new ImageIcon("images/layout_loose_selected.gif")); + layoutPanel.addContent(loose); + loose.addActionListener(new LayoutSetter(TimelineVisuals.Layout.LOOSE)); + layoutGroup.add(loose); + + ImageIcon graphIcon = new ImageIcon("images/layout_graph.gif"); + JRadioButton graph = new JRadioButton(graphIcon, false); + graph.setSelectedIcon(new ImageIcon("images/layout_graph_selected.gif")); + layoutPanel.addContent(graph); + graph.addActionListener(new LayoutSetter(TimelineVisuals.Layout.GRAPH)); + layoutGroup.add(graph); + + controls.add(layoutPanel, BorderLayout.CENTER); + } + + class LayoutSetter implements ActionListener + { + TimelineVisuals.Layout layout; + + LayoutSetter(TimelineVisuals.Layout layout) + { + this.layout = layout; + } + + @Override + public void actionPerformed(ActionEvent e) + { + visuals.setLayoutStyle(layout); + redraw(); + } + } + + void moveTime(Interval interval) + { + new TimeAnimator(interval).start(); + } + + void redraw() + { + visuals.layout(); + timelinePanel.drawVisualization(); + repaint(); + } + + @Override + protected void onscreen(boolean majorChange) + { + visuals.init(majorChange); + reset(majorChange); + redraw(); + scroller.calibrate(); + } + + @Override + protected void _note(TFEvent e) + { + visuals.note(e); + reset(e.affectsRowSet()); + } + + void reset(boolean forceViewChange) + { + if (forceViewChange || getModel().getViewInterval() == null) + { + int numSelected = getModel().getActs().size(); + int numVisible = DBUtils.count(getModel().getActs(), visuals.getViewInterval(), + getModel().getDB().getField(VirtualField.START)); + if (numVisible < 10 && numSelected > numVisible) + { + moveTime(visuals.getFitToVisibleRange()); + } + } + // uncomment this if we are using the fit button again. + //fit.setEnabled(getModel().getActs().size()<getModel().getDB().size()); + redraw(); + scroller.calibrate(); + } + + class TimeAnimator extends Thread + { + Interval i1, i2; + + TimeAnimator(Interval i2) + { + this.i1 = visuals.getViewInterval(); + this.i2 = i2; + } + + TimeAnimator(Interval i1, Interval i2) + { + this.i1 = i1; + this.i2 = i2; + } + + public void run() + { + int n = 15; + for (int i = 0; i < n; i++) + { + long start = ((n - i) * i1.start + i * i2.start) / n; + long end = ((n - i) * i1.end + i * i2.end) / n; + try + { + visuals.setTimeBounds(start, end); + redraw(); + + sleep(20); + } catch (Exception e) + { + } + } + } + } + + class ScrollingTimeline extends JPanel + { + //JScrollBar bar; + + public ScrollingTimeline() + { + setLayout(new BorderLayout()); + //scrollpane = new JScrollPane(timelinePanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + //add(scrollpane, BorderLayout.CENTER); + add(timelinePanel, BorderLayout.CENTER); + bar = new JScrollBar(JScrollBar.VERTICAL); + add(bar, BorderLayout.EAST); + //bar = scrollpane.getVerticalScrollBar(); + bar.addAdjustmentListener(new AdjustmentListener() + { + + @Override + public void adjustmentValueChanged(AdjustmentEvent e) + { + timeline.setDY(bar.getValue()); + timelinePanel.drawVisualization(); + //timelinePanel.repaint(); + } + }); + } + + public void setBounds(int x, int y, int w, int h) + { + super.setBounds(x, y, w, h); + calibrate(); + } + + void calibrate() + { +// timelinePanel.setBorder(BorderFactory.createLineBorder(Color.red, 2)); + //javax.swing.border.Border b = scrollpane.getViewportBorder(); + //scrollpane.setViewportBorder(BorderFactory.createLineBorder(Color.red, 2)); +// scrollpane.getViewport().setBounds(0,0,100,100); //.setViewSize(new Dimension(100, visuals.getFullHeight())); +// if (true) // visuals==null) +// return; + final int height = getSize().height * 90 / 100; // room for bottom + final int desired = Math.max(height, visuals.getFullHeight()); +// bar.setVisible(desired > height); +// bar.setMinimum(0); // is this double setting necessary? +// bar.setMaximum(desired); // more testing is needed, it solved problems +// bar.setVisibleAmount(height); // on certain Macs are one point. + SwingUtilities.invokeLater(new Runnable() + { + + @Override + public void run() + { + bar.setMinimum(0); + bar.setMaximum(desired); + bar.setVisibleAmount(height); + } + }); + } + } + + class TimelinePanel extends AbstractVisualizationView + { + public TimelinePanel(TFModel model) + { + super(model); + + addMouseListener(new MouseAdapter() + { + @Override + public void mouseClicked(MouseEvent e) + { + if (e.getClickCount() == 2) + { + moveTime(visuals.getViewInterval().subinterval(.333, .667)); + } + } + + @Override + public void mouseExited(MouseEvent e) + { + mouse.setLocation(new Point(-1, -1)); + repaint(); + } + + @Override + public void mousePressed(MouseEvent e) + { + // was this a right-click or ctrl-click? ignore. + if (e.isPopupTrigger()) + { + return; + } + // did we click on a date label? + for (Mouseover o : objectLocations) + { + if (o.contains(e.getX(), e.getY()) && o.thing instanceof Interval) + { + moveTime((Interval) o.thing); + return; + } + } + // if not, prepare + firstMouse.setLocation(e.getX(), e.getY()); + mouseIsDown = true; + repaint(); + } + + @Override + public void mouseReleased(MouseEvent event) + { + if (!mouseIsDown) // this means we had clicked on a date label. + { + return; + } + + mouseIsDown = false; + int a = firstMouse.x; + int b = mouse.x; + + long start = visuals.getTimeScale().toTime(a); + long end = visuals.getTimeScale().toTime(b); + if (b - a < 0) + { + Interval total = visuals.getGlobalInterval(); + + long t = start; + start = end; + end = t; + + // reversed + long S = visuals.getTimeScale().getInterval().start; + long E = visuals.getTimeScale().getInterval().end; + + // start*M + B = S + // end*M + B = E + double M = (double)(S - E) / (start - end); + long B = E - (long)(end * M); + start = (long)(S*M) + B; + end = (long)(E*M) + B; + if (start < total.start) + { + start = total.start; + } + if (end > total.end) + { + end = total.end; + } + } + + moveTime(new Interval(start, end)); + } + }); + + addMouseWheelListener(new MouseAdapter() + { + @Override + public void mouseWheelMoved(MouseWheelEvent e) + { + //bar.setValue(bar.getValue() + e.getWheelRotation() * 10); + visuals.scale *= Math.exp(e.getWheelRotation()/10.0); + visuals.scale = visuals.layout(); + //System.out.println(visuals.scale); + timelinePanel.drawVisualization(); + scroller.calibrate(); + repaint(); + } + }); + } + + public RoughTime getTime(Point p) + { + TimeScale scale = visuals.getTimeScale(); + long timestamp = scale.toTime(p.x); + return new RoughTime(timestamp, TimeUnit.DAY); + } + + protected void drawVisualization(Graphics2D g) + { + if (g == null) + { + return; + } + + g.setColor(Color.white); + g.fillRect(0, 0, 2 * getSize().width, getSize().height); + visuals.setBounds(0, 0, getSize().width, getSize().height); + objectLocations = new ArrayList<Mouseover>(); + timeline.render(g, objectLocations); + grid.render(g, objectLocations); + } + + protected boolean paintOnTop(Graphics2D g, int w, int h) + { + if (!mouseIsDown) + { + return false; + } + + int a = mouse.x; + int b = firstMouse.x; + if (a > b) + { + g.setColor(new Color(255, 255, 120, 100)); + g.fillRect(b, 0, a - b, h); + } + else + { + g.setColor(new Color(255, 120, 255, 100)); + g.fillRect(a, 0, b - a, h); + } + + g.setColor(Color.blue); + g.drawLine(a, 0, a, h); + g.drawLine(b, 0, b, h); + return true; + } + } + + @Override + public String getName() + { + return "Timeline"; + } +} diff --git a/timeflow/vis/GroupVisualAct.java b/timeflow/vis/GroupVisualAct.java new file mode 100755 index 0000000..ce2d71d --- /dev/null +++ b/timeflow/vis/GroupVisualAct.java @@ -0,0 +1,98 @@ +package timeflow.vis; + +import java.awt.*; +import java.util.*; + +import timeflow.data.db.*; +import timeflow.model.Display; +import timeflow.model.VirtualField; +import timeflow.util.*; + +public class GroupVisualAct extends VisualAct +{ + private ArrayList<Act> group=new ArrayList<Act>(); + private boolean mixed=false; + private DoubleBag<Color> colorBag; + int numActs=0; + double total=0; + + public GroupVisualAct(java.util.List<VisualAct> vacts, boolean mixed, Rectangle bounds) + { + super(vacts.get(0).act); + int n=vacts.size(); + + VisualAct proto=vacts.get(0); + + this.color=proto.color; + this.trackString=proto.trackString; + this.visible=proto.visible; + this.x=proto.x; + this.y=proto.y; + + this.spaceToRight=proto.spaceToRight; + this.start=proto.start; + this.group=new ArrayList<Act>(); + this.label="Group of "+n+" events"; + this.mouseOver=this.label; + this.colorBag=new DoubleBag<Color>(); + Field sizeField=act.getDB().getField(VirtualField.SIZE); + for (VisualAct v: vacts) + { + numActs++; + if(sizeField!=null) + total+=v.act.getValue(sizeField); + this.size+=v.size; + this.colorBag.add(v.color, v.size); + } + this.size=Math.sqrt(this.size); + this.mixed=mixed; + } + + public int getNumActs() + { + return numActs; + } + + public void add(Act secondAct) + { + if (group==null) + { + group=new ArrayList<Act>(); + if (act!=null) + group.add(act); + } + group.add(secondAct); + } + + public void draw(Graphics2D g, int ox, int oy, int r, Rectangle maxFill, boolean showDuration) + { + if (!mixed) + { + g.setColor(color); + g.fillOval(ox,oy-r,2*r,2*r); + g.drawOval(ox-2,oy-r-2,2*r+3,2*r+3); + } + else + { + java.util.List<Color> colors=colorBag.listTop(8, true); + double total=0; + for (Color c: colors) + total+=colorBag.num(c); + + // now draw pie chart thing. + double angle=0; + int pieCenterX=ox+r; + int pieCenterY=oy; + for (Color c: colors) + { + double num=colorBag.num(c); + double sa=(360*angle)/total; + int startAngle=(int)(sa); + int arcAngle=(int)(((360*(angle+num)))/total-sa); + g.setColor(c); + g.fillArc(pieCenterX-r,pieCenterY-r,2*r,2*r,startAngle,arcAngle); + angle+=num; + } + } + } +} diff --git a/timeflow/vis/Mouseover.java b/timeflow/vis/Mouseover.java new file mode 100755 index 0000000..adf3009 --- /dev/null +++ b/timeflow/vis/Mouseover.java @@ -0,0 +1,94 @@ +package timeflow.vis; + +import timeflow.model.*; + +import java.awt.*; +import java.util.ArrayList; + +public class Mouseover extends Rectangle { + public Object thing; + public Mouseover(Object thing, int x, int y, int w, int h) + { + super(x,y,w,h); + this.thing=thing; + } + + public void draw(Graphics2D g, int maxW, int maxH, Display display) + { + g.setColor(new Color(0,53,153)); + g.setColor(new Color(255,255,0,100)); + g.fill(this); + } + + protected void draw(Graphics2D g, int maxW, int maxH, Display display, ArrayList labels, int numLines) + { + if (labels==null || labels.size()==0) + return; + + // draw a background box. + + // find max number of chars, very very roughly! + int boxW=50; + for (int i=0; i<labels.size(); i+=2) + { + if (labels.get(i) instanceof String[]) + boxW=300; + else if (labels.get(i) instanceof String) + { + boxW=Math.max(boxW, 50+50*((String)labels.get(i)).length()); + } + } + + + boxW=Math.min(350, boxW); + int boxH=18*numLines+10; + int mx=this.x+this.width+5; + int my=this.y+this.height+35; + + // put box in a place where it does not obscure the data + // or go off screen. + if (my+boxH>maxH-10) + { + my=Math.max(10,this.y-boxH-5); + } + if (mx+boxW>maxW-10) + { + mx=Math.max(10,this.x-boxW-10); + } + int ty=my; + g.setColor(new Color(0,0,0,70)); + g.fillRoundRect(mx-11, my-16, boxW, boxH,12,12); + g.setColor(Color.white); + g.fillRoundRect(mx-15, my-20, boxW, boxH,12,12); + g.setColor(Color.darkGray); + g.drawRoundRect(mx-15, my-20, boxW, boxH,12,12); + + // finally, draw the darn labels. + for (int i=0; i<labels.size(); i+=2) + { + g.setFont(display.bold()); + String field=(String)labels.get(i); + g.drawString(field,mx,ty); + int sw=display.boldFontMetrics().stringWidth(field); + g.setFont(display.plain()); + Object o=labels.get(i+1); + if (o instanceof String) + { + g.drawString((String)o,mx+sw+9,ty); + ty+=18; + } + else + { + ArrayList<String> lines=(ArrayList<String>)o; + int dx=sw+9; + for (String line: lines) + { + g.drawString((String)line,mx+dx,ty); + ty+=18; + dx=0; + } + ty+=5; + } + } + } +} diff --git a/timeflow/vis/MouseoverLabel.java b/timeflow/vis/MouseoverLabel.java new file mode 100755 index 0000000..5ffa41b --- /dev/null +++ b/timeflow/vis/MouseoverLabel.java @@ -0,0 +1,32 @@ +package timeflow.vis; + +import java.awt.Graphics2D; +import java.util.ArrayList; + +import timeflow.data.db.Act; +import timeflow.data.db.ActDB; +import timeflow.data.db.Field; +import timeflow.model.Display; + +public class MouseoverLabel extends Mouseover { + + String label1, label2; + + public MouseoverLabel(String label1, String label2, int x, int y, int w, int h) { + super(label1, x, y, w, h); + this.label1=label1; + this.label2=label2; + } + + + public void draw(Graphics2D g, int maxW, int maxH, Display display) + { + super.draw(g, maxW, maxH, display); + ArrayList labels=new ArrayList(); + labels.add(label1); + labels.add(label2); + int numLines=1; + draw(g, maxW, maxH, display, labels, numLines); + } +} + diff --git a/timeflow/vis/TagVisualAct.java b/timeflow/vis/TagVisualAct.java new file mode 100755 index 0000000..a93cab1 --- /dev/null +++ b/timeflow/vis/TagVisualAct.java @@ -0,0 +1,52 @@ +package timeflow.vis; + +import timeflow.data.db.*; + +import java.awt.*; +import java.util.*; + +public class TagVisualAct extends VisualAct { + + Color[] colors; + private static Color[] nullColors={new Color(230,230,230)}; + + public TagVisualAct(Act act) { + super(act); + } + + public void setColors(Color[] colors) + { + this.colors=colors; + this.color=colors.length>0 ? colors[0] : Color.gray; + } + + public void draw(Graphics2D g, int ox, int oy, int r, Rectangle maxFill, boolean showDuration) + { + if (colors==null) + { + super.draw(g, ox, oy, r, maxFill, showDuration); + return; + } + + Color[] c= colors==null || colors.length==0 ? nullColors : colors; + int tx=ox-r; + int side=2*r; + for (int i=0; i<c.length; i++) + { + g.setColor(c[i]); + int y0=-5+(oy-r)+(2*side*i)/c.length; + int y1=-5+(oy-r)+(2*side*(i+1))/c.length; + if (size>=0) + g.fillRect(tx,y0,side+2,y1-y0); + else + g.drawRect(tx,y0,side+2,y1-y0); + } + + if (end!=null && showDuration) + { + int lineY=y+6; + g.fillRect(getX(), lineY, getEndX()-getX(), 2); + g.drawLine(getX(), lineY, getX(), lineY-4); + } + } +} diff --git a/timeflow/vis/TimeScale.java b/timeflow/vis/TimeScale.java new file mode 100755 index 0000000..cad23cd --- /dev/null +++ b/timeflow/vis/TimeScale.java @@ -0,0 +1,95 @@ +package timeflow.vis; + +import timeflow.data.time.*; + +import java.util.*; + +public class TimeScale +{ + private double low, high; + private Interval interval; + + public TimeScale() + { + low = 0; + high = 100; + interval = new Interval(new Date(0).getTime(), new Date().getTime()); + } + + public Interval getInterval() + { + return interval; + } + + public void setNumberRange(double low, double high) + { + this.low = low; + this.high = high; + } + + public void setDateRange(Interval t) + { + setDateRange(t.start, t.end); + } + + public void setDateRange(long first, long last) + { + interval.setTo(first, last); + } + + public boolean containsDate(long date) + { + return interval.contains(date); + } + + public boolean containsNum(double x) + { + return x >= low && x <= high; + } + + public long duration() + { + return interval.length(); + } + + public double toNum(long time) + { + return low + (high - low) * (time - interval.start) / (double) duration(); + } + + public long spaceToTime(double space) + { + return (long) (space * duration() / (high - low)); + } + + public int toInt(long time) + { + return (int) toNum(time); + } + + public long toTime(double num) + { + double millis = interval.start + duration() * (num - low) / (high - low); + return (long) millis; + } + + public double getLow() + { + return low; + } + + public void setLow(double low) + { + this.low = low; + } + + public double getHigh() + { + return high; + } + + public void setHigh(double high) + { + this.high = high; + } +} diff --git a/timeflow/vis/VisualAct.java b/timeflow/vis/VisualAct.java new file mode 100755 index 0000000..4c3569b --- /dev/null +++ b/timeflow/vis/VisualAct.java @@ -0,0 +1,314 @@ +package timeflow.vis; + +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.model.*; +import timeflow.vis.timeline.TimelineTrack; + +import java.awt.*; +import java.util.*; + +import timeflow.util.*; + +public class VisualAct implements Comparable +{ + + Color color; + String label; + String mouseOver; + double size = 1; + String trackString; + TimelineTrack track; + boolean visible; + Act act; + int x, y; + int spaceToRight; + RoughTime start, end; + int endX; + + public VisualAct(Act act) + { + this.act = act; + } + + public int getX() + { + return x; + } + + public int getDotR() + { + return Math.max(1, (int) Math.abs(size)); + } + + public void setX(int x) + { + this.x = x; + } + + public int getY() + { + return y; + } + + public void setY(int y) + { + this.y = y; + } + + public void draw(Graphics2D g, int ox, int oy, int r, Rectangle maxFill, boolean showDuration) + { + g.setColor(getColor()); + if (size >= 0) + { + // Slow! + g.fillOval(ox, y - r, 2 * r, 2 * r); + } else + { + // Slow! + g.drawOval(ox, y - r, 2 * r, 2 * r); + } + if (end != null && showDuration) + { + int lineY = y + 6; + g.fillRect(getX(), lineY, getEndX() - getX(), 2); + g.drawLine(getX(), lineY, getX(), lineY - 4); + } + + } + + public Mouseover draw(Graphics2D g, Rectangle maxFill, Rectangle bounds, + Display display, boolean text, boolean showDuration) + { + if (!isVisible()) + { + return null; + } + + if (x > bounds.x + bounds.width && (end == null || endX > bounds.x + bounds.width) + || x < bounds.x - 200 && (end == null || endX < bounds.x - 200)) + { + return null; + } + + g.setFont(display.plain()); + + int r = getDotR(); + if (r <= 0) + { + r = 1; + } + if (r > 30) + { + r = 30; + } + int ox = text ? x - 2 * r : x; + + draw(g, ox, y - 2, r, maxFill, showDuration); + + + if (!text) + { + return new VisualActMouseover(ox - 2, y - r - 4, 4 + 2 * r, 4 + 2 * r); + } + + int labelSpace = getSpaceToRight() - 12; + int sw = 0; + if (labelSpace > 50) + { + String s = display.format(getLabel(), labelSpace / 8, true); + int n = s.indexOf(' '); + int tx = x + 5; + int ty = y + 4; + if (n < 1) + { + g.drawString(s, tx, ty); + } else + { + String first = s.substring(0, n); + g.drawString(first, tx, ty); + Color c = ColorUtils.interpolate(g.getColor(), Color.white, .33); + g.setColor(c); + g.drawString(s.substring(n), tx + display.plainFontMetrics().stringWidth(first), ty); + } + sw = display.plainFontMetrics().stringWidth(s); + } + + return new VisualActMouseover(x - 3 - 2 * r, y - r - 8, 14 + sw, r + 13 + 2 * r); + } + + public Act getAct() + { + return act; + } + + public boolean isVisible() + { + return visible; + } + + public void setVisible(boolean visible) + { + this.visible = visible; + } + + public RoughTime getStart() + { + return start; + } + + public void setStart(RoughTime start) + { + this.start = start; + } + + public Color getColor() + { + return color; + } + + public void setColor(Color color) + { + this.color = color; + } + + public String getLabel() + { + return label; + } + + public void setLabel(String label) + { + this.label = label; + } + + public String getMouseOver() + { + return mouseOver; + } + + public void setMouseOver(String mouseOver) + { + this.mouseOver = mouseOver; + } + + public double getSize() + { + return size; + } + + public void setSize(double size) + { + this.size = size; + } + + public String getTrackString() + { + return trackString == null ? "" : trackString; + } + + public void setTrackString(String track) + { + this.trackString = track; + } + + public TimelineTrack getTrack() + { + return track; + } + + public void setTrack(TimelineTrack track) + { + this.track = track; + } + + public void setSpaceToRight(int spaceToRight) + { + this.spaceToRight = spaceToRight; + } + + public int getSpaceToRight() + { + return spaceToRight; + } + + public int getEndX() + { + return endX; + } + + public void setEndX(int endX) + { + this.endX = endX; + } + + public RoughTime getEnd() + { + return end; + } + + public void setEnd(RoughTime end) + { + this.end = end; + } + + @Override + public int compareTo(Object o) + { + return RoughTime.compare(start, ((VisualAct) o).start); + //start.compareTo(((VisualAct)o).start); + } + + class VisualActMouseover extends Mouseover + { + + public VisualActMouseover(int x, int y, int w, int h) + { + super(VisualAct.this, x, y, w, h); + } + + public void draw(Graphics2D g, int maxW, int maxH, Display display) + { + super.draw(g, maxW, maxH, display); + Act a = getAct(); + ActDB db = a.getDB(); + java.util.List<Field> fields = db.getFields(); + ArrayList labels = new ArrayList(); + int charWidth = 40; + int numLines = 1; + if (VisualAct.this instanceof GroupVisualAct) + { + GroupVisualAct gv = (GroupVisualAct) VisualAct.this; + labels.add(gv.getNumActs() + ""); + labels.add("items"); + Field sizeField = db.getField(VirtualField.SIZE); + if (sizeField != null) + { + labels.add("Total " + sizeField.getName()); + double t = ((GroupVisualAct) (VisualAct.this)).total; + labels.add(Display.format(t)); + numLines++; + } + } else + { + for (Field f : fields) + { + labels.add(f.getName()); + Object val = a.get(f); + String valString = display.toString(val); + if (f.getName().length() + valString.length() + 2 > charWidth) + { + ArrayList<String> lines = Display.breakLines(valString, charWidth, 2 + f.getName().length()); + labels.add(lines); + numLines += lines.size() + 1; + } else + { + labels.add(valString); + numLines++; + } + } + } + draw(g, maxW, maxH, display, labels, numLines); + } + } +} diff --git a/timeflow/vis/VisualActFactory.java b/timeflow/vis/VisualActFactory.java new file mode 100755 index 0000000..bd267b6 --- /dev/null +++ b/timeflow/vis/VisualActFactory.java @@ -0,0 +1,119 @@ +package timeflow.vis; + +import java.awt.Color; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; + +import timeflow.data.db.Act; +import timeflow.data.db.ActDB; +import timeflow.data.db.ActList; +import timeflow.data.db.Field; +import timeflow.model.TFModel; +import timeflow.model.VirtualField; + +public class VisualActFactory +{ + // create one VisualAct per Act + + private static java.util.List<VisualAct> create(ActList acts) + { + java.util.List<VisualAct> list = new ArrayList<VisualAct>(); + for (Act a : acts) + { + VisualAct v = new TagVisualAct(a); + list.add(v); + } + return list; + } + + // create one VisualAct per Act/tag combo. + public static java.util.List<VisualAct> create(ActList acts, Field tagField, boolean multipleColors) + { + if (tagField == null || tagField.getType() == String.class) + { + return create(acts); + } + java.util.List<VisualAct> list = new ArrayList<VisualAct>(); + for (Act a : acts) + { + String[] tags = a.getTextList(tagField); + if (tags == null || tags.length < 2) + { + VisualAct v = new TagVisualAct(a); + if (tags != null && tags.length == 1) + { + v.setTrackString(tags[0]); + } + list.add(v); + } else + { + for (String tag : tags) + { + VisualAct v = multipleColors ? new TagVisualAct(a) : new VisualAct(a); + v.setTrackString(tag); + list.add(v); + } + } + } + return list; + } + + public static Collection<VisualAct> makeEmFit(TFModel model, ArrayList<VisualAct> vacts, Rectangle bounds) + { + // Does everything fit? Because, if so, we're already good to go. + int area = bounds.width * bounds.height; + int room = area / 200; + if (vacts.size() <= room) + { + return vacts; + } + + ArrayList<VisualAct> results = new ArrayList<VisualAct>(); + + // OK. If: + // * there's room for more than one item, and + // * there's more than one color in use, + // + // Then let's see how many colors there are. Maybe we can do one bubble per color. + ActDB db = model.getDB(); + if (room > 1 && (db.getField(VirtualField.COLOR) != null || db.getField(VirtualField.TRACK) != null)) + { + HashMap<Color, ArrayList<VisualAct>> colorGroupings = new HashMap<Color, ArrayList<VisualAct>>(); + for (VisualAct v : vacts) + { + Color c = v.color; + ArrayList<VisualAct> grouping = colorGroupings.get(c); + if (grouping == null) + { + grouping = new ArrayList<VisualAct>(); + colorGroupings.put(c, grouping); + } + grouping.add(v); + } + + if (colorGroupings.size() <= room) // Great! The colors fit. We now return one group VisualAct per color. + { + for (Color c : colorGroupings.keySet()) + { + ArrayList<VisualAct> grouping = colorGroupings.get(c); + if (grouping.size() == 1) + { + results.add(grouping.get(0)); + } else if (grouping.size() > 1) + { + results.add(new GroupVisualAct(grouping, false, bounds)); + } + } + return results; + } + } + + // OK, too bad, even that doesn't fit. We will just create one fat VisualAct + // that descibes the aggregate. C'est la vie! + + results.add(new GroupVisualAct(vacts, true, bounds)); + return results; + } +} diff --git a/timeflow/vis/VisualEncoder.java b/timeflow/vis/VisualEncoder.java new file mode 100755 index 0000000..9deee56 --- /dev/null +++ b/timeflow/vis/VisualEncoder.java @@ -0,0 +1,138 @@ +package timeflow.vis; + +import timeflow.data.db.*; +import timeflow.model.*; + +import java.awt.Color; +import java.util.*; + +public class VisualEncoder +{ + + private TFModel model; + private java.util.List<VisualAct> visualActs = new ArrayList<VisualAct>(); + private double maxSize = 0; + + public VisualEncoder(TFModel model) + { + this.model = model; + } + + public java.util.List<VisualAct> getVisualActs() + { + return visualActs; + } + + public void createVisualActs() + { + Field colorField = model.getDB().getField(VirtualField.COLOR); + Field trackField = model.getDB().getField(VirtualField.TRACK); + boolean multipleColors = colorField != null && colorField.getType() == String[].class && colorField != trackField; + visualActs = VisualActFactory.create(model.getDB().all(), trackField, multipleColors); + Collections.sort(visualActs); + } + + public List<VisualAct> apply() + { + + ActList visibleActs = model.getActs(); + Field start = model.getDB().getField(VirtualField.START); + Field end = model.getDB().getField(VirtualField.END); + Field size = model.getDB().getField(VirtualField.SIZE); + if (size != null) + { + double[] minmax = DBUtils.minmax(model.getActs(), size); + maxSize = Math.max(Math.abs(minmax[0]), Math.abs(minmax[1])); + } + + // apply color, label, visibility, etc. + for (VisualAct v : visualActs) + { + Act a = v.getAct(); + v.setStart(start == null ? null : a.getTime(start)); + v.setEnd(end == null ? null : a.getTime(end)); + v.setVisible(visibleActs.contains(a) && v.getStart() != null && v.getStart().isDefined()); + apply(v); + } + + return visualActs; + } + + public void apply(VisualAct v) + { + Display display = model.getDisplay(); + ActDB db = model.getDB(); + Act a = v.getAct(); + Field label = db.getField(VirtualField.LABEL); + Field track = db.getField(VirtualField.TRACK); + Field color = db.getField(VirtualField.COLOR); + Field size = db.getField(VirtualField.SIZE); + + if (label == null) + { + v.setLabel(""); + } else + { + v.setLabel(a.getString(label)); + } + + double s = Display.MAX_DOT_SIZE; + if (size == null || maxSize == 0) + { + v.setSize(s / 3); + } else + { + double z = s * Math.sqrt(Math.abs(a.getValue(size)) / maxSize); + if (a.getValue(size) < 0) + { + z = -z; + } + v.setSize(z); + } + + // For setting the track: + // This is a little complicated, but if the track is set to + // tags (that is, track.getType()==String[].class) then + // the track string was set earlier so it doesn't need to be set now. + if (track == null) + { + v.setTrackString(""); + } else if (track.getType() == String.class) + { + v.setTrackString(a.getString(track)); + } + + if (color == null || color == track) + { + if (track == null) + { + v.setColor(display.getColor("timeline.unspecified.color")); + } else + { + v.setColor(display.makeColor(v.getTrackString())); + } + } else + { + if (color.getType() == String[].class) + { + String[] tags = a.getTextList(color); + if (tags == null || tags.length == 0) + { + ((TagVisualAct) v).setColors(new Color[0]); + } else + { + int n = tags.length; + Color[] c = new Color[n]; + for (int i = 0; i < n; i++) + { + c[i] = display.makeColor(tags[i]); + } + ((TagVisualAct) v).setColors(c); + } + } else + { + v.setColor(display.makeColor(a.getString(color))); + } + } + } +} \ No newline at end of file diff --git a/timeflow/vis/calendar/CalendarVisuals.java b/timeflow/vis/calendar/CalendarVisuals.java new file mode 100755 index 0000000..d37f51c --- /dev/null +++ b/timeflow/vis/calendar/CalendarVisuals.java @@ -0,0 +1,113 @@ +package timeflow.vis.calendar; + +import java.awt.*; +import java.util.Collection; + + +import timeflow.model.*; +import timeflow.vis.*; +import timeflow.data.time.*; +import timeflow.data.db.*; + +public class CalendarVisuals { + VisualEncoder encoder; + TFModel model; + Rectangle bounds=new Rectangle(); + public Grid grid; + + public enum Layout {DAY, WEEK, MONTH, YEAR}; + private Layout layoutStyle=Layout.DAY; + + public enum DrawStyle {LABEL, ICON}; + DrawStyle drawStyle=DrawStyle.ICON; + + public enum FitStyle {LOOSE, TIGHT}; + FitStyle fitStyle=FitStyle.LOOSE; + + public CalendarVisuals(TFModel model) + { + this.model=model; + encoder=new VisualEncoder(model); + } + + public void render(Graphics2D g, Collection<Mouseover> objectLocations) + { + grid.render(g, model.getDisplay(), bounds, this, objectLocations); + } + + public Interval getGlobalInterval() + { + return DBUtils.range(model.getDB().all(), VirtualField.START); + } + + public Rectangle getBounds() { + return bounds; + } + + public void setBounds(int x, int y, int w, int h) { + bounds.setBounds(x,y,w,h); + if (grid==null) + makeGrid(true); + } + + public void setDrawStyle(DrawStyle drawStyle) + { + this.drawStyle=drawStyle; + } + + public void setLayoutStyle(Layout layoutStyle) + { + this.layoutStyle=layoutStyle; + makeGrid(true); + } + + public void setFitStyle(FitStyle fitStyle) + { + this.fitStyle=fitStyle; + makeGrid(true); + } + + public void makeGrid(boolean fresh) + { + Interval interval=getGlobalInterval(); + int dy=0; + if (grid!=null) + dy=grid.dy; + switch(layoutStyle) + { + case DAY: grid=new Grid(TimeUnit.WEEK, TimeUnit.DAY_OF_WEEK, interval); break; + case WEEK: grid=new Grid(TimeUnit.multipleWeeks(8), TimeUnit.WEEK, interval); break; + case MONTH: grid=new Grid(TimeUnit.YEAR, TimeUnit.MONTH, interval); break; + case YEAR: grid=new Grid(TimeUnit.DECADE, TimeUnit.YEAR, interval); + } + grid.makeCells(encoder.getVisualActs()); + if (!fresh) + grid.dy=dy; + } + + public void init() + { + note(new TFEvent(TFEvent.Type.DATABASE_CHANGE,null)); + } + + public void initAllButGrid() + { + encoder.createVisualActs(); + encoder.apply(); + } + + public void note(TFEvent e) { + if (e.affectsRowSet()) + { + encoder.createVisualActs(); + } + + updateVisuals(e.affectsRowSet()); + } + + public void updateVisuals(boolean fresh) + { + encoder.apply(); + makeGrid(fresh); + } +} diff --git a/timeflow/vis/calendar/Grid.java b/timeflow/vis/calendar/Grid.java new file mode 100755 index 0000000..0dd2881 --- /dev/null +++ b/timeflow/vis/calendar/Grid.java @@ -0,0 +1,375 @@ +package timeflow.vis.calendar; + +import timeflow.data.time.*; +import timeflow.model.Display; +import timeflow.vis.*; + +import java.util.*; +import java.awt.*; +import java.text.*; + +public class Grid +{ + + TimeUnit rowUnit, columnUnit; + RoughTime startRow, endRow; + Interval interval; + HashMap<Long, GridCell> cells; + int[] screenGridX; + int cellHeight = 80, cellWidth, numCols, numRows; + Rectangle bounds = new Rectangle(); + int dy; + static final DateFormat dayOfWeek = new SimpleDateFormat("EEE"); + static final DateFormat month = new SimpleDateFormat("MMM d"); + static final String[] day = + { + "SUN", "MON", "TUES", "WED", "THURS", "FRI", "SAT" + }; + + Grid(TimeUnit rowUnit, TimeUnit columnUnit, Interval interval) + { + this.rowUnit = rowUnit; + this.columnUnit = columnUnit; + numCols = columnUnit.numUnitsIn(rowUnit); + setInterval(interval); + } + + public void setDY(int dy) + { + this.dy = dy; + } + + public int getCalendarHeight() + { + return bounds.height + bounds.y + 20; + } + + public RoughTime getTime(int x, int y) + { + y += dy; + if (!bounds.contains(x, y)) + { + return null; + } + + // find grid coordinates. + int gridX = (x - bounds.x) / cellWidth; + int gridY = (y - bounds.y) / cellHeight; + + return startRow.plus(rowUnit, gridY).plus(columnUnit, gridX); + } + + public double getScrollFraction() + { + double x = (getFirstDrawnTime().getTime() - startRow.getTime()) / (double) (endRow.getTime() - startRow.getTime()); + if (x < 0) + { + return 0; + } + if (x > 1) + { + return 1; + } + return x; + } + + public RoughTime getFirstDrawnTime() + { + int gridY = (dy - bounds.y) / cellHeight; + + return startRow.plus(rowUnit, gridY); + } + + private Point getGridCorner(long timestamp) + { + int diff = (int) columnUnit.difference(timestamp, startRow.getTime()); + int gridX = diff % numCols; + int gridY = diff / numCols; + return new Point(gridX, gridY); + } + + public Rectangle getCell(long timestamp) + { + Point p = getGridCorner(timestamp); + return new Rectangle(bounds.x + p.x * cellWidth, bounds.y + p.y * cellHeight - dy, + cellWidth, cellHeight); + } + + void setInterval(Interval interval) + { + this.interval = interval; + startRow = rowUnit.roundDown(interval.start); + endRow = rowUnit.roundDown(interval.end); + numRows = 1 + (int) (rowUnit.difference(endRow.getTime(), startRow.getTime())); + + // the next line fixes a problem with multi-century data sets. + // it works, but there's probably a better way to do this :-) + if (numRows > 50 && rowUnit.getRoughSize() >= TimeUnit.YEAR.getRoughSize()) + { + numRows++; + } + } + + void makeCells(java.util.List<VisualAct> visualActs) + { + cells = new HashMap<Long, GridCell>(); + for (VisualAct v : visualActs) + { + if (v.getStart() == null) + { + continue; + } + long timestamp = v.getStart().getTime(); + RoughTime timeKey = columnUnit.roundDown(timestamp); + GridCell cell = cells.get(timeKey.getTime()); + if (cell == null) + { + cell = new GridCell(timeKey); + cells.put(timeKey.getTime(), cell); + Point p = getGridCorner(timestamp); + cell.gridX = p.x; + cell.gridY = p.y; + } + cell.visualActs.add(v); + } + } + + void render(Graphics2D g, Display display, Rectangle screenBounds, CalendarVisuals visuals, + Collection<Mouseover> objectLocations) + { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int left = 110, right = 20; + int padY = 50; + boolean shouldLabel = visuals.drawStyle == CalendarVisuals.DrawStyle.LABEL; + + cellWidth = (screenBounds.width - left - right) / numCols; + + boolean fitTight = visuals.fitStyle == CalendarVisuals.FitStyle.TIGHT; + int idealHeight = fitTight ? 12 : Display.CALENDAR_CELL_HEIGHT; + cellHeight = Math.max(idealHeight, (screenBounds.height - padY - 10) / numRows); + this.bounds.setBounds(left, padY, numCols * cellWidth, numRows * cellHeight); + + g.setColor(new Color(240, 240, 240));//Color.white); + g.fill(screenBounds); + g.setColor(new Color(245, 245, 245)); + g.drawRect(bounds.x, bounds.y - dy, bounds.width, bounds.height); + g.setFont(display.bold()); + + // draw vertical grid lines. + Color gridColor = new Color(220, 220, 220); + + for (int i = 0; i <= numCols; i++) + { + int x = bounds.x + i * cellWidth; + g.setColor(gridColor); + g.drawLine(x, bounds.y - dy, x, bounds.y + bounds.height - dy); + if (rowUnit == TimeUnit.WEEK && i < 7) + { + g.setColor(Color.gray); + g.drawString(day[i], x, bounds.y - dy - 6); + } + } + + // horizontal grid lines. + RoughTime labelTime = startRow.copy(); + int lastLabelY = -100; + int lastYear = -1000000; + FontMetrics fm = display.boldFontMetrics(); + int skipped = 0; + for (int i = 0; i < numRows; i++) + { + int y = bounds.y + i * cellHeight; + if (y - dy > -50) + { + if (skipped > 0) + { + rowUnit.addTo(labelTime, skipped); + skipped = 0; + } + g.setColor(gridColor); + g.drawLine(bounds.x, y - dy, bounds.x + bounds.width, y - dy); + if (y - lastLabelY > 30 || lastLabelY < 0) + { + String label = null; + if (rowUnit == TimeUnit.WEEK) + { + int year = TimeUtils.cal(labelTime.getTime()).get(Calendar.YEAR); + if (year != lastYear) + { + label = labelTime.format(); + } else + { + label = month.format(labelTime.toDate()); + } + lastYear = year; + } else + { + label = labelTime.format(); + } + g.setColor(Color.gray); + g.drawString(label, bounds.x - fm.stringWidth(label) - 15, y + 15 - dy); + lastLabelY = y; + } + if (y - dy > screenBounds.height) + { + break; + } + + // now draw, in gray, the labels for each of the boxes. + if (!fitTight) + { + RoughTime gridLabel = labelTime.copy(); + int labelH = 13; + for (int j = 0; j < numCols; j++) + { + + g.setColor(Color.gray); + g.setFont(display.bold()); + String label = columnUnit.format(gridLabel.toDate()); + g.drawString(label, bounds.x + j * cellWidth + 3, y - dy + labelH); + columnUnit.addTo(gridLabel); + } + } + rowUnit.addTo(labelTime); + } else + { + skipped++; + } + } + + // draw a frame around the whole thing. + g.setColor(Color.darkGray); + g.drawRect(bounds.x, bounds.y - dy, bounds.width, bounds.height); + + // draw backgrounds + for (GridCell cell : cells.values()) + { + // are any visible? + boolean visible = false; + for (VisualAct v : cell.visualActs) + { + if (v.isVisible()) + { + visible = true; + break; + } + } + int cx = bounds.x + cell.gridX * cellWidth; + int cy = bounds.y + cell.gridY * cellHeight - dy; + + if (cy < screenBounds.y - 50 || cy > screenBounds.y + screenBounds.height + 50) + { + continue; + } + + // label top of cell. + int labelH = 0; + g.setColor(new Color(240, 240, 240)); + + if (visible) + { + g.setColor(Color.white); + g.fillRect(cx + 1, cy + 1, cellWidth - 1, cellHeight - 1); + } + if (cellHeight > 42) + { + labelH = 13; + g.setColor(Color.darkGray); + g.setFont(display.bold()); + String label = columnUnit.format(cell.time.toDate()); + g.drawString(label, cx + 3, cy + labelH); + } + + } + + + + // draw items. + int mx = 10, my = shouldLabel ? 18 : 10; + for (GridCell cell : cells.values()) + { + + int cx = bounds.x + cell.gridX * cellWidth; + int cy = bounds.y + cell.gridY * cellHeight - dy; + + if (cy < screenBounds.y - 50 || cy > screenBounds.y + screenBounds.height + 50) + { + continue; + } + + // label top of cell. + int labelH = cellHeight > 42 ? 13 : 0; + + // now draw the items in the cell. + // old, non-aggregation code: + + // START AGGREGATION CODE + + ArrayList<VisualAct> visibleActs = new ArrayList<VisualAct>(); + for (VisualAct v : cell.visualActs) + { + if (v.isVisible()) + { + visibleActs.add(v); + } + } + Iterator<VisualAct> vacts = + VisualActFactory.makeEmFit(visuals.model, visibleActs, new Rectangle(cx, cy, cellWidth, cellHeight)).iterator(); + + // END AGGREGATION CODE + + int leftX = 6; + int cdx = leftX; + int topDotY = Math.min(labelH + 16, cellHeight / 2); + int cdy = topDotY; + while (vacts.hasNext()) + { + VisualAct v = vacts.next(); + if (!v.isVisible()) + { + continue; + } + + // set x,y, room to right. + int x = cx + cdx; + int y = cy + cdy; + + int space = cellWidth - 20; + v.setX(x); + v.setY(y); + v.setSpaceToRight(space); + Mouseover o = v.draw(g, new Rectangle(cx + 1, cy + labelH + 1, cellWidth - 2, cellHeight - 2 - labelH), + bounds, display, shouldLabel, false); + if (o != null) + { + objectLocations.add(o); + } + + // go to next location. if we're labeling, we do this vertically. + // otherwise, left-to-right, then top-to-bottom. + + if (shouldLabel) + { + cdy += my; + if (cdy > cellHeight - 2 - my) + { + g.drawString("...", x, y + my); + break; + } + } else + { + cdx += mx; + if (cdx > cellWidth - mx / 2 - 2 && vacts.hasNext()) + { + cdx = leftX; + cdy += my; + if (cdy > cellHeight - my / 2) + { + break; + } + } + } + } + } + } +} diff --git a/timeflow/vis/calendar/GridCell.java b/timeflow/vis/calendar/GridCell.java new file mode 100755 index 0000000..44dd7ef --- /dev/null +++ b/timeflow/vis/calendar/GridCell.java @@ -0,0 +1,20 @@ +package timeflow.vis.calendar; + + +import timeflow.vis.*; +import timeflow.data.time.*; + +import java.util.*; +import java.awt.*; + +public class GridCell { + ArrayList<VisualAct> visualActs=new ArrayList<VisualAct>(); + Rectangle bounds; + RoughTime time; + int gridX, gridY; + + GridCell(RoughTime time) + { + this.time=time; + } +} diff --git a/timeflow/vis/timeline/AxisRenderer.java b/timeflow/vis/timeline/AxisRenderer.java new file mode 100755 index 0000000..7d020cb --- /dev/null +++ b/timeflow/vis/timeline/AxisRenderer.java @@ -0,0 +1,88 @@ +package timeflow.vis.timeline; + +import java.awt.*; +import java.util.*; + +import timeflow.data.time.Interval; +import timeflow.data.time.TimeUtils; +import timeflow.model.*; +import timeflow.vis.Mouseover; +import timeflow.vis.TimeScale; + +public class AxisRenderer { + + TimelineVisuals visuals; + + public AxisRenderer(TimelineVisuals visuals) + { + this.visuals=visuals; + } + + public void render(Graphics2D g, Collection<Mouseover> objectLocations) + { + TFModel model=visuals.getModel(); + g.setColor(model.getDisplay().getColor("chart.background")); + Rectangle bounds=visuals.getBounds(); + + TimeScale scale=visuals.getTimeScale(); + java.util.List<AxisTicMarks> t=AxisTicMarks.allRelevant(scale.getInterval()); + + int dateLabelH=model.getDisplay().getInt("timeline.datelabel.height"); + int y=bounds.y+bounds.height-dateLabelH; + + // draw in reverse order so bigger granularity at top. + int n=t.size(); + for (int i=0; i<n; i++) + { + render(t.get(i), g, bounds.x, y, dateLabelH-1, bounds.y, i==0, objectLocations); + y-=dateLabelH; + } + } + + void render(AxisTicMarks t, Graphics2D g, int x, int y, int h, int top, boolean full, Collection<Mouseover> objectLocations) + { + TFModel model=visuals.getModel(); + + int n=t.tics.size(); + for (int i=0; i<n-1; i++) + { + + long start=t.tics.get(i); + long end=t.tics.get(i+1); + + int x0=Math.max(x,visuals.getTimeScale().toInt(start)); + int x1=visuals.getTimeScale().toInt(end); + + int dayOfWeek=TimeUtils.cal(start).get(Calendar.DAY_OF_WEEK); + + g.setColor(t.unit.isDayOrLess() && (dayOfWeek==1 || dayOfWeek==7) ? + new Color(245,245,245) : new Color(240,240,240)); + + g.fillRect(x0, y, x1-x0-1, h); + g.setColor(Color.white); + g.drawLine(x1-1, y, x1-1, y+h); + g.drawLine(x0,y+h,x1,y+h); + objectLocations.add(new Mouseover(new Interval(start,end), x0, y, x1-x0-1, h)); + + g.setFont(model.getDisplay().timeLabel()); + String label=full? t.unit.formatFull(start) : t.unit.format(new Date(start)); + int tx=x0+3; + int ty=y+h-5; + g.setColor(full ? Color.darkGray : Color.gray); + int sw=model.getDisplay().timeLabelFontMetrics().stringWidth(label); + if (sw<x1-tx-3) + g.drawString(label, tx,ty); + else + { + int c=label.indexOf(':'); + if (c>0) + { + label=label.substring(0,c); + sw=model.getDisplay().timeLabelFontMetrics().stringWidth(label); + if (sw<x1-tx-3) + g.drawString(label, tx,ty); + } + } + } + } +} diff --git a/timeflow/vis/timeline/AxisTicMarks.java b/timeflow/vis/timeline/AxisTicMarks.java new file mode 100755 index 0000000..85a16dd --- /dev/null +++ b/timeflow/vis/timeline/AxisTicMarks.java @@ -0,0 +1,117 @@ +package timeflow.vis.timeline; + +import java.util.*; + +import timeflow.data.time.*; + +public class AxisTicMarks { + public TimeUnit unit; + public List<Long> tics; + + private static final TimeUnit[] units={ + TimeUnit.YEAR, TimeUnit.MONTH, TimeUnit.DAY, TimeUnit.HOUR, TimeUnit.MINUTE, TimeUnit.SECOND + }; + + private static final TimeUnit[] histUnits={ + TimeUnit.YEAR.times(100), TimeUnit.YEAR.times(50), TimeUnit.YEAR.times(25), + TimeUnit.YEAR.times(10), TimeUnit.YEAR.times(5), TimeUnit.YEAR.times(2), TimeUnit.YEAR, + TimeUnit.MONTH.times(6), TimeUnit.MONTH.times(3), TimeUnit.MONTH.times(2), TimeUnit.MONTH, + TimeUnit.WEEK, TimeUnit.DAY.times(2), TimeUnit.DAY, + + TimeUnit.HOUR, + TimeUnit.MINUTE, + TimeUnit.SECOND + }; + + public AxisTicMarks(TimeUnit unit, long start, long end) + { + this.unit=unit; + tics=new ArrayList<Long>(); + RoughTime r=unit.roundDown(start); + tics.add(r.getTime()); + do + { + unit.addTo(r); + tics.add(r.getTime()); + } while (r.getTime()<end); + } + + + + public static List<AxisTicMarks> allRelevant(Interval interval) + { + return allRelevant(interval.start, interval.end); + } + + public static List<AxisTicMarks> allRelevant(long start, long end) + { + return allRelevant(start, end, 40); + } + + public static AxisTicMarks histoTics(long start, long end) + { + for (int i=histUnits.length-1; i>=0; i--) + { + TimeUnit u=histUnits[i]; + long estimate=u.approxNumInRange(start, end); + if (estimate<200 || i==0) + { + AxisTicMarks t=new AxisTicMarks(u, start, end); + return t; + } + } + return null; + } + + public static List<AxisTicMarks> allRelevant(long start, long end, long maxTics) + { + List<AxisTicMarks> list=new ArrayList<AxisTicMarks>(); + + + for (int i=0; i<units.length; i++) + { + TimeUnit u=units[i]; + long estimate=u.approxNumInRange(start, end); + + if (estimate<maxTics) + { + AxisTicMarks t=new AxisTicMarks(u, start, end); + if (list.size()>0) + { + AxisTicMarks last=list.get(0); + if (last.tics.size()==t.tics.size()) + list.remove(0); + } + list.add(t); + + } + } + while (list.size()>2) + list.remove(0); + + if (list.size()==0) // uh oh! must be many years. we will add in bigger increments. + { + long length=end-start; + long size=365*24*60*60*1000L; + int m=1; + maxTics=15; + while (m<2000000000 && length/(m*size)>maxTics) + { + if (length/(2*m*size)<=maxTics) + { + m*=2; + break; + } + if (length/(5*m*size)<=maxTics) + { + m*=5; + break; + } + m*=10; + } + AxisTicMarks t=new AxisTicMarks(TimeUnit.multipleYears(m), start, end); + list.add(t); + } + return list; + } +} diff --git a/timeflow/vis/timeline/TimelineRenderer.java b/timeflow/vis/timeline/TimelineRenderer.java new file mode 100755 index 0000000..4c115a5 --- /dev/null +++ b/timeflow/vis/timeline/TimelineRenderer.java @@ -0,0 +1,184 @@ +package timeflow.vis.timeline; + +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.model.*; +import timeflow.vis.Mouseover; +import timeflow.vis.MouseoverLabel; +import timeflow.vis.VisualAct; + +import timeflow.util.*; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.util.*; +import java.util.List; + +public class TimelineRenderer +{ + + private TimelineVisuals visuals; + private int dy; + + public TimelineRenderer(TimelineVisuals visuals) + { + this.visuals = visuals; + } + + public void setDY(int dy) + { + this.dy = dy; + } + + public void render(Graphics2D g, Collection<Mouseover> objectLocations) + { + AffineTransform old = g.getTransform(); + g.setTransform(AffineTransform.getTranslateInstance(0, -dy)); + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + TFModel model = visuals.getModel(); + Display display = model.getDisplay(); + ActDB db = model.getDB(); + + if (display.emptyMessage(g, model)) + { + return; + } + + // need to check this, because resize events don't (and shouldn't) register with central TFModel. + visuals.layoutIfChanged(); + + java.util.List<VisualAct> visualActs = visuals.getVisualActs(); + + if (visualActs == null || visualActs.size() == 0) + { + g.drawString("No data", 10, 30); + return; + } + + Rectangle bounds = visuals.getBounds(); + boolean colorTrackLabels = db.getField(VirtualField.COLOR) == null || db.getField(VirtualField.COLOR).equals(db.getField(VirtualField.TRACK)); + + // draw tracks, if more than 1. + if (visuals.trackList.size() > 1) + { + boolean zebra = true; + + for (TimelineTrack t : visuals.trackList) + { + if (zebra) + { + g.setColor(display.getColor("timeline.zebra")); + g.fillRect(bounds.x, t.y0, bounds.width, t.y1 - t.y0); + } + zebra = !zebra; + g.setColor(display.getColor("timeline.grid")); + g.drawLine(bounds.x, t.y0, bounds.x + bounds.width, t.y0); + } + } + + Interval screenInterval = visuals.getViewInterval().subinterval(-.5, 1.5); + AxisTicMarks tics = AxisTicMarks.histoTics(screenInterval.start, screenInterval.end); + for (TimelineTrack t : visuals.trackList) + { + // now... if not in graph mode, just draw items + if (visuals.getLayoutStyle() != TimelineVisuals.Layout.GRAPH)//max<(t.y1-t.y0)/20) + { + for (VisualAct v : t.visualActs) + { + Mouseover o = v.draw(g, null, bounds, display, true, true); + if (o != null) + { + o.y -= dy; + objectLocations.add(o); + } + } + continue; + } + + if (true) continue; + + // draw bars. to do so, we make a histogram of visible items. + t.histogram = new DoubleBag<Long>(); + for (VisualAct v : t.visualActs) + { + long time = v.getStart().getTime(); + if (screenInterval.contains(time)) + { + t.histogram.add(tics.unit.roundDown(v.getStart().getTime()).getTime(), 1);//v.getSize()); + } + } + + // get max of items. + double max = t.histogram.getMax(); + + // now draw bars on screen. + Color fg = colorTrackLabels ? model.getDisplay().makeColor(t.label) : Color.gray; + if (visuals.trackList.size() < 2) + { + fg = Color.gray; + } + + List<Long> keys = t.histogram.unordered(); + Collections.sort(keys); + for (Long r : keys) + { + double num = t.histogram.num(r); + int x1 = visuals.getTimeScale().toInt(r); + int x2 = visuals.getTimeScale().toInt(tics.unit.roundUp(r + 1).getTime()); + int barY = t.y1 - (int) (.9 * ((t.y1 - t.y0) * num) / max); + g.setColor(new Color(230, 230, 230)); + int m = 12; + g.fillRoundRect(x1 + 3, barY + 3, x2 - x1 - 1, t.y1 - barY, m, m); + + g.setColor(fg); + g.fillRoundRect(x1, barY, x2 - x1 - 1, t.y1 - barY, m, m); + + MouseoverLabel mouse = new MouseoverLabel("" + Math.round(num), "items", x1, barY, x2 - x1 - 1, t.y1 - barY); + objectLocations.add(mouse); + } + } + + // finally label the tracks. we do this last so that the labels go on top of the data. + + if (visuals.trackList.size() > 1) + { + boolean zebra = false; + FontMetrics hugeFm = display.hugeFontMetrics(); + for (TimelineTrack t : visuals.trackList) + { + + // now label the track. + //if (t.y1 - t.y0 > 23) + { + Color fg = colorTrackLabels ? model.getDisplay().makeColor(t.label) : Color.darkGray; + Color bg = zebra ? display.getColor("timeline.zebra") : Color.white; + + String label = t.label; + if (label.equals(Display.MISC_CODE)) + { + label = display.getMiscLabel(); + } else if (label.length() == 0) + { + label = display.getNullLabel(); + } else + { + label = display.format(label, 20, false); + } + + // draw background. + g.setColor(bg); + int sw = hugeFm.stringWidth(label); + g.fillRect(0, t.y1 - 20, sw + 8, 19); + + // draw foreground (actual label) + g.setFont(display.huge()); + g.setColor(fg); + g.drawString(label, 2, t.y1); // - 5); + } + zebra = !zebra; + } + } + g.setTransform(old); + } +} diff --git a/timeflow/vis/timeline/TimelineSlider.java b/timeflow/vis/timeline/TimelineSlider.java new file mode 100755 index 0000000..005db28 --- /dev/null +++ b/timeflow/vis/timeline/TimelineSlider.java @@ -0,0 +1,239 @@ +package timeflow.vis.timeline; + +import timeflow.data.db.*; +import timeflow.data.time.*; +import timeflow.model.*; +import timeflow.vis.TimeScale; +import timeflow.vis.VisualAct; +import timeflow.vis.timeline.*; + +import timeflow.util.*; + +import java.awt.*; +import javax.swing.*; +import java.awt.event.*; + +public class TimelineSlider extends ModelPanel +{ + + TimelineVisuals visuals; + Interval original; + long minRange; + int ew = 10; + int eventRadius = 2; + TimeScale scale; + Point mouseHit = new Point(); + Point mouse = new Point(-1, 0); + + enum Modify + { + + START, END, POSITION, NONE + }; + Modify change = Modify.NONE; + Rectangle startRect = new Rectangle(-1, -1, 0, 0); + Rectangle endRect = new Rectangle(-1, -1, 0, 0); + Rectangle positionRect = new Rectangle(-1, -1, 0, 0); + Color sidePlain = Color.orange; + Color sideMouse = new Color(230, 100, 0); + + public TimelineSlider(final TimelineVisuals visuals, final long minRange, final Runnable action) + { + super(visuals.getModel()); + + this.minRange = minRange; + this.visuals = visuals; + + addMouseListener(new MouseAdapter() + { + + @Override + public void mousePressed(MouseEvent e) + { + int mx = e.getX(); + int my = e.getY(); + if (positionRect.contains(mx, my)) + { + change = Modify.POSITION; + } else if (startRect.contains(mx, my)) + { + change = Modify.START; + } else if (endRect.contains(mx, my)) + { + change = Modify.END; + } else + { + change = Modify.NONE; + } + mouseHit.setLocation(mx, my); + original = window().copy(); + mouse.setLocation(mx, my); + repaint(); + } + + @Override + public void mouseReleased(MouseEvent e) + { + change = Modify.NONE; + repaint(); + } + }); + addMouseMotionListener(new MouseMotionAdapter() + { + + @Override + public void mouseDragged(MouseEvent e) + { + + if (change == Modify.NONE) + { + return; + } + mouse.setLocation(e.getX(), e.getY()); + int mouseDiff = mouse.x - mouseHit.x; + Interval limits = visuals.getGlobalInterval(); + long timeDiff = scale.spaceToTime(mouseDiff); + + switch (change) + { + case POSITION: + window().translateTo(original.start + timeDiff); + window().clampInside(limits); + break; + case START: + window().start = Math.min(original.start + timeDiff, original.end - minRange); + window().start = Math.max(window().start, limits.start); + break; + case END: + window().end = Math.max(original.end + timeDiff, original.start + minRange); + window().end = Math.min(window().end, limits.end); + } + getModel().setViewInterval(window()); + action.run(); + repaint(); + } + }); + } + + private Interval window() + { + return visuals.getViewInterval(); + } + + @Override + public Dimension getPreferredSize() + { + return new Dimension(600, 30); + } + + public void setMinRange(long minRange) + { + this.minRange = minRange; + } + + @Override + public void note(TFEvent e) + { + repaint(); + } + + void setTimeInterval(Interval interval) + { + window().setTo(interval); + repaint(); + } + + public void paintComponent(Graphics g1) + { + int w = getSize().width, h = getSize().height; + Graphics2D g = (Graphics2D) g1; + + long start = System.currentTimeMillis(); + + // draw main backdrop. + g.setColor(Color.white); + g.fillRect(0, 0, w, h); + + if (visuals.getModel() == null || visuals.getModel().getActs() == null) + { + g.setColor(Color.darkGray); + g.drawString("No data for timeline.", 5, 20); + return; + } + + scale = new TimeScale(); + scale.setDateRange(visuals.getGlobalInterval()); + scale.setNumberRange(ew, w - ew); + + + // draw the area for the central "thumb". + int lx = scale.toInt(window().start); + int rx = scale.toInt(window().end); + g.setColor(change == Modify.POSITION ? new Color(255, 255, 120) : new Color(255, 245, 200)); + positionRect.setBounds(lx, 0, rx - lx, h); + g.fill(positionRect); + + // Figure out how best to draw events. + // If there are too many, we just draw a kind of histogram of frequency, + // rather than using the timeline layout. + int slotW = 2 * eventRadius; + int slotNum = w / slotW + 1; + int[] slots = new int[slotNum]; + int mostInSlot = 0; + for (VisualAct v : visuals.getVisualActs()) + { + if (!v.isVisible()) + { + continue; + } + int x = scale.toInt(v.getStart().getTime()); + int s = x / slotW; + if (s >= 0 && s < slotNum) + { + slots[s]++; + mostInSlot = Math.max(mostInSlot, slots[s]); + } + } + if (mostInSlot > 30) + { + g.setColor(Color.gray); + for (int i = 0; i < slots.length; i++) + { + int sh = (h * slots[i]) / mostInSlot; + g.fillRect(slotW * i, h - sh, slotW, sh); + } + } else + { + // draw individual events. + for (VisualAct v : visuals.getVisualActs()) + { + if (!v.isVisible()) + { + continue; + } + g.setColor(v.getColor()); + int x = scale.toInt(v.getStart().getTime()); + + int y = eventRadius + (int) (v.getY() * h) / (visuals.getBounds().height - 2 * eventRadius); + g.fillRect(x - 1, y - eventRadius, 2 * eventRadius, 3); + if (v.getEnd() != null) + { + int endX = scale.toInt(v.getEnd().getTime()); + g.drawLine(x, y, endX, y); + } + } + } + + g.setColor(Color.gray); + g.drawLine(0, 0, w, 0); + g.drawLine(0, h - 1, w, h - 1); + + // draw "expansion" areas on sides of thumb. + startRect.setBounds(positionRect.x - ew, 1, ew, h - 2); + g.setColor(change == Modify.START ? sideMouse : sidePlain); + g.fill(startRect); + endRect.setBounds(positionRect.x + positionRect.width, 1, ew, h - 2); + g.setColor(change == Modify.END ? sideMouse : sidePlain); + g.fill(endRect); + } +} diff --git a/timeflow/vis/timeline/TimelineTrack.java b/timeflow/vis/timeline/TimelineTrack.java new file mode 100755 index 0000000..e86c2f4 --- /dev/null +++ b/timeflow/vis/timeline/TimelineTrack.java @@ -0,0 +1,129 @@ +package timeflow.vis.timeline; + +import timeflow.vis.VisualAct; +import timeflow.data.time.*; +import timeflow.util.*; + +import java.util.*; + +public class TimelineTrack implements Comparable +{ + + String label; + List<VisualAct> visualActs = new ArrayList<VisualAct>(); + int y0, y1; + DoubleBag<Long> histogram; + + TimelineTrack(String label) + { + this.label = label; + } + + void add(VisualAct v) + { + visualActs.add(v); + } + + int size() + { + return visualActs.size(); + } + + // assumes a>b>0 + int gcd(int a, int b) + { + int mod = a % b; + if (mod == 0) + { + return b; + } + return gcd(b, mod); + } + + int nearAndRelPrime(int target, int modulus) + { + if (target < 2) + { + return 1; + } + while (gcd(modulus, target) > 1) + { + target--; + } + return target; + } + + // top and height are in proportion of total height of frame. + void layout(double top, double height, TimelineVisuals visuals) + { + int n = visualActs.size(); + if (n == 0) + { + return; + } + + int labelHeight = 0; // 80; + int fh = visuals.getBounds().height - labelHeight; + int fy = visuals.getBounds().y; + int cellH = visuals.getModel().getDisplay().getInt("timeline.item.height.min"); + + int fontHeight = 12; + + y0 = fy + (int) (fh * top); + y1 = fy + (int) (fh * (top + height)); + //int mid = (y0 + y1) / 2; + int iy0 = y0; // Math.min(y0 + 5, mid); + int iy1 = y1; // Math.max(y1 - 15, mid); + + int numCells = Math.max(1, (iy1 - iy0) / cellH); + + VisualAct[] rights = new VisualAct[numCells]; + + int step = nearAndRelPrime((int) (.61803399 * numCells), numCells); + int i = 0; + VisualAct last = null; + for (VisualAct v : visualActs) + { + if (!v.isVisible() || !v.getTrack().equals(this)) + { + continue; + } + v.setSpaceToRight(1000); + + double num = visuals.getTimeScale().toNum(v.getStart().getTime()); + int x = (int) num; + + int cell = numCells < 2 ? 0 : (i % numCells); + int y = iy0 + cell * cellH; // (iy1 - iy0 < 12 ? 0 : cell * cellH); + v.setX(x); + v.setY(y + fontHeight); + + if (v.getEnd() != null) + { + v.setEndX((int) visuals.getTimeScale().toNum(v.getEnd().getTime())); + } + + if (rights[cell] != null) + { + int space = x - rights[cell].getX(); + rights[cell].setSpaceToRight(space); + } + rights[cell] = v; + if ((last != null && v.getStart().getTime() == last.getStart().getTime()) + || visuals.getLayoutStyle() == TimelineVisuals.Layout.TIGHT) + { + i++; + } else + { + i += step; + } + last = v; + } + } + + @Override + public int compareTo(Object o) + { + return ((TimelineTrack) o).size() - size(); + } +} diff --git a/timeflow/vis/timeline/TimelineVisuals.java b/timeflow/vis/timeline/TimelineVisuals.java new file mode 100755 index 0000000..4e54d8a --- /dev/null +++ b/timeflow/vis/timeline/TimelineVisuals.java @@ -0,0 +1,345 @@ +package timeflow.vis.timeline; + +import timeflow.data.db.*; +import timeflow.data.db.filter.*; +import timeflow.data.time.*; +import timeflow.model.*; +import timeflow.vis.*; + +import java.util.*; +import java.awt.*; + +/* + * A VisualEncoding takes the info about which fields to translate to + * which visual aspects, and applies that to particular Acts. + */ +public class TimelineVisuals +{ + + private Map<String, TimelineTrack> trackTable = new HashMap<String, TimelineTrack>(); + ArrayList<TimelineTrack> trackList = new ArrayList<TimelineTrack>(); + private TimeScale timeScale = new TimeScale(); + private Rectangle bounds = new Rectangle(); + private boolean frameChanged; + private int numShown = 0; + private Interval globalInterval; + + public double scale = 1; + + public enum Layout + { + + TIGHT, LOOSE, GRAPH + }; + private Layout layoutStyle = Layout.TIGHT; + private VisualEncoder encoder; + private TFModel model; + private int fullHeight; + + public int getFullHeight() + { + return fullHeight; + } + + public TimelineVisuals(TFModel model) + { + this.model = model; + encoder = new VisualEncoder(model); + } + + public TimeScale getTimeScale() + { + return timeScale; + } + + public Rectangle getBounds() + { + return bounds; + } + + public void setBounds(int x, int y, int w, int h) + { + bounds.setBounds(x, y, w, h); + timeScale.setLow(x); + timeScale.setHigh(x + w); + frameChanged = true; + } + + public Layout getLayoutStyle() + { + return layoutStyle; + } + + public void setLayoutStyle(Layout style) + { + layoutStyle = style; + layout(); + } + + public Interval getFitToVisibleRange() + { + ActList acts = model.getActs(); + + // add a little bit to the right so we can see labels... + ActDB db = getModel().getDB(); + Field endField = db.getField(VirtualField.END); + Interval i = null; + if (endField == null) + { + i = DBUtils.range(acts, VirtualField.START); + } else + { + i = DBUtils.range(acts, new Field[] + { + db.getField(VirtualField.START), endField + }); + } + if (i.length() == 0) + { + i.expand(globalInterval.length() / 20); + } + i = i.subinterval(-.05, 1.1); + i.intersection(globalInterval); + return i; + } + + public void fitToVisible() + { + Interval i = getFitToVisibleRange(); + setTimeBounds(i.start, i.end); + } + + public void zoomOut() + { + setTimeBounds(globalInterval.start, globalInterval.end); + } + + public void setTimeBounds(long first, long last) + { + timeScale.setDateRange(first, last); + frameChanged = true; + model.setViewInterval(new Interval(first, last)); + } + + public Interval getGlobalInterval() + { + if (globalInterval == null && model != null && model.getDB() != null) + { + createGlobalInterval(); + } + return globalInterval; + } + + public void createGlobalInterval() + { + globalInterval = DBUtils.range(model.getDB().all(), VirtualField.START).subinterval(-.05, 1.1); + } + + public Interval getViewInterval() + { + return timeScale.getInterval(); + } + + public java.util.List<VisualAct> getVisualActs() + { + return encoder.getVisualActs(); + } + + public void layoutIfChanged() + { + if (frameChanged) + { + layout(); + } + } + + public void init(boolean majorChange) + { + note(new TFEvent(majorChange ? TFEvent.Type.DATABASE_CHANGE : TFEvent.Type.ACT_CHANGE, null)); + } + + public void note(TFEvent e) + { + ActList all = null; + if (e.type == TFEvent.Type.DATABASE_CHANGE) + { + all = model.getDB().all(); + createGlobalInterval(); + Interval i = guessInitialViewInterval(all, globalInterval); + setTimeBounds(i.start, i.end); + } + if (e.affectsRowSet()) + { + all = model.getDB().all(); + encoder.createVisualActs(); + createGlobalInterval(); + } else + { + encoder.createVisualActs(); + } + Interval v = model.getViewInterval(); + if (v != null && v.start != timeScale.getInterval().start) + { + timeScale.getInterval().translateTo(v.start); + } + updateVisuals(); + } + + private Interval guessInitialViewInterval(ActList acts, Interval fullRange) + { + if (acts.size() < 50) + { + return fullRange.copy(); + } + + Interval best = null; + int most = -1; + double d = Math.max(.1, 50. / acts.size()); + d = Math.min(1. / 3, d); + for (double x = 0; x < 1 - d; x += d / 4) + { + Interval i = fullRange.subinterval(x, x + d); + TimeIntervalFilter f = new TimeIntervalFilter(i, getModel().getDB().getField(VirtualField.START)); + int num = 0; + for (Act a : acts) + { + if (f.accept(a)) + { + num++; + } + } + if (num > most) + { + most = num; + best = i; + } + } + return best; + } + + public void updateVisuals() + { + scale = 1; + updateVisualEncoding(); + layout(); + } + + public TFModel getModel() + { + return model; + } + + public int getNumTracks() + { + return trackList.size(); + } + + public double layout() + { + ActList acts = model.getActs(); + if (acts == null) + { + return scale; + } + + double labelHeight = 30; + + double min = bounds.height == 0 ? 0 : labelHeight / bounds.height; + double cellH = (double)getModel().getDisplay().getInt("timeline.item.height.min") / bounds.height; + + double maxCount = 0; + + // Set the minimum scale + for (TimelineTrack t : trackList) + { + //double height = t.size() * scale / (double) numShown; + + maxCount = Math.max(t.size(), maxCount); + } + + scale = Math.min(cellH * numShown, scale); + scale = Math.max(min * numShown / maxCount, scale); + + double top = 0; + for (TimelineTrack t : trackList) + { + double height = Math.max(min, t.size() * scale / (double) numShown); + t.layout(top, height, this); + top += height; + } + fullHeight = (int) (top * bounds.height); + + Collections.sort(trackList); + frameChanged = false; + + return scale; + } + + private void updateVisualEncoding() + { + java.util.List<VisualAct> acts = encoder.apply(); + + // now arrange on tracks + trackTable = new HashMap<String, TimelineTrack>(); + trackList = new ArrayList<TimelineTrack>(); + numShown = 0; + for (VisualAct v : acts) + { + if (!v.isVisible()) + { + continue; + } + numShown++; + String s = v.getTrackString(); + TimelineTrack t = trackTable.get(s); + if (t == null) + { + t = new TimelineTrack(s); + trackTable.put(s, t); + trackList.add(t); + } + t.add(v); + v.setTrack(t); + } + + /* + // the following code is no longer used, but could come in handy again one day... + + // If there is more than one "small" track, then we will coalesce them into + // one bigger "miscellaneous" track. + int minSize=numShown/30;//Math.max(3,numShown/30); + ArrayList<TimelineTrack> small=new ArrayList<TimelineTrack>(); + for (TimelineTrack t: trackList) + { + if (t.size()<minSize) + small.add(t); + } + if (small.size()>1) + { + // create a new Track for "miscellaneous." + TimelineTrack misc=new TimelineTrack(Display.MISC_CODE); + trackList.add(misc); + trackTable.put(misc.label, misc); + + // remove the old tracks. + for (TimelineTrack t:small) + { + trackList.remove(t); + trackTable.remove(t.label); + for (VisualAct v: t.visualActs) + { + v.setTrack(misc); + misc.add(v); + } + } + // sort miscellaneous items in time order. + //Collections.sort(misc.visualActs); + } + */ + + for (TimelineTrack t : trackList) + { + Collections.sort(t.visualActs); + } + } +} -- Gitblit v1.6.2