Ticket #18295: split_multipolygon_utilsplugin2.patch

File split_multipolygon_utilsplugin2.patch, 21.0 KB (added by Woazboat, 4 years ago)

Patch for utilsplugin2 split object multipolygon support

  • plugins/utilsplugin2/src/org/openstreetmap/josm/plugins/utilsplugin2/actions/SplitObjectAction.java

     
    88import java.awt.event.ActionEvent;
    99import java.awt.event.KeyEvent;
    1010import java.util.ArrayList;
     11import java.util.Arrays;
    1112import java.util.Collection;
    1213import java.util.Collections;
    1314import java.util.HashMap;
     
    1415import java.util.HashSet;
    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.Command;
     25import org.openstreetmap.josm.command.AddCommand;
     26import org.openstreetmap.josm.command.ChangeMembersCommand;
    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;
    2737import org.openstreetmap.josm.data.osm.Way;
     38import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
     39import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData;
     40import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
     41import org.openstreetmap.josm.data.validation.Severity;
     42import org.openstreetmap.josm.data.validation.TestError;
     43import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
     44import org.openstreetmap.josm.data.osm.Relation;
     45import org.openstreetmap.josm.data.osm.RelationMember;
    2846import org.openstreetmap.josm.gui.Notification;
     47import org.openstreetmap.josm.spi.preferences.Config;
    2948import org.openstreetmap.josm.tools.Shortcut;
     49import org.openstreetmap.josm.tools.CheckParameterUtil;
     50import org.openstreetmap.josm.tools.Pair;
    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).
    3556 * The nodes remain in their original order.
     
    3859 * immediately.
    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.
    4366     */
     
    6487
    6588        List<Node> selectedNodes = new ArrayList<>(ds.getSelectedNodes());
    6689        List<Way> selectedWays = new ArrayList<>(ds.getSelectedWays());
     90        List<Relation> selectedRelations = new ArrayList<>(ds.getSelectedRelations());
    6791
     92        Relation selectedMultipolygon = null;
    6893        Way selectedWay = null;
    6994        Way splitWay = null;
    7095
     
    87112            selectedWay = selectedWays.get(0);      // two nodes and a way is selected, so use this selected way
    88113        }
    89114
     115        if (selectedRelations.size() > 1) {
     116            showWarningNotification(
     117                    tr("Only one multipolygon can be selected for splitting"));
     118            return;
     119        }
     120
     121        if ((selectedRelations.size() == 1) && selectedRelations.get(0).isMultipolygon()) {
     122            selectedMultipolygon = selectedRelations.get(0);
     123        }
     124
     125        if (selectedMultipolygon != null) {
     126            if (splitWay == null) {
     127                showWarningNotification(
     128                        tr("Splitting multipolygons requires a split way to be selected"));
     129                return;
     130            }
     131
     132            boolean allowInvalidMpSplit = Config.getPref().getBoolean(ALLOW_INV_MP_SPLIT_KEY, false);
     133
     134            splitMultipolygonAtWayChecked(selectedMultipolygon, splitWay, allowInvalidMpSplit);
     135            return;
     136        }
     137
    90138        // If only nodes are selected, try to guess which way to split. This works if there
    91139        // is exactly one way that all nodes are part of.
    92140        if (selectedWay == null && !selectedNodes.isEmpty()) {
     
    123171                if (entry.getValue().equals(selectedNodes.size())) {
    124172                    if (selectedWay != null) {
    125173                        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.",
     174                                trn("There is more than one way using the node you selected. Please select the way as well.",
     175                                        "There is more than one way using the nodes you selected. Please select the way as well.",
    128176                                        selectedNodes.size())
    129177                                );
    130178                        return;
     
    219267    }
    220268
    221269    /**
     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    /**
    222559     * Checks if the selection consists of something we can work with.
    223560     * Checks only if the number and type of items selected looks good;
    224561     * does not check whether the selected items are really a valid
     
    230567    private boolean checkSelection(Collection<? extends OsmPrimitive> selection) {
    231568        int node = 0;
    232569        int ways = 0;
     570        int multipolygons = 0;
    233571        for (OsmPrimitive p : selection) {
    234572            if (p instanceof Way) {
    235573                ways++;
    236574            } else if (p instanceof Node) {
    237575                node++;
     576            } else if (p.isMultipolygon()) {
     577                multipolygons++;
    238578            } else
    239579                return false;
    240580        }
    241         return node == 2 || ways == 1 || ways == 2; //only 2 nodes selected. one split-way selected. split-way + way to split.
     581        return (node == 2 || ways == 1 || ways == 2) || //only 2 nodes selected. one split-way selected. split-way + way to split.
     582               (multipolygons == 1 && ways == 1);
    242583    }
    243584
    244585    @Override
     
    255596        setEnabled(checkSelection(selection));
    256597    }
    257598
    258     void showWarningNotification(String msg) {
     599    private static void showWarningNotification(String msg) {
    259600        new Notification(msg)
    260601        .setIcon(JOptionPane.WARNING_MESSAGE).show();
    261602    }