001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.dialogs.relation;
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.BorderLayout;
009    import java.awt.Dimension;
010    import java.awt.FlowLayout;
011    import java.awt.GridBagConstraints;
012    import java.awt.GridBagLayout;
013    import java.awt.event.ActionEvent;
014    import java.awt.event.FocusAdapter;
015    import java.awt.event.FocusEvent;
016    import java.awt.event.KeyEvent;
017    import java.awt.event.MouseAdapter;
018    import java.awt.event.MouseEvent;
019    import java.awt.event.WindowAdapter;
020    import java.awt.event.WindowEvent;
021    import java.beans.PropertyChangeEvent;
022    import java.beans.PropertyChangeListener;
023    import java.util.ArrayList;
024    import java.util.Collection;
025    import java.util.Collections;
026    import java.util.HashMap;
027    import java.util.HashSet;
028    import java.util.Iterator;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.Set;
032    
033    import javax.swing.*;
034    import javax.swing.event.ChangeEvent;
035    import javax.swing.event.ChangeListener;
036    import javax.swing.event.DocumentEvent;
037    import javax.swing.event.DocumentListener;
038    import javax.swing.event.ListSelectionEvent;
039    import javax.swing.event.ListSelectionListener;
040    import javax.swing.event.TableModelEvent;
041    import javax.swing.event.TableModelListener;
042    
043    import org.openstreetmap.josm.Main;
044    import org.openstreetmap.josm.actions.CopyAction;
045    import org.openstreetmap.josm.actions.JosmAction;
046    import org.openstreetmap.josm.actions.PasteTagsAction.TagPaster;
047    import org.openstreetmap.josm.command.AddCommand;
048    import org.openstreetmap.josm.command.ChangeCommand;
049    import org.openstreetmap.josm.command.Command;
050    import org.openstreetmap.josm.command.ConflictAddCommand;
051    import org.openstreetmap.josm.data.conflict.Conflict;
052    import org.openstreetmap.josm.data.osm.DataSet;
053    import org.openstreetmap.josm.data.osm.OsmPrimitive;
054    import org.openstreetmap.josm.data.osm.PrimitiveData;
055    import org.openstreetmap.josm.data.osm.Relation;
056    import org.openstreetmap.josm.data.osm.RelationMember;
057    import org.openstreetmap.josm.data.osm.Tag;
058    import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
059    import org.openstreetmap.josm.gui.DefaultNameFormatter;
060    import org.openstreetmap.josm.gui.HelpAwareOptionPane;
061    import org.openstreetmap.josm.gui.MainMenu;
062    import org.openstreetmap.josm.gui.SideButton;
063    import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
064    import org.openstreetmap.josm.gui.dialogs.properties.PresetListPanel.PresetHandler;
065    import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
066    import org.openstreetmap.josm.gui.help.HelpUtil;
067    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
068    import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
069    import org.openstreetmap.josm.gui.tagging.TagModel;
070    import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
071    import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
072    import org.openstreetmap.josm.tools.ImageProvider;
073    import org.openstreetmap.josm.tools.Shortcut;
074    import org.openstreetmap.josm.tools.WindowGeometry;
075    
076    /**
077     * This dialog is for editing relations.
078     *
079     */
080    public class GenericRelationEditor extends RelationEditor  {
081        /** the tag table and its model */
082        private TagEditorPanel tagEditorPanel;
083        private ReferringRelationsBrowser referrerBrowser;
084        private ReferringRelationsBrowserModel referrerModel;
085    
086        /** the member table */
087        private MemberTable memberTable;
088        private MemberTableModel memberTableModel;
089    
090        /** the model for the selection table */
091        private SelectionTable selectionTable;
092        private SelectionTableModel selectionTableModel;
093    
094        private AutoCompletingTextField tfRole;
095    
096        /** the menu item in the windows menu. Required to properly
097         * hide on dialog close.
098         */
099        private JMenuItem windowMenuItem;
100    
101        /**
102         * Creates a new relation editor for the given relation. The relation will be saved if the user
103         * selects "ok" in the editor.
104         *
105         * If no relation is given, will create an editor for a new relation.
106         *
107         * @param layer the {@link OsmDataLayer} the new or edited relation belongs to
108         * @param relation relation to edit, or null to create a new one.
109         * @param selectedMembers a collection of members which shall be selected initially
110         */
111        public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) {
112            super(layer, relation, selectedMembers);
113    
114            setRememberWindowGeometry(getClass().getName() + ".geometry",
115                    WindowGeometry.centerInWindow(Main.parent, new Dimension(700, 650)));
116    
117            // init the various models
118            //
119            memberTableModel = new MemberTableModel(getLayer());
120            memberTableModel.register();
121            selectionTableModel = new SelectionTableModel(getLayer());
122            selectionTableModel.register();
123            referrerModel = new ReferringRelationsBrowserModel(relation);
124    
125            tagEditorPanel = new TagEditorPanel(new PresetHandler() {
126    
127                @Override
128                public void updateTags(List<Tag> tags) {
129                    GenericRelationEditor.this.updateTags(tags);
130                }
131    
132                @Override
133                public Collection<OsmPrimitive> getSelection() {
134                    Relation relation = new Relation();
135                    tagEditorPanel.getModel().applyToPrimitive(relation);
136                    return Collections.<OsmPrimitive>singletonList(relation);
137                }
138            });
139    
140            // populate the models
141            //
142            if (relation != null) {
143                tagEditorPanel.getModel().initFromPrimitive(relation);
144                this.memberTableModel.populate(relation);
145                if (!getLayer().data.getRelations().contains(relation)) {
146                    // treat it as a new relation if it doesn't exist in the
147                    // data set yet.
148                    setRelation(null);
149                }
150            } else {
151                tagEditorPanel.getModel().clear();
152                this.memberTableModel.populate(null);
153            }
154            tagEditorPanel.getModel().ensureOneTag();
155    
156            JSplitPane pane = buildSplitPane();
157            pane.setPreferredSize(new Dimension(100, 100));
158    
159            JPanel pnl = new JPanel();
160            pnl.setLayout(new BorderLayout());
161            pnl.add(pane, BorderLayout.CENTER);
162            pnl.setBorder(BorderFactory.createRaisedBevelBorder());
163    
164            getContentPane().setLayout(new BorderLayout());
165            JTabbedPane tabbedPane = new JTabbedPane();
166            tabbedPane.add(tr("Tags and Members"), pnl);
167            referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel, this);
168            tabbedPane.add(tr("Parent Relations"), referrerBrowser);
169            tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation));
170            tabbedPane.addChangeListener(
171                    new ChangeListener() {
172                        public void stateChanged(ChangeEvent e) {
173                            JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource();
174                            int index = sourceTabbedPane.getSelectedIndex();
175                            String title = sourceTabbedPane.getTitleAt(index);
176                            if (title.equals(tr("Parent Relations"))) {
177                                referrerBrowser.init();
178                            }
179                        }
180                    }
181            );
182    
183            getContentPane().add(buildToolBar(), BorderLayout.NORTH);
184            getContentPane().add(tabbedPane, BorderLayout.CENTER);
185            getContentPane().add(buildOkCancelButtonPanel(), BorderLayout.SOUTH);
186    
187            setSize(findMaxDialogSize());
188    
189            addWindowListener(
190                    new WindowAdapter() {
191                        @Override
192                        public void windowOpened(WindowEvent e) {
193                            cleanSelfReferences();
194                        }
195                    }
196            );
197    
198            memberTableModel.setSelectedMembers(selectedMembers);
199            HelpUtil.setHelpContext(getRootPane(),ht("/Dialog/RelationEditor"));
200        }
201    
202        /**
203         * Creates the toolbar
204         *
205         * @return the toolbar
206         */
207        protected JToolBar buildToolBar() {
208            JToolBar tb  = new JToolBar();
209            tb.setFloatable(false);
210            tb.add(new ApplyAction());
211            tb.add(new DuplicateRelationAction());
212            DeleteCurrentRelationAction deleteAction = new DeleteCurrentRelationAction();
213            addPropertyChangeListener(deleteAction);
214            tb.add(deleteAction);
215            return tb;
216        }
217    
218        /**
219         * builds the panel with the OK and the Cancel button
220         *
221         * @return the panel with the OK and the Cancel button
222         */
223        protected JPanel buildOkCancelButtonPanel() {
224            JPanel pnl = new JPanel();
225            pnl.setLayout(new FlowLayout(FlowLayout.CENTER));
226    
227            pnl.add(new SideButton(new OKAction()));
228            pnl.add(new SideButton(new CancelAction()));
229            pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor"))));
230            return pnl;
231        }
232    
233        /**
234         * builds the panel with the tag editor
235         *
236         * @return the panel with the tag editor
237         */
238        protected JPanel buildTagEditorPanel() {
239            JPanel pnl = new JPanel();
240            pnl.setLayout(new GridBagLayout());
241    
242            GridBagConstraints gc = new GridBagConstraints();
243            gc.gridx = 0;
244            gc.gridy = 0;
245            gc.gridheight = 1;
246            gc.gridwidth = 1;
247            gc.fill = GridBagConstraints.HORIZONTAL;
248            gc.anchor = GridBagConstraints.FIRST_LINE_START;
249            gc.weightx = 1.0;
250            gc.weighty = 0.0;
251            pnl.add(new JLabel(tr("Tags")), gc);
252    
253            gc.gridx = 0;
254            gc.gridy = 1;
255            gc.fill = GridBagConstraints.BOTH;
256            gc.anchor = GridBagConstraints.CENTER;
257            gc.weightx = 1.0;
258            gc.weighty = 1.0;
259            pnl.add(tagEditorPanel, gc);
260            return pnl;
261        }
262    
263        /**
264         * builds the panel for the relation member editor
265         *
266         * @return the panel for the relation member editor
267         */
268        protected JPanel buildMemberEditorPanel() {
269            final JPanel pnl = new JPanel();
270            pnl.setLayout(new GridBagLayout());
271            // setting up the member table
272            memberTable = new MemberTable(getLayer(),memberTableModel);
273            memberTable.addMouseListener(new MemberTableDblClickAdapter());
274            memberTableModel.addMemberModelListener(memberTable);
275    
276            final JScrollPane scrollPane = new JScrollPane(memberTable);
277    
278            GridBagConstraints gc = new GridBagConstraints();
279            gc.gridx = 0;
280            gc.gridy = 0;
281            gc.gridheight = 1;
282            gc.gridwidth = 3;
283            gc.fill = GridBagConstraints.HORIZONTAL;
284            gc.anchor = GridBagConstraints.FIRST_LINE_START;
285            gc.weightx = 1.0;
286            gc.weighty = 0.0;
287            pnl.add(new JLabel(tr("Members")), gc);
288    
289            gc.gridx = 0;
290            gc.gridy = 1;
291            gc.gridheight = 1;
292            gc.gridwidth = 1;
293            gc.fill = GridBagConstraints.VERTICAL;
294            gc.anchor = GridBagConstraints.NORTHWEST;
295            gc.weightx = 0.0;
296            gc.weighty = 1.0;
297            pnl.add(buildLeftButtonPanel(), gc);
298    
299            gc.gridx = 1;
300            gc.gridy = 1;
301            gc.fill = GridBagConstraints.BOTH;
302            gc.anchor = GridBagConstraints.CENTER;
303            gc.weightx = 0.6;
304            gc.weighty = 1.0;
305            pnl.add(scrollPane, gc);
306    
307            // --- role editing
308            JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT));
309            p3.add(new JLabel(tr("Apply Role:")));
310            tfRole = new AutoCompletingTextField(10);
311            tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members"));
312            tfRole.addFocusListener(new FocusAdapter() {
313                @Override
314                public void focusGained(FocusEvent e) {
315                    tfRole.selectAll();
316                }
317            });
318            tfRole.setAutoCompletionList(new AutoCompletionList());
319            tfRole.addFocusListener(
320                    new FocusAdapter() {
321                        @Override
322                        public void focusGained(FocusEvent e) {
323                            AutoCompletionList list = tfRole.getAutoCompletionList();
324                            if (list != null) {
325                                list.clear();
326                                getLayer().data.getAutoCompletionManager().populateWithMemberRoles(list);
327                            }
328                        }
329                    }
330            );
331            tfRole.setText(Main.pref.get("relation.editor.generic.lastrole", ""));
332            p3.add(tfRole);
333            SetRoleAction setRoleAction = new SetRoleAction();
334            memberTableModel.getSelectionModel().addListSelectionListener(setRoleAction);
335            tfRole.getDocument().addDocumentListener(setRoleAction);
336            tfRole.addActionListener(setRoleAction);
337            memberTableModel.getSelectionModel().addListSelectionListener(
338                    new ListSelectionListener() {
339                        public void valueChanged(ListSelectionEvent e) {
340                            tfRole.setEnabled(memberTable.getSelectedRowCount() > 0);
341                        }
342                    }
343            );
344            tfRole.setEnabled(memberTable.getSelectedRowCount() > 0);
345            SideButton btnApply = new SideButton(setRoleAction);
346            btnApply.setPreferredSize(new Dimension(20,20));
347            btnApply.setText("");
348            p3.add(btnApply);
349    
350            gc.gridx = 1;
351            gc.gridy = 2;
352            gc.fill = GridBagConstraints.BOTH;
353            gc.anchor = GridBagConstraints.CENTER;
354            gc.weightx = 1.0;
355            gc.weighty = 0.0;
356            pnl.add(p3, gc);
357    
358            JPanel pnl2 = new JPanel();
359            pnl2.setLayout(new GridBagLayout());
360    
361            gc.gridx = 0;
362            gc.gridy = 0;
363            gc.gridheight = 1;
364            gc.gridwidth = 3;
365            gc.fill = GridBagConstraints.HORIZONTAL;
366            gc.anchor = GridBagConstraints.FIRST_LINE_START;
367            gc.weightx = 1.0;
368            gc.weighty = 0.0;
369            pnl2.add(new JLabel(tr("Selection")), gc);
370    
371            gc.gridx = 0;
372            gc.gridy = 1;
373            gc.gridheight = 1;
374            gc.gridwidth = 1;
375            gc.fill = GridBagConstraints.VERTICAL;
376            gc.anchor = GridBagConstraints.NORTHWEST;
377            gc.weightx = 0.0;
378            gc.weighty = 1.0;
379            pnl2.add(buildSelectionControlButtonPanel(), gc);
380    
381            gc.gridx = 1;
382            gc.gridy = 1;
383            gc.weightx = 1.0;
384            gc.weighty = 1.0;
385            gc.fill = GridBagConstraints.BOTH;
386            pnl2.add(buildSelectionTablePanel(), gc);
387    
388            final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
389            splitPane.setLeftComponent(pnl);
390            splitPane.setRightComponent(pnl2);
391            splitPane.setOneTouchExpandable(false);
392            addWindowListener(new WindowAdapter() {
393                @Override
394                public void windowOpened(WindowEvent e) {
395                    // has to be called when the window is visible, otherwise
396                    // no effect
397                    splitPane.setDividerLocation(0.6);
398                }
399            });
400    
401            JPanel pnl3 = new JPanel();
402            pnl3.setLayout(new BorderLayout());
403            pnl3.add(splitPane, BorderLayout.CENTER);
404    
405            new PasteMembersAction();
406            new CopyMembersAction();
407            new PasteTagsAction();
408    
409            return pnl3;
410        }
411    
412        /**
413         * builds the panel with the table displaying the currently selected primitives
414         *
415         * @return
416         */
417        protected JPanel buildSelectionTablePanel() {
418            JPanel pnl = new JPanel();
419            pnl.setLayout(new BorderLayout());
420            selectionTable = new SelectionTable(selectionTableModel, new SelectionTableColumnModel(memberTableModel));
421            selectionTable.setMemberTableModel(memberTableModel);
422            selectionTable.setRowHeight(tfRole.getPreferredSize().height);
423            JScrollPane pane = new JScrollPane(selectionTable);
424            pnl.add(pane, BorderLayout.CENTER);
425            return pnl;
426        }
427    
428        /**
429         * builds the {@link JSplitPane} which divides the editor in an upper and a lower half
430         *
431         * @return the split panel
432         */
433        protected JSplitPane buildSplitPane() {
434            final JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
435            pane.setTopComponent(buildTagEditorPanel());
436            pane.setBottomComponent(buildMemberEditorPanel());
437            pane.setOneTouchExpandable(true);
438            addWindowListener(new WindowAdapter() {
439                @Override
440                public void windowOpened(WindowEvent e) {
441                    // has to be called when the window is visible, otherwise
442                    // no effect
443                    pane.setDividerLocation(0.3);
444                }
445            });
446            return pane;
447        }
448    
449        /**
450         * build the panel with the buttons on the left
451         *
452         * @return
453         */
454        protected JToolBar buildLeftButtonPanel() {
455            JToolBar tb = new JToolBar();
456            tb.setOrientation(JToolBar.VERTICAL);
457            tb.setFloatable(false);
458    
459            // -- move up action
460            MoveUpAction moveUpAction = new MoveUpAction();
461            memberTableModel.getSelectionModel().addListSelectionListener(moveUpAction);
462            tb.add(moveUpAction);
463            memberTable.getActionMap().put("moveUp", moveUpAction);
464    
465            // -- move down action
466            MoveDownAction moveDownAction = new MoveDownAction();
467            memberTableModel.getSelectionModel().addListSelectionListener(moveDownAction);
468            tb.add(moveDownAction);
469            memberTable.getActionMap().put("moveDown", moveDownAction);
470            
471            tb.addSeparator();
472    
473            // -- edit action
474            EditAction editAction = new EditAction();
475            memberTableModel.getSelectionModel().addListSelectionListener(editAction);
476            tb.add(editAction);
477            
478            // -- delete action
479            RemoveAction removeSelectedAction = new RemoveAction();
480            memberTable.getSelectionModel().addListSelectionListener(removeSelectedAction);
481            tb.add(removeSelectedAction);
482            memberTable.getActionMap().put("removeSelected", removeSelectedAction);
483            
484            tb.addSeparator();
485            // -- sort action
486            SortAction sortAction = new SortAction();
487            memberTableModel.addTableModelListener(sortAction);
488            tb.add(sortAction);
489    
490            // -- reverse action
491            ReverseAction reverseAction = new ReverseAction();
492            memberTableModel.addTableModelListener(reverseAction);
493            tb.add(reverseAction);
494    
495            tb.addSeparator();
496    
497            // -- download action
498            DownloadIncompleteMembersAction downloadIncompleteMembersAction = new DownloadIncompleteMembersAction();
499            memberTable.getModel().addTableModelListener(downloadIncompleteMembersAction);
500            tb.add(downloadIncompleteMembersAction);
501            memberTable.getActionMap().put("downloadIncomplete", downloadIncompleteMembersAction);
502    
503            // -- download selected action
504            DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction = new DownloadSelectedIncompleteMembersAction();
505            memberTable.getModel().addTableModelListener(downloadSelectedIncompleteMembersAction);
506            memberTable.getSelectionModel().addListSelectionListener(downloadSelectedIncompleteMembersAction);
507            tb.add(downloadSelectedIncompleteMembersAction);
508    
509            InputMap inputMap = memberTable.getInputMap(MemberTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
510            inputMap.put((KeyStroke) removeSelectedAction.getValue(AbstractAction.ACCELERATOR_KEY),"removeSelected");
511            inputMap.put((KeyStroke) moveUpAction.getValue(AbstractAction.ACCELERATOR_KEY),"moveUp");
512            inputMap.put((KeyStroke) moveDownAction.getValue(AbstractAction.ACCELERATOR_KEY),"moveDown");
513            inputMap.put((KeyStroke) downloadIncompleteMembersAction.getValue(AbstractAction.ACCELERATOR_KEY),"downloadIncomplete");
514            
515            return tb;
516        }
517    
518        /**
519         * build the panel with the buttons for adding or removing the current selection
520         *
521         * @return
522         */
523        protected JToolBar buildSelectionControlButtonPanel() {
524            JToolBar tb = new JToolBar(JToolBar.VERTICAL);
525            tb.setFloatable(false);
526    
527            // -- add at start action
528            AddSelectedAtStartAction addSelectionAction = new AddSelectedAtStartAction();
529            selectionTableModel.addTableModelListener(addSelectionAction);
530            tb.add(addSelectionAction);
531    
532            // -- add before selected action
533            AddSelectedBeforeSelection addSelectedBeforeSelectionAction = new AddSelectedBeforeSelection();
534            selectionTableModel.addTableModelListener(addSelectedBeforeSelectionAction);
535            memberTableModel.getSelectionModel().addListSelectionListener(addSelectedBeforeSelectionAction);
536            tb.add(addSelectedBeforeSelectionAction);
537    
538            // -- add after selected action
539            AddSelectedAfterSelection addSelectedAfterSelectionAction = new AddSelectedAfterSelection();
540            selectionTableModel.addTableModelListener(addSelectedAfterSelectionAction);
541            memberTableModel.getSelectionModel().addListSelectionListener(addSelectedAfterSelectionAction);
542            tb.add(addSelectedAfterSelectionAction);
543    
544            // -- add at end action
545            AddSelectedAtEndAction addSelectedAtEndAction = new AddSelectedAtEndAction();
546            selectionTableModel.addTableModelListener(addSelectedAtEndAction);
547            tb.add(addSelectedAtEndAction);
548    
549            tb.addSeparator();
550    
551            // -- select members action
552            SelectedMembersForSelectionAction selectMembersForSelectionAction = new SelectedMembersForSelectionAction();
553            selectionTableModel.addTableModelListener(selectMembersForSelectionAction);
554            memberTableModel.addTableModelListener(selectMembersForSelectionAction);
555            tb.add(selectMembersForSelectionAction);
556    
557            // -- select action
558            SelectPrimitivesForSelectedMembersAction selectAction = new SelectPrimitivesForSelectedMembersAction();
559            memberTable.getSelectionModel().addListSelectionListener(selectAction);
560            tb.add(selectAction);
561    
562            tb.addSeparator();
563    
564            // -- remove selected action
565            RemoveSelectedAction removeSelectedAction = new RemoveSelectedAction();
566            selectionTableModel.addTableModelListener(removeSelectedAction);
567            tb.add(removeSelectedAction);
568    
569            return tb;
570        }
571    
572        @Override
573        protected Dimension findMaxDialogSize() {
574            return new Dimension(700, 650);
575        }
576    
577        @Override
578        public void setVisible(boolean visible) {
579            if (visible) {
580                tagEditorPanel.initAutoCompletion(getLayer());
581            }
582            super.setVisible(visible);
583            if (visible) {
584                RelationDialogManager.getRelationDialogManager().positionOnScreen(this);
585                if(windowMenuItem == null) {
586                    addToWindowMenu();
587                }
588            } else {
589                // make sure all registered listeners are unregistered
590                //
591                selectionTableModel.unregister();
592                memberTableModel.unregister();
593                memberTable.unlinkAsListener();
594                if(windowMenuItem != null) {
595                    Main.main.menu.windowMenu.remove(windowMenuItem);
596                    windowMenuItem = null;
597                }
598                dispose();
599            }
600        }
601    
602        /** adds current relation editor to the windows menu (in the "volatile" group) o*/
603        protected void addToWindowMenu() {
604            String name = getRelation() == null ? tr("New Relation") : getRelation().getLocalName();
605            final String tt = tr("Focus Relation Editor with relation ''{0}'' in layer ''{1}''",
606                    name, getLayer().getName());
607            name = tr("Relation Editor: {0}", name == null ? getRelation().getId() : name);
608            final JMenu wm = Main.main.menu.windowMenu;
609            final JosmAction focusAction = new JosmAction(name, "dialogs/relationlist", tt, null, false, false) {
610                @Override
611                public void actionPerformed(ActionEvent e) {
612                    final RelationEditor r = (RelationEditor) getValue("relationEditor");
613                    r.setVisible(true);
614                }
615            };
616            focusAction.putValue("relationEditor", this);
617            windowMenuItem = MainMenu.add(wm, focusAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
618        }
619    
620        /**
621         * checks whether the current relation has members referring to itself. If so,
622         * warns the users and provides an option for removing these members.
623         *
624         */
625        protected void cleanSelfReferences() {
626            ArrayList<OsmPrimitive> toCheck = new ArrayList<OsmPrimitive>();
627            toCheck.add(getRelation());
628            if (memberTableModel.hasMembersReferringTo(toCheck)) {
629                int ret = ConditionalOptionPaneUtil.showOptionDialog(
630                        "clean_relation_self_references",
631                        Main.parent,
632                        tr("<html>There is at least one member in this relation referring<br>"
633                                + "to the relation itself.<br>"
634                                + "This creates circular dependencies and is discouraged.<br>"
635                                + "How do you want to proceed with circular dependencies?</html>"),
636                                tr("Warning"),
637                                JOptionPane.YES_NO_OPTION,
638                                JOptionPane.WARNING_MESSAGE,
639                                new String[]{tr("Remove them, clean up relation"), tr("Ignore them, leave relation as is")},
640                                tr("Remove them, clean up relation")
641                );
642                switch(ret) {
643                case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: return;
644                case JOptionPane.CLOSED_OPTION: return;
645                case JOptionPane.NO_OPTION: return;
646                case JOptionPane.YES_OPTION:
647                    memberTableModel.removeMembersReferringTo(toCheck);
648                    break;
649                }
650            }
651        }
652    
653        private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut) {
654            getRootPane().getActionMap().put(actionName, action);
655            getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
656            // Assign also to JTables because they have their own Copy&Paste implementation (which is disabled in this case but eats key shortcuts anyway)
657            memberTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
658            memberTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
659            memberTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
660            selectionTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
661            selectionTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
662            selectionTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
663        }
664    
665        protected void updateTags(List<Tag> tags) {
666    
667            if (tags.isEmpty())
668                return;
669    
670            Map<String, TagModel> modelTags = new HashMap<String, TagModel>();
671            for (int i=0; i<tagEditorPanel.getModel().getRowCount(); i++) {
672                TagModel tagModel = tagEditorPanel.getModel().get(i);
673                modelTags.put(tagModel.getName(), tagModel);
674            }
675            for (Tag tag: tags) {
676                TagModel existing = modelTags.get(tag.getKey());
677    
678                if (tag.getValue().isEmpty()) {
679                    if (existing != null) {
680                        tagEditorPanel.getModel().delete(tag.getKey());
681                    }
682                } else {
683                    if (existing != null) {
684                        tagEditorPanel.getModel().updateTagValue(existing, tag.getValue());
685                    } else {
686                        tagEditorPanel.getModel().add(tag.getKey(), tag.getValue());
687                    }
688                }
689    
690            }
691        }
692    
693        static class AddAbortException extends Exception {
694        }
695    
696        static boolean confirmAddingPrimtive(OsmPrimitive primitive) throws AddAbortException {
697            String msg = tr("<html>This relation already has one or more members referring to<br>"
698                    + "the object ''{0}''<br>"
699                    + "<br>"
700                    + "Do you really want to add another relation member?</html>",
701                    primitive.getDisplayName(DefaultNameFormatter.getInstance())
702                );
703            int ret = ConditionalOptionPaneUtil.showOptionDialog(
704                    "add_primitive_to_relation",
705                    Main.parent,
706                    msg,
707                    tr("Multiple members referring to same object."),
708                    JOptionPane.YES_NO_CANCEL_OPTION,
709                    JOptionPane.WARNING_MESSAGE,
710                    null,
711                    null
712            );
713            switch(ret) {
714            case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION : return true;
715            case JOptionPane.YES_OPTION: return true;
716            case JOptionPane.NO_OPTION: return false;
717            case JOptionPane.CLOSED_OPTION: return false;
718            case JOptionPane.CANCEL_OPTION: throw new AddAbortException();
719            }
720            // should not happen
721            return false;
722        }
723    
724        static void warnOfCircularReferences(OsmPrimitive primitive) {
725            String msg = tr("<html>You are trying to add a relation to itself.<br>"
726                    + "<br>"
727                    + "This creates circular references and is therefore discouraged.<br>"
728                    + "Skipping relation ''{0}''.</html>",
729                    primitive.getDisplayName(DefaultNameFormatter.getInstance()));
730            JOptionPane.showMessageDialog(
731                    Main.parent,
732                    msg,
733                    tr("Warning"),
734                    JOptionPane.WARNING_MESSAGE);
735        }
736    
737        public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) {
738            try {
739                Relation relation = new Relation(orig);
740                boolean modified = false;
741                for (OsmPrimitive p : primitivesToAdd) {
742                    if (p instanceof Relation && orig != null && orig.equals(p)) {
743                        warnOfCircularReferences(p);
744                        continue;
745                    } else if (MemberTableModel.hasMembersReferringTo(relation.getMembers(), Collections.singleton(p))
746                            && !confirmAddingPrimtive(p)) {
747                        continue;
748                    }
749                    relation.addMember(new RelationMember("", p));
750                    modified = true;
751                }
752                return modified ? new ChangeCommand(orig, relation) : null;
753            } catch (AddAbortException ign) {
754                return null;
755            }
756        }
757    
758        abstract class AddFromSelectionAction extends AbstractAction {
759            protected boolean isPotentialDuplicate(OsmPrimitive primitive) {
760                return memberTableModel.hasMembersReferringTo(Collections.singleton(primitive));
761            }
762    
763            protected List<OsmPrimitive> filterConfirmedPrimitives(List<OsmPrimitive> primitives) throws AddAbortException {
764                if (primitives == null || primitives.isEmpty())
765                    return primitives;
766                ArrayList<OsmPrimitive> ret = new ArrayList<OsmPrimitive>();
767                Iterator<OsmPrimitive> it = primitives.iterator();
768                while(it.hasNext()) {
769                    OsmPrimitive primitive = it.next();
770                    if (primitive instanceof Relation && getRelation() != null && getRelation().equals(primitive)) {
771                        warnOfCircularReferences(primitive);
772                        continue;
773                    }
774                    if (isPotentialDuplicate(primitive))  {
775                        if (confirmAddingPrimtive(primitive)) {
776                            ret.add(primitive);
777                        }
778                        continue;
779                    } else {
780                        ret.add(primitive);
781                    }
782                }
783                return ret;
784            }
785        }
786    
787        class AddSelectedAtStartAction extends AddFromSelectionAction implements TableModelListener {
788            public AddSelectedAtStartAction() {
789                putValue(SHORT_DESCRIPTION,
790                        tr("Add all objects selected in the current dataset before the first member"));
791                putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copystartright"));
792                // putValue(NAME, tr("Add Selected"));
793                refreshEnabled();
794            }
795    
796            protected void refreshEnabled() {
797                setEnabled(selectionTableModel.getRowCount() > 0);
798            }
799    
800            public void actionPerformed(ActionEvent e) {
801                try {
802                    List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
803                    memberTableModel.addMembersAtBeginning(toAdd);
804                } catch(AddAbortException ex) {
805                    // do nothing
806                }
807            }
808    
809            public void tableChanged(TableModelEvent e) {
810                refreshEnabled();
811            }
812        }
813    
814        class AddSelectedAtEndAction extends AddFromSelectionAction implements TableModelListener {
815            public AddSelectedAtEndAction() {
816                putValue(SHORT_DESCRIPTION, tr("Add all objects selected in the current dataset after the last member"));
817                putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyendright"));
818                // putValue(NAME, tr("Add Selected"));
819                refreshEnabled();
820            }
821    
822            protected void refreshEnabled() {
823                setEnabled(selectionTableModel.getRowCount() > 0);
824            }
825    
826            public void actionPerformed(ActionEvent e) {
827                try {
828                    List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
829                    memberTableModel.addMembersAtEnd(toAdd);
830                } catch(AddAbortException ex) {
831                    // do nothing
832                }
833            }
834    
835            public void tableChanged(TableModelEvent e) {
836                refreshEnabled();
837            }
838        }
839    
840        class AddSelectedBeforeSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener {
841            public AddSelectedBeforeSelection() {
842                putValue(SHORT_DESCRIPTION,
843                        tr("Add all objects selected in the current dataset before the first selected member"));
844                putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copybeforecurrentright"));
845                // putValue(NAME, tr("Add Selected"));
846                refreshEnabled();
847            }
848    
849            protected void refreshEnabled() {
850                setEnabled(selectionTableModel.getRowCount() > 0
851                        && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0);
852            }
853    
854            public void actionPerformed(ActionEvent e) {
855                try {
856                    List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
857                    memberTableModel.addMembersBeforeIdx(toAdd, memberTableModel
858                            .getSelectionModel().getMinSelectionIndex());
859                } catch(AddAbortException ex) {
860                    // do nothing
861                }
862    
863            }
864    
865            public void tableChanged(TableModelEvent e) {
866                refreshEnabled();
867            }
868    
869            public void valueChanged(ListSelectionEvent e) {
870                refreshEnabled();
871            }
872        }
873    
874        class AddSelectedAfterSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener {
875            public AddSelectedAfterSelection() {
876                putValue(SHORT_DESCRIPTION,
877                        tr("Add all objects selected in the current dataset after the last selected member"));
878                putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyaftercurrentright"));
879                // putValue(NAME, tr("Add Selected"));
880                refreshEnabled();
881            }
882    
883            protected void refreshEnabled() {
884                setEnabled(selectionTableModel.getRowCount() > 0
885                        && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0);
886            }
887    
888            public void actionPerformed(ActionEvent e) {
889                try {
890                    List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
891                    memberTableModel.addMembersAfterIdx(toAdd, memberTableModel
892                            .getSelectionModel().getMaxSelectionIndex());
893                } catch(AddAbortException ex) {
894                    // do nothing
895                }
896            }
897    
898            public void tableChanged(TableModelEvent e) {
899                refreshEnabled();
900            }
901    
902            public void valueChanged(ListSelectionEvent e) {
903                refreshEnabled();
904            }
905        }
906    
907        class RemoveSelectedAction extends AbstractAction implements TableModelListener {
908            public RemoveSelectedAction() {
909                putValue(SHORT_DESCRIPTION, tr("Remove all members referring to one of the selected objects"));
910                putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "deletemembers"));
911                // putValue(NAME, tr("Remove Selected"));
912                updateEnabledState();
913            }
914    
915            protected void updateEnabledState() {
916                DataSet ds = getLayer().data;
917                if (ds == null || ds.getSelected().isEmpty()) {
918                    setEnabled(false);
919                    return;
920                }
921                // only enable the action if we have members referring to the
922                // selected primitives
923                //
924                setEnabled(memberTableModel.hasMembersReferringTo(ds.getSelected()));
925            }
926    
927            public void actionPerformed(ActionEvent e) {
928                memberTableModel.removeMembersReferringTo(selectionTableModel.getSelection());
929            }
930    
931            public void tableChanged(TableModelEvent e) {
932                updateEnabledState();
933            }
934        }
935    
936        /**
937         * Selects  members in the relation editor which refer to primitives in the current
938         * selection of the context layer.
939         *
940         */
941        class SelectedMembersForSelectionAction extends AbstractAction implements TableModelListener {
942            public SelectedMembersForSelectionAction() {
943                putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection"));
944                putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectmembers"));
945                updateEnabledState();
946            }
947    
948            protected void updateEnabledState() {
949                boolean enabled = selectionTableModel.getRowCount() > 0
950                &&  !memberTableModel.getChildPrimitives(getLayer().data.getSelected()).isEmpty();
951    
952                if (enabled) {
953                    putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to {0} objects in the current selection",memberTableModel.getChildPrimitives(getLayer().data.getSelected()).size()));
954                } else {
955                    putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection"));
956                }
957                setEnabled(enabled);
958            }
959    
960            public void actionPerformed(ActionEvent e) {
961                memberTableModel.selectMembersReferringTo(getLayer().data.getSelected());
962            }
963    
964            public void tableChanged(TableModelEvent e) {
965                updateEnabledState();
966    
967            }
968        }
969    
970        /**
971         * Selects primitives in the layer this editor belongs to. The selected primitives are
972         * equal to the set of primitives the currently selected relation members refer to.
973         *
974         */
975        class SelectPrimitivesForSelectedMembersAction extends AbstractAction implements ListSelectionListener {
976            public SelectPrimitivesForSelectedMembersAction() {
977                putValue(SHORT_DESCRIPTION, tr("Select objects for selected relation members"));
978                putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectprimitives"));
979                updateEnabledState();
980            }
981    
982            protected void updateEnabledState() {
983                setEnabled(memberTable.getSelectedRowCount() > 0);
984            }
985    
986            public void actionPerformed(ActionEvent e) {
987                getLayer().data.setSelected(memberTableModel.getSelectedChildPrimitives());
988            }
989    
990            public void valueChanged(ListSelectionEvent e) {
991                updateEnabledState();
992            }
993        }
994    
995        class SortAction extends AbstractAction implements TableModelListener {
996            public SortAction() {
997                String tooltip = tr("Sort the relation members");
998                putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort"));
999                putValue(NAME, tr("Sort"));
1000                Shortcut sc = Shortcut.registerShortcut("relationeditor:sort", tr("Relation Editor: Sort"),
1001                    KeyEvent.VK_END, Shortcut.ALT);
1002                sc.setAccelerator(this);
1003                putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1004                updateEnabledState();
1005            }
1006    
1007            public void actionPerformed(ActionEvent e) {
1008                memberTableModel.sort();
1009            }
1010    
1011            protected void updateEnabledState() {
1012                setEnabled(memberTableModel.getRowCount() > 0);
1013            }
1014    
1015            public void tableChanged(TableModelEvent e) {
1016                updateEnabledState();
1017            }
1018        }
1019    
1020        class ReverseAction extends AbstractAction implements TableModelListener {
1021            public ReverseAction() {
1022                putValue(SHORT_DESCRIPTION, tr("Reverse the order of the relation members"));
1023                putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "reverse"));
1024                putValue(NAME, tr("Reverse"));
1025            //  Shortcut.register Shortcut("relationeditor:reverse", tr("Relation Editor: Reverse"),
1026            //      KeyEvent.VK_END, Shortcut.ALT)
1027                updateEnabledState();
1028            }
1029    
1030            public void actionPerformed(ActionEvent e) {
1031                memberTableModel.reverse();
1032            }
1033    
1034            protected void updateEnabledState() {
1035                setEnabled(memberTableModel.getRowCount() > 0);
1036            }
1037    
1038            public void tableChanged(TableModelEvent e) {
1039                updateEnabledState();
1040            }
1041        }
1042    
1043        class MoveUpAction extends AbstractAction implements ListSelectionListener {
1044            public MoveUpAction() {
1045                String tooltip = tr("Move the currently selected members up");
1046                putValue(SMALL_ICON, ImageProvider.get("dialogs", "moveup"));
1047                // putValue(NAME, tr("Move Up"));
1048                Shortcut sc = Shortcut.registerShortcut("relationeditor:moveup", tr("Relation Editor: Move Up"),
1049                    KeyEvent.VK_UP, Shortcut.ALT);
1050                sc.setAccelerator(this);
1051                putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1052                setEnabled(false);
1053            }
1054    
1055            public void actionPerformed(ActionEvent e) {
1056                memberTableModel.moveUp(memberTable.getSelectedRows());
1057            }
1058    
1059            public void valueChanged(ListSelectionEvent e) {
1060                setEnabled(memberTableModel.canMoveUp(memberTable.getSelectedRows()));
1061            }
1062        }
1063    
1064        class MoveDownAction extends AbstractAction implements ListSelectionListener {
1065            public MoveDownAction() {
1066                String tooltip = tr("Move the currently selected members down");
1067                putValue(SMALL_ICON, ImageProvider.get("dialogs", "movedown"));
1068                // putValue(NAME, tr("Move Down"));
1069                Shortcut sc = Shortcut.registerShortcut("relationeditor:movedown", tr("Relation Editor: Move Down"),
1070                    KeyEvent.VK_DOWN, Shortcut.ALT);
1071                sc.setAccelerator(this);
1072                putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1073                setEnabled(false);
1074            }
1075    
1076            public void actionPerformed(ActionEvent e) {
1077                memberTableModel.moveDown(memberTable.getSelectedRows());
1078            }
1079    
1080            public void valueChanged(ListSelectionEvent e) {
1081                setEnabled(memberTableModel.canMoveDown(memberTable.getSelectedRows()));
1082            }
1083        }
1084    
1085        class RemoveAction extends AbstractAction implements ListSelectionListener {
1086            public RemoveAction() {
1087                String tooltip = tr("Remove the currently selected members from this relation");
1088                putValue(SMALL_ICON, ImageProvider.get("dialogs", "remove"));
1089                putValue(NAME, tr("Remove"));
1090                Shortcut sc = Shortcut.registerShortcut("relationeditor:remove", tr("Relation Editor: Remove"),
1091                    KeyEvent.VK_DELETE, Shortcut.ALT);
1092                sc.setAccelerator(this);
1093                putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1094                setEnabled(false);
1095            }
1096    
1097            public void actionPerformed(ActionEvent e) {
1098                memberTableModel.remove(memberTable.getSelectedRows());
1099            }
1100    
1101            public void valueChanged(ListSelectionEvent e) {
1102                setEnabled(memberTableModel.canRemove(memberTable.getSelectedRows()));
1103            }
1104        }
1105    
1106        class DeleteCurrentRelationAction extends AbstractAction implements PropertyChangeListener{
1107            public DeleteCurrentRelationAction() {
1108                putValue(SHORT_DESCRIPTION, tr("Delete the currently edited relation"));
1109                putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
1110                putValue(NAME, tr("Delete"));
1111                updateEnabledState();
1112            }
1113    
1114            public void run() {
1115                Relation toDelete = getRelation();
1116                if (toDelete == null)
1117                    return;
1118                org.openstreetmap.josm.actions.mapmode.DeleteAction.deleteRelation(
1119                        getLayer(),
1120                        toDelete
1121                );
1122            }
1123    
1124            public void actionPerformed(ActionEvent e) {
1125                run();
1126            }
1127    
1128            protected void updateEnabledState() {
1129                setEnabled(getRelationSnapshot() != null);
1130            }
1131    
1132            public void propertyChange(PropertyChangeEvent evt) {
1133                if (evt.getPropertyName().equals(RELATION_SNAPSHOT_PROP)) {
1134                    updateEnabledState();
1135                }
1136            }
1137        }
1138    
1139        abstract class SavingAction extends AbstractAction {
1140            /**
1141             * apply updates to a new relation
1142             */
1143            protected void applyNewRelation() {
1144                final Relation newRelation = new Relation();
1145                tagEditorPanel.getModel().applyToPrimitive(newRelation);
1146                memberTableModel.applyToRelation(newRelation);
1147                List<RelationMember> newMembers = new ArrayList<RelationMember>();
1148                for (RelationMember rm: newRelation.getMembers()) {
1149                    if (!rm.getMember().isDeleted()) {
1150                        newMembers.add(rm);
1151                    }
1152                }
1153                if (newRelation.getMembersCount() != newMembers.size()) {
1154                    newRelation.setMembers(newMembers);
1155                    String msg = tr("One or more members of this new relation have been deleted while the relation editor\n" +
1156                    "was open. They have been removed from the relation members list.");
1157                    JOptionPane.showMessageDialog(Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE);
1158                }
1159                // If the user wanted to create a new relation, but hasn't added any members or
1160                // tags, don't add an empty relation
1161                if (newRelation.getMembersCount() == 0 && !newRelation.hasKeys())
1162                    return;
1163                Main.main.undoRedo.add(new AddCommand(getLayer(),newRelation));
1164    
1165                // make sure everybody is notified about the changes
1166                //
1167                getLayer().data.fireSelectionChanged();
1168                GenericRelationEditor.this.setRelation(newRelation);
1169                RelationDialogManager.getRelationDialogManager().updateContext(
1170                        getLayer(),
1171                        getRelation(),
1172                        GenericRelationEditor.this
1173                );
1174                SwingUtilities.invokeLater(new Runnable() {
1175                    @Override
1176                    public void run() {
1177                        // Relation list gets update in EDT so selecting my be postponed to following EDT run
1178                        Main.map.relationListDialog.selectRelation(newRelation);
1179                    }
1180                });
1181            }
1182    
1183            /**
1184             * Apply the updates for an existing relation which has been changed
1185             * outside of the relation editor.
1186             *
1187             */
1188            protected void applyExistingConflictingRelation() {
1189                Relation editedRelation = new Relation(getRelation());
1190                tagEditorPanel.getModel().applyToPrimitive(editedRelation);
1191                memberTableModel.applyToRelation(editedRelation);
1192                Conflict<Relation> conflict = new Conflict<Relation>(getRelation(), editedRelation);
1193                Main.main.undoRedo.add(new ConflictAddCommand(getLayer(),conflict));
1194            }
1195    
1196            /**
1197             * Apply the updates for an existing relation which has not been changed
1198             * outside of the relation editor.
1199             *
1200             */
1201            protected void applyExistingNonConflictingRelation() {
1202                Relation editedRelation = new Relation(getRelation());
1203                tagEditorPanel.getModel().applyToPrimitive(editedRelation);
1204                memberTableModel.applyToRelation(editedRelation);
1205                Main.main.undoRedo.add(new ChangeCommand(getRelation(), editedRelation));
1206                getLayer().data.fireSelectionChanged();
1207                // this will refresh the snapshot and update the dialog title
1208                //
1209                setRelation(getRelation());
1210            }
1211    
1212            protected boolean confirmClosingBecauseOfDirtyState() {
1213                ButtonSpec [] options = new ButtonSpec[] {
1214                        new ButtonSpec(
1215                                tr("Yes, create a conflict and close"),
1216                                ImageProvider.get("ok"),
1217                                tr("Click to create a conflict and close this relation editor") ,
1218                                null /* no specific help topic */
1219                        ),
1220                        new ButtonSpec(
1221                                tr("No, continue editing"),
1222                                ImageProvider.get("cancel"),
1223                                tr("Click to return to the relation editor and to resume relation editing") ,
1224                                null /* no specific help topic */
1225                        )
1226                };
1227    
1228                int ret = HelpAwareOptionPane.showOptionDialog(
1229                        Main.parent,
1230                        tr("<html>This relation has been changed outside of the editor.<br>"
1231                                + "You cannot apply your changes and continue editing.<br>"
1232                                + "<br>"
1233                                + "Do you want to create a conflict and close the editor?</html>"),
1234                                tr("Conflict in data"),
1235                                JOptionPane.WARNING_MESSAGE,
1236                                null,
1237                                options,
1238                                options[0], // OK is default
1239                                "/Dialog/RelationEditor#RelationChangedOutsideOfEditor"
1240                );
1241                return ret == 0;
1242            }
1243    
1244            protected void warnDoubleConflict() {
1245                JOptionPane.showMessageDialog(
1246                        Main.parent,
1247                        tr("<html>Layer ''{0}'' already has a conflict for object<br>"
1248                                + "''{1}''.<br>"
1249                                + "Please resolve this conflict first, then try again.</html>",
1250                                getLayer().getName(),
1251                                getRelation().getDisplayName(DefaultNameFormatter.getInstance())
1252                        ),
1253                        tr("Double conflict"),
1254                        JOptionPane.WARNING_MESSAGE
1255                );
1256            }
1257        }
1258    
1259        class ApplyAction extends SavingAction {
1260            public ApplyAction() {
1261                putValue(SHORT_DESCRIPTION, tr("Apply the current updates"));
1262                putValue(SMALL_ICON, ImageProvider.get("save"));
1263                putValue(NAME, tr("Apply"));
1264                setEnabled(true);
1265            }
1266    
1267            public void run() {
1268                if (getRelation() == null) {
1269                    applyNewRelation();
1270                } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot())
1271                        || tagEditorPanel.getModel().isDirty()) {
1272                    if (isDirtyRelation()) {
1273                        if (confirmClosingBecauseOfDirtyState()) {
1274                            if (getLayer().getConflicts().hasConflictForMy(getRelation())) {
1275                                warnDoubleConflict();
1276                                return;
1277                            }
1278                            applyExistingConflictingRelation();
1279                            setVisible(false);
1280                        }
1281                    } else {
1282                        applyExistingNonConflictingRelation();
1283                    }
1284                }
1285            }
1286    
1287            public void actionPerformed(ActionEvent e) {
1288                run();
1289            }
1290        }
1291    
1292        class OKAction extends SavingAction {
1293            public OKAction() {
1294                putValue(SHORT_DESCRIPTION, tr("Apply the updates and close the dialog"));
1295                putValue(SMALL_ICON, ImageProvider.get("ok"));
1296                putValue(NAME, tr("OK"));
1297                setEnabled(true);
1298            }
1299    
1300            public void run() {
1301                Main.pref.put("relation.editor.generic.lastrole", tfRole.getText());
1302                if (getRelation() == null) {
1303                    applyNewRelation();
1304                } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot())
1305                        || tagEditorPanel.getModel().isDirty()) {
1306                    if (isDirtyRelation()) {
1307                        if (confirmClosingBecauseOfDirtyState()) {
1308                            if (getLayer().getConflicts().hasConflictForMy(getRelation())) {
1309                                warnDoubleConflict();
1310                                return;
1311                            }
1312                            applyExistingConflictingRelation();
1313                        } else
1314                            return;
1315                    } else {
1316                        applyExistingNonConflictingRelation();
1317                    }
1318                }
1319                setVisible(false);
1320            }
1321    
1322            public void actionPerformed(ActionEvent e) {
1323                run();
1324            }
1325        }
1326    
1327        class CancelAction extends SavingAction {
1328            public CancelAction() {
1329                putValue(SHORT_DESCRIPTION, tr("Cancel the updates and close the dialog"));
1330                putValue(SMALL_ICON, ImageProvider.get("cancel"));
1331                putValue(NAME, tr("Cancel"));
1332    
1333                getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
1334                .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE");
1335                getRootPane().getActionMap().put("ESCAPE", this);
1336                setEnabled(true);
1337            }
1338    
1339            public void actionPerformed(ActionEvent e) {
1340                if (!memberTableModel.hasSameMembersAs(getRelationSnapshot()) || tagEditorPanel.getModel().isDirty()) {
1341                    //give the user a chance to save the changes
1342                    int ret = confirmClosingByCancel();
1343                    if (ret == 0) { //Yes, save the changes
1344                        //copied from OKAction.run()
1345                        Main.pref.put("relation.editor.generic.lastrole", tfRole.getText());
1346                        if (getRelation() == null) {
1347                            applyNewRelation();
1348                        } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot())
1349                                || tagEditorPanel.getModel().isDirty()) {
1350                            if (isDirtyRelation()) {
1351                                if (confirmClosingBecauseOfDirtyState()) {
1352                                    if (getLayer().getConflicts().hasConflictForMy(getRelation())) {
1353                                        warnDoubleConflict();
1354                                        return;
1355                                    }
1356                                    applyExistingConflictingRelation();
1357                                } else
1358                                    return;
1359                            } else {
1360                                applyExistingNonConflictingRelation();
1361                            }
1362                        }
1363                    }
1364                    else if (ret == 2) //Cancel, continue editing
1365                        return;
1366                    //in case of "No, discard", there is no extra action to be performed here.
1367                }
1368                setVisible(false);
1369            }
1370    
1371            protected int confirmClosingByCancel() {
1372                ButtonSpec [] options = new ButtonSpec[] {
1373                        new ButtonSpec(
1374                                tr("Yes, save the changes and close"),
1375                                ImageProvider.get("ok"),
1376                                tr("Click to save the changes and close this relation editor") ,
1377                                null /* no specific help topic */
1378                        ),
1379                        new ButtonSpec(
1380                                tr("No, discard the changes and close"),
1381                                ImageProvider.get("cancel"),
1382                                tr("Click to discard the changes and close this relation editor") ,
1383                                null /* no specific help topic */
1384                        ),
1385                        new ButtonSpec(
1386                                tr("Cancel, continue editing"),
1387                                ImageProvider.get("cancel"),
1388                                tr("Click to return to the relation editor and to resume relation editing") ,
1389                                null /* no specific help topic */
1390                        )
1391                };
1392    
1393                int ret = HelpAwareOptionPane.showOptionDialog(
1394                        Main.parent,
1395                        tr("<html>The relation has been changed.<br>"
1396                                + "<br>"
1397                                + "Do you want to save your changes?</html>"),
1398                                tr("Unsaved changes"),
1399                                JOptionPane.WARNING_MESSAGE,
1400                                null,
1401                                options,
1402                                options[0], // OK is default,
1403                                "/Dialog/RelationEditor#DiscardChanges"
1404                );
1405                return ret;
1406            }
1407        }
1408    
1409        class AddTagAction extends AbstractAction {
1410            public AddTagAction() {
1411                putValue(SHORT_DESCRIPTION, tr("Add an empty tag"));
1412                putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
1413                // putValue(NAME, tr("Cancel"));
1414                setEnabled(true);
1415            }
1416    
1417            public void actionPerformed(ActionEvent e) {
1418                tagEditorPanel.getModel().appendNewTag();
1419            }
1420        }
1421    
1422        class DownloadIncompleteMembersAction extends AbstractAction implements TableModelListener {
1423            public DownloadIncompleteMembersAction() {
1424                String tooltip = tr("Download all incomplete members");
1425                putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincomplete"));
1426                putValue(NAME, tr("Download Members"));
1427                Shortcut sc = Shortcut.registerShortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"),
1428                    KeyEvent.VK_HOME, Shortcut.ALT);
1429                sc.setAccelerator(this);
1430                putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
1431                updateEnabledState();
1432            }
1433    
1434            public void actionPerformed(ActionEvent e) {
1435                if (!isEnabled())
1436                    return;
1437                Main.worker.submit(new DownloadRelationMemberTask(
1438                        getRelation(),
1439                        memberTableModel.getIncompleteMemberPrimitives(),
1440                        getLayer(),
1441                        GenericRelationEditor.this)
1442                );
1443            }
1444    
1445            protected void updateEnabledState() {
1446                setEnabled(memberTableModel.hasIncompleteMembers());
1447            }
1448    
1449            public void tableChanged(TableModelEvent e) {
1450                updateEnabledState();
1451            }
1452        }
1453    
1454        class DownloadSelectedIncompleteMembersAction extends AbstractAction implements ListSelectionListener, TableModelListener{
1455            public DownloadSelectedIncompleteMembersAction() {
1456                putValue(SHORT_DESCRIPTION, tr("Download selected incomplete members"));
1457                putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincompleteselected"));
1458                putValue(NAME, tr("Download Members"));
1459            //  Shortcut.register Shortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"),
1460            //      KeyEvent.VK_K, Shortcut.ALT)
1461                updateEnabledState();
1462            }
1463    
1464            public void actionPerformed(ActionEvent e) {
1465                if (!isEnabled())
1466                    return;
1467                Main.worker.submit(new DownloadRelationMemberTask(
1468                        getRelation(),
1469                        memberTableModel.getSelectedIncompleteMemberPrimitives(),
1470                        getLayer(),
1471                        GenericRelationEditor.this)
1472                );
1473            }
1474    
1475            protected void updateEnabledState() {
1476                setEnabled(memberTableModel.hasIncompleteSelectedMembers());
1477            }
1478    
1479            public void valueChanged(ListSelectionEvent e) {
1480                updateEnabledState();
1481            }
1482    
1483            public void tableChanged(TableModelEvent e) {
1484                updateEnabledState();
1485            }
1486        }
1487    
1488        class SetRoleAction extends AbstractAction implements ListSelectionListener, DocumentListener {
1489            public SetRoleAction() {
1490                putValue(SHORT_DESCRIPTION, tr("Sets a role for the selected members"));
1491                putValue(SMALL_ICON, ImageProvider.get("apply"));
1492                putValue(NAME, tr("Apply Role"));
1493                refreshEnabled();
1494            }
1495    
1496            protected void refreshEnabled() {
1497                setEnabled(memberTable.getSelectedRowCount() > 0);
1498            }
1499    
1500            protected boolean isEmptyRole() {
1501                return tfRole.getText() == null || tfRole.getText().trim().equals("");
1502            }
1503    
1504            protected boolean confirmSettingEmptyRole(int onNumMembers) {
1505                String message = "<html>"
1506                    + trn("You are setting an empty role on {0} object.",
1507                            "You are setting an empty role on {0} objects.", onNumMembers, onNumMembers)
1508                            + "<br>"
1509                            + tr("This is equal to deleting the roles of these objects.") +
1510                            "<br>"
1511                            + tr("Do you really want to apply the new role?") + "</html>";
1512                String [] options = new String[] {
1513                        tr("Yes, apply it"),
1514                        tr("No, do not apply")
1515                };
1516                int ret = ConditionalOptionPaneUtil.showOptionDialog(
1517                        "relation_editor.confirm_applying_empty_role",
1518                        Main.parent,
1519                        message,
1520                        tr("Confirm empty role"),
1521                        JOptionPane.YES_NO_OPTION,
1522                        JOptionPane.WARNING_MESSAGE,
1523                        options,
1524                        options[0]
1525                );
1526                switch(ret) {
1527                case JOptionPane.YES_OPTION: return true;
1528                case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION: return true;
1529                default:
1530                    return false;
1531                }
1532            }
1533    
1534            public void actionPerformed(ActionEvent e) {
1535                if (isEmptyRole()) {
1536                    if (! confirmSettingEmptyRole(memberTable.getSelectedRowCount()))
1537                        return;
1538                }
1539                memberTableModel.updateRole(memberTable.getSelectedRows(), tfRole.getText());
1540            }
1541    
1542            public void valueChanged(ListSelectionEvent e) {
1543                refreshEnabled();
1544            }
1545    
1546            public void changedUpdate(DocumentEvent e) {
1547                refreshEnabled();
1548            }
1549    
1550            public void insertUpdate(DocumentEvent e) {
1551                refreshEnabled();
1552            }
1553    
1554            public void removeUpdate(DocumentEvent e) {
1555                refreshEnabled();
1556            }
1557        }
1558    
1559        /**
1560         * Creates a new relation with a copy of the current editor state
1561         *
1562         */
1563        class DuplicateRelationAction extends AbstractAction {
1564            public DuplicateRelationAction() {
1565                putValue(SHORT_DESCRIPTION, tr("Create a copy of this relation and open it in another editor window"));
1566                // FIXME provide an icon
1567                putValue(SMALL_ICON, ImageProvider.get("duplicate"));
1568                putValue(NAME, tr("Duplicate"));
1569                setEnabled(true);
1570            }
1571    
1572            public void actionPerformed(ActionEvent e) {
1573                Relation copy = new Relation();
1574                tagEditorPanel.getModel().applyToPrimitive(copy);
1575                memberTableModel.applyToRelation(copy);
1576                RelationEditor editor = RelationEditor.getEditor(getLayer(), copy, memberTableModel.getSelectedMembers());
1577                editor.setVisible(true);
1578            }
1579        }
1580    
1581        /**
1582         * Action for editing the currently selected relation
1583         *
1584         *
1585         */
1586        class EditAction extends AbstractAction implements ListSelectionListener {
1587            public EditAction() {
1588                putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to"));
1589                putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit"));
1590                //putValue(NAME, tr("Edit"));
1591                refreshEnabled();
1592            }
1593    
1594            protected void refreshEnabled() {
1595                setEnabled(memberTable.getSelectedRowCount() == 1
1596                        && memberTableModel.isEditableRelation(memberTable.getSelectedRow()));
1597            }
1598    
1599            protected Collection<RelationMember> getMembersForCurrentSelection(Relation r) {
1600                Collection<RelationMember> members = new HashSet<RelationMember>();
1601                Collection<OsmPrimitive> selection = getLayer().data.getSelected();
1602                for (RelationMember member: r.getMembers()) {
1603                    if (selection.contains(member.getMember())) {
1604                        members.add(member);
1605                    }
1606                }
1607                return members;
1608            }
1609    
1610            public void run() {
1611                int idx = memberTable.getSelectedRow();
1612                if (idx < 0)
1613                    return;
1614                OsmPrimitive primitive = memberTableModel.getReferredPrimitive(idx);
1615                if (!(primitive instanceof Relation))
1616                    return;
1617                Relation r = (Relation) primitive;
1618                if (r.isIncomplete())
1619                    return;
1620    
1621                RelationEditor editor = RelationEditor.getEditor(getLayer(), r, getMembersForCurrentSelection(r));
1622                editor.setVisible(true);
1623            }
1624    
1625            public void actionPerformed(ActionEvent e) {
1626                if (!isEnabled())
1627                    return;
1628                run();
1629            }
1630    
1631            public void valueChanged(ListSelectionEvent e) {
1632                refreshEnabled();
1633            }
1634        }
1635    
1636        class PasteMembersAction extends AddFromSelectionAction {
1637    
1638            public PasteMembersAction() {
1639                registerCopyPasteAction(this, "PASTE_MEMBERS", Shortcut.getPasteKeyStroke());
1640            }
1641    
1642            @Override
1643            public void actionPerformed(ActionEvent e) {
1644                try {
1645                    List<PrimitiveData> primitives = Main.pasteBuffer.getDirectlyAdded();
1646                    DataSet ds = getLayer().data;
1647                    List<OsmPrimitive> toAdd = new ArrayList<OsmPrimitive>();
1648                    boolean hasNewInOtherLayer = false;
1649    
1650                    for (PrimitiveData primitive: primitives) {
1651                        OsmPrimitive primitiveInDs = ds.getPrimitiveById(primitive);
1652                        if (primitiveInDs != null) {
1653                            toAdd.add(primitiveInDs);
1654                        } else if (!primitive.isNew()) {
1655                            OsmPrimitive p = primitive.getType().newInstance(primitive.getUniqueId(), true);
1656                            ds.addPrimitive(p);
1657                            toAdd.add(p);
1658                        } else {
1659                            hasNewInOtherLayer = true;
1660                            break;
1661                        }
1662                    }
1663    
1664                    if (hasNewInOtherLayer) {
1665                        JOptionPane.showMessageDialog(Main.parent, tr("Members from paste buffer cannot be added because they are not included in current layer"));
1666                        return;
1667                    }
1668    
1669                    toAdd = filterConfirmedPrimitives(toAdd);
1670                    int index = memberTableModel.getSelectionModel().getMaxSelectionIndex();
1671                    if (index == -1) {
1672                        index = memberTableModel.getRowCount() - 1;
1673                    }
1674                    memberTableModel.addMembersAfterIdx(toAdd, index);
1675    
1676                    tfRole.requestFocusInWindow();
1677    
1678                } catch (AddAbortException ex) {
1679                    // Do nothing
1680                }
1681            }
1682        }
1683    
1684        class CopyMembersAction extends AbstractAction {
1685    
1686            public CopyMembersAction() {
1687                registerCopyPasteAction(this, "COPY_MEMBERS", Shortcut.getCopyKeyStroke());
1688            }
1689    
1690            @Override
1691            public void actionPerformed(ActionEvent e) {
1692                Set<OsmPrimitive> primitives = new HashSet<OsmPrimitive>();
1693                for (RelationMember rm: memberTableModel.getSelectedMembers()) {
1694                    primitives.add(rm.getMember());
1695                }
1696                if (!primitives.isEmpty()) {
1697                    CopyAction.copy(getLayer(), primitives);
1698                }
1699            }
1700    
1701        }
1702    
1703        class PasteTagsAction extends AbstractAction {
1704    
1705            public PasteTagsAction() {
1706                registerCopyPasteAction(this, "PASTE_TAGS", Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), KeyEvent.VK_V, Shortcut.CTRL_SHIFT).getKeyStroke());
1707            }
1708    
1709            @Override
1710            public void actionPerformed(ActionEvent e) {
1711                Relation relation = new Relation();
1712                tagEditorPanel.getModel().applyToPrimitive(relation);
1713                TagPaster tagPaster = new TagPaster(Main.pasteBuffer.getDirectlyAdded(), Collections.<OsmPrimitive>singletonList(relation));
1714                updateTags(tagPaster.execute());
1715            }
1716    
1717        }
1718    
1719        class MemberTableDblClickAdapter extends MouseAdapter {
1720            @Override
1721            public void mouseClicked(MouseEvent e) {
1722                if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
1723                    new EditAction().run();
1724                }
1725            }
1726        }
1727    }