package tim.prune.save;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import tim.prune.App;
import tim.prune.GenericFunction;
import tim.prune.I18nManager;
import tim.prune.config.ColourScheme;
import tim.prune.config.Config;
import tim.prune.data.DataPoint;
import tim.prune.data.DoubleRange;
import tim.prune.data.Track;
import tim.prune.gui.BaseImageDefinitionPanel;
import tim.prune.gui.GuiGridLayout;
import tim.prune.gui.WholeNumberField;
import tim.prune.gui.colour.PointColourer;
import tim.prune.gui.map.MapSource;
import tim.prune.gui.map.MapSourceLibrary;
import tim.prune.gui.map.MapUtils;
import tim.prune.load.GenericFileFilter;
import tim.prune.threedee.ImageDefinition;
/**
* Class to handle the exporting of map images, optionally with track data drawn on top.
* This allows images larger than the screen to be generated.
*/
public class ImageExporter extends GenericFunction implements BaseImageConsumer
{
private JDialog _dialog = null;
private JCheckBox _drawDataCheckbox = null;
private JCheckBox _drawTrackPointsCheckbox = null;
private WholeNumberField _textScaleField = null;
private BaseImageDefinitionPanel _baseImagePanel = null;
private JFileChooser _fileChooser = null;
private JButton _okButton = null;
/**
* Constructor
* @param inApp App object
*/
public ImageExporter(App inApp)
{
super(inApp);
}
/** Get the name key */
public String getNameKey() {
return "function.exportimage";
}
/**
* Begin the function by showing the input dialog
*/
public void begin()
{
// Make dialog window
if (_dialog == null)
{
_dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
_dialog.setLocationRelativeTo(_parentFrame);
_dialog.getContentPane().add(makeDialogComponents());
_dialog.pack();
_textScaleField.setValue(100);
}
// Check if there is a cache to use
if (!BaseImageConfigDialog.isImagePossible())
{
_app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible");
return;
}
_baseImagePanel.updateBaseImageDetails();
baseImageChanged();
// Show dialog
_dialog.setVisible(true);
}
/**
* Make the dialog components to select the export options
* @return Component holding gui elements
*/
private Component makeDialogComponents()
{
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout(4, 4));
// Checkbox for drawing track or not
_drawDataCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrack"));
_drawDataCheckbox.setSelected(true); // draw by default
// Also whether to draw track points or not
_drawTrackPointsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrackpoints"));
_drawTrackPointsCheckbox.setSelected(true);
// Add listener to en/disable trackpoints checkbox
_drawDataCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
_drawTrackPointsCheckbox.setEnabled(_drawDataCheckbox.isSelected());
}
});
// TODO: Maybe have other controls such as line width, symbol scale factor
JPanel controlsPanel = new JPanel();
GuiGridLayout grid = new GuiGridLayout(controlsPanel);
grid.add(new JLabel(I18nManager.getText("dialog.exportimage.textscalepercent") + ": "));
_textScaleField = new WholeNumberField(3);
_textScaleField.setText("888");
grid.add(_textScaleField);
// OK, Cancel buttons
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
_okButton = new JButton(I18nManager.getText("button.ok"));
_okButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
doExport();
_baseImagePanel.getGrouter().clearMapImage();
_dialog.dispose();
}
});
buttonPanel.add(_okButton);
JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
cancelButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
_baseImagePanel.getGrouter().clearMapImage();
_dialog.dispose();
}
});
buttonPanel.add(cancelButton);
panel.add(buttonPanel, BorderLayout.SOUTH);
// Listener to close dialog if escape pressed
KeyAdapter closer = new KeyAdapter() {
public void keyReleased(KeyEvent e)
{
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
_dialog.dispose();
_baseImagePanel.getGrouter().clearMapImage();
}
}
};
_drawDataCheckbox.addKeyListener(closer);
// Panel for the base image
_baseImagePanel = new BaseImageDefinitionPanel(this, _dialog, _app.getTrackInfo().getTrack());
// Panel for the checkboxes at the top
JPanel checkPanel = new JPanel();
checkPanel.setLayout(new BoxLayout(checkPanel, BoxLayout.Y_AXIS));
checkPanel.add(_drawDataCheckbox);
checkPanel.add(_drawTrackPointsCheckbox);
// add these panels to the holder panel
JPanel holderPanel = new JPanel();
holderPanel.setLayout(new BorderLayout(5, 5));
holderPanel.add(checkPanel, BorderLayout.NORTH);
holderPanel.add(controlsPanel, BorderLayout.CENTER);
holderPanel.add(_baseImagePanel, BorderLayout.SOUTH);
holderPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
panel.add(holderPanel, BorderLayout.NORTH);
return panel;
}
/**
* Select the file and export data to it
*/
private void doExport()
{
_okButton.setEnabled(false);
// OK pressed, so choose output file
if (_fileChooser == null)
{
_fileChooser = new JFileChooser();
_fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
_fileChooser.setFileFilter(new GenericFileFilter("filetype.png", new String[] {"png"}));
_fileChooser.setAcceptAllFileFilterUsed(false);
// start from directory in config which should be set
final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
}
// Allow choose again if an existing file is selected
boolean chooseAgain = false;
do
{
chooseAgain = false;
if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
{
// OK pressed and file chosen
File pngFile = _fileChooser.getSelectedFile();
if (!pngFile.getName().toLowerCase().endsWith(".png"))
{
pngFile = new File(pngFile.getAbsolutePath() + ".png");
}
// Check if file exists and if necessary prompt for overwrite
Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
if (!pngFile.exists() || JOptionPane.showOptionDialog(_parentFrame,
I18nManager.getText("dialog.save.overwrite.text"),
I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
== JOptionPane.YES_OPTION)
{
// Export the file
if (!exportFile(pngFile))
{
// export failed so need to choose again
chooseAgain = true;
}
}
else
{
// overwrite cancelled so need to choose again
chooseAgain = true;
}
}
} while (chooseAgain);
}
/**
* Export the track data to the specified file
* @param inPngFile File object to save to
* @return true if successful
*/
private boolean exportFile(File inPngFile)
{
// Get the image file from the grouter
ImageDefinition imageDef = _baseImagePanel.getImageDefinition();
MapSource source = MapSourceLibrary.getSource(imageDef.getSourceIndex());
MapGrouter grouter = _baseImagePanel.getGrouter();
GroutedImage baseImage = grouter.getMapImage(_app.getTrackInfo().getTrack(), source,
imageDef.getZoom());
if (baseImage == null || !baseImage.isValid())
{
_app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
return true;
}
try
{
if (_drawDataCheckbox.isSelected())
{
// Draw the track on top of this image
drawData(baseImage);
}
// Write composite image to file
if (!ImageIO.write(baseImage.getImage(), "png", inPngFile)) {
_app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
return false; // choose again - the image creation worked but the save failed
}
}
catch (IOException ioe) {
System.err.println("Can't write image: " + ioe.getClass().getName());
}
return true;
}
/**
* Draw the track and waypoint data from the current Track onto the given image
* @param inImage GroutedImage from map tiles
*/
private void drawData(GroutedImage inImage)
{
// Work out x, y limits for drawing
DoubleRange xRange = inImage.getXRange();
DoubleRange yRange = inImage.getYRange();
final int zoomFactor = 1 << _baseImagePanel.getImageDefinition().getZoom();
Graphics g = inImage.getImage().getGraphics();
// TODO: Set line width, style etc
final PointColourer pointColourer = _app.getPointColourer();
final Color defaultPointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
g.setColor(defaultPointColour);
// Loop to draw all track points
final Track track = _app.getTrackInfo().getTrack();
final int numPoints = track.getNumPoints();
int prevX = 0, prevY = 0;
for (int i=0; i<numPoints; i++)
{
DataPoint point = track.getPoint(i);
if (!point.isWaypoint())
{
// Determine what colour to use to draw the track point
if (pointColourer != null)
{
Color c = pointColourer.getColour(i);
g.setColor(c == null ? defaultPointColour : c);
}
double x = track.getX(i) - xRange.getMinimum();
double y = track.getY(i) - yRange.getMinimum();
// use zoom level to calculate pixel coords on image
int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
// System.out.println("Point: x=" + x + ", px=" + px + ", y=" + y + ", py=" + py);
if (!point.getSegmentStart()) {
// draw from previous point to this one
g.drawLine(prevX, prevY, px, py);
}
// Only draw points if requested
if (_drawTrackPointsCheckbox.isSelected())
{
g.drawRect(px-2, py-2, 3, 3);
}
// save coordinates
prevX = px; prevY = py;
}
}
// Now the waypoints
final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
g.setColor(textColour);
// Loop again to draw waypoints
for (int i=0; i<numPoints; i++)
{
DataPoint point = track.getPoint(i);
if (point.isWaypoint())
{
// draw blob for each waypoint
double x = track.getX(i) - xRange.getMinimum();
double y = track.getY(i) - yRange.getMinimum();
// use zoom level to calculate pixel coords on image
int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
g.fillRect(px-3, py-3, 6, 6);
}
}
// Set text size according to input
int fontScalePercent = _textScaleField.getValue();
if (fontScalePercent > 10 && fontScalePercent <= 999)
{
Font gFont = g.getFont();
g.setFont(gFont.deriveFont((float) (gFont.getSize() * 0.01 * fontScalePercent)));
}
FontMetrics fm = g.getFontMetrics();
final int nameHeight = fm.getHeight();
final int imageSize = inImage.getImageSize();
// Loop over points again, draw photo points
final Color photoColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
g.setColor(photoColour);
for (int i=0; i<numPoints; i++)
{
DataPoint point = track.getPoint(i);
if (point.hasMedia())
{
// draw blob for each photo
double x = track.getX(i) - xRange.getMinimum();
double y = track.getY(i) - yRange.getMinimum();
// use zoom level to calculate pixel coords on image
int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
g.fillRect(px-3, py-3, 6, 6);
}
}
// Loop over points again, now draw names for waypoints
g.setColor(textColour);
for (int i=0; i<numPoints; i++)
{
DataPoint point = track.getPoint(i);
if (point.isWaypoint())
{
double x = track.getX(i) - xRange.getMinimum();
double y = track.getY(i) - yRange.getMinimum();
int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
// Figure out where to draw waypoint name so it doesn't obscure track
String waypointName = point.getWaypointName();
int nameWidth = fm.stringWidth(waypointName);
boolean drawnName = false;
// Make arrays for coordinates right left up down
int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
{
// Shift arrays for coordinates right left up down
nameXs[0] += 2; nameXs[1] -= 2;
nameYs[2] -= 2; nameYs[3] += 2;
// Check each direction in turn right left up down
for (int a=0; a<4; a++)
{
if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < imageSize
&& nameYs[a] < imageSize && (nameYs[a] - nameHeight) > 0
&& !MapUtils.overlapsPoints(inImage.getImage(), nameXs[a], nameYs[a],
nameWidth, nameHeight, textColour))
{
// Found a rectangle to fit - draw name here and quit
g.drawString(waypointName, nameXs[a], nameYs[a]);
drawnName = true;
break;
}
}
}
}
}
// Maybe draw note at the bottom, export from GpsPrune? Filename?
// Note: Differences from main map: No mapPosition (modifying position and visible points),
// no selection, no opacities, maybe different scale/text factors
}
/**
* Base image has changed, need to enable/disable ok button
*/
public void baseImageChanged()
{
final boolean useImage = _baseImagePanel.getImageDefinition().getUseImage();
final int zoomLevel = _baseImagePanel.getImageDefinition().getZoom();
final boolean okEnabled = useImage && _baseImagePanel.getFoundData()
&& MapGrouter.isZoomLevelOk(_app.getTrackInfo().getTrack(), zoomLevel);
_okButton.setEnabled(okEnabled);
}
}