001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.command;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collection;
010import java.util.HashSet;
011import java.util.Iterator;
012import java.util.LinkedList;
013import java.util.List;
014import java.util.Objects;
015import java.util.Optional;
016import java.util.Set;
017import java.util.function.Consumer;
018
019import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
020import org.openstreetmap.josm.data.osm.Node;
021import org.openstreetmap.josm.data.osm.OsmPrimitive;
022import org.openstreetmap.josm.data.osm.PrimitiveId;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.RelationMember;
025import org.openstreetmap.josm.data.osm.Way;
026import org.openstreetmap.josm.spi.preferences.Config;
027import org.openstreetmap.josm.tools.CheckParameterUtil;
028import org.openstreetmap.josm.tools.Logging;
029
030/**
031 * Splits a way into multiple ways (all identical except for their node list).
032 *
033 * Ways are just split at the selected nodes.  The nodes remain in their
034 * original order.  Selected nodes at the end of a way are ignored.
035 *
036 * @since 12828 ({@code SplitWayAction} converted to a {@link Command})
037 */
038public class SplitWayCommand extends SequenceCommand {
039
040    private static volatile Consumer<String> warningNotifier = Logging::warn;
041
042    /**
043     * Sets the global warning notifier.
044     * @param notifier warning notifier in charge of displaying warning message, if any. Must not be null
045     */
046    public static void setWarningNotifier(Consumer<String> notifier) {
047        warningNotifier = Objects.requireNonNull(notifier);
048    }
049
050    private final List<? extends PrimitiveId> newSelection;
051    private final Way originalWay;
052    private final List<Way> newWays;
053
054    /**
055     * Create a new {@code SplitWayCommand}.
056     * @param name The description text
057     * @param commandList The sequence of commands that should be executed.
058     * @param newSelection The new list of selected primitives ids (which is saved for later retrieval with {@link #getNewSelection})
059     * @param originalWay The original way being split (which is saved for later retrieval with {@link #getOriginalWay})
060     * @param newWays The resulting new ways (which is saved for later retrieval with {@link #getOriginalWay})
061     */
062    public SplitWayCommand(String name, Collection<Command> commandList,
063            List<? extends PrimitiveId> newSelection, Way originalWay, List<Way> newWays) {
064        super(name, commandList);
065        this.newSelection = newSelection;
066        this.originalWay = originalWay;
067        this.newWays = newWays;
068    }
069
070    /**
071     * Replies the new list of selected primitives ids
072     * @return The new list of selected primitives ids
073     */
074    public List<? extends PrimitiveId> getNewSelection() {
075        return newSelection;
076    }
077
078    /**
079     * Replies the original way being split
080     * @return The original way being split
081     */
082    public Way getOriginalWay() {
083        return originalWay;
084    }
085
086    /**
087     * Replies the resulting new ways
088     * @return The resulting new ways
089     */
090    public List<Way> getNewWays() {
091        return newWays;
092    }
093
094    /**
095     * Determines which way chunk should reuse the old id and its history
096     */
097    @FunctionalInterface
098    public interface Strategy {
099
100        /**
101         * Determines which way chunk should reuse the old id and its history.
102         *
103         * @param wayChunks the way chunks
104         * @return the way to keep
105         */
106        Way determineWayToKeep(Iterable<Way> wayChunks);
107
108        /**
109         * Returns a strategy which selects the way chunk with the highest node count to keep.
110         * @return strategy which selects the way chunk with the highest node count to keep
111         */
112        static Strategy keepLongestChunk() {
113            return wayChunks -> {
114                    Way wayToKeep = null;
115                    for (Way i : wayChunks) {
116                        if (wayToKeep == null || i.getNodesCount() > wayToKeep.getNodesCount()) {
117                            wayToKeep = i;
118                        }
119                    }
120                    return wayToKeep;
121                };
122        }
123
124        /**
125         * Returns a strategy which selects the first way chunk.
126         * @return strategy which selects the first way chunk
127         */
128        static Strategy keepFirstChunk() {
129            return wayChunks -> wayChunks.iterator().next();
130        }
131    }
132
133    /**
134     * Splits the nodes of {@code wayToSplit} into a list of node sequences
135     * which are separated at the nodes in {@code splitPoints}.
136     *
137     * This method displays warning messages if {@code wayToSplit} and/or
138     * {@code splitPoints} aren't consistent.
139     *
140     * Returns null, if building the split chunks fails.
141     *
142     * @param wayToSplit the way to split. Must not be null.
143     * @param splitPoints the nodes where the way is split. Must not be null.
144     * @return the list of chunks
145     */
146    public static List<List<Node>> buildSplitChunks(Way wayToSplit, List<Node> splitPoints) {
147        CheckParameterUtil.ensureParameterNotNull(wayToSplit, "wayToSplit");
148        CheckParameterUtil.ensureParameterNotNull(splitPoints, "splitPoints");
149
150        Set<Node> nodeSet = new HashSet<>(splitPoints);
151        List<List<Node>> wayChunks = new LinkedList<>();
152        List<Node> currentWayChunk = new ArrayList<>();
153        wayChunks.add(currentWayChunk);
154
155        Iterator<Node> it = wayToSplit.getNodes().iterator();
156        while (it.hasNext()) {
157            Node currentNode = it.next();
158            boolean atEndOfWay = currentWayChunk.isEmpty() || !it.hasNext();
159            currentWayChunk.add(currentNode);
160            if (nodeSet.contains(currentNode) && !atEndOfWay) {
161                currentWayChunk = new ArrayList<>();
162                currentWayChunk.add(currentNode);
163                wayChunks.add(currentWayChunk);
164            }
165        }
166
167        // Handle circular ways specially.
168        // If you split at a circular way at two nodes, you just want to split
169        // it at these points, not also at the former endpoint.
170        // So if the last node is the same first node, join the last and the
171        // first way chunk.
172        List<Node> lastWayChunk = wayChunks.get(wayChunks.size() - 1);
173        if (wayChunks.size() >= 2
174                && wayChunks.get(0).get(0) == lastWayChunk.get(lastWayChunk.size() - 1)
175                && !nodeSet.contains(wayChunks.get(0).get(0))) {
176            if (wayChunks.size() == 2) {
177                warningNotifier.accept(tr("You must select two or more nodes to split a circular way."));
178                return null;
179            }
180            lastWayChunk.remove(lastWayChunk.size() - 1);
181            lastWayChunk.addAll(wayChunks.get(0));
182            wayChunks.remove(wayChunks.size() - 1);
183            wayChunks.set(0, lastWayChunk);
184        }
185
186        if (wayChunks.size() < 2) {
187            if (wayChunks.get(0).get(0) == wayChunks.get(0).get(wayChunks.get(0).size() - 1)) {
188                warningNotifier.accept(
189                        tr("You must select two or more nodes to split a circular way."));
190            } else {
191                warningNotifier.accept(
192                        tr("The way cannot be split at the selected nodes. (Hint: Select nodes in the middle of the way.)"));
193            }
194            return null;
195        }
196        return wayChunks;
197    }
198
199    /**
200     * Creates new way objects for the way chunks and transfers the keys from the original way.
201     * @param way the original way whose  keys are transferred
202     * @param wayChunks the way chunks
203     * @return the new way objects
204     */
205    public static List<Way> createNewWaysFromChunks(Way way, Iterable<List<Node>> wayChunks) {
206        final List<Way> newWays = new ArrayList<>();
207        for (List<Node> wayChunk : wayChunks) {
208            Way wayToAdd = new Way();
209            wayToAdd.setKeys(way.getKeys());
210            wayToAdd.setNodes(wayChunk);
211            newWays.add(wayToAdd);
212        }
213        return newWays;
214    }
215
216    /**
217     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
218     * the result of this process in an instance of {@link SplitWayCommand}.
219     *
220     * Note that changes are not applied to the data yet. You have to
221     * submit the command first, i.e. {@code Main.main.undoredo.add(result)}.
222     *
223     * @param way the way to split. Must not be null.
224     * @param wayChunks the list of way chunks into the way is split. Must not be null.
225     * @param selection The list of currently selected primitives
226     * @return the result from the split operation
227     */
228    public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks, Collection<? extends OsmPrimitive> selection) {
229        return splitWay(way, wayChunks, selection, Strategy.keepLongestChunk());
230    }
231
232    /**
233     * Splits the way {@code way} into chunks of {@code wayChunks} and replies
234     * the result of this process in an instance of {@link SplitWayCommand}.
235     * The {@link SplitWayCommand.Strategy} is used to determine which
236     * way chunk should reuse the old id and its history.
237     *
238     * Note that changes are not applied to the data yet. You have to
239     * submit the command first, i.e. {@code Main.main.undoredo.add(result)}.
240     *
241     * @param way the way to split. Must not be null.
242     * @param wayChunks the list of way chunks into the way is split. Must not be null.
243     * @param selection The list of currently selected primitives
244     * @param splitStrategy The strategy used to determine which way chunk should reuse the old id and its history
245     * @return the result from the split operation
246     */
247    public static SplitWayCommand splitWay(Way way, List<List<Node>> wayChunks,
248            Collection<? extends OsmPrimitive> selection, Strategy splitStrategy) {
249        // build a list of commands, and also a new selection list
250        final List<OsmPrimitive> newSelection = new ArrayList<>(selection.size() + wayChunks.size());
251        newSelection.addAll(selection);
252
253        // Create all potential new ways
254        final List<Way> newWays = createNewWaysFromChunks(way, wayChunks);
255
256        // Determine which part reuses the existing way
257        final Way wayToKeep = splitStrategy.determineWayToKeep(newWays);
258
259        return wayToKeep != null ? doSplitWay(way, wayToKeep, newWays, newSelection) : null;
260    }
261
262    /**
263     * Effectively constructs the {@link SplitWayCommand}.
264     * This method is only public for {@code SplitWayAction}.
265     *
266     * @param way the way to split. Must not be null.
267     * @param wayToKeep way chunk which should reuse the old id and its history
268     * @param newWays potential new ways
269     * @param newSelection new selection list to update (optional: can be null)
270     * @return the {@code SplitWayCommand}
271     */
272    public static SplitWayCommand doSplitWay(Way way, Way wayToKeep, List<Way> newWays, List<OsmPrimitive> newSelection) {
273
274        Collection<Command> commandList = new ArrayList<>(newWays.size());
275        Collection<String> nowarnroles = Config.getPref().getList("way.split.roles.nowarn",
276                Arrays.asList("outer", "inner", "forward", "backward", "north", "south", "east", "west"));
277
278        // Change the original way
279        final Way changedWay = new Way(way);
280        changedWay.setNodes(wayToKeep.getNodes());
281        commandList.add(new ChangeCommand(way, changedWay));
282        if (/*!isMapModeDraw &&*/ newSelection != null && !newSelection.contains(way)) {
283            newSelection.add(way);
284        }
285        final int indexOfWayToKeep = newWays.indexOf(wayToKeep);
286        newWays.remove(wayToKeep);
287
288        if (/*!isMapModeDraw &&*/ newSelection != null) {
289            newSelection.addAll(newWays);
290        }
291        for (Way wayToAdd : newWays) {
292            commandList.add(new AddCommand(way.getDataSet(), wayToAdd));
293        }
294
295        boolean warnmerole = false;
296        boolean warnme = false;
297        // now copy all relations to new way also
298
299        for (Relation r : OsmPrimitive.getFilteredList(way.getReferrers(), Relation.class)) {
300            if (!r.isUsable()) {
301                continue;
302            }
303            Relation c = null;
304            String type = Optional.ofNullable(r.get("type")).orElse("");
305
306            int ic = 0;
307            int ir = 0;
308            List<RelationMember> relationMembers = r.getMembers();
309            for (RelationMember rm: relationMembers) {
310                if (rm.isWay() && rm.getMember() == way) {
311                    boolean insert = true;
312                    if ("restriction".equals(type) || "destination_sign".equals(type)) {
313                        /* this code assumes the restriction is correct. No real error checking done */
314                        String role = rm.getRole();
315                        if ("from".equals(role) || "to".equals(role)) {
316                            OsmPrimitive via = findVia(r, type);
317                            List<Node> nodes = new ArrayList<>();
318                            if (via != null) {
319                                if (via instanceof Node) {
320                                    nodes.add((Node) via);
321                                } else if (via instanceof Way) {
322                                    nodes.add(((Way) via).lastNode());
323                                    nodes.add(((Way) via).firstNode());
324                                }
325                            }
326                            Way res = null;
327                            for (Node n : nodes) {
328                                if (changedWay.isFirstLastNode(n)) {
329                                    res = way;
330                                }
331                            }
332                            if (res == null) {
333                                for (Way wayToAdd : newWays) {
334                                    for (Node n : nodes) {
335                                        if (wayToAdd.isFirstLastNode(n)) {
336                                            res = wayToAdd;
337                                        }
338                                    }
339                                }
340                                if (res != null) {
341                                    if (c == null) {
342                                        c = new Relation(r);
343                                    }
344                                    c.addMember(new RelationMember(role, res));
345                                    c.removeMembersFor(way);
346                                    insert = false;
347                                }
348                            } else {
349                                insert = false;
350                            }
351                        } else if (!"via".equals(role)) {
352                            warnme = true;
353                        }
354                    } else if (!("route".equals(type)) && !("multipolygon".equals(type))) {
355                        warnme = true;
356                    }
357                    if (c == null) {
358                        c = new Relation(r);
359                    }
360
361                    if (insert) {
362                        if (rm.hasRole() && !nowarnroles.contains(rm.getRole())) {
363                            warnmerole = true;
364                        }
365
366                        Boolean backwards = null;
367                        int k = 1;
368                        while (ir - k >= 0 || ir + k < relationMembers.size()) {
369                            if ((ir - k >= 0) && relationMembers.get(ir - k).isWay()) {
370                                Way w = relationMembers.get(ir - k).getWay();
371                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
372                                    backwards = Boolean.FALSE;
373                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
374                                    backwards = Boolean.TRUE;
375                                }
376                                break;
377                            }
378                            if ((ir + k < relationMembers.size()) && relationMembers.get(ir + k).isWay()) {
379                                Way w = relationMembers.get(ir + k).getWay();
380                                if ((w.lastNode() == way.firstNode()) || w.firstNode() == way.firstNode()) {
381                                    backwards = Boolean.TRUE;
382                                } else if ((w.firstNode() == way.lastNode()) || w.lastNode() == way.lastNode()) {
383                                    backwards = Boolean.FALSE;
384                                }
385                                break;
386                            }
387                            k++;
388                        }
389
390                        int j = ic;
391                        final List<Way> waysToAddBefore = newWays.subList(0, indexOfWayToKeep);
392                        for (Way wayToAdd : waysToAddBefore) {
393                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
394                            j++;
395                            if (Boolean.TRUE.equals(backwards)) {
396                                c.addMember(ic + 1, em);
397                            } else {
398                                c.addMember(j - 1, em);
399                            }
400                        }
401                        final List<Way> waysToAddAfter = newWays.subList(indexOfWayToKeep, newWays.size());
402                        for (Way wayToAdd : waysToAddAfter) {
403                            RelationMember em = new RelationMember(rm.getRole(), wayToAdd);
404                            j++;
405                            if (Boolean.TRUE.equals(backwards)) {
406                                c.addMember(ic, em);
407                            } else {
408                                c.addMember(j, em);
409                            }
410                        }
411                        ic = j;
412                    }
413                }
414                ic++;
415                ir++;
416            }
417
418            if (c != null) {
419                commandList.add(new ChangeCommand(r.getDataSet(), r, c));
420            }
421        }
422        if (warnmerole) {
423            warningNotifier.accept(
424                    tr("A role based relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
425        } else if (warnme) {
426            warningNotifier.accept(
427                    tr("A relation membership was copied to all new ways.<br>You should verify this and correct it when necessary."));
428        }
429
430        return new SplitWayCommand(
431                    /* for correct i18n of plural forms - see #9110 */
432                    trn("Split way {0} into {1} part", "Split way {0} into {1} parts", newWays.size() + 1,
433                            way.getDisplayName(DefaultNameFormatter.getInstance()), newWays.size() + 1),
434                    commandList,
435                    newSelection,
436                    way,
437                    newWays
438            );
439    }
440
441    static OsmPrimitive findVia(Relation r, String type) {
442        if (type != null) {
443            switch (type) {
444            case "restriction":
445                return findRelationMember(r, "via").orElse(null);
446            case "destination_sign":
447                // Prefer intersection over sign, see #12347
448                return findRelationMember(r, "intersection").orElse(findRelationMember(r, "sign").orElse(null));
449            default:
450                return null;
451            }
452        }
453        return null;
454    }
455
456    static Optional<OsmPrimitive> findRelationMember(Relation r, String role) {
457        return r.getMembers().stream().filter(rmv -> role.equals(rmv.getRole()))
458                .map(RelationMember::getMember).findAny();
459    }
460
461    /**
462     * Splits the way {@code way} at the nodes in {@code atNodes} and replies
463     * the result of this process in an instance of {@link SplitWayCommand}.
464     *
465     * Note that changes are not applied to the data yet. You have to
466     * submit the command first, i.e. {@code Main.main.undoredo.add(result)}.
467     *
468     * Replies null if the way couldn't be split at the given nodes.
469     *
470     * @param way the way to split. Must not be null.
471     * @param atNodes the list of nodes where the way is split. Must not be null.
472     * @param selection The list of currently selected primitives
473     * @return the result from the split operation
474     */
475    public static SplitWayCommand split(Way way, List<Node> atNodes, Collection<? extends OsmPrimitive> selection) {
476        List<List<Node>> chunks = buildSplitChunks(way, atNodes);
477        return chunks != null ? splitWay(way, chunks, selection) : null;
478    }
479}