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