1 package tim.prune.save;
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.FlowLayout;
6 import java.awt.GridLayout;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.event.KeyAdapter;
10 import java.awt.event.KeyEvent;
12 import java.io.FileOutputStream;
13 import java.io.IOException;
14 import java.io.OutputStreamWriter;
15 import java.io.Writer;
16 import java.nio.charset.Charset;
18 import javax.swing.BorderFactory;
19 import javax.swing.Box;
20 import javax.swing.BoxLayout;
21 import javax.swing.JButton;
22 import javax.swing.JCheckBox;
23 import javax.swing.JDialog;
24 import javax.swing.JFileChooser;
25 import javax.swing.JFrame;
26 import javax.swing.JLabel;
27 import javax.swing.JOptionPane;
28 import javax.swing.JPanel;
29 import javax.swing.JTextField;
32 import tim.prune.GenericFunction;
33 import tim.prune.GpsPruner;
34 import tim.prune.I18nManager;
35 import tim.prune.UpdateMessageBroker;
36 import tim.prune.config.Config;
37 import tim.prune.data.Altitude;
38 import tim.prune.data.Coordinate;
39 import tim.prune.data.DataPoint;
40 import tim.prune.data.Field;
41 import tim.prune.data.Timestamp;
42 import tim.prune.data.TrackInfo;
43 import tim.prune.load.GenericFileFilter;
44 import tim.prune.save.xml.GpxCacherList;
48 * Class to export track information
49 * into a specified Gpx file
51 public class GpxExporter extends GenericFunction implements Runnable
53 private TrackInfo _trackInfo = null;
54 private JDialog _dialog = null;
55 private JTextField _nameField = null;
56 private JTextField _descriptionField = null;
57 private PointTypeSelector _pointTypeSelector = null;
58 private JCheckBox _timestampsCheckbox = null;
59 private JCheckBox _copySourceCheckbox = null;
60 private File _exportFile = null;
62 /** this program name */
63 private static final String GPX_CREATOR = "Prune v" + GpsPruner.VERSION_NUMBER + " activityworkshop.net";
68 * @param inApp app object
70 public GpxExporter(App inApp)
73 _trackInfo = inApp.getTrackInfo();
77 public String getNameKey() {
78 return "function.exportgpx";
82 * Show the dialog to select options and export file
89 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
90 _dialog.setLocationRelativeTo(_parentFrame);
91 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
92 _dialog.getContentPane().add(makeDialogComponents());
95 _pointTypeSelector.init(_app.getTrackInfo());
96 _dialog.setVisible(true);
101 * Create dialog components
102 * @return Panel containing all gui elements in dialog
104 private Component makeDialogComponents()
106 JPanel dialogPanel = new JPanel();
107 dialogPanel.setLayout(new BorderLayout());
108 JPanel mainPanel = new JPanel();
109 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
110 // Make a central panel with the text boxes
111 JPanel descPanel = new JPanel();
112 descPanel.setLayout(new GridLayout(2, 2));
113 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.name")));
114 _nameField = new JTextField(10);
115 descPanel.add(_nameField);
116 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.desc")));
117 _descriptionField = new JTextField(10);
118 descPanel.add(_descriptionField);
119 mainPanel.add(descPanel);
120 mainPanel.add(Box.createVerticalStrut(5));
121 // point type selection (track points, waypoints, photo points)
122 _pointTypeSelector = new PointTypeSelector();
123 mainPanel.add(_pointTypeSelector);
124 // checkboxes for timestamps and copying
125 JPanel checkPanel = new JPanel();
126 _timestampsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.includetimestamps"));
127 _timestampsCheckbox.setSelected(true);
128 checkPanel.add(_timestampsCheckbox);
129 _copySourceCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.copysource"));
130 _copySourceCheckbox.setSelected(true);
131 checkPanel.add(_copySourceCheckbox);
132 mainPanel.add(checkPanel);
133 dialogPanel.add(mainPanel, BorderLayout.CENTER);
135 // close dialog if escape pressed
136 _nameField.addKeyListener(new KeyAdapter() {
137 public void keyReleased(KeyEvent e) {
138 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
143 // button panel at bottom
144 JPanel buttonPanel = new JPanel();
145 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
146 JButton okButton = new JButton(I18nManager.getText("button.ok"));
147 ActionListener okListener = new ActionListener() {
148 public void actionPerformed(ActionEvent e)
153 okButton.addActionListener(okListener);
154 _descriptionField.addActionListener(okListener);
155 buttonPanel.add(okButton);
156 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
157 cancelButton.addActionListener(new ActionListener() {
158 public void actionPerformed(ActionEvent e) {
162 buttonPanel.add(cancelButton);
163 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
164 dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
170 * Start the export process based on the input parameters
172 private void startExport()
174 // OK pressed, so check selections
175 if (!_pointTypeSelector.getAnythingSelected()) {
176 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
177 I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
180 // Choose output file
181 File saveFile = chooseGpxFile(_parentFrame);
182 if (saveFile != null)
184 // New file or overwrite confirmed, so initiate export in separate thread
185 _exportFile = saveFile;
186 new Thread(this).start();
191 * Select a GPX file to save to
192 * @param inParentFrame parent frame for file chooser dialog
193 * @return selected File, or null if selection cancelled
195 public static File chooseGpxFile(JFrame inParentFrame)
197 File saveFile = null;
198 JFileChooser fileChooser = new JFileChooser();
199 fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
200 fileChooser.setFileFilter(new GenericFileFilter("filetype.gpx", new String[] {"gpx"}));
201 fileChooser.setAcceptAllFileFilterUsed(false);
202 // start from directory in config which should be set
203 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
204 if (configDir != null) {fileChooser.setCurrentDirectory(new File(configDir));}
206 // Allow choose again if an existing file is selected
207 boolean chooseAgain = false;
211 if (fileChooser.showSaveDialog(inParentFrame) == JFileChooser.APPROVE_OPTION)
213 // OK pressed and file chosen
214 File file = fileChooser.getSelectedFile();
215 // Check file extension
216 if (!file.getName().toLowerCase().endsWith(".gpx"))
218 file = new File(file.getAbsolutePath() + ".gpx");
220 // Check if file exists and if necessary prompt for overwrite
221 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
222 if (!file.exists() || JOptionPane.showOptionDialog(inParentFrame,
223 I18nManager.getText("dialog.save.overwrite.text"),
224 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
225 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
226 == JOptionPane.YES_OPTION)
228 // new file or overwrite confirmed
233 // file exists and overwrite cancelled - select again
237 } while (chooseAgain);
242 * Run method for controlling separate thread for exporting
246 OutputStreamWriter writer = null;
249 // normal writing to file
250 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
251 boolean[] saveFlags = {_pointTypeSelector.getTrackpointsSelected(), _pointTypeSelector.getWaypointsSelected(),
252 _pointTypeSelector.getPhotopointsSelected(), _pointTypeSelector.getJustSelection(),
253 _timestampsCheckbox.isSelected()};
255 final int numPoints = exportData(writer, _trackInfo, _nameField.getText(),
256 _descriptionField.getText(), saveFlags, _copySourceCheckbox.isSelected());
260 // Store directory in config for later
261 Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
263 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
264 + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
265 + " " + _exportFile.getAbsolutePath());
266 // export successful so need to close dialog and return
270 catch (IOException ioe)
272 // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
274 if (writer != null) writer.close();
276 catch (IOException ioe2) {}
277 JOptionPane.showMessageDialog(_parentFrame,
278 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
279 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
281 // if not returned already, export failed so need to recall the file selection
287 * Export the information to the given writer
288 * @param inWriter writer object
289 * @param inInfo track info object
290 * @param inName name of track (optional)
291 * @param inDesc description of track (optional)
292 * @param inSaveFlags array of booleans to export tracks, waypoints, photos, timestamps
293 * @param inUseCopy true to copy source if available
294 * @return number of points written
295 * @throws IOException if io errors occur on write
297 public static int exportData(OutputStreamWriter inWriter, TrackInfo inInfo, String inName,
298 String inDesc, boolean[] inSaveFlags, boolean inUseCopy) throws IOException
300 // Instantiate source file cachers in case we want to copy output
301 GpxCacherList gpxCachers = null;
302 if (inUseCopy) gpxCachers = new GpxCacherList(inInfo.getFileInfo());
303 // Write or copy headers
304 inWriter.write(getXmlHeaderString(inWriter));
305 inWriter.write(getGpxHeaderString(gpxCachers));
307 String trackName = "PruneTrack";
308 if (inName != null && !inName.equals(""))
311 inWriter.write("\t<name>");
312 inWriter.write(trackName);
313 inWriter.write("</name>\n");
316 inWriter.write("\t<desc>");
317 inWriter.write((inDesc != null && !inDesc.equals(""))?inDesc:"Export from Prune");
318 inWriter.write("</desc>\n");
321 DataPoint point = null;
322 boolean hasTrackpoints = false;
323 final boolean exportTrackpoints = inSaveFlags[0];
324 final boolean exportWaypoints = inSaveFlags[1];
325 final boolean exportPhotos = inSaveFlags[2];
326 final boolean exportSelection = inSaveFlags[3];
327 final boolean exportTimestamps = inSaveFlags[4];
329 int selStart = -1, selEnd = -1;
330 if (exportSelection) {
331 selStart = inInfo.getSelection().getStart();
332 selEnd = inInfo.getSelection().getEnd();
334 // Loop over waypoints
335 final int numPoints = inInfo.getTrack().getNumPoints();
337 for (i=0; i<numPoints; i++)
339 point = inInfo.getTrack().getPoint(i);
340 if (!exportSelection || (i>=selStart && i<=selEnd)) {
341 // Make a wpt element for each waypoint
342 if (point.isWaypoint()) {
345 String pointSource = (inUseCopy?getPointSource(gpxCachers, point):null);
346 if (pointSource != null) {
347 inWriter.write(pointSource);
348 inWriter.write('\n');
351 exportWaypoint(point, inWriter, exportTimestamps);
357 hasTrackpoints = true;
361 // Export both route points and then track points
362 if (hasTrackpoints && (exportTrackpoints || exportPhotos))
364 // Output all route points (if any)
365 numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
366 exportTimestamps, true, gpxCachers, "<rtept", "\t<rte><number>1</number>\n", null, "\t</rte>\n");
367 // Output all track points, if any
368 String trackStart = "\t<trk><name>" + trackName + "</name><number>1</number><trkseg>\n";
369 numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
370 exportTimestamps, false, gpxCachers, "<trkpt", trackStart, "\t</trkseg>\n\t<trkseg>\n",
371 "\t</trkseg></trk>\n");
374 inWriter.write("</gpx>\n");
379 * Loop through the track outputting the relevant track points
380 * @param inWriter writer object for output
381 * @param inInfo track info object containing track
382 * @param inExportSelection true to just output current selection
383 * @param inExportTrackpoints true to output track points
384 * @param inExportPhotos true to output photo points
385 * @param exportTimestamps true to include timestamps in export
386 * @param inOnlyCopies true to only export if source can be copied
387 * @param inCachers list of GpxCachers
388 * @param inPointTag tag to match for each point
389 * @param inStartTag start tag to output
390 * @param inSegmentTag tag to output between segments (or null)
391 * @param inEndTag end tag to output
393 private static int writeTrackPoints(OutputStreamWriter inWriter,
394 TrackInfo inInfo, boolean inExportSelection, boolean inExportTrackpoints,
395 boolean inExportPhotos, boolean exportTimestamps, boolean inOnlyCopies,
396 GpxCacherList inCachers, String inPointTag, String inStartTag,
397 String inSegmentTag, String inEndTag)
400 // Note: far too many input parameters to this method but avoids duplication
401 // of output functionality for writing track points and route points
402 int numPoints = inInfo.getTrack().getNumPoints();
403 int selStart = inInfo.getSelection().getStart();
404 int selEnd = inInfo.getSelection().getEnd();
406 // Loop over track points
407 for (int i=0; i<numPoints; i++)
409 DataPoint point = inInfo.getTrack().getPoint(i);
410 if ((!inExportSelection || (i>=selStart && i<=selEnd)) && !point.isWaypoint())
412 if ((point.getPhoto()==null && inExportTrackpoints) || (point.getPhoto()!=null && inExportPhotos))
414 // get the source from the point (if any)
415 String pointSource = getPointSource(inCachers, point);
416 boolean writePoint = (pointSource != null && pointSource.toLowerCase().startsWith(inPointTag))
417 || (pointSource == null && !inOnlyCopies);
420 // restart track segment if necessary
421 if ((numSaved > 0) && point.getSegmentStart() && (inSegmentTag != null)) {
422 inWriter.write(inSegmentTag);
424 if (numSaved == 0) {inWriter.write(inStartTag);}
425 if (pointSource != null) {
426 inWriter.write(pointSource);
427 inWriter.write('\n');
430 if (!inOnlyCopies) {exportTrackpoint(point, inWriter, exportTimestamps);}
437 if (numSaved > 0) {inWriter.write(inEndTag);}
443 * Get the point source for the specified point
444 * @param inCachers list of GPX cachers to ask for source
445 * @param inPoint point object
446 * @return xml source if available, or null otherwise
448 private static String getPointSource(GpxCacherList inCachers, DataPoint inPoint)
450 if (inCachers == null || inPoint == null) {return null;}
451 String source = inCachers.getSourceString(inPoint);
452 if (source == null || !inPoint.isModified()) {return source;}
453 // Point has been modified - maybe it's possible to modify the source
454 source = replaceGpxTags(source, "lat=\"", "\"", inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
455 source = replaceGpxTags(source, "lon=\"", "\"", inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
456 source = replaceGpxTags(source, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
457 source = replaceGpxTags(source, "<time>", "</time>", inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
458 if (inPoint.isWaypoint()) {source = replaceGpxTags(source, "<name>", "</name>", inPoint.getWaypointName());} // only for waypoints
463 * Replace the given value into the given XML string
464 * @param inSource source XML for point
465 * @param inStartTag start tag for field
466 * @param inEndTag end tag for field
467 * @param inValue value to replace between start tag and end tag
468 * @return modified String, or null if not possible
470 private static String replaceGpxTags(String inSource, String inStartTag, String inEndTag, String inValue)
472 if (inSource == null) {return null;}
473 // Look for start and end tags within source
474 final int startPos = inSource.indexOf(inStartTag);
475 final int endPos = inSource.indexOf(inEndTag, startPos+inStartTag.length());
476 if (startPos > 0 && endPos > 0)
478 String origValue = inSource.substring(startPos + inStartTag.length(), endPos);
479 if (inValue != null && origValue.equals(inValue)) {
483 else if (inValue == null || inValue.equals("")) {
484 // Need to delete value
485 return inSource.substring(0, startPos) + inSource.substring(endPos + inEndTag.length());
488 // Need to replace value
489 return inSource.substring(0, startPos+inStartTag.length()) + inValue + inSource.substring(endPos);
492 // Value not found for this field in original source
493 if (inValue == null || inValue.equals("")) {return inSource;}
498 * Get the header string for the xml document including encoding
499 * @param inWriter writer object
500 * @return header string defining encoding
502 private static String getXmlHeaderString(OutputStreamWriter inWriter)
504 String encoding = inWriter.getEncoding();
506 encoding = Charset.forName(encoding).name();
508 catch (Exception e) {} // ignore failure to find encoding
509 return "<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>\n";
513 * Get the header string for the gpx tag
514 * @param inCachers cacher list to ask for headers, if available
515 * @return header string from cachers or as default
517 private static String getGpxHeaderString(GpxCacherList inCachers)
519 String gpxHeader = null;
520 if (inCachers != null) {gpxHeader = inCachers.getFirstHeader();}
521 if (gpxHeader == null || gpxHeader.length() < 5)
523 // Create default (1.0) header
524 gpxHeader = "<gpx version=\"1.0\" creator=\"" + GPX_CREATOR
525 + "\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
526 + " xmlns=\"http://www.topografix.com/GPX/1/0\""
527 + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\n";
529 return gpxHeader + "\n";
533 * Export the specified waypoint into the file
534 * @param inPoint waypoint to export
535 * @param inWriter writer object
536 * @param inTimestamps true to export timestamps too
537 * @throws IOException on write failure
539 private static void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps)
542 inWriter.write("\t<wpt lat=\"");
543 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
544 inWriter.write("\" lon=\"");
545 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
546 inWriter.write("\">\n");
547 // altitude if available
548 if (inPoint.hasAltitude())
550 inWriter.write("\t\t<ele>");
551 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
552 inWriter.write("</ele>\n");
554 // timestamp if available (point might have timestamp and then be turned into a waypoint)
555 if (inPoint.hasTimestamp() && inTimestamps)
557 inWriter.write("\t\t<time>");
558 inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
559 inWriter.write("</time>\n");
561 // write waypoint name after elevation and time
562 inWriter.write("\t\t<name>");
563 inWriter.write(inPoint.getWaypointName().trim());
564 inWriter.write("</name>\n");
565 // write waypoint type if any
566 String type = inPoint.getFieldValue(Field.WAYPT_TYPE);
570 if (!type.equals(""))
572 inWriter.write("\t\t<type>");
573 inWriter.write(type);
574 inWriter.write("</type>\n");
577 inWriter.write("\t</wpt>\n");
582 * Export the specified trackpoint into the file
583 * @param inPoint trackpoint to export
584 * @param inWriter writer object
585 * @param inTimestamps true to export timestamps too
587 private static void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps)
590 inWriter.write("\t\t<trkpt lat=\"");
591 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
592 inWriter.write("\" lon=\"");
593 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
594 inWriter.write("\">");
596 if (inPoint.hasAltitude())
598 inWriter.write("<ele>");
599 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
600 inWriter.write("</ele>");
602 // timestamp if available (and selected)
603 if (inPoint.hasTimestamp() && inTimestamps)
605 inWriter.write("<time>");
606 inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
607 inWriter.write("</time>");
609 inWriter.write("</trkpt>\n");