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