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