001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.Collections;
013import java.util.HashMap;
014import java.util.HashSet;
015import java.util.LinkedList;
016import java.util.List;
017import java.util.Map;
018import java.util.Set;
019
020import javax.swing.JOptionPane;
021import javax.swing.JPanel;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.command.AddCommand;
025import org.openstreetmap.josm.command.ChangeCommand;
026import org.openstreetmap.josm.command.ChangeNodesCommand;
027import org.openstreetmap.josm.command.Command;
028import org.openstreetmap.josm.command.SequenceCommand;
029import org.openstreetmap.josm.data.osm.Node;
030import org.openstreetmap.josm.data.osm.OsmPrimitive;
031import org.openstreetmap.josm.data.osm.Relation;
032import org.openstreetmap.josm.data.osm.RelationMember;
033import org.openstreetmap.josm.data.osm.Way;
034import org.openstreetmap.josm.gui.MapView;
035import org.openstreetmap.josm.gui.Notification;
036import org.openstreetmap.josm.tools.Shortcut;
037
038/**
039 * Duplicate nodes that are used by multiple ways.
040 *
041 * Resulting nodes are identical, up to their position.
042 *
043 * This is the opposite of the MergeNodesAction.
044 *
045 * If a single node is selected, it will copy that node and remove all tags from the old one
046 */
047public class UnGlueAction extends JosmAction {
048
049    private transient Node selectedNode;
050    private transient Way selectedWay;
051    private transient Set<Node> selectedNodes;
052
053    /**
054     * Create a new UnGlueAction.
055     */
056    public UnGlueAction() {
057        super(tr("UnGlue Ways"), "unglueways", tr("Duplicate nodes that are used by multiple ways."),
058                Shortcut.registerShortcut("tools:unglue", tr("Tool: {0}", tr("UnGlue Ways")), KeyEvent.VK_G, Shortcut.DIRECT), true);
059        putValue("help", ht("/Action/UnGlue"));
060    }
061
062    /**
063     * Called when the action is executed.
064     *
065     * This method does some checking on the selection and calls the matching unGlueWay method.
066     */
067    @Override
068    public void actionPerformed(ActionEvent e) {
069
070        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
071
072        String errMsg = null;
073        int errorTime = Notification.TIME_DEFAULT;
074        if (checkSelection(selection)) {
075            if (!checkAndConfirmOutlyingUnglue()) {
076                // FIXME: Leaving action without clearing selectedNode, selectedWay, selectedNodes
077                return;
078            }
079            int count = 0;
080            for (Way w : OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) {
081                if (!w.isUsable() || w.getNodesCount() < 1) {
082                    continue;
083                }
084                count++;
085            }
086            if (count < 2) {
087                boolean selfCrossing = false;
088                if (count == 1) {
089                    // First try unglue self-crossing way
090                    selfCrossing = unglueSelfCrossingWay();
091                }
092                // If there aren't enough ways, maybe the user wanted to unglue the nodes
093                // (= copy tags to a new node)
094                if (!selfCrossing)
095                    if (checkForUnglueNode(selection)) {
096                        unglueNode(e);
097                    } else {
098                        errorTime = Notification.TIME_SHORT;
099                        errMsg = tr("This node is not glued to anything else.");
100                    }
101            } else {
102                // and then do the work.
103                unglueWays();
104            }
105        } else if (checkSelection2(selection)) {
106            if (!checkAndConfirmOutlyingUnglue()) {
107                // FIXME: Leaving action without clearing selectedNode, selectedWay, selectedNodes
108                return;
109            }
110            Set<Node> tmpNodes = new HashSet<>();
111            for (Node n : selectedNodes) {
112                int count = 0;
113                for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) {
114                    if (!w.isUsable()) {
115                        continue;
116                    }
117                    count++;
118                }
119                if (count >= 2) {
120                    tmpNodes.add(n);
121                }
122            }
123            if (tmpNodes.isEmpty()) {
124                if (selection.size() > 1) {
125                    errMsg =  tr("None of these nodes are glued to anything else.");
126                } else {
127                    errMsg = tr("None of this way''s nodes are glued to anything else.");
128                }
129            } else {
130                // and then do the work.
131                selectedNodes = tmpNodes;
132                unglueWays2();
133            }
134        } else {
135            errorTime = Notification.TIME_VERY_LONG;
136            errMsg =
137                tr("The current selection cannot be used for unglueing.")+'\n'+
138                '\n'+
139                tr("Select either:")+'\n'+
140                tr("* One tagged node, or")+'\n'+
141                tr("* One node that is used by more than one way, or")+'\n'+
142                tr("* One node that is used by more than one way and one of those ways, or")+'\n'+
143                tr("* One way that has one or more nodes that are used by more than one way, or")+'\n'+
144                tr("* One way and one or more of its nodes that are used by more than one way.")+'\n'+
145                '\n'+
146                tr("Note: If a way is selected, this way will get fresh copies of the unglued\n"+
147                        "nodes and the new nodes will be selected. Otherwise, all ways will get their\n"+
148                "own copy and all nodes will be selected.");
149        }
150
151        if (errMsg != null) {
152            new Notification(
153                    errMsg)
154                    .setIcon(JOptionPane.ERROR_MESSAGE)
155                    .setDuration(errorTime)
156                    .show();
157        }
158
159        selectedNode = null;
160        selectedWay = null;
161        selectedNodes = null;
162    }
163
164    /**
165     * Assumes there is one tagged Node stored in selectedNode that it will try to unglue.
166     * (i.e. copy node and remove all tags from the old one. Relations will not be removed)
167     */
168    private void unglueNode(ActionEvent e) {
169        List<Command> cmds = new LinkedList<>();
170
171        Node c = new Node(selectedNode);
172        c.removeAll();
173        getCurrentDataSet().clearSelection(c);
174        cmds.add(new ChangeCommand(selectedNode, c));
175
176        Node n = new Node(selectedNode, true);
177
178        // If this wasn't called from menu, place it where the cursor is/was
179        if (e.getSource() instanceof JPanel) {
180            MapView mv = Main.map.mapView;
181            n.setCoor(mv.getLatLon(mv.lastMEvent.getX(), mv.lastMEvent.getY()));
182        }
183
184        cmds.add(new AddCommand(n));
185
186        fixRelations(selectedNode, cmds, Collections.singletonList(n));
187
188        Main.main.undoRedo.add(new SequenceCommand(tr("Unglued Node"), cmds));
189        getCurrentDataSet().setSelected(n);
190        Main.map.mapView.repaint();
191    }
192
193    /**
194     * Checks if selection is suitable for ungluing. This is the case when there's a single,
195     * tagged node selected that's part of at least one way (ungluing an unconnected node does
196     * not make sense. Due to the call order in actionPerformed, this is only called when the
197     * node is only part of one or less ways.
198     *
199     * @param selection The selection to check against
200     * @return {@code true} if selection is suitable
201     */
202    private boolean checkForUnglueNode(Collection<? extends OsmPrimitive> selection) {
203        if (selection.size() != 1)
204            return false;
205        OsmPrimitive n = (OsmPrimitive) selection.toArray()[0];
206        if (!(n instanceof Node))
207            return false;
208        if (OsmPrimitive.getFilteredList(n.getReferrers(), Way.class).isEmpty())
209            return false;
210
211        selectedNode = (Node) n;
212        return selectedNode.isTagged();
213    }
214
215    /**
216     * Checks if the selection consists of something we can work with.
217     * Checks only if the number and type of items selected looks good.
218     *
219     * If this method returns "true", selectedNode and selectedWay will
220     * be set.
221     *
222     * Returns true if either one node is selected or one node and one
223     * way are selected and the node is part of the way.
224     *
225     * The way will be put into the object variable "selectedWay", the
226     * node into "selectedNode".
227     * @return true if either one node is selected or one node and one way are selected and the node is part of the way
228     */
229    private boolean checkSelection(Collection<? extends OsmPrimitive> selection) {
230
231        int size = selection.size();
232        if (size < 1 || size > 2)
233            return false;
234
235        selectedNode = null;
236        selectedWay = null;
237
238        for (OsmPrimitive p : selection) {
239            if (p instanceof Node) {
240                selectedNode = (Node) p;
241                if (size == 1 || selectedWay != null)
242                    return size == 1 || selectedWay.containsNode(selectedNode);
243            } else if (p instanceof Way) {
244                selectedWay = (Way) p;
245                if (size == 2 && selectedNode != null)
246                    return selectedWay.containsNode(selectedNode);
247            }
248        }
249
250        return false;
251    }
252
253    /**
254     * Checks if the selection consists of something we can work with.
255     * Checks only if the number and type of items selected looks good.
256     *
257     * Returns true if one way and any number of nodes that are part of
258     * that way are selected. Note: "any" can be none, then all nodes of
259     * the way are used.
260     *
261     * The way will be put into the object variable "selectedWay", the
262     * nodes into "selectedNodes".
263     * @return true if one way and any number of nodes that are part of that way are selected
264     */
265    private boolean checkSelection2(Collection<? extends OsmPrimitive> selection) {
266        if (selection.isEmpty())
267            return false;
268
269        selectedWay = null;
270        for (OsmPrimitive p : selection) {
271            if (p instanceof Way) {
272                if (selectedWay != null)
273                    return false;
274                selectedWay = (Way) p;
275            }
276        }
277        if (selectedWay == null)
278            return false;
279
280        selectedNodes = new HashSet<>();
281        for (OsmPrimitive p : selection) {
282            if (p instanceof Node) {
283                Node n = (Node) p;
284                if (!selectedWay.containsNode(n))
285                    return false;
286                selectedNodes.add(n);
287            }
288        }
289
290        if (selectedNodes.isEmpty()) {
291            selectedNodes.addAll(selectedWay.getNodes());
292        }
293
294        return true;
295    }
296
297    /**
298     * dupe the given node of the given way
299     *
300     * assume that OrginalNode is in the way
301     * <ul>
302     * <li>the new node will be put into the parameter newNodes.</li>
303     * <li>the add-node command will be put into the parameter cmds.</li>
304     * <li>the changed way will be returned and must be put into cmds by the caller!</li>
305     * </ul>
306     * @return new way
307     */
308    private static Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) {
309        // clone the node for the way
310        Node newNode = new Node(originalNode, true /* clear OSM ID */);
311        newNodes.add(newNode);
312        cmds.add(new AddCommand(newNode));
313
314        List<Node> nn = new ArrayList<>();
315        for (Node pushNode : w.getNodes()) {
316            if (originalNode == pushNode) {
317                pushNode = newNode;
318            }
319            nn.add(pushNode);
320        }
321        Way newWay = new Way(w);
322        newWay.setNodes(nn);
323
324        return newWay;
325    }
326
327    /**
328     * put all newNodes into the same relation(s) that originalNode is in
329     */
330    private void fixRelations(Node originalNode, List<Command> cmds, List<Node> newNodes) {
331        // modify all relations containing the node
332        for (Relation r : OsmPrimitive.getFilteredList(originalNode.getReferrers(), Relation.class)) {
333            if (r.isDeleted()) {
334                continue;
335            }
336            Relation newRel = null;
337            Map<String, Integer> rolesToReAdd = null; // <role name, index>
338            int i = 0;
339            for (RelationMember rm : r.getMembers()) {
340                if (rm.isNode() && rm.getMember() == originalNode) {
341                    if (newRel == null) {
342                        newRel = new Relation(r);
343                        rolesToReAdd = new HashMap<>();
344                    }
345                    rolesToReAdd.put(rm.getRole(), i);
346                }
347                i++;
348            }
349            if (newRel != null) {
350                for (Node n : newNodes) {
351                    for (Map.Entry<String, Integer> role : rolesToReAdd.entrySet()) {
352                        newRel.addMember(role.getValue() + 1, new RelationMember(role.getKey(), n));
353                    }
354                }
355                cmds.add(new ChangeCommand(r, newRel));
356            }
357        }
358    }
359
360    /**
361     * dupe a single node into as many nodes as there are ways using it, OR
362     *
363     * dupe a single node once, and put the copy on the selected way
364     */
365    private void unglueWays() {
366        List<Command> cmds = new LinkedList<>();
367        List<Node> newNodes = new LinkedList<>();
368
369        if (selectedWay == null) {
370            Way wayWithSelectedNode = null;
371            LinkedList<Way> parentWays = new LinkedList<>();
372            for (OsmPrimitive osm : selectedNode.getReferrers()) {
373                if (osm.isUsable() && osm instanceof Way) {
374                    Way w = (Way) osm;
375                    if (wayWithSelectedNode == null && !w.isFirstLastNode(selectedNode)) {
376                        wayWithSelectedNode = w;
377                    } else {
378                        parentWays.add(w);
379                    }
380                }
381            }
382            if (wayWithSelectedNode == null) {
383                parentWays.removeFirst();
384            }
385            for (Way w : parentWays) {
386                cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes)));
387            }
388        } else {
389            cmds.add(new ChangeCommand(selectedWay, modifyWay(selectedNode, selectedWay, cmds, newNodes)));
390        }
391
392        fixRelations(selectedNode, cmds, newNodes);
393        execCommands(cmds, newNodes);
394    }
395
396    /**
397     * Add commands to undo-redo system.
398     * @param cmds Commands to execute
399     * @param newNodes New created nodes by this set of command
400     */
401    private static void execCommands(List<Command> cmds, List<Node> newNodes) {
402        Main.main.undoRedo.add(new SequenceCommand(/* for correct i18n of plural forms - see #9110 */
403                trn("Dupe into {0} node", "Dupe into {0} nodes", newNodes.size() + 1, newNodes.size() + 1), cmds));
404        // select one of the new nodes
405        getCurrentDataSet().setSelected(newNodes.get(0));
406    }
407
408    /**
409     * Duplicates a node used several times by the same way. See #9896.
410     * @return true if action is OK false if there is nothing to do
411     */
412    private boolean unglueSelfCrossingWay() {
413        // According to previous check, only one valid way through that node
414        List<Command> cmds = new LinkedList<>();
415        Way way = null;
416        for (Way w: OsmPrimitive.getFilteredList(selectedNode.getReferrers(), Way.class)) {
417            if (w.isUsable() && w.getNodesCount() >= 1) {
418                way = w;
419            }
420        }
421        List<Node> oldNodes = way.getNodes();
422        List<Node> newNodes = new ArrayList<>(oldNodes.size());
423        List<Node> addNodes = new ArrayList<>();
424        boolean seen = false;
425        for (Node n: oldNodes) {
426            if (n == selectedNode) {
427                if (seen) {
428                    Node newNode = new Node(n, true /* clear OSM ID */);
429                    newNodes.add(newNode);
430                    cmds.add(new AddCommand(newNode));
431                    newNodes.add(newNode);
432                    addNodes.add(newNode);
433                } else {
434                    newNodes.add(n);
435                    seen = true;
436                }
437            } else {
438                newNodes.add(n);
439            }
440        }
441        if (addNodes.isEmpty()) {
442            // selectedNode doesn't need unglue
443            return false;
444        }
445        cmds.add(new ChangeNodesCommand(way, newNodes));
446        // Update relation
447        fixRelations(selectedNode, cmds, addNodes);
448        execCommands(cmds, addNodes);
449        return true;
450     }
451
452    /**
453     * dupe all nodes that are selected, and put the copies on the selected way
454     *
455     */
456    private void unglueWays2() {
457        List<Command> cmds = new LinkedList<>();
458        List<Node> allNewNodes = new LinkedList<>();
459        Way tmpWay = selectedWay;
460
461        for (Node n : selectedNodes) {
462            List<Node> newNodes = new LinkedList<>();
463            tmpWay = modifyWay(n, tmpWay, cmds, newNodes);
464            fixRelations(n, cmds, newNodes);
465            allNewNodes.addAll(newNodes);
466        }
467        cmds.add(new ChangeCommand(selectedWay, tmpWay)); // only one changeCommand for a way, else garbage will happen
468
469        Main.main.undoRedo.add(new SequenceCommand(
470                trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes",
471                        selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds));
472        getCurrentDataSet().setSelected(allNewNodes);
473    }
474
475    @Override
476    protected void updateEnabledState() {
477        if (getCurrentDataSet() == null) {
478            setEnabled(false);
479        } else {
480            updateEnabledState(getCurrentDataSet().getSelected());
481        }
482    }
483
484    @Override
485    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
486        setEnabled(selection != null && !selection.isEmpty());
487    }
488
489    protected boolean checkAndConfirmOutlyingUnglue() {
490        List<OsmPrimitive> primitives = new ArrayList<>(2 + (selectedNodes == null ? 0 : selectedNodes.size()));
491        if (selectedNodes != null)
492            primitives.addAll(selectedNodes);
493        if (selectedNode != null)
494            primitives.add(selectedNode);
495        return Command.checkAndConfirmOutlyingOperation("unglue",
496                tr("Unglue confirmation"),
497                tr("You are about to unglue nodes outside of the area you have downloaded."
498                        + "<br>"
499                        + "This can cause problems because other objects (that you do not see) might use them."
500                        + "<br>"
501                        + "Do you really want to unglue?"),
502                tr("You are about to unglue incomplete objects."
503                        + "<br>"
504                        + "This will cause problems because you don''t see the real object."
505                        + "<br>" + "Do you really want to unglue?"),
506                primitives, null);
507    }
508}