Ignore:
Timestamp:
2009-09-12T06:21:30+02:00 (16 years ago)
Author:
Gubaer
Message:

rewrite of MergeNodesAction
new: new conflict resolution dialog for conflicts during node merging. Can resolve conflicts in relation members too.

Location:
trunk/src/org/openstreetmap/josm/actions
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/actions/CombineWayAction.java

    r2079 r2095  
    3333import org.openstreetmap.josm.data.osm.Way;
    3434import org.openstreetmap.josm.gui.ExtendedDialog;
    35 import org.openstreetmap.josm.gui.conflict.tags.CombineWaysConflictResolverDialog;
     35import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
    3636import org.openstreetmap.josm.tools.Pair;
    3737import org.openstreetmap.josm.tools.Shortcut;
     
    166166        completeTagCollectionWithMissingTags(completeWayTags, ways);
    167167
    168         CombineWaysConflictResolverDialog dialog = CombineWaysConflictResolverDialog.getInstance();
     168        CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
    169169        dialog.getTagConflictResolverModel().populate(completeWayTags);
    170         dialog.setTargetWay(targetWay);
     170        dialog.setTargetPrimitive(targetWay);
    171171        dialog.getRelationMemberConflictResolverModel().populate(
    172172                referringRelations.getRelations(),
     
    191191        cmds.add(new DeleteCommand(deletedWays));
    192192        cmds.add(new ChangeCommand(targetWay, modifiedTargetWay));
    193         cmds.addAll(dialog.buildResolutionCommands(targetWay));
     193        cmds.addAll(dialog.buildResolutionCommands());
    194194        final SequenceCommand sequenceCommand = new SequenceCommand(tr("Combine {0} ways", ways.size()), cmds);
    195195
  • trunk/src/org/openstreetmap/josm/actions/MergeNodesAction.java

    r2070 r2095  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    6 import java.awt.GridBagLayout;
    76import java.awt.event.ActionEvent;
    87import java.awt.event.KeyEvent;
    98import java.util.ArrayList;
    109import java.util.Collection;
    11 import java.util.HashMap;
    1210import java.util.HashSet;
    1311import java.util.LinkedList;
    14 import java.util.List;
    15 import java.util.Map;
    1612import java.util.Set;
    17 import java.util.TreeMap;
    18 import java.util.TreeSet;
    19 import java.util.Map.Entry;
    20 
    21 import javax.swing.Box;
    22 import javax.swing.JComboBox;
    23 import javax.swing.JLabel;
     13
    2414import javax.swing.JOptionPane;
    25 import javax.swing.JPanel;
    2615
    2716import org.openstreetmap.josm.Main;
     
    3019import org.openstreetmap.josm.command.DeleteCommand;
    3120import org.openstreetmap.josm.command.SequenceCommand;
     21import org.openstreetmap.josm.data.osm.BackreferencedDataSet;
    3222import org.openstreetmap.josm.data.osm.Node;
    3323import org.openstreetmap.josm.data.osm.OsmPrimitive;
    34 import org.openstreetmap.josm.data.osm.Relation;
    35 import org.openstreetmap.josm.data.osm.RelationMember;
    36 import org.openstreetmap.josm.data.osm.TigerUtils;
     24import org.openstreetmap.josm.data.osm.Tag;
     25import org.openstreetmap.josm.data.osm.TagCollection;
    3726import org.openstreetmap.josm.data.osm.Way;
    38 import org.openstreetmap.josm.data.osm.visitor.CollectBackReferencesVisitor;
    39 import org.openstreetmap.josm.gui.ExtendedDialog;
    40 import org.openstreetmap.josm.tools.GBC;
    41 import org.openstreetmap.josm.tools.Pair;
     27import org.openstreetmap.josm.data.osm.BackreferencedDataSet.RelationToChildReference;
     28import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
     29import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    4230import org.openstreetmap.josm.tools.Shortcut;
    4331
    4432
    4533/**
    46  * Merge two or more nodes into one node.
    47  * (based on Combine ways)
    48  *
    49  * @author Matthew Newton
    50  *
     34 * Merges a collection of nodes into one node.
     35 *
    5136 */
    5237public class MergeNodesAction extends JosmAction {
     
    6045        if (!isEnabled())
    6146            return;
    62 
    6347        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
    64         LinkedList<Node> selectedNodes = new LinkedList<Node>();
    65 
    66         // the selection check should stop this procedure starting if
    67         // nothing but node are selected - otherwise we don't care
    68         // anyway as long as we have at least two nodes
    69         for (OsmPrimitive osm : selection)
    70             if (osm instanceof Node) {
    71                 selectedNodes.add((Node)osm);
    72             }
    73 
     48        Set<Node> selectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class);
    7449        if (selectedNodes.size() < 2) {
    7550            JOptionPane.showMessageDialog(
     
    8257        }
    8358
     59
     60        Node targetNode = selectTargetNode(selectedNodes);
     61        Command cmd = mergeNodes(Main.main.getEditLayer(), selectedNodes, targetNode);
     62        if (cmd != null) {
     63            Main.main.undoRedo.add(cmd);
     64            Main.main.getEditLayer().data.setSelected(targetNode);
     65        }
     66    }
     67
     68    protected void completeTagCollectionWithMissingTags(TagCollection tc, Collection<Node> mergedNodes) {
     69        for (String key: tc.getKeys()) {
     70            // make sure the empty value is in the tag set if a tag is not present
     71            // on all merged nodes
     72            //
     73            for (Node n: mergedNodes) {
     74                if (n.get(key) == null) {
     75                    tc.add(new Tag(key)); // add a tag with key and empty value
     76                }
     77            }
     78        }
     79        // remove irrelevant tags
     80        //
     81        tc.removeByKey("created_by");
     82    }
     83
     84    protected void completeTagCollectionForEditing(TagCollection tc) {
     85        for (String key: tc.getKeys()) {
     86            // make sure the empty value is in the tag set such that we can delete the tag
     87            // in the conflict dialog if necessary
     88            //
     89            tc.add(new Tag(key,""));
     90        }
     91    }
     92
     93    /**
     94     * Selects a node out of a collection of candidate nodes. The selected
     95     * node will become the target node the remaining nodes are merged to.
     96     *
     97     * @param candidates the collection of candidate nodes
     98     * @return the selected target node
     99     */
     100    public Node selectTargetNode(Collection<Node> candidates) {
    84101        // Find which node to merge into (i.e. which one will be left)
    85102        // - this should be combined from two things:
     
    93110        // that the user doesn't know which node will be chosen (so
    94111        // (2) is not implemented yet.)  :-(
    95         Node useNode = null;
    96         for (Node n: selectedNodes) {
     112        Node targetNode = null;
     113        for (Node n: candidates) {
    97114            if (n.getId() > 0) {
    98                 useNode = n;
     115                targetNode = n;
    99116                break;
    100117            }
    101118        }
    102         if (useNode == null) {
    103             useNode = selectedNodes.iterator().next();
    104         }
    105 
    106         mergeNodes(selectedNodes, useNode);
    107     }
    108 
    109     /**
    110      * really do the merging - returns the node that is left
    111      */
    112     public Node mergeNodes(LinkedList<Node> allNodes, Node dest) {
    113         Node newNode = new Node(dest);
    114 
    115         // Check whether all ways have identical relationship membership. More
    116         // specifically: If one of the selected ways is a member of relation X
    117         // in role Y, then all selected ways must be members of X in role Y.
    118 
    119         // FIXME: In a later revision, we should display some sort of conflict
    120         // dialog like we do for tags, to let the user choose which relations
    121         // should be kept.
    122 
    123         // Step 1, iterate over all relations and figure out which of our
    124         // selected ways are members of a relation.
    125         HashMap<Pair<Relation,String>, HashSet<Node>> backlinks =
    126             new HashMap<Pair<Relation,String>, HashSet<Node>>();
    127         HashSet<Relation> relationsUsingNodes = new HashSet<Relation>();
    128         for (Relation r : getCurrentDataSet().relations) {
    129             if (r.isDeleted() || r.incomplete) {
    130                 continue;
    131             }
    132             for (RelationMember rm : r.getMembers()) {
    133                 if (rm.isNode()) {
    134                     for (Node n : allNodes) {
    135                         if (rm.getMember() == n) {
    136                             Pair<Relation,String> pair = new Pair<Relation,String>(r, rm.getRole());
    137                             HashSet<Node> nodelinks = new HashSet<Node>();
    138                             if (backlinks.containsKey(pair)) {
    139                                 nodelinks = backlinks.get(pair);
    140                             } else {
    141                                 nodelinks = new HashSet<Node>();
    142                                 backlinks.put(pair, nodelinks);
    143                             }
    144                             nodelinks.add(n);
    145 
    146                             // this is just a cache for later use
    147                             relationsUsingNodes.add(r);
    148                         }
    149                     }
     119        if (targetNode == null) {
     120            // an arbitrary node
     121            targetNode = candidates.iterator().next();
     122        }
     123        return targetNode;
     124    }
     125
     126
     127    /**
     128     * Merges the nodes in <code>node</code> onto one of the nodes. Uses the dataset
     129     * managed by <code>layer</code> as reference.
     130     *
     131     * @param layer the reference data layer. Must not be null.
     132     * @param nodes the collection of nodes. Ignored if null.
     133     * @param targetNode the target node the collection of nodes is merged to. Must not be null.
     134     * @throws IllegalArgumentException thrown if layer is null
     135     * @throws IllegalArgumentException thrown if targetNode is null
     136     *
     137     */
     138    public Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode) throws IllegalArgumentException{
     139        if (layer == null)
     140            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "nodes"));
     141        if (targetNode == null)
     142            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "targetNode"));
     143
     144        if (nodes == null)
     145            return null;
     146        nodes.remove(null); // just in case
     147        BackreferencedDataSet backreferences = new BackreferencedDataSet(layer.data);
     148        backreferences.build();
     149        return mergeNodes(layer,backreferences, nodes, targetNode);
     150    }
     151
     152    /**
     153     * Merges the nodes in <code>node</code> onto one of the nodes. Uses the dataset
     154     * managed by <code>layer</code> as reference. <code>backreferences</code> is precomputed
     155     * collection of all parent/child references in the dataset.
     156     *
     157     * @param layer layer the reference data layer. Must not be null.
     158     * @param backreferences if null, backreferneces are first computed from layer.data; otherwise
     159     *    backreferences.getSource() == layer.data must hold
     160     * @param nodes the collection of nodes. Ignored if null.
     161     * @param targetNode the target node the collection of nodes is merged to. Must not be null.
     162     * @throw IllegalArgumentException thrown if layer is null
     163     * @throw IllegalArgumentException thrown if  backreferences.getSource() != layer.data
     164     */
     165    public Command mergeNodes(OsmDataLayer layer, BackreferencedDataSet backreferences, Collection<Node> nodes, Node targetNode) {
     166        if (layer == null)
     167            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "nodes"));
     168        if (targetNode == null)
     169            throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "targetNode"));
     170        if (nodes == null)
     171            return null;
     172        if (backreferences == null) {
     173            backreferences = new BackreferencedDataSet(layer.data);
     174            backreferences.build();
     175        }
     176
     177        Set<RelationToChildReference> relationToNodeReferences = backreferences.getRelationToChildReferences(nodes);
     178
     179        // build the tag collection
     180        //
     181        TagCollection nodeTags = TagCollection.unionOfAllPrimitives(nodes);
     182        completeTagCollectionWithMissingTags(nodeTags, nodes);
     183        TagCollection nodeTagsToEdit = new TagCollection(nodeTags);
     184        completeTagCollectionForEditing(nodeTagsToEdit);
     185
     186        // launch a conflict resolution dialog, if necessary
     187        //
     188        CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
     189        dialog.getTagConflictResolverModel().populate(nodeTagsToEdit);
     190        dialog.getRelationMemberConflictResolverModel().populate(relationToNodeReferences);
     191        dialog.setTargetPrimitive(targetNode);
     192        dialog.prepareDefaultDecisions();
     193        if (! nodeTags.isApplicableToPrimitive() || relationToNodeReferences.size() > 1) {
     194            dialog.setVisible(true);
     195            if (dialog.isCancelled())
     196                return null;
     197        }
     198        LinkedList<Command> cmds = new LinkedList<Command>();
     199
     200        // the nodes we will have to delete
     201        //
     202        Collection<OsmPrimitive> nodesToDelete = new HashSet<OsmPrimitive>(nodes);
     203        nodesToDelete.remove(targetNode);
     204
     205        // change the ways referring to at least one of the merge nodes
     206        //
     207        Collection<Way> waysToDelete= new HashSet<Way>();
     208        for (Way w : OsmPrimitive.getFilteredList(backreferences.getParents(nodesToDelete), Way.class)) {
     209            // OK - this way contains one or more nodes to change
     210            ArrayList<Node> newNodes = new ArrayList<Node>(w.getNodesCount());
     211            for (Node n: w.getNodes()) {
     212                if (! nodesToDelete.contains(n)) {
     213                    newNodes.add(n);
    150214                }
    151215            }
    152         }
    153 
    154         // Complain to the user if the ways don't have equal memberships.
    155         for (HashSet<Node> nodelinks : backlinks.values()) {
    156             if (!nodelinks.containsAll(allNodes)) {
    157                 ExtendedDialog ed = new ExtendedDialog(Main.parent,
    158                         tr("Merge nodes with different memberships?"),
    159                         new String[] {tr("Merge Anyway"), tr("Cancel")});
    160                 ed.setButtonIcons(new String[] {"mergenodes.png", "cancel.png"});
    161                 ed.setContent(tr("The selected nodes have differing relation memberships.  "
    162                         + "Do you still want to merge them?"));
    163                 ed.showDialog();
    164 
    165                 if (ed.getValue() == 1) {
    166                     break;
    167                 }
    168                 return null;
    169             }
    170         }
    171 
    172         // collect properties for later conflict resolving
    173         Map<String, Set<String>> props = new TreeMap<String, Set<String>>();
    174         for (Node n : allNodes) {
    175             for (Entry<String,String> e : n.entrySet()) {
    176                 if (!props.containsKey(e.getKey())) {
    177                     props.put(e.getKey(), new TreeSet<String>());
    178                 }
    179                 props.get(e.getKey()).add(e.getValue());
    180             }
    181         }
    182 
    183         // display conflict dialog
    184         Map<String, JComboBox> components = new HashMap<String, JComboBox>();
    185         JPanel p = new JPanel(new GridBagLayout());
    186         for (Entry<String, Set<String>> e : props.entrySet()) {
    187             if (TigerUtils.isTigerTag(e.getKey())) {
    188                 String combined = TigerUtils.combineTags(e.getKey(), e.getValue());
    189                 newNode.put(e.getKey(), combined);
    190             } else if (e.getValue().size() > 1) {
    191                 JComboBox c = new JComboBox(e.getValue().toArray());
    192                 c.setEditable(true);
    193                 p.add(new JLabel(e.getKey()), GBC.std());
    194                 p.add(Box.createHorizontalStrut(10), GBC.std());
    195                 p.add(c, GBC.eol());
    196                 components.put(e.getKey(), c);
    197             } else {
    198                 newNode.put(e.getKey(), e.getValue().iterator().next());
    199             }
    200         }
    201 
    202         if (!components.isEmpty()) {
    203             ExtendedDialog dialog = new ExtendedDialog(
    204                     Main.parent,
    205                     tr("Enter values for all conflicts."),
    206                     new String[] {tr("Solve Conflicts"), tr("Cancel")}
    207             );
    208             dialog.setButtonIcons(new String[] {"dialogs/conflict.png", "cancel.png"});
    209             dialog.setContent(p);
    210             dialog.showDialog();
    211             int answer = dialog.getValue();
    212 
    213             if (answer != 1)
    214                 return null;
    215             for (Entry<String, JComboBox> e : components.entrySet()) {
    216                 newNode.put(e.getKey(), e.getValue().getEditor().getItem().toString());
    217             }
    218         }
    219 
    220         LinkedList<Command> cmds = new LinkedList<Command>();
    221 
    222         if (!newNode.getKeys().equals(dest.getKeys())) {
    223             cmds.add(new ChangeCommand(dest, newNode));
    224         }
    225 
    226         Collection<OsmPrimitive> del = new HashSet<OsmPrimitive>();
    227 
    228         for (Way w : getCurrentDataSet().ways) {
    229             if (w.isDeleted() || w.incomplete || w.getNodesCount() < 1) {
    230                 continue;
    231             }
    232             boolean modify = false;
    233             for (Node sn : allNodes) {
    234                 if (sn == dest) {
    235                     continue;
    236                 }
    237                 if (w.containsNode(sn)) {
    238                     modify = true;
    239                 }
    240             }
    241             if (!modify) {
    242                 continue;
    243             }
    244             // OK - this way contains one or more nodes to change
    245             ArrayList<Node> nn = new ArrayList<Node>();
    246             Node lastNode = null;
    247             for (Node pushNode: w.getNodes()) {
    248                 if (allNodes.contains(pushNode)) {
    249                     pushNode = dest;
    250                 }
    251                 if (pushNode != lastNode) {
    252                     nn.add(pushNode);
    253                 }
    254                 lastNode = pushNode;
    255             }
    256             if (nn.size() < 2) {
    257                 CollectBackReferencesVisitor backRefs =
    258                     new CollectBackReferencesVisitor(getCurrentDataSet(), false);
    259                 w.visit(backRefs);
    260                 if (!backRefs.data.isEmpty()) {
     216            if (newNodes.size() < 2) {
     217                if (backreferences.getParents(w).isEmpty()) {
     218                    waysToDelete.add(w);
     219                } else {
    261220                    JOptionPane.showMessageDialog(
    262221                            Main.parent,
     
    268227                    return null;
    269228                }
    270                 del.add(w);
     229            } else if(newNodes.size() < 2 && backreferences.getParents(w).isEmpty()) {
     230                waysToDelete.add(w);
    271231            } else {
    272232                Way newWay = new Way(w);
    273                 newWay.setNodes(nn);
     233                newWay.setNodes(newNodes);
    274234                cmds.add(new ChangeCommand(w, newWay));
    275235            }
    276236        }
    277237
    278         // delete any merged nodes
    279         del.addAll(allNodes);
    280         del.remove(dest);
    281         if (!del.isEmpty()) {
    282             cmds.add(new DeleteCommand(del));
    283         }
    284 
    285         // modify all relations containing the now-deleted nodes
    286         for (Relation r : relationsUsingNodes) {
    287             List<RelationMember> newMembers = new ArrayList<RelationMember>();
    288             HashSet<String> rolesToReAdd = new HashSet<String>();
    289             for (RelationMember rm : r.getMembers()) {
    290                 // Don't copy the member if it points to one of our nodes,
    291                 // just keep a note to re-add it later on.
    292                 if (allNodes.contains(rm.getMember())) {
    293                     rolesToReAdd.add(rm.getRole());
    294                 } else {
    295                     newMembers.add(rm);
    296                 }
    297             }
    298             for (String role : rolesToReAdd) {
    299                 newMembers.add(new RelationMember(role, dest));
    300             }
    301             Relation newRel = new Relation(r);
    302             newRel.setMembers(newMembers);
    303             cmds.add(new ChangeCommand(r, newRel));
    304         }
    305 
    306         Main.main.undoRedo.add(new SequenceCommand(tr("Merge {0} nodes", allNodes.size()), cmds));
    307         getCurrentDataSet().setSelected(dest);
    308 
    309         return dest;
     238        // build the commands
     239        //
     240        if (!nodesToDelete.isEmpty()) {
     241            cmds.add(new DeleteCommand(nodesToDelete));
     242        }
     243        if (!waysToDelete.isEmpty()) {
     244            cmds.add(new DeleteCommand(waysToDelete));
     245        }
     246        cmds.addAll(dialog.buildResolutionCommands());
     247        Command cmd = new SequenceCommand(tr("Merge {0} nodes", nodes.size()), cmds);
     248        return cmd;
    310249    }
    311250
  • trunk/src/org/openstreetmap/josm/actions/mapmode/SelectAction.java

    r2031 r2095  
    1717import java.util.LinkedList;
    1818import java.util.List;
     19import java.util.Set;
    1920import java.util.TreeSet;
    2021import java.util.logging.Logger;
     
    4344import org.openstreetmap.josm.gui.SelectionManager;
    4445import org.openstreetmap.josm.gui.SelectionManager.SelectionEnded;
     46import org.openstreetmap.josm.gui.dialogs.LayerListDialog.MergeAction;
    4547import org.openstreetmap.josm.gui.layer.Layer;
    4648import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     
    489491                    if (nn != null) {
    490492                        Node n = nn.iterator().next();
    491                         LinkedList<Node> selNodes = new LinkedList<Node>();
    492                         for (OsmPrimitive osm : selection)
    493                             if (osm instanceof Node) {
    494                                 selNodes.add((Node)osm);
    495                             }
    496                         if (selNodes.size() > 0) {
    497                             selNodes.add(n);
    498                             new MergeNodesAction().mergeNodes(selNodes, n);
     493                        Set<Node> selectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class);
     494                        if (!selectedNodes.isEmpty()) {
     495                            selectedNodes.add(n);
     496                            MergeNodesAction mergeAction = new MergeNodesAction();
     497                            Node targetNode = mergeAction.selectTargetNode(selectedNodes);
     498                            mergeAction.mergeNodes(Main.main.getEditLayer(),selectedNodes, targetNode);
    499499                        }
    500500                    }
Note: See TracChangeset for help on using the changeset viewer.