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