]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/ImageExporter.java
Version 17, September 2014
[GpsPrune.git] / tim / prune / save / ImageExporter.java
1 package tim.prune.save;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Component;
6 import java.awt.FlowLayout;
7 import java.awt.Font;
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;
14 import java.io.File;
15 import java.io.IOException;
16
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;
27
28 import tim.prune.App;
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.load.GenericFileFilter;
44 import tim.prune.threedee.ImageDefinition;
45
46 /**
47  * Class to handle the exporting of map images, optionally with track data drawn on top.
48  * This allows images larger than the screen to be generated.
49  */
50 public class ImageExporter extends GenericFunction implements BaseImageConsumer
51 {
52         private JDialog   _dialog = null;
53         private JCheckBox _drawDataCheckbox = null;
54         private JCheckBox _drawTrackPointsCheckbox = null;
55         private WholeNumberField _textScaleField = null;
56         private BaseImageDefinitionPanel _baseImagePanel = null;
57         private JFileChooser _fileChooser = null;
58         private JButton   _okButton = null;
59
60         /**
61          * Constructor
62          * @param inApp App object
63          */
64         public ImageExporter(App inApp)
65         {
66                 super(inApp);
67         }
68
69         /** Get the name key */
70         public String getNameKey() {
71                 return "function.exportimage";
72         }
73
74         /**
75          * Begin the function by showing the input dialog
76          */
77         public void begin()
78         {
79                 // Make dialog window
80                 if (_dialog == null)
81                 {
82                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
83                         _dialog.setLocationRelativeTo(_parentFrame);
84                         _dialog.getContentPane().add(makeDialogComponents());
85                         _dialog.pack();
86                         _textScaleField.setValue(100);
87                 }
88
89                 // Check if there is a cache to use
90                 if (!BaseImageConfigDialog.isImagePossible())
91                 {
92                         _app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible");
93                         return;
94                 }
95
96                 _baseImagePanel.updateBaseImageDetails();
97                 baseImageChanged();
98                 // Show dialog
99                 _dialog.setVisible(true);
100         }
101
102         /**
103          * Make the dialog components to select the export options
104          * @return Component holding gui elements
105          */
106         private Component makeDialogComponents()
107         {
108                 JPanel panel = new JPanel();
109                 panel.setLayout(new BorderLayout(4, 4));
110                 // Checkbox for drawing track or not
111                 _drawDataCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrack"));
112                 _drawDataCheckbox.setSelected(true); // draw by default
113                 // Also whether to draw track points or not
114                 _drawTrackPointsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrackpoints"));
115                 _drawTrackPointsCheckbox.setSelected(true);
116                 // Add listener to en/disable trackpoints checkbox
117                 _drawDataCheckbox.addActionListener(new ActionListener() {
118                         public void actionPerformed(ActionEvent arg0) {
119                                 _drawTrackPointsCheckbox.setEnabled(_drawDataCheckbox.isSelected());
120                         }
121                 });
122
123                 // TODO: Maybe have other controls such as line width, symbol scale factor
124                 JPanel controlsPanel = new JPanel();
125                 GuiGridLayout grid = new GuiGridLayout(controlsPanel);
126                 grid.add(new JLabel(I18nManager.getText("dialog.exportimage.textscalepercent") + ": "));
127                 _textScaleField = new WholeNumberField(3);
128                 _textScaleField.setText("888");
129                 grid.add(_textScaleField);
130
131                 // OK, Cancel buttons
132                 JPanel buttonPanel = new JPanel();
133                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
134                 _okButton = new JButton(I18nManager.getText("button.ok"));
135                 _okButton.addActionListener(new ActionListener() {
136                         public void actionPerformed(ActionEvent e)
137                         {
138                                 doExport();
139                                 _baseImagePanel.getGrouter().clearMapImage();
140                                 _dialog.dispose();
141                         }
142                 });
143                 buttonPanel.add(_okButton);
144                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
145                 cancelButton.addActionListener(new ActionListener() {
146                         public void actionPerformed(ActionEvent e)
147                         {
148                                 _baseImagePanel.getGrouter().clearMapImage();
149                                 _dialog.dispose();
150                         }
151                 });
152                 buttonPanel.add(cancelButton);
153                 panel.add(buttonPanel, BorderLayout.SOUTH);
154
155                 // Listener to close dialog if escape pressed
156                 KeyAdapter closer = new KeyAdapter() {
157                         public void keyReleased(KeyEvent e)
158                         {
159                                 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
160                                         _dialog.dispose();
161                                         _baseImagePanel.getGrouter().clearMapImage();
162                                 }
163                         }
164                 };
165                 _drawDataCheckbox.addKeyListener(closer);
166
167                 // Panel for the base image
168                 _baseImagePanel = new BaseImageDefinitionPanel(this, _dialog, _app.getTrackInfo().getTrack());
169
170                 // Panel for the checkboxes at the top
171                 JPanel checkPanel = new JPanel();
172                 checkPanel.setLayout(new BoxLayout(checkPanel, BoxLayout.Y_AXIS));
173                 checkPanel.add(_drawDataCheckbox);
174                 checkPanel.add(_drawTrackPointsCheckbox);
175
176                 // add these panels to the holder panel
177                 JPanel holderPanel = new JPanel();
178                 holderPanel.setLayout(new BorderLayout(5, 5));
179                 holderPanel.add(checkPanel, BorderLayout.NORTH);
180                 holderPanel.add(controlsPanel, BorderLayout.CENTER);
181                 holderPanel.add(_baseImagePanel, BorderLayout.SOUTH);
182                 holderPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
183
184                 panel.add(holderPanel, BorderLayout.NORTH);
185                 return panel;
186         }
187
188
189         /**
190          * Select the file and export data to it
191          */
192         private void doExport()
193         {
194                 _okButton.setEnabled(false);
195                 // OK pressed, so choose output file
196                 if (_fileChooser == null)
197                 {
198                         _fileChooser = new JFileChooser();
199                         _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
200                         _fileChooser.setFileFilter(new GenericFileFilter("filetype.png", new String[] {"png"}));
201                         _fileChooser.setAcceptAllFileFilterUsed(false);
202                         // start from directory in config which should be set
203                         final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
204                         if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
205                 }
206
207                 // Allow choose again if an existing file is selected
208                 boolean chooseAgain = false;
209                 do
210                 {
211                         chooseAgain = false;
212                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
213                         {
214                                 // OK pressed and file chosen
215                                 File pngFile = _fileChooser.getSelectedFile();
216                                 if (!pngFile.getName().toLowerCase().endsWith(".png"))
217                                 {
218                                         pngFile = new File(pngFile.getAbsolutePath() + ".png");
219                                 }
220                                 // Check if file exists and if necessary prompt for overwrite
221                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
222                                 if (!pngFile.exists() || JOptionPane.showOptionDialog(_parentFrame,
223                                                 I18nManager.getText("dialog.save.overwrite.text"),
224                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
225                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
226                                         == JOptionPane.YES_OPTION)
227                                 {
228                                         // Export the file
229                                         if (!exportFile(pngFile))
230                                         {
231                                                 // export failed so need to choose again
232                                                 chooseAgain = true;
233                                         }
234                                 }
235                                 else
236                                 {
237                                         // overwrite cancelled so need to choose again
238                                         chooseAgain = true;
239                                 }
240                         }
241                 } while (chooseAgain);
242         }
243
244         /**
245          * Export the track data to the specified file
246          * @param inPngFile File object to save to
247          * @return true if successful
248          */
249         private boolean exportFile(File inPngFile)
250         {
251                 // Get the image file from the grouter
252                 ImageDefinition imageDef = _baseImagePanel.getImageDefinition();
253                 MapSource source = MapSourceLibrary.getSource(imageDef.getSourceIndex());
254                 MapGrouter grouter = _baseImagePanel.getGrouter();
255                 GroutedImage baseImage = grouter.getMapImage(_app.getTrackInfo().getTrack(), source,
256                         imageDef.getZoom());
257                 if (baseImage == null || !baseImage.isValid())
258                 {
259                         _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
260                         return true;
261                 }
262                 try
263                 {
264                         if (_drawDataCheckbox.isSelected())
265                         {
266                                 // Draw the track on top of this image
267                                 drawData(baseImage);
268                         }
269                         // Write composite image to file
270                         if (!ImageIO.write(baseImage.getImage(), "png", inPngFile)) {
271                                 _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
272                                 return false; // choose again - the image creation worked but the save failed
273                         }
274                 }
275                 catch (IOException ioe) {
276                         System.err.println("Can't write image: " + ioe.getClass().getName());
277                 }
278                 return true;
279         }
280
281         /**
282          * Draw the track and waypoint data from the current Track onto the given image
283          * @param inImage GroutedImage from map tiles
284          */
285         private void drawData(GroutedImage inImage)
286         {
287                 // Work out x, y limits for drawing
288                 DoubleRange xRange = inImage.getXRange();
289                 DoubleRange yRange = inImage.getYRange();
290                 final int zoomFactor = 1 << _baseImagePanel.getImageDefinition().getZoom();
291                 Graphics g = inImage.getImage().getGraphics();
292                 // TODO: Set line width, style etc
293                 final PointColourer pointColourer = _app.getPointColourer();
294                 final Color defaultPointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
295                 g.setColor(defaultPointColour);
296
297                 // Loop to draw all track points
298                 final Track track = _app.getTrackInfo().getTrack();
299                 final int numPoints = track.getNumPoints();
300                 int prevX = 0, prevY = 0;
301                 for (int i=0; i<numPoints; i++)
302                 {
303                         DataPoint point = track.getPoint(i);
304                         if (!point.isWaypoint())
305                         {
306                                 // Determine what colour to use to draw the track point
307                                 if (pointColourer != null)
308                                 {
309                                         Color c = pointColourer.getColour(i);
310                                         g.setColor(c == null ? defaultPointColour : c);
311                                 }
312                                 double x = track.getX(i) - xRange.getMinimum();
313                                 double y = track.getY(i) - yRange.getMinimum();
314                                 // use zoom level to calculate pixel coords on image
315                                 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
316                                 // System.out.println("Point: x=" + x + ", px=" + px + ", y=" + y + ", py=" + py);
317                                 if (!point.getSegmentStart()) {
318                                         // draw from previous point to this one
319                                         g.drawLine(prevX, prevY, px, py);
320                                 }
321                                 // Only draw points if requested
322                                 if (_drawTrackPointsCheckbox.isSelected())
323                                 {
324                                         g.drawRect(px-2, py-2, 3, 3);
325                                 }
326                                 // save coordinates
327                                 prevX = px; prevY = py;
328                         }
329                 }
330
331                 // Now the waypoints
332                 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
333                 g.setColor(textColour);
334                 // Loop again to draw waypoints
335                 for (int i=0; i<numPoints; i++)
336                 {
337                         DataPoint point = track.getPoint(i);
338                         if (point.isWaypoint())
339                         {
340                                 // draw blob for each waypoint
341                                 double x = track.getX(i) - xRange.getMinimum();
342                                 double y = track.getY(i) - yRange.getMinimum();
343                                 // use zoom level to calculate pixel coords on image
344                                 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
345                                 g.fillRect(px-3, py-3, 6, 6);
346                         }
347                 }
348                 // Set text size according to input
349                 int fontScalePercent = _textScaleField.getValue();
350                 if (fontScalePercent > 10 && fontScalePercent <= 999)
351                 {
352                         Font gFont = g.getFont();
353                         g.setFont(gFont.deriveFont((float) (gFont.getSize() * 0.01 * fontScalePercent)));
354                 }
355                 FontMetrics fm = g.getFontMetrics();
356                 final int nameHeight = fm.getHeight();
357                 final int imageSize = inImage.getImageSize();
358
359                 // Loop over points again, draw photo points
360                 final Color photoColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
361                 g.setColor(photoColour);
362                 for (int i=0; i<numPoints; i++)
363                 {
364                         DataPoint point = track.getPoint(i);
365                         if (point.hasMedia())
366                         {
367                                 // draw blob for each photo
368                                 double x = track.getX(i) - xRange.getMinimum();
369                                 double y = track.getY(i) - yRange.getMinimum();
370                                 // use zoom level to calculate pixel coords on image
371                                 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
372                                 g.fillRect(px-3, py-3, 6, 6);
373                         }
374                 }
375
376                 // Loop over points again, now draw names for waypoints
377                 g.setColor(textColour);
378                 for (int i=0; i<numPoints; i++)
379                 {
380                         DataPoint point = track.getPoint(i);
381                         if (point.isWaypoint())
382                         {
383                                 double x = track.getX(i) - xRange.getMinimum();
384                                 double y = track.getY(i) - yRange.getMinimum();
385                                 int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
386
387                                 // Figure out where to draw waypoint name so it doesn't obscure track
388                                 String waypointName = point.getWaypointName();
389                                 int nameWidth = fm.stringWidth(waypointName);
390                                 boolean drawnName = false;
391                                 // Make arrays for coordinates right left up down
392                                 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
393                                 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
394                                 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
395                                 {
396                                         // Shift arrays for coordinates right left up down
397                                         nameXs[0] += 2; nameXs[1] -= 2;
398                                         nameYs[2] -= 2; nameYs[3] += 2;
399                                         // Check each direction in turn right left up down
400                                         for (int a=0; a<4; a++)
401                                         {
402                                                 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < imageSize
403                                                         && nameYs[a] < imageSize && (nameYs[a] - nameHeight) > 0
404                                                         && !MapUtils.overlapsPoints(inImage.getImage(), nameXs[a], nameYs[a],
405                                                                 nameWidth, nameHeight, textColour))
406                                                 {
407                                                         // Found a rectangle to fit - draw name here and quit
408                                                         g.drawString(waypointName, nameXs[a], nameYs[a]);
409                                                         drawnName = true;
410                                                         break;
411                                                 }
412                                         }
413                                 }
414                         }
415                 }
416
417                 // Maybe draw note at the bottom, export from GpsPrune?  Filename?
418                 // Note: Differences from main map: No mapPosition (modifying position and visible points),
419                 //       no selection, no opacities, maybe different scale/text factors
420         }
421
422         /**
423          * Base image has changed, need to enable/disable ok button
424          */
425         public void baseImageChanged()
426         {
427                 final boolean useImage = _baseImagePanel.getImageDefinition().getUseImage();
428                 final int zoomLevel = _baseImagePanel.getImageDefinition().getZoom();
429                 final boolean okEnabled = useImage && _baseImagePanel.getFoundData()
430                         && MapGrouter.isZoomLevelOk(_app.getTrackInfo().getTrack(), zoomLevel);
431                 _okButton.setEnabled(okEnabled);
432         }
433 }