| | 270 | * Splits a multipolygon into two separate multipolygons along a way using {@link #splitMultipolygonAtWay} |
| | 271 | * if the resulting multipolygons are valid. |
| | 272 | * Inner polygon rings are automatically assigned to the appropriate multipolygon relation based on their location. |
| | 273 | * Performs a complete check of the resulting multipolygons using {@link MultipolygonTest} and aborts + displays |
| | 274 | * warning messages to the user if errors are encountered. |
| | 275 | * @param mpRelation the multipolygon relation to split. |
| | 276 | * @param splitWay the way along which the multipolygon should be split. |
| | 277 | * Must start and end on the outer ways and must not intersect with or connect to any of the multipolygon inners. |
| | 278 | * @param allowInvalidSplit allow multipolygon splits that result in invalid multipolygons. |
| | 279 | * @return the new multipolygon relations after splitting + the executed commands |
| | 280 | * (already executed and added to the {@link UndoRedoHandler}). |
| | 281 | * Relation and command lists are empty if split did not succeed. |
| | 282 | * |
| | 283 | * @since xxx |
| | 284 | */ |
| | 285 | public static Pair<List<Relation>, List<Command>> splitMultipolygonAtWayChecked(Relation mpRelation, |
| | 286 | Way splitWay, |
| | 287 | boolean allowInvalidSplit) { |
| | 288 | CheckParameterUtil.ensureParameterNotNull(mpRelation, "mpRelation"); |
| | 289 | CheckParameterUtil.ensureParameterNotNull(splitWay, "splitWay"); |
| | 290 | CheckParameterUtil.ensureThat(mpRelation.isMultipolygon(), "mpRelation.isMultipolygon"); |
| | 291 | |
| | 292 | try { |
| | 293 | Pair<List<Relation>, List<Command>> splitResult = splitMultipolygonAtWay(mpRelation, splitWay, allowInvalidSplit); |
| | 294 | List<Relation> mpRelations = splitResult.a; |
| | 295 | List<Command> commands = splitResult.b; |
| | 296 | |
| | 297 | List<TestError> mpErrorsPostSplit = new ArrayList<>(); |
| | 298 | for (Relation mp : mpRelations) { |
| | 299 | MultipolygonTest mpTestPostSplit = new MultipolygonTest(); |
| | 300 | |
| | 301 | mpTestPostSplit.visit(mp); |
| | 302 | |
| | 303 | List<TestError> severeErrors = mpTestPostSplit.getErrors().stream() |
| | 304 | .filter(e -> e.getSeverity().getLevel() <= Severity.ERROR.getLevel()) |
| | 305 | .collect(Collectors.toList()); |
| | 306 | |
| | 307 | mpErrorsPostSplit.addAll(severeErrors); |
| | 308 | } |
| | 309 | |
| | 310 | // Commands were already executed. Either undo them on error or add them to the UndoRedoHandler |
| | 311 | if (mpErrorsPostSplit.size() > 0) { |
| | 312 | if (!allowInvalidSplit) { |
| | 313 | showWarningNotification(tr("Multipolygon split would create invalid multipolygons! Split was not performed.")); |
| | 314 | for (TestError testError : mpErrorsPostSplit) { |
| | 315 | showWarningNotification(testError.getMessage()); |
| | 316 | } |
| | 317 | for (int i = commands.size()-1; i >= 0; --i) { |
| | 318 | commands.get(i).undoCommand(); |
| | 319 | } |
| | 320 | |
| | 321 | return new Pair<List<Relation>, List<Command>>(new ArrayList<>(), new ArrayList<>()); |
| | 322 | } else { |
| | 323 | showWarningNotification(tr("Multipolygon split created invalid multipolygons! Please review and fix these errors.")); |
| | 324 | for (TestError testError : mpErrorsPostSplit) { |
| | 325 | showWarningNotification(testError.getMessage()); |
| | 326 | } |
| | 327 | } |
| | 328 | } |
| | 329 | |
| | 330 | for (Command mpSplitCommand : commands) { |
| | 331 | UndoRedoHandler.getInstance().add(mpSplitCommand, false); |
| | 332 | } |
| | 333 | |
| | 334 | mpRelation.getDataSet().setSelected(mpRelations); |
| | 335 | return splitResult; |
| | 336 | |
| | 337 | } catch (IllegalArgumentException e) { |
| | 338 | // Changes were already undone in splitMultipolygonAtWay |
| | 339 | showWarningNotification(e.getMessage()); |
| | 340 | return new Pair<List<Relation>, List<Command>>(new ArrayList<>(), new ArrayList<>()); |
| | 341 | } |
| | 342 | } |
| | 343 | |
| | 344 | /** |
| | 345 | * Splits a multipolygon into two separate multipolygons along a way. |
| | 346 | * Inner polygon rings are automatically assigned to the appropriate multipolygon relation based on their location. |
| | 347 | * @param mpRelation the multipolygon relation to split. |
| | 348 | * @param splitWay the way along which the multipolygon should be split. |
| | 349 | * Must start and end on the outer ways and must not intersect with or connect to any of the multipolygon inners. |
| | 350 | * @param allowInvalidSplit allow multipolygon splits that result in invalid multipolygons. |
| | 351 | * @return the new multipolygon relations after splitting + the commands required for the split |
| | 352 | * (already executed, but not yet added to the {@link UndoRedoHandler}). |
| | 353 | * @throws IllegalArgumentException if the multipolygon has errors and/or the splitWay is unsuitable for |
| | 354 | * splitting the multipolygon (e.g. because it crosses inners and {@code allowInvalidSplit == false}). |
| | 355 | * |
| | 356 | * @since xxx |
| | 357 | */ |
| | 358 | public static Pair<List<Relation>, List<Command>> splitMultipolygonAtWay(Relation mpRelation, |
| | 359 | Way splitWay, |
| | 360 | boolean allowInvalidSplit) throws IllegalArgumentException { |
| | 361 | CheckParameterUtil.ensureParameterNotNull(mpRelation, "mpRelation"); |
| | 362 | CheckParameterUtil.ensureParameterNotNull(splitWay, "splitWay"); |
| | 363 | CheckParameterUtil.ensureThat(mpRelation.isMultipolygon(), "mpRelation.isMultipolygon"); |
| | 364 | |
| | 365 | List<Command> commands = new ArrayList<>(); |
| | 366 | List<Relation> mpRelations = new ArrayList<>(); |
| | 367 | mpRelations.add(mpRelation); |
| | 368 | |
| | 369 | Multipolygon mp = new Multipolygon(mpRelation); |
| | 370 | |
| | 371 | if (mp.isIncomplete()) { |
| | 372 | throw new IllegalArgumentException(tr("Cannot split incomplete multipolygon")); |
| | 373 | } |
| | 374 | |
| | 375 | /* Splitting multipolygons with multiple outer rings technically works, but assignment of parts is |
| | 376 | * unpredictable and could lead to unwanted fragmentation. */ |
| | 377 | if (mp.getOuterPolygons().size() > 1) { |
| | 378 | throw new IllegalArgumentException(tr("Cannot split multipolygon with multiple outer polygons")); |
| | 379 | } |
| | 380 | |
| | 381 | if (mpRelation.getMembers().stream().filter(RelationMember::isWay).anyMatch(w -> w.getWay() == splitWay)) { |
| | 382 | throw new IllegalArgumentException(tr("Split ways must not be a member of the multipolygon")); |
| | 383 | } |
| | 384 | |
| | 385 | if (mp.getOpenEnds().size() != 0) { |
| | 386 | throw new IllegalArgumentException(tr("Multipolygon has unclosed rings")); |
| | 387 | } |
| | 388 | |
| | 389 | List<Way> outerWaysUnsplit = mp.getOuterWays(); |
| | 390 | |
| | 391 | Node firstNode = splitWay.firstNode(); |
| | 392 | Node lastNode = splitWay.lastNode(); |
| | 393 | |
| | 394 | Set<Way> firstNodeWays = firstNode.getParentWays().stream().filter(outerWaysUnsplit::contains).collect(Collectors.toSet()); |
| | 395 | Set<Way> lastNodeWays = lastNode.getParentWays().stream().filter(outerWaysUnsplit::contains).collect(Collectors.toSet()); |
| | 396 | |
| | 397 | if (firstNodeWays.size() == 0 || lastNodeWays.size() == 0) { |
| | 398 | throw new IllegalArgumentException(tr("The split way does not start/end on the multipolygon outer ways")); |
| | 399 | } |
| | 400 | |
| | 401 | List<SplitWayCommand> splits = splitMultipolygonWaysAtNodes(mpRelation, Arrays.asList(firstNode, lastNode)); |
| | 402 | commands.addAll(splits); |
| | 403 | |
| | 404 | // Need to refresh the multipolygon members after splitting |
| | 405 | mp = new Multipolygon(mpRelation); |
| | 406 | |
| | 407 | List<JoinedPolygon> joinedOuter = null; |
| | 408 | try { |
| | 409 | joinedOuter = MultipolygonBuilder.joinWays(mp.getOuterWays()); |
| | 410 | } catch (JoinedPolygonCreationException e) { |
| | 411 | for (int i = commands.size()-1; i >= 0; --i) { |
| | 412 | commands.get(i).undoCommand(); |
| | 413 | } |
| | 414 | throw new IllegalArgumentException(tr("Error in multipolygon: {0}", e.getMessage()), e); |
| | 415 | } |
| | 416 | |
| | 417 | // Find outer subring that should be moved to the new multipolygon |
| | 418 | for (JoinedPolygon outerRing : joinedOuter) { |
| | 419 | int firstIndex = -1; |
| | 420 | int lastIndex = -1; |
| | 421 | |
| | 422 | if (outerRing.nodes.containsAll(Arrays.asList(firstNode, lastNode))) { |
| | 423 | |
| | 424 | for (int i = 0; i < outerRing.ways.size() && (firstIndex == -1 || lastIndex == -1); i++) { |
| | 425 | Way w = outerRing.ways.get(i); |
| | 426 | Boolean reversed = outerRing.reversed.get(i); |
| | 427 | |
| | 428 | Node cStartNode = reversed ? w.lastNode() : w.firstNode(); |
| | 429 | Node cEndNode = reversed ? w.firstNode() : w.lastNode(); |
| | 430 | |
| | 431 | if (cStartNode == firstNode) { |
| | 432 | firstIndex = i; |
| | 433 | } |
| | 434 | if (cEndNode == lastNode) { |
| | 435 | lastIndex = i; |
| | 436 | } |
| | 437 | } |
| | 438 | } |
| | 439 | |
| | 440 | if (firstIndex != -1 && lastIndex != -1) { |
| | 441 | int startIt = -1; |
| | 442 | int endIt = -1; |
| | 443 | |
| | 444 | if (firstIndex <= lastIndex) { |
| | 445 | startIt = firstIndex; |
| | 446 | endIt = lastIndex + 1; |
| | 447 | } else { |
| | 448 | startIt = lastIndex + 1; |
| | 449 | endIt = firstIndex; |
| | 450 | } |
| | 451 | |
| | 452 | /* Found outer subring for new multipolygon, now create new mp relation and move |
| | 453 | * members + close old and new mp with split way */ |
| | 454 | List<Way> newOuterRingWays = outerRing.ways.subList(startIt, endIt); |
| | 455 | |
| | 456 | RelationMember splitWayMember = new RelationMember("outer", splitWay); |
| | 457 | |
| | 458 | List<RelationMember> mpMembers = mpRelation.getMembers(); |
| | 459 | List<RelationMember> newMpMembers = mpMembers.stream() |
| | 460 | .filter(m -> m.isWay() && newOuterRingWays.contains(m.getWay())) |
| | 461 | .collect(Collectors.toList()); |
| | 462 | |
| | 463 | mpMembers.removeAll(newMpMembers); |
| | 464 | mpMembers.add(splitWayMember); |
| | 465 | |
| | 466 | Relation newMpRelation = new Relation(mpRelation, true, false); |
| | 467 | newMpMembers.add(splitWayMember); |
| | 468 | newMpRelation.setMembers(newMpMembers); |
| | 469 | |
| | 470 | Multipolygon newMp = new Multipolygon(newMpRelation); |
| | 471 | |
| | 472 | // Check if inners need to be moved to new multipolygon |
| | 473 | for (PolyData inner : mp.getInnerPolygons()) { |
| | 474 | for (PolyData newOuter : newMp.getOuterPolygons()) { |
| | 475 | Intersection intersection = newOuter.contains(inner.get()); |
| | 476 | switch (intersection) { |
| | 477 | case INSIDE: |
| | 478 | Collection<Long> innerWayIds = inner.getWayIds(); |
| | 479 | List<RelationMember> innerWayMembers = mpMembers.stream() |
| | 480 | .filter(m -> m.isWay() && innerWayIds.contains(m.getWay().getUniqueId())) |
| | 481 | .collect(Collectors.toList()); |
| | 482 | |
| | 483 | mpMembers.removeAll(innerWayMembers); |
| | 484 | for (RelationMember innerWayMember : innerWayMembers) { |
| | 485 | newMpRelation.addMember(innerWayMember); |
| | 486 | } |
| | 487 | |
| | 488 | break; |
| | 489 | case CROSSING: |
| | 490 | if (!allowInvalidSplit) { |
| | 491 | for (int i = commands.size()-1; i >= 0; --i) { |
| | 492 | commands.get(i).undoCommand(); |
| | 493 | } |
| | 494 | |
| | 495 | throw new IllegalArgumentException(tr("Split way crosses inner polygon")); |
| | 496 | } |
| | 497 | |
| | 498 | break; |
| | 499 | default: |
| | 500 | break; |
| | 501 | } |
| | 502 | } |
| | 503 | } |
| | 504 | |
| | 505 | List<Command> mpCreationCommands = new ArrayList<>(); |
| | 506 | mpCreationCommands.add(new ChangeMembersCommand(mpRelation, mpMembers)); |
| | 507 | mpCreationCommands.add(new AddCommand(mpRelation.getDataSet(), newMpRelation)); |
| | 508 | |
| | 509 | SequenceCommand sequenceCommand = new SequenceCommand(mpRelation.getDataSet(), "Split Multipolygon", mpCreationCommands, false); |
| | 510 | sequenceCommand.executeCommand(); |
| | 511 | commands.add(sequenceCommand); |
| | 512 | |
| | 513 | mpRelations.add(newMpRelation); |
| | 514 | } |
| | 515 | } |
| | 516 | |
| | 517 | return new Pair<List<Relation>, List<Command>>(mpRelations, commands); |
| | 518 | } |
| | 519 | |
| | 520 | /** |
| | 521 | * Splits all ways of the multipolygon at the given nodes |
| | 522 | * @param mpRelation the multipolygon relation whose ways should be split |
| | 523 | * @param splitNodes the nodes at which the multipolygon ways should be split |
| | 524 | * @return a list of (already executed) commands for the split ways |
| | 525 | * |
| | 526 | * @since xxx |
| | 527 | */ |
| | 528 | public static List<SplitWayCommand> splitMultipolygonWaysAtNodes(Relation mpRelation, Collection<Node> splitNodes) { |
| | 529 | CheckParameterUtil.ensureParameterNotNull(mpRelation, "mpRelation"); |
| | 530 | CheckParameterUtil.ensureParameterNotNull(splitNodes, "splitNodes"); |
| | 531 | |
| | 532 | Set<Way> mpWays = mpRelation.getMembers().stream() |
| | 533 | .filter(RelationMember::isWay) |
| | 534 | .collect(Collectors.mapping(RelationMember::getWay, Collectors.toSet())); |
| | 535 | |
| | 536 | List<SplitWayCommand> splitCmds = new ArrayList<>(); |
| | 537 | for (Way way : mpWays) { |
| | 538 | List<Node> containedNodes = way.getNodes().stream() |
| | 539 | .filter(n -> splitNodes.contains(n) && |
| | 540 | (way.isClosed() || (n != way.firstNode() && n != way.lastNode()))) |
| | 541 | .collect(Collectors.toList()); |
| | 542 | |
| | 543 | if (containedNodes.size() > 0) { |
| | 544 | List<List<Node>> wayChunks = SplitWayCommand.buildSplitChunks(way, containedNodes); |
| | 545 | |
| | 546 | if (wayChunks != null) { |
| | 547 | SplitWayCommand result = SplitWayCommand.splitWay( |
| | 548 | way, wayChunks, Collections.<OsmPrimitive>emptyList()); |
| | 549 | result.executeCommand(); // relation members are overwritten/broken if there are multiple unapplied splits |
| | 550 | splitCmds.add(result); |
| | 551 | } |
| | 552 | } |
| | 553 | } |
| | 554 | |
| | 555 | return splitCmds; |
| | 556 | } |
| | 557 | |
| | 558 | /** |