001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer.gpx;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.GridBagLayout;
007import java.awt.event.ActionEvent;
008import java.awt.event.ActionListener;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.Date;
012import java.util.List;
013import java.util.Map.Entry;
014
015import javax.swing.BorderFactory;
016import javax.swing.ButtonGroup;
017import javax.swing.JCheckBox;
018import javax.swing.JLabel;
019import javax.swing.JPanel;
020import javax.swing.JRadioButton;
021
022import org.openstreetmap.josm.data.gpx.GpxConstants;
023import org.openstreetmap.josm.data.gpx.GpxTrack;
024import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
025import org.openstreetmap.josm.data.gpx.WayPoint;
026import org.openstreetmap.josm.data.osm.DataSet;
027import org.openstreetmap.josm.data.osm.Node;
028import org.openstreetmap.josm.data.osm.Way;
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.OsmDataLayer;
033import org.openstreetmap.josm.spi.preferences.Config;
034import org.openstreetmap.josm.tools.GBC;
035import org.openstreetmap.josm.tools.date.DateUtils;
036
037/**
038 * Converts a {@link GpxLayer} to a {@link OsmDataLayer}.
039 * @since 14129 (extracted from {@link ConvertToDataLayerAction})
040 */
041public class ConvertFromGpxLayerAction extends ConvertToDataLayerAction<GpxLayer> {
042
043    private static final String GPX_SETTING = "gpx.convert-tags";
044
045    /**
046     * Creates a new {@code FromGpxLayer}.
047     * @param layer the source layer
048     */
049    public ConvertFromGpxLayerAction(GpxLayer layer) {
050        super(layer);
051    }
052
053    @Override
054    public DataSet convert() {
055        final DataSet ds = new DataSet();
056
057        List<String> keys = new ArrayList<>();
058        String convertTags = Config.getPref().get(GPX_SETTING, "ask");
059        boolean check = "list".equals(convertTags) || "ask".equals(convertTags);
060        boolean none = "no".equals(convertTags); // no need to convert tags when no dialog will be shown anyways
061
062        for (GpxTrack trk : layer.data.getTracks()) {
063            for (GpxTrackSegment segment : trk.getSegments()) {
064                List<Node> nodes = new ArrayList<>();
065                for (WayPoint p : segment.getWayPoints()) {
066                    Node n = new Node(p.getCoor());
067                    for (Entry<String, Object> entry : p.attr.entrySet()) {
068                        String key = entry.getKey();
069                        Object obj = p.get(key);
070                        if (check && !keys.contains(key) && (obj instanceof String || obj instanceof Date)) {
071                            keys.add(key);
072                        }
073                        if (obj instanceof String) {
074                            String str = (String) obj;
075                            if (!none) {
076                                // only convert when required
077                                n.put(key, str);
078                            }
079                        } else if (obj instanceof Date && GpxConstants.PT_TIME.equals(key)) {
080                            // timestamps should always be converted
081                            Date date = (Date) obj;
082                            if (!none) { //... but the tag will only be set when required
083                                n.put(key, DateUtils.fromDate(date));
084                            }
085                            n.setTimestamp(date);
086                        }
087                    }
088                    ds.addPrimitive(n);
089                    nodes.add(n);
090                }
091                Way w = new Way();
092                w.setNodes(nodes);
093                ds.addPrimitive(w);
094            }
095        }
096        //gpx.convert-tags: all, list, *ask, no
097        //gpx.convert-tags.last: *all, list, no
098        //gpx.convert-tags.list.yes
099        //gpx.convert-tags.list.no
100        List<String> listPos = Config.getPref().getList(GPX_SETTING + ".list.yes");
101        List<String> listNeg = Config.getPref().getList(GPX_SETTING + ".list.no");
102        if (check && !keys.isEmpty()) {
103            // Either "list" or "ask" was stored in the settings, so the Nodes have to be filtered after all tags have been processed
104            List<String> allTags = new ArrayList<>(listPos);
105            allTags.addAll(listNeg);
106            if (!allTags.containsAll(keys) || "ask".equals(convertTags)) {
107                // not all keys are in positive/negative list, so we have to ask the user
108                TagConversionDialogResponse res = showTagConversionDialog(keys, listPos, listNeg);
109                if (res.sel == null) {
110                    return null;
111                }
112                listPos = res.listPos;
113
114                if ("no".equals(res.sel)) {
115                    // User just chose not to convert any tags, but that was unknown before the initial conversion
116                    return filterDataSet(ds, null);
117                } else if ("all".equals(res.sel)) {
118                    return ds;
119                }
120            }
121            if (!listPos.containsAll(keys)) {
122                return filterDataSet(ds, listPos);
123            }
124        }
125        return ds;
126    }
127
128    /**
129     * Filters the tags of the given {@link DataSet}
130     * @param ds The {@link DataSet}
131     * @param listPos A {@code List<String>} containing the tags to be kept, can be {@code null} if all tags are to be removed
132     * @return The {@link DataSet}
133     * @since 14103
134     */
135    public DataSet filterDataSet(DataSet ds, List<String> listPos) {
136        Collection<Node> nodes = ds.getNodes();
137        for (Node n : nodes) {
138            for (String key : n.keySet()) {
139                if (listPos == null || !listPos.contains(key)) {
140                    n.put(key, null);
141                }
142            }
143        }
144        return ds;
145    }
146
147    /**
148     * Shows the TagConversionDialog asking the user whether to keep all, some or no tags
149     * @param keys The keys present during the current conversion
150     * @param listPos The keys that were previously selected
151     * @param listNeg The keys that were previously unselected
152     * @return {@link TagConversionDialogResponse} containing the selection
153     */
154    private static TagConversionDialogResponse showTagConversionDialog(List<String> keys, List<String> listPos, List<String> listNeg) {
155        TagConversionDialogResponse res = new TagConversionDialogResponse(listPos, listNeg);
156        String lSel = Config.getPref().get(GPX_SETTING + ".last", "all");
157
158        JPanel p = new JPanel(new GridBagLayout());
159        ButtonGroup r = new ButtonGroup();
160
161        p.add(new JLabel(
162                tr("The GPX layer contains fields that can be converted to OSM tags. How would you like to proceed?")),
163                GBC.eol());
164        JRadioButton rAll = new JRadioButton(tr("Convert all fields"), "all".equals(lSel));
165        r.add(rAll);
166        p.add(rAll, GBC.eol());
167
168        JRadioButton rList = new JRadioButton(tr("Only convert the following fields:"), "list".equals(lSel));
169        r.add(rList);
170        p.add(rList, GBC.eol());
171
172        JPanel q = new JPanel();
173
174        List<JCheckBox> checkList = new ArrayList<>();
175        for (String key : keys) {
176            JCheckBox cTmp = new JCheckBox(key, !listNeg.contains(key));
177            checkList.add(cTmp);
178            q.add(cTmp);
179        }
180
181        q.setBorder(BorderFactory.createEmptyBorder(0, 20, 5, 0));
182        p.add(q, GBC.eol());
183
184        JRadioButton rNone = new JRadioButton(tr("Do not convert any fields"), "no".equals(lSel));
185        r.add(rNone);
186        p.add(rNone, GBC.eol());
187
188        ActionListener enabler = new TagConversionDialogRadioButtonActionListener(checkList, true);
189        ActionListener disabler = new TagConversionDialogRadioButtonActionListener(checkList, false);
190
191        if (!"list".equals(lSel)) {
192            disabler.actionPerformed(null);
193        }
194
195        rAll.addActionListener(disabler);
196        rList.addActionListener(enabler);
197        rNone.addActionListener(disabler);
198
199        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Options"),
200                tr("Convert"), tr("Convert and remember selection"), tr("Cancel"))
201                .setButtonIcons("exportgpx", "exportgpx", "cancel").setContent(p);
202        int ret = ed.showDialog().getValue();
203
204        if (ret == 1 || ret == 2) {
205            for (JCheckBox cItem : checkList) {
206                String key = cItem.getText();
207                if (cItem.isSelected()) {
208                    if (!res.listPos.contains(key)) {
209                        res.listPos.add(key);
210                    }
211                    res.listNeg.remove(key);
212                } else {
213                    if (!res.listNeg.contains(key)) {
214                        res.listNeg.add(key);
215                    }
216                    res.listPos.remove(key);
217                }
218            }
219            if (rAll.isSelected()) {
220                res.sel = "all";
221            } else if (rNone.isSelected()) {
222                res.sel = "no";
223            }
224            Config.getPref().put(GPX_SETTING + ".last", res.sel);
225            if (ret == 2) {
226                Config.getPref().put(GPX_SETTING, res.sel);
227            } else {
228                Config.getPref().put(GPX_SETTING, "ask");
229            }
230            Config.getPref().putList(GPX_SETTING + ".list.yes", res.listPos);
231            Config.getPref().putList(GPX_SETTING + ".list.no", res.listNeg);
232        } else {
233            res.sel = null;
234        }
235        return res;
236    }
237
238    private static class TagConversionDialogResponse {
239
240        final List<String> listPos;
241        final List<String> listNeg;
242        String sel = "list";
243
244        TagConversionDialogResponse(List<String> p, List<String> n) {
245            listPos = new ArrayList<>(p);
246            listNeg = new ArrayList<>(n);
247        }
248    }
249
250    private static class TagConversionDialogRadioButtonActionListener implements ActionListener {
251
252        private final boolean enable;
253        private final List<JCheckBox> checkList;
254
255        TagConversionDialogRadioButtonActionListener(List<JCheckBox> chks, boolean en) {
256            enable = en;
257            checkList = chks;
258        }
259
260        @Override
261        public void actionPerformed(ActionEvent arg0) {
262            for (JCheckBox ch : checkList) {
263                ch.setEnabled(enable);
264            }
265        }
266    }
267}