Ticket #23481: josm_export_relation_to_gpx_r18721_rev02rc.patch
File josm_export_relation_to_gpx_r18721_rev02rc.patch, 18.8 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; 27 38 import org.openstreetmap.josm.data.gpx.GpxData; … … 31 42 import org.openstreetmap.josm.data.osm.Node; 32 43 import org.openstreetmap.josm.data.osm.Relation; 33 44 import org.openstreetmap.josm.data.osm.RelationMember; 45 import org.openstreetmap.josm.data.validation.tests.RelationChecker; 46 import org.openstreetmap.josm.gui.ExtendedDialog; 34 47 import org.openstreetmap.josm.gui.MainApplication; 35 48 import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType; 36 49 import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionTypeCalculator; … … 37 50 import org.openstreetmap.josm.gui.layer.GpxLayer; 38 51 import org.openstreetmap.josm.gui.layer.Layer; 39 52 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 53 import org.openstreetmap.josm.spi.preferences.Config; 54 import org.openstreetmap.josm.tools.GBC; 40 55 import org.openstreetmap.josm.tools.SubclassFilteredCollection; 41 56 import org.openstreetmap.josm.tools.Utils; 42 57 … … 49 64 public class ExportRelationToGpxAction extends GpxExportAction 50 65 implements IPrimitiveAction { 51 66 67 private static final String SETTING_KEY = "gpx.export-from-relation"; 68 52 69 /** Enumeration of export variants */ 53 70 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, 71 /** concatenate members from last to first element, instead of first to last */ 72 LAST_MEMBER_FIRST, 58 73 /** export to GPX layer and add to LayerManager */ 59 74 TO_LAYER, 60 75 /** export to GPX file and open FileChooser */ … … 69 84 70 85 /** Construct a new ExportRelationToGpxAction with default mode */ 71 86 public ExportRelationToGpxAction() { 72 this(EnumSet.of( FROM_FIRST_MEMBER,TO_FILE));87 this(EnumSet.of(TO_FILE)); 73 88 } 74 89 90 /** A flat representation of the input data */ 91 private List<RelationMember> flat; 92 93 /** The relations sourced to build {@code ExportRelationToGpxAction.flat} */ 94 private List<Relation> relsFound; 95 96 /** Discard relation members with unknown roles if the set is not empty. */ 97 private Set<String> okRoles; 98 99 /** Ignore relation members' roles, treat any role as the empty role during export. */ 100 private List<Boolean> ignRoles; 101 75 102 /** 76 103 * Constructs a new {@code ExportRelationToGpxAction} 77 104 * … … 86 113 87 114 private static String name(Set<Mode> mode) { 88 115 if (mode.contains(TO_FILE)) { 89 if ( mode.contains(FROM_FIRST_MEMBER)) {116 if (!mode.contains(LAST_MEMBER_FIRST)) { 90 117 return tr("Export GPX file starting from first member"); 91 118 } else { 92 119 return tr("Export GPX file starting from last member"); 93 120 } 94 121 } else { 95 if ( mode.contains(FROM_FIRST_MEMBER)) {122 if (!mode.contains(LAST_MEMBER_FIRST)) { 96 123 return tr("Convert to GPX layer starting from first member"); 97 124 } else { 98 125 return tr("Convert to GPX layer starting from last member"); … … 101 128 } 102 129 103 130 private static String tooltip(Set<Mode> mode) { 104 if ( mode.contains(FROM_FIRST_MEMBER)) {131 if (!mode.contains(LAST_MEMBER_FIRST)) { 105 132 return tr("Flatten this relation to a single gpx track recursively, " + 106 133 "starting with the first member, successively continuing to the last."); 107 134 } else { … … 110 137 } 111 138 } 112 139 113 @Override 114 protected Layer getLayer() { 115 List<RelationMember> flat = new ArrayList<>(); 140 protected void prepareData() { 141 ignRoles = new ArrayList<>(); 142 okRoles = new HashSet<>(); 143 flat = new ArrayList<>(); 144 relsFound = new ArrayList<>(); 116 145 117 146 List<RelationMember> init = new ArrayList<>(); 118 147 relations.forEach(t -> init.add(new RelationMember("", t))); … … 119 148 120 149 Stack<Iterator<RelationMember>> stack = new Stack<>(); 121 150 stack.push(modeAwareIterator(init)); 122 123 List<Relation> relsFound = new ArrayList<>();124 151 do { 125 152 Iterator<RelationMember> i = stack.peek(); 126 153 if (!i.hasNext()) … … 128 155 while (i.hasNext()) { 129 156 RelationMember m = i.next(); 130 157 if (m.isRelation() && !m.getRelation().isIncomplete()) { 131 finalList<RelationMember> members = m.getRelation().getMembers();158 List<RelationMember> members = m.getRelation().getMembers(); 132 159 stack.push(modeAwareIterator(members)); 133 160 relsFound.add(m.getRelation()); 134 161 break; … … 138 165 } 139 166 } 140 167 } while (!stack.isEmpty()); 168 } 141 169 170 @Override 171 protected Layer getLayer() { 142 172 GpxData gpxData = new GpxData(); 143 173 final String layerName; 144 174 long time = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) - 24*3600; 145 175 176 if (!okRoles.isEmpty()) { 177 flat.removeIf(rm -> !okRoles.contains(rm.getRole())); 178 } else if (!ignRoles.isEmpty()) { 179 for (int i = 0; i < flat.size(); i++) { 180 RelationMember rm = flat.get(i); 181 if (!rm.getRole().isEmpty()) 182 flat.set(i, new RelationMember("", rm.getMember())); 183 } 184 } 185 146 186 if (!flat.isEmpty()) { 147 187 Map<String, Object> trkAttr = new HashMap<>(); 148 188 Collection<Collection<WayPoint>> trk = new ArrayList<>(); … … 178 218 trkseg.add(OsmDataLayer.nodeToWayPoint(n, TimeUnit.SECONDS.toMillis(time))); 179 219 time += 1; 180 220 } 221 } else if (i+1 < flat.size()) { 222 WayConnectionType nxt = wct.get(i+1); 223 nxt.linkPrev &= wayConnectionType.linkPrev; 181 224 } 182 225 } 183 226 gpxData.addTrack(new GpxTrack(trk, trkAttr)); … … 194 237 } 195 238 196 239 private <T> Iterator<T> modeAwareIterator(List<T> list) { 197 return mode.contains( FROM_FIRST_MEMBER)198 ? list.iterator()199 : new LinkedList<>(list).descendingIterator();240 return mode.contains(LAST_MEMBER_FIRST) 241 ? new LinkedList<>(list).descendingIterator() 242 : list.iterator(); 200 243 } 201 244 245 private static Set<String> getRolesInPresets(List<Relation> lr) { 246 return lr.stream().flatMap(n -> RelationChecker.getPresetDefinedRolesFor(n).stream()).collect(Collectors.toSet()); 247 } 248 202 249 /** 250 * Shows a dialog asking the user if relation members of 251 * <li>any role</li> 252 * <li>a user selection of roles</li> 253 * <li>roles known/defined in relation presets<li> 254 * should be considered when exporting. 203 255 * 256 * The second case allows exclusion of preset-defined roles, 257 * although this may produce unexpected results. It is an 258 * expert feature. 259 * 260 * @param lrm a flat list of RelationMembers about to be exported 261 * @param lr a list of relations lrm was sourced from 262 * @param oR okRoles, possibly modified by the method 263 * 264 * @return true if the dialog was confirmed, false if cancelled 265 */ 266 private static boolean askRelationMemberRolesToExport(List<RelationMember> lrm, List<Relation> lr, Set<String> oR, List<Boolean> iR) { 267 // "do not ask again" handled here: 268 switch (Config.getPref().get(SETTING_KEY, "ask")) { 269 case "all": 270 oR.clear(); 271 return true; 272 case "list": 273 oR.addAll(Config.getPref().getList(SETTING_KEY + ".list")); 274 return true; 275 case "presets": 276 oR.addAll(getRolesInPresets(lr)); 277 return true; 278 case "treatempty": 279 iR.add(true); 280 return true; 281 } 282 283 String lSel = Config.getPref().get(SETTING_KEY + ".last", "all"); 284 List<String> userRoles = Config.getPref().getList(SETTING_KEY + ".list"); 285 Set<String> rolesInData = lrm.stream().map(rm -> rm.getRole()).collect(Collectors.toSet()); 286 Set<String> rolesInPresets = getRolesInPresets(lr); 287 288 // skip asking if all RelationMembers are assigned the same role 289 if (rolesInData.size() == 1) { 290 oR.clear(); 291 return true; 292 } 293 294 JPanel p = new JPanel(new GridBagLayout()); 295 ButtonGroup r = new ButtonGroup(); 296 297 p.add(new JLabel("<html><body style=\"width:404px;\">" 298 + tr("This converts the relation to GPX format.") + "<br><br>" 299 + tr("Relation members may be filtered out based on their role. Which roles should be considered by export?") 300 + "</body></html>"), GBC.eol()); 301 JRadioButton rAll = new JRadioButton(tr("Any role"), "all".equals(lSel)); 302 r.add(rAll); 303 p.add(rAll, GBC.eol()); 304 305 JRadioButton rList = new JRadioButton(tr("Only selected roles:"), "list".equals(lSel)); 306 rList.setToolTipText("excluding forward / backward will impact WayConnectionTypeCalculator and thus may produce unexpected results"); 307 r.add(rList); 308 p.add(rList, GBC.eol()); 309 310 JPanel q = new JPanel(); 311 312 List<JCheckBox> checkList = new ArrayList<>(); 313 ActionListener ensureAtLeastOneSelected = new ActionListener() { 314 @Override 315 public void actionPerformed(ActionEvent e) { 316 if (checkList.stream().noneMatch(cb -> cb.isSelected())) 317 checkList.get(0).setSelected(true); 318 } 319 }; 320 for (String role : rolesInData) { 321 JCheckBox cTmp = new JCheckBox(role, (userRoles.isEmpty() ? rolesInPresets : userRoles).contains(role)); 322 cTmp.addActionListener(ensureAtLeastOneSelected); 323 checkList.add(cTmp); 324 q.add(cTmp); 325 } 326 327 q.setBorder(BorderFactory.createEmptyBorder(0, 20, 5, 0)); 328 p.add(q, GBC.eol()); 329 330 JRadioButton rPre = new JRadioButton(tr("Roles as defined in presets"), "presets".equals(lSel)); 331 r.add(rPre); 332 p.add(rPre, GBC.eol()); 333 334 JRadioButton rIgn = new JRadioButton(tr("Ignore roles / treat all as empty"), "treatempty".equals(lSel)); 335 r.add(rIgn); 336 p.add(rIgn, GBC.eol()); 337 338 rList.addActionListener(new ActionListener() { 339 @Override 340 public void actionPerformed(ActionEvent e) { 341 for (JCheckBox ch : checkList) { 342 ch.setEnabled(true); 343 } 344 } 345 }); 346 ActionListener disabler = new ActionListener() { 347 @Override 348 public void actionPerformed(ActionEvent e) { 349 for (JCheckBox ch : checkList) { 350 ch.setEnabled(false); 351 } 352 } 353 }; 354 rAll.addActionListener(disabler); 355 rPre.addActionListener(disabler); 356 rIgn.addActionListener(disabler); 357 358 if (!"list".equals(lSel)) { 359 disabler.actionPerformed(null); 360 } 361 362 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Options"), 363 tr("Convert"), tr("Convert and remember selection"), tr("Cancel")) 364 .setButtonIcons("exportgpx", "exportgpx", "cancel").setContent(p); 365 int ret = ed.showDialog().getValue(); 366 367 if (ret == 1 || ret == 2) { 368 userRoles = checkList.stream().filter(cb -> cb.isSelected()).map(cb -> cb.getText()).collect(Collectors.toList()); 369 String sel = rAll.isSelected() ? "all" : (rIgn.isSelected() ? "treatempty" : (rPre.isSelected() ? "presets" : "list")); 370 Config.getPref().put(SETTING_KEY + ".last", sel); 371 Config.getPref().put(SETTING_KEY, (ret == 2) ? sel : "ask"); 372 switch (sel) { 373 case "all": 374 oR.clear(); 375 break; 376 case "list": 377 oR.addAll(userRoles); 378 Config.getPref().putList(SETTING_KEY + ".list", userRoles); 379 break; 380 case "presets": 381 oR.addAll(rolesInPresets); 382 break; 383 case "treatempty": 384 iR.add(true); 385 break; 386 } 387 } else { 388 return false; 389 } 390 391 return true; 392 } 393 394 /** 395 * 204 396 * @param e the ActionEvent 205 397 */ 206 398 @Override 207 399 public void actionPerformed(ActionEvent e) { 400 prepareData(); 401 if (!askRelationMemberRolesToExport(flat, relsFound, okRoles, ignRoles)) 402 return; 208 403 if (mode.contains(TO_LAYER)) 209 404 MainApplication.getLayerManager().addLayer(getLayer()); 210 405 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);