Changeset 35792 in osm for applications


Ignore:
Timestamp:
2021-07-17T17:39:07+02:00 (3 years ago)
Author:
Don-vip
Message:

fix #18295 - add support for splitting multipolygons to the utilsplugin2 split object action (patch by Woazboat)

What this enhancement adds:

  • Multipolygons can be split along a selected way that bisects the multipolygon and connects to the outer ring
  • Inners are automatically assigned to the appropriate multipolygon
  • Also works when the outer ring consists of multiple ways
  • Warns user and doesn't perform split by default if the resulting multipolygons would be invalid (can be allowed via a preference flag, user is still prompted to fix occurring errors)

Limitations:

  • Cannot split multipolygons with multiple outer rings (this is supported by the code on a technical level, but has been disabled intentionally to prevent fragmentation since the assignment of multipolygon parts is essentially unpredictable and could lead to small disconnected slices floating around)
  • Split way must not connect to or intersect inners (this would require conversion of inners to outers which is not implemented)
  • Split way relation member is not added in-order to be contiguous with the rest of the outer ring but simply appended
  • It is undetermined wich multipolygon part retains the old history/id and which one is created as a new relation (this could/should be improved so that e.g. the larger multipolygon retains the history)
File:
1 edited

Legend:

Unmodified
Added
Removed
  • applications/editors/josm/plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/actions/SplitObjectAction.java

    r35579 r35792  
    99import java.awt.event.KeyEvent;
    1010import java.util.ArrayList;
     11import java.util.Arrays;
    1112import java.util.Collection;
    1213import java.util.Collections;
     
    1516import java.util.List;
    1617import java.util.Map;
     18import java.util.Set;
     19import java.util.stream.Collectors;
    1720
    1821import javax.swing.JOptionPane;
    1922
    2023import org.openstreetmap.josm.actions.JosmAction;
     24import org.openstreetmap.josm.command.AddCommand;
     25import org.openstreetmap.josm.command.ChangeMembersCommand;
     26import org.openstreetmap.josm.command.Command;
    2127import org.openstreetmap.josm.command.DeleteCommand;
     28import org.openstreetmap.josm.command.SequenceCommand;
    2229import org.openstreetmap.josm.command.SplitWayCommand;
    2330import org.openstreetmap.josm.data.UndoRedoHandler;
    2431import org.openstreetmap.josm.data.osm.DataSet;
     32import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
     33import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;
     34import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygonCreationException;
    2535import org.openstreetmap.josm.data.osm.Node;
    2636import org.openstreetmap.josm.data.osm.OsmPrimitive;
     37import org.openstreetmap.josm.data.osm.Relation;
     38import org.openstreetmap.josm.data.osm.RelationMember;
    2739import org.openstreetmap.josm.data.osm.Way;
     40import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
     41import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
     42import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
     43import org.openstreetmap.josm.data.validation.Severity;
     44import org.openstreetmap.josm.data.validation.TestError;
     45import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
    2846import org.openstreetmap.josm.gui.Notification;
     47import org.openstreetmap.josm.spi.preferences.Config;
     48import org.openstreetmap.josm.tools.CheckParameterUtil;
     49import org.openstreetmap.josm.tools.Pair;
    2950import org.openstreetmap.josm.tools.Shortcut;
    3051
    3152/**
    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.
    3354 *
    3455 * The closed ways are just split at the selected nodes (which must be exactly two).
     
    3960 */
    4061public class SplitObjectAction extends JosmAction {
     62    private static final String ALLOW_INV_MP_SPLIT_KEY = "utilsplugin2.split-object.allowInvalidMultipolygonSplit";
     63
    4164    /**
    4265     * Create a new SplitObjectAction.
     
    6588        List<Node> selectedNodes = new ArrayList<>(ds.getSelectedNodes());
    6689        List<Way> selectedWays = new ArrayList<>(ds.getSelectedWays());
    67 
     90        List<Relation> selectedRelations = new ArrayList<>(ds.getSelectedRelations());
     91
     92        Relation selectedMultipolygon = null;
    6893        Way selectedWay = null;
    6994        Way splitWay = null;
     
    88113        }
    89114
     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
    90136        // If only nodes are selected, try to guess which way to split. This works if there
    91137        // is exactly one way that all nodes are part of.
     
    124170                    if (selectedWay != null) {
    125171                        showWarningNotification(
    126                                 trn("There is more than one way using the node you selected. Please select the way also.",
    127                                         "There is more than one way using the nodes you selected. Please select the way also.",
     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.",
    128174                                        selectedNodes.size())
    129175                                );
     
    217263            getLayerManager().getEditDataSet().setSelected(result.getNewSelection());
    218264        }
     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;
    219546    }
    220547
     
    231558        int node = 0;
    232559        int ways = 0;
     560        int multipolygons = 0;
    233561        for (OsmPrimitive p : selection) {
    234562            if (p instanceof Way) {
     
    236564            } else if (p instanceof Node) {
    237565                node++;
     566            } else if (p.isMultipolygon()) {
     567                multipolygons++;
    238568            } else
    239569                return false;
    240570        }
    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);
    242573    }
    243574
     
    256587    }
    257588
    258     void showWarningNotification(String msg) {
     589    private static void showWarningNotification(String msg) {
    259590        new Notification(msg)
    260591        .setIcon(JOptionPane.WARNING_MESSAGE).show();
Note: See TracChangeset for help on using the changeset viewer.