]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - src/tim/prune/save/ImageExporter.java
Moved source into separate src directory due to popular request
[GpsPrune.git] / src / tim / prune / save / ImageExporter.java
diff --git a/src/tim/prune/save/ImageExporter.java b/src/tim/prune/save/ImageExporter.java
new file mode 100644 (file)
index 0000000..ea18892
--- /dev/null
@@ -0,0 +1,450 @@
+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.BoxLayout;
+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 tim.prune.App;
+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.BaseImageDefinitionPanel;
+import tim.prune.gui.GuiGridLayout;
+import tim.prune.gui.WholeNumberField;
+import tim.prune.gui.colour.PointColourer;
+import tim.prune.gui.map.MapSource;
+import tim.prune.gui.map.MapSourceLibrary;
+import tim.prune.gui.map.MapUtils;
+import tim.prune.gui.map.WpIconDefinition;
+import tim.prune.gui.map.WpIconLibrary;
+import tim.prune.load.GenericFileFilter;
+import tim.prune.threedee.ImageDefinition;
+
+/**
+ * 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 BaseImageConsumer
+{
+       private JDialog   _dialog = null;
+       private JCheckBox _drawDataCheckbox = null;
+       private JCheckBox _drawTrackPointsCheckbox = null;
+       private WholeNumberField _textScaleField = null;
+       private BaseImageDefinitionPanel _baseImagePanel = 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);
+               }
+
+               // Check if there is a cache to use
+               if (!BaseImageConfigDialog.isImagePossible())
+               {
+                       _app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible");
+                       return;
+               }
+
+               _baseImagePanel.updateBaseImageDetails();
+               baseImageChanged();
+               // 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
+               // Also whether to draw track points or not
+               _drawTrackPointsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrackpoints"));
+               _drawTrackPointsCheckbox.setSelected(true);
+               // Add listener to en/disable trackpoints checkbox
+               _drawDataCheckbox.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent arg0) {
+                               _drawTrackPointsCheckbox.setEnabled(_drawDataCheckbox.isSelected());
+                       }
+               });
+
+               // 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();
+                               _baseImagePanel.getGrouter().clearMapImage();
+                               _dialog.dispose();
+                       }
+               });
+               buttonPanel.add(_okButton);
+               JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
+               cancelButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e)
+                       {
+                               _baseImagePanel.getGrouter().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();
+                                       _baseImagePanel.getGrouter().clearMapImage();
+                               }
+                       }
+               };
+               _drawDataCheckbox.addKeyListener(closer);
+
+               // Panel for the base image
+               _baseImagePanel = new BaseImageDefinitionPanel(this, _dialog, _app.getTrackInfo().getTrack());
+
+               // Panel for the checkboxes at the top
+               JPanel checkPanel = new JPanel();
+               checkPanel.setLayout(new BoxLayout(checkPanel, BoxLayout.Y_AXIS));
+               checkPanel.add(_drawDataCheckbox);
+               checkPanel.add(_drawTrackPointsCheckbox);
+
+               // add these panels to the holder panel
+               JPanel holderPanel = new JPanel();
+               holderPanel.setLayout(new BorderLayout(5, 5));
+               holderPanel.add(checkPanel, BorderLayout.NORTH);
+               holderPanel.add(controlsPanel, BorderLayout.CENTER);
+               holderPanel.add(_baseImagePanel, BorderLayout.SOUTH);
+               holderPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
+
+               panel.add(holderPanel, BorderLayout.NORTH);
+               return panel;
+       }
+
+
+       /**
+        * Select the file and export data to it
+        */
+       private void doExport()
+       {
+               _okButton.setEnabled(false);
+               // OK pressed, so choose output file
+               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
+               ImageDefinition imageDef = _baseImagePanel.getImageDefinition();
+               MapSource source = MapSourceLibrary.getSource(imageDef.getSourceIndex());
+               MapGrouter grouter = _baseImagePanel.getGrouter();
+               GroutedImage baseImage = grouter.getMapImage(_app.getTrackInfo().getTrack(), source,
+                       imageDef.getZoom());
+               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();
+               final int zoomFactor = 1 << _baseImagePanel.getImageDefinition().getZoom();
+               Graphics g = inImage.getImage().getGraphics();
+               // TODO: Set line width, style etc
+               final PointColourer pointColourer = _app.getPointColourer();
+               final Color defaultPointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
+               g.setColor(defaultPointColour);
+
+               // Loop to draw all track points
+               final Track track = _app.getTrackInfo().getTrack();
+               final int numPoints = track.getNumPoints();
+               int prevX = 0, prevY = 0;
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = track.getPoint(i);
+                       if (!point.isWaypoint())
+                       {
+                               // Determine what colour to use to draw the track point
+                               if (pointColourer != null)
+                               {
+                                       Color c = pointColourer.getColour(i);
+                                       g.setColor(c == null ? defaultPointColour : c);
+                               }
+                               double x = track.getX(i) - xRange.getMinimum();
+                               double y = track.getY(i) - yRange.getMinimum();
+                               // use zoom level to calculate pixel coords on image
+                               int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
+                               // System.out.println("Point: x=" + x + ", px=" + px + ", y=" + y + ", py=" + py);
+                               if (!point.getSegmentStart()) {
+                                       // draw from previous point to this one
+                                       g.drawLine(prevX, prevY, px, py);
+                               }
+                               // Only draw points if requested
+                               if (_drawTrackPointsCheckbox.isSelected())
+                               {
+                                       g.drawRect(px-2, py-2, 3, 3);
+                               }
+                               // save coordinates
+                               prevX = px; prevY = py;
+                       }
+               }
+
+               // Now the waypoints
+               final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
+               g.setColor(textColour);
+               WpIconDefinition wpIconDefinition = null;
+               final int wpType = Config.getConfigInt(Config.KEY_WAYPOINT_ICONS);
+               if (wpType != WpIconLibrary.WAYPT_DEFAULT)
+               {
+                       wpIconDefinition = WpIconLibrary.getIconDefinition(wpType, WpIconLibrary.SIZE_MEDIUM);
+               }
+               // Loop again to draw waypoints
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = track.getPoint(i);
+                       if (point.isWaypoint())
+                       {
+                               // use zoom level to calculate pixel coords on image
+                               double x = track.getX(i) - xRange.getMinimum();
+                               double y = track.getY(i) - yRange.getMinimum();
+                               int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
+                               // Fill Rect or draw icon image?
+                               g.fillRect(px-3, py-3, 6, 6);
+                               if (wpIconDefinition == null)
+                               {
+                                       g.fillRect(px-3, py-3, 6, 6);
+                               }
+                               else
+                               {
+                                       g.drawImage(wpIconDefinition.getImageIcon().getImage(), px-wpIconDefinition.getXOffset(),
+                                               py-wpIconDefinition.getYOffset(), null);
+                               }
+                       }
+               }
+               // Set text size according to input
+               int fontScalePercent = _textScaleField.getValue();
+               if (fontScalePercent > 10 && fontScalePercent <= 999)
+               {
+                       Font gFont = g.getFont();
+                       g.setFont(gFont.deriveFont((float) (gFont.getSize() * 0.01 * fontScalePercent)));
+               }
+               FontMetrics fm = g.getFontMetrics();
+               final int nameHeight = fm.getHeight();
+               final int imageSize = inImage.getImageSize();
+
+               // Loop over points again, draw photo points
+               final Color photoColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
+               g.setColor(photoColour);
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = track.getPoint(i);
+                       if (point.hasMedia())
+                       {
+                               // draw blob for each photo
+                               double x = track.getX(i) - xRange.getMinimum();
+                               double y = track.getY(i) - yRange.getMinimum();
+                               // use zoom level to calculate pixel coords on image
+                               int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
+                               g.fillRect(px-3, py-3, 6, 6);
+                       }
+               }
+
+               // Loop over points again, now draw names for waypoints
+               g.setColor(textColour);
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = track.getPoint(i);
+                       if (point.isWaypoint())
+                       {
+                               double x = track.getX(i) - xRange.getMinimum();
+                               double y = track.getY(i) - yRange.getMinimum();
+                               int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
+
+                               // Figure out where to draw waypoint name so it doesn't obscure track
+                               String waypointName = point.getWaypointName();
+                               int nameWidth = fm.stringWidth(waypointName);
+                               boolean drawnName = false;
+                               // Make arrays for coordinates right left up down
+                               int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
+                               int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
+                               for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
+                               {
+                                       // Shift arrays for coordinates right left up down
+                                       nameXs[0] += 2; nameXs[1] -= 2;
+                                       nameYs[2] -= 2; nameYs[3] += 2;
+                                       // Check each direction in turn right left up down
+                                       for (int a=0; a<4; a++)
+                                       {
+                                               if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < imageSize
+                                                       && nameYs[a] < imageSize && (nameYs[a] - nameHeight) > 0
+                                                       && !MapUtils.overlapsPoints(inImage.getImage(), nameXs[a], nameYs[a],
+                                                               nameWidth, nameHeight, textColour))
+                                               {
+                                                       // Found a rectangle to fit - draw name here and quit
+                                                       g.drawString(waypointName, nameXs[a], nameYs[a]);
+                                                       drawnName = true;
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               // Maybe draw note at the bottom, export from GpsPrune?  Filename?
+               // Note: Differences from main map: No mapPosition (modifying position and visible points),
+               //       no selection, no opacities, maybe different scale/text factors
+       }
+
+       /**
+        * Base image has changed, need to enable/disable ok button
+        */
+       public void baseImageChanged()
+       {
+               final boolean useImage = _baseImagePanel.getImageDefinition().getUseImage();
+               final int zoomLevel = _baseImagePanel.getImageDefinition().getZoom();
+               final boolean okEnabled = useImage && _baseImagePanel.getFoundData()
+                       && MapGrouter.isZoomLevelOk(_app.getTrackInfo().getTrack(), zoomLevel);
+               _okButton.setEnabled(okEnabled);
+       }
+}