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;
10 import java.io.FileOutputStream;
11 import java.io.IOException;
12 import java.io.OutputStreamWriter;
13 import java.io.Writer;
15 import javax.swing.BorderFactory;
16 import javax.swing.Box;
17 import javax.swing.BoxLayout;
18 import javax.swing.JButton;
19 import javax.swing.JCheckBox;
20 import javax.swing.JDialog;
21 import javax.swing.JFileChooser;
22 import javax.swing.JFrame;
23 import javax.swing.JLabel;
24 import javax.swing.JOptionPane;
25 import javax.swing.JPanel;
26 import javax.swing.JTextField;
29 import tim.prune.GenericFunction;
30 import tim.prune.GpsPruner;
31 import tim.prune.I18nManager;
32 import tim.prune.UpdateMessageBroker;
33 import tim.prune.config.Config;
34 import tim.prune.data.Altitude;
35 import tim.prune.data.Coordinate;
36 import tim.prune.data.DataPoint;
37 import tim.prune.data.Field;
38 import tim.prune.data.Timestamp;
39 import tim.prune.data.TrackInfo;
40 import tim.prune.load.GenericFileFilter;
41 import tim.prune.save.xml.GpxCacherList;
45 * Class to export track information
46 * into a specified Gpx file
48 public class GpxExporter extends GenericFunction implements Runnable
50 private TrackInfo _trackInfo = null;
51 private JDialog _dialog = null;
52 private JTextField _nameField = null;
53 private JTextField _descriptionField = null;
54 private PointTypeSelector _pointTypeSelector = null;
55 private JCheckBox _timestampsCheckbox = null;
56 private JCheckBox _copySourceCheckbox = null;
57 private File _exportFile = null;
59 /** this program name */
60 private static final String GPX_CREATOR = "Prune v" + GpsPruner.VERSION_NUMBER + " activityworkshop.net";
65 * @param inApp app object
67 public GpxExporter(App inApp)
70 _trackInfo = inApp.getTrackInfo();
74 public String getNameKey() {
75 return "function.exportgpx";
79 * Show the dialog to select options and export file
86 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
87 _dialog.setLocationRelativeTo(_parentFrame);
88 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
89 _dialog.getContentPane().add(makeDialogComponents());
92 _pointTypeSelector.init(_app.getTrackInfo());
93 _dialog.setVisible(true);
98 * Create dialog components
99 * @return Panel containing all gui elements in dialog
101 private Component makeDialogComponents()
103 JPanel dialogPanel = new JPanel();
104 dialogPanel.setLayout(new BorderLayout());
105 JPanel mainPanel = new JPanel();
106 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
107 // Make a central panel with the text boxes
108 JPanel descPanel = new JPanel();
109 descPanel.setLayout(new GridLayout(2, 2));
110 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.name")));
111 _nameField = new JTextField(10);
112 descPanel.add(_nameField);
113 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.desc")));
114 _descriptionField = new JTextField(10);
115 descPanel.add(_descriptionField);
116 mainPanel.add(descPanel);
117 mainPanel.add(Box.createVerticalStrut(5));
118 // point type selection (track points, waypoints, photo points)
119 _pointTypeSelector = new PointTypeSelector();
120 mainPanel.add(_pointTypeSelector);
121 // checkboxes for timestamps and copying
122 JPanel checkPanel = new JPanel();
123 _timestampsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.includetimestamps"));
124 _timestampsCheckbox.setSelected(true);
125 checkPanel.add(_timestampsCheckbox);
126 _copySourceCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.copysource"));
127 _copySourceCheckbox.setSelected(true);
128 checkPanel.add(_copySourceCheckbox);
129 mainPanel.add(checkPanel);
130 dialogPanel.add(mainPanel, BorderLayout.CENTER);
132 // button panel at bottom
133 JPanel buttonPanel = new JPanel();
134 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
135 JButton okButton = new JButton(I18nManager.getText("button.ok"));
136 ActionListener okListener = new ActionListener() {
137 public void actionPerformed(ActionEvent e)
142 okButton.addActionListener(okListener);
143 _descriptionField.addActionListener(okListener);
144 buttonPanel.add(okButton);
145 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
146 cancelButton.addActionListener(new ActionListener() {
147 public void actionPerformed(ActionEvent e)
152 buttonPanel.add(cancelButton);
153 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
154 dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
160 * Start the export process based on the input parameters
162 private void startExport()
164 // OK pressed, so check selections
165 if (!_pointTypeSelector.getAnythingSelected()) {
166 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
167 I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
170 // Choose output file
171 File saveFile = chooseGpxFile(_parentFrame);
172 if (saveFile != null)
174 // New file or overwrite confirmed, so initiate export in separate thread
175 _exportFile = saveFile;
176 new Thread(this).start();
181 * Select a GPX file to save to
182 * @param inParentFrame parent frame for file chooser dialog
183 * @return selected File, or null if selection cancelled
185 public static File chooseGpxFile(JFrame inParentFrame)
187 File saveFile = null;
188 JFileChooser fileChooser = new JFileChooser();
189 fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
190 fileChooser.setFileFilter(new GenericFileFilter("filetype.gpx", new String[] {"gpx"}));
191 fileChooser.setAcceptAllFileFilterUsed(false);
192 // start from directory in config which should be set
193 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
194 if (configDir != null) {fileChooser.setCurrentDirectory(new File(configDir));}
196 // Allow choose again if an existing file is selected
197 boolean chooseAgain = false;
201 if (fileChooser.showSaveDialog(inParentFrame) == JFileChooser.APPROVE_OPTION)
203 // OK pressed and file chosen
204 File file = fileChooser.getSelectedFile();
205 // Check file extension
206 if (!file.getName().toLowerCase().endsWith(".gpx"))
208 file = new File(file.getAbsolutePath() + ".gpx");
210 // Check if file exists and if necessary prompt for overwrite
211 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
212 if (!file.exists() || JOptionPane.showOptionDialog(inParentFrame,
213 I18nManager.getText("dialog.save.overwrite.text"),
214 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
215 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
216 == JOptionPane.YES_OPTION)
218 // new file or overwrite confirmed
223 // file exists and overwrite cancelled - select again
227 } while (chooseAgain);
232 * Run method for controlling separate thread for exporting
236 OutputStreamWriter writer = null;
239 // normal writing to file
240 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
241 boolean[] saveFlags = {_pointTypeSelector.getTrackpointsSelected(), _pointTypeSelector.getWaypointsSelected(),
242 _pointTypeSelector.getPhotopointsSelected(), _pointTypeSelector.getJustSelection(),
243 _timestampsCheckbox.isSelected()};
245 final int numPoints = exportData(writer, _trackInfo, _nameField.getText(),
246 _descriptionField.getText(), saveFlags, _copySourceCheckbox.isSelected());
250 // Store directory in config for later
251 Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
253 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
254 + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
255 + " " + _exportFile.getAbsolutePath());
256 // export successful so need to close dialog and return
260 catch (IOException ioe)
262 // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
264 if (writer != null) writer.close();
266 catch (IOException ioe2) {}
267 JOptionPane.showMessageDialog(_parentFrame,
268 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
269 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
271 // if not returned already, export failed so need to recall the file selection
277 * Export the information to the given writer
278 * @param inWriter writer object
279 * @param inInfo track info object
280 * @param inName name of track (optional)
281 * @param inDesc description of track (optional)
282 * @param inSaveFlags array of booleans to export tracks, waypoints, photos, timestamps
283 * @param inUseCopy true to copy source if available
284 * @return number of points written
285 * @throws IOException if io errors occur on write
287 public static int exportData(OutputStreamWriter inWriter, TrackInfo inInfo, String inName,
288 String inDesc, boolean[] inSaveFlags, boolean inUseCopy) throws IOException
290 // Instantiate source file cachers in case we want to copy output
291 GpxCacherList gpxCachers = null;
292 if (inUseCopy) gpxCachers = new GpxCacherList(inInfo.getFileInfo());
293 // Write or copy header
294 inWriter.write(getHeaderString(gpxCachers));
296 String trackName = "PruneTrack";
297 if (inName != null && !inName.equals(""))
300 inWriter.write("\t<name>");
301 inWriter.write(trackName);
302 inWriter.write("</name>\n");
305 inWriter.write("\t<desc>");
306 if (inDesc != null && !inDesc.equals("")) {
307 inWriter.write(inDesc);
310 inWriter.write("Export from Prune");
312 inWriter.write("</desc>\n");
315 DataPoint point = null;
316 boolean hasTrackpoints = false;
317 final boolean exportTrackpoints = inSaveFlags[0];
318 final boolean exportWaypoints = inSaveFlags[1];
319 final boolean exportPhotos = inSaveFlags[2];
320 final boolean exportSelection = inSaveFlags[3];
321 final boolean exportTimestamps = inSaveFlags[4];
323 int selStart = -1, selEnd = -1;
324 if (exportSelection) {
325 selStart = inInfo.getSelection().getStart();
326 selEnd = inInfo.getSelection().getEnd();
328 // Loop over waypoints
329 final int numPoints = inInfo.getTrack().getNumPoints();
331 for (i=0; i<numPoints; i++)
333 point = inInfo.getTrack().getPoint(i);
334 if (!exportSelection || (i>=selStart && i<=selEnd)) {
335 // Make a wpt element for each waypoint
336 if (point.isWaypoint()) {
339 String pointSource = (inUseCopy?getPointSource(gpxCachers, point):null);
340 if (pointSource != null) {
341 inWriter.write(pointSource);
342 inWriter.write('\n');
345 exportWaypoint(point, inWriter, exportTimestamps);
351 hasTrackpoints = true;
355 // Export both route points and then track points
356 if (hasTrackpoints && (exportTrackpoints || exportPhotos))
358 // Output all route points (if any)
359 numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
360 exportTimestamps, true, gpxCachers, "<rtept", "\t<rte><number>1</number>\n", null, "\t</rte>\n");
361 // Output all track points, if any
362 String trackStart = "\t<trk><name>" + trackName + "</name><number>1</number><trkseg>\n";
363 numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
364 exportTimestamps, false, gpxCachers, "<trkpt", trackStart, "\t</trkseg>\n\t<trkseg>\n",
365 "\t</trkseg></trk>\n");
368 inWriter.write("</gpx>\n");
373 * Loop through the track outputting the relevant track points
374 * @param inWriter writer object for output
375 * @param inInfo track info object containing track
376 * @param inExportSelection true to just output current selection
377 * @param inExportTrackpoints true to output track points
378 * @param inExportPhotos true to output photo points
379 * @param exportTimestamps true to include timestamps in export
380 * @param inOnlyCopies true to only export if source can be copied
381 * @param inCachers list of GpxCachers
382 * @param inPointTag tag to match for each point
383 * @param inStartTag start tag to output
384 * @param inSegmentTag tag to output between segments (or null)
385 * @param inEndTag end tag to output
387 private static int writeTrackPoints(OutputStreamWriter inWriter,
388 TrackInfo inInfo, boolean inExportSelection, boolean inExportTrackpoints,
389 boolean inExportPhotos, boolean exportTimestamps, boolean inOnlyCopies,
390 GpxCacherList inCachers, String inPointTag, String inStartTag,
391 String inSegmentTag, String inEndTag)
394 // Note: far too many input parameters to this method but avoids duplication
395 // of output functionality for writing track points and route points
396 int numPoints = inInfo.getTrack().getNumPoints();
397 int selStart = inInfo.getSelection().getStart();
398 int selEnd = inInfo.getSelection().getEnd();
400 // Loop over track points
401 for (int i=0; i<numPoints; i++)
403 DataPoint point = inInfo.getTrack().getPoint(i);
404 if ((!inExportSelection || (i>=selStart && i<=selEnd)) && !point.isWaypoint())
406 if ((point.getPhoto()==null && inExportTrackpoints) || (point.getPhoto()!=null && inExportPhotos))
408 // get the source from the point (if any)
409 String pointSource = getPointSource(inCachers, point);
410 boolean writePoint = (pointSource != null && pointSource.toLowerCase().startsWith(inPointTag))
411 || (pointSource == null && !inOnlyCopies);
414 // restart track segment if necessary
415 if ((numSaved > 0) && point.getSegmentStart() && (inSegmentTag != null)) {
416 inWriter.write(inSegmentTag);
418 if (numSaved == 0) {inWriter.write(inStartTag);}
419 if (pointSource != null) {
420 inWriter.write(pointSource);
421 inWriter.write('\n');
424 if (!inOnlyCopies) {exportTrackpoint(point, inWriter, exportTimestamps);}
431 if (numSaved > 0) {inWriter.write(inEndTag);}
437 * Get the point source for the specified point
438 * @param inCachers list of GPX cachers to ask for source
439 * @param inPoint point object
440 * @return xml source if available, or null otherwise
442 private static String getPointSource(GpxCacherList inCachers, DataPoint inPoint)
444 if (inCachers == null || inPoint == null) {return null;}
445 String source = inCachers.getSourceString(inPoint);
446 if (source == null || !inPoint.isModified()) {return source;}
447 // Point has been modified - maybe it's possible to modify the source
448 source = replaceGpxTags(source, "lat=\"", "\"", inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
449 source = replaceGpxTags(source, "lon=\"", "\"", inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
450 source = replaceGpxTags(source, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
451 source = replaceGpxTags(source, "<time>", "</time>", inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
452 if (inPoint.isWaypoint()) {source = replaceGpxTags(source, "<name>", "</name>", inPoint.getWaypointName());} // only for waypoints
457 * Replace the given value into the given XML string
458 * @param inSource source XML for point
459 * @param inStartTag start tag for field
460 * @param inEndTag end tag for field
461 * @param inValue value to replace between start tag and end tag
462 * @return modified String, or null if not possible
464 private static String replaceGpxTags(String inSource, String inStartTag, String inEndTag, String inValue)
466 if (inSource == null) {return null;}
467 // Look for start and end tags within source
468 final int startPos = inSource.indexOf(inStartTag);
469 final int endPos = inSource.indexOf(inEndTag, startPos+inStartTag.length());
470 if (startPos > 0 && endPos > 0)
472 String origValue = inSource.substring(startPos + inStartTag.length(), endPos);
473 if (inValue != null && origValue.equals(inValue)) {
477 else if (inValue == null || inValue.equals("")) {
478 // Need to delete value
479 return inSource.substring(0, startPos) + inSource.substring(endPos + inEndTag.length());
482 // Need to replace value
483 return inSource.substring(0, startPos+inStartTag.length()) + inValue + inSource.substring(endPos);
486 // Value not found for this field in original source
487 if (inValue == null || inValue.equals("")) {return inSource;}
492 * Get the header string for the gpx
493 * @param inCachers cacher list to ask for headers, if available
494 * @return header string from cachers or as default
496 private static String getHeaderString(GpxCacherList inCachers)
498 String gpxHeader = null;
499 if (inCachers != null) {gpxHeader = inCachers.getFirstHeader();}
500 if (gpxHeader == null || gpxHeader.length() < 5)
502 // Create default (1.0) header
503 gpxHeader = "<gpx version=\"1.0\" creator=\"" + GPX_CREATOR
504 + "\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
505 + " xmlns=\"http://www.topografix.com/GPX/1/0\""
506 + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\n";
508 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + gpxHeader + "\n";
512 * Export the specified waypoint into the file
513 * @param inPoint waypoint to export
514 * @param inWriter writer object
515 * @param inTimestamps true to export timestamps too
516 * @throws IOException on write failure
518 private static void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps)
521 inWriter.write("\t<wpt lat=\"");
522 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
523 inWriter.write("\" lon=\"");
524 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
525 inWriter.write("\">\n");
526 // altitude if available
527 if (inPoint.hasAltitude())
529 inWriter.write("\t\t<ele>");
530 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
531 inWriter.write("</ele>\n");
533 // timestamp if available (point might have timestamp and then be turned into a waypoint)
534 if (inPoint.hasTimestamp() && inTimestamps)
536 inWriter.write("\t\t<time>");
537 inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
538 inWriter.write("</time>\n");
540 // write waypoint name after elevation and time
541 inWriter.write("\t\t<name>");
542 inWriter.write(inPoint.getWaypointName().trim());
543 inWriter.write("</name>\n");
544 // write waypoint type if any
545 String type = inPoint.getFieldValue(Field.WAYPT_TYPE);
549 if (!type.equals(""))
551 inWriter.write("\t\t<type>");
552 inWriter.write(type);
553 inWriter.write("</type>\n");
556 inWriter.write("\t</wpt>\n");
561 * Export the specified trackpoint into the file
562 * @param inPoint trackpoint to export
563 * @param inWriter writer object
564 * @param inTimestamps true to export timestamps too
566 private static void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps)
569 inWriter.write("\t\t<trkpt lat=\"");
570 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
571 inWriter.write("\" lon=\"");
572 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
573 inWriter.write("\">");
575 if (inPoint.hasAltitude())
577 inWriter.write("<ele>");
578 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
579 inWriter.write("</ele>");
581 // timestamp if available (and selected)
582 if (inPoint.hasTimestamp() && inTimestamps)
584 inWriter.write("<time>");
585 inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
586 inWriter.write("</time>");
588 inWriter.write("</trkpt>\n");