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}