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