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