package com.github.pfichtner.jrunalyser.ui.mapprofile; import static com.github.pfichtner.jrunalyser.base.data.stat.Predicates.LinkedWayPoints.hasLink; import static com.google.common.collect.Iterables.filter; import java.awt.BasicStroke; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.math.BigDecimal; import java.util.List; import java.util.concurrent.TimeUnit; import javax.swing.ButtonGroup; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JRadioButtonMenuItem; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartMouseEvent; import org.jfree.chart.ChartMouseListener; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.DatasetRenderingOrder; import org.jfree.chart.plot.Plot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.PlotRenderingInfo; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.AbstractRenderer; import org.jfree.chart.renderer.xy.StandardXYItemRenderer; import org.jfree.chart.renderer.xy.XYAreaRenderer; import org.jfree.chart.util.RelativeDateFormat; import org.jfree.data.Range; import org.jfree.data.xy.DefaultXYDataset; import org.jfree.data.xy.XYDataset; import com.github.pfichtner.jrunalyser.base.ViewMode; import com.github.pfichtner.jrunalyser.base.data.DistanceUnit; import com.github.pfichtner.jrunalyser.base.data.LinkedTrackPoint; import com.github.pfichtner.jrunalyser.base.data.Pace; import com.github.pfichtner.jrunalyser.base.data.Speed; import com.github.pfichtner.jrunalyser.base.data.WayPoint; import com.github.pfichtner.jrunalyser.base.data.stat.Predicates; import com.github.pfichtner.jrunalyser.base.data.track.Track; import com.github.pfichtner.jrunalyser.base.stat.Boxplot; import com.github.pfichtner.jrunalyser.base.stat.Boxplot.InterquatileRange; import com.github.pfichtner.jrunalyser.di.Inject; import com.github.pfichtner.jrunalyser.ui.base.AbstractUiPlugin; import com.github.pfichtner.jrunalyser.ui.base.GridDataProvider; import com.github.pfichtner.jrunalyser.ui.base.i18n.I18N; import com.github.pfichtner.jrunalyser.ui.dock.ebus.MouseOverWaypoint; import com.github.pfichtner.jrunalyser.ui.dock.ebus.TrackLoaded; import com.github.pfichtner.jrunalyser.ui.mapprofile.config.DatasetConfig; import com.github.pfichtner.jrunalyser.ui.mapprofile.config.DatasetConfigDelegate; import com.github.pfichtner.jrunalyser.ui.mapprofile.config.DefaultDatasetConfig; import com.github.pfichtner.jrunalyser.ui.mapprofile.config.MovingAverageConfigDecorator; import com.github.pfichtner.jrunalyser.ui.mapprofile.config.StrokeRendererConfigDecorator; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.common.primitives.Doubles; public class MapProfilePlugin extends AbstractUiPlugin implements GridDataProvider { private static final I18N i18n = new I18N.Builder(MapProfilePlugin.class) .withParent( com.github.pfichtner.jrunalyser.ui.base.UiPlugins.getI18n()) .build(); private EventBus eventBus; private final static List<DatasetConfig> configs = ImmutableList .of(createElevationConfig(), createPaceConfig(), createGradientConfig()); private static final int MOVING_AVG_VALUE = 100; private static final int MOVING_AVG_SKIP = 0; private final JFreeChart chart; private final ChartPanel chartPanel; private ViewMode viewMode = ViewMode.BY_DISTANCE; private Function<LinkedTrackPoint, ? extends Number> xFunc = createXFunc(this.viewMode); private String yAxisLabel = createYAxisLabel(this.viewMode); private Track track; public MapProfilePlugin() { XYDataset empty = new DefaultXYDataset(); this.chart = ChartFactory.createXYLineChart(null, this.yAxisLabel, "", //$NON-NLS-1$ empty, PlotOrientation.VERTICAL, true, true, false); Plot plot = this.chart.getPlot(); ((XYPlot) plot).setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD); this.chartPanel = new ChartPanel(this.chart); this.chartPanel.getPopupMenu().add(createMenu()); this.chartPanel.addChartMouseListener(new ChartMouseListener() { @Override public void chartMouseMoved(ChartMouseEvent mouseChartEvent) { if (MapProfilePlugin.this.track == null) return; Plot plot = mouseChartEvent.getChart().getPlot(); if (plot instanceof XYPlot) { XYPlot xyPlot = (XYPlot) plot; double point = MapProfilePlugin.this.chartPanel .translateScreenToJava2D( mouseChartEvent.getTrigger().getPoint()) .getX(); PlotRenderingInfo plotInfo = MapProfilePlugin.this.chartPanel .getChartRenderingInfo().getPlotInfo(); double xPos = xyPlot.getDomainAxis().java2DToValue(point, plotInfo.getDataArea(), xyPlot.getDomainAxisEdge()); WayPoint selected = searchWaypoint( filter(MapProfilePlugin.this.track.getTrackpoints(), Predicates.LinkedWayPoints.hasLink()), MapProfilePlugin.this.xFunc, xPos); if (selected != null) { MapProfilePlugin.this.eventBus .post(new MouseOverWaypoint( MapProfilePlugin.this.track, selected)); } } } @Override public void chartMouseClicked(ChartMouseEvent chartMouseEvent) { // nothing to do } private LinkedTrackPoint searchWaypoint( Iterable<? extends LinkedTrackPoint> wps, Function<LinkedTrackPoint, ? extends Number> xFunc, double xPos) { BigDecimal actXpos = BigDecimal.ZERO; BigDecimal searchedXpos = new BigDecimal(xPos); for (LinkedTrackPoint wp : wps) { actXpos = actXpos.add(new BigDecimal(xFunc.apply(wp) .toString())); if (actXpos.compareTo(searchedXpos) >= 0) { return wp; } } return null; } }); } private JMenu createMenu() { JMenu jMenu = new JMenu( i18n.getText("com.github.pfichtner.jrunalyser.ui.mapprofile.MapProfilePlugin.xaxis")); //$NON-NLS-1$ JMenuItem mi1 = new JRadioButtonMenuItem( i18n.getText("com.github.pfichtner.jrunalyser.ui.mapprofile.MapProfilePlugin.distance"), //$NON-NLS-1$ this.viewMode == ViewMode.BY_DISTANCE); mi1.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (((JRadioButtonMenuItem) e.getSource()).isSelected()) { setViewMode(ViewMode.BY_DISTANCE); NumberAxis newAxis = new NumberAxis(); MapProfilePlugin.this.chart.getXYPlot().setDomainAxis( newAxis); } } }); jMenu.add(mi1); JMenuItem mi2 = new JRadioButtonMenuItem( i18n.getText("com.github.pfichtner.jrunalyser.ui.mapprofile.MapProfilePlugin.duration"), //$NON-NLS-1$ this.viewMode == ViewMode.BY_DURATION); mi2.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (((JRadioButtonMenuItem) e.getSource()).isSelected()) { setViewMode(ViewMode.BY_DURATION); } } }); jMenu.add(mi2); ButtonGroup group = new ButtonGroup(); group.add(mi1); group.add(mi2); return jMenu; } public void setViewMode(ViewMode viewMode) { this.viewMode = viewMode; this.xFunc = createXFunc(viewMode); this.yAxisLabel = createYAxisLabel(viewMode); if (this.track != null) { createDatasetsFromTrack(); } if (this.chartPanel != null) { MapProfilePlugin.this.chart.getXYPlot().setDomainAxis( createYAxisRenderer(viewMode, this.yAxisLabel)); this.chart.fireChartChanged(); } } @Override public String getTitle() { return getText("com.github.pfichtner.jrunalyser.ui.mapprofile.MapProfilePlugin.title"); //$NON-NLS-1$ } @Override public JPanel getPanel() { return this.chartPanel; } @Subscribe public void setTrack(TrackLoaded message) { this.track = message.getTrack(); DatasetConfig primary = getIndex(configs, 0); final XYPlot plot = this.chart.getXYPlot(); plot.setRangeAxis(primary.getIndex(), primary.createNumberAxis(this.track)); createDatasetsFromTrack(); for (DatasetConfig config : configs) { plot.setRenderer(config.getIndex(), config.getRenderer()); } } private void createDatasetsFromTrack() { final XYPlot plot = this.chart.getXYPlot(); for (DatasetConfig config : configs) { int idx = config.getIndex(); plot.setRangeAxis(idx, config.createNumberAxis(this.track)); plot.setDataset(idx, config.createDataset(this.track, this.xFunc)); plot.mapDatasetToRangeAxis(idx, idx); } } private static Function<LinkedTrackPoint, ? extends Number> createXFunc( ViewMode vm) { switch (vm) { case BY_DISTANCE: return createDistanceFunction(DistanceUnit.METERS); case BY_DURATION: return createDurationFunction(TimeUnit.MILLISECONDS); default: throw new IllegalStateException("Unknown mode " + vm); //$NON-NLS-1$ } } private static String createYAxisLabel(ViewMode vm) { switch (vm) { case BY_DISTANCE: return i18n .getText("com.github.pfichtner.jrunalyser.ui.mapprofile.MapProfilePlugin.distanceInMeter"); //$NON-NLS-1$ case BY_DURATION: return i18n .getText("com.github.pfichtner.jrunalyser.ui.mapprofile.MapProfilePlugin.durationInSecs"); //$NON-NLS-1$ default: throw new IllegalStateException("Unknown mode " + vm); //$NON-NLS-1$ } } private static ValueAxis createYAxisRenderer(ViewMode vm, String label) { switch (vm) { case BY_DISTANCE: return new NumberAxis(label); case BY_DURATION: DateAxis domainAxis = new DateAxis(label); domainAxis.setDateFormatOverride(new RelativeDateFormat()); return domainAxis; default: throw new IllegalStateException("Unknown mode " + vm); //$NON-NLS-1$ } } private static DatasetConfig createElevationConfig() { DatasetConfig config = new DatasetConfigDelegate( new DefaultDatasetConfig.Builder(0) .description( getText("com.github.pfichtner.jrunalyser.ui.mapprofile.MapProfilePlugin.elevation")) //$NON-NLS-1$ .yFunc(createElevationFunction()) .renderer( rendererColor(new XYAreaRenderer( XYAreaRenderer.AREA), Color.lightGray .darker())).build()) { @Override public NumberAxis createNumberAxis(Track track) { NumberAxis axis = super.createNumberAxis(track); axis.setAutoRangeIncludesZero(false); WayPoint maxEle = track.getStatistics().getMaxElevation(); WayPoint minEle = track.getStatistics().getMinElevation(); if (maxEle != null && maxEle.getElevation() != null && minEle != null && minEle.getElevation() != null) { int max = maxEle.getElevation().intValue(); int min = minEle.getElevation().intValue(); int add = (int) (((double) (max - min)) / 3); axis.setRange(new Range((double) min - add, (double) max + add)); } return axis; } }; return new StrokeRendererConfigDecorator( new MovingAverageConfigDecorator(config, "", MOVING_AVG_VALUE, MOVING_AVG_SKIP), //$NON-NLS-1$ new BasicStroke(2)); } private static <T extends AbstractRenderer> T rendererColor(T renderer, Color color) { renderer.setSeriesPaint(0, color); return renderer; } private static DatasetConfig createPaceConfig() { final Function<LinkedTrackPoint, Double> paceFunction = createPaceFunction( TimeUnit.MINUTES, DistanceUnit.KILOMETERS); DatasetConfig config = new DatasetConfigDelegate( new DefaultDatasetConfig.Builder(1) .description( i18n.getText("com.github.pfichtner.jrunalyser.ui.mapprofile.MapProfilePlugin.pace")) //$NON-NLS-1$ .yFunc(paceFunction) .renderer( rendererColor(new StandardXYItemRenderer(), Color.ORANGE)).build()) { @Override public NumberAxis createNumberAxis(Track track) { NumberAxis axis = super.createNumberAxis(track); axis.setInverted(true); Boxplot boxplot = new Boxplot(Doubles.toArray(FluentIterable .from(track.getTrackpoints()).filter(hasLink()) .transform(paceFunction).toList())); InterquatileRange innerFences = boxplot.innerFences(); axis.setRange(new Range(innerFences.getLower(), innerFences .getUpper())); axis.setAutoRangeIncludesZero(false); return axis; } }; return new StrokeRendererConfigDecorator( new MovingAverageConfigDecorator(config, "", MOVING_AVG_VALUE, MOVING_AVG_SKIP), //$NON-NLS-1$ new BasicStroke(2)); } private static DatasetConfig createGradientConfig() { DatasetConfig config = new DatasetConfigDelegate( new DefaultDatasetConfig.Builder(2) .description( i18n.getText("com.github.pfichtner.jrunalyser.ui.mapprofile.MapProfilePlugin.gradient")) //$NON-NLS-1$ .yFunc(createGradientFunction(DistanceUnit.METERS)) .renderer( rendererColor(new StandardXYItemRenderer(), Color.RED)).build()) { @Override public NumberAxis createNumberAxis(Track track) { NumberAxis axis = super.createNumberAxis(track); axis.setAutoRange(true); return axis; } }; return new MovingAverageConfigDecorator(config, "", MOVING_AVG_VALUE, MOVING_AVG_SKIP); //$NON-NLS-1$ } private static DatasetConfig getIndex(List<DatasetConfig> configs, int val) { for (DatasetConfig config : configs) { if (config.getIndex() == val) { return config; } } return null; } private static Function<LinkedTrackPoint, Integer> createElevationFunction() { return new Function<LinkedTrackPoint, Integer>() { @Override public Integer apply(LinkedTrackPoint wp) { return wp.getElevation(); } }; } private static Function<Speed, Pace> createSpeed2PaceFunction( final TimeUnit minutes, final DistanceUnit kilometers) { return new Function<Speed, Pace>() { @Override public Pace apply(Speed speed) { return speed.toPace(minutes, kilometers); } }; } private static Function<LinkedTrackPoint, Double> createPaceFunction( final TimeUnit minutes, final DistanceUnit kilometers) { return Functions .compose( new Function<Pace, Double>() { @Override public Double apply(Pace pace) { return Double.valueOf(pace.getValue( pace.getTimeUnit(), pace.getDistanceUnit())); } }, Functions .compose( createSpeed2PaceFunction(minutes, kilometers), com.github.pfichtner.jrunalyser.base.data.stat.Functions.Links.speedOfLink)); } private static Function<LinkedTrackPoint, Double> createDistanceFunction( final DistanceUnit distanceUnit) { return new Function<LinkedTrackPoint, Double>() { @Override public Double apply(LinkedTrackPoint wp) { return Double.valueOf(wp.getLink().getDistance() .getValue(distanceUnit)); } }; } private static Function<LinkedTrackPoint, Double> createDurationFunction( final TimeUnit timeUnit) { return new Function<LinkedTrackPoint, Double>() { @Override public Double apply(LinkedTrackPoint tpd) { return Double.valueOf(tpd.getLink().getDuration() .getValue(timeUnit)); } }; } private static Function<LinkedTrackPoint, Double> createGradientFunction( final DistanceUnit distanceUnit) { return new Function<LinkedTrackPoint, Double>() { @Override public Double apply(LinkedTrackPoint tpd) { return Double.valueOf(tpd.getLink().getGradient() .convertTo(distanceUnit).getValue()); } }; } @Inject public void setEventBus(EventBus eventBus) { this.eventBus = eventBus; } public static String getText(String key, Object... args) { return i18n.getText(key, args); } }