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}