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