1 package tim.prune.save;
3 import java.awt.BorderLayout;
5 import java.awt.Component;
6 import java.awt.FlowLayout;
8 import java.awt.FontMetrics;
9 import java.awt.Graphics;
10 import java.awt.event.ActionEvent;
11 import java.awt.event.ActionListener;
12 import java.awt.event.KeyAdapter;
13 import java.awt.event.KeyEvent;
15 import java.io.IOException;
17 import javax.imageio.ImageIO;
18 import javax.swing.BorderFactory;
19 import javax.swing.JButton;
20 import javax.swing.JCheckBox;
21 import javax.swing.JDialog;
22 import javax.swing.JFileChooser;
23 import javax.swing.JLabel;
24 import javax.swing.JOptionPane;
25 import javax.swing.JPanel;
26 import javax.swing.border.EtchedBorder;
29 import tim.prune.DataSubscriber;
30 import tim.prune.GenericFunction;
31 import tim.prune.I18nManager;
32 import tim.prune.config.ColourScheme;
33 import tim.prune.config.Config;
34 import tim.prune.data.DataPoint;
35 import tim.prune.data.DoubleRange;
36 import tim.prune.data.Track;
37 import tim.prune.gui.GuiGridLayout;
38 import tim.prune.gui.WholeNumberField;
39 import tim.prune.gui.map.MapSource;
40 import tim.prune.gui.map.MapSourceLibrary;
41 import tim.prune.gui.map.MapUtils;
42 import tim.prune.load.GenericFileFilter;
45 * Class to handle the exporting of map images, optionally with track data drawn on top.
46 * This allows images larger than the screen to be generated.
48 public class ImageExporter extends GenericFunction implements DataSubscriber
50 private JDialog _dialog = null;
51 private JCheckBox _drawDataCheckbox = null;
52 private WholeNumberField _textScaleField = null;
53 private JLabel _baseImageLabel = null;
54 private BaseImageConfigDialog _baseImageConfig = null;
55 private JFileChooser _fileChooser = null;
56 private JButton _okButton = null;
60 * @param inApp App object
62 public ImageExporter(App inApp)
67 /** Get the name key */
68 public String getNameKey() {
69 return "function.exportimage";
73 * Begin the function by showing the input dialog
80 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
81 _dialog.setLocationRelativeTo(_parentFrame);
82 _dialog.getContentPane().add(makeDialogComponents());
84 _textScaleField.setValue(100);
86 // Make base image dialog too
87 if (_baseImageConfig == null) {
88 _baseImageConfig = new BaseImageConfigDialog(this, _dialog, _app.getTrackInfo().getTrack());
91 // Check if there is a cache to use
92 if (!BaseImageConfigDialog.isImagePossible())
94 _app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible");
98 updateBaseImageDetails();
100 _dialog.setVisible(true);
104 * Make the dialog components to select the export options
105 * @return Component holding gui elements
107 private Component makeDialogComponents()
109 JPanel panel = new JPanel();
110 panel.setLayout(new BorderLayout(4, 4));
111 // Checkbox for drawing track or not
112 _drawDataCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrack"));
113 _drawDataCheckbox.setSelected(true); // draw by default
115 // TODO: Maybe have other controls such as line width, symbol scale factor
116 JPanel controlsPanel = new JPanel();
117 GuiGridLayout grid = new GuiGridLayout(controlsPanel);
118 grid.add(new JLabel(I18nManager.getText("dialog.exportimage.textscalepercent") + ": "));
119 _textScaleField = new WholeNumberField(3);
120 _textScaleField.setText("888");
121 grid.add(_textScaleField);
123 // OK, Cancel buttons
124 JPanel buttonPanel = new JPanel();
125 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
126 _okButton = new JButton(I18nManager.getText("button.ok"));
127 _okButton.addActionListener(new ActionListener() {
128 public void actionPerformed(ActionEvent e)
131 MapGrouter.clearMapImage();
135 buttonPanel.add(_okButton);
136 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
137 cancelButton.addActionListener(new ActionListener() {
138 public void actionPerformed(ActionEvent e)
140 MapGrouter.clearMapImage();
144 buttonPanel.add(cancelButton);
145 panel.add(buttonPanel, BorderLayout.SOUTH);
147 // Listener to close dialog if escape pressed
148 KeyAdapter closer = new KeyAdapter() {
149 public void keyReleased(KeyEvent e)
151 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
153 MapGrouter.clearMapImage();
157 _drawDataCheckbox.addKeyListener(closer);
159 // Panel for the base image
160 JPanel imagePanel = new JPanel();
161 imagePanel.setLayout(new BorderLayout(10, 4));
162 imagePanel.add(new JLabel(I18nManager.getText("dialog.exportpov.baseimage") + ": "), BorderLayout.WEST);
163 _baseImageLabel = new JLabel("Typical sourcename");
164 imagePanel.add(_baseImageLabel, BorderLayout.CENTER);
165 JButton baseImageButton = new JButton(I18nManager.getText("button.edit"));
166 baseImageButton.addActionListener(new ActionListener() {
167 public void actionPerformed(ActionEvent event) {
171 baseImageButton.addKeyListener(closer);
172 imagePanel.add(baseImageButton, BorderLayout.EAST);
173 imagePanel.setBorder(BorderFactory.createCompoundBorder(
174 BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(4, 4, 4, 4))
177 // add these panels to the holder panel
178 JPanel holderPanel = new JPanel();
179 holderPanel.setLayout(new BorderLayout(5, 5));
180 holderPanel.add(_drawDataCheckbox, BorderLayout.NORTH);
181 holderPanel.add(controlsPanel, BorderLayout.CENTER);
182 holderPanel.add(imagePanel, BorderLayout.SOUTH);
183 holderPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
185 panel.add(holderPanel, BorderLayout.NORTH);
190 * Change the base image by calling the BaseImageConfigDialog
192 private void changeBaseImage()
194 // Check if there is a cache to use
195 if (BaseImageConfigDialog.isImagePossible())
197 // Show new dialog to choose image details
198 _baseImageConfig.beginWithImageYes();
203 * Callback from base image config dialog
205 public void dataUpdated(byte inUpdateType)
207 updateBaseImageDetails();
211 public void actionCompleted(String inMessage) {
215 * Update the description label according to the selected base image details
217 private void updateBaseImageDetails()
220 if (_baseImageConfig.useImage())
222 MapSource source = MapSourceLibrary.getSource(_baseImageConfig.getSourceIndex());
223 if (source != null) {
224 desc = source.getName() + " ("
225 + _baseImageConfig.getZoomLevel() + ")";
229 desc = I18nManager.getText("dialog.about.no");
231 _baseImageLabel.setText(desc);
232 _okButton.setEnabled(_baseImageConfig.useImage() && _baseImageConfig.getFoundData()
233 && MapGrouter.isZoomLevelOk(_app.getTrackInfo().getTrack(), _baseImageConfig.getZoomLevel()));
237 * Select the file and export data to it
239 private void doExport()
241 // OK pressed, so choose output file
242 _okButton.setEnabled(false);
243 if (_fileChooser == null)
245 _fileChooser = new JFileChooser();
246 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
247 _fileChooser.setFileFilter(new GenericFileFilter("filetype.png", new String[] {"png"}));
248 _fileChooser.setAcceptAllFileFilterUsed(false);
249 // start from directory in config which should be set
250 final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
251 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
254 // Allow choose again if an existing file is selected
255 boolean chooseAgain = false;
259 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
261 // OK pressed and file chosen
262 File pngFile = _fileChooser.getSelectedFile();
263 if (!pngFile.getName().toLowerCase().endsWith(".png"))
265 pngFile = new File(pngFile.getAbsolutePath() + ".png");
267 // Check if file exists and if necessary prompt for overwrite
268 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
269 if (!pngFile.exists() || JOptionPane.showOptionDialog(_parentFrame,
270 I18nManager.getText("dialog.save.overwrite.text"),
271 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
272 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
273 == JOptionPane.YES_OPTION)
276 if (!exportFile(pngFile))
278 // export failed so need to choose again
284 // overwrite cancelled so need to choose again
288 } while (chooseAgain);
292 * Export the track data to the specified file
293 * @param inPngFile File object to save to
294 * @return true if successful
296 private boolean exportFile(File inPngFile)
298 // Get the image file from the grouter
299 MapSource source = MapSourceLibrary.getSource(_baseImageConfig.getSourceIndex());
300 GroutedImage baseImage = MapGrouter.getMapImage(_app.getTrackInfo().getTrack(), source,
301 _baseImageConfig.getZoomLevel());
302 if (baseImage == null || !baseImage.isValid())
304 _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
309 if (_drawDataCheckbox.isSelected())
311 // Draw the track on top of this image
314 // Write composite image to file
315 if (!ImageIO.write(baseImage.getImage(), "png", inPngFile)) {
316 _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
317 return false; // choose again - the image creation worked but the save failed
320 catch (IOException ioe) {
321 System.err.println("Can't write image: " + ioe.getClass().getName());
327 * Draw the track and waypoint data from the current Track onto the given image
328 * @param inImage GroutedImage from map tiles
330 private void drawData(GroutedImage inImage)
332 // Work out x, y limits for drawing
333 DoubleRange xRange = inImage.getXRange();
334 DoubleRange yRange = inImage.getYRange();
335 int zoomFactor = 1 << _baseImageConfig.getZoomLevel();
336 Graphics g = inImage.getImage().getGraphics();
337 // TODO: Set colour, line width
338 g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_POINT));
341 final Track track = _app.getTrackInfo().getTrack();
342 final int numPoints = track.getNumPoints();
343 int prevX = 0, prevY = 0;
344 for (int i=0; i<numPoints; i++)
346 DataPoint point = track.getPoint(i);
347 if (!point.isWaypoint())
349 double x = track.getX(i) - xRange.getMinimum();
350 double y = track.getY(i) - yRange.getMinimum();
351 // use zoom level to calculate pixel coords on image
352 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
353 // System.out.println("Point: x=" + x + ", px=" + px + ", y=" + y + ", py=" + py);
354 if (!point.getSegmentStart()) {
355 // draw from previous point to this one
356 g.drawLine(prevX, prevY, px, py);
359 g.drawRect(px-2, py-2, 3, 3);
361 prevX = px; prevY = py;
365 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
366 g.setColor(textColour);
368 for (int i=0; i<numPoints; i++)
370 DataPoint point = track.getPoint(i);
371 if (point.isWaypoint())
373 // draw blob for each waypoint
374 double x = track.getX(i) - xRange.getMinimum();
375 double y = track.getY(i) - yRange.getMinimum();
376 // use zoom level to calculate pixel coords on image
377 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
378 g.fillRect(px-3, py-3, 6, 6);
381 // Set text size according to input
382 int fontScalePercent = _textScaleField.getValue();
383 if (fontScalePercent > 10 && fontScalePercent <= 999)
385 Font gFont = g.getFont();
386 g.setFont(gFont.deriveFont((float) (gFont.getSize() * 0.01 * fontScalePercent)));
388 FontMetrics fm = g.getFontMetrics();
389 final int nameHeight = fm.getHeight();
390 final int imageSize = inImage.getImageSize();
392 // Loop over points again, draw photo points
393 final Color photoColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
394 g.setColor(photoColour);
395 for (int i=0; i<numPoints; i++)
397 DataPoint point = track.getPoint(i);
398 if (point.hasMedia())
400 // draw blob for each photo
401 double x = track.getX(i) - xRange.getMinimum();
402 double y = track.getY(i) - yRange.getMinimum();
403 // use zoom level to calculate pixel coords on image
404 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
405 g.fillRect(px-3, py-3, 6, 6);
409 // Loop over points again, now draw names for waypoints
410 g.setColor(textColour);
411 for (int i=0; i<numPoints; i++)
413 DataPoint point = track.getPoint(i);
414 if (point.isWaypoint())
416 double x = track.getX(i) - xRange.getMinimum();
417 double y = track.getY(i) - yRange.getMinimum();
418 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
420 // Figure out where to draw waypoint name so it doesn't obscure track
421 String waypointName = point.getWaypointName();
422 int nameWidth = fm.stringWidth(waypointName);
423 boolean drawnName = false;
424 // Make arrays for coordinates right left up down
425 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
426 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
427 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
429 // Shift arrays for coordinates right left up down
430 nameXs[0] += 2; nameXs[1] -= 2;
431 nameYs[2] -= 2; nameYs[3] += 2;
432 // Check each direction in turn right left up down
433 for (int a=0; a<4; a++)
435 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < imageSize
436 && nameYs[a] < imageSize && (nameYs[a] - nameHeight) > 0
437 && !MapUtils.overlapsPoints(inImage.getImage(), nameXs[a], nameYs[a],
438 nameWidth, nameHeight, textColour))
440 // Found a rectangle to fit - draw name here and quit
441 g.drawString(waypointName, nameXs[a], nameYs[a]);
450 // Maybe draw note at the bottom, export from GpsPrune? Filename?
451 // Note: Differences from main map: No mapPosition (modifying position and visible points),
452 // no selection, no opacities, maybe different scale/text factors