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