import javax.imageio.ImageWriter;
import javax.swing.Box;
import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
+import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import tim.prune.UpdateMessageBroker;
import tim.prune.config.ColourUtils;
import tim.prune.config.Config;
-import tim.prune.data.Altitude;
import tim.prune.data.Coordinate;
import tim.prune.data.DataPoint;
import tim.prune.data.Field;
+import tim.prune.data.RecentFile;
+import tim.prune.data.Timestamp;
import tim.prune.data.Track;
import tim.prune.data.TrackInfo;
-import tim.prune.gui.ColourChooser;
-import tim.prune.gui.ColourPatch;
+import tim.prune.data.UnitSetLibrary;
import tim.prune.gui.DialogCloser;
import tim.prune.gui.ImageUtils;
+import tim.prune.gui.WholeNumberField;
+import tim.prune.gui.colour.ColourChooser;
+import tim.prune.gui.colour.ColourPatch;
import tim.prune.load.GenericFileFilter;
+import tim.prune.save.xml.XmlUtils;
/**
* Class to export track information
private JDialog _dialog = null;
private JTextField _descriptionField = null;
private PointTypeSelector _pointTypeSelector = null;
+ private JRadioButton _gxExtensionsRadio = null;
private JCheckBox _altitudesCheckbox = null;
private JCheckBox _kmzCheckbox = null;
private JCheckBox _exportImagesCheckbox = null;
+ private JLabel _imageSizeLabel = null;
+ private WholeNumberField _imageSizeField = null;
private ColourPatch _colourPatch = null;
private JLabel _progressLabel = null;
private JProgressBar _progressBar = null;
private static final String KML_FILENAME_IN_KMZ = "doc.kml";
// Default width and height of thumbnail images in Kmz
private static final int DEFAULT_THUMBNAIL_WIDTH = 240;
- private static final int DEFAULT_THUMBNAIL_HEIGHT = 240;
// Default track colour
private static final Color DEFAULT_TRACK_COLOUR = new Color(204, 0, 0); // red
_dialog.pack();
_colourChooser = new ColourChooser(_dialog);
}
+ // Fill in image size from config
+ _imageSizeField.setValue(Config.getConfigInt(Config.KEY_KMZ_IMAGE_SIZE));
enableCheckboxes();
_descriptionField.setEnabled(true);
_okButton.setEnabled(true);
colourPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.trackcolour")));
colourPanel.add(_colourPatch);
mainPanel.add(colourPanel);
+ // Pair of radio buttons for standard/extended KML
+ JRadioButton standardKmlRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.standardkml"));
+ _gxExtensionsRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.extendedkml"));
+ ButtonGroup bGroup = new ButtonGroup();
+ bGroup.add(standardKmlRadio); bGroup.add(_gxExtensionsRadio);
+ JPanel radioPanel = new JPanel();
+ radioPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 1));
+ radioPanel.add(standardKmlRadio);
+ radioPanel.add(_gxExtensionsRadio);
+ standardKmlRadio.setSelected(true);
+ mainPanel.add(radioPanel);
// Checkbox for altitude export
_altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
_altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
_altitudesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
mainPanel.add(_altitudesCheckbox);
+
// Checkboxes for kmz export and image export
_kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
_kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
_kmzCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
+ // enable image checkbox if kmz activated
_kmzCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
- // enable image checkbox if kmz activated
enableCheckboxes();
}
});
_exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages"));
_exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
_exportImagesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT);
+ // enable image size fields if image checkbox changes
+ _exportImagesCheckbox.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent arg0) {
+ enableImageSizeFields();
+ }
+ });
mainPanel.add(_exportImagesCheckbox);
+ // Panel for the image size
+ JPanel imageSizePanel = new JPanel();
+ imageSizePanel.setLayout(new FlowLayout(FlowLayout.CENTER));
+ _imageSizeLabel = new JLabel(I18nManager.getText("dialog.exportkml.imagesize"));
+ _imageSizeLabel.setAlignmentX(Component.RIGHT_ALIGNMENT);
+ imageSizePanel.add(_imageSizeLabel);
+ _imageSizeField = new WholeNumberField(4);
+ imageSizePanel.add(_imageSizeField);
+ mainPanel.add(imageSizePanel);
+
mainPanel.add(Box.createVerticalStrut(10));
_progressLabel = new JLabel("...");
_progressLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
_exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
_exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
+ enableImageSizeFields();
+ }
+
+ /**
+ * Enable and disable the image size fields according to the checkboxes
+ */
+ private void enableImageSizeFields()
+ {
+ boolean exportImages = _exportImagesCheckbox.isEnabled() && _exportImagesCheckbox.isSelected();
+ _imageSizeField.setEnabled(exportImages);
+ _imageSizeLabel.setEnabled(exportImages);
}
+ /**
+ * @return true if using gx extensions for kml export
+ */
+ private boolean useGxExtensions() {
+ return _gxExtensionsRadio.isSelected();
+ }
/**
* Start the export process based on the input parameters
*/
boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected();
_progressBar.setMaximum(exportImages?getNumPhotosToExport():1);
- // Determine photo thumbnail size from config
- int thumbWidth = Config.getConfigInt(Config.KEY_KMZ_IMAGE_WIDTH);
- if (thumbWidth < DEFAULT_THUMBNAIL_WIDTH) {thumbWidth = DEFAULT_THUMBNAIL_WIDTH;}
- int thumbHeight = Config.getConfigInt(Config.KEY_KMZ_IMAGE_HEIGHT);
- if (thumbHeight < DEFAULT_THUMBNAIL_HEIGHT) {thumbHeight = DEFAULT_THUMBNAIL_HEIGHT;}
// Create array for image dimensions in case it's required
_imageDimensions = new Dimension[_track.getNumPoints()];
// Export images into zip file too if requested
if (exportImages)
{
+ // Get entered value for image size, store in config
+ int thumbSize = _imageSizeField.getValue();
+ if (thumbSize < DEFAULT_THUMBNAIL_WIDTH) {thumbSize = DEFAULT_THUMBNAIL_WIDTH;}
+ Config.setConfigInt(Config.KEY_KMZ_IMAGE_SIZE, thumbSize);
+
// Create thumbnails of each photo in turn and add to zip as images/image<n>.jpg
// This is done first so that photo sizes are known for later
- exportThumbnails(zipOutputStream, thumbWidth, thumbHeight);
+ exportThumbnails(zipOutputStream, thumbSize);
}
writer = new OutputStreamWriter(zipOutputStream);
// Make an entry in the zip file for the kml file
_imageDimensions = null;
// Store directory in config for later
Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
+ // Add to recent file list
+ Config.getRecentFileList().addFile(new RecentFile(_exportFile, true));
// show confirmation
+ UpdateMessageBroker.informSubscribers();
UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
+ " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
+ " " + _exportFile.getAbsolutePath());
boolean writePhotos = _pointTypeSelector.getPhotopointsSelected();
boolean writeAudios = _pointTypeSelector.getAudiopointsSelected();
boolean justSelection = _pointTypeSelector.getJustSelection();
- inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
- inWriter.write("\t<name>");
+ // Define xml header (depending on whether extensions are used or not)
+ if (useGxExtensions()) {
+ inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.2\" xmlns:gx=\"http://www.google.com/kml/ext/2.2\">\n");
+ }
+ else {
+ inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n");
+ }
+ inWriter.write("<Folder>\n\t<name>");
if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
{
- inWriter.write(_descriptionField.getText());
+ inWriter.write(XmlUtils.fixCdata(_descriptionField.getText()));
}
else {
- inWriter.write("Export from Prune");
+ inWriter.write("Export from GpsPrune");
}
inWriter.write("</name>\n");
}
// Make a blob with description for each photo
// Photos have already been written so picture sizes already known
- if (point.getPhoto() != null && writePhotos && writeCurrentPoint)
+ if (point.getPhoto() != null && point.getPhoto().isValid() && writePhotos && writeCurrentPoint)
{
if (!writtenPhotoHeader)
{
exportPhotoPoint(point, inWriter, inExportImages, i, photoNum, absoluteAltitudes);
numSaved++;
}
- // Make a blob with description for each audio file
+ // Make a blob with description for each audio clip
if (point.getAudio() != null && writeAudios && writeCurrentPoint)
{
if (!writtenAudioHeader)
// Make a line for the track, if there is one
if (hasTrackpoints && writeTrack)
{
- // Set up strings for start and end of track segment
- String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
- + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
- + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
- + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
- + "\t\t</Style>\n\t\t<LineString>\n";
- if (absoluteAltitudes) {
- trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
+ boolean useGxExtensions = _gxExtensionsRadio.isSelected();
+ if (useGxExtensions)
+ {
+ // Write track using the Google Extensions to KML including gx:Track
+ numSaved += writeGxTrack(inWriter, absoluteAltitudes, selStart, selEnd);
}
else {
- trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
+ // Write track using standard KML
+ numSaved += writeStandardTrack(inWriter, absoluteAltitudes, selStart, selEnd);
+ }
+ }
+ inWriter.write("</Folder>\n</kml>\n");
+ return numSaved;
+ }
+
+
+ /**
+ * Write out the track using standard KML LineString tag
+ * @param inWriter writer object to write to
+ * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground
+ * @param inSelStart start index of selection, or -1 if whole track
+ * @param inSelEnd end index of selection, or -1 if whole track
+ * @return number of track points written
+ */
+ private int writeStandardTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart,
+ int inSelEnd)
+ throws IOException
+ {
+ int numSaved = 0;
+ // Set up strings for start and end of track segment
+ String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
+ + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
+ + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
+ + "\t\t</Style>\n\t\t<LineString>\n";
+ if (inAbsoluteAltitudes) {
+ trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
+ }
+ else {
+ trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
+ }
+ trackStart += "\t\t\t<coordinates>";
+ String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
+
+ boolean justSelection = _pointTypeSelector.getJustSelection();
+
+ // Start segment
+ inWriter.write(trackStart);
+ // Loop over track points
+ boolean firstTrackpoint = true;
+ final int numPoints = _track.getNumPoints();
+ for (int i=0; i<numPoints; i++)
+ {
+ DataPoint point = _track.getPoint(i);
+ boolean writeCurrentPoint = !justSelection || (i>=inSelStart && i<=inSelEnd);
+ if (!point.isWaypoint() && writeCurrentPoint)
+ {
+ // start new track segment if necessary
+ if (point.getSegmentStart() && !firstTrackpoint) {
+ inWriter.write(trackEnd);
+ inWriter.write(trackStart);
+ }
+ if (point.getPhoto() == null)
+ {
+ exportTrackpoint(point, inWriter);
+ numSaved++;
+ firstTrackpoint = false;
+ }
}
- trackStart += "\t\t\t<coordinates>";
- String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
-
- // Start segment
- inWriter.write(trackStart);
- // Loop over track points
- boolean firstTrackpoint = true;
- for (i=0; i<numPoints; i++)
+ }
+ // end segment
+ inWriter.write(trackEnd);
+ return numSaved;
+ }
+
+
+ /**
+ * Write out the track using Google's KML Extensions such as gx:Track
+ * @param inWriter writer object to write to
+ * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground
+ * @param inSelStart start index of selection, or -1 if whole track
+ * @param inSelEnd end index of selection, or -1 if whole track
+ * @return number of track points written
+ */
+ private int writeGxTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart,
+ int inSelEnd)
+ throws IOException
+ {
+ int numSaved = 0;
+ // Set up strings for start and end of track segment
+ String trackStart = "\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
+ + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
+ + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
+ + "\t\t</Style>\n\t\t<gx:Track>\n";
+ if (inAbsoluteAltitudes) {
+ trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
+ }
+ else {
+ trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
+ }
+ String trackEnd = "\n\t\t</gx:Track>\n\t</Placemark>\n";
+
+ boolean justSelection = _pointTypeSelector.getJustSelection();
+
+ // Start segment
+ inWriter.write(trackStart);
+ StringBuilder whenList = new StringBuilder();
+ StringBuilder coordList = new StringBuilder();
+
+ // Loop over track points
+ boolean firstTrackpoint = true;
+ final int numPoints = _track.getNumPoints();
+ for (int i=0; i<numPoints; i++)
+ {
+ DataPoint point = _track.getPoint(i);
+ boolean writeCurrentPoint = !justSelection || (i>=inSelStart && i<=inSelEnd);
+ if (!point.isWaypoint() && writeCurrentPoint)
{
- point = _track.getPoint(i);
- boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd);
- if (!point.isWaypoint() && writeCurrentPoint)
+ // start new track segment if necessary
+ if (point.getSegmentStart() && !firstTrackpoint)
+ {
+ inWriter.write(whenList.toString());
+ inWriter.write('\n');
+ inWriter.write(coordList.toString());
+ inWriter.write('\n');
+ inWriter.write(trackEnd);
+ whenList.setLength(0); coordList.setLength(0);
+ inWriter.write(trackStart);
+ }
+ if (point.getPhoto() == null)
{
- // start new track segment if necessary
- if (point.getSegmentStart() && !firstTrackpoint) {
- inWriter.write(trackEnd);
- inWriter.write(trackStart);
+ // Add timestamp (if any) to the list
+ whenList.append("<when>");
+ if (point.hasTimestamp()) {
+ whenList.append(point.getTimestamp().getText(Timestamp.Format.ISO8601));
+ }
+ whenList.append("</when>\n");
+ // Add coordinates to the list
+ coordList.append("<gx:coord>");
+ coordList.append(point.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' ');
+ coordList.append(point.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' ');
+ if (point.hasAltitude()) {
+ coordList.append("" + point.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
}
- if (point.getPhoto() == null)
- {
- exportTrackpoint(point, inWriter);
- numSaved++;
- firstTrackpoint = false;
+ else {
+ coordList.append('0');
}
+ coordList.append("</gx:coord>\n");
+ numSaved++;
+ firstTrackpoint = false;
}
}
- // end segment
- inWriter.write(trackEnd);
}
- inWriter.write("</Folder>\n</kml>");
+ // end segment
+ inWriter.write(whenList.toString());
+ inWriter.write('\n');
+ inWriter.write(coordList.toString());
+ inWriter.write('\n');
+ inWriter.write(trackEnd);
return numSaved;
}
+
/**
* Reverse the hex code for the colours for KML's stupid backwards format
* @param inCode colour code rrggbb
private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
{
String name = inPoint.getWaypointName().trim();
- exportNamedPoint(inPoint, inWriter, name, null, null, inAbsoluteAltitude);
+ exportNamedPoint(inPoint, inWriter, name, inPoint.getFieldValue(Field.DESCRIPTION), null, inAbsoluteAltitude);
}
*/
private void exportAudioPoint(DataPoint inPoint, Writer inWriter, boolean inAbsoluteAltitude) throws IOException
{
- String name = inPoint.getAudio().getFile().getName();
- String desc = inPoint.getAudio().getFile().getAbsolutePath();
+ String name = inPoint.getAudio().getName();
+ String desc = null;
+ if (inPoint.getAudio().getFile() != null) {
+ desc = inPoint.getAudio().getFile().getAbsolutePath();
+ }
exportNamedPoint(inPoint, inWriter, name, desc, "audio_icon", inAbsoluteAltitude);
}
int inPointNumber, int inImageNumber, boolean inAbsoluteAltitude)
throws IOException
{
- String name = inPoint.getPhoto().getFile().getName();
+ String name = inPoint.getPhoto().getName();
String desc = null;
if (inImageLink)
{
// Create html for the thumbnail images
desc = "<![CDATA[<br/><table border='0'><tr><td><center><img src='images/image"
+ inImageNumber + ".jpg' width='" + imageSize.width + "' height='" + imageSize.height + "'></center></td></tr>"
- + "<tr><td><center>" + inPoint.getPhoto().getFile().getName() + "</center></td></tr></table>]]>";
+ + "<tr><td><center>" + name + "</center></td></tr></table>]]>";
}
// Export point
exportNamedPoint(inPoint, inWriter, name, desc, "camera_icon", inAbsoluteAltitude);
throws IOException
{
inWriter.write("\t<Placemark>\n\t\t<name>");
- inWriter.write(inName);
+ inWriter.write(XmlUtils.fixCdata(inName));
inWriter.write("</name>\n");
if (inDesc != null)
{
// Write out description
- inWriter.write("<description>");
- inWriter.write(inDesc);
- inWriter.write("</description>");
+ inWriter.write("\t\t<description>");
+ inWriter.write(XmlUtils.fixCdata(inDesc));
+ inWriter.write("</description>\n");
}
if (inStyle != null)
{
inWriter.write(',');
// Altitude if point has one
if (inPoint.hasAltitude()) {
- inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
+ inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
}
else {
inWriter.write('0');
// Altitude if point has one
inWriter.write(',');
if (inPoint.hasAltitude()) {
- inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
+ inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
}
else {
inWriter.write('0');
/**
* Loop through the photos and create thumbnails
* @param inZipStream zip stream to save image files to
- * @param inThumbWidth thumbnail width
- * @param inThumbHeight thumbnail height
+ * @param inThumbSize thumbnail size
*/
- private void exportThumbnails(ZipOutputStream inZipStream, int inThumbWidth, int inThumbHeight)
+ private void exportThumbnails(ZipOutputStream inZipStream, int inThumbSize)
throws IOException
{
// set up image writer
ImageWriter imageWriter = writers.next();
// Check selection checkbox
- boolean justSelection = _pointTypeSelector.getJustSelection();
+ final boolean justSelection = _pointTypeSelector.getJustSelection();
int selStart = -1, selEnd = -1;
if (justSelection) {
selStart = _trackInfo.getSelection().getStart();
selEnd = _trackInfo.getSelection().getEnd();
}
- int numPoints = _track.getNumPoints();
+ final int numPoints = _track.getNumPoints();
DataPoint point = null;
int photoNum = 0;
// Loop over all points in track
for (int i=0; i<numPoints && !_cancelPressed; i++)
{
point = _track.getPoint(i);
- if (point.getPhoto() != null && (!justSelection || (i>=selStart && i<=selEnd)))
+ if (point.getPhoto() != null && point.getPhoto().isValid() && (!justSelection || (i>=selStart && i<=selEnd)))
{
photoNum++;
// Make a new entry in zip file
ZipEntry entry = new ZipEntry("images/image" + photoNum + ".jpg");
inZipStream.putNextEntry(entry);
// Load image and write to outstream
- ImageIcon icon = new ImageIcon(point.getPhoto().getFile().getAbsolutePath());
+ ImageIcon icon = point.getPhoto().createImageIcon();
- // Scale and smooth image to required size
+ // Scale image to required size (not smoothed)
BufferedImage bufferedImage = ImageUtils.rotateImage(icon.getImage(),
- inThumbWidth, inThumbHeight, point.getPhoto().getRotationDegrees());
+ inThumbSize, inThumbSize, point.getPhoto().getRotationDegrees());
// Store image dimensions so that it doesn't have to be calculated again for the points
_imageDimensions[i] = new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight());