001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.properties;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Container;
008import java.awt.Font;
009import java.awt.GridBagLayout;
010import java.awt.Point;
011import java.awt.event.ActionEvent;
012import java.awt.event.InputEvent;
013import java.awt.event.KeyEvent;
014import java.awt.event.MouseAdapter;
015import java.awt.event.MouseEvent;
016import java.io.IOException;
017import java.net.URI;
018import java.net.URISyntaxException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.EnumSet;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Set;
031import java.util.TreeMap;
032import java.util.TreeSet;
033
034import javax.swing.AbstractAction;
035import javax.swing.JComponent;
036import javax.swing.JLabel;
037import javax.swing.JPanel;
038import javax.swing.JPopupMenu;
039import javax.swing.JScrollPane;
040import javax.swing.JTable;
041import javax.swing.KeyStroke;
042import javax.swing.ListSelectionModel;
043import javax.swing.event.ListSelectionEvent;
044import javax.swing.event.ListSelectionListener;
045import javax.swing.event.RowSorterEvent;
046import javax.swing.event.RowSorterListener;
047import javax.swing.table.DefaultTableCellRenderer;
048import javax.swing.table.DefaultTableModel;
049import javax.swing.table.TableCellRenderer;
050import javax.swing.table.TableColumnModel;
051import javax.swing.table.TableModel;
052import javax.swing.table.TableRowSorter;
053
054import org.openstreetmap.josm.Main;
055import org.openstreetmap.josm.actions.JosmAction;
056import org.openstreetmap.josm.actions.relation.DownloadMembersAction;
057import org.openstreetmap.josm.actions.relation.DownloadSelectedIncompleteMembersAction;
058import org.openstreetmap.josm.actions.relation.SelectInRelationListAction;
059import org.openstreetmap.josm.actions.relation.SelectMembersAction;
060import org.openstreetmap.josm.actions.relation.SelectRelationAction;
061import org.openstreetmap.josm.actions.search.SearchAction.SearchSetting;
062import org.openstreetmap.josm.actions.search.SearchCompiler;
063import org.openstreetmap.josm.command.ChangeCommand;
064import org.openstreetmap.josm.command.ChangePropertyCommand;
065import org.openstreetmap.josm.command.Command;
066import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
067import org.openstreetmap.josm.data.SelectionChangedListener;
068import org.openstreetmap.josm.data.osm.IRelation;
069import org.openstreetmap.josm.data.osm.Node;
070import org.openstreetmap.josm.data.osm.OsmPrimitive;
071import org.openstreetmap.josm.data.osm.Relation;
072import org.openstreetmap.josm.data.osm.RelationMember;
073import org.openstreetmap.josm.data.osm.Tag;
074import org.openstreetmap.josm.data.osm.Way;
075import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
076import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
077import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
078import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
079import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
080import org.openstreetmap.josm.data.preferences.StringProperty;
081import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
082import org.openstreetmap.josm.gui.DefaultNameFormatter;
083import org.openstreetmap.josm.gui.ExtendedDialog;
084import org.openstreetmap.josm.gui.PopupMenuHandler;
085import org.openstreetmap.josm.gui.SideButton;
086import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
087import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
088import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
089import org.openstreetmap.josm.gui.help.HelpUtil;
090import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
091import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
092import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
093import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
094import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
095import org.openstreetmap.josm.gui.util.HighlightHelper;
096import org.openstreetmap.josm.gui.widgets.CompileSearchTextDecorator;
097import org.openstreetmap.josm.gui.widgets.DisableShortcutsOnFocusGainedTextField;
098import org.openstreetmap.josm.gui.widgets.JosmTextField;
099import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
100import org.openstreetmap.josm.tools.AlphanumComparator;
101import org.openstreetmap.josm.tools.GBC;
102import org.openstreetmap.josm.tools.HttpClient;
103import org.openstreetmap.josm.tools.ImageProvider;
104import org.openstreetmap.josm.tools.InputMapUtils;
105import org.openstreetmap.josm.tools.LanguageInfo;
106import org.openstreetmap.josm.tools.OpenBrowser;
107import org.openstreetmap.josm.tools.Shortcut;
108import org.openstreetmap.josm.tools.Utils;
109
110/**
111 * This dialog displays the tags of the current selected primitives.
112 *
113 * If no object is selected, the dialog list is empty.
114 * If only one is selected, all tags of this object are selected.
115 * If more than one object are selected, the sum of all tags are displayed. If the
116 * different objects share the same tag, the shared value is displayed. If they have
117 * different values, all of them are put in a combo box and the string "<different>"
118 * is displayed in italic.
119 *
120 * Below the list, the user can click on an add, modify and delete tag button to
121 * edit the table selection value.
122 *
123 * The command is applied to all selected entries.
124 *
125 * @author imi
126 */
127public class PropertiesDialog extends ToggleDialog
128implements SelectionChangedListener, ActiveLayerChangeListener, DataSetListenerAdapter.Listener {
129
130    /**
131     * hook for roadsigns plugin to display a small button in the upper right corner of this dialog
132     */
133    public static final JPanel pluginHook = new JPanel();
134
135    /**
136     * The tag data of selected objects.
137     */
138    private final ReadOnlyTableModel tagData = new ReadOnlyTableModel();
139    private final PropertiesCellRenderer cellRenderer = new PropertiesCellRenderer();
140    private final transient TableRowSorter<ReadOnlyTableModel> tagRowSorter = new TableRowSorter<>(tagData);
141    private final JosmTextField tagTableFilter;
142
143    /**
144     * The membership data of selected objects.
145     */
146    private final DefaultTableModel membershipData = new ReadOnlyTableModel();
147
148    /**
149     * The tags table.
150     */
151    private final JTable tagTable = new JTable(tagData);
152
153    /**
154     * The membership table.
155     */
156    private final JTable membershipTable = new JTable(membershipData);
157
158    /** JPanel containing both previous tables */
159    private final JPanel bothTables = new JPanel(new GridBagLayout());
160
161    // Popup menus
162    private final JPopupMenu tagMenu = new JPopupMenu();
163    private final JPopupMenu membershipMenu = new JPopupMenu();
164    private final JPopupMenu blankSpaceMenu = new JPopupMenu();
165
166    // Popup menu handlers
167    private final transient PopupMenuHandler tagMenuHandler = new PopupMenuHandler(tagMenu);
168    private final transient PopupMenuHandler membershipMenuHandler = new PopupMenuHandler(membershipMenu);
169    private final transient PopupMenuHandler blankSpaceMenuHandler = new PopupMenuHandler(blankSpaceMenu);
170
171    private final transient Map<String, Map<String, Integer>> valueCount = new TreeMap<>();
172    /**
173     * This sub-object is responsible for all adding and editing of tags
174     */
175    private final transient TagEditHelper editHelper = new TagEditHelper(tagTable, tagData, valueCount);
176
177    private final transient DataSetListenerAdapter dataChangedAdapter = new DataSetListenerAdapter(this);
178    private final HelpAction helpAction = new HelpAction();
179    private final TaginfoAction taginfoAction = new TaginfoAction();
180    private final PasteValueAction pasteValueAction = new PasteValueAction();
181    private final CopyValueAction copyValueAction = new CopyValueAction();
182    private final CopyKeyValueAction copyKeyValueAction = new CopyKeyValueAction();
183    private final CopyAllKeyValueAction copyAllKeyValueAction = new CopyAllKeyValueAction();
184    private final SearchAction searchActionSame = new SearchAction(true);
185    private final SearchAction searchActionAny = new SearchAction(false);
186    private final AddAction addAction = new AddAction();
187    private final EditAction editAction = new EditAction();
188    private final DeleteAction deleteAction = new DeleteAction();
189    private final JosmAction[] josmActions = new JosmAction[]{addAction, editAction, deleteAction};
190
191    // relation actions
192    private final SelectInRelationListAction setRelationSelectionAction = new SelectInRelationListAction();
193    private final SelectRelationAction selectRelationAction = new SelectRelationAction(false);
194    private final SelectRelationAction addRelationToSelectionAction = new SelectRelationAction(true);
195
196    private final DownloadMembersAction downloadMembersAction = new DownloadMembersAction();
197    private final DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction =
198            new DownloadSelectedIncompleteMembersAction();
199
200    private final SelectMembersAction selectMembersAction = new SelectMembersAction(false);
201    private final SelectMembersAction addMembersToSelectionAction = new SelectMembersAction(true);
202
203    private final transient HighlightHelper highlightHelper = new HighlightHelper();
204
205    /**
206     * The Add button (needed to be able to disable it)
207     */
208    private final SideButton btnAdd = new SideButton(addAction);
209    /**
210     * The Edit button (needed to be able to disable it)
211     */
212    private final SideButton btnEdit = new SideButton(editAction);
213    /**
214     * The Delete button (needed to be able to disable it)
215     */
216    private final SideButton btnDel = new SideButton(deleteAction);
217    /**
218     * Matching preset display class
219     */
220    private final PresetListPanel presets = new PresetListPanel();
221
222    /**
223     * Text to display when nothing selected.
224     */
225    private final JLabel selectSth = new JLabel("<html><p>"
226            + tr("Select objects for which to change tags.") + "</p></html>");
227
228    private final PreferenceChangedListener preferenceListener = e -> {
229                if (Main.getLayerManager().getEditDataSet() != null) {
230                    // Re-load data when display preference change
231                    updateSelection();
232                }
233            };
234
235    private final transient TaggingPresetHandler presetHandler = new TaggingPresetCommandHandler();
236
237    /**
238     * Create a new PropertiesDialog
239     */
240    public PropertiesDialog() {
241        super(tr("Tags/Memberships"), "propertiesdialog", tr("Tags for selected objects."),
242                Shortcut.registerShortcut("subwindow:properties", tr("Toggle: {0}", tr("Tags/Memberships")), KeyEvent.VK_P,
243                        Shortcut.ALT_SHIFT), 150, true);
244
245        HelpUtil.setHelpContext(this, HelpUtil.ht("/Dialog/TagsMembership"));
246
247        setupTagsMenu();
248        buildTagsTable();
249
250        setupMembershipMenu();
251        buildMembershipTable();
252
253        tagTableFilter = setupFilter();
254
255        // combine both tables and wrap them in a scrollPane
256        boolean top = Main.pref.getBoolean("properties.presets.top", true);
257        if (top) {
258            bothTables.add(presets, GBC.std().fill(GBC.HORIZONTAL).insets(5, 2, 5, 2).anchor(GBC.NORTHWEST));
259            double epsilon = Double.MIN_VALUE; // need to set a weight or else anchor value is ignored
260            bothTables.add(pluginHook, GBC.eol().insets(0, 1, 1, 1).anchor(GBC.NORTHEAST).weight(epsilon, epsilon));
261        }
262        bothTables.add(selectSth, GBC.eol().fill().insets(10, 10, 10, 10));
263        bothTables.add(tagTableFilter, GBC.eol().fill(GBC.HORIZONTAL));
264        bothTables.add(tagTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL));
265        bothTables.add(tagTable, GBC.eol().fill(GBC.BOTH));
266        bothTables.add(membershipTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL));
267        bothTables.add(membershipTable, GBC.eol().fill(GBC.BOTH));
268        if (!top) {
269            bothTables.add(presets, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 2, 5, 2));
270        }
271
272        setupBlankSpaceMenu();
273        setupKeyboardShortcuts();
274
275        // Let the actions know when selection in the tables change
276        tagTable.getSelectionModel().addListSelectionListener(editAction);
277        membershipTable.getSelectionModel().addListSelectionListener(editAction);
278        tagTable.getSelectionModel().addListSelectionListener(deleteAction);
279        membershipTable.getSelectionModel().addListSelectionListener(deleteAction);
280
281        JScrollPane scrollPane = (JScrollPane) createLayout(bothTables, true,
282                Arrays.asList(this.btnAdd, this.btnEdit, this.btnDel));
283
284        MouseClickWatch mouseClickWatch = new MouseClickWatch();
285        tagTable.addMouseListener(mouseClickWatch);
286        membershipTable.addMouseListener(mouseClickWatch);
287        scrollPane.addMouseListener(mouseClickWatch);
288
289        selectSth.setPreferredSize(scrollPane.getSize());
290        presets.setSize(scrollPane.getSize());
291
292        editHelper.loadTagsIfNeeded();
293
294        Main.pref.addKeyPreferenceChangeListener("display.discardable-keys", preferenceListener);
295    }
296
297    private void buildTagsTable() {
298        // setting up the tags table
299        tagData.setColumnIdentifiers(new String[]{tr("Key"), tr("Value")});
300        tagTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
301        tagTable.getTableHeader().setReorderingAllowed(false);
302
303        tagTable.getColumnModel().getColumn(0).setCellRenderer(cellRenderer);
304        tagTable.getColumnModel().getColumn(1).setCellRenderer(cellRenderer);
305        tagTable.setRowSorter(tagRowSorter);
306
307        final RemoveHiddenSelection removeHiddenSelection = new RemoveHiddenSelection();
308        tagTable.getSelectionModel().addListSelectionListener(removeHiddenSelection);
309        tagRowSorter.addRowSorterListener(removeHiddenSelection);
310        tagRowSorter.setComparator(0, AlphanumComparator.getInstance());
311        tagRowSorter.setComparator(1, (o1, o2) -> {
312            if (o1 instanceof Map && o2 instanceof Map) {
313                final String v1 = ((Map) o1).size() == 1 ? (String) ((Map) o1).keySet().iterator().next() : tr("<different>");
314                final String v2 = ((Map) o2).size() == 1 ? (String) ((Map) o2).keySet().iterator().next() : tr("<different>");
315                return AlphanumComparator.getInstance().compare(v1, v2);
316            } else {
317                return AlphanumComparator.getInstance().compare(String.valueOf(o1), String.valueOf(o2));
318            }
319        });
320    }
321
322    private void buildMembershipTable() {
323        membershipData.setColumnIdentifiers(new String[]{tr("Member Of"), tr("Role"), tr("Position")});
324        membershipTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
325
326        TableColumnModel mod = membershipTable.getColumnModel();
327        membershipTable.getTableHeader().setReorderingAllowed(false);
328        mod.getColumn(0).setCellRenderer(new MemberOfCellRenderer());
329        mod.getColumn(1).setCellRenderer(new RoleCellRenderer());
330        mod.getColumn(2).setCellRenderer(new PositionCellRenderer());
331        mod.getColumn(2).setPreferredWidth(20);
332        mod.getColumn(1).setPreferredWidth(40);
333        mod.getColumn(0).setPreferredWidth(200);
334    }
335
336    /**
337     * Creates the popup menu @field blankSpaceMenu and its launcher on main panel.
338     */
339    private void setupBlankSpaceMenu() {
340        if (Main.pref.getBoolean("properties.menu.add_edit_delete", true)) {
341            blankSpaceMenuHandler.addAction(addAction);
342            PopupMenuLauncher launcher = new BlankSpaceMenuLauncher(blankSpaceMenu);
343            bothTables.addMouseListener(launcher);
344            tagTable.addMouseListener(launcher);
345        }
346    }
347
348    /**
349     * Creates the popup menu @field membershipMenu and its launcher on membership table.
350     */
351    private void setupMembershipMenu() {
352        // setting up the membership table
353        if (Main.pref.getBoolean("properties.menu.add_edit_delete", true)) {
354            membershipMenuHandler.addAction(editAction);
355            membershipMenuHandler.addAction(deleteAction);
356            membershipMenu.addSeparator();
357        }
358        membershipMenuHandler.addAction(setRelationSelectionAction);
359        membershipMenuHandler.addAction(selectRelationAction);
360        membershipMenuHandler.addAction(addRelationToSelectionAction);
361        membershipMenuHandler.addAction(selectMembersAction);
362        membershipMenuHandler.addAction(addMembersToSelectionAction);
363        membershipMenu.addSeparator();
364        membershipMenuHandler.addAction(downloadMembersAction);
365        membershipMenuHandler.addAction(downloadSelectedIncompleteMembersAction);
366        membershipMenu.addSeparator();
367        membershipMenu.add(helpAction);
368        membershipMenu.add(taginfoAction);
369
370        membershipTable.addMouseListener(new PopupMenuLauncher(membershipMenu) {
371            @Override
372            protected int checkTableSelection(JTable table, Point p) {
373                int row = super.checkTableSelection(table, p);
374                List<Relation> rels = new ArrayList<>();
375                for (int i: table.getSelectedRows()) {
376                    rels.add((Relation) table.getValueAt(i, 0));
377                }
378                membershipMenuHandler.setPrimitives(rels);
379                return row;
380            }
381
382            @Override
383            public void mouseClicked(MouseEvent e) {
384                //update highlights
385                if (Main.isDisplayingMapView()) {
386                    int row = membershipTable.rowAtPoint(e.getPoint());
387                    if (row >= 0 && highlightHelper.highlightOnly((Relation) membershipTable.getValueAt(row, 0))) {
388                        Main.map.mapView.repaint();
389                    }
390                }
391                super.mouseClicked(e);
392            }
393
394            @Override
395            public void mouseExited(MouseEvent me) {
396                highlightHelper.clear();
397            }
398        });
399    }
400
401    /**
402     * Creates the popup menu @field tagMenu and its launcher on tag table.
403     */
404    private void setupTagsMenu() {
405        if (Main.pref.getBoolean("properties.menu.add_edit_delete", true)) {
406            tagMenu.add(addAction);
407            tagMenu.add(editAction);
408            tagMenu.add(deleteAction);
409            tagMenu.addSeparator();
410        }
411        tagMenu.add(pasteValueAction);
412        tagMenu.add(copyValueAction);
413        tagMenu.add(copyKeyValueAction);
414        tagMenu.add(copyAllKeyValueAction);
415        tagMenu.addSeparator();
416        tagMenu.add(searchActionAny);
417        tagMenu.add(searchActionSame);
418        tagMenu.addSeparator();
419        tagMenu.add(helpAction);
420        tagMenu.add(taginfoAction);
421        tagTable.addMouseListener(new PopupMenuLauncher(tagMenu));
422    }
423
424    public void setFilter(final SearchCompiler.Match filter) {
425        this.tagRowSorter.setRowFilter(new SearchBasedRowFilter(filter));
426    }
427
428    /**
429     * Assigns all needed keys like Enter and Spacebar to most important actions.
430     */
431    private void setupKeyboardShortcuts() {
432
433        // ENTER = editAction, open "edit" dialog
434        InputMapUtils.addEnterActionWhenAncestor(tagTable, editAction);
435        InputMapUtils.addEnterActionWhenAncestor(membershipTable, editAction);
436
437        // INSERT button = addAction, open "add tag" dialog
438        tagTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
439                .put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), "onTableInsert");
440        tagTable.getActionMap().put("onTableInsert", addAction);
441
442        // unassign some standard shortcuts for JTable to allow upload / download / image browsing
443        InputMapUtils.unassignCtrlShiftUpDown(tagTable, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
444        InputMapUtils.unassignPageUpDown(tagTable, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
445
446        // unassign some standard shortcuts for correct copy-pasting, fix #8508
447        tagTable.setTransferHandler(null);
448
449        tagTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
450                .put(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK), "onCopy");
451        tagTable.getActionMap().put("onCopy", copyKeyValueAction);
452
453        // allow using enter to add tags for all look&feel configurations
454        InputMapUtils.enableEnter(this.btnAdd);
455
456        // DEL button = deleteAction
457        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
458                KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"
459                );
460        getActionMap().put("delete", deleteAction);
461
462        // F1 button = custom help action
463        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
464                helpAction.getKeyStroke(), "onHelp");
465        getActionMap().put("onHelp", helpAction);
466    }
467
468    private JosmTextField setupFilter() {
469        final JosmTextField f = new DisableShortcutsOnFocusGainedTextField();
470        f.setToolTipText(tr("Tag filter"));
471        final CompileSearchTextDecorator decorator = CompileSearchTextDecorator.decorate(f);
472        f.addPropertyChangeListener("filter", evt -> setFilter(decorator.getMatch()));
473        return f;
474    }
475
476    /**
477     * This simply fires up an {@link RelationEditor} for the relation shown; everything else
478     * is the editor's business.
479     *
480     * @param row position
481     */
482    private void editMembership(int row) {
483        Relation relation = (Relation) membershipData.getValueAt(row, 0);
484        Main.map.relationListDialog.selectRelation(relation);
485        RelationEditor.getEditor(
486                Main.getLayerManager().getEditLayer(),
487                relation,
488                ((MemberInfo) membershipData.getValueAt(row, 1)).role
489        ).setVisible(true);
490    }
491
492    private static int findViewRow(JTable table, TableModel model, Object value) {
493        for (int i = 0; i < model.getRowCount(); i++) {
494            if (model.getValueAt(i, 0).equals(value))
495                return table.convertRowIndexToView(i);
496        }
497        return -1;
498    }
499
500    /**
501     * Update selection status, call @{link #selectionChanged} function.
502     */
503    private void updateSelection() {
504        // Parameter is ignored in this class
505        selectionChanged(null);
506    }
507
508    @Override
509    public void showNotify() {
510        DatasetEventManager.getInstance().addDatasetListener(dataChangedAdapter, FireMode.IN_EDT_CONSOLIDATED);
511        SelectionEventManager.getInstance().addSelectionListener(this, FireMode.IN_EDT_CONSOLIDATED);
512        Main.getLayerManager().addActiveLayerChangeListener(this);
513        for (JosmAction action : josmActions) {
514            Main.registerActionShortcut(action);
515        }
516        updateSelection();
517    }
518
519    @Override
520    public void hideNotify() {
521        DatasetEventManager.getInstance().removeDatasetListener(dataChangedAdapter);
522        SelectionEventManager.getInstance().removeSelectionListener(this);
523        Main.getLayerManager().removeActiveLayerChangeListener(this);
524        for (JosmAction action : josmActions) {
525            Main.unregisterActionShortcut(action);
526        }
527    }
528
529    @Override
530    public void setVisible(boolean b) {
531        super.setVisible(b);
532        if (b && Main.getLayerManager().getEditDataSet() != null) {
533            updateSelection();
534        }
535    }
536
537    @Override
538    public void destroy() {
539        super.destroy();
540        Main.pref.removeKeyPreferenceChangeListener("display.discardable-keys", preferenceListener);
541        Container parent = pluginHook.getParent();
542        if (parent != null) {
543            parent.remove(pluginHook);
544        }
545    }
546
547    @Override
548    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
549        if (!isVisible())
550            return;
551        if (tagTable == null)
552            return; // selection changed may be received in base class constructor before init
553        if (tagTable.getCellEditor() != null) {
554            tagTable.getCellEditor().cancelCellEditing();
555        }
556
557        // Ignore parameter as we do not want to operate always on real selection here, especially in draw mode
558        Collection<OsmPrimitive> newSel = Main.main.getInProgressSelection();
559        if (newSel == null) {
560            newSel = Collections.<OsmPrimitive>emptyList();
561        }
562
563        String selectedTag;
564        Relation selectedRelation = null;
565        selectedTag = editHelper.getChangedKey(); // select last added or last edited key by default
566        if (selectedTag == null && tagTable.getSelectedRowCount() == 1) {
567            selectedTag = editHelper.getDataKey(tagTable.getSelectedRow());
568        }
569        if (membershipTable.getSelectedRowCount() == 1) {
570            selectedRelation = (Relation) membershipData.getValueAt(membershipTable.getSelectedRow(), 0);
571        }
572
573        // re-load tag data
574        tagData.setRowCount(0);
575
576        final boolean displayDiscardableKeys = Main.pref.getBoolean("display.discardable-keys", false);
577        final Map<String, Integer> keyCount = new HashMap<>();
578        final Map<String, String> tags = new HashMap<>();
579        valueCount.clear();
580        Set<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
581        for (OsmPrimitive osm : newSel) {
582            types.add(TaggingPresetType.forPrimitive(osm));
583            for (String key : osm.keySet()) {
584                if (displayDiscardableKeys || !OsmPrimitive.getDiscardableKeys().contains(key)) {
585                    String value = osm.get(key);
586                    keyCount.put(key, keyCount.containsKey(key) ? keyCount.get(key) + 1 : 1);
587                    if (valueCount.containsKey(key)) {
588                        Map<String, Integer> v = valueCount.get(key);
589                        v.put(value, v.containsKey(value) ? v.get(value) + 1 : 1);
590                    } else {
591                        Map<String, Integer> v = new TreeMap<>();
592                        v.put(value, 1);
593                        valueCount.put(key, v);
594                    }
595                }
596            }
597        }
598        for (Entry<String, Map<String, Integer>> e : valueCount.entrySet()) {
599            int count = 0;
600            for (Entry<String, Integer> e1 : e.getValue().entrySet()) {
601                count += e1.getValue();
602            }
603            if (count < newSel.size()) {
604                e.getValue().put("", newSel.size() - count);
605            }
606            tagData.addRow(new Object[]{e.getKey(), e.getValue()});
607            tags.put(e.getKey(), e.getValue().size() == 1
608                    ? e.getValue().keySet().iterator().next() : tr("<different>"));
609        }
610
611        membershipData.setRowCount(0);
612
613        Map<Relation, MemberInfo> roles = new HashMap<>();
614        for (OsmPrimitive primitive: newSel) {
615            for (OsmPrimitive ref: primitive.getReferrers(true)) {
616                if (ref instanceof Relation && !ref.isIncomplete() && !ref.isDeleted()) {
617                    Relation r = (Relation) ref;
618                    MemberInfo mi = roles.get(r);
619                    if (mi == null) {
620                        mi = new MemberInfo(newSel);
621                    }
622                    roles.put(r, mi);
623                    int i = 1;
624                    for (RelationMember m : r.getMembers()) {
625                        if (m.getMember() == primitive) {
626                            mi.add(m, i);
627                        }
628                        ++i;
629                    }
630                }
631            }
632        }
633
634        List<Relation> sortedRelations = new ArrayList<>(roles.keySet());
635        sortedRelations.sort((o1, o2) -> {
636            int comp = Boolean.compare(o1.isDisabledAndHidden(), o2.isDisabledAndHidden());
637            return comp != 0 ? comp : DefaultNameFormatter.getInstance().getRelationComparator().compare(o1, o2);
638        });
639
640        for (Relation r: sortedRelations) {
641            membershipData.addRow(new Object[]{r, roles.get(r)});
642        }
643
644        presets.updatePresets(types, tags, presetHandler);
645
646        membershipTable.getTableHeader().setVisible(membershipData.getRowCount() > 0);
647        membershipTable.setVisible(membershipData.getRowCount() > 0);
648
649        boolean hasSelection = !newSel.isEmpty();
650        boolean hasTags = hasSelection && tagData.getRowCount() > 0;
651        boolean hasMemberships = hasSelection && membershipData.getRowCount() > 0;
652        addAction.setEnabled(hasSelection);
653        editAction.setEnabled(hasTags || hasMemberships);
654        deleteAction.setEnabled(hasTags || hasMemberships);
655        tagTable.setVisible(hasTags);
656        tagTable.getTableHeader().setVisible(hasTags);
657        tagTableFilter.setVisible(hasTags);
658        selectSth.setVisible(!hasSelection);
659        pluginHook.setVisible(hasSelection);
660
661        int selectedIndex;
662        if (selectedTag != null && (selectedIndex = findViewRow(tagTable, tagData, selectedTag)) != -1) {
663            tagTable.changeSelection(selectedIndex, 0, false, false);
664        } else if (selectedRelation != null && (selectedIndex = findViewRow(membershipTable, membershipData, selectedRelation)) != -1) {
665            membershipTable.changeSelection(selectedIndex, 0, false, false);
666        } else if (hasTags) {
667            tagTable.changeSelection(0, 0, false, false);
668        } else if (hasMemberships) {
669            membershipTable.changeSelection(0, 0, false, false);
670        }
671
672        if (tagData.getRowCount() != 0 || membershipData.getRowCount() != 0) {
673            if (newSel.size() > 1) {
674                setTitle(tr("Objects: {2} / Tags: {0} / Memberships: {1}",
675                    tagData.getRowCount(), membershipData.getRowCount(), newSel.size()));
676            } else {
677                setTitle(tr("Tags: {0} / Memberships: {1}",
678                    tagData.getRowCount(), membershipData.getRowCount()));
679            }
680        } else {
681            setTitle(tr("Tags / Memberships"));
682        }
683    }
684
685    /* ---------------------------------------------------------------------------------- */
686    /* ActiveLayerChangeListener                                                          */
687    /* ---------------------------------------------------------------------------------- */
688    @Override
689    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
690        if (e.getSource().getEditLayer() == null) {
691            editHelper.saveTagsIfNeeded();
692        }
693        // it is time to save history of tags
694        updateSelection();
695    }
696
697    @Override
698    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
699        updateSelection();
700    }
701
702    /**
703     * Replies the tag popup menu handler.
704     * @return The tag popup menu handler
705     */
706    public PopupMenuHandler getPropertyPopupMenuHandler() {
707        return tagMenuHandler;
708    }
709
710    /**
711     * Returns the selected tag.
712     * @return The current selected tag
713     */
714    public Tag getSelectedProperty() {
715        int row = tagTable.getSelectedRow();
716        if (row == -1) return null;
717        Map<String, Integer> map = editHelper.getDataValues(row);
718        return new Tag(
719                editHelper.getDataKey(row),
720                map.size() > 1 ? "" : map.keySet().iterator().next());
721    }
722
723    /**
724     * Replies the membership popup menu handler.
725     * @return The membership popup menu handler
726     */
727    public PopupMenuHandler getMembershipPopupMenuHandler() {
728        return membershipMenuHandler;
729    }
730
731    /**
732     * Returns the selected relation membership.
733     * @return The current selected relation membership
734     */
735    public IRelation getSelectedMembershipRelation() {
736        int row = membershipTable.getSelectedRow();
737        return row > -1 ? (IRelation) membershipData.getValueAt(row, 0) : null;
738    }
739
740    /**
741     * Adds a custom table cell renderer to render cells of the tags table.
742     *
743     * If the renderer is not capable performing a {@link TableCellRenderer#getTableCellRendererComponent},
744     * it should return {@code null} to fall back to the
745     * {@link PropertiesCellRenderer#getTableCellRendererComponent default implementation}.
746     * @param renderer the renderer to add
747     * @since 9149
748     */
749    public void addCustomPropertiesCellRenderer(TableCellRenderer renderer) {
750        cellRenderer.addCustomRenderer(renderer);
751    }
752
753    /**
754     * Removes a custom table cell renderer.
755     * @param renderer the renderer to remove
756     * @since 9149
757     */
758    public void removeCustomPropertiesCellRenderer(TableCellRenderer renderer) {
759        cellRenderer.removeCustomRenderer(renderer);
760    }
761
762    static final class MemberOfCellRenderer extends DefaultTableCellRenderer {
763        @Override
764        public Component getTableCellRendererComponent(JTable table, Object value,
765                boolean isSelected, boolean hasFocus, int row, int column) {
766            Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
767            if (value == null)
768                return this;
769            if (c instanceof JLabel) {
770                JLabel label = (JLabel) c;
771                Relation r = (Relation) value;
772                label.setText(r.getDisplayName(DefaultNameFormatter.getInstance()));
773                if (r.isDisabledAndHidden()) {
774                    label.setFont(label.getFont().deriveFont(Font.ITALIC));
775                }
776            }
777            return c;
778        }
779    }
780
781    static final class RoleCellRenderer extends DefaultTableCellRenderer {
782        @Override
783        public Component getTableCellRendererComponent(JTable table, Object value,
784                boolean isSelected, boolean hasFocus, int row, int column) {
785            if (value == null)
786                return this;
787            Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
788            boolean isDisabledAndHidden = ((Relation) table.getValueAt(row, 0)).isDisabledAndHidden();
789            if (c instanceof JLabel) {
790                JLabel label = (JLabel) c;
791                label.setText(((MemberInfo) value).getRoleString());
792                if (isDisabledAndHidden) {
793                    label.setFont(label.getFont().deriveFont(Font.ITALIC));
794                }
795            }
796            return c;
797        }
798    }
799
800    static final class PositionCellRenderer extends DefaultTableCellRenderer {
801        @Override
802        public Component getTableCellRendererComponent(JTable table, Object value,
803                boolean isSelected, boolean hasFocus, int row, int column) {
804            Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
805            boolean isDisabledAndHidden = ((Relation) table.getValueAt(row, 0)).isDisabledAndHidden();
806            if (c instanceof JLabel) {
807                JLabel label = (JLabel) c;
808                label.setText(((MemberInfo) table.getValueAt(row, 1)).getPositionString());
809                if (isDisabledAndHidden) {
810                    label.setFont(label.getFont().deriveFont(Font.ITALIC));
811                }
812            }
813            return c;
814        }
815    }
816
817    static final class BlankSpaceMenuLauncher extends PopupMenuLauncher {
818        BlankSpaceMenuLauncher(JPopupMenu menu) {
819            super(menu);
820        }
821
822        @Override
823        protected boolean checkSelection(Component component, Point p) {
824            if (component instanceof JTable) {
825                return ((JTable) component).rowAtPoint(p) == -1;
826            }
827            return true;
828        }
829    }
830
831    static final class TaggingPresetCommandHandler implements TaggingPresetHandler {
832        @Override
833        public void updateTags(List<Tag> tags) {
834            Command command = TaggingPreset.createCommand(getSelection(), tags);
835            if (command != null) {
836                Main.main.undoRedo.add(command);
837            }
838        }
839
840        @Override
841        public Collection<OsmPrimitive> getSelection() {
842            return Main.main == null ? Collections.<OsmPrimitive>emptyList() : Main.main.getInProgressSelection();
843        }
844    }
845
846    /**
847     * Class that watches for mouse clicks
848     * @author imi
849     */
850    public class MouseClickWatch extends MouseAdapter {
851        @Override
852        public void mouseClicked(MouseEvent e) {
853            if (e.getClickCount() < 2) {
854                // single click, clear selection in other table not clicked in
855                if (e.getSource() == tagTable) {
856                    membershipTable.clearSelection();
857                } else if (e.getSource() == membershipTable) {
858                    tagTable.clearSelection();
859                }
860            } else if (e.getSource() == tagTable) {
861                // double click, edit or add tag
862                int row = tagTable.rowAtPoint(e.getPoint());
863                if (row > -1) {
864                    boolean focusOnKey = tagTable.columnAtPoint(e.getPoint()) == 0;
865                    editHelper.editTag(row, focusOnKey);
866                } else {
867                    editHelper.addTag();
868                    btnAdd.requestFocusInWindow();
869                }
870            } else if (e.getSource() == membershipTable) {
871                int row = membershipTable.rowAtPoint(e.getPoint());
872                if (row > -1) {
873                    editMembership(row);
874                }
875            } else {
876                editHelper.addTag();
877                btnAdd.requestFocusInWindow();
878            }
879        }
880
881        @Override
882        public void mousePressed(MouseEvent e) {
883            if (e.getSource() == tagTable) {
884                membershipTable.clearSelection();
885            } else if (e.getSource() == membershipTable) {
886                tagTable.clearSelection();
887            }
888        }
889    }
890
891    static class MemberInfo {
892        private final List<RelationMember> role = new ArrayList<>();
893        private Set<OsmPrimitive> members = new HashSet<>();
894        private List<Integer> position = new ArrayList<>();
895        private Collection<OsmPrimitive> selection;
896        private String positionString;
897        private String roleString;
898
899        MemberInfo(Collection<OsmPrimitive> selection) {
900            this.selection = selection;
901        }
902
903        void add(RelationMember r, Integer p) {
904            role.add(r);
905            members.add(r.getMember());
906            position.add(p);
907        }
908
909        String getPositionString() {
910            if (positionString == null) {
911                positionString = Utils.getPositionListString(position);
912                // if not all objects from the selection are member of this relation
913                if (selection.stream().anyMatch(p -> !members.contains(p))) {
914                    positionString += ",\u2717";
915                }
916                members = null;
917                position = null;
918                selection = null;
919            }
920            return Utils.shortenString(positionString, 20);
921        }
922
923        String getRoleString() {
924            if (roleString == null) {
925                for (RelationMember r : role) {
926                    if (roleString == null) {
927                        roleString = r.getRole();
928                    } else if (!roleString.equals(r.getRole())) {
929                        roleString = tr("<different>");
930                        break;
931                    }
932                }
933            }
934            return roleString;
935        }
936
937        @Override
938        public String toString() {
939            return "MemberInfo{" +
940                    "roles='" + roleString + '\'' +
941                    ", positions='" + positionString + '\'' +
942                    '}';
943        }
944    }
945
946    /**
947     * Class that allows fast creation of read-only table model with String columns
948     */
949    public static class ReadOnlyTableModel extends DefaultTableModel {
950        @Override
951        public boolean isCellEditable(int row, int column) {
952            return false;
953        }
954
955        @Override
956        public Class<?> getColumnClass(int columnIndex) {
957            return String.class;
958        }
959    }
960
961    /**
962     * Action handling delete button press in properties dialog.
963     */
964    class DeleteAction extends JosmAction implements ListSelectionListener {
965
966        private static final String DELETE_FROM_RELATION_PREF = "delete_from_relation";
967
968        DeleteAction() {
969            super(tr("Delete"), /* ICON() */ "dialogs/delete", tr("Delete the selected key in all objects"),
970                    Shortcut.registerShortcut("properties:delete", tr("Delete Tags"), KeyEvent.VK_D,
971                            Shortcut.ALT_CTRL_SHIFT), false);
972            updateEnabledState();
973        }
974
975        protected void deleteTags(int ... rows) {
976            // convert list of rows to HashMap (and find gap for nextKey)
977            Map<String, String> tags = new HashMap<>(rows.length);
978            int nextKeyIndex = rows[0];
979            for (int row : rows) {
980                String key = editHelper.getDataKey(row);
981                if (row == nextKeyIndex + 1) {
982                    nextKeyIndex = row; // no gap yet
983                }
984                tags.put(key, null);
985            }
986
987            // find key to select after deleting other tags
988            String nextKey = null;
989            int rowCount = tagData.getRowCount();
990            if (rowCount > rows.length) {
991                if (nextKeyIndex == rows[rows.length-1]) {
992                    // no gap found, pick next or previous key in list
993                    nextKeyIndex = nextKeyIndex + 1 < rowCount ? nextKeyIndex + 1 : rows[0] - 1;
994                } else {
995                    // gap found
996                    nextKeyIndex++;
997                }
998                nextKey = editHelper.getDataKey(nextKeyIndex);
999            }
1000
1001            Collection<OsmPrimitive> sel = Main.main.getInProgressSelection();
1002            Main.main.undoRedo.add(new ChangePropertyCommand(sel, tags));
1003
1004            membershipTable.clearSelection();
1005            if (nextKey != null) {
1006                tagTable.changeSelection(findViewRow(tagTable, tagData, nextKey), 0, false, false);
1007            }
1008        }
1009
1010        protected void deleteFromRelation(int row) {
1011            Relation cur = (Relation) membershipData.getValueAt(row, 0);
1012
1013            Relation nextRelation = null;
1014            int rowCount = membershipTable.getRowCount();
1015            if (rowCount > 1) {
1016                nextRelation = (Relation) membershipData.getValueAt(row + 1 < rowCount ? row + 1 : row - 1, 0);
1017            }
1018
1019            ExtendedDialog ed = new ExtendedDialog(Main.parent,
1020                    tr("Change relation"),
1021                    new String[] {tr("Delete from relation"), tr("Cancel")});
1022            ed.setButtonIcons(new String[] {"dialogs/delete", "cancel"});
1023            ed.setContent(tr("Really delete selection from relation {0}?", cur.getDisplayName(DefaultNameFormatter.getInstance())));
1024            ed.toggleEnable(DELETE_FROM_RELATION_PREF);
1025            ed.showDialog();
1026
1027            if (ed.getValue() != 1)
1028                return;
1029
1030            Relation rel = new Relation(cur);
1031            for (OsmPrimitive primitive: Main.main.getInProgressSelection()) {
1032                rel.removeMembersFor(primitive);
1033            }
1034            Main.main.undoRedo.add(new ChangeCommand(cur, rel));
1035
1036            tagTable.clearSelection();
1037            if (nextRelation != null) {
1038                membershipTable.changeSelection(findViewRow(membershipTable, membershipData, nextRelation), 0, false, false);
1039            }
1040        }
1041
1042        @Override
1043        public void actionPerformed(ActionEvent e) {
1044            if (tagTable.getSelectedRowCount() > 0) {
1045                int[] rows = tagTable.getSelectedRows();
1046                deleteTags(rows);
1047            } else if (membershipTable.getSelectedRowCount() > 0) {
1048                ConditionalOptionPaneUtil.startBulkOperation(DELETE_FROM_RELATION_PREF);
1049                int[] rows = membershipTable.getSelectedRows();
1050                // delete from last relation to conserve row numbers in the table
1051                for (int i = rows.length-1; i >= 0; i--) {
1052                    deleteFromRelation(rows[i]);
1053                }
1054                ConditionalOptionPaneUtil.endBulkOperation(DELETE_FROM_RELATION_PREF);
1055            }
1056        }
1057
1058        @Override
1059        protected final void updateEnabledState() {
1060            setEnabled(
1061                    (tagTable != null && tagTable.getSelectedRowCount() >= 1)
1062                    || (membershipTable != null && membershipTable.getSelectedRowCount() > 0)
1063                    );
1064        }
1065
1066        @Override
1067        public void valueChanged(ListSelectionEvent e) {
1068            updateEnabledState();
1069        }
1070    }
1071
1072    /**
1073     * Action handling add button press in properties dialog.
1074     */
1075    class AddAction extends JosmAction {
1076        AddAction() {
1077            super(tr("Add"), /* ICON() */ "dialogs/add", tr("Add a new key/value pair to all objects"),
1078                    Shortcut.registerShortcut("properties:add", tr("Add Tag"), KeyEvent.VK_A,
1079                            Shortcut.ALT), false);
1080        }
1081
1082        @Override
1083        public void actionPerformed(ActionEvent e) {
1084            editHelper.addTag();
1085            btnAdd.requestFocusInWindow();
1086        }
1087    }
1088
1089    /**
1090     * Action handling edit button press in properties dialog.
1091     */
1092    class EditAction extends JosmAction implements ListSelectionListener {
1093        EditAction() {
1094            super(tr("Edit"), /* ICON() */ "dialogs/edit", tr("Edit the value of the selected key for all objects"),
1095                    Shortcut.registerShortcut("properties:edit", tr("Edit Tags"), KeyEvent.VK_S,
1096                            Shortcut.ALT), false);
1097            updateEnabledState();
1098        }
1099
1100        @Override
1101        public void actionPerformed(ActionEvent e) {
1102            if (!isEnabled())
1103                return;
1104            if (tagTable.getSelectedRowCount() == 1) {
1105                int row = tagTable.getSelectedRow();
1106                editHelper.editTag(row, false);
1107            } else if (membershipTable.getSelectedRowCount() == 1) {
1108                int row = membershipTable.getSelectedRow();
1109                editMembership(row);
1110            }
1111        }
1112
1113        @Override
1114        protected void updateEnabledState() {
1115            setEnabled(
1116                    (tagTable != null && tagTable.getSelectedRowCount() == 1)
1117                    ^ (membershipTable != null && membershipTable.getSelectedRowCount() == 1)
1118                    );
1119        }
1120
1121        @Override
1122        public void valueChanged(ListSelectionEvent e) {
1123            updateEnabledState();
1124        }
1125    }
1126
1127    class HelpAction extends AbstractAction {
1128        HelpAction() {
1129            putValue(NAME, tr("Go to OSM wiki for tag help"));
1130            putValue(SHORT_DESCRIPTION, tr("Launch browser with wiki help for selected object"));
1131            putValue(SMALL_ICON, ImageProvider.get("dialogs", "search"));
1132            putValue(ACCELERATOR_KEY, getKeyStroke());
1133        }
1134
1135        public KeyStroke getKeyStroke() {
1136            return KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0);
1137        }
1138
1139        @Override
1140        public void actionPerformed(ActionEvent e) {
1141            try {
1142                String base = Main.pref.get("url.openstreetmap-wiki", "https://wiki.openstreetmap.org/wiki/");
1143                String lang = LanguageInfo.getWikiLanguagePrefix();
1144                final List<URI> uris = new ArrayList<>();
1145                int row;
1146                if (tagTable.getSelectedRowCount() == 1) {
1147                    row = tagTable.getSelectedRow();
1148                    String key = Utils.encodeUrl(editHelper.getDataKey(row));
1149                    Map<String, Integer> m = editHelper.getDataValues(row);
1150                    String val = Utils.encodeUrl(m.entrySet().iterator().next().getKey());
1151
1152                    uris.add(new URI(String.format("%s%sTag:%s=%s", base, lang, key, val)));
1153                    uris.add(new URI(String.format("%sTag:%s=%s", base, key, val)));
1154                    uris.add(new URI(String.format("%s%sKey:%s", base, lang, key)));
1155                    uris.add(new URI(String.format("%sKey:%s", base, key)));
1156                    uris.add(new URI(String.format("%s%sMap_Features", base, lang)));
1157                    uris.add(new URI(String.format("%sMap_Features", base)));
1158                } else if (membershipTable.getSelectedRowCount() == 1) {
1159                    row = membershipTable.getSelectedRow();
1160                    String type = ((Relation) membershipData.getValueAt(row, 0)).get("type");
1161                    if (type != null) {
1162                        type = Utils.encodeUrl(type);
1163                    }
1164
1165                    if (type != null && !type.isEmpty()) {
1166                        uris.add(new URI(String.format("%s%sRelation:%s", base, lang, type)));
1167                        uris.add(new URI(String.format("%sRelation:%s", base, type)));
1168                    }
1169
1170                    uris.add(new URI(String.format("%s%sRelations", base, lang)));
1171                    uris.add(new URI(String.format("%sRelations", base)));
1172                } else {
1173                    // give the generic help page, if more than one element is selected
1174                    uris.add(new URI(String.format("%s%sMap_Features", base, lang)));
1175                    uris.add(new URI(String.format("%sMap_Features", base)));
1176                }
1177
1178                Main.worker.execute(() -> displayHelp(uris));
1179            } catch (URISyntaxException e1) {
1180                Main.error(e1);
1181            }
1182        }
1183
1184        private void displayHelp(final List<URI> uris) {
1185            try {
1186                // find a page that actually exists in the wiki
1187                HttpClient.Response conn;
1188                for (URI u : uris) {
1189                    conn = HttpClient.create(u.toURL(), "HEAD").connect();
1190
1191                    if (conn.getResponseCode() != 200) {
1192                        conn.disconnect();
1193                    } else {
1194                        long osize = conn.getContentLength();
1195                        if (osize > -1) {
1196                            conn.disconnect();
1197
1198                            final URI newURI = new URI(u.toString()
1199                                    .replace("=", "%3D") /* do not URLencode whole string! */
1200                                    .replaceFirst("/wiki/", "/w/index.php?redirect=no&title=")
1201                            );
1202                            conn = HttpClient.create(newURI.toURL(), "HEAD").connect();
1203                        }
1204
1205                        /* redirect pages have different content length, but retrieving a "nonredirect"
1206                         *  page using index.php and the direct-link method gives slightly different
1207                         *  content lengths, so we have to be fuzzy.. (this is UGLY, recode if u know better)
1208                         */
1209                        if (conn.getContentLength() != -1 && osize > -1 && Math.abs(conn.getContentLength() - osize) > 200) {
1210                            Main.info("{0} is a mediawiki redirect", u);
1211                            conn.disconnect();
1212                        } else {
1213                            conn.disconnect();
1214
1215                            OpenBrowser.displayUrl(u.toString());
1216                            break;
1217                        }
1218                    }
1219                }
1220            } catch (URISyntaxException | IOException e1) {
1221                Main.error(e1);
1222            }
1223        }
1224    }
1225
1226    class TaginfoAction extends JosmAction {
1227
1228        final transient StringProperty TAGINFO_URL_PROP = new StringProperty("taginfo.url", "https://taginfo.openstreetmap.org/");
1229
1230        TaginfoAction() {
1231            super(tr("Go to Taginfo"), "dialogs/taginfo", tr("Launch browser with Taginfo statistics for selected object"), null, false);
1232        }
1233
1234        @Override
1235        public void actionPerformed(ActionEvent e) {
1236            final String url;
1237            if (tagTable.getSelectedRowCount() == 1) {
1238                final int row = tagTable.getSelectedRow();
1239                final String key = Utils.encodeUrl(editHelper.getDataKey(row));
1240                Map<String, Integer> values = editHelper.getDataValues(row);
1241                if (values.size() == 1) {
1242                    url = TAGINFO_URL_PROP.get() + "tags/" + key /* do not URL encode key, otherwise addr:street does not work */
1243                            + '=' + Utils.encodeUrl(values.keySet().iterator().next());
1244                } else {
1245                    url = TAGINFO_URL_PROP.get() + "keys/" + key; /* do not URL encode key, otherwise addr:street does not work */
1246                }
1247            } else if (membershipTable.getSelectedRowCount() == 1) {
1248                final String type = ((Relation) membershipData.getValueAt(membershipTable.getSelectedRow(), 0)).get("type");
1249                url = TAGINFO_URL_PROP.get() + "relations/" + type;
1250            } else {
1251                return;
1252            }
1253            OpenBrowser.displayUrl(url);
1254        }
1255    }
1256
1257    class PasteValueAction extends AbstractAction {
1258        PasteValueAction() {
1259            putValue(NAME, tr("Paste Value"));
1260            putValue(SHORT_DESCRIPTION, tr("Paste the value of the selected tag from clipboard"));
1261        }
1262
1263        @Override
1264        public void actionPerformed(ActionEvent ae) {
1265            if (tagTable.getSelectedRowCount() != 1)
1266                return;
1267            String key = editHelper.getDataKey(tagTable.getSelectedRow());
1268            Collection<OsmPrimitive> sel = Main.main.getInProgressSelection();
1269            String clipboard = ClipboardUtils.getClipboardStringContent();
1270            if (sel.isEmpty() || clipboard == null)
1271                return;
1272            Main.main.undoRedo.add(new ChangePropertyCommand(sel, key, Utils.strip(clipboard)));
1273        }
1274    }
1275
1276    abstract class AbstractCopyAction extends AbstractAction {
1277
1278        protected abstract Collection<String> getString(OsmPrimitive p, String key);
1279
1280        @Override
1281        public void actionPerformed(ActionEvent ae) {
1282            int[] rows = tagTable.getSelectedRows();
1283            Set<String> values = new TreeSet<>();
1284            Collection<OsmPrimitive> sel = Main.main.getInProgressSelection();
1285            if (rows.length == 0 || sel.isEmpty()) return;
1286
1287            for (int row: rows) {
1288                String key = editHelper.getDataKey(row);
1289                if (sel.isEmpty())
1290                    return;
1291                for (OsmPrimitive p : sel) {
1292                    Collection<String> s = getString(p, key);
1293                    if (s != null) {
1294                        values.addAll(s);
1295                    }
1296                }
1297            }
1298            if (!values.isEmpty()) {
1299                ClipboardUtils.copyString(Utils.join("\n", values));
1300            }
1301        }
1302    }
1303
1304    class CopyValueAction extends AbstractCopyAction {
1305
1306        /**
1307         * Constructs a new {@code CopyValueAction}.
1308         */
1309        CopyValueAction() {
1310            putValue(NAME, tr("Copy Value"));
1311            putValue(SHORT_DESCRIPTION, tr("Copy the value of the selected tag to clipboard"));
1312        }
1313
1314        @Override
1315        protected Collection<String> getString(OsmPrimitive p, String key) {
1316            String v = p.get(key);
1317            return v == null ? null : Collections.singleton(v);
1318        }
1319    }
1320
1321    class CopyKeyValueAction extends AbstractCopyAction {
1322
1323        CopyKeyValueAction() {
1324            putValue(NAME, tr("Copy selected Key(s)/Value(s)"));
1325            putValue(SHORT_DESCRIPTION, tr("Copy the key and value of the selected tag(s) to clipboard"));
1326        }
1327
1328        @Override
1329        protected Collection<String> getString(OsmPrimitive p, String key) {
1330            String v = p.get(key);
1331            return v == null ? null : Collections.singleton(new Tag(key, v).toString());
1332        }
1333    }
1334
1335    class CopyAllKeyValueAction extends AbstractCopyAction {
1336
1337        CopyAllKeyValueAction() {
1338            putValue(NAME, tr("Copy all Keys/Values"));
1339            putValue(SHORT_DESCRIPTION, tr("Copy the key and value of all the tags to clipboard"));
1340            Shortcut sc = Shortcut.registerShortcut("system:copytags", tr("Edit: {0}", tr("Copy Tags")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
1341            Main.registerActionShortcut(this, sc);
1342            sc.setAccelerator(this);
1343        }
1344
1345        @Override
1346        protected Collection<String> getString(OsmPrimitive p, String key) {
1347            List<String> r = new LinkedList<>();
1348            for (Entry<String, String> kv : p.getKeys().entrySet()) {
1349                r.add(new Tag(kv.getKey(), kv.getValue()).toString());
1350            }
1351            return r;
1352        }
1353    }
1354
1355    class SearchAction extends AbstractAction {
1356        private final boolean sameType;
1357
1358        SearchAction(boolean sameType) {
1359            this.sameType = sameType;
1360            if (sameType) {
1361                putValue(NAME, tr("Search Key/Value/Type"));
1362                putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag, restrict to type (i.e., node/way/relation)"));
1363            } else {
1364                putValue(NAME, tr("Search Key/Value"));
1365                putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag"));
1366            }
1367        }
1368
1369        @Override
1370        public void actionPerformed(ActionEvent e) {
1371            if (tagTable.getSelectedRowCount() != 1)
1372                return;
1373            String key = editHelper.getDataKey(tagTable.getSelectedRow());
1374            Collection<OsmPrimitive> sel = Main.main.getInProgressSelection();
1375            if (sel.isEmpty())
1376                return;
1377            final SearchSetting ss = createSearchSetting(key, sel, sameType);
1378            org.openstreetmap.josm.actions.search.SearchAction.searchWithoutHistory(ss);
1379        }
1380    }
1381
1382    static SearchSetting createSearchSetting(String key, Collection<OsmPrimitive> sel, boolean sameType) {
1383        String sep = "";
1384        StringBuilder s = new StringBuilder();
1385        Set<String> consideredTokens = new TreeSet<>();
1386        for (OsmPrimitive p : sel) {
1387            String val = p.get(key);
1388            if (val == null || (!sameType && consideredTokens.contains(val))) {
1389                continue;
1390            }
1391            String t = "";
1392            if (!sameType) {
1393                t = "";
1394            } else if (p instanceof Node) {
1395                t = "type:node ";
1396            } else if (p instanceof Way) {
1397                t = "type:way ";
1398            } else if (p instanceof Relation) {
1399                t = "type:relation ";
1400            }
1401            String token = new StringBuilder(t).append(val).toString();
1402            if (consideredTokens.add(token)) {
1403                s.append(sep).append('(').append(t).append(SearchCompiler.buildSearchStringForTag(key, val)).append(')');
1404                sep = " OR ";
1405            }
1406        }
1407
1408        final SearchSetting ss = new SearchSetting();
1409        ss.text = s.toString();
1410        ss.caseSensitive = true;
1411        return ss;
1412    }
1413
1414    /**
1415     * Clears the row selection when it is filtered away by the row sorter.
1416     */
1417    private class RemoveHiddenSelection implements ListSelectionListener, RowSorterListener {
1418
1419        void removeHiddenSelection() {
1420            try {
1421                tagRowSorter.convertRowIndexToModel(tagTable.getSelectedRow());
1422            } catch (IndexOutOfBoundsException ignore) {
1423                Main.trace(ignore);
1424                Main.trace("Clearing tagTable selection");
1425                tagTable.clearSelection();
1426            }
1427        }
1428
1429        @Override
1430        public void valueChanged(ListSelectionEvent event) {
1431            removeHiddenSelection();
1432        }
1433
1434        @Override
1435        public void sorterChanged(RowSorterEvent e) {
1436            removeHiddenSelection();
1437        }
1438    }
1439}