001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.command;
003
004import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.GO_AHEAD_WITHOUT_DOWNLOADS;
005import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.GO_AHEAD_WITH_DOWNLOADS;
006import static org.openstreetmap.josm.command.SplitWayCommand.MissingMemberStrategy.USER_ABORTED;
007import static org.openstreetmap.josm.command.SplitWayCommand.WhenRelationOrderUncertain.ASK_USER_FOR_CONSENT_TO_DOWNLOAD;
008import static org.openstreetmap.josm.tools.I18n.tr;
009import static org.openstreetmap.josm.tools.I18n.trn;
010
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.Collection;
014import java.util.Collections;
015import java.util.EnumSet;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.Iterator;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.Map;
022import java.util.Objects;
023import java.util.Optional;
024import java.util.Set;
025import java.util.function.Consumer;
026import java.util.stream.Collectors;
027
028import javax.swing.JOptionPane;
029
030import org.openstreetmap.josm.data.osm.DataSet;
031import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
032import org.openstreetmap.josm.data.osm.Node;
033import org.openstreetmap.josm.data.osm.OsmPrimitive;
034import org.openstreetmap.josm.data.osm.PrimitiveId;
035import org.openstreetmap.josm.data.osm.Relation;
036import org.openstreetmap.josm.data.osm.RelationMember;
037import org.openstreetmap.josm.data.osm.Way;
038import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
039import org.openstreetmap.josm.gui.ExceptionDialogUtil;
040import org.openstreetmap.josm.gui.MainApplication;
041import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
042import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
043import org.openstreetmap.josm.io.MultiFetchServerObjectReader;
044import org.openstreetmap.josm.io.OsmTransferException;
045import org.openstreetmap.josm.spi.preferences.Config;
046import org.openstreetmap.josm.tools.CheckParameterUtil;
047import org.openstreetmap.josm.tools.Logging;
048/**
049 * Splits a way into multiple ways (all identical except for their node list).
050 *
051 * Ways are just split at the selected nodes.  The nodes remain in their
052 * original order.  Selected nodes at the end of a way are ignored.
053 *
054 * @since 12828 ({@code SplitWayAction} converted to a {@link Command})
055 */
056public class SplitWayCommand extends SequenceCommand {
057
058    private static volatile Consumer<String> warningNotifier = Logging::warn;
059    private static final String DOWNLOAD_MISSING_PREF_KEY = "split_way_download_missing_members";
060
061    private static final class RelationInformation {
062        boolean warnme;
063        boolean insert;
064        Relation relation;
065    }
066
067    /**
068     * Sets the global warning notifier.
069     * @param notifier warning notifier in charge of displaying warning message, if any. Must not be null
070     */
071    public static void setWarningNotifier(Consumer<String> notifier) {
072        warningNotifier = Objects.requireNonNull(notifier);
073    }
074
075    private final List<? extends PrimitiveId> newSelection;
076    private final Way originalWay;
077    private final List<Way> newWays;
078
079    /** Map&lt;Restriction type, type to treat it as&gt; */
080    private static final Map<String, String> relationSpecialTypes = new HashMap<>();
081    static {
082        relationSpecialTypes.put("restriction", "restriction");
083        relationSpecialTypes.put("destination_sign", "restriction");
084        relationSpecialTypes.put("connectivity", "restriction");
085    }
086
087    /**
088     * Create a new {@code SplitWayCommand}.
089     * @param name The description text
090     * @param commandList The sequence of commands that should be executed.
091     * @param newSelection The new list of selected primitives ids (which is saved for later retrieval with {@link #getNewSelection})
092     * @param originalWay The original way being split (which is saved for later retrieval with {@link #getOriginalWay})
093     * @param newWays The resulting new ways (which is saved for later retrieval with {@link #getNewWays})
094     */
095    public SplitWayCommand(String name, Collection<Command> commandList,
096            List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
097        super(name, commandList);
098        this.newSelection = newSelection;
099        this.originalWay = originalWay;
100        this.newWays = newWays;
101    }
102
103    /**
104     * Replies the new list of selected primitives ids
105     * @return The new list of selected primitives ids
106     */
107    public List<? extends PrimitiveId> getNewSelection() {
108        return newSelection;
109    }
110
111    /**
112     * Replies the original way being split
113     * @return The original way being split
114     */
115    public Way getOriginalWay() {
116        return originalWay;
117    }
118
119    /**
120     * Replies the resulting new ways
121     * @return The resulting new ways
122     */
123    public List<Way> getNewWays() {
124        return newWays;
125    }
126
127    /**
128     * Determines which way chunk should reuse the old id and its history
129     */
130    @FunctionalInterface
131    public interface Strategy {
132
133        /**
134         * Determines which way chunk should reuse the old id and its history.
135         *
136         * @param wayChunks the way chunks
137         * @return the way to keep
138         */
139        Way determineWayToKeep(Iterable<Way> wayChunks);
140
141        /**
142         * Returns a strategy which selects the way chunk with the highest node count to keep.
143         * @return strategy which selects the way chunk with the highest node count to keep
144         */
145        static Strategy keepLongestChunk() {
146            return wayChunks -> {
147                    Way wayToKeep = null;
148                    for (Way i : wayChunks) {
149                        if (wayToKeep == null || i.getNodesCount() > wayToKeep.getNodesCount()) {
150                            wayToKeep = i;
151                        }
152                    }
153                    return wayToKeep;
154                };
155        }
156
157        /**
158         * Returns a strategy which selects the first way chunk.
159         * @return strategy which selects the first way chunk
160         */
161        static Strategy keepFirstChunk() {
162            return wayChunks -> wayChunks.iterator().next();
163        }
164    }
165
166    /**
167     * Splits the nodes of {@code wayToSplit} into a list of node sequences
168     * which are separated at the nodes in {@code splitPoints}.
169     *
170     * This method displays warning messages if {@code wayToSplit} and/or
171     * {@code splitPoints} aren't consistent.
172     *
173     * Returns null, if building the split chunks fails.
174     *
175     * @param wayToSplit the way to split. Must not be null.
176     * @param splitPoints the nodes where the way is split. Must not be null.
177     * @return the list of chunks
178     */
179    public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) {
180        CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
181        CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
182
183        Set<Node> nodeSet = new HashSet<>(splitPoints);
184        List<List<Node>> wayChunks = new LinkedList<>();
185        List<Node> currentWayChunk = new ArrayList<>();
186        wayChunks.add(currentWayChunk);
187
188        Iterator<Node> it = wayToSplit.getNodes().iterator();
189        while (it.hasNext()) {
190            Node currentNode = it.next();
191            boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
192            currentWayChunk.add(currentNode);
193            if (nodeSet.contains(currentNode) && !atEndOfWay) {
194                currentWayChunk = new ArrayList<>();
195                currentWayChunk.add(currentNode);
196                wayChunks.add(currentWayChunk);
197            }
198        }
199
200        // Handle circular ways specially.
201        // If you split at a circular way at two nodes, you just want to split
202        // it at these points, not also at the former endpoint.
203        // So if the last node is the same first node, join the last and the
204        // first way chunk.
205        List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
206        if (wayChunks.size() >= 2
207                && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
208                && !nodeSet.contains(wayChunks.get(0).get(0))) {
209            if (wayChunks.size() == 2) {
210                warningNotifier.accept(tr("You must select two or more nodes to split a circular way."));
211                return null;
212            }
213            lastWayChunk.remove(lastWayChunk.size() - 1);
214            lastWayChunk.addAll(wayChunks.get(0));
215            wayChunks.remove(wayChunks.size() - 1);
216            wayChunks.set(0, lastWayChunk);
217        }
218
219        if (wayChunks.size() < 2) {
220            if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
221                warningNotifier.accept(
222                        tr("You must select two or more nodes to split a circular way."));
223            } else {
224                warningNotifier.accept(
225                        tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"));
226            }
227            return null;
228        }
229        return wayChunks;
230    }
231
232    /**
233     * Creates new way objects for the way chunks and transfers the keys from the original way.
234     * @param way the original way whose  keys are transferred
235     * @param wayChunks the way chunks
236     * @return the new way objects
237     */
238    public static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) {
239        final List<Way> newWays = new ArrayList<>();
240        for (List<Node> wayChunk : wayChunks) {
241            Way wayToAdd = new Way();
242            wayToAdd.setKeys(way.getKeys());
243            wayToAdd.setNodes(wayChunk);
244            newWays.add(wayToAdd);
245        }
246        return newWays;
247    }
248
249    /**
250     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
251     * the result of this process in an instance of {@link SplitWayCommand}.
252     *
253     * Note that changes are not applied to the data yet. You have to
254     * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}.
255     *
256     * @param way the way to split. Must not be null.
257     * @param wayChunks the list of way chunks into the way is split. Must not be null.
258     * @param selection The list of currently selected primitives
259     * @return the result from the split operation
260     */
261    public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) {
262        return splitWay(way, wayChunks, selection, Strategy.keepLongestChunk());
263    }
264
265    /**
266     * Splits the way {@code way} into chunks of {@code wayChunks} and replies the result of this process in an instance
267     * of {@link SplitWayCommand}. The {@link SplitWayCommand.Strategy} is used to determine which way chunk should
268     * reuse the old id and its history.
269     * <p>
270     * If the split way is part of relations, and the order of the new parts in these relations cannot be determined due
271     * to missing relation members, the user will be asked to consent to downloading these missing members.
272     * <p>
273     * Note that changes are not applied to the data yet. You have to submit the command first, i.e. {@code
274     * UndoRedoHandler.getInstance().add(result)}.
275     *
276     * @param way           the way to split. Must not be null.
277     * @param wayChunks     the list of way chunks into the way is split. Must not be null.
278     * @param selection     The list of currently selected primitives
279     * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
280     * @return the result from the split operation
281     */
282    public static SplitWayCommand splitWay(Way way,
283                                           List<List<Node>> wayChunks,
284                                           Collection<? extends OsmPrimitive> selection,
285                                           Strategy splitStrategy) {
286
287        // This method could be refactored to use an Optional in the future, but would need to be deprecated first
288        // to phase out use by plugins.
289        return splitWay(way, wayChunks, selection, splitStrategy, ASK_USER_FOR_CONSENT_TO_DOWNLOAD).orElse(null);
290    }
291
292    /**
293     * Splits the way {@code way} into chunks of {@code wayChunks} and replies the result of this process in an instance
294     * of {@link SplitWayCommand}. The {@link SplitWayCommand.Strategy} is used to determine which way chunk should
295     * reuse the old id and its history.
296     * <p>
297     * Note that changes are not applied to the data yet. You have to submit the command first, i.e. {@code
298     * UndoRedoHandler.getInstance().add(result)}.
299     *
300     * @param way                        the way to split. Must not be null.
301     * @param wayChunks                  the list of way chunks into the way is split. Must not be null.
302     * @param selection                  The list of currently selected primitives
303     * @param splitStrategy              The strategy used to determine which way chunk should reuse the old id and its
304     *                                   history
305     * @param whenRelationOrderUncertain What to do when the split way is part of relations, and the order of the new
306     *                                   parts in the relation cannot be determined without downloading missing relation
307     *                                   members.
308     * @return The result from the split operation, may be an empty {@link Optional} if the operation is aborted.
309     */
310    public static Optional<SplitWayCommand> splitWay(Way way,
311                                                     List<List<Node>> wayChunks,
312                                                     Collection<? extends OsmPrimitive> selection,
313                                                     Strategy splitStrategy,
314                                                     WhenRelationOrderUncertain whenRelationOrderUncertain) {
315        // build a list of commands, and also a new selection list
316        final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
317        newSelection.addAll(selection);
318
319        // Create all potential new ways
320        final List<Way> newWays = createNewWaysFromChunks(way, wayChunks);
321
322        // Determine which part reuses the existing way
323        final Way wayToKeep = splitStrategy.determineWayToKeep(newWays);
324
325        return wayToKeep != null
326                ? doSplitWay(way, wayToKeep, newWays, newSelection, whenRelationOrderUncertain)
327                : Optional.empty();
328    }
329
330    /**
331     * Effectively constructs the {@link SplitWayCommand}.
332     * This method is only public for {@code SplitWayAction}.
333     *
334     * @param way the way to split. Must not be null.
335     * @param wayToKeep way chunk which should reuse the old id and its history
336     * @param newWays potential new ways
337     * @param newSelection new selection list to update (optional: can be null)
338     * @param whenRelationOrderUncertain Action to perform when the order of the new parts in relations the way is
339     *                                   member of could not be reliably determined. See
340     *                                   {@link WhenRelationOrderUncertain}.
341     * @return the {@code SplitWayCommand}
342     */
343    public static Optional<SplitWayCommand> doSplitWay(Way way,
344                                                       Way wayToKeep,
345                                                       List<Way> newWays,
346                                                       List<OsmPrimitive> newSelection,
347                                                       WhenRelationOrderUncertain whenRelationOrderUncertain) {
348        if (whenRelationOrderUncertain == null) whenRelationOrderUncertain = ASK_USER_FOR_CONSENT_TO_DOWNLOAD;
349
350        final int indexOfWayToKeep = newWays.indexOf(wayToKeep);
351        newWays.remove(wayToKeep);
352
353        // Figure out the order of relation members (if any).
354        Analysis analysis = analyseSplit(way, wayToKeep, newWays);
355
356        // If there are relations that cannot be split properly without downloading more members,
357        // present the user with an option to do so, or to abort the split.
358        Set<Relation> relationsNeedingMoreMembers = new HashSet<>();
359        Set<OsmPrimitive> incompleteMembers = new HashSet<>();
360        for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) {
361            if (!relationAnalysis.getNeededIncompleteMembers().isEmpty()) {
362                incompleteMembers.addAll(relationAnalysis.getNeededIncompleteMembers());
363                relationsNeedingMoreMembers.add(relationAnalysis.getRelation());
364            }
365        }
366
367        MissingMemberStrategy missingMemberStrategy;
368        if (relationsNeedingMoreMembers.isEmpty()) {
369            // The split can be performed without any extra downloads.
370            missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS;
371        } else {
372            switch (whenRelationOrderUncertain) {
373                case ASK_USER_FOR_CONSENT_TO_DOWNLOAD:
374                    // If the analysis shows that for some relations missing members should be downloaded, offer the user the
375                    // chance to consent to this.
376
377                    // Only ask the user about downloading missing members when they haven't consented to this before.
378                    if (ConditionalOptionPaneUtil.getDialogReturnValue(DOWNLOAD_MISSING_PREF_KEY) == Integer.MAX_VALUE) {
379                        // User has previously told us downloading missing relation members is fine.
380                        missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS;
381                    } else {
382                        // Ask the user.
383                        missingMemberStrategy = offerToDownloadMissingMembersIfNeeded(analysis, relationsNeedingMoreMembers.size());
384                    }
385                    break;
386                case SPLIT_ANYWAY:
387                    missingMemberStrategy = GO_AHEAD_WITHOUT_DOWNLOADS;
388                    break;
389                case DOWNLOAD_MISSING_MEMBERS:
390                    missingMemberStrategy = GO_AHEAD_WITH_DOWNLOADS;
391                    break;
392                case ABORT:
393                default:
394                    missingMemberStrategy = USER_ABORTED;
395                    break;
396            }
397        }
398
399        try {
400            switch (missingMemberStrategy) {
401            case GO_AHEAD_WITH_DOWNLOADS:
402                try {
403                    downloadMissingMembers(incompleteMembers);
404                } catch (OsmTransferException e) {
405                    ExceptionDialogUtil.explainException(e);
406                    return Optional.empty();
407                }
408                // If missing relation members were downloaded, perform the analysis again to find the relation
409                // member order for all relations.
410                analysis.cleanup();
411                analysis = analyseSplit(way, wayToKeep, newWays);
412                break;
413            case GO_AHEAD_WITHOUT_DOWNLOADS:
414                // Proceed with the split with the information we have.
415                // This can mean that there are no missing members we want, or that the user chooses to continue
416                // the split without downloading them.
417                break;
418            case USER_ABORTED:
419            default:
420                return Optional.empty();
421            }
422            return Optional.of(splitBasedOnAnalyses(way, newWays, newSelection, analysis, indexOfWayToKeep));
423        } finally {
424            // see #19885
425            wayToKeep.setNodes(null);
426            analysis.cleanup();
427        }
428    }
429
430    static Analysis analyseSplit(Way way,
431                                 Way wayToKeep,
432                                 List<Way> newWays) {
433        Collection<Command> commandList = new ArrayList<>();
434        Collection<String> nowarnroles = Config.getPref().getList("way.split.roles.nowarn",
435                Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
436
437        // Change the original way
438        final List<Node> changedWayNodes = wayToKeep.getNodes();
439        commandList.add(new ChangeNodesCommand(way, changedWayNodes));
440        for (Way wayToAdd : newWays) {
441            commandList.add(new AddCommand(way.getDataSet(), wayToAdd));
442        }
443
444        List<RelationAnalysis> relationAnalyses = new ArrayList<>();
445        EnumSet<WarningType> warnings = EnumSet.noneOf(WarningType.class);
446        int numberOfRelations = 0;
447
448        for (Relation r : OsmPrimitive.getParentRelations(Collections.singleton(way))) {
449            if (!r.isUsable()) {
450                continue;
451            }
452
453            numberOfRelations++;
454            boolean isSimpleCase = true;
455
456            Relation c = null;
457            String type = Optional.ofNullable(r.get("type")).orElse("");
458            // Known types of ordered relations.
459            boolean isOrderedRelation = "route".equals(type) || "multipolygon".equals(type) || "boundary".equals(type);
460
461            for (int ir = 0; ir < r.getMembersCount(); ir++) {
462                RelationMember rm = r.getMember(ir);
463                if (rm.getMember() == way) {
464                    boolean insert = true;
465                    if (relationSpecialTypes.containsKey(type) && "restriction".equals(relationSpecialTypes.get(type))) {
466                        RelationInformation rValue = treatAsRestriction(r, rm, c, newWays, way, changedWayNodes);
467                        if (rValue.warnme) warnings.add(WarningType.GENERIC);
468                        insert = rValue.insert;
469                        c = rValue.relation; // Value.relation is null or contains a modified copy
470                    } else if (!isOrderedRelation) {
471                        // Warn the user when relations that are not a route or multipolygon are modified as a result
472                        // of splitting up the way, because we can't tell if this might break anything.
473                        warnings.add(WarningType.GENERIC);
474                    }
475
476                    if (insert) {
477                        if (c == null) {
478                            c = new Relation(r);
479                        }
480                        if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
481                            warnings.add(WarningType.ROLE);
482                        }
483
484                        // Attempt to determine the direction the ways in the relation are ordered.
485                        Direction direction = Direction.UNKNOWN;
486                        Set<Way> missingWays = new HashSet<>();
487                        if (isOrderedRelation) {
488                            if (way.lastNode() == way.firstNode()) {
489                                // Self-closing way.
490                                direction = Direction.IRRELEVANT;
491                            } else {
492                                // For ordered relations, looking beyond the nearest neighbour members is not required,
493                                // and can even cause the wrong direction to be guessed (with closed loops).
494                                if (ir - 1 >= 0 && r.getMember(ir - 1).isWay()) {
495                                    Way w = r.getMember(ir - 1).getWay();
496                                    if (w.isIncomplete())
497                                        missingWays.add(w);
498                                    else {
499                                        if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
500                                            direction = Direction.FORWARDS;
501                                        } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
502                                            direction = Direction.BACKWARDS;
503                                        }
504                                    }
505                                }
506                                if (ir + 1 < r.getMembersCount() && r.getMember(ir + 1).isWay()) {
507                                    Way w = r.getMember(ir + 1).getWay();
508                                    if (w.isIncomplete())
509                                        missingWays.add(w);
510                                    else {
511                                        if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
512                                            direction = Direction.BACKWARDS;
513                                        } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
514                                            direction = Direction.FORWARDS;
515                                        }
516                                    }
517                                }
518
519                                if (direction == Direction.UNKNOWN && missingWays.isEmpty()) {
520                                    // we cannot detect the direction and no way is missing.
521                                    // We can safely assume that the direction doesn't matter.
522                                    direction = Direction.IRRELEVANT;
523                                }
524                            }
525                        } else {
526                            int k = 1;
527                            while (ir - k >= 0 || ir + k < r.getMembersCount()) {
528                                if (ir - k >= 0 && r.getMember(ir - k).isWay()) {
529                                    Way w = r.getMember(ir - k).getWay();
530                                    if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
531                                        direction = Direction.FORWARDS;
532                                    } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
533                                        direction = Direction.BACKWARDS;
534                                    }
535                                    break;
536                                }
537                                if (ir + k < r.getMembersCount() && r.getMember(ir + k).isWay()) {
538                                    Way w = r.getMember(ir + k).getWay();
539                                    if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
540                                        direction = Direction.BACKWARDS;
541                                    } else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
542                                        direction = Direction.FORWARDS;
543                                    }
544                                    break;
545                                }
546                                k++;
547                            }
548                        }
549
550                        if (direction == Direction.UNKNOWN) {
551                            // We don't have enough information to determine the order of the new ways in this relation.
552                            // This may cause relations to be saved with the two new way sections in reverse order.
553                            //
554                            // This often breaks routes.
555                            //
556                        } else {
557                            missingWays = Collections.emptySet();
558                        }
559                        relationAnalyses.add(new RelationAnalysis(c, rm, direction, missingWays));
560                        isSimpleCase = false;
561                    }
562                }
563            }
564            if (c != null && isSimpleCase) {
565                if (!r.getMembers().equals(c.getMembers())) {
566                    commandList.add(new ChangeMembersCommand(r, new ArrayList<>(c.getMembers())));
567                }
568                c.setMembers(null); // see #19885
569            }
570        }
571        return new Analysis(relationAnalyses, commandList, warnings, numberOfRelations);
572    }
573
574    static class Analysis {
575        List<RelationAnalysis> relationAnalyses;
576        Collection<Command> commands;
577        EnumSet<WarningType> warningTypes;
578        private final int numberOfRelations;
579
580        Analysis(List<RelationAnalysis> relationAnalyses,
581                 Collection<Command> commandList,
582                 EnumSet<WarningType> warnings,
583                 int numberOfRelations) {
584            this.relationAnalyses = relationAnalyses;
585            commands = commandList;
586            warningTypes = warnings;
587            this.numberOfRelations = numberOfRelations;
588        }
589
590        /**
591         * Unlink temporary copies of relations. See #19885
592         */
593        void cleanup() {
594            for (RelationAnalysis ra : relationAnalyses) {
595                if (ra.relation.getDataSet() == null)
596                    ra.relation.setMembers(null);
597            }
598        }
599
600        List<RelationAnalysis> getRelationAnalyses() {
601            return relationAnalyses;
602        }
603
604        Collection<Command> getCommands() {
605            return commands;
606        }
607
608        EnumSet<WarningType> getWarningTypes() {
609            return warningTypes;
610        }
611
612        public int getNumberOfRelations() {
613            return numberOfRelations;
614        }
615    }
616
617    static MissingMemberStrategy offerToDownloadMissingMembersIfNeeded(Analysis analysis,
618                                                                       int numRelationsNeedingMoreMembers) {
619        String[] options = {
620                tr("Yes, download the missing members"),
621                tr("No, abort the split operation"),
622                tr("No, perform the split without downloading")
623        };
624
625        String msgMemberOfRelations = trn(
626                "This way is part of a relation.",
627                "This way is part of {0} relations.",
628                analysis.getNumberOfRelations(),
629                analysis.getNumberOfRelations()
630        );
631
632        String msgReferToRelations;
633        if (analysis.getNumberOfRelations() == 1) {
634            msgReferToRelations = tr("this relation");
635        } else if (analysis.getNumberOfRelations() == numRelationsNeedingMoreMembers) {
636            msgReferToRelations = tr("these relations");
637        } else {
638            msgReferToRelations = trn(
639                    "one relation",
640                    "{0} relations",
641                    numRelationsNeedingMoreMembers,
642                    numRelationsNeedingMoreMembers
643            );
644        }
645
646        String msgRelationsMissingData = tr(
647                "For {0} the correct order of the new way parts could not be determined. " +
648                        "To fix this, some missing relation members should be downloaded first.",
649                msgReferToRelations
650        );
651
652        JMultilineLabel msg = new JMultilineLabel(msgMemberOfRelations + " " + msgRelationsMissingData);
653        msg.setMaxWidth(600);
654
655        int ret = JOptionPane.showOptionDialog(
656                MainApplication.getMainFrame(),
657                msg,
658                tr("Download missing relation members?"),
659                JOptionPane.OK_CANCEL_OPTION,
660                JOptionPane.QUESTION_MESSAGE,
661                null,
662                options,
663                options[0]
664        );
665
666        switch (ret) {
667            case JOptionPane.OK_OPTION:
668                // Ask the user if they want to do this automatically from now on. We only ask this for the download
669                // action, because automatically cancelling is confusing (the user can't tell why this happened), and
670                // automatically performing the split without downloading missing members despite needing them is
671                // likely to break a lot of routes. The user also can't tell the difference between a split that needs
672                // no downloads at all, and this special case where downloading missing relation members will prevent
673                // broken relations.
674                ConditionalOptionPaneUtil.showMessageDialog(
675                        DOWNLOAD_MISSING_PREF_KEY,
676                        MainApplication.getMainFrame(),
677                        tr("Missing relation members will be downloaded. Should this be done automatically from now on?"),
678                        tr("Downloading missing relation members"),
679                        JOptionPane.INFORMATION_MESSAGE
680                );
681                return GO_AHEAD_WITH_DOWNLOADS;
682            case JOptionPane.CANCEL_OPTION:
683                return GO_AHEAD_WITHOUT_DOWNLOADS;
684            default:
685                return USER_ABORTED;
686        }
687    }
688
689    static void downloadMissingMembers(Set<OsmPrimitive> incompleteMembers) throws OsmTransferException {
690        // Download the missing members.
691        MultiFetchServerObjectReader reader = MultiFetchServerObjectReader.create();
692        reader.append(incompleteMembers);
693
694        DataSet ds = reader.parseOsm(NullProgressMonitor.INSTANCE);
695        MainApplication.getLayerManager().getEditLayer().mergeFrom(ds);
696    }
697
698    static SplitWayCommand splitBasedOnAnalyses(Way way,
699                                                List<Way> newWays,
700                                                List<OsmPrimitive> newSelection,
701                                                Analysis analysis,
702                                                int indexOfWayToKeep) {
703        if (newSelection != null && !newSelection.contains(way)) {
704            newSelection.add(way);
705        }
706
707        if (newSelection != null) {
708            newSelection.addAll(newWays);
709        }
710
711        // Perform the split.
712        for (RelationAnalysis relationAnalysis : analysis.getRelationAnalyses()) {
713            RelationMember rm = relationAnalysis.getRelationMember();
714            Relation relation = relationAnalysis.getRelation();
715            Direction direction = relationAnalysis.getDirection();
716
717            int position = -1;
718            for (int i = 0; i < relation.getMembersCount(); i++) {
719                // search for identical member (can't use indexOf() as it uses equals()
720                if (rm == relation.getMember(i)) {
721                    position = i;
722                    break;
723                }
724            }
725
726            // sanity check
727            if (position < 0) {
728                throw new AssertionError("Relation member not found");
729            }
730
731            int j = position;
732            final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep);
733            for (Way wayToAdd : waysToAddBefore) {
734                RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
735                j++;
736                if (direction == Direction.BACKWARDS) {
737                    relation.addMember(position + 1, em);
738                } else {
739                    relation.addMember(j - 1, em);
740                }
741            }
742            final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size());
743            for (Way wayToAdd : waysToAddAfter) {
744                RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
745                j++;
746                if (direction == Direction.BACKWARDS) {
747                    relation.addMember(position, em);
748                } else {
749                    relation.addMember(j, em);
750                }
751            }
752        }
753
754        // add one command for each complex case with relations
755        final DataSet ds = way.getDataSet();
756        for (Relation r : analysis.getRelationAnalyses().stream().map(RelationAnalysis::getRelation).collect(Collectors.toSet())) {
757            Relation orig = (Relation) ds.getPrimitiveById(r);
758            analysis.getCommands().add(new ChangeMembersCommand(orig, new ArrayList<>(r.getMembers())));
759            r.setMembers(null); // see #19885
760        }
761
762        EnumSet<WarningType> warnings = analysis.getWarningTypes();
763
764        if (warnings.contains(WarningType.ROLE)) {
765            warningNotifier.accept(
766                    tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
767        } else if (warnings.contains(WarningType.GENERIC)) {
768            warningNotifier.accept(
769                    tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
770        }
771
772        return new SplitWayCommand(
773                    /* for correct i18n of plural forms - see #9110 */
774                    trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1,
775                            way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1),
776                    analysis.getCommands(),
777                    newSelection,
778                    way,
779                    newWays
780            );
781    }
782
783    private static RelationInformation treatAsRestriction(Relation r,
784            RelationMember rm, Relation c, Collection<Way> newWays, Way way,
785            List<Node> changedWayNodes) {
786        RelationInformation relationInformation = new RelationInformation();
787        /* this code assumes the restriction is correct. No real error checking done */
788        String role = rm.getRole();
789        String type = Optional.ofNullable(r.get("type")).orElse("");
790        if ("from".equals(role) || "to".equals(role)) {
791            List<Node> nodes = new ArrayList<>();
792            for (OsmPrimitive via : findVias(r, type)) {
793                if (via instanceof Node) {
794                    nodes.add((Node) via);
795                } else if (via instanceof Way) {
796                    nodes.add(((Way) via).lastNode());
797                    nodes.add(((Way) via).firstNode());
798                }
799            }
800            Way res = null;
801            for (Node n : nodes) {
802                if (changedWayNodes.get(0) == n || changedWayNodes.get(changedWayNodes.size() - 1) == n) {
803                    res = way;
804                }
805            }
806            if (res == null) {
807                for (Way wayToAdd : newWays) {
808                    for (Node n : nodes) {
809                        if (wayToAdd.isFirstLastNode(n)) {
810                            res = wayToAdd;
811                        }
812                    }
813                }
814                if (res != null) {
815                    if (c == null) {
816                        c = new Relation(r);
817                    }
818                    c.addMember(new RelationMember(role, res));
819                    c.removeMembersFor(way);
820                }
821            }
822        } else if (!"via".equals(role)) {
823            relationInformation.warnme = true;
824        } else {
825            relationInformation.insert = true;
826        }
827        relationInformation.relation = c;
828        return relationInformation;
829    }
830
831    static List<? extends OsmPrimitive> findVias(Relation r, String type) {
832        if (type != null) {
833            switch (type) {
834            case "connectivity":
835            case "restriction":
836                return r.findRelationMembers("via");
837            case "destination_sign":
838                // Prefer intersection over sign, see #12347
839                List<? extends OsmPrimitive> intersections = r.findRelationMembers("intersection");
840                return intersections.isEmpty() ? r.findRelationMembers("sign") : intersections;
841            default:
842                break;
843            }
844        }
845        return Collections.emptyList();
846    }
847
848    /**
849     * Splits the way {@code way} at the nodes in {@code atNodes} and replies
850     * the result of this process in an instance of {@link SplitWayCommand}.
851     *
852     * Note that changes are not applied to the data yet. You have to
853     * submit the command first, i.e. {@code UndoRedoHandler.getInstance().add(result)}.
854     *
855     * Replies null if the way couldn't be split at the given nodes.
856     *
857     * @param way the way to split. Must not be null.
858     * @param atNodes the list of nodes where the way is split. Must not be null.
859     * @param selection The list of currently selected primitives
860     * @return the result from the split operation
861     */
862    public static SplitWayCommand split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
863        List<List<Node>> chunks = buildSplitChunks(way, atNodes);
864        return chunks != null ? splitWay(way, chunks, selection) : null;
865    }
866
867    /**
868     * Add relations that are treated in a specific way.
869     * @param relationType The value in the {@code type} key
870     * @param treatAs The type of relation to treat the {@code relationType} as.
871     * Currently only supports relations that can be handled like "restriction"
872     * relations.
873     * @return the previous value associated with relationType, or null if there was no mapping
874     * @since 15078
875     */
876    public static String addSpecialRelationType(String relationType, String treatAs) {
877        return relationSpecialTypes.put(relationType, treatAs);
878    }
879
880    /**
881     * Get the types of relations that are treated differently
882     * @return {@code Map<Relation Type, Type of Relation it is to be treated as>}
883     * @since 15078
884     */
885    public static Map<String, String> getSpecialRelationTypes() {
886        return relationSpecialTypes;
887    }
888
889    /**
890     * What to do when the split way is part of relations, and the order of the new parts in the relation cannot be
891     * determined without downloading missing relation members.
892     */
893    public enum WhenRelationOrderUncertain {
894        /**
895         * Ask the user to consent to downloading the missing members. The user can abort the operation or choose to
896         * proceed without downloading anything.
897         */
898        ASK_USER_FOR_CONSENT_TO_DOWNLOAD,
899        /**
900         * If there are relation members missing, and these are needed to determine the order of the new parts in
901         * that relation, abort the split operation.
902         */
903        ABORT,
904        /**
905         * If there are relation members missing, and these are needed to determine the order of the new parts in
906         * that relation, continue with the split operation anyway, without downloading anything. Caution: use this
907         * option with care.
908         */
909        SPLIT_ANYWAY,
910        /**
911         * If there are relation members missing, and these are needed to determine the order of the new parts in
912         * that relation, automatically download these without prompting the user.
913         */
914        DOWNLOAD_MISSING_MEMBERS
915    }
916
917    static class RelationAnalysis {
918        private final Relation relation;
919        private final RelationMember relationMember;
920        private final Direction direction;
921        private final Set<Way> neededIncompleteMembers;
922
923        RelationAnalysis(Relation relation,
924                                RelationMember relationMember,
925                                Direction direction,
926                                Set<Way> neededIncompleteMembers) {
927            this.relation = relation;
928            this.relationMember = relationMember;
929            this.direction = direction;
930            this.neededIncompleteMembers = neededIncompleteMembers;
931        }
932
933        RelationMember getRelationMember() {
934            return relationMember;
935        }
936
937        Direction getDirection() {
938            return direction;
939        }
940
941        public Set<Way> getNeededIncompleteMembers() {
942            return neededIncompleteMembers;
943        }
944
945        Relation getRelation() {
946            return relation;
947        }
948    }
949
950    enum Direction {
951        FORWARDS,
952        BACKWARDS,
953        UNKNOWN,
954        IRRELEVANT
955    }
956
957    enum WarningType {
958        GENERIC,
959        ROLE
960    }
961
962    enum MissingMemberStrategy {
963        GO_AHEAD_WITH_DOWNLOADS,
964        GO_AHEAD_WITHOUT_DOWNLOADS,
965        USER_ABORTED
966    }
967}