X-Git-Url: https://gitweb.fperrin.net/?a=blobdiff_plain;f=tim%2Fprune%2Fsave%2FImageExporter.java;fp=tim%2Fprune%2Fsave%2FImageExporter.java;h=fabed33bb9553583cf7d18986dc2289e27ca0eb2;hb=7f5ed2be62905bd37717376dc22d09e5ea7edb4d;hp=0000000000000000000000000000000000000000;hpb=b361869e590bbca32664c16ac24d6296926594a5;p=GpsPrune.git diff --git a/tim/prune/save/ImageExporter.java b/tim/prune/save/ImageExporter.java new file mode 100644 index 0000000..fabed33 --- /dev/null +++ b/tim/prune/save/ImageExporter.java @@ -0,0 +1,454 @@ +package tim.prune.save; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.border.EtchedBorder; + +import tim.prune.App; +import tim.prune.DataSubscriber; +import tim.prune.GenericFunction; +import tim.prune.I18nManager; +import tim.prune.config.ColourScheme; +import tim.prune.config.Config; +import tim.prune.data.DataPoint; +import tim.prune.data.DoubleRange; +import tim.prune.data.Track; +import tim.prune.gui.GuiGridLayout; +import tim.prune.gui.WholeNumberField; +import tim.prune.gui.map.MapSource; +import tim.prune.gui.map.MapSourceLibrary; +import tim.prune.gui.map.MapUtils; +import tim.prune.load.GenericFileFilter; + +/** + * Class to handle the exporting of map images, optionally with track data drawn on top. + * This allows images larger than the screen to be generated. + */ +public class ImageExporter extends GenericFunction implements DataSubscriber +{ + private JDialog _dialog = null; + private JCheckBox _drawDataCheckbox = null; + private WholeNumberField _textScaleField = null; + private JLabel _baseImageLabel = null; + private BaseImageConfigDialog _baseImageConfig = null; + private JFileChooser _fileChooser = null; + private JButton _okButton = null; + + /** + * Constructor + * @param inApp App object + */ + public ImageExporter(App inApp) + { + super(inApp); + } + + /** Get the name key */ + public String getNameKey() { + return "function.exportimage"; + } + + /** + * Begin the function by showing the input dialog + */ + public void begin() + { + // Make dialog window + if (_dialog == null) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true); + _dialog.setLocationRelativeTo(_parentFrame); + _dialog.getContentPane().add(makeDialogComponents()); + _dialog.pack(); + _textScaleField.setValue(100); + } + // Make base image dialog too + if (_baseImageConfig == null) { + _baseImageConfig = new BaseImageConfigDialog(this, _dialog, _app.getTrackInfo().getTrack()); + } + + // Check if there is a cache to use + if (!BaseImageConfigDialog.isImagePossible()) + { + _app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible"); + return; + } + + updateBaseImageDetails(); + // Show dialog + _dialog.setVisible(true); + } + + /** + * Make the dialog components to select the export options + * @return Component holding gui elements + */ + private Component makeDialogComponents() + { + JPanel panel = new JPanel(); + panel.setLayout(new BorderLayout(4, 4)); + // Checkbox for drawing track or not + _drawDataCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrack")); + _drawDataCheckbox.setSelected(true); // draw by default + + // TODO: Maybe have other controls such as line width, symbol scale factor + JPanel controlsPanel = new JPanel(); + GuiGridLayout grid = new GuiGridLayout(controlsPanel); + grid.add(new JLabel(I18nManager.getText("dialog.exportimage.textscalepercent") + ": ")); + _textScaleField = new WholeNumberField(3); + _textScaleField.setText("888"); + grid.add(_textScaleField); + + // OK, Cancel buttons + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + _okButton = new JButton(I18nManager.getText("button.ok")); + _okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + doExport(); + MapGrouter.clearMapImage(); + _dialog.dispose(); + } + }); + buttonPanel.add(_okButton); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + MapGrouter.clearMapImage(); + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + panel.add(buttonPanel, BorderLayout.SOUTH); + + // Listener to close dialog if escape pressed + KeyAdapter closer = new KeyAdapter() { + public void keyReleased(KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + _dialog.dispose(); + MapGrouter.clearMapImage(); + } + } + }; + _drawDataCheckbox.addKeyListener(closer); + + // Panel for the base image + JPanel imagePanel = new JPanel(); + imagePanel.setLayout(new BorderLayout(10, 4)); + imagePanel.add(new JLabel(I18nManager.getText("dialog.exportpov.baseimage") + ": "), BorderLayout.WEST); + _baseImageLabel = new JLabel("Typical sourcename"); + imagePanel.add(_baseImageLabel, BorderLayout.CENTER); + JButton baseImageButton = new JButton(I18nManager.getText("button.edit")); + baseImageButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + changeBaseImage(); + } + }); + baseImageButton.addKeyListener(closer); + imagePanel.add(baseImageButton, BorderLayout.EAST); + imagePanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(4, 4, 4, 4)) + ); + + // add these panels to the holder panel + JPanel holderPanel = new JPanel(); + holderPanel.setLayout(new BorderLayout(5, 5)); + holderPanel.add(_drawDataCheckbox, BorderLayout.NORTH); + holderPanel.add(controlsPanel, BorderLayout.CENTER); + holderPanel.add(imagePanel, BorderLayout.SOUTH); + holderPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + + panel.add(holderPanel, BorderLayout.NORTH); + return panel; + } + + /** + * Change the base image by calling the BaseImageConfigDialog + */ + private void changeBaseImage() + { + // Check if there is a cache to use + if (BaseImageConfigDialog.isImagePossible()) + { + // Show new dialog to choose image details + _baseImageConfig.beginWithImageYes(); + } + } + + /** + * Callback from base image config dialog + */ + public void dataUpdated(byte inUpdateType) + { + updateBaseImageDetails(); + } + + /** Not required */ + public void actionCompleted(String inMessage) { + } + + /** + * Update the description label according to the selected base image details + */ + private void updateBaseImageDetails() + { + String desc = null; + if (_baseImageConfig.useImage()) + { + MapSource source = MapSourceLibrary.getSource(_baseImageConfig.getSourceIndex()); + if (source != null) { + desc = source.getName() + " (" + + _baseImageConfig.getZoomLevel() + ")"; + } + } + if (desc == null) { + desc = I18nManager.getText("dialog.about.no"); + } + _baseImageLabel.setText(desc); + _okButton.setEnabled(_baseImageConfig.useImage() && _baseImageConfig.getFoundData() + && MapGrouter.isZoomLevelOk(_app.getTrackInfo().getTrack(), _baseImageConfig.getZoomLevel())); + } + + /** + * Select the file and export data to it + */ + private void doExport() + { + // OK pressed, so choose output file + _okButton.setEnabled(false); + if (_fileChooser == null) + { + _fileChooser = new JFileChooser(); + _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG); + _fileChooser.setFileFilter(new GenericFileFilter("filetype.png", new String[] {"png"})); + _fileChooser.setAcceptAllFileFilterUsed(false); + // start from directory in config which should be set + final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR); + if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));} + } + + // Allow choose again if an existing file is selected + boolean chooseAgain = false; + do + { + chooseAgain = false; + if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) + { + // OK pressed and file chosen + File pngFile = _fileChooser.getSelectedFile(); + if (!pngFile.getName().toLowerCase().endsWith(".png")) + { + pngFile = new File(pngFile.getAbsolutePath() + ".png"); + } + // Check if file exists and if necessary prompt for overwrite + Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")}; + if (!pngFile.exists() || JOptionPane.showOptionDialog(_parentFrame, + I18nManager.getText("dialog.save.overwrite.text"), + I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1]) + == JOptionPane.YES_OPTION) + { + // Export the file + if (!exportFile(pngFile)) + { + // export failed so need to choose again + chooseAgain = true; + } + } + else + { + // overwrite cancelled so need to choose again + chooseAgain = true; + } + } + } while (chooseAgain); + } + + /** + * Export the track data to the specified file + * @param inPngFile File object to save to + * @return true if successful + */ + private boolean exportFile(File inPngFile) + { + // Get the image file from the grouter + MapSource source = MapSourceLibrary.getSource(_baseImageConfig.getSourceIndex()); + GroutedImage baseImage = MapGrouter.getMapImage(_app.getTrackInfo().getTrack(), source, + _baseImageConfig.getZoomLevel()); + if (baseImage == null || !baseImage.isValid()) + { + _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage"); + return true; + } + try + { + if (_drawDataCheckbox.isSelected()) + { + // Draw the track on top of this image + drawData(baseImage); + } + // Write composite image to file + if (!ImageIO.write(baseImage.getImage(), "png", inPngFile)) { + _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage"); + return false; // choose again - the image creation worked but the save failed + } + } + catch (IOException ioe) { + System.err.println("Can't write image: " + ioe.getClass().getName()); + } + return true; + } + + /** + * Draw the track and waypoint data from the current Track onto the given image + * @param inImage GroutedImage from map tiles + */ + private void drawData(GroutedImage inImage) + { + // Work out x, y limits for drawing + DoubleRange xRange = inImage.getXRange(); + DoubleRange yRange = inImage.getYRange(); + int zoomFactor = 1 << _baseImageConfig.getZoomLevel(); + Graphics g = inImage.getImage().getGraphics(); + // TODO: Set colour, line width + g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_POINT)); + + // Loop over points + final Track track = _app.getTrackInfo().getTrack(); + final int numPoints = track.getNumPoints(); + int prevX = 0, prevY = 0; + for (int i=0; i