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 headers
294 inWriter.write(getXmlHeaderString(inWriter));
295 inWriter.write(getGpxHeaderString(gpxCachers));
297 String trackName = "PruneTrack";
298 if (inName != null && !inName.equals(""))
301 inWriter.write("\t<name>");
302 inWriter.write(trackName);
303 inWriter.write("</name>\n");
306 inWriter.write("\t<desc>");
307 inWriter.write((inDesc != null && !inDesc.equals(""))?inDesc:"Export from Prune");
308 inWriter.write("</desc>\n");
311 DataPoint point = null;
312 boolean hasTrackpoints = false;
313 final boolean exportTrackpoints = inSaveFlags[0];
314 final boolean exportWaypoints = inSaveFlags[1];
315 final boolean exportPhotos = inSaveFlags[2];
316 final boolean exportSelection = inSaveFlags[3];
317 final boolean exportTimestamps = inSaveFlags[4];
319 int selStart = -1, selEnd = -1;
320 if (exportSelection) {
321 selStart = inInfo.getSelection().getStart();
322 selEnd = inInfo.getSelection().getEnd();
324 // Loop over waypoints
325 final int numPoints = inInfo.getTrack().getNumPoints();
327 for (i=0; i<numPoints; i++)
329 point = inInfo.getTrack().getPoint(i);
330 if (!exportSelection || (i>=selStart && i<=selEnd)) {
331 // Make a wpt element for each waypoint
332 if (point.isWaypoint()) {
335 String pointSource = (inUseCopy?getPointSource(gpxCachers, point):null);
336 if (pointSource != null) {
337 inWriter.write(pointSource);
338 inWriter.write('\n');
341 exportWaypoint(point, inWriter, exportTimestamps);
347 hasTrackpoints = true;
351 // Export both route points and then track points
352 if (hasTrackpoints && (exportTrackpoints || exportPhotos))
354 // Output all route points (if any)
355 numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
356 exportTimestamps, true, gpxCachers, "<rtept", "\t<rte><number>1</number>\n", null, "\t</rte>\n");
357 // Output all track points, if any
358 String trackStart = "\t<trk><name>" + trackName + "</name><number>1</number><trkseg>\n";
359 numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
360 exportTimestamps, false, gpxCachers, "<trkpt", trackStart, "\t</trkseg>\n\t<trkseg>\n",
361 "\t</trkseg></trk>\n");
364 inWriter.write("</gpx>\n");
369 * Loop through the track outputting the relevant track points
370 * @param inWriter writer object for output
371 * @param inInfo track info object containing track
372 * @param inExportSelection true to just output current selection
373 * @param inExportTrackpoints true to output track points
374 * @param inExportPhotos true to output photo points
375 * @param exportTimestamps true to include timestamps in export
376 * @param inOnlyCopies true to only export if source can be copied
377 * @param inCachers list of GpxCachers
378 * @param inPointTag tag to match for each point
379 * @param inStartTag start tag to output
380 * @param inSegmentTag tag to output between segments (or null)
381 * @param inEndTag end tag to output
383 private static int writeTrackPoints(OutputStreamWriter inWriter,
384 TrackInfo inInfo, boolean inExportSelection, boolean inExportTrackpoints,
385 boolean inExportPhotos, boolean exportTimestamps, boolean inOnlyCopies,
386 GpxCacherList inCachers, String inPointTag, String inStartTag,
387 String inSegmentTag, String inEndTag)
390 // Note: far too many input parameters to this method but avoids duplication
391 // of output functionality for writing track points and route points
392 int numPoints = inInfo.getTrack().getNumPoints();
393 int selStart = inInfo.getSelection().getStart();
394 int selEnd = inInfo.getSelection().getEnd();
396 // Loop over track points
397 for (int i=0; i<numPoints; i++)
399 DataPoint point = inInfo.getTrack().getPoint(i);
400 if ((!inExportSelection || (i>=selStart && i<=selEnd)) && !point.isWaypoint())
402 if ((point.getPhoto()==null && inExportTrackpoints) || (point.getPhoto()!=null && inExportPhotos))
404 // get the source from the point (if any)
405 String pointSource = getPointSource(inCachers, point);
406 boolean writePoint = (pointSource != null && pointSource.toLowerCase().startsWith(inPointTag))
407 || (pointSource == null && !inOnlyCopies);
410 // restart track segment if necessary
411 if ((numSaved > 0) && point.getSegmentStart() && (inSegmentTag != null)) {
412 inWriter.write(inSegmentTag);
414 if (numSaved == 0) {inWriter.write(inStartTag);}
415 if (pointSource != null) {
416 inWriter.write(pointSource);
417 inWriter.write('\n');
420 if (!inOnlyCopies) {exportTrackpoint(point, inWriter, exportTimestamps);}
427 if (numSaved > 0) {inWriter.write(inEndTag);}
433 * Get the point source for the specified point
434 * @param inCachers list of GPX cachers to ask for source
435 * @param inPoint point object
436 * @return xml source if available, or null otherwise
438 private static String getPointSource(GpxCacherList inCachers, DataPoint inPoint)
440 if (inCachers == null || inPoint == null) {return null;}
441 String source = inCachers.getSourceString(inPoint);
442 if (source == null || !inPoint.isModified()) {return source;}
443 // Point has been modified - maybe it's possible to modify the source
444 source = replaceGpxTags(source, "lat=\"", "\"", inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
445 source = replaceGpxTags(source, "lon=\"", "\"", inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
446 source = replaceGpxTags(source, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
447 source = replaceGpxTags(source, "<time>", "</time>", inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
448 if (inPoint.isWaypoint()) {source = replaceGpxTags(source, "<name>", "</name>", inPoint.getWaypointName());} // only for waypoints
453 * Replace the given value into the given XML string
454 * @param inSource source XML for point
455 * @param inStartTag start tag for field
456 * @param inEndTag end tag for field
457 * @param inValue value to replace between start tag and end tag
458 * @return modified String, or null if not possible
460 private static String replaceGpxTags(String inSource, String inStartTag, String inEndTag, String inValue)
462 if (inSource == null) {return null;}
463 // Look for start and end tags within source
464 final int startPos = inSource.indexOf(inStartTag);
465 final int endPos = inSource.indexOf(inEndTag, startPos+inStartTag.length());
466 if (startPos > 0 && endPos > 0)
468 String origValue = inSource.substring(startPos + inStartTag.length(), endPos);
469 if (inValue != null && origValue.equals(inValue)) {
473 else if (inValue == null || inValue.equals("")) {
474 // Need to delete value
475 return inSource.substring(0, startPos) + inSource.substring(endPos + inEndTag.length());
478 // Need to replace value
479 return inSource.substring(0, startPos+inStartTag.length()) + inValue + inSource.substring(endPos);
482 // Value not found for this field in original source
483 if (inValue == null || inValue.equals("")) {return inSource;}
488 * Get the header string for the xml document including encoding
489 * @param inWriter writer object
490 * @return header string defining encoding
492 private static String getXmlHeaderString(OutputStreamWriter inWriter)
494 String encoding = inWriter.getEncoding();
495 final String encodingUpper = encoding.toUpperCase();
496 if (encodingUpper.equals("UTF8") || encodingUpper.equals("UTF-8")) {
499 return "<?xml version=\"1.0\" encoding=\"" + encoding + "\"?>\n";
503 * Get the header string for the gpx tag
504 * @param inCachers cacher list to ask for headers, if available
505 * @return header string from cachers or as default
507 private static String getGpxHeaderString(GpxCacherList inCachers)
509 String gpxHeader = null;
510 if (inCachers != null) {gpxHeader = inCachers.getFirstHeader();}
511 if (gpxHeader == null || gpxHeader.length() < 5)
513 // Create default (1.0) header
514 gpxHeader = "<gpx version=\"1.0\" creator=\"" + GPX_CREATOR
515 + "\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
516 + " xmlns=\"http://www.topografix.com/GPX/1/0\""
517 + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\n";
519 return gpxHeader + "\n";
523 * Export the specified waypoint into the file
524 * @param inPoint waypoint to export
525 * @param inWriter writer object
526 * @param inTimestamps true to export timestamps too
527 * @throws IOException on write failure
529 private static void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps)
532 inWriter.write("\t<wpt lat=\"");
533 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
534 inWriter.write("\" lon=\"");
535 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
536 inWriter.write("\">\n");
537 // altitude if available
538 if (inPoint.hasAltitude())
540 inWriter.write("\t\t<ele>");
541 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
542 inWriter.write("</ele>\n");
544 // timestamp if available (point might have timestamp and then be turned into a waypoint)
545 if (inPoint.hasTimestamp() && inTimestamps)
547 inWriter.write("\t\t<time>");
548 inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
549 inWriter.write("</time>\n");
551 // write waypoint name after elevation and time
552 inWriter.write("\t\t<name>");
553 inWriter.write(inPoint.getWaypointName().trim());
554 inWriter.write("</name>\n");
555 // write waypoint type if any
556 String type = inPoint.getFieldValue(Field.WAYPT_TYPE);
560 if (!type.equals(""))
562 inWriter.write("\t\t<type>");
563 inWriter.write(type);
564 inWriter.write("</type>\n");
567 inWriter.write("\t</wpt>\n");
572 * Export the specified trackpoint into the file
573 * @param inPoint trackpoint to export
574 * @param inWriter writer object
575 * @param inTimestamps true to export timestamps too
577 private static void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps)
580 inWriter.write("\t\t<trkpt lat=\"");
581 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
582 inWriter.write("\" lon=\"");
583 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
584 inWriter.write("\">");
586 if (inPoint.hasAltitude())
588 inWriter.write("<ele>");
589 inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
590 inWriter.write("</ele>");
592 // timestamp if available (and selected)
593 if (inPoint.hasTimestamp() && inTimestamps)
595 inWriter.write("<time>");
596 inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
597 inWriter.write("</time>");
599 inWriter.write("</trkpt>\n");