com.stackframe.bentographer.BentoGrapher.java Source code

Java tutorial

Introduction

Here is the source code for com.stackframe.bentographer.BentoGrapher.java

Source

/*
 * Copyright 2011 StackFrame, LLC
 *
 * This is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3
 * as published by the Free Software Foundation.
 *
 * You should have received a copy of the GNU General Public License
 * along with this file.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.stackframe.bentographer;

import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import info.monitorenter.gui.chart.Chart2D;
import info.monitorenter.gui.chart.IAxis;
import info.monitorenter.gui.chart.ITrace2D;
import info.monitorenter.gui.chart.pointpainters.PointPainterDisc;
import info.monitorenter.gui.chart.rangepolicies.RangePolicyFixedViewport;
import info.monitorenter.gui.chart.traces.Trace2DSimple;
import info.monitorenter.util.Range;
import java.awt.Component;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JOptionPane;

/**
 * A simple tool for making charts from Bento databases.
 *
 * @author mcculley
 */
public class BentoGrapher {

    // Types of fields we ignore when graphing.
    private static final ImmutableSet<String> ignorableFields = ImmutableSet.of(
            "com.filemaker.bento.field.layout.horizontalSeparator", "com.filemaker.bento.field.core.text",
            "com.filemaker.bento.field.layout.textBox", "com.filemaker.bento.field.layout.columnDivider",
            "com.filemaker.bento.field.core.media");

    private static Connection openConnection(File file) throws SQLException {
        return DriverManager.getConnection("jdbc:sqlite:" + file.getPath());
    }

    private static Collection<Field> getFields(Connection connection, Library library) throws SQLException {
        ImmutableList.Builder<Field> fields = new ImmutableList.Builder();
        PreparedStatement ps = connection.prepareStatement(
                "SELECT gn_label AS label, gn_name AS column, gn_typeName AS type FROM gn_field WHERE gn_domain=?");
        try {
            ps.setInt(1, library.domain);
            ResultSet rs = ps.executeQuery();
            try {
                while (rs.next()) {
                    String name = rs.getString("label");
                    String column = rs.getString("column");
                    String type = rs.getString("type");
                    fields.add(new Field(name, type, "gn_" + column));
                }
            } finally {
                rs.close();
            }
        } finally {
            ps.close();
        }

        return fields.build();
    }

    private static Object select(Collection<?> libraries, String message, String title) {
        Object[] possibleValues = libraries.toArray();
        Component parent = null;
        int type = JOptionPane.INFORMATION_MESSAGE;
        Icon icon = null;
        Object defaultValue = possibleValues[0];
        Object selectedValue = JOptionPane.showInputDialog(parent, message, title, type, icon, possibleValues,
                defaultValue);
        return selectedValue;
    }

    private static Map<String, Library> getLibraries(Connection connection) throws SQLException {
        ImmutableMap.Builder<String, Library> libraries = new ImmutableMap.Builder();
        PreparedStatement ps = connection.prepareStatement(
                "SELECT gn_sourceitem.gn_label AS label, gn_domain.gn_name AS name, gn_domain.gnpk AS domain FROM gn_sourceitem INNER JOIN gn_domain ON (gn_sourceitem.gn_domain = gn_domain.gnpk) where gn_sourceitem.gn_parent is null");
        try {
            ResultSet rs = ps.executeQuery();
            try {
                while (rs.next()) {
                    String name = rs.getString("name");
                    String label = rs.getString("label");
                    int domain = rs.getInt("domain");
                    libraries.put(label, new Library(name, label, domain));
                }
            } finally {
                rs.close();
            }
        } finally {
            ps.close();
        }

        return libraries.build();
    }

    private static Map<Number, Number> getData(Connection connection, Library library, Field x, Field y)
            throws SQLException {
        Map<Number, Number> data = new TreeMap();
        PreparedStatement ps = connection
                .prepareStatement(String.format("SELECT %s AS x, %s AS y FROM %s WHERE %s IS NOT NULL", x.column,
                        y.column, library.tableName(), y.column));
        try {
            ResultSet rs = ps.executeQuery();
            try {
                while (rs.next()) {
                    double xValue = rs.getDouble("x");
                    double yValue = rs.getDouble("y");
                    data.put(xValue, yValue);
                }
            } finally {
                rs.close();
            }
        } finally {
            ps.close();
        }

        return ImmutableMap.copyOf(data);
    }

