]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/GpxExporter.java
Version 14, October 2012
[GpsPrune.git] / tim / prune / save / GpxExporter.java
1 package tim.prune.save;
2
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.io.File;
10 import java.io.FileOutputStream;
11 import java.io.IOException;
12 import java.io.OutputStreamWriter;
13 import java.io.Writer;
14 import java.nio.charset.Charset;
15
16 import javax.swing.BorderFactory;
17 import javax.swing.Box;
18 import javax.swing.BoxLayout;
19 import javax.swing.ButtonGroup;
20 import javax.swing.JButton;
21 import javax.swing.JCheckBox;
22 import javax.swing.JDialog;
23 import javax.swing.JFileChooser;
24 import javax.swing.JFrame;
25 import javax.swing.JLabel;
26 import javax.swing.JOptionPane;
27 import javax.swing.JPanel;
28 import javax.swing.JRadioButton;
29 import javax.swing.JTextField;
30 import javax.swing.border.EtchedBorder;
31
32 import tim.prune.App;
33 import tim.prune.GenericFunction;
34 import tim.prune.GpsPrune;
35 import tim.prune.I18nManager;
36 import tim.prune.UpdateMessageBroker;
37 import tim.prune.config.Config;
38 import tim.prune.data.Altitude;
39 import tim.prune.data.AudioClip;
40 import tim.prune.data.Coordinate;
41 import tim.prune.data.DataPoint;
42 import tim.prune.data.Field;
43 import tim.prune.data.MediaObject;
44 import tim.prune.data.Photo;
45 import tim.prune.data.RecentFile;
46 import tim.prune.data.Timestamp;
47 import tim.prune.data.TrackInfo;
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;
52
53
54 /**
55  * Class to export track information
56  * into a specified Gpx file
57  */
58 public class GpxExporter extends GenericFunction implements Runnable
59 {
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         private static String _systemEncoding = null;
71
72         /** this program name */
73         private static final String GPX_CREATOR = "GpsPrune v" + GpsPrune.VERSION_NUMBER + " activityworkshop.net";
74
75
76         /**
77          * Constructor
78          * @param inApp app object
79          */
80         public GpxExporter(App inApp)
81         {
82                 super(inApp);
83                 _trackInfo = inApp.getTrackInfo();
84         }
85
86         /** Get name key */
87         public String getNameKey() {
88                 return "function.exportgpx";
89         }
90
91         /**
92          * Show the dialog to select options and export file
93          */
94         public void begin()
95         {
96                 // Make dialog window
97                 if (_dialog == null)
98                 {
99                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
100                         _dialog.setLocationRelativeTo(_parentFrame);
101                         _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
102                         _systemEncoding = getSystemEncoding();
103                         _dialog.getContentPane().add(makeDialogComponents());
104                         _dialog.pack();
105                 }
106                 _pointTypeSelector.init(_app.getTrackInfo());
107                 _encodingsPanel.setVisible(!isSystemUtf8());
108                 if (!isSystemUtf8()) {
109                         _useSystemRadio.setText(I18nManager.getText("dialog.exportgpx.encoding.system")
110                                 + " (" + (_systemEncoding == null ? "unknown" : _systemEncoding) + ")");
111                 }
112                 _dialog.setVisible(true);
113         }
114
115
116         /**
117          * Create dialog components
118          * @return Panel containing all gui elements in dialog
119          */
120         private Component makeDialogComponents()
121         {
122                 JPanel dialogPanel = new JPanel();
123                 dialogPanel.setLayout(new BorderLayout());
124                 JPanel mainPanel = new JPanel();
125                 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
126                 // Make a panel for the name/desc text boxes
127                 JPanel descPanel = new JPanel();
128                 descPanel.setLayout(new GridLayout(2, 2));
129                 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.name")));
130                 _nameField = new JTextField(10);
131                 descPanel.add(_nameField);
132                 descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.desc")));
133                 _descriptionField = new JTextField(10);
134                 descPanel.add(_descriptionField);
135                 mainPanel.add(descPanel);
136                 mainPanel.add(Box.createVerticalStrut(5));
137                 // point type selection (track points, waypoints, photo points)
138                 _pointTypeSelector = new PointTypeSelector();
139                 mainPanel.add(_pointTypeSelector);
140                 // checkboxes for timestamps and copying
141                 JPanel checkPanel = new JPanel();
142                 _timestampsCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.includetimestamps"));
143                 _timestampsCheckbox.setSelected(true);
144                 checkPanel.add(_timestampsCheckbox);
145                 _copySourceCheckbox = new JCheckBox(I18nManager.getText("dialog.exportgpx.copysource"));
146                 _copySourceCheckbox.setSelected(true);
147                 checkPanel.add(_copySourceCheckbox);
148                 mainPanel.add(checkPanel);
149                 // panel for selecting character encoding
150                 _encodingsPanel = new JPanel();
151                 if (!isSystemUtf8())
152                 {
153                         // only add this panel if system isn't utf8 (or can't be identified yet)
154                         _encodingsPanel.setBorder(BorderFactory.createCompoundBorder(
155                                 BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(4, 4, 4, 4)));
156                         _encodingsPanel.setLayout(new BorderLayout());
157                         _encodingsPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.encoding")), BorderLayout.NORTH);
158                         JPanel radioPanel = new JPanel();
159                         radioPanel.setLayout(new FlowLayout());
160                         ButtonGroup radioGroup = new ButtonGroup();
161                         _useSystemRadio = new JRadioButton(I18nManager.getText("dialog.exportgpx.encoding.system"));
162                         _forceUtf8Radio = new JRadioButton(I18nManager.getText("dialog.exportgpx.encoding.utf8"));
163                         radioGroup.add(_useSystemRadio);
164                         radioGroup.add(_forceUtf8Radio);
165                         radioPanel.add(_useSystemRadio);
166                         radioPanel.add(_forceUtf8Radio);
167                         _useSystemRadio.setSelected(true);
168                         _encodingsPanel.add(radioPanel, BorderLayout.CENTER);
169                         mainPanel.add(_encodingsPanel);
170                 }
171                 dialogPanel.add(mainPanel, BorderLayout.CENTER);
172
173                 // close dialog if escape pressed
174                 _nameField.addKeyListener(new DialogCloser(_dialog));
175                 // button panel at bottom
176                 JPanel buttonPanel = new JPanel();
177                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
178                 JButton okButton = new JButton(I18nManager.getText("button.ok"));
179                 ActionListener okListener = new ActionListener() {
180                         public void actionPerformed(ActionEvent e)
181                         {
182                                 startExport();
183                         }
184                 };
185                 okButton.addActionListener(okListener);
186                 _descriptionField.addActionListener(okListener);
187                 buttonPanel.add(okButton);
188                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
189                 cancelButton.addActionListener(new ActionListener() {
190                         public void actionPerformed(ActionEvent e) {
191                                 _dialog.dispose();
192                         }
193                 });
194                 buttonPanel.add(cancelButton);
195                 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
196                 dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
197                 return dialogPanel;
198         }
199
200
201         /**
202          * Start the export process based on the input parameters
203          */
204         private void startExport()
205         {
206                 // OK pressed, so check selections
207                 if (!_pointTypeSelector.getAnythingSelected())
208                 {
209                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.notypesselected"),
210                                 I18nManager.getText("dialog.saveoptions.title"), JOptionPane.WARNING_MESSAGE);
211                         return;
212                 }
213                 // Choose output file
214                 File saveFile = chooseGpxFile(_parentFrame);
215                 if (saveFile != null)
216                 {
217                         // New file or overwrite confirmed, so initiate export in separate thread
218                         _exportFile = saveFile;
219                         new Thread(this).start();
220                 }
221         }
222
223
224         /**
225          * Select a GPX file to save to
226          * @param inParentFrame parent frame for file chooser dialog
227          * @return selected File, or null if selection cancelled
228          */
229         public static File chooseGpxFile(JFrame inParentFrame)
230         {
231                 File saveFile = null;
232                 JFileChooser fileChooser = new JFileChooser();
233                 fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
234                 fileChooser.setFileFilter(new GenericFileFilter("filetype.gpx", new String[] {"gpx"}));
235                 fileChooser.setAcceptAllFileFilterUsed(false);
236                 // start from directory in config which should be set
237                 String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
238                 if (configDir != null) {fileChooser.setCurrentDirectory(new File(configDir));}
239
240                 // Allow choose again if an existing file is selected
241                 boolean chooseAgain = false;
242                 do
243                 {
244                         chooseAgain = false;
245                         if (fileChooser.showSaveDialog(inParentFrame) == JFileChooser.APPROVE_OPTION)
246                         {
247                                 // OK pressed and file chosen
248                                 File file = fileChooser.getSelectedFile();
249                                 // Check file extension
250                                 if (!file.getName().toLowerCase().endsWith(".gpx"))
251                                 {
252                                         file = new File(file.getAbsolutePath() + ".gpx");
253                                 }
254                                 // Check if file exists and if necessary prompt for overwrite
255                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
256                                 if (!file.exists() || JOptionPane.showOptionDialog(inParentFrame,
257                                                 I18nManager.getText("dialog.save.overwrite.text"),
258                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
259                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
260                                         == JOptionPane.YES_OPTION)
261                                 {
262                                         // new file or overwrite confirmed
263                                         saveFile = file;
264                                 }
265                                 else
266                                 {
267                                         // file exists and overwrite cancelled - select again
268                                         chooseAgain = true;
269                                 }
270                         }
271                 } while (chooseAgain);
272                 return saveFile;
273         }
274
275
276         /**
277          * Run method for controlling separate thread for exporting
278          */
279         public void run()
280         {
281                 // Instantiate source file cachers in case we want to copy output
282                 GpxCacherList gpxCachers = null;
283                 if (_copySourceCheckbox.isSelected()) {
284                         gpxCachers = new GpxCacherList(_trackInfo.getFileInfo());
285                 }
286                 OutputStreamWriter writer = null;
287                 try
288                 {
289                         // normal writing to file - firstly specify UTF8 encoding if requested
290                         if (_forceUtf8Radio != null && _forceUtf8Radio.isSelected())
291                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile), "UTF-8");
292                         else
293                                 writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
294                         final boolean[] saveFlags = {_pointTypeSelector.getTrackpointsSelected(), _pointTypeSelector.getWaypointsSelected(),
295                                 _pointTypeSelector.getPhotopointsSelected(), _pointTypeSelector.getAudiopointsSelected(),
296                                 _pointTypeSelector.getJustSelection(), _timestampsCheckbox.isSelected()};
297                         // write file
298                         final int numPoints = exportData(writer, _trackInfo, _nameField.getText(),
299                                 _descriptionField.getText(), saveFlags, gpxCachers);
300
301                         // close file
302                         writer.close();
303                         // Store directory in config for later
304                         Config.setConfigString(Config.KEY_TRACK_DIR, _exportFile.getParentFile().getAbsolutePath());
305                         // Add to recent file list
306                         Config.getRecentFileList().addFile(new RecentFile(_exportFile, true));
307                         // Show confirmation
308                         UpdateMessageBroker.informSubscribers();
309                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
310                                  + " " + numPoints + " " + I18nManager.getText("confirm.save.ok2")
311                                  + " " + _exportFile.getAbsolutePath());
312                         // export successful so need to close dialog and return
313                         _dialog.dispose();
314                         return;
315                 }
316                 catch (IOException ioe)
317                 {
318                         // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
319                         try {
320                                 if (writer != null) writer.close();
321                         }
322                         catch (IOException ioe2) {}
323                         JOptionPane.showMessageDialog(_parentFrame,
324                                 I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
325                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
326                 }
327                 // if not returned already, export failed so need to recall the file selection
328                 startExport();
329         }
330
331
332         /**
333          * Export the information to the given writer
334          * @param inWriter writer object
335          * @param inInfo track info object
336          * @param inName name of track (optional)
337          * @param inDesc description of track (optional)
338          * @param inSaveFlags array of booleans to export tracks, waypoints, photos, audios, selection, timestamps
339          * @param inGpxCachers list of Gpx cachers containing input data
340          * @return number of points written
341          * @throws IOException if io errors occur on write
342          */
343         public static int exportData(OutputStreamWriter inWriter, TrackInfo inInfo, String inName,
344                 String inDesc, boolean[] inSaveFlags, GpxCacherList inGpxCachers) throws IOException
345         {
346                 // Write or copy headers
347                 inWriter.write(getXmlHeaderString(inWriter));
348                 final String gpxHeader = getGpxHeaderString(inGpxCachers);
349                 final boolean isVersion1_1 = (gpxHeader.toUpperCase().indexOf("GPX/1/1") > 0);
350                 inWriter.write(gpxHeader);
351                 // Name field
352                 String trackName = (inName != null && !inName.equals("")) ? inName : "GpsPruneTrack";
353                 writeNameAndDescription(inWriter, inName, inDesc, isVersion1_1);
354
355                 int i = 0;
356                 DataPoint point = null;
357                 final boolean exportTrackpoints = inSaveFlags[0];
358                 final boolean exportWaypoints = inSaveFlags[1];
359                 final boolean exportPhotos = inSaveFlags[2];
360                 final boolean exportAudios = inSaveFlags[3];
361                 final boolean exportSelection = inSaveFlags[4];
362                 final boolean exportTimestamps = inSaveFlags[5];
363                 // Examine selection
364                 int selStart = -1, selEnd = -1;
365                 if (exportSelection) {
366                         selStart = inInfo.getSelection().getStart();
367                         selEnd = inInfo.getSelection().getEnd();
368                 }
369                 // Loop over waypoints
370                 final int numPoints = inInfo.getTrack().getNumPoints();
371                 int numSaved = 0;
372                 for (i=0; i<numPoints; i++)
373                 {
374                         point = inInfo.getTrack().getPoint(i);
375                         if (!exportSelection || (i>=selStart && i<=selEnd))
376                         {
377                                 // Make a wpt element for each waypoint
378                                 if (point.isWaypoint() && exportWaypoints)
379                                 {
380                                         String pointSource = (inGpxCachers == null? null : getPointSource(inGpxCachers, point));
381                                         if (pointSource != null)
382                                         {
383                                                 // If timestamp checkbox is off, strip time
384                                                 if (!exportTimestamps) {
385                                                         pointSource = stripTime(pointSource);
386                                                 }
387                                                 inWriter.write(pointSource);
388                                                 inWriter.write('\n');
389                                         }
390                                         else {
391                                                 exportWaypoint(point, inWriter, exportTimestamps, exportPhotos, exportAudios);
392                                         }
393                                         numSaved++;
394                                 }
395                         }
396                 }
397                 // Export both route points and then track points
398                 if (exportTrackpoints || exportPhotos || exportAudios)
399                 {
400                         // Output all route points (if any)
401                         numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
402                                 exportAudios, exportTimestamps, true, inGpxCachers, "<rtept", "\t<rte><number>1</number>\n",
403                                 null, "\t</rte>\n");
404                         // Output all track points, if any
405                         String trackStart = "\t<trk><name>" + trackName + "</name><number>1</number><trkseg>\n";
406                         numSaved += writeTrackPoints(inWriter, inInfo, exportSelection, exportTrackpoints, exportPhotos,
407                                 exportAudios, exportTimestamps, false, inGpxCachers, "<trkpt", trackStart,
408                                 "\t</trkseg>\n\t<trkseg>\n", "\t</trkseg></trk>\n");
409                 }
410
411                 inWriter.write("</gpx>\n");
412                 return numSaved;
413         }
414
415
416         /**
417          * Write the name and description according to the GPX version number
418          * @param inWriter writer object
419          * @param inName name, or null if none supplied
420          * @param inDesc description, or null if none supplied
421          * @param inIsVersion1_1 true if gpx version 1.1, false for version 1.0
422          */
423         private static void writeNameAndDescription(OutputStreamWriter inWriter,
424                 String inName, String inDesc, boolean inIsVersion1_1) throws IOException
425         {
426                 String desc = (inDesc != null && !inDesc.equals("")) ? inDesc : "Export from GpsPrune";
427                 // Position of name and description fields needs to be different for GPX1.0 and GPX1.1
428                 if (inIsVersion1_1)
429                 {
430                         // GPX 1.1 has the name and description inside a metadata tag
431                         inWriter.write("\t<metadata>\n");
432                 }
433                 if (inName != null && !inName.equals(""))
434                 {
435                         inWriter.write("\t\t<name>");
436                         inWriter.write(inName);
437                         inWriter.write("</name>\n");
438                 }
439                 inWriter.write("\t\t<desc>");
440                 inWriter.write(desc);
441                 inWriter.write("</desc>\n");
442                 if (inIsVersion1_1)
443                 {
444                         inWriter.write("\t</metadata>\n");
445                 }
446         }
447
448         /**
449          * Loop through the track outputting the relevant track points
450          * @param inWriter writer object for output
451          * @param inInfo track info object containing track
452          * @param inExportSelection true to just output current selection
453          * @param inExportTrackpoints true to output track points
454          * @param inExportPhotos true to output photo points
455          * @param inExportAudios true to output audio points
456          * @param inExportTimestamps true to include timestamps in export
457          * @param inOnlyCopies true to only export if source can be copied
458          * @param inCachers list of GpxCachers
459          * @param inPointTag tag to match for each point
460          * @param inStartTag start tag to output
461          * @param inSegmentTag tag to output between segments (or null)
462          * @param inEndTag end tag to output
463          */
464         private static int writeTrackPoints(OutputStreamWriter inWriter,
465                 TrackInfo inInfo, boolean inExportSelection, boolean inExportTrackpoints,
466                 boolean inExportPhotos, boolean inExportAudios, boolean exportTimestamps,
467                 boolean inOnlyCopies, GpxCacherList inCachers, String inPointTag,
468                 String inStartTag, String inSegmentTag, String inEndTag)
469         throws IOException
470         {
471                 // Note: far too many input parameters to this method but avoids duplication
472                 // of output functionality for writing track points and route points
473                 int numPoints = inInfo.getTrack().getNumPoints();
474                 int selStart = inInfo.getSelection().getStart();
475                 int selEnd = inInfo.getSelection().getEnd();
476                 int numSaved = 0;
477                 // Loop over track points
478                 for (int i=0; i<numPoints; i++)
479                 {
480                         DataPoint point = inInfo.getTrack().getPoint(i);
481                         if ((!inExportSelection || (i>=selStart && i<=selEnd)) && !point.isWaypoint())
482                         {
483                                 if ((point.getPhoto()==null && inExportTrackpoints) || (point.getPhoto()!=null && inExportPhotos)
484                                         || (point.getAudio()!=null && inExportAudios))
485                                 {
486                                         // get the source from the point (if any)
487                                         String pointSource = getPointSource(inCachers, point);
488                                         // Clear point source if it's the wrong type of point (eg changed from waypoint or route point)
489                                         if (pointSource != null && !pointSource.toLowerCase().startsWith(inPointTag)) {pointSource = null;}
490                                         if (pointSource != null || !inOnlyCopies)
491                                         {
492                                                 // restart track segment if necessary
493                                                 if ((numSaved > 0) && point.getSegmentStart() && (inSegmentTag != null)) {
494                                                         inWriter.write(inSegmentTag);
495                                                 }
496                                                 if (numSaved == 0) {inWriter.write(inStartTag);}
497                                                 if (pointSource != null)
498                                                 {
499                                                         // If timestamps checkbox is off, strip the time
500                                                         if (!exportTimestamps) {
501                                                                 pointSource = stripTime(pointSource);
502                                                         }
503                                                         inWriter.write(pointSource);
504                                                         inWriter.write('\n');
505                                                 }
506                                                 else {
507                                                         if (!inOnlyCopies) {exportTrackpoint(point, inWriter, exportTimestamps, inExportPhotos, inExportAudios);}
508                                                 }
509                                                 numSaved++;
510                                         }
511                                 }
512                         }
513                 }
514                 if (numSaved > 0) {inWriter.write(inEndTag);}
515                 return numSaved;
516         }
517
518
519         /**
520          * Get the point source for the specified point
521          * @param inCachers list of GPX cachers to ask for source
522          * @param inPoint point object
523          * @return xml source if available, or null otherwise
524          */
525         private static String getPointSource(GpxCacherList inCachers, DataPoint inPoint)
526         {
527                 if (inCachers == null || inPoint == null) {return null;}
528                 String source = inCachers.getSourceString(inPoint);
529                 if (source == null || !inPoint.isModified()) {return source;}
530                 // Point has been modified - maybe it's possible to modify the source
531                 source = replaceGpxTags(source, "lat=\"", "\"", inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
532                 source = replaceGpxTags(source, "lon=\"", "\"", inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
533                 source = replaceGpxTags(source, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
534                 source = replaceGpxTags(source, "<time>", "</time>", inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
535                 if (inPoint.isWaypoint())
536                 {
537                         source = replaceGpxTags(source, "<name>", "</name>", inPoint.getWaypointName());
538                         source = replaceGpxTags(source, "<description>", "</description>",
539                                 XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION)));
540                 }
541                 // photo / audio links
542                 if (source != null && (inPoint.hasMedia() || source.indexOf("</link>") > 0)) {
543                         source = replaceMediaLinks(source, makeMediaLink(inPoint));
544                 }
545                 return source;
546         }
547
548         /**
549          * Replace the given value into the given XML string
550          * @param inSource source XML for point
551          * @param inStartTag start tag for field
552          * @param inEndTag end tag for field
553          * @param inValue value to replace between start tag and end tag
554          * @return modified String, or null if not possible
555          */
556         private static String replaceGpxTags(String inSource, String inStartTag, String inEndTag, String inValue)
557         {
558                 if (inSource == null) {return null;}
559                 // Look for start and end tags within source
560                 final int startPos = inSource.indexOf(inStartTag);
561                 final int endPos = inSource.indexOf(inEndTag, startPos+inStartTag.length());
562                 if (startPos > 0 && endPos > 0)
563                 {
564                         String origValue = inSource.substring(startPos + inStartTag.length(), endPos);
565                         if (inValue != null && origValue.equals(inValue)) {
566                                 // Value unchanged
567                                 return inSource;
568                         }
569                         else if (inValue == null || inValue.equals("")) {
570                                 // Need to delete value
571                                 return inSource.substring(0, startPos) + inSource.substring(endPos + inEndTag.length());
572                         }
573                         else {
574                                 // Need to replace value
575                                 return inSource.substring(0, startPos+inStartTag.length()) + inValue + inSource.substring(endPos);
576                         }
577                 }
578                 // Value not found for this field in original source
579                 if (inValue == null || inValue.equals("")) {return inSource;}
580                 return null;
581         }
582
583
584         /**
585          * Replace the media tags in the given XML string
586          * @param inSource source XML for point
587          * @param inValue value for the current point
588          * @return modified String, or null if not possible
589          */
590         private static String replaceMediaLinks(String inSource, String inValue)
591         {
592                 if (inSource == null) {return null;}
593                 // Note that this method is very similar to replaceGpxTags except there can be multiple link tags
594                 // and the tags must have attributes.  So either one heavily parameterized method or two.
595                 // Look for start and end tags within source
596                 final String STARTTEXT = "<link";
597                 final String ENDTEXT = "</link>";
598                 final int startPos = inSource.indexOf(STARTTEXT);
599                 final int endPos = inSource.lastIndexOf(ENDTEXT);
600                 if (startPos > 0 && endPos > 0)
601                 {
602                         String origValue = inSource.substring(startPos, endPos + ENDTEXT.length());
603                         if (inValue != null && origValue.equals(inValue)) {
604                                 // Value unchanged
605                                 return inSource;
606                         }
607                         else if (inValue == null || inValue.equals("")) {
608                                 // Need to delete value
609                                 return inSource.substring(0, startPos) + inSource.substring(endPos + ENDTEXT.length());
610                         }
611                         else {
612                                 // Need to replace value
613                                 return inSource.substring(0, startPos) + inValue + inSource.substring(endPos + ENDTEXT.length());
614                         }
615                 }
616                 // Value not found for this field in original source
617                 if (inValue == null || inValue.equals("")) {return inSource;}
618                 return null;
619         }
620
621
622         /**
623          * Get the header string for the xml document including encoding
624          * @param inWriter writer object
625          * @return header string defining encoding
626          */
627         private static String getXmlHeaderString(OutputStreamWriter inWriter)
628         {
629                 return "<?xml version=\"1.0\" encoding=\"" + getEncoding(inWriter) + "\"?>\n";
630         }
631
632
633         /**
634          * Get the default system encoding using a writer
635          * @param inWriter writer object
636          * @return string defining encoding
637          */
638         private static String getEncoding(OutputStreamWriter inWriter)
639         {
640                 String encoding = inWriter.getEncoding();
641                 try {
642                         encoding =  Charset.forName(encoding).name();
643                 }
644                 catch (Exception e) {} // ignore failure to find encoding
645                 return encoding;
646         }
647
648
649         /**
650          * Use a temporary file to obtain the name of the default system encoding
651          * @return name of default system encoding, or null if write failed
652          */
653         private static String getSystemEncoding()
654         {
655                 File tempFile = null;
656                 String encoding = null;
657                 try
658                 {
659                         tempFile = File.createTempFile("prune", null);
660                         OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(tempFile));
661                         encoding = getEncoding(writer);
662                         writer.close();
663                 }
664                 catch (IOException e) {} // value stays null
665                 // Delete temp file
666                 if (tempFile != null && tempFile.exists()) {
667                         if (!tempFile.delete()) {
668                                 System.err.println("Cannot delete temp file: " + tempFile.getAbsolutePath());
669                         }
670                 }
671                 // If writing failed (eg permissions) then just ask system for default
672                 if (encoding == null) encoding = Charset.defaultCharset().name();
673                 return encoding;
674         }
675
676         /**
677          * Creates temp file if necessary to check system encoding
678          * @return true if system uses UTF-8 by default
679          */
680         private static boolean isSystemUtf8()
681         {
682                 if (_systemEncoding == null) _systemEncoding = getSystemEncoding();
683                 return (_systemEncoding != null && _systemEncoding.toUpperCase().equals("UTF-8"));
684         }
685
686         /**
687          * Get the header string for the gpx tag
688          * @param inCachers cacher list to ask for headers, if available
689          * @return header string from cachers or as default
690          */
691         private static String getGpxHeaderString(GpxCacherList inCachers)
692         {
693                 String gpxHeader = null;
694                 if (inCachers != null) {gpxHeader = inCachers.getFirstHeader();}
695                 if (gpxHeader == null || gpxHeader.length() < 5)
696                 {
697                         // Create default (1.0) header
698                         gpxHeader = "<gpx version=\"1.0\" creator=\"" + GPX_CREATOR
699                                 + "\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
700                                 + " xmlns=\"http://www.topografix.com/GPX/1/0\""
701                                 + " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">\n";
702                 }
703                 return gpxHeader + "\n";
704         }
705
706
707         /**
708          * Export the specified waypoint into the file
709          * @param inPoint waypoint to export
710          * @param inWriter writer object
711          * @param inTimestamps true to export timestamps too
712          * @param inPhoto true to export link to photo
713          * @param inAudio true to export link to audio
714          * @throws IOException on write failure
715          */
716         private static void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps,
717                 boolean inPhoto, boolean inAudio)
718                 throws IOException
719         {
720                 inWriter.write("\t<wpt lat=\"");
721                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
722                 inWriter.write("\" lon=\"");
723                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
724                 inWriter.write("\">\n");
725                 // altitude if available
726                 if (inPoint.hasAltitude())
727                 {
728                         inWriter.write("\t\t<ele>");
729                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
730                         inWriter.write("</ele>\n");
731                 }
732                 // timestamp if available (point might have timestamp and then be turned into a waypoint)
733                 if (inPoint.hasTimestamp() && inTimestamps)
734                 {
735                         inWriter.write("\t\t<time>");
736                         inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
737                         inWriter.write("</time>\n");
738                 }
739                 // write waypoint name after elevation and time
740                 inWriter.write("\t\t<name>");
741                 inWriter.write(inPoint.getWaypointName().trim());
742                 inWriter.write("</name>\n");
743                 // description, if any
744                 String desc = XmlUtils.fixCdata(inPoint.getFieldValue(Field.DESCRIPTION));
745                 if (desc != null && !desc.equals(""))
746                 {
747                         inWriter.write("\t\t<description>");
748                         inWriter.write(desc);
749                         inWriter.write("</description>\n");
750                 }
751                 // Media links, if any
752                 if (inPhoto && inPoint.getPhoto() != null)
753                 {
754                         inWriter.write("\t\t");
755                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
756                         inWriter.write('\n');
757                 }
758                 if (inAudio && inPoint.getAudio() != null)
759                 {
760                         inWriter.write("\t\t");
761                         inWriter.write(makeMediaLink(inPoint.getAudio()));
762                         inWriter.write('\n');
763                 }
764                 // write waypoint type if any
765                 String type = inPoint.getFieldValue(Field.WAYPT_TYPE);
766                 if (type != null)
767                 {
768                         type = type.trim();
769                         if (!type.equals(""))
770                         {
771                                 inWriter.write("\t\t<type>");
772                                 inWriter.write(type);
773                                 inWriter.write("</type>\n");
774                         }
775                 }
776                 inWriter.write("\t</wpt>\n");
777         }
778
779
780         /**
781          * Export the specified trackpoint into the file
782          * @param inPoint trackpoint to export
783          * @param inWriter writer object
784          * @param inTimestamps true to export timestamps too
785          * @param inExportPhoto true to export photo link
786          * @param inExportAudio true to export audio link
787          */
788         private static void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inTimestamps,
789                 boolean inExportPhoto, boolean inExportAudio)
790                 throws IOException
791         {
792                 inWriter.write("\t\t<trkpt lat=\"");
793                 inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
794                 inWriter.write("\" lon=\"");
795                 inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT));
796                 inWriter.write("\">");
797                 // altitude
798                 if (inPoint.hasAltitude())
799                 {
800                         inWriter.write("<ele>");
801                         inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
802                         inWriter.write("</ele>");
803                 }
804                 // timestamp if available (and selected)
805                 if (inPoint.hasTimestamp() && inTimestamps)
806                 {
807                         inWriter.write("<time>");
808                         inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
809                         inWriter.write("</time>");
810                 }
811                 // photo, audio
812                 if (inPoint.getPhoto() != null && inExportPhoto) {
813                         inWriter.write(makeMediaLink(inPoint.getPhoto()));
814                 }
815                 if (inPoint.getAudio() != null && inExportAudio) {
816                         inWriter.write(makeMediaLink(inPoint.getAudio()));
817                 }
818                 inWriter.write("</trkpt>\n");
819         }
820
821
822         /**
823          * Make the xml for the media link(s)
824          * @param inPoint point to generate text for
825          * @return link tags, or null if no links
826          */
827         private static String makeMediaLink(DataPoint inPoint)
828         {
829                 Photo photo = inPoint.getPhoto();
830                 AudioClip audio = inPoint.getAudio();
831                 if (photo == null && audio == null) {
832                         return null;
833                 }
834                 String linkText = "";
835                 if (photo != null) {
836                         linkText = makeMediaLink(photo);
837                 }
838                 if (audio != null) {
839                         linkText += makeMediaLink(audio);
840                 }
841                 return linkText;
842         }
843
844         /**
845          * Make the media link for a single media item
846          * @param inMedia media item, either photo or audio
847          * @return link for this media
848          */
849         private static String makeMediaLink(MediaObject inMedia)
850         {
851                 if (inMedia.getFile() != null)
852                         // file link
853                         return "<link href=\"" + inMedia.getFile().getAbsolutePath() + "\"><text>" + inMedia.getName() + "</text></link>";
854                 if (inMedia.getUrl() != null)
855                         // url link
856                         return "<link href=\"" + inMedia.getUrl() + "\"><text>" + inMedia.getName() + "</text></link>";
857                 // No link available, must have been loaded from zip file - no link possible
858                 return "";
859         }
860
861
862         /**
863          * Strip the time from a GPX point source string
864          * @param inPointSource point source to copy
865          * @return point source with timestamp removed
866          */
867         private static String stripTime(String inPointSource)
868         {
869                 return inPointSource.replaceAll("<time>.*?</time>", "");
870         }
871 }