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