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.Date;
011import java.util.List;
012import java.util.Map;
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.GpxExtension;
024import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
025import org.openstreetmap.josm.data.gpx.IGpxTrack;
026import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
027import org.openstreetmap.josm.data.gpx.WayPoint;
028import org.openstreetmap.josm.data.osm.DataSet;
029import org.openstreetmap.josm.data.osm.Node;
030import org.openstreetmap.josm.data.osm.OsmPrimitive;
031import org.openstreetmap.josm.data.osm.Way;
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.OsmDataLayer;
036import org.openstreetmap.josm.spi.preferences.Config;
037import org.openstreetmap.josm.tools.GBC;
038import org.openstreetmap.josm.tools.date.DateUtils;
039
040/**
041 * Converts a {@link GpxLayer} to a {@link OsmDataLayer}.
042 * @since 14129 (extracted from {@link ConvertToDataLayerAction})
043 */
044public class ConvertFromGpxLayerAction extends ConvertToDataLayerAction<GpxLayer> {
045
046    private static final String GPX_SETTING = "gpx.convert-tags";
047
048    /**
049     * Creates a new {@code FromGpxLayer}.
050     * @param layer the source layer
051     */
052    public ConvertFromGpxLayerAction(GpxLayer layer) {
053        super(layer);
054    }
055
056    @Override
057    public DataSet convert() {
058        final DataSet ds = new DataSet();
059        ds.setGPXNamespaces(layer.data.getNamespaces());
060
061        List<String> keys = new ArrayList<>(); // note that items in this list don't have the GPX_PREFIX
062        String convertTags = Config.getPref().get(GPX_SETTING, "ask");
063        boolean check = "list".equals(convertTags) || "ask".equals(convertTags);
064        boolean none = "no".equals(convertTags); // no need to convert tags when no dialog will be shown anyways
065
066        for (IGpxTrack trk : layer.data.getTracks()) {
067            for (IGpxTrackSegment segment : trk.getSegments()) {
068                List<Node> nodes = new ArrayList<>();
069                for (WayPoint p : segment.getWayPoints()) {
070                    Node n = new Node(p.getCoor());
071                    addAttributes(p.getAttributes(), n, keys, check, none);
072                    if (!none) {
073                        addExtensions(p.getExtensions(), n, false, keys, check);
074                    }
075                    ds.addPrimitive(n);
076                    nodes.add(n);
077                }
078                Way w = new Way();
079                w.setNodes(nodes);
080                addAttributes(trk.getAttributes(), w, keys, check, none);
081                addAttributes(segment.getAttributes(), w, keys, check, none);
082                if (!none) {
083                    addExtensions(trk.getExtensions(), w, false, keys, check);
084                    addExtensions(segment.getExtensions(), w, true, keys, check);
085                }
086                ds.addPrimitive(w);
087            }
088        }
089        //gpx.convert-tags: all, list, *ask, no
090        //gpx.convert-tags.last: *all, list, no
091        //gpx.convert-tags.list.yes
092        //gpx.convert-tags.list.no
093        List<String> listPos = Config.getPref().getList(GPX_SETTING + ".list.yes");
094        List<String> listNeg = Config.getPref().getList(GPX_SETTING + ".list.no");
095        if (check && !keys.isEmpty()) {
096            // Either "list" or "ask" was stored in the settings, so the Nodes have to be filtered after all tags have been processed
097            List<String> allTags = new ArrayList<>(listPos);
098            allTags.addAll(listNeg);
099            if (!allTags.containsAll(keys) || "ask".equals(convertTags)) {
100                // not all keys are in positive/negative list, so we have to ask the user
101                TagConversionDialogResponse res = showTagConversionDialog(keys, listPos, listNeg);
102                if (res.sel == null) {
103                    return null;
104                }
105                listPos = res.listPos;
106
107                if ("no".equals(res.sel)) {
108                    // User just chose not to convert any tags, but that was unknown before the initial conversion
109                    return filterDataSet(ds, null);
110                } else if ("all".equals(res.sel)) {
111                    return ds;
112                }
113            }
114            if (!listPos.containsAll(keys)) {
115                return filterDataSet(ds, listPos);
116            }
117        }
118        return ds;
119    }
120
121    private static void addAttributes(Map<String, Object> attr, OsmPrimitive p, List<String> keys, boolean check, boolean none) {
122        for (Entry<String, Object> entry : attr.entrySet()) {
123            String key = entry.getKey();
124            Object obj = entry.getValue();
125            if (check && !keys.contains(key) && (obj instanceof String || obj instanceof Number || obj instanceof Date)) {
126                keys.add(key);
127            }
128            if (!none && (obj instanceof String || obj instanceof Number)) {
129                // only convert when required
130                p.put(GpxConstants.GPX_PREFIX + key, obj.toString());
131            } else if (obj instanceof Date && GpxConstants.PT_TIME.equals(key)) {
132                // timestamps should always be converted
133                Date date = (Date) obj;
134                if (!none) { //... but the tag will only be set when required
135                    p.put(GpxConstants.GPX_PREFIX + key, DateUtils.fromDate(date));
136                }
137                p.setTimestamp(date);
138            }
139        }
140    }
141
142    private static void addExtensions(GpxExtensionCollection exts, OsmPrimitive p, boolean seg, List<String> keys, boolean check) {
143        for (GpxExtension ext : exts) {
144            String value = ext.getValue();
145            if (value != null && !value.isEmpty()) {
146                String extpre = "extension:";
147                String pre = ext.getPrefix();
148                if (pre == null || pre.isEmpty()) {
149                    pre = "other";
150                }
151                // needs to be distinguished since both track and segment extensions are applied to the resulting way
152                String segpre = seg ? "segment:" : "";
153                String key = ext.getFlatKey();
154                String fullkey = GpxConstants.GPX_PREFIX + extpre + pre + ":" + segpre + key;
155                if (GpxConstants.EXTENSION_ABBREVIATIONS.containsKey(fullkey)) {
156                    fullkey = GpxConstants.EXTENSION_ABBREVIATIONS.get(fullkey);
157                }
158                if (check && !keys.contains(fullkey)) {
159                    keys.add(fullkey);
160                }
161                p.put(fullkey, value);
162            }
163            addExtensions(ext.getExtensions(), p, seg, keys, check);
164        }
165    }
166
167    /**
168     * Filters the tags of the given {@link DataSet}
169     * @param ds The {@link DataSet}
170     * @param listPos A {@code List<String>} containing the tags (without prefix) to be kept, can be {@code null} if all tags are to be removed
171     * @return The {@link DataSet}
172     * @since 14103
173     */
174    public DataSet filterDataSet(DataSet ds, List<String> listPos) {
175        for (OsmPrimitive p : ds.getPrimitives(p -> p instanceof Node || p instanceof Way)) {
176            for (String key : p.keySet()) {
177                String listkey;
178                if (listPos != null && key.startsWith(GpxConstants.GPX_PREFIX)) {
179                    listkey = key.substring(GpxConstants.GPX_PREFIX.length());
180                } else {
181                    listkey = key;
182                }
183                if (listPos == null || !listPos.contains(listkey)) {
184                   p.put(key, null);
185                }
186            }
187        }
188        return ds;
189    }
190
191    /**
192     * Shows the TagConversionDialog asking the user whether to keep all, some or no tags
193     * @param keys The keys present during the current conversion
194     * @param listPos The keys that were previously selected
195     * @param listNeg The keys that were previously unselected
196     * @return {@link TagConversionDialogResponse} containing the selection
197     */
198    private static TagConversionDialogResponse showTagConversionDialog(List<String> keys, List<String> listPos, List<String> listNeg) {
199        TagConversionDialogResponse res = new TagConversionDialogResponse(listPos, listNeg);
200        String lSel = Config.getPref().get(GPX_SETTING + ".last", "all");
201
202        JPanel p = new JPanel(new GridBagLayout());
203        ButtonGroup r = new ButtonGroup();
204
205        p.add(new JLabel(
206                tr("The GPX layer contains fields that can be converted to OSM tags. How would you like to proceed?")),
207                GBC.eol());
208        JRadioButton rAll = new JRadioButton(tr("Convert all fields"), "all".equals(lSel));
209        r.add(rAll);
210        p.add(rAll, GBC.eol());
211
212        JRadioButton rList = new JRadioButton(tr("Only convert the following fields:"), "list".equals(lSel));
213        r.add(rList);
214        p.add(rList, GBC.eol());
215
216        JPanel q = new JPanel();
217
218        List<JCheckBox> checkList = new ArrayList<>();
219        for (String key : keys) {
220            JCheckBox cTmp = new JCheckBox(key, !listNeg.contains(key));
221            checkList.add(cTmp);
222            q.add(cTmp);
223        }
224
225        q.setBorder(BorderFactory.createEmptyBorder(0, 20, 5, 0));
226        p.add(q, GBC.eol());
227
228        JRadioButton rNone = new JRadioButton(tr("Do not convert any fields"), "no".equals(lSel));
229        r.add(rNone);
230        p.add(rNone, GBC.eol());
231
232        ActionListener enabler = new TagConversionDialogRadioButtonActionListener(checkList, true);
233        ActionListener disabler = new TagConversionDialogRadioButtonActionListener(checkList, false);
234
235        if (!"list".equals(lSel)) {
236            disabler.actionPerformed(null);
237        }
238
239        rAll.addActionListener(disabler);
240        rList.addActionListener(enabler);
241        rNone.addActionListener(disabler);
242
243        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Options"),
244                tr("Convert"), tr("Convert and remember selection"), tr("Cancel"))
245                .setButtonIcons("exportgpx", "exportgpx", "cancel").setContent(p);
246        int ret = ed.showDialog().getValue();
247
248        if (ret == 1 || ret == 2) {
249            for (JCheckBox cItem : checkList) {
250                String key = cItem.getText();
251                if (cItem.isSelected()) {
252                    if (!res.listPos.contains(key)) {
253                        res.listPos.add(key);
254                    }
255                    res.listNeg.remove(key);
256                } else {
257                    if (!res.listNeg.contains(key)) {
258                        res.listNeg.add(key);
259                    }
260                    res.listPos.remove(key);
261                }
262            }
263            if (rAll.isSelected()) {
264                res.sel = "all";
265            } else if (rNone.isSelected()) {
266                res.sel = "no";
267            }
268            Config.getPref().put(GPX_SETTING + ".last", res.sel);
269            if (ret == 2) {
270                Config.getPref().put(GPX_SETTING, res.sel);
271            } else {
272                Config.getPref().put(GPX_SETTING, "ask");
273            }
274            Config.getPref().putList(GPX_SETTING + ".list.yes", res.listPos);
275            Config.getPref().putList(GPX_SETTING + ".list.no", res.listNeg);
276        } else {
277            res.sel = null;
278        }
279        return res;
280    }
281
282    private static class TagConversionDialogResponse {
283
284        final List<String> listPos;
285        final List<String> listNeg;
286        String sel = "list";
287
288        TagConversionDialogResponse(List<String> p, List<String> n) {
289            listPos = new ArrayList<>(p);
290            listNeg = new ArrayList<>(n);
291        }
292    }
293
294    private static class TagConversionDialogRadioButtonActionListener implements ActionListener {
295
296        private final boolean enable;
297        private final List<JCheckBox> checkList;
298
299        TagConversionDialogRadioButtonActionListener(List<JCheckBox> chks, boolean en) {
300            enable = en;
301            checkList = chks;
302        }
303
304        @Override
305        public void actionPerformed(ActionEvent arg0) {
306            for (JCheckBox ch : checkList) {
307                ch.setEnabled(enable);
308            }
309        }
310    }
311}