]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/save/ImageExporter.java
Version 20.4, May 2021
[GpsPrune.git] / src / 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.gui.map.WpIconDefinition;
44 import tim.prune.gui.map.WpIconLibrary;
45 import tim.prune.load.GenericFileFilter;
46 import tim.prune.threedee.ImageDefinition;
47
48 /**
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.
51  */
52 public class ImageExporter extends GenericFunction implements BaseImageConsumer
53 {
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;
61
62         /**
63          * Constructor
64          * @param inApp App object
65          */
66         public ImageExporter(App inApp)
67         {
68                 super(inApp);
69         }
70
71         /** Get the name key */
72         public String getNameKey() {
73                 return "function.exportimage";
74         }
75
76         /**
77          * Begin the function by showing the input dialog
78          */
79         public void begin()
80         {
81                 // Make dialog window
82                 if (_dialog == null)
83                 {
84                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
85                         _dialog.setLocationRelativeTo(_parentFrame);
86                         _dialog.getContentPane().add(makeDialogComponents());
87                         _dialog.pack();
88                         _textScaleField.setValue(100);
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                 _baseImagePanel.updateBaseImageDetails();
99                 baseImageChanged();
100                 // Show dialog
101                 _dialog.setVisible(true);
102         }
103
104         /**
105          * Make the dialog components to select the export options
106          * @return Component holding gui elements
107          */
108         private Component makeDialogComponents()
109         {
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());
122                         }
123                 });
124
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);
132
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)
139                         {
140                                 doExport();
141                                 _baseImagePanel.getGrouter().clearMapImage();
142                                 _dialog.dispose();
143                         }
144                 });
145                 buttonPanel.add(_okButton);
146                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
147                 cancelButton.addActionListener(new ActionListener() {
148                         public void actionPerformed(ActionEvent e)
149                         {
150                                 _baseImagePanel.getGrouter().clearMapImage();
151                                 _dialog.dispose();
152                         }
153                 });
154                 buttonPanel.add(cancelButton);
155                 panel.add(buttonPanel, BorderLayout.SOUTH);
156
157                 // Listener to close dialog if escape pressed
158                 KeyAdapter closer = new KeyAdapter() {
159                         public void keyReleased(KeyEvent e)
160                         {
161                                 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
162                                         _dialog.dispose();
163                                         _baseImagePanel.getGrouter().clearMapImage();
164                                 }
165                         }
166                 };
167                 _drawDataCheckbox.addKeyListener(closer);
168
169                 // Panel for the base image
170                 _baseImagePanel = new BaseImageDefinitionPanel(this, _dialog, _app.getTrackInfo().getTrack());
171
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);
177
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));
185
186                 panel.add(holderPanel, BorderLayout.NORTH);
187                 return panel;
188         }
189
190
191         /**
192          * Select the file and export data to it
193          */
194         private void doExport()
195         {
196                 _okButton.setEnabled(false);
197                 // OK pressed, so choose output file
198                 if (_fileChooser == null)
199                 {
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));}
207                 }
208
209                 // Allow choose again if an existing file is selected
210                 boolean chooseAgain = false;
211                 do
212                 {
213                         chooseAgain = false;
214                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
215                         {
216                                 // OK pressed and file chosen
217                                 File pngFile = _fileChooser.getSelectedFile();
218                                 if (!pngFile.getName().toLowerCase().endsWith(".png"))
219                                 {
220                                         pngFile = new File(pngFile.getAbsolutePath() + ".png");
221                                 }
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)
229                                 {
230                                         // Export the file
231                                         if (!exportFile(pngFile))
232                                         {
233                                                 // export failed so need to choose again
234                                                 chooseAgain = true;
235                                         }
236                                 }
237                                 else
238                                 {
239                                         // overwrite cancelled so need to choose again
240                                         chooseAgain = true;
241                                 }
242                         }
243                 } while (chooseAgain);
244         }
245
246         /**
247          * Export the track data to the specified file
248          * @param inPngFile File object to save to
249          * @return true if successful
250          */
251         private boolean exportFile(File inPngFile)
252         {
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,
258                         imageDef.getZoom());
259                 if (baseImage == null || !baseImage.isValid())
260                 {
261                         _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
262                         return true;
263                 }
264                 try
265                 {
266                         if (_drawDataCheckbox.isSelected())
267                         {
268                                 // Draw the track on top of this image
269                                 drawData(baseImage);
270                         }
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
275                         }
276                 }
277                 catch (IOException ioe) {
278                         System.err.println("Can't write image: " + ioe.getClass().getName());
279                 }
280                 return true;
281         }
282
283         /**
284          * Draw the track and waypoint data from the current Track onto the given image
285          * @param inImage GroutedImage from map tiles
286          */
287         private void drawData(GroutedImage inImage)
288         {
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);
298
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++)
305                 {
306                         DataPoint point = track.getPoint(i);
307                         if (!point.isWaypoint())
308                         {
309                                 // Determine what colour to use to draw the track point
310                                 if (pointColourer != null)
311                                 {
312                                         Color c = pointColourer.getColour(i);
313                                         g.setColor(c == null ? defaultPointColour : c);
314                                 }
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);
323                                 }
324                                 // Only draw points if requested
325                                 if (_drawTrackPointsCheckbox.isSelected())
326                                 {
327                                         g.drawRect(px-2, py-2, 3, 3);
328                                 }
329                                 // save coordinates
330                                 prevX = px; prevY = py;
331                                 gotPreviousPoint = true;
332                         }
333                 }
334
335                 // Now the waypoints
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)
341                 {
342                         wpIconDefinition = WpIconLibrary.getIconDefinition(wpType, WpIconLibrary.SIZE_MEDIUM);
343                 }
344                 // Loop again to draw waypoints
345                 for (int i=0; i<numPoints; i++)
346                 {
347                         DataPoint point = track.getPoint(i);
348                         if (point.isWaypoint())
349                         {
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)
357                                 {
358                                         g.fillRect(px-3, py-3, 6, 6);
359                                 }
360                                 else
361                                 {
362                                         g.drawImage(wpIconDefinition.getImageIcon().getImage(), px-wpIconDefinition.getXOffset(),
363                                                 py-wpIconDefinition.getYOffset(), null);
364                                 }
365                         }
366                 }
367                 // Set text size according to input
368                 int fontScalePercent = _textScaleField.getValue();
369                 if (fontScalePercent > 0 && fontScalePercent <= 999)
370                 {
371                         Font gFont = g.getFont();
372                         g.setFont(gFont.deriveFont((float) (gFont.getSize() * 0.01 * fontScalePercent)));
373                 }
374                 FontMetrics fm = g.getFontMetrics();
375                 final int nameHeight = fm.getHeight();
376                 final int imageSize = inImage.getImageSize();
377
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++)
382                 {
383                         DataPoint point = track.getPoint(i);
384                         if (point.hasMedia())
385                         {
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);
392                         }
393                 }
394
395                 // Loop over points again, now draw names for waypoints
396                 g.setColor(textColour);
397                 for (int i=0; i<numPoints; i++)
398                 {
399                         DataPoint point = track.getPoint(i);
400                         if (point.isWaypoint() && fontScalePercent > 0)
401                         {
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);
405
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)
414                                 {
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++)
420                                         {
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))
425                                                 {
426                                                         // Found a rectangle to fit - draw name here and quit
427                                                         g.drawString(waypointName, nameXs[a], nameYs[a]);
428                                                         drawnName = true;
429                                                         break;
430                                                 }
431                                         }
432                                 }
433                         }
434                 }
435
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
439         }
440
441         /**
442          * Base image has changed, need to enable/disable ok button
443          */
444         public void baseImageChanged()
445         {
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);
451         }
452 }