X-Git-Url: http://gitweb.fperrin.net/?p=GpsPrune.git;a=blobdiff_plain;f=src%2Ftim%2Fprune%2Fgui%2Fprofile%2FProfileChart.java;fp=src%2Ftim%2Fprune%2Fgui%2Fprofile%2FProfileChart.java;h=466fda2175de2418569297c0722c6cfb1a2fd3c0;hp=0000000000000000000000000000000000000000;hb=ce6f2161b8596f7018d6a76bff79bc9e571f35fd;hpb=2d8cb72e84d5cc1089ce77baf1e34ea3ea2f8465 diff --git a/src/tim/prune/gui/profile/ProfileChart.java b/src/tim/prune/gui/profile/ProfileChart.java new file mode 100644 index 0000000..466fda2 --- /dev/null +++ b/src/tim/prune/gui/profile/ProfileChart.java @@ -0,0 +1,584 @@ +package tim.prune.gui.profile; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; + +import tim.prune.I18nManager; +import tim.prune.config.ColourScheme; +import tim.prune.config.Config; +import tim.prune.data.Field; +import tim.prune.data.FieldList; +import tim.prune.data.TrackInfo; +import tim.prune.gui.GenericDisplay; + +/** + * Chart component for the profile display + */ +public class ProfileChart extends GenericDisplay implements MouseListener +{ + /** Inner class to handle popup menu clicks */ + class MenuClicker implements ActionListener + { + private Field _field = null; + MenuClicker(Field inField) {_field = inField;} + /** React to menu click by changing the field */ + public void actionPerformed(ActionEvent arg0) { + changeView(_field); + } + } + + /** Inner class to remember a single index */ + class PointIndex + { + public int index = -1; + public boolean hasValue = false; + public PointIndex() + { + index = -1; + hasValue = false; + } + /** Set a single value */ + public void set(int inValue) + { + index = inValue; + hasValue = (inValue != -1); + } + /** Add an index to the minimum calculation */ + public void setMin(PointIndex other) + { + if (!other.hasValue) {return;} + if (!hasValue) { + index = other.index; + hasValue = other.hasValue; + } + else { + index = Math.min(index, other.index); + } + } + /** Add an index to the maximum calculation */ + public void setMax(PointIndex other) + { + if (!other.hasValue) {return;} + if (!hasValue) { + index = other.index; + hasValue = other.hasValue; + } + else { + index = Math.max(index, other.index); + } + } + /** @return true if two Indexes are equal */ + public boolean equals(PointIndex other) + { + if (!hasValue || !other.hasValue) { + return hasValue == other.hasValue; + } + return index == other.index; + } + } + + /** Inner class to remember previous chart parameters */ + class ChartParameters + { + public PointIndex selectedPoint = new PointIndex(); + public PointIndex rangeStart = new PointIndex(), rangeEnd = new PointIndex(); + public void clear() + { + selectedPoint.hasValue = false; + rangeStart.hasValue = false; + rangeEnd.hasValue = false; + } + /** Get the minimum index which has changed between two sets of parameters */ + public int getMinChangedIndex(ChartParameters other) + { + PointIndex minIndex = new PointIndex(); + if (!selectedPoint.equals(other.selectedPoint)) { + minIndex.setMin(selectedPoint); + minIndex.setMin(other.selectedPoint); + } + if (!rangeStart.equals(other.rangeStart)) { + minIndex.setMin(rangeStart); + minIndex.setMin(other.rangeStart); + } + if (!rangeEnd.equals(other.rangeEnd)) { + minIndex.setMin(rangeEnd); + minIndex.setMin(other.rangeEnd); + } + return minIndex.index; + } + /** Get the maximum index which has changed between two sets of parameters */ + public int getMaxChangedIndex(ChartParameters other) + { + PointIndex maxIndex = new PointIndex(); + if (!selectedPoint.equals(other.selectedPoint)) { + maxIndex.setMax(selectedPoint); + maxIndex.setMax(other.selectedPoint); + } + if (!rangeStart.equals(other.rangeStart)) { + maxIndex.setMax(rangeStart); + maxIndex.setMax(other.rangeStart); + } + if (!rangeEnd.equals(other.rangeEnd)) { + maxIndex.setMax(rangeEnd); + maxIndex.setMax(other.rangeEnd); + } + return maxIndex.index; + } + /** @return true if the parameters are completely empty (cleared) */ + public boolean isEmpty() + { + return !selectedPoint.hasValue && !rangeStart.hasValue && !rangeEnd.hasValue; + } + } + + /** Current scale factor in x direction*/ + private double _xScaleFactor = 0.0; + /** Data to show on chart */ + private ProfileData _data = null; + /** Label for chart type, units */ + private JLabel _label = null; + /** Right-click popup menu */ + private JPopupMenu _popup = null; + /** Parameters last time chart was drawn */ + private ChartParameters _previousParameters = new ChartParameters(); + + /** Possible scales to use */ + private static final int[] LINE_SCALES = {10000, 5000, 2000, 1000, 500, 200, 100, 50, 10, 5, 2, 1}; + /** Border width around black line */ + private static final int BORDER_WIDTH = 6; + /** Minimum size for profile chart in pixels */ + private static final Dimension MINIMUM_SIZE = new Dimension(200, 110); + /** Colour to use for text if no data found */ + private static final Color COLOR_NODATA_TEXT = Color.GRAY; + + + /** + * Constructor + * @param inTrackInfo Track info object + */ + public ProfileChart(TrackInfo inTrackInfo) + { + super(inTrackInfo); + _data = new AltitudeData(inTrackInfo.getTrack()); + addMouseListener(this); + setLayout(new FlowLayout(FlowLayout.LEFT)); + _label = new JLabel("Altitude"); // text will be replaced later + add(_label); + makePopup(); + } + + + /** + * Override minimum size method to restrict slider + */ + public Dimension getMinimumSize() + { + return MINIMUM_SIZE; + } + + /** + * Override paint method to draw map + * @param g Graphics object + */ + public void paint(Graphics g) + { + super.paint(g); + // Set antialiasing depending on Config + ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + Config.getConfigBoolean(Config.KEY_ANTIALIAS) ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); + ColourScheme colourScheme = Config.getColourScheme(); + paintBackground(g, colourScheme); + + if (_track == null || _track.getNumPoints() <= 0) + { + return; + } + + _label.setText(_data.getLabel()); + int width = getWidth(); + int height = getHeight(); + + // Set up colours + final Color barColour = colourScheme.getColour(ColourScheme.IDX_POINT); + final Color rangeColour = colourScheme.getColour(ColourScheme.IDX_SELECTION); + final Color currentColour = colourScheme.getColour(ColourScheme.IDX_PRIMARY); + final Color secondColour = colourScheme.getColour(ColourScheme.IDX_SECONDARY); + final Color lineColour = colourScheme.getColour(ColourScheme.IDX_LINES); + + // message if no data for the current field in track + if (!_data.hasData()) + { + g.setColor(lineColour); + g.drawString(I18nManager.getText(_data.getNoDataKey()), 50, (height+_label.getHeight())/2); + paintChildren(g); + return; + } + + // Find minimum and maximum values to plot + double minValue = _data.getMinValue(); + double maxValue = _data.getMaxValue(); + if (maxValue <= minValue) {maxValue = minValue + 1; minValue--;} + + final int numPoints = _track.getNumPoints(); + _xScaleFactor = 1.0 * (width - 2 * BORDER_WIDTH - 1) / numPoints; + int usableHeight = height - 2 * BORDER_WIDTH - _label.getHeight(); + double yScaleFactor = 1.0 * usableHeight / (maxValue - minValue); + int barWidth = (int) (_xScaleFactor + 1.0); + int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex(); + // selection start, end + int selectionStart = -1, selectionEnd = -1; + if (_trackInfo.getSelection().hasRangeSelected()) { + selectionStart = _trackInfo.getSelection().getStart(); + selectionEnd = _trackInfo.getSelection().getEnd(); + } + + int y = 0; + double value = 0.0; + // horizontal lines for scale - set to round numbers eg 500 + final int lineScale = getLineScale(minValue, maxValue); + double scaleValue = Math.ceil(minValue/lineScale) * lineScale; + final int zeroY = height - BORDER_WIDTH - (int) (yScaleFactor * (0.0 - minValue)); + + g.setColor(lineColour); + if (lineScale >= 1) + { + while (scaleValue < maxValue) + { + y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue)); + g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y); + scaleValue += lineScale; + } + } + else if (minValue < 0.0) + { + // just draw zero line + y = zeroY; + g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y); + } + + try + { + // loop through points + g.setColor(barColour); + for (int p = 0; p < numPoints; p++) + { + if (p == selectionStart) + g.setColor(rangeColour); + else if (p == (selectionEnd+1)) + g.setColor(barColour); + + final int x = (int) (_xScaleFactor * p) + 1; + if (_data.hasData(p)) + { + value = _data.getData(p); + // Normal case is the minimum value greater than zero + if (minValue >= 0) + { + y = (int) (yScaleFactor * (value - minValue)); + g.fillRect(BORDER_WIDTH+x, height-BORDER_WIDTH - y, barWidth, y); + } + else if (value >= 0.0) { + // Bar upwards from the zero line + y = height-BORDER_WIDTH - (int) (yScaleFactor * (value - minValue)); + g.fillRect(BORDER_WIDTH+x, y, barWidth, zeroY - y); + } + else { + // Bar downwards from the zero line + int barHeight = (int) (yScaleFactor * value); + g.fillRect(BORDER_WIDTH+x, zeroY, barWidth, -barHeight); + } + } + } + + // current point (make sure it's drawn last) + if (selectedPoint >= 0) + { + final int sel_x = (int) (_xScaleFactor * selectedPoint) + 1; + g.setColor(secondColour); + g.fillRect(BORDER_WIDTH + sel_x, height-usableHeight-BORDER_WIDTH+1, barWidth, usableHeight-2); + if (_data.hasData(selectedPoint)) + { + g.setColor(currentColour); + value = _data.getData(selectedPoint); + y = (int) (yScaleFactor * (value - minValue)); + g.fillRect(BORDER_WIDTH + sel_x, height-BORDER_WIDTH - y, barWidth, y); + } + } + } + catch (NullPointerException npe) { // ignore, probably due to data being changed + } + // Draw numbers on top of the graph to mark scale + if (lineScale >= 1) + { + int textHeight = g.getFontMetrics().getHeight(); + scaleValue = (int) (minValue / lineScale + 1) * lineScale; + if (minValue < 0.0) {scaleValue -= lineScale;} + y = 0; + g.setColor(currentColour); + while (scaleValue < maxValue) + { + y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue)); + // Limit y so String isn't above border + if (y < (BORDER_WIDTH + textHeight)) { + y = BORDER_WIDTH + textHeight; + } + g.drawString(""+(int)scaleValue, BORDER_WIDTH + 5, y); + scaleValue += lineScale; + } + } + // Paint label on top + paintChildren(g); + } + + + /** + * Paint the background for the chart + * @param inG graphics object + * @param inColourScheme colour scheme + */ + private void paintBackground(Graphics inG, ColourScheme inColourScheme) + { + final int width = getWidth(); + final int height = getHeight(); + // Get colours + final Color borderColour = inColourScheme.getColour(ColourScheme.IDX_BORDERS); + final Color backgroundColour = inColourScheme.getColour(ColourScheme.IDX_BACKGROUND); + // background + inG.setColor(backgroundColour); + inG.fillRect(0, 0, width, height); + if (width < 2*BORDER_WIDTH || height < 2*BORDER_WIDTH) return; + // Display message if no data to be displayed + if (_track == null || _track.getNumPoints() <= 0) + { + inG.setColor(COLOR_NODATA_TEXT); + inG.drawString(I18nManager.getText("display.nodata"), 50, height/2); + } + else + { + inG.setColor(borderColour); + inG.drawRect(BORDER_WIDTH, BORDER_WIDTH + _label.getHeight(), + width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH-_label.getHeight()); + } + } + + /** + * Make the popup menu for right-clicking the chart + */ + private synchronized void makePopup() + { + _popup = new JPopupMenu(); + JMenuItem altItem = new JMenuItem(I18nManager.getText("fieldname.altitude")); + altItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + changeView(Field.ALTITUDE); + }}); + _popup.add(altItem); + JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed")); + speedItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + changeView(Field.SPEED); + }}); + _popup.add(speedItem); + JMenuItem vertSpeedItem = new JMenuItem(I18nManager.getText("fieldname.verticalspeed")); + vertSpeedItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + changeView(Field.VERTICAL_SPEED); + }}); + _popup.add(vertSpeedItem); + // Go through track's master field list, see if any other fields to list + boolean addSeparator = true; + FieldList fields = _track.getFieldList(); + for (int i=0; i 10) return -1; + // If more than 1 line then use this scale + if (numLines > 1) return scale; + } + // no suitable scale found so just use minimum + return LINE_SCALES[numScales-1]; + } + + + /** + * Method to inform map that data has changed + */ + public void dataUpdated(byte inUpdateType) + { + // Try not to recalculate all the values unless necessary + if (inUpdateType != SELECTION_CHANGED) + { + _data.init(Config.getUnitSet()); + _previousParameters.clear(); + } + // Update the menu if necessary + if ((inUpdateType & DATA_ADDED_OR_REMOVED) > 0) { + makePopup(); + } + if (inUpdateType == SELECTION_CHANGED) { + triggerPartialRepaint(); + } + else + { + repaint(); + } + } + + /** + * For performance reasons, only repaint the part of the graphics affected by + * the change in selection + */ + private void triggerPartialRepaint() + { + ChartParameters currentParameters = new ChartParameters(); + currentParameters.selectedPoint.set(_trackInfo.getSelection().getCurrentPointIndex()); + if (_trackInfo.getSelection().hasRangeSelected()) + { + currentParameters.rangeStart.set(_trackInfo.getSelection().getStart()); + currentParameters.rangeEnd.set(_trackInfo.getSelection().getEnd()); + } + + int minPointIndex = currentParameters.getMinChangedIndex(_previousParameters); + minPointIndex = Math.max(minPointIndex, 0); + int maxPointIndex = currentParameters.getMaxChangedIndex(_previousParameters); + if (maxPointIndex < minPointIndex) { + maxPointIndex = _trackInfo.getTrack().getNumPoints() - 1; + } + // System.out.println("Redraw from index: " + minPointIndex + " to " + maxPointIndex); + _previousParameters = currentParameters; + final int region_x = (int) (_xScaleFactor * minPointIndex) + BORDER_WIDTH; + final int region_width = (int) (_xScaleFactor * (maxPointIndex-minPointIndex+2)) + 2; + repaint(region_x, 0, region_width, getHeight()); + } + + /** + * React to click on profile display + */ + public void mouseClicked(MouseEvent e) + { + if (_track == null || _track.getNumPoints() < 1) {return;} + // left clicks + if (!e.isMetaDown()) + { + int xClick = e.getX(); + int yClick = e.getY(); + // Check click is within main area (not in border) + if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH) + && yClick < (getHeight() - BORDER_WIDTH)) + { + // work out which data point is nearest and select it + int pointNum = (int) ((e.getX() - BORDER_WIDTH) / _xScaleFactor); + // If shift clicked, then extend selection + if (e.isShiftDown()) { + _trackInfo.extendSelection(pointNum); + } + else { + _trackInfo.selectPoint(pointNum); + } + } + } + else { + // right clicks + _popup.show(this, e.getX(), e.getY()); + } + } + + /** + * Called by clicking on popup menu to change the view + * @param inField field to show + */ + private void changeView(Field inField) + { + if (inField == Field.ALTITUDE) + { + if (!(_data instanceof AltitudeData)) { + _data = new AltitudeData(_track); + } + } + else if (inField == Field.SPEED) { + if (!(_data instanceof SpeedData)) { + _data = new SpeedData(_track); + } + } + else if (inField == Field.VERTICAL_SPEED) { + if (!(_data instanceof VerticalSpeedData)) { + _data = new VerticalSpeedData(_track); + } + } + else + { + if (!(_data instanceof ArbitraryData) || ((ArbitraryData)_data).getField() != inField) { + _data = new ArbitraryData(_track, inField); + } + } + _data.init(Config.getUnitSet()); + repaint(); + } + + /** + * mouse enter events ignored + */ + public void mouseEntered(MouseEvent e) + {} + + /** + * mouse exit events ignored + */ + public void mouseExited(MouseEvent e) + {} + + /** + * ignore mouse pressed for now too + */ + public void mousePressed(MouseEvent e) + {} + + /** + * and also ignore mouse released + */ + public void mouseReleased(MouseEvent e) + {} +}