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.BoxLayout;
20 import javax.swing.JButton;
21 import javax.swing.JCheckBox;
22 import javax.swing.JDialog;
23 import javax.swing.JFileChooser;
24 import javax.swing.JLabel;
25 import javax.swing.JOptionPane;
26 import javax.swing.JPanel;
29 import tim.prune.GenericFunction;
30 import tim.prune.I18nManager;
31 import tim.prune.config.ColourScheme;
32 import tim.prune.config.Config;
33 import tim.prune.data.DataPoint;
34 import tim.prune.data.DoubleRange;
35 import tim.prune.data.Track;
36 import tim.prune.gui.BaseImageDefinitionPanel;
37 import tim.prune.gui.GuiGridLayout;
38 import tim.prune.gui.WholeNumberField;
39 import tim.prune.gui.colour.PointColourer;
40 import tim.prune.gui.map.MapSource;
41 import tim.prune.gui.map.MapSourceLibrary;
42 import tim.prune.gui.map.MapUtils;
43 import tim.prune.gui.map.WpIconDefinition;
44 import tim.prune.gui.map.WpIconLibrary;
45 import tim.prune.load.GenericFileFilter;
46 import tim.prune.threedee.ImageDefinition;
49 * Class to handle the exporting of map images, optionally with track data drawn on top.
50 * This allows images larger than the screen to be generated.
52 public class ImageExporter extends GenericFunction implements BaseImageConsumer
54 private JDialog _dialog = null;
55 private JCheckBox _drawDataCheckbox = null;
56 private JCheckBox _drawTrackPointsCheckbox = null;
57 private WholeNumberField _textScaleField = null;
58 private BaseImageDefinitionPanel _baseImagePanel = null;
59 private JFileChooser _fileChooser = null;
60 private JButton _okButton = null;
64 * @param inApp App object
66 public ImageExporter(App inApp)
71 /** Get the name key */
72 public String getNameKey() {
73 return "function.exportimage";
77 * Begin the function by showing the input dialog
84 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
85 _dialog.setLocationRelativeTo(_parentFrame);
86 _dialog.getContentPane().add(makeDialogComponents());
88 _textScaleField.setValue(100);
91 // Check if there is a cache to use
92 if (!BaseImageConfigDialog.isImagePossible())
94 _app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible");
98 _baseImagePanel.updateBaseImageDetails();
101 _dialog.setVisible(true);
105 * Make the dialog components to select the export options
106 * @return Component holding gui elements
108 private Component makeDialogComponents()
110 JPanel panel = new JPanel();
111 panel.setLayout(new BorderLayout(4, 4));
112 // Checkbox for drawing track or not
113 _drawDataCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrack"));
114 _drawDataCheckbox.setSelected(true); // draw by default
115 // Also whether to draw track points or not
116 _drawTrackPointsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrackpoints"));
117 _drawTrackPointsCheckbox.setSelected(true);
118 // Add listener to en/disable trackpoints checkbox
119 _drawDataCheckbox.addActionListener(new ActionListener() {
120 public void actionPerformed(ActionEvent arg0) {
121 _drawTrackPointsCheckbox.setEnabled(_drawDataCheckbox.isSelected());
125 // TODO: Maybe have other controls such as line width, symbol scale factor
126 JPanel controlsPanel = new JPanel();
127 GuiGridLayout grid = new GuiGridLayout(controlsPanel);
128 grid.add(new JLabel(I18nManager.getText("dialog.exportimage.textscalepercent") + ": "));
129 _textScaleField = new WholeNumberField(3);
130 _textScaleField.setText("888");
131 grid.add(_textScaleField);
133 // OK, Cancel buttons
134 JPanel buttonPanel = new JPanel();
135 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
136 _okButton = new JButton(I18nManager.getText("button.ok"));
137 _okButton.addActionListener(new ActionListener() {
138 public void actionPerformed(ActionEvent e)
141 _baseImagePanel.getGrouter().clearMapImage();
145 buttonPanel.add(_okButton);
146 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
147 cancelButton.addActionListener(new ActionListener() {
148 public void actionPerformed(ActionEvent e)
150 _baseImagePanel.getGrouter().clearMapImage();
154 buttonPanel.add(cancelButton);
155 panel.add(buttonPanel, BorderLayout.SOUTH);
157 // Listener to close dialog if escape pressed
158 KeyAdapter closer = new KeyAdapter() {
159 public void keyReleased(KeyEvent e)
161 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
163 _baseImagePanel.getGrouter().clearMapImage();
167 _drawDataCheckbox.addKeyListener(closer);
169 // Panel for the base image
170 _baseImagePanel = new BaseImageDefinitionPanel(this, _dialog, _app.getTrackInfo().getTrack());
172 // Panel for the checkboxes at the top
173 JPanel checkPanel = new JPanel();
174 checkPanel.setLayout(new BoxLayout(checkPanel, BoxLayout.Y_AXIS));
175 checkPanel.add(_drawDataCheckbox);
176 checkPanel.add(_drawTrackPointsCheckbox);
178 // add these panels to the holder panel
179 JPanel holderPanel = new JPanel();
180 holderPanel.setLayout(new BorderLayout(5, 5));
181 holderPanel.add(checkPanel, BorderLayout.NORTH);
182 holderPanel.add(controlsPanel, BorderLayout.CENTER);
183 holderPanel.add(_baseImagePanel, BorderLayout.SOUTH);
184 holderPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
186 panel.add(holderPanel, BorderLayout.NORTH);
192 * Select the file and export data to it
194 private void doExport()
196 _okButton.setEnabled(false);
197 // OK pressed, so choose output file
198 if (_fileChooser == null)
200 _fileChooser = new JFileChooser();
201 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
202 _fileChooser.setFileFilter(new GenericFileFilter("filetype.png", new String[] {"png"}));
203 _fileChooser.setAcceptAllFileFilterUsed(false);
204 // start from directory in config which should be set
205 final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
206 if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
209 // Allow choose again if an existing file is selected
210 boolean chooseAgain = false;
214 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
216 // OK pressed and file chosen
217 File pngFile = _fileChooser.getSelectedFile();
218 if (!pngFile.getName().toLowerCase().endsWith(".png"))
220 pngFile = new File(pngFile.getAbsolutePath() + ".png");
222 // Check if file exists and if necessary prompt for overwrite
223 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
224 if (!pngFile.exists() || JOptionPane.showOptionDialog(_parentFrame,
225 I18nManager.getText("dialog.save.overwrite.text"),
226 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
227 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
228 == JOptionPane.YES_OPTION)
231 if (!exportFile(pngFile))
233 // export failed so need to choose again
239 // overwrite cancelled so need to choose again
243 } while (chooseAgain);
247 * Export the track data to the specified file
248 * @param inPngFile File object to save to
249 * @return true if successful
251 private boolean exportFile(File inPngFile)
253 // Get the image file from the grouter
254 ImageDefinition imageDef = _baseImagePanel.getImageDefinition();
255 MapSource source = MapSourceLibrary.getSource(imageDef.getSourceIndex());
256 MapGrouter grouter = _baseImagePanel.getGrouter();
257 GroutedImage baseImage = grouter.getMapImage(_app.getTrackInfo().getTrack(), source,
259 if (baseImage == null || !baseImage.isValid())
261 _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
266 if (_drawDataCheckbox.isSelected())
268 // Draw the track on top of this image
271 // Write composite image to file
272 if (!ImageIO.write(baseImage.getImage(), "png", inPngFile)) {
273 _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
274 return false; // choose again - the image creation worked but the save failed
277 catch (IOException ioe) {
278 System.err.println("Can't write image: " + ioe.getClass().getName());
284 * Draw the track and waypoint data from the current Track onto the given image
285 * @param inImage GroutedImage from map tiles
287 private void drawData(GroutedImage inImage)
289 // Work out x, y limits for drawing
290 DoubleRange xRange = inImage.getXRange();
291 DoubleRange yRange = inImage.getYRange();
292 final int zoomFactor = 1 << _baseImagePanel.getImageDefinition().getZoom();
293 Graphics g = inImage.getImage().getGraphics();
294 // TODO: Set line width, style etc
295 final PointColourer pointColourer = _app.getPointColourer();
296 final Color defaultPointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
297 g.setColor(defaultPointColour);
299 // Loop to draw all track points
300 final Track track = _app.getTrackInfo().getTrack();
301 final int numPoints = track.getNumPoints();
302 int prevX = 0, prevY = 0;
303 boolean gotPreviousPoint = false;
304 for (int i=0; i<numPoints; i++)
306 DataPoint point = track.getPoint(i);
307 if (!point.isWaypoint())
309 // Determine what colour to use to draw the track point
310 if (pointColourer != null)
312 Color c = pointColourer.getColour(i);
313 g.setColor(c == null ? defaultPointColour : c);
315 double x = track.getX(i) - xRange.getMinimum();
316 double y = track.getY(i) - yRange.getMinimum();
317 // use zoom level to calculate pixel coords on image
318 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
319 // System.out.println("Point: x=" + x + ", px=" + px + ", y=" + y + ", py=" + py);
320 if (!point.getSegmentStart() && gotPreviousPoint) {
321 // draw from previous point to this one
322 g.drawLine(prevX, prevY, px, py);
324 // Only draw points if requested
325 if (_drawTrackPointsCheckbox.isSelected())
327 g.drawRect(px-2, py-2, 3, 3);
330 prevX = px; prevY = py;
331 gotPreviousPoint = true;
336 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
337 g.setColor(textColour);
338 WpIconDefinition wpIconDefinition = null;
339 final int wpType = Config.getConfigInt(Config.KEY_WAYPOINT_ICONS);
340 if (wpType != WpIconLibrary.WAYPT_DEFAULT)
342 wpIconDefinition = WpIconLibrary.getIconDefinition(wpType, WpIconLibrary.SIZE_MEDIUM);
344 // Loop again to draw waypoints
345 for (int i=0; i<numPoints; i++)
347 DataPoint point = track.getPoint(i);
348 if (point.isWaypoint())
350 // use zoom level to calculate pixel coords on image
351 double x = track.getX(i) - xRange.getMinimum();
352 double y = track.getY(i) - yRange.getMinimum();
353 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
354 // Fill Rect or draw icon image?
355 g.fillRect(px-3, py-3, 6, 6);
356 if (wpIconDefinition == null)
358 g.fillRect(px-3, py-3, 6, 6);
362 g.drawImage(wpIconDefinition.getImageIcon().getImage(), px-wpIconDefinition.getXOffset(),
363 py-wpIconDefinition.getYOffset(), null);
367 // Set text size according to input
368 int fontScalePercent = _textScaleField.getValue();
369 if (fontScalePercent > 0 && fontScalePercent <= 999)
371 Font gFont = g.getFont();
372 g.setFont(gFont.deriveFont((float) (gFont.getSize() * 0.01 * fontScalePercent)));
374 FontMetrics fm = g.getFontMetrics();
375 final int nameHeight = fm.getHeight();
376 final int imageSize = inImage.getImageSize();
378 // Loop over points again, draw photo points
379 final Color photoColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
380 g.setColor(photoColour);
381 for (int i=0; i<numPoints; i++)
383 DataPoint point = track.getPoint(i);
384 if (point.hasMedia())
386 // draw blob for each photo
387 double x = track.getX(i) - xRange.getMinimum();
388 double y = track.getY(i) - yRange.getMinimum();
389 // use zoom level to calculate pixel coords on image
390 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
391 g.fillRect(px-3, py-3, 6, 6);
395 // Loop over points again, now draw names for waypoints
396 g.setColor(textColour);
397 for (int i=0; i<numPoints; i++)
399 DataPoint point = track.getPoint(i);
400 if (point.isWaypoint() && fontScalePercent > 0)
402 double x = track.getX(i) - xRange.getMinimum();
403 double y = track.getY(i) - yRange.getMinimum();
404 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
406 // Figure out where to draw waypoint name so it doesn't obscure track
407 String waypointName = point.getWaypointName();
408 int nameWidth = fm.stringWidth(waypointName);
409 boolean drawnName = false;
410 // Make arrays for coordinates right left up down
411 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
412 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
413 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
415 // Shift arrays for coordinates right left up down
416 nameXs[0] += 2; nameXs[1] -= 2;
417 nameYs[2] -= 2; nameYs[3] += 2;
418 // Check each direction in turn right left up down
419 for (int a=0; a<4; a++)
421 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < imageSize
422 && nameYs[a] < imageSize && (nameYs[a] - nameHeight) > 0
423 && !MapUtils.overlapsPoints(inImage.getImage(), nameXs[a], nameYs[a],
424 nameWidth, nameHeight, textColour))
426 // Found a rectangle to fit - draw name here and quit
427 g.drawString(waypointName, nameXs[a], nameYs[a]);
436 // Maybe draw note at the bottom, export from GpsPrune? Filename?
437 // Note: Differences from main map: No mapPosition (modifying position and visible points),
438 // no selection, no opacities, maybe different scale/text factors
442 * Base image has changed, need to enable/disable ok button
444 public void baseImageChanged()
446 final boolean useImage = _baseImagePanel.getImageDefinition().getUseImage();
447 final int zoomLevel = _baseImagePanel.getImageDefinition().getZoom();
448 final boolean okEnabled = useImage && _baseImagePanel.getFoundData()
449 && MapGrouter.isZoomLevelOk(_app.getTrackInfo().getTrack(), zoomLevel);
450 _okButton.setEnabled(okEnabled);