package com.github.pfichtner.jrunalyser.ui.dock;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.eknet.swing.task.Mode.BLOCKING;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.SwingUtilities;
import org.eknet.swing.task.AbstractTask;
import org.eknet.swing.task.TaskManager;
import org.eknet.swing.task.Tracker;
import org.eknet.swing.task.impl.TaskManagerImpl;
import org.eknet.swing.task.ui.TaskGlassPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.CGrid;
import bibliothek.gui.dock.common.DefaultSingleCDockable;
import bibliothek.gui.dock.common.menu.SingleCDockableListMenuPiece;
import bibliothek.gui.dock.common.theme.ThemeMap;
import bibliothek.gui.dock.facile.menu.RootMenuPiece;
import com.ezware.dialog.task.TaskDialogs;
import com.github.pfichtner.jrunalyser.base.data.Distance;
import com.github.pfichtner.jrunalyser.base.data.Distances;
import com.github.pfichtner.jrunalyser.base.data.Duration;
import com.github.pfichtner.jrunalyser.base.data.Durations;
import com.github.pfichtner.jrunalyser.base.data.WayPoint;
import com.github.pfichtner.jrunalyser.base.data.jaxb.GpxMarshaller;
import com.github.pfichtner.jrunalyser.base.data.jaxb.GpxUnmarshaller;
import com.github.pfichtner.jrunalyser.base.data.stat.Functions.StatisticsProviders;
import com.github.pfichtner.jrunalyser.base.data.stat.Functions.Statisticss;
import com.github.pfichtner.jrunalyser.base.data.stat.Statistics;
import com.github.pfichtner.jrunalyser.base.data.track.DefaultTrack;
import com.github.pfichtner.jrunalyser.base.data.track.Id;
import com.github.pfichtner.jrunalyser.base.data.track.StatisticsProvider;
import com.github.pfichtner.jrunalyser.base.data.track.Track;
import com.github.pfichtner.jrunalyser.base.datasource.CachingDatasourceFascadeProxy;
import com.github.pfichtner.jrunalyser.base.datasource.DataSourceDatasourceFascadeAdapter;
import com.github.pfichtner.jrunalyser.base.datasource.Datasource;
import com.github.pfichtner.jrunalyser.base.datasource.DatasourceFascade;
import com.github.pfichtner.jrunalyser.base.datasource.DatasourceFascadeEvent;
import com.github.pfichtner.jrunalyser.base.datasource.DatasourceFascadeEvent.Type;
import com.github.pfichtner.jrunalyser.base.datasource.DatasourceFascadeListener;
import com.github.pfichtner.jrunalyser.base.datasource.InMemoryDataSource;
import com.github.pfichtner.jrunalyser.base.datasource.SerializatingDatasourceFascade;
import com.github.pfichtner.jrunalyser.base.datasource.StatCalculatorDatasourceFascade;
import com.github.pfichtner.jrunalyser.di.Injector;
import com.github.pfichtner.jrunalyser.ui.base.DefaultGridData;
import com.github.pfichtner.jrunalyser.ui.base.DockPlugin;
import com.github.pfichtner.jrunalyser.ui.base.GridData;
import com.github.pfichtner.jrunalyser.ui.base.GridDataProvider;
import com.github.pfichtner.jrunalyser.ui.base.UiPlugin;
import com.github.pfichtner.jrunalyser.ui.base.i18n.I18N;
import com.github.pfichtner.jrunalyser.ui.dock.ebus.EventBusMessage;
import com.github.pfichtner.jrunalyser.ui.dock.ebus.TrackAdded;
import com.github.pfichtner.jrunalyser.ui.dock.ebus.TrackRemoved;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
public class Dock {
private static final I18N i18n = I18N
.builder(Dock.class)
.withParent(
com.github.pfichtner.jrunalyser.ui.base.UiPlugins.getI18n())
.build();
public static I18N getI18n() {
return i18n;
}
/**
* Listener that checks that all message sent over the event bus are
* annotated using @EventBusMessage.
*
* @author Peter Fichtner
*/
private static class CheckAnnotationListener {
private final Class<? extends Annotation> expected;
public CheckAnnotationListener(Class<? extends Annotation> expected) {
this.expected = expected;
}
@Subscribe
public void anyMessage(Object message) {
log.debug("EventBus message: {}", message); //$NON-NLS-1$
Class<? extends Object> msgType = message.getClass();
try {
checkState(msgType.isAnnotationPresent(this.expected),
"%s must be annotated using %s", msgType.getName(), //$NON-NLS-1$
this.expected.getName());
} catch (Exception e) {
showError(e);
}
}
}
private static final Logger log = LoggerFactory.getLogger(Dock.class);
private static class MyDataSource extends InMemoryDataSource {
private final File base;
public MyDataSource(File base) {
this.base = checkNotNull(base, "Directory must not be null"); //$NON-NLS-1$
}
@Override
public Track addTrack(Track track) {
File file = new File(this.base, createFileName(track) + ".gpx"); //$NON-NLS-1$
checkState(!file.exists(), "File %s already exists!", //$NON-NLS-1$
file.getName());
try {
GpxMarshaller.writeTrack(file, track);
return addTrack(file);
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
public Track addTrack(File file) throws IOException {
Track track = GpxUnmarshaller.loadTrack(file);
// recalc key
Track result = new DefaultTrack(new FileId(file),
track.getMetadata(), track.getWaypoints(),
track.getSegments(), track.getStatistics());
super.addTrack(result);
return result;
}
private String createFileName(Track track) {
checkState(
!checkNotNull(checkNotNull(track, "Track must not be null") //$NON-NLS-1$
.getTrackpoints(), "Waypoints must not be null").isEmpty(), //$NON-NLS-1$
"Waypoints must not be empty"); //$NON-NLS-1$
return formatGoogleStyle(track.getTrackpoints().get(0).getTime()
.longValue());
}
private String formatGoogleStyle(long value) {
return new SimpleDateFormat("dd_MM_yyyy HH_mm").format(new Date( //$NON-NLS-1$
value));
}
}
public static class FileId implements Id {
private final File file;
public FileId(File file) {
this.file = file;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((this.file == null) ? 0 : this.file.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
FileId other = (FileId) obj;
if (this.file == null) {
if (other.file != null)
return false;
} else if (!this.file.equals(other.file))
return false;
return true;
}
@Override
public String toString() {
return "FileId [file=" + this.file + "]"; //$NON-NLS-1$ //$NON-NLS-2$
}
}
private static final String TITLE = i18n
.getText("com.github.pfichtner.jrunalyser.ui.dock.Dock.title"); //$NON-NLS-1$
private static final String COMMON_WPTS = "common.wpts"; //$NON-NLS-1$
public static void main(String[] args) throws IOException,
InterruptedException, InvocationTargetException {
setupExceptionHandler();
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
init();
}
});
}
private static void init() {
final EventBus eventBus = new EventBus();
eventBus.register(new CheckAnnotationListener(EventBusMessage.class));
final DatasourceFascade dsf = createDatasourceFascade();
initInBackground(dsf);
dsf.addListener(new DatasourceFascadeListener() {
@Override
public void contentChanged(DatasourceFascadeEvent ev) {
Type type = ev.getType();
switch (type) {
case ADDED:
try {
// load it via dsf to ensure it has stats
eventBus.post(new TrackAdded(dsf.loadTrack(ev
.getTrack().getId())));
} catch (IOException e) {
throw Throwables.propagate(e);
}
break;
case REMOVED:
eventBus.post(new TrackRemoved(ev.getTrack()));
break;
// TODO We should fire TrackLoaded on MODIFIED
default:
break;
}
}
});
JFrame frame = new JFrame(TITLE);
final CControl control = new CControl(frame);
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
saveState(control);
((JFrame) e.getSource())
.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
});
control.setTheme(ThemeMap.KEY_ECLIPSE_THEME);
frame.add(control.getContentArea());
GridData defaultGridData = new DefaultGridData(1, 3, 3, 1);
CGrid grid = new CGrid(control);
Iterable<UiPlugin> plugins = loadPlugins();
for (UiPlugin plugin : plugins) {
log.info("Adding plugin type {}: {}", plugin.getClass().getName(), //$NON-NLS-1$
plugin);
register(plugin, eventBus, dsf, frame);
if (plugin instanceof DockPlugin) {
DockPlugin dockPlugin = (DockPlugin) plugin;
GridData gd = dockPlugin instanceof GridDataProvider ? ((GridDataProvider) dockPlugin)
.getGridData() : defaultGridData;
grid.add(gd.getGridX(), gd.getGridY(), gd.getGridWidth(),
gd.getGridHeight(), createDockable(dockPlugin));
}
}
control.getContentArea().deploy(grid);
RootMenuPiece menuBuilder = new RootMenuPiece(
i18n.getText("com.github.pfichtner.jrunalyser.ui.dock.Dock.mWindows.title"), //$NON-NLS-1$
false);
menuBuilder.add(new SingleCDockableListMenuPiece(control));
JMenuBar menuBar = new JMenuBar();
menuBar.add(createFileMenu(dsf, frame, eventBus));
menuBar.add(menuBuilder.getMenu());
MenuHack h = new MenuHack(eventBus);
menuBar.add(h.getSegmentMenu());
menuBar.add(h.getHighlightMenu());
frame.setJMenuBar(menuBar);
loadState(control);
final Dimension screenSize = Toolkit.getDefaultToolkit()
.getScreenSize();
frame.setBounds(20, 20, screenSize.width - 80, screenSize.height - 60);
// center
frame.setLocationRelativeTo(null);
frame.setVisible(true);
TaskManager tm = new TaskManagerImpl();
frame.setGlassPane(new TaskGlassPane(tm));
}
private static void initInBackground(final DatasourceFascade dsf) {
new Thread() {
{
start();
}
@Override
public void run() {
try {
Set<Id> trackIds = dsf.getTrackIds();
if (!trackIds.isEmpty()) {
{
Distance highest = greatestDistance(dsf, trackIds);
for (Distance distance : Distances
.distanceIterator(highest)) {
log.info("Precalculating {}", distance); //$NON-NLS-1$
dsf.listTracks(distance);
}
}
{
Duration highest = greatestDuration(dsf, trackIds);
for (Duration duration : Durations
.durationIterator(highest)) {
log.info("Precalculating {}", duration); //$NON-NLS-1$
dsf.listTracks(duration);
}
}
}
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
private Distance greatestDistance(final DatasourceFascade dsf,
Set<Id> trackIds) throws IOException {
return getMax(getStats(dsf, trackIds).transform(
Statisticss.distance));
}
private Duration greatestDuration(final DatasourceFascade dsf,
Set<Id> trackIds) throws IOException {
return getMax(getStats(dsf, trackIds).transform(
Statisticss.duration));
}
private <T extends Comparable<T>> T getMax(
FluentIterable<T> iterable) {
return Ordering.natural().max(iterable);
}
private FluentIterable<Statistics> getStats(
final DatasourceFascade dsf, Set<Id> trackIds)
throws IOException {
return FluentIterable.from(trackIds).transform(loadTrack(dsf))
.transform(StatisticsProviders.statistics);
}
private Function<Id, StatisticsProvider> loadTrack(
final DatasourceFascade dsf) {
return new Function<Id, StatisticsProvider>() {
@Override
public StatisticsProvider apply(Id id) {
try {
return dsf.loadTrack(id);
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
};
}
};
}
private static DatasourceFascade createDatasourceFascade() {
final File baseDir = new File(System.getProperty("user.home"), "gpx"); //$NON-NLS-1$ //$NON-NLS-2$
checkState(baseDir.exists() || baseDir.mkdirs(),
"Cannot create directory %s", baseDir); //$NON-NLS-1$
Datasource datasource = Suppliers.background(
new Supplier<Datasource>() {
@Override
public Datasource get() {
try {
return createDatasource(baseDir);
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
}, Datasource.class);
DataSourceDatasourceFascadeAdapter dsf0 = new DataSourceDatasourceFascadeAdapter(
datasource);
SerializatingDatasourceFascade dsf2 = new SerializatingDatasourceFascade(
baseDir, dsf0);
CachingDatasourceFascadeProxy dsf3 = new CachingDatasourceFascadeProxy(
dsf2);
StatCalculatorDatasourceFascade dsf4 = new StatCalculatorDatasourceFascade(
dsf3);
CachingDatasourceFascadeProxy dsf5 = new CachingDatasourceFascadeProxy(
dsf4);
return dsf5;
}
private static Datasource createDatasource(final File baseDir)
throws IOException {
MyDataSource dataSource = new MyDataSource(baseDir);
File cwps = new File(baseDir, COMMON_WPTS);
if (cwps.canRead()) {
for (WayPoint wayPoint : GpxUnmarshaller.loadTrack(cwps)
.getWaypoints()) {
dataSource.addCommonWaypoint(wayPoint);
}
}
try {
ExecutorService es = Executors.newFixedThreadPool(Runtime
.getRuntime().availableProcessors());
es.invokeAll(createCallables(dataSource, getFiles(dataSource)));
es.shutdown();
} catch (InterruptedException e) {
throw Throwables.propagate(e);
}
return dataSource;
}
private static File[] getFiles(MyDataSource dataSource) {
return dataSource.base.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile() && !file.getName().equals(COMMON_WPTS);
}
});
}
private static Collection<Callable<Track>> createCallables(
final MyDataSource dataSource, File[] listFiles) {
List<Callable<Track>> result = Lists
.newArrayListWithExpectedSize(listFiles.length);
for (final File file : listFiles) {
result.add(new Callable<Track>() {
@Override
public Track call() throws IOException {
log.debug("Loading {}", file); //$NON-NLS-1$
Track addedTrack;
addedTrack = dataSource.addTrack(file);
log.info("{} loaded", file); //$NON-NLS-1$
return addedTrack;
}
});
}
return result;
}
private static Iterable<UiPlugin> loadPlugins() {
return Lists
.newArrayList(ServiceLoader.load(UiPlugin.class).iterator());
}
private static boolean loadState(CControl control) {
File layoutFile = getLayoutFile();
if (!layoutFile.exists()) {
return false;
}
try {
control.readXML(layoutFile);
return true;
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
private static void saveState(CControl control) {
try {
control.writeXML(getLayoutFile());
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
private static File getLayoutFile() {
return new File(getSettingBaseDir(), "main-layout.xml"); //$NON-NLS-1$
}
private static File getSettingBaseDir() {
File baseDir = new File(System.getProperty("user.home"), "." //$NON-NLS-1$ //$NON-NLS-2$
+ TITLE.toLowerCase());
checkState(baseDir.exists() || baseDir.mkdirs(),
"Cannot create directory %s", baseDir); //$NON-NLS-1$
return baseDir;
}
private static void setupExceptionHandler() {
// TODO http://www.javaspecialists.eu/archive/Issue196.html
final UncaughtExceptionHandler exceptionHandler = new UncaughtExceptionHandler() {
public void uncaughtException(final Thread thread, final Throwable t) {
showError(t);
}
};
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
System.setProperty(
"sun.awt.exception.handler", exceptionHandler.getClass().getName()); //$NON-NLS-1$
}
private static void showError(final Throwable t) {
try {
TaskDialogs.showException(t);
} catch (final Throwable t2) {
/*
* don't let the Throwable get thrown out, will cause infinite
* looping!
*/
t2.printStackTrace();
}
}
private static JMenu createFileMenu(
final DatasourceFascade datasourceFascade, final JFrame parent,
final EventBus eventBus) {
JMenu jMenu = new JMenu(TITLE);
JMenuItem menuItem = new JMenuItem(
i18n.getText("com.github.pfichtner.jrunalyser.ui.dock.Dock.miAddGpx.title")); //$NON-NLS-1$
menuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ev) {
JFileChooser chooser = new JFileChooser();
chooser.setMultiSelectionEnabled(true);
if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
File[] selectedFiles = chooser.getSelectedFiles();
Component glassPane = parent.getGlassPane();
if (glassPane instanceof TaskGlassPane) {
TaskGlassPane taskGlassPane = (TaskGlassPane) glassPane;
TaskManager tm = taskGlassPane.getTaskManager();
int cpus = Runtime.getRuntime().availableProcessors();
List<List<File>> partitions = Lists.partition(
Arrays.asList(selectedFiles),
(selectedFiles.length + cpus - 1) / cpus);
for (final List<File> files : partitions) {
tm.create(
new AbstractTask<Void, Void>(
i18n.getText("com.github.pfichtner.jrunalyser.ui.dock.Dock.importDialog.title"), BLOCKING) { //$NON-NLS-1$
@Override
public Void execute(
Tracker<Void> tracker) {
int i = 0;
for (File file : files) {
tracker.setProgress(0,
files.size(), i++);
tracker.setPhase(i18n
.getText(
"com.github.pfichtner.jrunalyser.ui.dock.Dock.importDialog.format", file)); //$NON-NLS-1$
try {
datasourceFascade
.addTrack(GpxUnmarshaller
.loadTrack(file));
} catch (IOException e) {
throw Throwables
.propagate(e);
}
}
return null;
}
}).execute();
}
} else {
for (File selectedFile : selectedFiles) {
try {
datasourceFascade.addTrack(GpxUnmarshaller
.loadTrack(selectedFile));
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
}
}
}
});
jMenu.add(menuItem);
return jMenu;
}
private static DefaultSingleCDockable createDockable(final DockPlugin plugin) {
DefaultSingleCDockable dockable = new DefaultSingleCDockable(
plugin.getId(), plugin.getTitle(), plugin.getPanel());
dockable.setCloseable(true);
return dockable;
}
private static <T> T register(final T plugin, EventBus eventBus,
DatasourceFascade dsf, Component parent) {
eventBus.register(plugin);
Injector.inject(plugin, EventBus.class, eventBus);
Injector.inject(plugin, DatasourceFascade.class, dsf);
Injector.inject(plugin, Component.class, parent);
return plugin;
}
}