]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/ImageExporter.java
fabed33bb9553583cf7d18986dc2289e27ca0eb2
[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.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;
27
28 import tim.prune.App;
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;
43
44 /**
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.
47  */
48 public class ImageExporter extends GenericFunction implements DataSubscriber
49 {
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;
57
58         /**
59          * Constructor
60          * @param inApp App object
61          */
62         public ImageExporter(App inApp)
63         {
64                 super(inApp);
65         }
66
67         /** Get the name key */
68         public String getNameKey() {
69                 return "function.exportimage";
70         }
71
72         /**
73          * Begin the function by showing the input dialog
74          */
75         public void begin()
76         {
77                 // Make dialog window
78                 if (_dialog == null)
79                 {
80                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
81                         _dialog.setLocationRelativeTo(_parentFrame);
82                         _dialog.getContentPane().add(makeDialogComponents());
83                         _dialog.pack();
84                         _textScaleField.setValue(100);
85                 }
86                 // Make base image dialog too
87                 if (_baseImageConfig == null) {
88                         _baseImageConfig = new BaseImageConfigDialog(this, _dialog, _app.getTrackInfo().getTrack());
89                 }
90
91                 // Check if there is a cache to use
92                 if (!BaseImageConfigDialog.isImagePossible())
93                 {
94                         _app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible");
95                         return;
96                 }
97
98                 updateBaseImageDetails();
99                 // Show dialog
100                 _dialog.setVisible(true);
101         }
102
103         /**
104          * Make the dialog components to select the export options
105          * @return Component holding gui elements
106          */
107         private Component makeDialogComponents()
108         {
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
114
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);
122
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)
129                         {
130                                 doExport();
131                                 MapGrouter.clearMapImage();
132                                 _dialog.dispose();
133                         }
134                 });
135                 buttonPanel.add(_okButton);
136                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
137                 cancelButton.addActionListener(new ActionListener() {
138                         public void actionPerformed(ActionEvent e)
139                         {
140                                 MapGrouter.clearMapImage();
141                                 _dialog.dispose();
142                         }
143                 });
144                 buttonPanel.add(cancelButton);
145                 panel.add(buttonPanel, BorderLayout.SOUTH);
146
147                 // Listener to close dialog if escape pressed
148                 KeyAdapter closer = new KeyAdapter() {
149                         public void keyReleased(KeyEvent e)
150                         {
151                                 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
152                                         _dialog.dispose();
153                                         MapGrouter.clearMapImage();
154                                 }
155                         }
156                 };
157                 _drawDataCheckbox.addKeyListener(closer);
158
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) {
168                                 changeBaseImage();
169                         }
170                 });
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))
175                 );
176
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));
184
185                 panel.add(holderPanel, BorderLayout.NORTH);
186                 return panel;
187         }
188
189         /**
190          * Change the base image by calling the BaseImageConfigDialog
191          */
192         private void changeBaseImage()
193         {
194                 // Check if there is a cache to use
195                 if (BaseImageConfigDialog.isImagePossible())
196                 {
197                         // Show new dialog to choose image details
198                         _baseImageConfig.beginWithImageYes();
199                 }
200         }
201
202         /**
203          * Callback from base image config dialog
204          */
205         public void dataUpdated(byte inUpdateType)
206         {
207                 updateBaseImageDetails();
208         }
209
210         /** Not required */
211         public void actionCompleted(String inMessage) {
212         }
213
214         /**
215          * Update the description label according to the selected base image details
216          */
217         private void updateBaseImageDetails()
218         {
219                 String desc = null;
220                 if (_baseImageConfig.useImage())
221                 {
222                         MapSource source = MapSourceLibrary.getSource(_baseImageConfig.getSourceIndex());
223                         if (source != null) {
224                                 desc = source.getName() + " ("
225                                         + _baseImageConfig.getZoomLevel() + ")";
226                         }
227                 }
228                 if (desc == null) {
229                         desc = I18nManager.getText("dialog.about.no");
230                 }
231                 _baseImageLabel.setText(desc);
232                 _okButton.setEnabled(_baseImageConfig.useImage() && _baseImageConfig.getFoundData()
233                         && MapGrouter.isZoomLevelOk(_app.getTrackInfo().getTrack(), _baseImageConfig.getZoomLevel()));
234         }
235
236         /**
237          * Select the file and export data to it
238          */
239         private void doExport()
240         {
241                 // OK pressed, so choose output file
242                 _okButton.setEnabled(false);
243                 if (_fileChooser == null)
244                 {
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));}
252                 }
253
254                 // Allow choose again if an existing file is selected
255                 boolean chooseAgain = false;
256                 do
257                 {
258                         chooseAgain = false;
259                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
260                         {
261                                 // OK pressed and file chosen
262                                 File pngFile = _fileChooser.getSelectedFile();
263                                 if (!pngFile.getName().toLowerCase().endsWith(".png"))
264                                 {
265                                         pngFile = new File(pngFile.getAbsolutePath() + ".png");
266                                 }
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)
274                                 {
275                                         // Export the file
276                                         if (!exportFile(pngFile))
277                                         {
278                                                 // export failed so need to choose again
279                                                 chooseAgain = true;
280                                         }
281                                 }
282                                 else
283                                 {
284                                         // overwrite cancelled so need to choose again
285                                         chooseAgain = true;
286                                 }
287                         }
288                 } while (chooseAgain);
289         }
290
291         /**
292          * Export the track data to the specified file
293          * @param inPngFile File object to save to
294          * @return true if successful
295          */
296         private boolean exportFile(File inPngFile)
297         {
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())
303                 {
304                         _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
305                         return true;
306                 }
307                 try
308                 {
309                         if (_drawDataCheckbox.isSelected())
310                         {
311                                 // Draw the track on top of this image
312                                 drawData(baseImage);
313                         }
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
318                         }
319                 }
320                 catch (IOException ioe) {
321                         System.err.println("Can't write image: " + ioe.getClass().getName());
322                 }
323                 return true;
324         }
325
326         /**
327          * Draw the track and waypoint data from the current Track onto the given image
328          * @param inImage GroutedImage from map tiles
329          */
330         private void drawData(GroutedImage inImage)
331         {
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));
339
340                 // Loop over points
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++)
345                 {
346                         DataPoint point = track.getPoint(i);
347                         if (!point.isWaypoint())
348                         {
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);
357                                 }
358                                 // draw this point
359                                 g.drawRect(px-2, py-2, 3, 3);
360                                 // save coordinates
361                                 prevX = px; prevY = py;
362                         }
363                 }
364                 // Draw waypoints
365                 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
366                 g.setColor(textColour);
367                 // Loop over points
368                 for (int i=0; i<numPoints; i++)
369                 {
370                         DataPoint point = track.getPoint(i);
371                         if (point.isWaypoint())
372                         {
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);
379                         }
380                 }
381                 // Set text size according to input
382                 int fontScalePercent = _textScaleField.getValue();
383                 if (fontScalePercent > 10 && fontScalePercent <= 999)
384                 {
385                         Font gFont = g.getFont();
386                         g.setFont(gFont.deriveFont((float) (gFont.getSize() * 0.01 * fontScalePercent)));
387                 }
388                 FontMetrics fm = g.getFontMetrics();
389                 final int nameHeight = fm.getHeight();
390                 final int imageSize = inImage.getImageSize();
391
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++)
396                 {
397                         DataPoint point = track.getPoint(i);
398                         if (point.hasMedia())
399                         {
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);
406                         }
407                 }
408
409                 // Loop over points again, now draw names for waypoints
410                 g.setColor(textColour);
411                 for (int i=0; i<numPoints; i++)
412                 {
413                         DataPoint point = track.getPoint(i);
414                         if (point.isWaypoint())
415                         {
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);
419
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)
428                                 {
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++)
434                                         {
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))
439                                                 {
440                                                         // Found a rectangle to fit - draw name here and quit
441                                                         g.drawString(waypointName, nameXs[a], nameYs[a]);
442                                                         drawnName = true;
443                                                         break;
444                                                 }
445                                         }
446                                 }
447                         }
448                 }
449
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
453         }
454 }