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