Ticket #23481: josm_export_relation_to_gpx_r18721_rev03rc.patch
File josm_export_relation_to_gpx_r18721_rev03rc.patch, 20.9 KB (added by , 20 months ago) |
---|
-
src/org/openstreetmap/josm/actions/relation/ExportRelationToGpxAction.java
1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.actions.relation; 3 3 4 import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode. FROM_FIRST_MEMBER;4 import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.LAST_MEMBER_FIRST; 5 5 import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.TO_FILE; 6 6 import static org.openstreetmap.josm.actions.relation.ExportRelationToGpxAction.Mode.TO_LAYER; 7 7 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 8 8 import static org.openstreetmap.josm.tools.I18n.tr; 9 9 10 import java.awt.GridBagLayout; 10 11 import java.awt.event.ActionEvent; 12 import java.awt.event.ActionListener; 11 13 import java.util.ArrayList; 12 14 import java.util.Arrays; 13 15 import java.util.Collection; … … 14 16 import java.util.Collections; 15 17 import java.util.EnumSet; 16 18 import java.util.HashMap; 19 import java.util.HashSet; 17 20 import java.util.Iterator; 18 21 import java.util.LinkedList; 19 22 import java.util.List; … … 21 24 import java.util.Set; 22 25 import java.util.Stack; 23 26 import java.util.concurrent.TimeUnit; 27 import java.util.stream.Collectors; 24 28 29 import javax.swing.BorderFactory; 30 import javax.swing.ButtonGroup; 31 import javax.swing.JCheckBox; 32 import javax.swing.JLabel; 33 import javax.swing.JPanel; 34 import javax.swing.JRadioButton; 35 25 36 import org.openstreetmap.josm.actions.GpxExportAction; 26 37 import org.openstreetmap.josm.actions.IPrimitiveAction; 38 import org.openstreetmap.josm.data.gpx.GpxConstants; 27 39 import org.openstreetmap.josm.data.gpx.GpxData; 28 40 import org.openstreetmap.josm.data.gpx.GpxTrack; 29 41 import org.openstreetmap.josm.data.gpx.WayPoint; … … 31 43 import org.openstreetmap.josm.data.osm.Node; 32 44 import org.openstreetmap.josm.data.osm.Relation; 33 45 import org.openstreetmap.josm.data.osm.RelationMember; 46 import org.openstreetmap.josm.data.validation.tests.RelationChecker; 47 import org.openstreetmap.josm.gui.ExtendedDialog; 34 48 import org.openstreetmap.josm.gui.MainApplication; 35 49 import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType; 36 50 import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionTypeCalculator; … … 37 51 import org.openstreetmap.josm.gui.layer.GpxLayer; 38 52 import org.openstreetmap.josm.gui.layer.Layer; 39 53 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 54 import org.openstreetmap.josm.spi.preferences.Config; 55 import org.openstreetmap.josm.tools.GBC; 40 56 import org.openstreetmap.josm.tools.SubclassFilteredCollection; 41 57 import org.openstreetmap.josm.tools.Utils; 42 58 … … 49 65 public class ExportRelationToGpxAction extends GpxExportAction 50 66 implements IPrimitiveAction { 51 67 68 private static final String SETTING_KEY = "gpx.export-from-relation"; 69 52 70 /** Enumeration of export variants */ 53 71 public enum Mode { 54 /** concatenate members from first to last element */ 55 FROM_FIRST_MEMBER, 56 /** concatenate members from last to first element */ 57 FROM_LAST_MEMBER, 72 /** concatenate members from last to first element, instead of first to last */ 73 LAST_MEMBER_FIRST, 58 74 /** export to GPX layer and add to LayerManager */ 59 75 TO_LAYER, 60 76 /** export to GPX file and open FileChooser */ … … 69 85 70 86 /** Construct a new ExportRelationToGpxAction with default mode */ 71 87 public ExportRelationToGpxAction() { 72 this(EnumSet.of( FROM_FIRST_MEMBER,TO_FILE));88 this(EnumSet.of(TO_FILE)); 73 89 } 74 90 91 /** A flat representation of the input data */ 92 private List<RelationMember> flat; 93 94 /** The relations sourced to build {@code ExportRelationToGpxAction.flat} */ 95 private List<Relation> relsFound; 96 97 /** Discard relation members with unknown roles if the set is not empty. */ 98 private Set<String> okRoles; 99 100 /** Ignore relation members' roles, treat any role as the empty role during export. */ 101 private boolean ignRoles; 102 103 /** Ignore node tags and timestamp, export minimal trackpoints of lat, lon, ele */ 104 private boolean minipt; 105 75 106 /** 76 107 * Constructs a new {@code ExportRelationToGpxAction} 77 108 * … … 86 117 87 118 private static String name(Set<Mode> mode) { 88 119 if (mode.contains(TO_FILE)) { 89 if ( mode.contains(FROM_FIRST_MEMBER)) {120 if (!mode.contains(LAST_MEMBER_FIRST)) { 90 121 return tr("Export GPX file starting from first member"); 91 122 } else { 92 123 return tr("Export GPX file starting from last member"); 93 124 } 94 125 } else { 95 if ( mode.contains(FROM_FIRST_MEMBER)) {126 if (!mode.contains(LAST_MEMBER_FIRST)) { 96 127 return tr("Convert to GPX layer starting from first member"); 97 128 } else { 98 129 return tr("Convert to GPX layer starting from last member"); … … 101 132 } 102 133 103 134 private static String tooltip(Set<Mode> mode) { 104 if ( mode.contains(FROM_FIRST_MEMBER)) {135 if (!mode.contains(LAST_MEMBER_FIRST)) { 105 136 return tr("Flatten this relation to a single gpx track recursively, " + 106 137 "starting with the first member, successively continuing to the last."); 107 138 } else { … … 110 141 } 111 142 } 112 143 113 @Override 114 protected Layer getLayer() { 115 List<RelationMember> flat = new ArrayList<>(); 144 protected void prepareData() { 145 ignRoles = false; 146 okRoles = new HashSet<>(); 147 flat = new ArrayList<>(); 148 relsFound = new ArrayList<>(); 116 149 117 150 List<RelationMember> init = new ArrayList<>(); 118 151 relations.forEach(t -> init.add(new RelationMember("", t))); … … 119 152 120 153 Stack<Iterator<RelationMember>> stack = new Stack<>(); 121 154 stack.push(modeAwareIterator(init)); 122 123 List<Relation> relsFound = new ArrayList<>();124 155 do { 125 156 Iterator<RelationMember> i = stack.peek(); 126 157 if (!i.hasNext()) … … 128 159 while (i.hasNext()) { 129 160 RelationMember m = i.next(); 130 161 if (m.isRelation() && !m.getRelation().isIncomplete()) { 131 finalList<RelationMember> members = m.getRelation().getMembers();162 List<RelationMember> members = m.getRelation().getMembers(); 132 163 stack.push(modeAwareIterator(members)); 133 164 relsFound.add(m.getRelation()); 134 165 break; … … 138 169 } 139 170 } 140 171 } while (!stack.isEmpty()); 172 } 141 173 174 @Override 175 protected Layer getLayer() { 142 176 GpxData gpxData = new GpxData(); 143 177 final String layerName; 144 178 long time = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) - 24*3600; 145 179 180 if (!okRoles.isEmpty()) { 181 flat.removeIf(rm -> !okRoles.contains(rm.getRole())); 182 } else if (ignRoles) { 183 for (int i = 0; i < flat.size(); i++) { 184 RelationMember rm = flat.get(i); 185 if (!rm.getRole().isEmpty()) 186 flat.set(i, new RelationMember("", rm.getMember())); 187 } 188 } 189 146 190 if (!flat.isEmpty()) { 147 191 Map<String, Object> trkAttr = new HashMap<>(); 148 192 Collection<Collection<WayPoint>> trk = new ArrayList<>(); … … 167 211 .findFirst() 168 212 .ifPresent(r -> { 169 213 trkAttr.put("name", r.getName() != null ? r.getName() : Long.toString(r.getId())); 170 trkAttr.put("desc", tr("based on osm route relation data, timestamps are synthetic")); 214 trkAttr.put("desc", tr("based on osm route relation data") 215 + (!minipt ? ", timestamps are synthetic" : "")); 171 216 }); 172 217 GpxData.ensureUniqueName(trkAttr, names, (String) trkAttr.get("name")); 173 218 } … … 175 220 if (wayConnectionType.direction == WayConnectionType.Direction.BACKWARD) 176 221 Collections.reverse(ln); 177 222 for (Node n: ln) { 178 trkseg.add(OsmDataLayer.nodeToWayPoint(n, TimeUnit.SECONDS.toMillis(time))); 179 time += 1; 223 WayPoint wpt; 224 if (minipt) { 225 wpt = new WayPoint(n.getCoor()); 226 wpt.put(GpxConstants.PT_ELE, n.hasKey(GpxConstants.GPX_PREFIX + GpxConstants.PT_ELE) 227 ? n.get(GpxConstants.GPX_PREFIX + GpxConstants.PT_ELE) 228 : n.get("ele")); 229 } else { 230 wpt = OsmDataLayer.nodeToWayPoint(n, TimeUnit.SECONDS.toMillis(time)); 231 time += 1; 232 } 233 trkseg.add(wpt); 180 234 } 235 } else if (i+1 < flat.size()) { 236 WayConnectionType nxt = wct.get(i+1); 237 nxt.linkPrev &= wayConnectionType.linkPrev; 181 238 } 182 239 } 183 240 gpxData.addTrack(new GpxTrack(trk, trkAttr)); … … 194 251 } 195 252 196 253 private <T> Iterator<T> modeAwareIterator(List<T> list) { 197 return mode.contains( FROM_FIRST_MEMBER)198 ? list.iterator()199 : new LinkedList<>(list).descendingIterator();254 return mode.contains(LAST_MEMBER_FIRST) 255 ? new LinkedList<>(list).descendingIterator() 256 : list.iterator(); 200 257 } 201 258 259 private static Set<String> getRolesInPresets(List<Relation> lr) { 260 return lr.stream().flatMap(n -> RelationChecker.getPresetDefinedRolesFor(n).stream()).collect(Collectors.toSet()); 261 } 262 202 263 /** 264 * Shows a dialog asking the user if relation members of 265 * <li>any role</li> 266 * <li>a user selection of roles</li> 267 * <li>roles known/defined in relation presets</li> 268 * should be considered when exporting. 203 269 * 270 * The second case allows exclusion of preset-defined roles, 271 * although this may produce unexpected results. It is an 272 * expert feature. 273 * 274 * @return true if the dialog was confirmed, false if cancelled 275 */ 276 private boolean askRelationMemberRolesToExport() { 277 minipt = Config.getPref().getBoolean(SETTING_KEY + ".minipt", false); 278 279 // "do not ask again" handled here: 280 switch (Config.getPref().get(SETTING_KEY, "ask")) { 281 case "all": 282 okRoles.clear(); 283 return true; 284 case "list": 285 okRoles.addAll(Config.getPref().getList(SETTING_KEY + ".list")); 286 return true; 287 case "presets": 288 okRoles.addAll(getRolesInPresets(relsFound)); 289 return true; 290 case "treatempty": 291 ignRoles = true; 292 return true; 293 } 294 295 String lSel = Config.getPref().get(SETTING_KEY + ".last", "all"); 296 List<String> userRoles = Config.getPref().getList(SETTING_KEY + ".list"); 297 Set<String> rolesInData = flat.stream().map(rm -> rm.getRole()).collect(Collectors.toSet()); 298 Set<String> rolesInPresets = getRolesInPresets(relsFound); 299 300 // skip asking if all RelationMembers are assigned the same role 301 if (rolesInData.size() == 1) { 302 okRoles.clear(); 303 return true; 304 } 305 306 JPanel p = new JPanel(new GridBagLayout()); 307 ButtonGroup r = new ButtonGroup(); 308 309 p.add(new JLabel("<html><body style=\"width:404px;\">" 310 + tr("This converts the relation to GPX format.") + "<br/><br/>" 311 + tr("Relation members may be filtered out based on their role. Which roles should be considered by export?") 312 + "</body></html>"), GBC.eol()); 313 JRadioButton rAll = new JRadioButton(tr("Any role"), "all".equals(lSel)); 314 r.add(rAll); 315 p.add(rAll, GBC.eol()); 316 317 JRadioButton rList = new JRadioButton(tr("Only selected roles:"), "list".equals(lSel)); 318 rList.setToolTipText("excluding forward / backward will impact WayConnectionTypeCalculator and thus may produce unexpected results"); 319 r.add(rList); 320 p.add(rList, GBC.eol()); 321 322 JPanel q = new JPanel(); 323 324 List<JCheckBox> checkList = new ArrayList<>(); 325 ActionListener ensureAtLeastOneSelected = new ActionListener() { 326 @Override 327 public void actionPerformed(ActionEvent e) { 328 if (checkList.stream().noneMatch(cb -> cb.isSelected())) 329 checkList.get(0).setSelected(true); 330 } 331 }; 332 for (String role : rolesInData) { 333 JCheckBox cTmp = new JCheckBox(role, (userRoles.isEmpty() ? rolesInPresets : userRoles).contains(role)); 334 cTmp.addActionListener(ensureAtLeastOneSelected); 335 checkList.add(cTmp); 336 q.add(cTmp); 337 } 338 339 q.setBorder(BorderFactory.createEmptyBorder(0, 20, 5, 0)); 340 p.add(q, GBC.eol()); 341 342 JRadioButton rPre = new JRadioButton(tr("Roles as defined in presets"), "presets".equals(lSel)); 343 r.add(rPre); 344 p.add(rPre, GBC.eol()); 345 346 JRadioButton rIgn = new JRadioButton(tr("Ignore roles / treat all as empty"), "treatempty".equals(lSel)); 347 r.add(rIgn); 348 p.add(rIgn, GBC.eol()); 349 350 rList.addActionListener(new ActionListener() { 351 @Override 352 public void actionPerformed(ActionEvent e) { 353 for (JCheckBox ch : checkList) { 354 ch.setEnabled(true); 355 } 356 } 357 }); 358 ActionListener disabler = new ActionListener() { 359 @Override 360 public void actionPerformed(ActionEvent e) { 361 for (JCheckBox ch : checkList) { 362 ch.setEnabled(false); 363 } 364 } 365 }; 366 rAll.addActionListener(disabler); 367 rPre.addActionListener(disabler); 368 rIgn.addActionListener(disabler); 369 370 if (!"list".equals(lSel)) { 371 disabler.actionPerformed(null); 372 } 373 374 p.add(new JLabel("<html><body style=\"width:404px;\"><br/>" 375 + tr("Some node tags are exported to trackpoint attributes by default. Exclude them?") 376 + "</body></html>"), GBC.eol()); 377 JCheckBox cbMiniPt = new JCheckBox("Write minimal trackpoints", minipt); 378 cbMiniPt.setToolTipText("export lat, lon and possibly ele; discard other tags and timestamps"); 379 p.add(cbMiniPt, GBC.eol()); 380 381 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Options"), 382 tr("Convert"), tr("Convert and remember selection"), tr("Cancel")) 383 .setButtonIcons("exportgpx", "exportgpx", "cancel").setContent(p); 384 int ret = ed.showDialog().getValue(); 385 386 if (ret == 1 || ret == 2) { 387 userRoles = checkList.stream().filter(cb -> cb.isSelected()).map(cb -> cb.getText()).collect(Collectors.toList()); 388 String sel = rAll.isSelected() ? "all" : (rIgn.isSelected() ? "treatempty" : (rPre.isSelected() ? "presets" : "list")); 389 Config.getPref().put(SETTING_KEY + ".last", sel); 390 Config.getPref().put(SETTING_KEY, (ret == 2) ? sel : "ask"); 391 switch (sel) { 392 case "all": 393 okRoles.clear(); 394 break; 395 case "list": 396 okRoles.addAll(userRoles); 397 Config.getPref().putList(SETTING_KEY + ".list", userRoles); 398 break; 399 case "presets": 400 okRoles.addAll(rolesInPresets); 401 break; 402 case "treatempty": 403 ignRoles = true; 404 break; 405 } 406 407 minipt = cbMiniPt.isSelected(); 408 Config.getPref().putBoolean(SETTING_KEY + ".minipt", minipt); 409 } else { 410 return false; 411 } 412 413 return true; 414 } 415 416 /** 417 * 204 418 * @param e the ActionEvent 205 419 */ 206 420 @Override 207 421 public void actionPerformed(ActionEvent e) { 422 prepareData(); 423 if (!askRelationMemberRolesToExport()) 424 return; 208 425 if (mode.contains(TO_LAYER)) 209 426 MainApplication.getLayerManager().addLayer(getLayer()); 210 427 if (mode.contains(TO_FILE)) -
src/org/openstreetmap/josm/data/osm/Relation.java
556 556 return Stream.of(members).map(RelationMember::getRole).filter(role -> !role.isEmpty()).collect(Collectors.toSet()); 557 557 } 558 558 559 public List<? extends OsmPrimitive> findRelationMembersWithUnknownRole(Set<String> knownRoles) { 560 return Stream.of(members) 561 .filter(m -> knownRoles.isEmpty() || knownRoles.stream().noneMatch(kr -> kr.equals(m.getRole()))) 562 .map(RelationMember::getMember).collect(Collectors.toList()); 563 } 564 559 565 @Override 560 566 public List<? extends OsmPrimitive> findRelationMembers(String role) { 561 567 return IRelation.super.findRelationMembers(role).stream() -
src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java
15 15 import java.util.LinkedList; 16 16 import java.util.List; 17 17 import java.util.Map; 18 import java.util.Set; 18 19 import java.util.stream.Collectors; 19 20 20 21 import org.openstreetmap.josm.command.ChangeMembersCommand; … … 480 481 return Collections.unmodifiableList(test.loops.iterator().next()); 481 482 } 482 483 484 /** 485 * Check RelationMember roles to be known in presets, report members with unknown roles. 486 * @param n the relation to check 487 * @return An empty list if all members have known roles, or a list of members with preset-wise unknown roles. 488 */ 489 public static List<? extends OsmPrimitive> checkMembersForRoleUnknownInPresets(Relation n) { 490 return n.findRelationMembersWithUnknownRole(getPresetDefinedRolesFor(n)); 491 } 492 493 /** 494 * Reply a set of preset-defined "known" roles for the type of relation passed. 495 * 496 * The data of the relation instance' members is irrelevant for this call. 497 * Preset data is looked up based on the relation tags, in particular its 498 * type tag. Thus, the returned set may very well contain roles not as- 499 * signed to any of the passed instance' members. 500 * 501 * @param n the relation whose type the preset-defined roles should be determined for 502 * @return a set of known roles for the relation's type. 503 */ 504 public static Set<String> getPresetDefinedRolesFor(Relation n) { 505 initializePresets(); 506 Map<Role, String> allroles = buildAllRoles(n); 507 return allroles.keySet().stream().map(r -> r.key).collect(Collectors.toSet()); 508 } 509 483 510 } -
src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java
129 129 130 130 /** export relation to GPX track action */ 131 131 private final ExportRelationToGpxAction exportRelationFromFirstAction = 132 new ExportRelationToGpxAction(EnumSet.of(Mode. FROM_FIRST_MEMBER, Mode.TO_FILE));132 new ExportRelationToGpxAction(EnumSet.of(Mode.TO_FILE)); 133 133 private final ExportRelationToGpxAction exportRelationFromLastAction = 134 new ExportRelationToGpxAction(EnumSet.of(Mode. FROM_LAST_MEMBER, Mode.TO_FILE));134 new ExportRelationToGpxAction(EnumSet.of(Mode.LAST_MEMBER_FIRST, Mode.TO_FILE)); 135 135 private final ExportRelationToGpxAction exportRelationFromFirstToLayerAction = 136 new ExportRelationToGpxAction(EnumSet.of(Mode. FROM_FIRST_MEMBER, Mode.TO_LAYER));136 new ExportRelationToGpxAction(EnumSet.of(Mode.TO_LAYER)); 137 137 private final ExportRelationToGpxAction exportRelationFromLastToLayerAction = 138 new ExportRelationToGpxAction(EnumSet.of(Mode. FROM_LAST_MEMBER, Mode.TO_LAYER));138 new ExportRelationToGpxAction(EnumSet.of(Mode.LAST_MEMBER_FIRST, Mode.TO_LAYER)); 139 139 140 140 private final transient HighlightHelper highlightHelper = new HighlightHelper(); 141 141 private final boolean highlightEnabled = Config.getPref().getBoolean("draw.target-highlight", true);