001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import java.util.Collection;
005import java.util.LinkedList;
006import java.util.Objects;
007import java.util.Set;
008import java.util.TreeSet;
009
010import org.openstreetmap.josm.data.osm.DataSet;
011import org.openstreetmap.josm.data.osm.Node;
012import org.openstreetmap.josm.data.osm.OsmPrimitive;
013import org.openstreetmap.josm.data.osm.Way;
014import org.openstreetmap.josm.tools.Utils;
015
016/**
017 * Auxiliary class for the {@link SelectNonBranchingWaySequencesAction}.
018 *
019 * @author Marko Mäkelä
020 */
021public class SelectNonBranchingWaySequences {
022    /**
023     * outer endpoints of selected ways
024     */
025    private final Set<Node> outerNodes;
026    /**
027     * endpoints of selected ways
028     */
029    private final Set<Node> nodes;
030
031    /**
032     * Creates a way selection
033     *
034     * @param ways selection a selection of ways
035     */
036    public SelectNonBranchingWaySequences(final Collection<Way> ways) {
037        if (ways.isEmpty()) {
038            // The selection cannot be extended.
039            outerNodes = null;
040            nodes = null;
041        } else {
042            nodes = new TreeSet<>();
043            outerNodes = new TreeSet<>();
044
045            for (Way way : ways) {
046                addNodes(way);
047            }
048        }
049    }
050
051    /**
052     * Add a way endpoint to nodes, outerNodes
053     *
054     * @param node a way endpoint
055     */
056    private void addNodes(Node node) {
057        if (node == null) return;
058        else if (!nodes.add(node))
059            outerNodes.remove(node);
060        else
061            outerNodes.add(node);
062    }
063
064    /**
065     * Add the endpoints of the way to nodes, outerNodes
066     *
067     * @param way a way whose endpoints are added
068     */
069    private void addNodes(Way way) {
070        addNodes(way.firstNode());
071        addNodes(way.lastNode());
072    }
073
074    /**
075     * Find out if the selection can be extended
076     *
077     * @return true if the selection can be extended
078     */
079    public boolean canExtend() {
080        return !Utils.isEmpty(outerNodes);
081    }
082
083    /**
084     * Finds out if the current selection can be extended.
085     *
086     * @param selection current selection (ways and others)
087     * @param node      perimeter node from which to extend the selection
088     * @return a way by which to extend the selection, or null
089     */
090    private static Way findWay(Collection<OsmPrimitive> selection, Node node) {
091        Way foundWay = null;
092
093        for (Way way : node.getParentWays()) {
094            if (way.getNodesCount() < 2 || !way.isFirstLastNode(node)
095                    || !way.isSelectable()
096                    || selection.contains(way))
097                continue;
098
099            /* A previously unselected way was found that is connected
100            to the node. */
101            if (foundWay != null)
102                /* This is not the only qualifying way. There is a
103                branch at the node, and we cannot extend the selection. */
104                return null;
105
106            /* Remember the first found qualifying way. */
107            foundWay = way;
108        }
109
110        /* Return the only way found, or null if none was found. */
111        return foundWay;
112    }
113
114    /**
115     * Finds out if the current selection can be extended.
116     * <p>
117     * The members outerNodes, nodes must have been initialized.
118     * How to update these members when extending the selection, @see extend().
119     * </p>
120     * @param selection current selection
121     * @return a way by which to extend the selection, or null
122     */
123    private Way findWay(Collection<OsmPrimitive> selection) {
124        return outerNodes.stream()
125                .map(node -> findWay(selection, node))
126                .filter(Objects::nonNull)
127                .findFirst().orElse(null);
128
129    }
130
131    /**
132     * Extend the current selection
133     *
134     * @param data the data set in which to extend the selection
135     */
136    public void extend(DataSet data) {
137        if (!canExtend())
138            return;
139
140        Collection<OsmPrimitive> currentSelection = data.getSelected();
141
142        Way way = findWay(currentSelection);
143
144        if (way == null)
145            return;
146
147        boolean selectionChanged = false;
148        Collection<OsmPrimitive> selection = new LinkedList<>(currentSelection);
149
150        do {
151            if (!selection.add(way))
152                break;
153
154            selectionChanged = true;
155            addNodes(way);
156
157            way = findWay(selection);
158        } while (way != null);
159
160        if (selectionChanged)
161            data.setSelected(selection);
162    }
163}