Changeset 35792 in osm for applications
- Timestamp:
- 2021-07-17T17:39:07+02:00 (3 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
applications/editors/josm/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/actions/SplitObjectAction.java
r35579 r35792 9 9 import java.awt.event.KeyEvent; 10 10 import java.util.ArrayList; 11 import java.util.Arrays; 11 12 import java.util.Collection; 12 13 import java.util.Collections; … … 15 16 import java.util.List; 16 17 import java.util.Map; 18 import java.util.Set; 19 import java.util.stream.Collectors; 17 20 18 21 import javax.swing.JOptionPane; 19 22 20 23 import org.openstreetmap.josm.actions.JosmAction; 24 import org.openstreetmap.josm.command.AddCommand; 25 import org.openstreetmap.josm.command.ChangeMembersCommand; 26 import org.openstreetmap.josm.command.Command; 21 27 import org.openstreetmap.josm.command.DeleteCommand; 28 import org.openstreetmap.josm.command.SequenceCommand; 22 29 import org.openstreetmap.josm.command.SplitWayCommand; 23 30 import org.openstreetmap.josm.data.UndoRedoHandler; 24 31 import org.openstreetmap.josm.data.osm.DataSet; 32 import org.openstreetmap.josm.data.osm.MultipolygonBuilder; 33 import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon; 34 import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygonCreationException; 25 35 import org.openstreetmap.josm.data.osm.Node; 26 36 import org.openstreetmap.josm.data.osm.OsmPrimitive; 37 import org.openstreetmap.josm.data.osm.Relation; 38 import org.openstreetmap.josm.data.osm.RelationMember; 27 39 import org.openstreetmap.josm.data.osm.Way; 40 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 41 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData; 42 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection; 43 import org.openstreetmap.josm.data.validation.Severity; 44 import org.openstreetmap.josm.data.validation.TestError; 45 import org.openstreetmap.josm.data.validation.tests.MultipolygonTest; 28 46 import org.openstreetmap.josm.gui.Notification; 47 import org.openstreetmap.josm.spi.preferences.Config; 48 import org.openstreetmap.josm.tools.CheckParameterUtil; 49 import org.openstreetmap.josm.tools.Pair; 29 50 import org.openstreetmap.josm.tools.Shortcut; 30 51 31 52 /** 32 * Splits a closed way (polygon) into two closed ways .53 * Splits a closed way (polygon) into two closed ways or a multipolygon into two separate multipolygons. 33 54 * 34 55 * The closed ways are just split at the selected nodes (which must be exactly two). … … 39 60 */ 40 61 public class SplitObjectAction extends JosmAction { 62 private static final String ALLOW_INV_MP_SPLIT_KEY = "utilsplugin2.split-object.allowInvalidMultipolygonSplit"; 63 41 64 /** 42 65 * Create a new SplitObjectAction. … … 65 88 List<Node> selectedNodes = new ArrayList<>(ds.getSelectedNodes()); 66 89 List<Way> selectedWays = new ArrayList<>(ds.getSelectedWays()); 67 90 List<Relation> selectedRelations = new ArrayList<>(ds.getSelectedRelations()); 91 92 Relation selectedMultipolygon = null; 68 93 Way selectedWay = null; 69 94 Way splitWay = null; … … 88 113 } 89 114 115 if (selectedRelations.size() > 1) { 116 showWarningNotification(tr("Only one multipolygon can be selected for splitting")); 117 return; 118 } 119 120 if ((selectedRelations.size() == 1) && selectedRelations.get(0).isMultipolygon()) { 121 selectedMultipolygon = selectedRelations.get(0); 122 } 123 124 if (selectedMultipolygon != null) { 125 if (splitWay == null) { 126 showWarningNotification(tr("Splitting multipolygons requires a split way to be selected")); 127 return; 128 } 129 130 boolean allowInvalidMpSplit = Config.getPref().getBoolean(ALLOW_INV_MP_SPLIT_KEY, false); 131 132 splitMultipolygonAtWayChecked(selectedMultipolygon, splitWay, allowInvalidMpSplit); 133 return; 134 } 135 90 136 // If only nodes are selected, try to guess which way to split. This works if there 91 137 // is exactly one way that all nodes are part of. … … 124 170 if (selectedWay != null) { 125 171 showWarningNotification( 126 trn("There is more than one way using the node you selected. Please select the way a lso.",127 "There is more than one way using the nodes you selected. Please select the way a lso.",172 trn("There is more than one way using the node you selected. Please select the way as well.", 173 "There is more than one way using the nodes you selected. Please select the way as well.", 128 174 selectedNodes.size()) 129 175 ); … … 217 263 getLayerManager().getEditDataSet().setSelected(result.getNewSelection()); 218 264 } 265 } 266 267 /** 268 * Splits a multipolygon into two separate multipolygons along a way using {@link #splitMultipolygonAtWay} 269 * if the resulting multipolygons are valid. 270 * Inner polygon rings are automatically assigned to the appropriate multipolygon relation based on their location. 271 * Performs a complete check of the resulting multipolygons using {@link MultipolygonTest} and aborts + displays 272 * warning messages to the user if errors are encountered. 273 * @param mpRelation the multipolygon relation to split. 274 * @param splitWay the way along which the multipolygon should be split. 275 * Must start and end on the outer ways and must not intersect with or connect to any of the multipolygon inners. 276 * @param allowInvalidSplit allow multipolygon splits that result in invalid multipolygons. 277 * @return the new multipolygon relations after splitting + the executed commands 278 * (already executed and added to the {@link UndoRedoHandler}). 279 * Relation and command lists are empty if split did not succeed. 280 */ 281 public static Pair<List<Relation>, List<Command>> splitMultipolygonAtWayChecked( 282 Relation mpRelation, Way splitWay, boolean allowInvalidSplit) { 283 284 CheckParameterUtil.ensureParameterNotNull(mpRelation, "mpRelation"); 285 CheckParameterUtil.ensureParameterNotNull(splitWay, "splitWay"); 286 CheckParameterUtil.ensureThat(mpRelation.isMultipolygon(), "mpRelation.isMultipolygon"); 287 288 try { 289 Pair<List<Relation>, List<Command>> splitResult = splitMultipolygonAtWay(mpRelation, splitWay, allowInvalidSplit); 290 List<Relation> mpRelations = splitResult.a; 291 List<Command> commands = splitResult.b; 292 293 List<TestError> mpErrorsPostSplit = new ArrayList<>(); 294 for (Relation mp : mpRelations) { 295 MultipolygonTest mpTestPostSplit = new MultipolygonTest(); 296 297 mpTestPostSplit.visit(mp); 298 299 List<TestError> severeErrors = mpTestPostSplit.getErrors().stream() 300 .filter(e -> e.getSeverity().getLevel() <= Severity.ERROR.getLevel()) 301 .collect(Collectors.toList()); 302 303 mpErrorsPostSplit.addAll(severeErrors); 304 } 305 306 // Commands were already executed. Either undo them on error or add them to the UndoRedoHandler 307 if (!mpErrorsPostSplit.isEmpty()) { 308 if (!allowInvalidSplit) { 309 showWarningNotification(tr("Multipolygon split would create invalid multipolygons! Split was not performed.")); 310 for (TestError testError : mpErrorsPostSplit) { 311 showWarningNotification(testError.getMessage()); 312 } 313 for (int i = commands.size()-1; i >= 0; --i) { 314 commands.get(i).undoCommand(); 315 } 316 317 return new Pair<>(new ArrayList<>(), new ArrayList<>()); 318 } else { 319 showWarningNotification(tr("Multipolygon split created invalid multipolygons! Please review and fix these errors.")); 320 for (TestError testError : mpErrorsPostSplit) { 321 showWarningNotification(testError.getMessage()); 322 } 323 } 324 } 325 326 for (Command mpSplitCommand : commands) { 327 UndoRedoHandler.getInstance().add(mpSplitCommand, false); 328 } 329 330 mpRelation.getDataSet().setSelected(mpRelations); 331 return splitResult; 332 333 } catch (IllegalArgumentException e) { 334 // Changes were already undone in splitMultipolygonAtWay 335 showWarningNotification(e.getMessage()); 336 return new Pair<>(new ArrayList<>(), new ArrayList<>()); 337 } 338 } 339 340 /** 341 * Splits a multipolygon into two separate multipolygons along a way. 342 * Inner polygon rings are automatically assigned to the appropriate multipolygon relation based on their location. 343 * @param mpRelation the multipolygon relation to split. 344 * @param splitWay the way along which the multipolygon should be split. 345 * Must start and end on the outer ways and must not intersect with or connect to any of the multipolygon inners. 346 * @param allowInvalidSplit allow multipolygon splits that result in invalid multipolygons. 347 * @return the new multipolygon relations after splitting + the commands required for the split 348 * (already executed, but not yet added to the {@link UndoRedoHandler}). 349 * @throws IllegalArgumentException if the multipolygon has errors and/or the splitWay is unsuitable for 350 * splitting the multipolygon (e.g. because it crosses inners and {@code allowInvalidSplit == false}). 351 */ 352 public static Pair<List<Relation>, List<Command>> splitMultipolygonAtWay(Relation mpRelation, 353 Way splitWay, 354 boolean allowInvalidSplit) throws IllegalArgumentException { 355 CheckParameterUtil.ensureParameterNotNull(mpRelation, "mpRelation"); 356 CheckParameterUtil.ensureParameterNotNull(splitWay, "splitWay"); 357 CheckParameterUtil.ensureThat(mpRelation.isMultipolygon(), "mpRelation.isMultipolygon"); 358 359 List<Command> commands = new ArrayList<>(); 360 List<Relation> mpRelations = new ArrayList<>(); 361 mpRelations.add(mpRelation); 362 363 Multipolygon mp = new Multipolygon(mpRelation); 364 365 if (mp.isIncomplete()) { 366 throw new IllegalArgumentException(tr("Cannot split incomplete multipolygon")); 367 } 368 369 /* Splitting multipolygons with multiple outer rings technically works, but assignment of parts is 370 * unpredictable and could lead to unwanted fragmentation. */ 371 if (mp.getOuterPolygons().size() > 1) { 372 throw new IllegalArgumentException(tr("Cannot split multipolygon with multiple outer polygons")); 373 } 374 375 if (mpRelation.getMembers().stream().filter(RelationMember::isWay).anyMatch(w -> w.getWay() == splitWay)) { 376 throw new IllegalArgumentException(tr("Split ways must not be a member of the multipolygon")); 377 } 378 379 if (!mp.getOpenEnds().isEmpty()) { 380 throw new IllegalArgumentException(tr("Multipolygon has unclosed rings")); 381 } 382 383 List<Way> outerWaysUnsplit = mp.getOuterWays(); 384 385 Node firstNode = splitWay.firstNode(); 386 Node lastNode = splitWay.lastNode(); 387 388 Set<Way> firstNodeWays = firstNode.getParentWays().stream().filter(outerWaysUnsplit::contains).collect(Collectors.toSet()); 389 Set<Way> lastNodeWays = lastNode.getParentWays().stream().filter(outerWaysUnsplit::contains).collect(Collectors.toSet()); 390 391 if (firstNodeWays.isEmpty() || lastNodeWays.isEmpty()) { 392 throw new IllegalArgumentException(tr("The split way does not start/end on the multipolygon outer ways")); 393 } 394 395 commands.addAll(splitMultipolygonWaysAtNodes(mpRelation, Arrays.asList(firstNode, lastNode))); 396 397 // Need to refresh the multipolygon members after splitting 398 mp = new Multipolygon(mpRelation); 399 400 List<JoinedPolygon> joinedOuter = null; 401 try { 402 joinedOuter = MultipolygonBuilder.joinWays(mp.getOuterWays()); 403 } catch (JoinedPolygonCreationException e) { 404 for (int i = commands.size()-1; i >= 0; --i) { 405 commands.get(i).undoCommand(); 406 } 407 throw new IllegalArgumentException(tr("Error in multipolygon: {0}", e.getMessage()), e); 408 } 409 410 // Find outer subring that should be moved to the new multipolygon 411 for (JoinedPolygon outerRing : joinedOuter) { 412 int firstIndex = -1; 413 int lastIndex = -1; 414 415 if (outerRing.nodes.containsAll(Arrays.asList(firstNode, lastNode))) { 416 for (int i = 0; i < outerRing.ways.size() && (firstIndex == -1 || lastIndex == -1); i++) { 417 Way w = outerRing.ways.get(i); 418 Boolean reversed = outerRing.reversed.get(i); 419 420 Node cStartNode = reversed ? w.lastNode() : w.firstNode(); 421 Node cEndNode = reversed ? w.firstNode() : w.lastNode(); 422 423 if (cStartNode == firstNode) { 424 firstIndex = i; 425 } 426 if (cEndNode == lastNode) { 427 lastIndex = i; 428 } 429 } 430 } 431 432 if (firstIndex != -1 && lastIndex != -1) { 433 int startIt = -1; 434 int endIt = -1; 435 436 if (firstIndex <= lastIndex) { 437 startIt = firstIndex; 438 endIt = lastIndex + 1; 439 } else { 440 startIt = lastIndex + 1; 441 endIt = firstIndex; 442 } 443 444 /* Found outer subring for new multipolygon, now create new mp relation and move 445 * members + close old and new mp with split way */ 446 List<Way> newOuterRingWays = outerRing.ways.subList(startIt, endIt); 447 448 RelationMember splitWayMember = new RelationMember("outer", splitWay); 449 450 List<RelationMember> mpMembers = mpRelation.getMembers(); 451 List<RelationMember> newMpMembers = mpMembers.stream() 452 .filter(m -> m.isWay() && newOuterRingWays.contains(m.getWay())) 453 .collect(Collectors.toList()); 454 455 mpMembers.removeAll(newMpMembers); 456 mpMembers.add(splitWayMember); 457 458 Relation newMpRelation = new Relation(mpRelation, true, false); 459 newMpMembers.add(splitWayMember); 460 newMpRelation.setMembers(newMpMembers); 461 462 Multipolygon newMp = new Multipolygon(newMpRelation); 463 464 // Check if inners need to be moved to new multipolygon 465 for (PolyData inner : mp.getInnerPolygons()) { 466 for (PolyData newOuter : newMp.getOuterPolygons()) { 467 Intersection intersection = newOuter.contains(inner.get()); 468 switch (intersection) { 469 case INSIDE: 470 Collection<Long> innerWayIds = inner.getWayIds(); 471 List<RelationMember> innerWayMembers = mpMembers.stream() 472 .filter(m -> m.isWay() && innerWayIds.contains(m.getWay().getUniqueId())) 473 .collect(Collectors.toList()); 474 475 mpMembers.removeAll(innerWayMembers); 476 for (RelationMember innerWayMember : innerWayMembers) { 477 newMpRelation.addMember(innerWayMember); 478 } 479 480 break; 481 case CROSSING: 482 if (!allowInvalidSplit) { 483 for (int i = commands.size()-1; i >= 0; --i) { 484 commands.get(i).undoCommand(); 485 } 486 487 throw new IllegalArgumentException(tr("Split way crosses inner polygon")); 488 } 489 490 break; 491 default: 492 break; 493 } 494 } 495 } 496 497 List<Command> mpCreationCommands = new ArrayList<>(); 498 mpCreationCommands.add(new ChangeMembersCommand(mpRelation, mpMembers)); 499 mpCreationCommands.add(new AddCommand(mpRelation.getDataSet(), newMpRelation)); 500 501 SequenceCommand sequenceCommand = new SequenceCommand(mpRelation.getDataSet(), "Split Multipolygon", mpCreationCommands, false); 502 sequenceCommand.executeCommand(); 503 commands.add(sequenceCommand); 504 505 mpRelations.add(newMpRelation); 506 } 507 } 508 509 return new Pair<>(mpRelations, commands); 510 } 511 512 /** 513 * Splits all ways of the multipolygon at the given nodes 514 * @param mpRelation the multipolygon relation whose ways should be split 515 * @param splitNodes the nodes at which the multipolygon ways should be split 516 * @return a list of (already executed) commands for the split ways 517 */ 518 public static List<SplitWayCommand> splitMultipolygonWaysAtNodes(Relation mpRelation, Collection<Node> splitNodes) { 519 CheckParameterUtil.ensureParameterNotNull(mpRelation, "mpRelation"); 520 CheckParameterUtil.ensureParameterNotNull(splitNodes, "splitNodes"); 521 522 Set<Way> mpWays = mpRelation.getMembers().stream() 523 .filter(RelationMember::isWay) 524 .collect(Collectors.mapping(RelationMember::getWay, Collectors.toSet())); 525 526 List<SplitWayCommand> splitCmds = new ArrayList<>(); 527 for (Way way : mpWays) { 528 List<Node> containedNodes = way.getNodes().stream() 529 .filter(n -> splitNodes.contains(n) && 530 (way.isClosed() || (n != way.firstNode() && n != way.lastNode()))) 531 .collect(Collectors.toList()); 532 533 if (!containedNodes.isEmpty()) { 534 List<List<Node>> wayChunks = SplitWayCommand.buildSplitChunks(way, containedNodes); 535 536 if (wayChunks != null) { 537 SplitWayCommand result = SplitWayCommand.splitWay( 538 way, wayChunks, Collections.<OsmPrimitive>emptyList()); 539 result.executeCommand(); // relation members are overwritten/broken if there are multiple unapplied splits 540 splitCmds.add(result); 541 } 542 } 543 } 544 545 return splitCmds; 219 546 } 220 547 … … 231 558 int node = 0; 232 559 int ways = 0; 560 int multipolygons = 0; 233 561 for (OsmPrimitive p : selection) { 234 562 if (p instanceof Way) { … … 236 564 } else if (p instanceof Node) { 237 565 node++; 566 } else if (p.isMultipolygon()) { 567 multipolygons++; 238 568 } else 239 569 return false; 240 570 } 241 return node == 2 || ways == 1 || ways == 2; //only 2 nodes selected. one split-way selected. split-way + way to split. 571 return (node == 2 || ways == 1 || ways == 2) || //only 2 nodes selected. one split-way selected. split-way + way to split. 572 (multipolygons == 1 && ways == 1); 242 573 } 243 574 … … 256 587 } 257 588 258 void showWarningNotification(String msg) {589 private static void showWarningNotification(String msg) { 259 590 new Notification(msg) 260 591 .setIcon(JOptionPane.WARNING_MESSAGE).show();
Note:
See TracChangeset
for help on using the changeset viewer.