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.ButtonGroup;
19 import javax.swing.JButton;
20 import javax.swing.JCheckBox;
21 import javax.swing.JDialog;
22 import javax.swing.JFileChooser;
23 import javax.swing.JFrame;
24 import javax.swing.JLabel;
25 import javax.swing.JOptionPane;
26 import javax.swing.JPanel;
27 import javax.swing.JRadioButton;
28 import javax.swing.JTextField;
29 import javax.swing.border.EtchedBorder;
32 import tim.prune.GenericFunction;
33 import tim.prune.GpsPrune;
34 import tim.prune.I18nManager;
35 import tim.prune.UpdateMessageBroker;
36 import tim.prune.config.Config;
37 import tim.prune.data.AudioClip;
38 import tim.prune.data.Coordinate;
39 import tim.prune.data.DataPoint;
40 import tim.prune.data.Field;
41 import tim.prune.data.MediaObject;
42 import tim.prune.data.Photo;
43 import tim.prune.data.RecentFile;
44 import tim.prune.data.SourceInfo;
45 import tim.prune.data.Timestamp;
46 import tim.prune.data.TrackInfo;
47 import tim.prune.data.UnitSetLibrary;
48 import tim.prune.gui.DialogCloser;
49 import tim.prune.load.GenericFileFilter;
50 import tim.prune.save.xml.GpxCacherList;
51 import tim.prune.save.xml.XmlUtils;
55 * Class to export track information
56 * into a specified Gpx file
58 public class GpxExporter extends GenericFunction implements Runnable
60 private TrackInfo _trackInfo = null;
61 private JDialog _dialog = null;
62 private JTextField _nameField = null;
63 private JTextField _descriptionField = null;
64 private PointTypeSelector _pointTypeSelector = null;
65 private JCheckBox _timestampsCheckbox = null;
66 private JCheckBox _copySourceCheckbox = null;
67 private JPanel _encodingsPanel = null;
68 private JRadioButton _useSystemRadio = null, _forceUtf8Radio = null;
69 private File _exportFile = null;
70 /** Remember the previous sourceInfo object to tell whether it has changed */
71 private SourceInfo _previousSourceInfo = null;
73 /** this program name */
74 private static final String GPX_CREATOR = "GpsPrune v" + GpsPrune.VERSION_NUMBER + " activityworkshop.net";
79 * @param inApp app object
81 public GpxExporter(App inApp)
84 _trackInfo = inApp.getTrackInfo();
88 public String getNameKey() {
89 return "function.exportgpx";
93 * Show the dialog to select options and export file
100 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
101 _dialog.setLocationRelativeTo(_parentFrame);
102 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
103 _dialog.getContentPane().add(makeDialogComponents());
106 _pointTypeSelector.init(_app.getTrackInfo());
107 _encodingsPanel.setVisible(!XmlUtils.isSystemUtf8());
108 if (!XmlUtils.isSystemUtf8())
110 String systemEncoding = XmlUtils.getSystemEncoding();
111 _useSystemRadio.setText(I18nManager.getText("dialog.exportgpx.encoding.system")
112 + " (" + (systemEncoding == null ? "unknown" : systemEncoding) + ")");
115 _dialog.setVisible(true);
120 * Create dialog components
121 * @return Panel containing all gui elements in dialog
123 private Component makeDialogComponents()
125 JPanel dialogPanel = new JPanel();
126 dialogPanel.setLayout(new BorderLayout());
127 JPanel mainPanel = new JPanel();
128 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
129 // Make a panel for the name/desc text boxes
130 JPanel descPanel = new JPanel();
131 descPanel.setLayout(new GridLayout(2, 2));
132 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.name")));
133 _nameField = new JTextField(10);
134 descPanel.add(_nameField);
135 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.desc")));
136 _descriptionField = new JTextField(10);
137 descPanel.add(_descriptionField);
138 mainPanel.add(descPanel);
139 mainPanel.add(Box.createVerticalStrut(5));
140 // point type selection (track points, waypoints, photo points)
141 _pointTypeSelector = new PointTypeSelector();
142 mainPanel.add(_pointTypeSelector);
143 // checkboxes for timestamps and copying
144 JPanel checkPanel = new JPanel();
145 _timestampsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.includetimestamps"));
146 _timestampsCheckbox.setSelected(true);
147 checkPanel.add(_timestampsCheckbox);
148 _copySourceCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.copysource"));
149 _copySourceCheckbox.setSelected(true);
150 checkPanel.add(_copySourceCheckbox);
151 mainPanel.add(checkPanel);
152 // panel for selecting character encoding
153 _encodingsPanel = new JPanel();
154 if (!XmlUtils.isSystemUtf8())
156 // only add this panel if system isn't utf8 (or can't be identified yet)
157 _encodingsPanel.setBorder(BorderFactory.createCompoundBorder(
158 BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(4, 4, 4, 4)));
159 _encodingsPanel.setLayout(new BorderLayout());
160 _encodingsPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.encoding")), BorderLayout.NORTH);
161 JPanel radioPanel = new JPanel();
162 radioPanel.setLayout(new FlowLayout());
163 ButtonGroup radioGroup = new ButtonGroup();
164 _useSystemRadio = new JRadioButton(I18nManager.getText("dialog.exportgpx.encoding.system"));
165 _forceUtf8Radio = new JRadioButton(I18nManager.getText("dialog.exportgpx.encoding.utf8"));
166 radioGroup.add(_useSystemRadio);
167 radioGroup.add(_forceUtf8Radio);
168 radioPanel.add(_useSystemRadio);
169 radioPanel.add(_forceUtf8Radio);
170 _useSystemRadio.setSelected(true);
171 _encodingsPanel.add(radioPanel, BorderLayout.CENTER);
172 mainPanel.add(_encodingsPanel);
174 dialogPanel.add(mainPanel, BorderLayout.CENTER);
176 // close dialog if escape pressed
177 _nameField.addKeyListener(new DialogCloser(_dialog));
178 // button panel at bottom
179 JPanel buttonPanel = new JPanel();
180 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
181 JButton okButton = new JButton(I18nManager.getText("button.ok"));
182 ActionListener okListener = new ActionListener() {
183 public void actionPerformed(ActionEvent e)
188 okButton.addActionListener(okListener);
189 _descriptionField.addActionListener(okListener);
190 buttonPanel.add(okButton);
191 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
192 cancelButton.addActionListener(new ActionListener() {
193 public void actionPerformed(ActionEvent e) {
197 buttonPanel.add(cancelButton);
198 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
199 dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
204 * Set the suggestion for the name with which to export
206 private void setFileTitle()
208 // Get the most recent file info
209 SourceInfo currentSource = _app.getTrackInfo().getFileInfo().getLastFileInfo();
210 if (currentSource != _previousSourceInfo)
212 String lastTitle = currentSource.getFileTitle();
213 if (lastTitle != null && !lastTitle.equals(""))
215 // Take the title of the last file loaded
216 _nameField.setText(lastTitle);
219 if (_nameField.getText().equals(""))
221 // no name given in the field already, so try to overwrite it
222 String lastTitle = _app.getTrackInfo().getFileInfo().getLastFileTitle();
223 if (lastTitle != null && !lastTitle.equals(""))
225 _nameField.setText(lastTitle);
228 // Remember this source info so we don't use it again
229 _previousSourceInfo = currentSource;
233 * Start the export process based on the input parameters
235 private void startExport()
237 // OK pressed, so check selections
238 if (!_pointTypeSelector.getAnythingSelected())
240 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
241 I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
244 // Choose output file
245 File saveFile = chooseGpxFile(_parentFrame);
246 if (saveFile != null)
248 // New file or overwrite confirmed, so initiate export in separate thread
249 _exportFile = saveFile;
250 new Thread(this).start();
256 * Select a GPX file to save to
257 * @param inParentFrame parent frame for file chooser dialog
258 * @return selected File, or null if selection cancelled
260 public static File chooseGpxFile(JFrame inParentFrame)
262 File saveFile = null;
263 JFileChooser fileChooser = new JFileChooser();
264 fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
265 fileChooser.setFileFilter(new GenericFileFilter("filetype.gpx", new String[] {"gpx"}));
266 fileChooser.setAcceptAllFileFilterUsed(false);
267 // start from directory in config which should be set
268 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
269 if (configDir != null) {fileChooser.setCurrentDirectory(new File(configDir));}
271 // Allow choose again if an existing file is selected
272 boolean chooseAgain = false;
276 if (fileChooser.showSaveDialog(inParentFrame) == JFileChooser.APPROVE_OPTION)
278 // OK pressed and file chosen
279 File file = fileChooser.getSelectedFile();
280 // Check file extension
281 if (!file.getName().toLowerCase().endsWith(".gpx"))
283 file = new File(file.getAbsolutePath() + ".gpx");
285 // Check if file exists and if necessary prompt for overwrite
286 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
287 if (!file.exists() || JOptionPane.showOptionDialog(inParentFrame,
288 I18nManager.getText("dialog.save.overwrite.text"),
289 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
290 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
291 == JOptionPane.YES_OPTION)
293 // new file or overwrite confirmed
298 // file exists and overwrite cancelled - select again
302 } while (chooseAgain);
308 * Run method for controlling separate thread for exporting
312 // Instantiate source file cachers in case we want to copy output
313 GpxCacherList gpxCachers = null;
314 if (_copySourceCheckbox.isSelected()) {
315 gpxCachers = new GpxCacherList(_trackInfo.getFileInfo());
317 OutputStreamWriter writer = null;
320 // normal writing to file - firstly specify UTF8 encoding if requested
321 if (_forceUtf8Radio != null && _forceUtf8Radio.isSelected())
322 writer = new OutputStreamWriter(new FileOutputStream(_exportFile), "UTF-8");
324 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
325 // TODO: Move to new method
326 SettingsForExport settings = new SettingsForExport();
327 settings.setExportTrackPoints(_pointTypeSelector.getTrackpointsSelected());
328 settings.setExportWaypoints(_pointTypeSelector.getWaypointsSelected());
329 settings.setExportPhotoPoints(_pointTypeSelector.getPhotopointsSelected());
330 settings.setExportAudiopoints(_pointTypeSelector.getAudiopointsSelected());
331 settings.setExportJustSelection(_pointTypeSelector.getJustSelection());
332 settings.setExportTimestamps(_timestampsCheckbox.isSelected());
334 final int numPoints = exportData(writer, _trackInfo, _nameField.getText(),
335 _descriptionField.getText(), settings, gpxCachers);
339 // Store directory in config for later
340 Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
341 // Add to recent file list
342 Config.getRecentFileList().addFile(new RecentFile(_exportFile, true));
344 UpdateMessageBroker.informSubscribers();
345 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
346 + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
347 + " " + _exportFile.getAbsolutePath());
348 // export successful so need to close dialog and return
352 catch (IOException ioe)
354 // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
356 if (writer != null) writer.close();
358 catch (IOException ioe2) {}
359 JOptionPane.showMessageDialog(_parentFrame,
360 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
361 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
363 // if not returned already, export failed so need to recall the file selection
369 * Export the information to the given writer
370 * @param inWriter writer object
371 * @param inInfo track info object
372 * @param inName name of track (optional)
373 * @param inDesc description of track (optional)
374 * @param inExportSettings flags for what to export and how
375 * @param inGpxCachers list of Gpx cachers containing input data
376 * @return number of points written
377 * @throws IOException if io errors occur on write
379 public static int exportData(OutputStreamWriter inWriter, TrackInfo inInfo, String inName,
380 String inDesc, SettingsForExport inSettings, GpxCacherList inGpxCachers) throws IOException
382 // Write or copy headers
383 inWriter.write(getXmlHeaderString(inWriter));
384 final String gpxHeader = getGpxHeaderString(inGpxCachers);
385 final boolean isVersion1_1 = (gpxHeader.toUpperCase().indexOf("GPX/1/1") > 0);
386 inWriter.write(gpxHeader);
387 // name and description
388 String trackName = (inName != null && !inName.equals("")) ? XmlUtils.fixCdata(inName) : "GpsPruneTrack";
389 String desc = (inDesc != null && !inDesc.equals("")) ? XmlUtils.fixCdata(inDesc) : "Export from GpsPrune";
390 writeNameAndDescription(inWriter, trackName, desc, isVersion1_1);
392 DataPoint point = null;
393 final boolean exportWaypoints = inSettings.getExportWaypoints();
394 final boolean exportSelection = inSettings.getExportJustSelection();
395 final boolean exportTimestamps = inSettings.getExportTimestamps();
397 int selStart = -1, selEnd = -1;
398 if (exportSelection) {
399 selStart = inInfo.getSelection().getStart();
400 selEnd = inInfo.getSelection().getEnd();
402 // Loop over waypoints
403 final int numPoints = inInfo.getTrack().getNumPoints();
405 for (int i=0; i<numPoints; i++)
407 point = inInfo.getTrack().getPoint(i);
408 if (!exportSelection || (i>=selStart && i<=selEnd))
410 // Make a wpt element for each waypoint
411 if (point.isWaypoint() && exportWaypoints)
413 String pointSource = (inGpxCachers == null? null : getPointSource(inGpxCachers, point));
414 if (pointSource != null)
416 // If timestamp checkbox is off, strip time
417 if (!exportTimestamps) {
418 pointSource = stripTime(pointSource);
420 inWriter.write('\t');
421 inWriter.write(pointSource);
422 inWriter.write('\n');
425 exportWaypoint(point, inWriter, inSettings);
431 // Export both route points and then track points
432 if (inSettings.getExportTrackPoints() || inSettings.getExportPhotoPoints() || inSettings.getExportAudioPoints())
434 // Output all route points (if any)
435 numSaved += writeTrackPoints(inWriter, inInfo, inSettings,
436 true, inGpxCachers, "<rtept", "\t<rte><number>1</number>\n",
438 // Output all track points, if any
439 String trackStart = "\t<trk>\n\t\t<name>" + trackName + "</name>\n\t\t<number>1</number>\n\t\t<trkseg>\n";
440 numSaved += writeTrackPoints(inWriter, inInfo, inSettings,
441 false, inGpxCachers, "<trkpt", trackStart,
442 "\t</trkseg>\n\t<trkseg>\n", "\t\t</trkseg>\n\t</trk>\n");
445 inWriter.write("</gpx>\n");
451 * Write the name and description according to the GPX version number
452 * @param inWriter writer object
453 * @param inName name, or null if none supplied
454 * @param inDesc description, or null if none supplied
455 * @param inIsVersion1_1 true if gpx version 1.1, false for version 1.0
457 private static void writeNameAndDescription(OutputStreamWriter inWriter,
458 String inName, String inDesc, boolean inIsVersion1_1) throws IOException
460 // Position of name and description fields needs to be different for GPX1.0 and GPX1.1
463 // GPX 1.1 has the name and description inside a metadata tag
464 inWriter.write("\t<metadata>\n");
466 if (inName != null && !inName.equals(""))
468 if (inIsVersion1_1) {inWriter.write('\t');}
469 inWriter.write("\t<name>");
470 inWriter.write(inName);
471 inWriter.write("</name>\n");
473 if (inIsVersion1_1) {inWriter.write('\t');}
474 inWriter.write("\t<desc>");
475 inWriter.write(inDesc);
476 inWriter.write("</desc>\n");
479 inWriter.write("\t</metadata>\n");
484 * Loop through the track outputting the relevant track points
485 * @param inWriter writer object for output
486 * @param inInfo track info object containing track
487 * @param inSettings export settings defining what should be exported
488 * @param inOnlyCopies true to only export if source can be copied
489 * @param inCachers list of GpxCachers
490 * @param inPointTag tag to match for each point
491 * @param inStartTag start tag to output
492 * @param inSegmentTag tag to output between segments (or null)
493 * @param inEndTag end tag to output
495 private static int writeTrackPoints(OutputStreamWriter inWriter,
496 TrackInfo inInfo, SettingsForExport inSettings,
497 boolean inOnlyCopies, GpxCacherList inCachers, String inPointTag,
498 String inStartTag, String inSegmentTag, String inEndTag)
501 // Note: Too many input parameters to this method but avoids duplication
502 // of output functionality for writing track points and route points
503 int numPoints = inInfo.getTrack().getNumPoints();
504 int selStart = inInfo.getSelection().getStart();
505 int selEnd = inInfo.getSelection().getEnd();
507 final boolean exportSelection = inSettings.getExportJustSelection();
508 final boolean exportTrackPoints = inSettings.getExportTrackPoints();
509 final boolean exportPhotos = inSettings.getExportPhotoPoints();
510 final boolean exportAudios = inSettings.getExportAudioPoints();
511 final boolean exportTimestamps = inSettings.getExportTimestamps();
512 // Loop over track points
513 for (int i=0; i<numPoints; i++)
515 DataPoint point = inInfo.getTrack().getPoint(i);
516 if ((!exportSelection || (i>=selStart && i<=selEnd)) && !point.isWaypoint())
518 if ((point.getPhoto()==null && exportTrackPoints) || (point.getPhoto()!=null && exportPhotos)
519 || (point.getAudio()!=null && exportAudios))
521 // get the source from the point (if any)
522 String pointSource = getPointSource(inCachers, point);
523 // Clear point source if it's the wrong type of point (eg changed from waypoint or route point)
524 if (pointSource != null && !pointSource.trim().toLowerCase().startsWith(inPointTag)) {
527 if (pointSource != null || !inOnlyCopies)
529 // restart track segment if necessary
530 if ((numSaved > 0) && point.getSegmentStart() && (inSegmentTag != null)) {
531 inWriter.write(inSegmentTag);
533 if (numSaved == 0) {inWriter.write(inStartTag);}
534 if (pointSource != null)
536 // If timestamps checkbox is off, strip the time
537 if (!exportTimestamps) {
538 pointSource = stripTime(pointSource);
540 inWriter.write(pointSource);
541 inWriter.write('\n');
546 exportTrackpoint(point, inWriter, inSettings);
555 inWriter.write(inEndTag);
562 * Get the point source for the specified point
563 * @param inCachers list of GPX cachers to ask for source
564 * @param inPoint point object
565 * @return xml source if available, or null otherwise
567 private static String getPointSource(GpxCacherList inCachers, DataPoint inPoint)
569 if (inCachers == null || inPoint == null) {return null;}
570 String source = inCachers.getSourceString(inPoint);
571 if (source == null || !inPoint.isModified()) {return source;}
572 // Point has been modified - maybe it's possible to modify the source
573 source = replaceGpxTags(source, "lat=\"", "\"", inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
574 source = replaceGpxTags(source, "lon=\"", "\"", inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
575 source = replaceGpxTags(source, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
576 source = replaceGpxTags(source, "<time>", "</time>", inPoint.getTimestamp().getText(Timestamp.Format.ISO8601, null));
577 if (inPoint.isWaypoint())
579 source = replaceGpxTags(source, "<name>", "</name>", XmlUtils.fixCdata(inPoint.getWaypointName()));
582 source = source.replaceAll("<description>", "<desc>").replaceAll("</description>", "</desc>");
584 source = replaceGpxTags(source, "<desc>", "</desc>",
585 XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION)));
587 // photo / audio links
588 if (source != null && (inPoint.hasMedia() || source.indexOf("</link>") > 0)) {
589 source = replaceMediaLinks(source, makeMediaLink(inPoint));
595 * Replace the given value into the given XML string
596 * @param inSource source XML for point
597 * @param inStartTag start tag for field
598 * @param inEndTag end tag for field
599 * @param inValue value to replace between start tag and end tag
600 * @return modified String, or null if not possible
602 private static String replaceGpxTags(String inSource, String inStartTag, String inEndTag, String inValue)
604 if (inSource == null) {return null;}
605 // Look for start and end tags within source
606 final int startPos = inSource.indexOf(inStartTag);
607 final int endPos = inSource.indexOf(inEndTag, startPos+inStartTag.length());
608 if (startPos > 0 && endPos > 0)
610 String origValue = inSource.substring(startPos + inStartTag.length(), endPos);
611 if (inValue != null && origValue.equals(inValue)) {
615 else if (inValue == null || inValue.equals("")) {
616 // Need to delete value
617 return inSource.substring(0, startPos) + inSource.substring(endPos + inEndTag.length());
620 // Need to replace value
621 return inSource.substring(0, startPos+inStartTag.length()) + inValue + inSource.substring(endPos);
624 // Value not found for this field in original source
625 if (inValue == null || inValue.equals("")) {return inSource;}
631 * Replace the media tags in the given XML string
632 * @param inSource source XML for point
633 * @param inValue value for the current point
634 * @return modified String, or null if not possible
636 private static String replaceMediaLinks(String inSource, String inValue)
638 if (inSource == null) {return null;}
639 // Note that this method is very similar to replaceGpxTags except there can be multiple link tags
640 // and the tags must have attributes. So either one heavily parameterized method or two.
641 // Look for start and end tags within source
642 final String STARTTEXT = "<link";
643 final String ENDTEXT = "</link>";
644 final int startPos = inSource.indexOf(STARTTEXT);
645 final int endPos = inSource.lastIndexOf(ENDTEXT);
646 if (startPos > 0 && endPos > 0)
648 String origValue = inSource.substring(startPos, endPos + ENDTEXT.length());
649 if (inValue != null && origValue.equals(inValue)) {
653 else if (inValue == null || inValue.equals("")) {
654 // Need to delete value
655 return inSource.substring(0, startPos) + inSource.substring(endPos + ENDTEXT.length());
658 // Need to replace value
659 return inSource.substring(0, startPos) + inValue + inSource.substring(endPos + ENDTEXT.length());
662 // Value not found for this field in original source
663 if (inValue == null || inValue.equals("")) {return inSource;}
669 * Get the header string for the xml document including encoding
670 * @param inWriter writer object
671 * @return header string defining encoding
673 private static String getXmlHeaderString(OutputStreamWriter inWriter)
675 return "<?xml version=\"1.0\" encoding=\"" + XmlUtils.getEncoding(inWriter) + "\"?>\n";
680 * Get the header string for the gpx tag
681 * @param inCachers cacher list to ask for headers, if available
682 * @return header string from cachers or as default
684 private static String getGpxHeaderString(GpxCacherList inCachers)
686 String gpxHeader = null;
687 if (inCachers != null) {gpxHeader = inCachers.getFirstHeader();}
688 if (gpxHeader == null || gpxHeader.length() < 5)
690 // TODO: Consider changing this to default to GPX 1.1
691 // Create default (1.0) header
692 gpxHeader = "<gpx version=\"1.0\" creator=\"" + GPX_CREATOR
693 + "\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
694 + " xmlns=\"http://www.topografix.com/GPX/1/0\""
695 + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\n";
697 return gpxHeader + "\n";
702 * Export the specified waypoint into the file
703 * @param inPoint waypoint to export
704 * @param inWriter writer object
705 * @param inSettings export settings
706 * @throws IOException on write failure
708 private static void exportWaypoint(DataPoint inPoint, Writer inWriter,
709 SettingsForExport inSettings)
712 inWriter.write("\t<wpt lat=\"");
713 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
714 inWriter.write("\" lon=\"");
715 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
716 inWriter.write("\">\n");
717 // altitude if available
718 if (inPoint.hasAltitude() || inSettings.getExportMissingAltitudesAsZero())
720 inWriter.write("\t\t<ele>");
721 inWriter.write(inPoint.hasAltitude() ? inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES) : "0");
722 inWriter.write("</ele>\n");
724 // timestamp if available (some waypoints have timestamps, some not)
725 if (inPoint.hasTimestamp() && inSettings.getExportTimestamps())
727 inWriter.write("\t\t<time>");
728 inWriter.write(inPoint.getTimestamp().getText(Timestamp.Format.ISO8601, null));
729 inWriter.write("</time>\n");
731 // write waypoint name after elevation and time
732 inWriter.write("\t\t<name>");
733 inWriter.write(XmlUtils.fixCdata(inPoint.getWaypointName().trim()));
734 inWriter.write("</name>\n");
735 // description, if any
736 String desc = XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION));
737 if (desc != null && !desc.equals(""))
739 inWriter.write("\t\t<desc>");
740 inWriter.write(desc);
741 inWriter.write("</desc>\n");
743 // Media links, if any
744 if (inSettings.getExportPhotoPoints() && inPoint.getPhoto() != null)
746 inWriter.write("\t\t");
747 inWriter.write(makeMediaLink(inPoint.getPhoto()));
748 inWriter.write('\n');
750 if (inSettings.getExportAudioPoints() && inPoint.getAudio() != null)
752 inWriter.write("\t\t");
753 inWriter.write(makeMediaLink(inPoint.getAudio()));
754 inWriter.write('\n');
756 // write waypoint type if any
757 String type = inPoint.getFieldValue(Field.WAYPT_TYPE);
761 if (!type.equals(""))
763 inWriter.write("\t\t<type>");
764 inWriter.write(type);
765 inWriter.write("</type>\n");
768 inWriter.write("\t</wpt>\n");
773 * Export the specified trackpoint into the file
774 * @param inPoint trackpoint to export
775 * @param inWriter writer object
776 * @param inSettings export settings
778 private static void exportTrackpoint(DataPoint inPoint, Writer inWriter, SettingsForExport inSettings)
781 inWriter.write("\t\t\t<trkpt lat=\"");
782 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
783 inWriter.write("\" lon=\"");
784 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
785 inWriter.write("\">\n");
787 if (inPoint.hasAltitude() || inSettings.getExportMissingAltitudesAsZero())
789 inWriter.write("\t\t\t\t<ele>");
790 inWriter.write(inPoint.hasAltitude() ? inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES) : "0");
791 inWriter.write("</ele>\n");
793 // Maybe take timestamp from photo if the point hasn't got one
794 Timestamp pointTimestamp = getPointTimestamp(inPoint, inSettings);
795 // timestamp if available (and selected)
796 if (pointTimestamp != null && inSettings.getExportTimestamps())
798 inWriter.write("\t\t\t\t<time>");
799 inWriter.write(pointTimestamp.getText(Timestamp.Format.ISO8601, null));
800 inWriter.write("</time>\n");
803 if (inPoint.getPhoto() != null && inSettings.getExportPhotoPoints())
805 inWriter.write("\t\t\t\t");
806 inWriter.write(makeMediaLink(inPoint.getPhoto()));
807 inWriter.write("\n");
809 if (inPoint.getAudio() != null && inSettings.getExportAudioPoints()) {
810 inWriter.write(makeMediaLink(inPoint.getAudio()));
812 inWriter.write("\t\t\t</trkpt>\n");
817 * Make the xml for the media link(s)
818 * @param inPoint point to generate text for
819 * @return link tags, or null if no links
821 private static String makeMediaLink(DataPoint inPoint)
823 Photo photo = inPoint.getPhoto();
824 AudioClip audio = inPoint.getAudio();
825 if (photo == null && audio == null) {
828 String linkText = "";
830 linkText = makeMediaLink(photo);
833 linkText += makeMediaLink(audio);
839 * Make the media link for a single media item
840 * @param inMedia media item, either photo or audio
841 * @return link for this media
843 private static String makeMediaLink(MediaObject inMedia)
845 if (inMedia.getFile() != null)
847 return "<link href=\"" + inMedia.getFile().getAbsolutePath() + "\"><text>" + inMedia.getName() + "</text></link>";
848 if (inMedia.getUrl() != null)
850 return "<link href=\"" + inMedia.getUrl() + "\"><text>" + inMedia.getName() + "</text></link>";
851 // No link available, must have been loaded from zip file - no link possible
857 * Strip the time from a GPX point source string
858 * @param inPointSource point source to copy
859 * @return point source with timestamp removed
861 private static String stripTime(String inPointSource)
863 return inPointSource.replaceAll("[ \t]*<time>.*?</time>", "");
867 * Get the timestamp from the point or its media
868 * @param inPoint point object
869 * @param inSettings export settings
870 * @return Timestamp object if available, or null
872 private static Timestamp getPointTimestamp(DataPoint inPoint, SettingsForExport inSettings)
874 if (inPoint.hasTimestamp())
876 return inPoint.getTimestamp();
878 if (inPoint.getPhoto() != null && inSettings.getExportPhotoPoints())
880 if (inPoint.getPhoto().hasTimestamp())
882 return inPoint.getPhoto().getTimestamp();
885 if (inPoint.getAudio() != null && inSettings.getExportAudioPoints())
887 if (inPoint.getAudio().hasTimestamp())
889 return inPoint.getAudio().getTimestamp();