001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io.importexport; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GridBagLayout; 007import java.awt.event.ActionListener; 008import java.awt.event.KeyAdapter; 009import java.awt.event.KeyEvent; 010import java.io.File; 011import java.io.IOException; 012import java.io.OutputStream; 013import java.text.MessageFormat; 014import java.time.Year; 015import java.util.Optional; 016 017import javax.swing.JButton; 018import javax.swing.JCheckBox; 019import javax.swing.JLabel; 020import javax.swing.JList; 021import javax.swing.JOptionPane; 022import javax.swing.JPanel; 023import javax.swing.JScrollPane; 024import javax.swing.ListSelectionModel; 025 026import org.openstreetmap.josm.data.gpx.GpxConstants; 027import org.openstreetmap.josm.data.gpx.GpxData; 028import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 029import org.openstreetmap.josm.gui.ExtendedDialog; 030import org.openstreetmap.josm.gui.MainApplication; 031import org.openstreetmap.josm.gui.layer.GpxLayer; 032import org.openstreetmap.josm.gui.layer.Layer; 033import org.openstreetmap.josm.gui.layer.OsmDataLayer; 034import org.openstreetmap.josm.gui.widgets.JosmTextArea; 035import org.openstreetmap.josm.gui.widgets.JosmTextField; 036import org.openstreetmap.josm.io.Compression; 037import org.openstreetmap.josm.io.GpxWriter; 038import org.openstreetmap.josm.spi.preferences.Config; 039import org.openstreetmap.josm.tools.CheckParameterUtil; 040import org.openstreetmap.josm.tools.GBC; 041 042/** 043 * Exports data to a .gpx file. Data may be native GPX or OSM data which will be converted. 044 * @since 1949 045 */ 046public class GpxExporter extends FileExporter implements GpxConstants { 047 048 private static final String GPL_WARNING = "<html><font color='red' size='-2'>" 049 + tr("Note: GPL is not compatible with the OSM license. Do not upload GPL licensed tracks.") + "</html>"; 050 051 private static final String[] LICENSES = { 052 "Creative Commons By-SA", 053 "Open Database License (ODbL)", 054 "public domain", 055 "GNU Lesser Public License (LGPL)", 056 "BSD License (MIT/X11)"}; 057 058 private static final String[] URLS = { 059 "https://creativecommons.org/licenses/by-sa/3.0", 060 "http://opendatacommons.org/licenses/odbl/1.0", 061 "public domain", 062 "https://www.gnu.org/copyleft/lesser.html", 063 "http://www.opensource.org/licenses/bsd-license.php"}; 064 065 /** 066 * Constructs a new {@code GpxExporter}. 067 */ 068 public GpxExporter() { 069 super(GpxImporter.getFileFilter()); 070 } 071 072 @Override 073 public boolean acceptFile(File pathname, Layer layer) { 074 if (!(layer instanceof OsmDataLayer) && !(layer instanceof GpxLayer)) 075 return false; 076 return super.acceptFile(pathname, layer); 077 } 078 079 @Override 080 public void exportData(File file, Layer layer) throws IOException { 081 exportData(file, layer, false); 082 } 083 084 @Override 085 public void exportDataQuiet(File file, Layer layer) throws IOException { 086 exportData(file, layer, true); 087 } 088 089 private void exportData(File file, Layer layer, boolean quiet) throws IOException { 090 CheckParameterUtil.ensureParameterNotNull(layer, "layer"); 091 if (!(layer instanceof OsmDataLayer) && !(layer instanceof GpxLayer)) 092 throw new IllegalArgumentException(MessageFormat.format("Expected instance of OsmDataLayer or GpxLayer. Got ''{0}''.", layer 093 .getClass().getName())); 094 CheckParameterUtil.ensureParameterNotNull(file, "file"); 095 096 String fn = file.getPath(); 097 if (fn.indexOf('.') == -1) { 098 fn += ".gpx"; 099 file = new File(fn); 100 } 101 102 GpxData gpxData; 103 if (quiet) { 104 gpxData = getGpxData(layer, file); 105 try (OutputStream fo = Compression.getCompressedFileOutputStream(file)) { 106 GpxWriter w = new GpxWriter(fo); 107 w.write(gpxData); 108 w.close(); 109 fo.flush(); 110 } 111 return; 112 } 113 114 // open the dialog asking for options 115 JPanel p = new JPanel(new GridBagLayout()); 116 117 // At this moment, we only need to know the attributes of the GpxData, 118 // conversion of OsmDataLayer (if needed) will be done after the dialog is closed. 119 if (layer instanceof GpxLayer) { 120 gpxData = ((GpxLayer) layer).data; 121 } else { 122 gpxData = new GpxData(); 123 } 124 125 p.add(new JLabel(tr("GPS track description")), GBC.eol()); 126 JosmTextArea desc = new JosmTextArea(3, 40); 127 desc.setWrapStyleWord(true); 128 desc.setLineWrap(true); 129 desc.setText(gpxData.getString(META_DESC)); 130 p.add(new JScrollPane(desc), GBC.eop().fill(GBC.BOTH)); 131 132 JCheckBox author = new JCheckBox(tr("Add author information"), Config.getPref().getBoolean("lastAddAuthor", true)); 133 p.add(author, GBC.eol()); 134 135 JLabel nameLabel = new JLabel(tr("Real name")); 136 p.add(nameLabel, GBC.std().insets(10, 0, 5, 0)); 137 JosmTextField authorName = new JosmTextField(); 138 p.add(authorName, GBC.eol().fill(GBC.HORIZONTAL)); 139 nameLabel.setLabelFor(authorName); 140 141 JLabel emailLabel = new JLabel(tr("E-Mail")); 142 p.add(emailLabel, GBC.std().insets(10, 0, 5, 0)); 143 JosmTextField email = new JosmTextField(); 144 p.add(email, GBC.eol().fill(GBC.HORIZONTAL)); 145 emailLabel.setLabelFor(email); 146 147 JLabel copyrightLabel = new JLabel(tr("Copyright (URL)")); 148 p.add(copyrightLabel, GBC.std().insets(10, 0, 5, 0)); 149 JosmTextField copyright = new JosmTextField(); 150 p.add(copyright, GBC.std().fill(GBC.HORIZONTAL)); 151 copyrightLabel.setLabelFor(copyright); 152 153 JButton predefined = new JButton(tr("Predefined")); 154 p.add(predefined, GBC.eol().insets(5, 0, 0, 0)); 155 156 JLabel copyrightYearLabel = new JLabel(tr("Copyright year")); 157 p.add(copyrightYearLabel, GBC.std().insets(10, 0, 5, 5)); 158 JosmTextField copyrightYear = new JosmTextField(""); 159 p.add(copyrightYear, GBC.eol().fill(GBC.HORIZONTAL)); 160 copyrightYearLabel.setLabelFor(copyrightYear); 161 162 JLabel warning = new JLabel("<html><font size='-2'> </html"); 163 p.add(warning, GBC.eol().fill(GBC.HORIZONTAL).insets(15, 0, 0, 0)); 164 addDependencies(gpxData, author, authorName, email, copyright, predefined, copyrightYear, nameLabel, emailLabel, 165 copyrightLabel, copyrightYearLabel, warning); 166 167 p.add(new JLabel(tr("Keywords")), GBC.eol()); 168 JosmTextField keywords = new JosmTextField(); 169 keywords.setText(gpxData.getString(META_KEYWORDS)); 170 p.add(keywords, GBC.eol().fill(GBC.HORIZONTAL)); 171 172 boolean sel = Config.getPref().getBoolean("gpx.export.colors", true); 173 JCheckBox colors = new JCheckBox(tr("Save track colors in GPX file"), sel); 174 p.add(colors, GBC.eol().fill(GBC.HORIZONTAL)); 175 JCheckBox garmin = new JCheckBox(tr("Use Garmin compatible GPX extensions"), 176 Config.getPref().getBoolean("gpx.export.colors.garmin", false)); 177 garmin.setEnabled(sel); 178 p.add(garmin, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 0, 0, 0)); 179 180 boolean hasPrefs = !gpxData.getLayerPrefs().isEmpty(); 181 JCheckBox layerPrefs = new JCheckBox(tr("Save layer specific preferences"), 182 hasPrefs && Config.getPref().getBoolean("gpx.export.prefs", true)); 183 layerPrefs.setEnabled(hasPrefs); 184 p.add(layerPrefs, GBC.eop().fill(GBC.HORIZONTAL)); 185 186 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), 187 tr("Export options"), 188 tr("Export and Save"), tr("Cancel")) 189 .setButtonIcons("exportgpx", "cancel") 190 .setContent(p); 191 192 colors.addActionListener(l -> { 193 garmin.setEnabled(colors.isSelected()); 194 }); 195 196 garmin.addActionListener(l -> { 197 if (garmin.isSelected() && !ConditionalOptionPaneUtil.showConfirmationDialog( 198 "gpx_color_garmin", 199 ed, 200 new JLabel("<html>" + tr("Garmin track extensions only support 16 colors.") + "<br>" 201 + tr("If you continue, the closest supported track color will be used.") 202 + "</html>"), 203 tr("Information"), 204 JOptionPane.OK_CANCEL_OPTION, 205 JOptionPane.INFORMATION_MESSAGE, 206 JOptionPane.OK_OPTION)) { 207 garmin.setSelected(false); 208 } 209 }); 210 211 if (ed.showDialog().getValue() != 1) { 212 setCanceled(true); 213 return; 214 } 215 setCanceled(false); 216 217 Config.getPref().putBoolean("lastAddAuthor", author.isSelected()); 218 if (!authorName.getText().isEmpty()) { 219 Config.getPref().put("lastAuthorName", authorName.getText()); 220 } 221 if (!copyright.getText().isEmpty()) { 222 Config.getPref().put("lastCopyright", copyright.getText()); 223 } 224 Config.getPref().putBoolean("gpx.export.colors", colors.isSelected()); 225 Config.getPref().putBoolean("gpx.export.colors.garmin", garmin.isSelected()); 226 if (hasPrefs) { 227 Config.getPref().putBoolean("gpx.export.prefs", layerPrefs.isSelected()); 228 } 229 ColorFormat cFormat = null; 230 if (colors.isSelected()) { 231 cFormat = garmin.isSelected() ? ColorFormat.GPXX : ColorFormat.GPXD; 232 } 233 234 gpxData = getGpxData(layer, file); 235 236 // add author and copyright details to the gpx data 237 if (author.isSelected()) { 238 if (!authorName.getText().isEmpty()) { 239 gpxData.put(META_AUTHOR_NAME, authorName.getText()); 240 gpxData.put(META_COPYRIGHT_AUTHOR, authorName.getText()); 241 } 242 if (!email.getText().isEmpty()) { 243 gpxData.put(META_AUTHOR_EMAIL, email.getText()); 244 } 245 if (!copyright.getText().isEmpty()) { 246 gpxData.put(META_COPYRIGHT_LICENSE, copyright.getText()); 247 } 248 if (!copyrightYear.getText().isEmpty()) { 249 gpxData.put(META_COPYRIGHT_YEAR, copyrightYear.getText()); 250 } 251 } 252 253 // add the description to the gpx data 254 if (!desc.getText().isEmpty()) { 255 gpxData.put(META_DESC, desc.getText()); 256 } 257 258 // add keywords to the gpx data 259 if (!keywords.getText().isEmpty()) { 260 gpxData.put(META_KEYWORDS, keywords.getText()); 261 } 262 263 try (OutputStream fo = Compression.getCompressedFileOutputStream(file)) { 264 GpxWriter w = new GpxWriter(fo); 265 w.write(gpxData, cFormat, layerPrefs.isSelected()); 266 w.close(); 267 fo.flush(); 268 } 269 } 270 271 private static GpxData getGpxData(Layer layer, File file) { 272 if (layer instanceof OsmDataLayer) { 273 return ((OsmDataLayer) layer).toGpxData(); 274 } else if (layer instanceof GpxLayer) { 275 return ((GpxLayer) layer).data; 276 } 277 return OsmDataLayer.toGpxData(MainApplication.getLayerManager().getEditDataSet(), file); 278 } 279 280 private static void enableCopyright(final GpxData data, final JosmTextField copyright, final JButton predefined, 281 final JosmTextField copyrightYear, final JLabel copyrightLabel, final JLabel copyrightYearLabel, 282 final JLabel warning, boolean enable) { 283 copyright.setEnabled(enable); 284 predefined.setEnabled(enable); 285 copyrightYear.setEnabled(enable); 286 copyrightLabel.setEnabled(enable); 287 copyrightYearLabel.setEnabled(enable); 288 warning.setText(enable ? GPL_WARNING : "<html><font size='-2'> </html"); 289 290 if (enable) { 291 if (copyrightYear.getText().isEmpty()) { 292 copyrightYear.setText(Optional.ofNullable(data.getString(META_COPYRIGHT_YEAR)).orElseGet( 293 () -> Year.now().toString())); 294 } 295 if (copyright.getText().isEmpty()) { 296 copyright.setText(Optional.ofNullable(data.getString(META_COPYRIGHT_LICENSE)).orElseGet( 297 () -> Config.getPref().get("lastCopyright", "https://creativecommons.org/licenses/by-sa/2.5"))); 298 copyright.setCaretPosition(0); 299 } 300 } else { 301 copyrightYear.setText(""); 302 copyright.setText(""); 303 } 304 } 305 306 // CHECKSTYLE.OFF: ParameterNumber 307 308 /** 309 * Add all those listeners to handle the enable state of the fields. 310 * @param data GPX data 311 * @param author Author checkbox 312 * @param authorName Author name textfield 313 * @param email E-mail textfield 314 * @param copyright Copyright textfield 315 * @param predefined Predefined button 316 * @param copyrightYear Copyright year textfield 317 * @param nameLabel Name label 318 * @param emailLabel E-mail label 319 * @param copyrightLabel Copyright label 320 * @param copyrightYearLabel Copyright year label 321 * @param warning Warning label 322 */ 323 private static void addDependencies( 324 final GpxData data, 325 final JCheckBox author, 326 final JosmTextField authorName, 327 final JosmTextField email, 328 final JosmTextField copyright, 329 final JButton predefined, 330 final JosmTextField copyrightYear, 331 final JLabel nameLabel, 332 final JLabel emailLabel, 333 final JLabel copyrightLabel, 334 final JLabel copyrightYearLabel, 335 final JLabel warning) { 336 337 // CHECKSTYLE.ON: ParameterNumber 338 ActionListener authorActionListener = e -> { 339 boolean b = author.isSelected(); 340 authorName.setEnabled(b); 341 email.setEnabled(b); 342 nameLabel.setEnabled(b); 343 emailLabel.setEnabled(b); 344 if (b) { 345 authorName.setText(Optional.ofNullable(data.getString(META_AUTHOR_NAME)).orElseGet( 346 () -> Config.getPref().get("lastAuthorName"))); 347 email.setText(Optional.ofNullable(data.getString(META_AUTHOR_EMAIL)).orElseGet( 348 () -> Config.getPref().get("lastAuthorEmail"))); 349 } else { 350 authorName.setText(""); 351 email.setText(""); 352 } 353 boolean isAuthorSet = !authorName.getText().isEmpty(); 354 GpxExporter.enableCopyright(data, copyright, predefined, copyrightYear, copyrightLabel, copyrightYearLabel, warning, 355 b && isAuthorSet); 356 }; 357 author.addActionListener(authorActionListener); 358 359 KeyAdapter authorNameListener = new KeyAdapter() { 360 @Override public void keyReleased(KeyEvent e) { 361 boolean b = !authorName.getText().isEmpty() && author.isSelected(); 362 GpxExporter.enableCopyright(data, copyright, predefined, copyrightYear, copyrightLabel, copyrightYearLabel, warning, b); 363 } 364 }; 365 authorName.addKeyListener(authorNameListener); 366 367 predefined.addActionListener(e -> { 368 JList<String> l = new JList<>(LICENSES); 369 l.setVisibleRowCount(LICENSES.length); 370 l.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 371 int answer = JOptionPane.showConfirmDialog( 372 MainApplication.getMainFrame(), 373 new JScrollPane(l), 374 tr("Choose a predefined license"), 375 JOptionPane.OK_CANCEL_OPTION, 376 JOptionPane.QUESTION_MESSAGE 377 ); 378 if (answer != JOptionPane.OK_OPTION || l.getSelectedIndex() == -1) 379 return; 380 StringBuilder license = new StringBuilder(); 381 for (int i : l.getSelectedIndices()) { 382 if (i == 2) { 383 license = new StringBuilder("public domain"); 384 break; 385 } 386 if (license.length() > 0) { 387 license.append(", "); 388 } 389 license.append(URLS[i]); 390 } 391 copyright.setText(license.toString()); 392 copyright.setCaretPosition(0); 393 }); 394 395 authorActionListener.actionPerformed(null); 396 authorNameListener.keyReleased(null); 397 } 398}