// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.elevation.gui; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import javax.swing.ComboBoxModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.event.ListDataListener; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.SystemOfMeasurement; import org.openstreetmap.josm.data.gpx.GpxData; import org.openstreetmap.josm.gui.dialogs.ToggleDialog; import org.openstreetmap.josm.gui.layer.GpxLayer; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; import org.openstreetmap.josm.plugins.elevation.ElevationHelper; import org.openstreetmap.josm.plugins.elevation.IElevationModel; import org.openstreetmap.josm.plugins.elevation.IElevationModelListener; import org.openstreetmap.josm.plugins.elevation.IElevationProfile; import org.openstreetmap.josm.plugins.elevation.gpx.ElevationModel; import org.openstreetmap.josm.tools.Shortcut; /** * @author Oliver Wieland <oliver.wieland@online.de> * Implements a JOSM ToggleDialog to show the elevation profile. It monitors the * connection between layer and elevation profile. */ public class ElevationProfileDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener, ComponentListener { private static final String EMPTY_DATA_STRING = "-"; private static final long serialVersionUID = -868463893732535577L; /* Elevation profile instance */ private IElevationModel model; /* GPX data */ private GpxLayer activeLayer = null; private final HashMap<GpxLayer, ElevationModel> layerMap = new HashMap<>(); /* UI elements */ private final ElevationProfilePanel profPanel; private final JLabel minHeightLabel; private final JLabel maxHeightLabel; private final JLabel avrgHeightLabel; private final JLabel elevationGainLabel; private final JLabel totalTimeLabel; private final JLabel distLabel; private final JComboBox<IElevationProfile> trackCombo; private final JButton zoomButton; /* Listener to the elevation model */ private final List<IElevationModelListener> listeners = new ArrayList<>(); /** * Corresponding layer instance within map view. */ private ElevationProfileLayer profileLayer; /** * Default constructor */ public ElevationProfileDialog() { this(tr("Elevation Profile"), "elevation", tr("Open the elevation profile window."), null, 200, true); } /** * Constructor (see below) */ public ElevationProfileDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight) { this(name, iconName, tooltip, shortcut, preferredHeight, false); } /** * Constructor * * @param name * the name of the dialog * @param iconName * the name of the icon to be displayed * @param tooltip * the tool tip * @param shortcut * the shortcut * @param preferredHeight * the preferred height for the dialog * @param defShow * if the dialog should be shown by default, if there is no * preference */ public ElevationProfileDialog(String name, String iconName, String tooltip, Shortcut shortcut, int preferredHeight, boolean defShow) { super(name, iconName, tooltip, shortcut, preferredHeight, defShow); // create model model = new ElevationModel(); // top panel JPanel rootPanel = new JPanel(); GridLayout gridLayout1 = new GridLayout(2, 1); rootPanel.setLayout(gridLayout1); // statistics panel JPanel statPanel = new JPanel(); GridLayout gridLayoutStat = new GridLayout(2, 6); statPanel.setLayout(gridLayoutStat); // first row: Headlines with bold font String[] labels = new String[]{tr("Min"), tr("Avrg"), tr("Max"), tr("Dist"), tr("Gain"), tr("Time")}; for (int i = 0; i < labels.length; i++) { JLabel lbl = new JLabel(labels[i]); lbl.setFont(getFont().deriveFont(Font.BOLD)); statPanel.add(lbl); } // second row minHeightLabel = new JLabel("0 m"); statPanel.add(minHeightLabel); avrgHeightLabel = new JLabel("0 m"); statPanel.add(avrgHeightLabel); maxHeightLabel = new JLabel("0 m"); statPanel.add(maxHeightLabel); distLabel = new JLabel("0 km"); statPanel.add(distLabel); elevationGainLabel = new JLabel("0 m"); statPanel.add(elevationGainLabel); totalTimeLabel = new JLabel("0"); statPanel.add(totalTimeLabel); // track selection panel JPanel trackPanel = new JPanel(); FlowLayout fl = new FlowLayout(FlowLayout.LEFT); trackPanel.setLayout(fl); JLabel lbTrack = new JLabel(tr("Tracks")); lbTrack.setFont(getFont().deriveFont(Font.BOLD)); trackPanel.add(lbTrack); zoomButton = new JButton(tr("Zoom")); zoomButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { if (model != null) { IElevationProfile profile = model.getCurrentProfile(); if (profile != null) { Main.map.mapView.zoomTo(profile.getBounds()); } } } }); zoomButton.setEnabled(false); trackCombo = new JComboBox<>(new TrackModel()); trackCombo.setPreferredSize(new Dimension(200, 24)); // HACK! trackCombo.setEnabled(false); // we have no model on startup trackPanel.add(trackCombo); trackPanel.add(zoomButton); // assemble root panel rootPanel.add(statPanel); rootPanel.add(trackPanel); JPanel mainPanel = new JPanel(new BorderLayout()); mainPanel.add(rootPanel, BorderLayout.PAGE_END); // add chart component profPanel = new ElevationProfilePanel(null); mainPanel.add(profPanel, BorderLayout.CENTER); profPanel.addComponentListener(this); createLayout(mainPanel, true, null); } @Override public void showNotify() { Main.getLayerManager().addLayerChangeListener(this); Main.getLayerManager().addActiveLayerChangeListener(this); if (Main.isDisplayingMapView()) { Layer layer = Main.getLayerManager().getActiveLayer(); if (layer instanceof GpxLayer) { setActiveLayer((GpxLayer) layer); } } } @Override public void hideNotify() { Main.getLayerManager().removeActiveLayerChangeListener(this); Main.getLayerManager().removeLayerChangeListener(this); } /** * Gets the elevation model instance. */ public IElevationModel getModel() { return model; } /** * Sets the elevation model instance. * @param model The new model. */ public void setModel(IElevationModel model) { if (this.model != model) { this.model = model; profPanel.setElevationModel(model); updateView(); } } /** * Gets the associated layer instance of the elevation profile. */ public ElevationProfileLayer getProfileLayer() { return profileLayer; } /** * Sets the associated layer instance of the elevation profile. * @param profileLayer The elevation profile layer. */ public void setProfileLayer(ElevationProfileLayer profileLayer) { if (this.profileLayer != profileLayer) { if (this.profileLayer != null) { profPanel.removeSelectionListener(this.profileLayer); } this.profileLayer = profileLayer; profPanel.addSelectionListener(this.profileLayer); } } /** * Refreshes the dialog when model data have changed and notifies clients * that the model has changed. */ private void updateView() { if (model == null) { disableView(); return; } IElevationProfile profile = model.getCurrentProfile(); if (profile != null) { // Show name of profile in title setTitle(String.format("%s: %s", tr("Elevation Profile"), profile.getName())); if (profile.hasElevationData()) { // Show elevation data minHeightLabel.setText( ElevationHelper.getElevationText(profile.getMinHeight())); maxHeightLabel.setText( ElevationHelper.getElevationText(profile.getMaxHeight())); avrgHeightLabel.setText( ElevationHelper.getElevationText(profile.getAverageHeight())); elevationGainLabel.setText( ElevationHelper.getElevationText(profile.getGain())); } // compute values for time and distance long diff = profile.getTimeDifference(); long minutes = diff / (1000 * 60); long hours = minutes / 60; minutes = minutes % 60; double dist = profile.getDistance(); totalTimeLabel.setText(String.format("%d:%02d h", hours, minutes)); distLabel.setText(SystemOfMeasurement.getSystemOfMeasurement().getDistText(dist)); trackCombo.setEnabled(model.profileCount() > 1); trackCombo.setModel(new TrackModel()); zoomButton.setEnabled(true); } else { // no elevation data, -> switch back to empty view disableView(); } fireModelChanged(); repaint(); } private void disableView() { setTitle(String.format("%s: (No data)", tr("Elevation Profile"))); minHeightLabel.setText(EMPTY_DATA_STRING); maxHeightLabel.setText(EMPTY_DATA_STRING); avrgHeightLabel.setText(EMPTY_DATA_STRING); elevationGainLabel.setText(EMPTY_DATA_STRING); totalTimeLabel.setText(EMPTY_DATA_STRING); distLabel.setText(EMPTY_DATA_STRING); trackCombo.setEnabled(false); zoomButton.setEnabled(false); } /** * Fires the 'model changed' event to all listeners. */ protected void fireModelChanged() { for (IElevationModelListener listener : listeners) { listener.elevationProfileChanged(getModel().getCurrentProfile()); } } /** * Adds a model listener to this instance. * * @param listener * The listener to add. */ public void addModelListener(IElevationModelListener listener) { this.listeners.add(listener); } /** * Removes a model listener from this instance. * * @param listener * The listener to remove. */ public void removeModelListener(IElevationModelListener listener) { this.listeners.remove(listener); } /** * Removes all listeners from this instance. */ public void removeAllListeners() { this.listeners.clear(); } @Override public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { Layer newLayer = Main.getLayerManager().getActiveLayer(); if (newLayer instanceof GpxLayer) { setActiveLayer((GpxLayer) newLayer); } } private void setActiveLayer(GpxLayer newLayer) { if (activeLayer != newLayer) { activeLayer = newLayer; // layer does not exist -> create if (!layerMap.containsKey(newLayer)) { GpxData gpxData = newLayer.data; ElevationModel newEM = new ElevationModel(newLayer.getName(), gpxData); layerMap.put(newLayer, newEM); } setModel(layerMap.get(newLayer)); } } @Override public void layerAdded(LayerAddEvent e) { Layer newLayer = e.getAddedLayer(); if (newLayer instanceof GpxLayer) { GpxLayer gpxLayer = (GpxLayer) newLayer; setActiveLayer(gpxLayer); } } @Override public void layerRemoving(LayerRemoveEvent e) { Layer oldLayer = e.getRemovedLayer(); if (layerMap.containsKey(oldLayer)) { layerMap.remove(oldLayer); } if (layerMap.size() == 0) { setModel(null); if (profileLayer != null) { profileLayer.setProfile(null); } } } @Override public void layerOrderChanged(LayerOrderChangeEvent e) { } @Override public void componentHidden(ComponentEvent e) { } @Override public void componentMoved(ComponentEvent e) { } @Override public void componentResized(ComponentEvent e) { } @Override public void componentShown(ComponentEvent e) { } class TrackModel implements ComboBoxModel<IElevationProfile> { private Collection<ListDataListener> listeners; @Override public void addListDataListener(ListDataListener arg0) { if (listeners == null) { listeners = new ArrayList<>(); } listeners.add(arg0); } @Override public IElevationProfile getElementAt(int index) { if (model == null) return null; IElevationProfile ep = model.getProfiles().get(index); return ep; } @Override public int getSize() { if (model == null) return 0; return model.profileCount(); } @Override public void removeListDataListener(ListDataListener listener) { if (listeners == null) return; listeners.remove(listener); } @Override public IElevationProfile getSelectedItem() { if (model == null) return null; return model.getCurrentProfile(); } @Override public void setSelectedItem(Object selectedObject) { if (model != null && selectedObject instanceof IElevationProfile) { model.setCurrentProfile((IElevationProfile) selectedObject); profileLayer.setProfile(model.getCurrentProfile()); repaint(); } } } }