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