    private static void makeGraph(Connection connection, Library library, Field x, Field y) throws SQLException {
        Map<Number, Number> data = getData(connection, library, x, y);
        Chart2D chart = new Chart2D();
        ITrace2D trace = new Trace2DSimple();
        trace.setName(y.name);
        trace.setPhysicalUnits(x.name, y.name);
        chart.addTrace(trace);
        trace.setPointHighlighter(new PointPainterDisc());
        for (Map.Entry<Number, Number> entry : data.entrySet()) {
            double xValue = entry.getKey().doubleValue();
            double yValue = entry.getValue().doubleValue();
            trace.addPoint(xValue, yValue);
        }

        // FIXME: This can't be the right way to do this. I just want to set the min Y to 0 instead of the smallest value.
        chart.getAxisY().setRangePolicy(new RangePolicyFixedViewport(new Range(0, chart.getAxisY().getMax())));

        chart.getAxisX().setAxisTitle(new IAxis.AxisTitle(x.name));

        JFrame frame = new JFrame(y.name);
        frame.getContentPane().add(chart);
        frame.setSize(800, 600);
        frame.setVisible(true);
    }

    private static File findDatabase() {
        File home = new File(System.getProperty("user.home"));
        return new File(home, "Library/Application Support/Bento/bento.bentodb/Contents/Resources/Database");
    }

    private static Comparator<Field> makeTypeComparator(final String typeName) {
        return new Comparator<Field>() {

            @Override
            public int compare(Field t, Field t1) {
                if (t.type.equals(t1.type)) {
                    return 0;
                } else {
                    if (t.type.equals(typeName)) {
                        return -1;
                    } else if (t1.type.equals(typeName)) {
                        return 1;
                    } else {
                        return 0;
                    }
                }
            }
        };
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws Exception {
        Class.forName("org.sqlite.JDBC");
        File db = findDatabase();
        Connection connection = openConnection(db);
        try {
            Map<String, Library> libraries = getLibraries(connection);
            String selected = (String) select(libraries.keySet(), "Choose Library", "Library");
            Library library = libraries.get(selected);
            Collection<Field> fields = getFields(connection, library);

            fields = Collections2.filter(fields, new Predicate<Field>() {

                @Override
                public boolean apply(Field t) {
                    return !ignorableFields.contains(t.type);
                }
            });

            Comparator<Field> dateComparator = makeTypeComparator("com.filemaker.bento.field.core.date");
            Comparator<Field> dateCreatedComparator = makeTypeComparator(
                    "com.filemaker.bento.field.private.timestamp.dateCreated");
            Comparator<Field> dateModifiedComparator = makeTypeComparator(
                    "com.filemaker.bento.field.private.timestamp.dateModified");
            Ordering<Field> xOrdering = Ordering.from(dateComparator).compound(dateCreatedComparator)
                    .compound(dateModifiedComparator);
            Collection<Field> sortedXFields = xOrdering.immutableSortedCopy(fields);

            // FIXME: This depends on the implemenation of Field.toString() returning the type name. It should use a Renderer or something.
            final Field x = (Field) select(sortedXFields, "Choose X", "X");

            fields = Collections2.filter(fields, new Predicate<Field>() {

                @Override
                public boolean apply(Field t) {
                    return t != x;
                }
            });

            Ordering<Field> yOrdering = Ordering.from(Collections.reverseOrder(dateModifiedComparator))
                    .compound(Collections.reverseOrder(dateCreatedComparator))
                    .compound(Collections.reverseOrder(dateComparator));
            Collection<Field> sortedYFields = yOrdering.immutableSortedCopy(fields);
            Field y = (Field) select(sortedYFields, "Choose Y", "Y");

            // FIXME: Make the rendering of dates smart. It looks like date is seconds since 1/1/2001 and is defined by Core Data.
            // FIXME: Make the graphs printable.
            // FIXME: Make it easy to dynamically add more Y values to same graph.
            // FIXME: Package up a binary that is easy to run. (JNLP/Web Startable?)
            // FIXME: Publish some screenshots to make it easier to understand what this is for.
            // FIXME: Make it possible to save graph paramters and automatically bring them back up.
            // FIXME: Make setting of min as 0 configurable.
            // FIXME: Fix graph to show actual data points on lines.

            makeGraph(connection, library, x, y);
        } finally {
            connection.close();
        }
    }
}