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