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 Number || obj instanceof Date)) {
071                            keys.add(key);
072                        }
073                        if (!none && (obj instanceof String || obj instanceof Number)) {
074                            // only convert when required
075                            n.put(key, obj.toString());
076                        } else if (obj instanceof Date && GpxConstants.PT_TIME.equals(key)) {
077                            // timestamps should always be converted
078                            Date date = (Date) obj;
079                            if (!none) { //... but the tag will only be set when required
080                                n.put(key, DateUtils.fromDate(date));
081                            }
082                            n.setTimestamp(date);
083                        }
084                    }
085                    ds.addPrimitive(n);
086                    nodes.add(n);
087                }
088                Way w = new Way();
089                w.setNodes(nodes);
090                ds.addPrimitive(w);
091            }
092        }
093        //gpx.convert-tags: all, list, *ask, no
094        //gpx.convert-tags.last: *all, list, no
095        //gpx.convert-tags.list.yes
096        //gpx.convert-tags.list.no
097        List<String> listPos = Config.getPref().getList(GPX_SETTING + ".list.yes");
098        List<String> listNeg = Config.getPref().getList(GPX_SETTING + ".list.no");
099        if (check && !keys.isEmpty()) {
100            // Either "list" or "ask" was stored in the settings, so the Nodes have to be filtered after all tags have been processed
101            List<String> allTags = new ArrayList<>(listPos);
102            allTags.addAll(listNeg);
103            if (!allTags.containsAll(keys) || "ask".equals(convertTags)) {
104                // not all keys are in positive/negative list, so we have to ask the user
105                TagConversionDialogResponse res = showTagConversionDialog(keys, listPos, listNeg);
106                if (res.sel == null) {
107                    return null;
108                }
109                listPos = res.listPos;
110
111                if ("no".equals(res.sel)) {
112                    // User just chose not to convert any tags, but that was unknown before the initial conversion
113                    return filterDataSet(ds, null);
114                } else if ("all".equals(res.sel)) {
115                    return ds;
116                }
117            }
118            if (!listPos.containsAll(keys)) {
119                return filterDataSet(ds, listPos);
120            }
121        }
122        return ds;
123    }
124
125    /**
126     * Filters the tags of the given {@link DataSet}
127     * @param ds The {@link DataSet}
128     * @param listPos A {@code List<String>} containing the tags to be kept, can be {@code null} if all tags are to be removed
129     * @return The {@link DataSet}
130     * @since 14103
131     */
132    public DataSet filterDataSet(DataSet ds, List<String> listPos) {
133        Collection<Node> nodes = ds.getNodes();
134        for (Node n : nodes) {
135            for (String key : n.keySet()) {
136                if (listPos == null || !listPos.contains(key)) {
137                    n.put(key, null);
138                }
139            }
140        }
141        return ds;
142    }
143
144    /**
145     * Shows the TagConversionDialog asking the user whether to keep all, some or no tags
146     * @param keys The keys present during the current conversion
147     * @param listPos The keys that were previously selected
148     * @param listNeg The keys that were previously unselected
149     * @return {@link TagConversionDialogResponse} containing the selection
150     */
151    private static TagConversionDialogResponse showTagConversionDialog(List<String> keys, List<String> listPos, List<String> listNeg) {
152        TagConversionDialogResponse res = new TagConversionDialogResponse(listPos, listNeg);
153        String lSel = Config.getPref().get(GPX_SETTING + ".last", "all");
154
155        JPanel p = new JPanel(new GridBagLayout());
156        ButtonGroup r = new ButtonGroup();
157
158        p.add(new JLabel(
159                tr("The GPX layer contains fields that can be converted to OSM tags. How would you like to proceed?")),
160                GBC.eol());
161        JRadioButton rAll = new JRadioButton(tr("Convert all fields"), "all".equals(lSel));
162        r.add(rAll);
163        p.add(rAll, GBC.eol());
164
165        JRadioButton rList = new JRadioButton(tr("Only convert the following fields:"), "list".equals(lSel));
166        r.add(rList);
167        p.add(rList, GBC.eol());
168
169        JPanel q = new JPanel();
170
171        List<JCheckBox> checkList = new ArrayList<>();
172        for (String key : keys) {
173            JCheckBox cTmp = new JCheckBox(key, !listNeg.contains(key));
174            checkList.add(cTmp);
175            q.add(cTmp);
176        }
177
178        q.setBorder(BorderFactory.createEmptyBorder(0, 20, 5, 0));
179        p.add(q, GBC.eol());
180
181        JRadioButton rNone = new JRadioButton(tr("Do not convert any fields"), "no".equals(lSel));
182        r.add(rNone);
183        p.add(rNone, GBC.eol());
184
185        ActionListener enabler = new TagConversionDialogRadioButtonActionListener(checkList, true);
186        ActionListener disabler = new TagConversionDialogRadioButtonActionListener(checkList, false);
187
188        if (!"list".equals(lSel)) {
189            disabler.actionPerformed(null);
190        }
191
192        rAll.addActionListener(disabler);
193        rList.addActionListener(enabler);
194        rNone.addActionListener(disabler);
195
196        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Options"),
197                tr("Convert"), tr("Convert and remember selection"), tr("Cancel"))
198                .setButtonIcons("exportgpx", "exportgpx", "cancel").setContent(p);
199        int ret = ed.showDialog().getValue();
200
201        if (ret == 1 || ret == 2) {
202            for (JCheckBox cItem : checkList) {
203                String key = cItem.getText();
204                if (cItem.isSelected()) {
205                    if (!res.listPos.contains(key)) {
206                        res.listPos.add(key);
207                    }
208                    res.listNeg.remove(key);
209                } else {
210                    if (!res.listNeg.contains(key)) {
211                        res.listNeg.add(key);
212                    }
213                    res.listPos.remove(key);
214                }
215            }
216            if (rAll.isSelected()) {
217                res.sel = "all";
218            } else if (rNone.isSelected()) {
219                res.sel = "no";
220            }
221            Config.getPref().put(GPX_SETTING + ".last", res.sel);
222            if (ret == 2) {
223                Config.getPref().put(GPX_SETTING, res.sel);
224            } else {
225                Config.getPref().put(GPX_SETTING, "ask");
226            }
227            Config.getPref().putList(GPX_SETTING + ".list.yes", res.listPos);
228            Config.getPref().putList(GPX_SETTING + ".list.no", res.listNeg);
229        } else {
230            res.sel = null;
231        }
232        return res;
233    }
234
235    private static class TagConversionDialogResponse {
236
237        final List<String> listPos;
238        final List<String> listNeg;
239        String sel = "list";
240
241        TagConversionDialogResponse(List<String> p, List<String> n) {
242            listPos = new ArrayList<>(p);
243            listNeg = new ArrayList<>(n);
244        }
245    }
246
247    private static class TagConversionDialogRadioButtonActionListener implements ActionListener {
248
249        private final boolean enable;
250        private final List<JCheckBox> checkList;
251
252        TagConversionDialogRadioButtonActionListener(List<JCheckBox> chks, boolean en) {
253            enable = en;
254            checkList = chks;
255        }
256
257        @Override
258        public void actionPerformed(ActionEvent arg0) {
259            for (JCheckBox ch : checkList) {
260                ch.setEnabled(enable);
261            }
262        }
263    }
264}