// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.dialogs; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.AbstractListModel; import javax.swing.Action; import javax.swing.DefaultListSelectionModel; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.PopupMenuListener; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; import org.openstreetmap.josm.data.osm.event.DataChangedEvent; import org.openstreetmap.josm.data.osm.event.DataSetListener; import org.openstreetmap.josm.data.osm.event.DatasetEventManager; import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode; import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; import org.openstreetmap.josm.gui.DefaultNameFormatter; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.MapView.LayerChangeListener; import org.openstreetmap.josm.gui.OsmPrimitivRenderer; import org.openstreetmap.josm.gui.SideButton; import org.openstreetmap.josm.gui.dialogs.relation.DownloadRelationMemberTask; import org.openstreetmap.josm.gui.dialogs.relation.DownloadRelationTask; import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.widgets.ListPopupMenu; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Shortcut; /** * A dialog showing all known relations, with buttons to add, edit, and * delete them. * * We don't have such dialogs for nodes, segments, and ways, because those * objects are visible on the map and can be selected there. Relations are not. */ public class RelationListDialog extends ToggleDialog implements DataSetListener { /** The display list. */ private JList displaylist; /** the list model used */ private RelationListModel model; /** the edit action */ private EditAction editAction; /** the delete action */ private DeleteAction deleteAction; private NewAction newAction; /** the popup menu */ private RelationDialogPopupMenu popupMenu; /** * constructor */ public RelationListDialog() { super(tr("Relations"), "relationlist", tr("Open a list of all relations."), Shortcut.registerShortcut("subwindow:relations", tr("Toggle: {0}", tr("Relations")), KeyEvent.VK_R, Shortcut.GROUP_LAYER, Shortcut.SHIFT_DEFAULT), 150); // create the list of relations // DefaultListSelectionModel selectionModel = new DefaultListSelectionModel(); model = new RelationListModel(selectionModel); displaylist = new JList(model); displaylist.setSelectionModel(selectionModel); displaylist.setCellRenderer(new OsmPrimitivRenderer() { /** * Don't show the default tooltip in the relation list. */ @Override protected String getComponentToolTipText(OsmPrimitive value) { return null; } }); displaylist.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); displaylist.addMouseListener(new MouseEventHandler()); // the new action // newAction = new NewAction(); // the edit action // editAction = new EditAction(); displaylist.addListSelectionListener(editAction); // the duplicate action // DuplicateAction duplicateAction = new DuplicateAction(); displaylist.addListSelectionListener(duplicateAction); // the delete action // deleteAction = new DeleteAction(); displaylist.addListSelectionListener(deleteAction); // the select action // SelectAction selectAction = new SelectAction(false); displaylist.addListSelectionListener(selectAction); createLayout(displaylist, true, Arrays.asList(new SideButton[] { new SideButton(newAction, false), new SideButton(editAction, false), new SideButton(duplicateAction, false), new SideButton(deleteAction, false), new SideButton(selectAction, false) })); // activate DEL in the list of relations displaylist.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE,0), "deleteRelation"); displaylist.getActionMap().put("deleteRelation", deleteAction); popupMenu = new RelationDialogPopupMenu(displaylist); } @Override public void showNotify() { MapView.addLayerChangeListener(newAction); newAction.updateEnabledState(); DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT); dataChanged(null); } @Override public void hideNotify() { MapView.removeLayerChangeListener(newAction); DatasetEventManager.getInstance().removeDatasetListener(this); } /** * Initializes the relation list dialog from a layer. If layer is null * or if it isn't an {@see OsmDataLayer} the dialog is reset to an empty dialog. * Otherwise it is initialized with the list of non-deleted and visible relations * in the layer's dataset. * * @param layer the layer. May be null. */ protected void initFromLayer(Layer layer) { if (layer == null || ! (layer instanceof OsmDataLayer)) { model.setRelations(null); return; } OsmDataLayer l = (OsmDataLayer)layer; model.setRelations(l.data.getRelations()); model.updateTitle(); } /** * Adds a selection listener to the relation list. * * @param listener the listener to add */ public void addListSelectionListener(ListSelectionListener listener) { displaylist.addListSelectionListener(listener); } /** * Removes a selection listener from the relation list. * * @param listener the listener to remove */ public void removeListSelectionListener(ListSelectionListener listener) { displaylist.removeListSelectionListener(listener); } /** * @return The selected relation in the list */ private Relation getSelected() { if(model.getSize() == 1) { displaylist.setSelectedIndex(0); } return (Relation) displaylist.getSelectedValue(); } /** * Selects the relation relation in the list of relations. * * @param relation the relation */ public void selectRelation(Relation relation) { if (relation == null) { model.setSelectedRelations(null); } else { model.setSelectedRelations(Collections.singletonList(relation)); Integer i = model.getRelationIndex(relation); if (i != null) { // Not all relations have to be in the list (for example when the relation list is hidden, it's not updated with new relations) displaylist.scrollRectToVisible(displaylist.getCellBounds(i, i)); } } } class MouseEventHandler extends MouseAdapter { protected void setCurrentRelationAsSelection() { Main.main.getCurrentDataSet().setSelected((Relation)displaylist.getSelectedValue()); } protected void editCurrentRelation() { new EditAction().launchEditor(getSelected()); } @Override public void mouseClicked(MouseEvent e) { if (Main.main.getEditLayer() == null) return; if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) { if (e.isControlDown()) { editCurrentRelation(); } else { setCurrentRelationAsSelection(); } } } private void openPopup(MouseEvent e) { Point p = e.getPoint(); int index = displaylist.locationToIndex(p); if (index < 0) return; if (!displaylist.getCellBounds(index, index).contains(e.getPoint())) return; if (! displaylist.isSelectedIndex(index)) { displaylist.setSelectedIndex(index); } popupMenu.show(displaylist, p.x, p.y-3); } @Override public void mousePressed(MouseEvent e) { if (Main.main.getEditLayer() == null) return; if (e.isPopupTrigger()) { openPopup(e); } } @Override public void mouseReleased(MouseEvent e) { if (Main.main.getEditLayer() == null) return; if (e.isPopupTrigger()) { openPopup(e); } } } /** * The edit action * */ class EditAction extends AbstractAction implements ListSelectionListener{ public EditAction() { putValue(SHORT_DESCRIPTION,tr( "Open an editor for the selected relation")); //putValue(NAME, tr("Edit")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit")); setEnabled(false); } protected Collection getMembersForCurrentSelection(Relation r) { Collection members = new HashSet(); Collection selection = Main.map.mapView.getEditLayer().data.getSelected(); for (RelationMember member: r.getMembers()) { if (selection.contains(member.getMember())) { members.add(member); } } return members; } public void launchEditor(Relation toEdit) { if (toEdit == null) return; RelationEditor.getEditor(Main.map.mapView.getEditLayer(),toEdit, getMembersForCurrentSelection(toEdit)).setVisible(true); } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; launchEditor(getSelected()); } public void valueChanged(ListSelectionEvent e) { setEnabled(displaylist.getSelectedIndices() != null && displaylist.getSelectedIndices().length == 1); } } /** * The delete action * */ class DeleteAction extends AbstractAction implements ListSelectionListener { class AbortException extends Exception {} public DeleteAction() { putValue(SHORT_DESCRIPTION,tr("Delete the selected relation")); //putValue(NAME, tr("Delete")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); setEnabled(false); } protected void deleteRelation(Relation toDelete) { if (toDelete == null) return; org.openstreetmap.josm.actions.mapmode.DeleteAction.deleteRelation( Main.main.getEditLayer(), toDelete ); } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; for (int i: displaylist.getSelectedIndices()) { deleteRelation(model.getRelation(i)); } } public void valueChanged(ListSelectionEvent e) { setEnabled(displaylist.getSelectedIndices() != null && displaylist.getSelectedIndices().length > 0); } } /** * The action for creating a new relation * */ static class NewAction extends AbstractAction implements LayerChangeListener{ public NewAction() { putValue(SHORT_DESCRIPTION,tr("Create a new relation")); //putValue(NAME, tr("New")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "addrelation")); updateEnabledState(); } public void run() { RelationEditor.getEditor(Main.main.getEditLayer(),null, null).setVisible(true); } public void actionPerformed(ActionEvent e) { run(); } protected void updateEnabledState() { setEnabled(Main.main != null && Main.main.getEditLayer() != null); } public void activeLayerChange(Layer oldLayer, Layer newLayer) { updateEnabledState(); } public void layerAdded(Layer newLayer) { updateEnabledState(); } public void layerRemoved(Layer oldLayer) { updateEnabledState(); } } /** * Creates a new relation with a copy of the current editor state * */ class DuplicateAction extends AbstractAction implements ListSelectionListener { public DuplicateAction() { putValue(SHORT_DESCRIPTION, tr("Create a copy of this relation and open it in another editor window")); putValue(SMALL_ICON, ImageProvider.get("duplicate")); //putValue(NAME, tr("Duplicate")); updateEnabledState(); } public void launchEditorForDuplicate(Relation original) { Relation copy = new Relation(original, true); copy.setModified(true); RelationEditor editor = RelationEditor.getEditor( Main.main.getEditLayer(), copy, null /* no selected members */ ); editor.setVisible(true); } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; launchEditorForDuplicate(getSelected()); } protected void updateEnabledState() { setEnabled(displaylist.getSelectedIndices() != null && displaylist.getSelectedIndices().length == 1); } public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * Sets the current selection to the list of relations selected in this dialog * */ class SelectAction extends AbstractAction implements ListSelectionListener{ boolean add; public SelectAction(boolean add) { putValue(SHORT_DESCRIPTION, add ? tr("Add the selected relations to the current selection") : tr("Set the current selection to the list of selected relations")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); putValue(NAME, add ? tr("Select relation (add)") : tr("Select relation")); this.add = add; updateEnabledState(); } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; int [] idx = displaylist.getSelectedIndices(); if (idx == null || idx.length == 0) return; ArrayList selection = new ArrayList(idx.length); for (int i: idx) { selection.add(model.getRelation(i)); } if(add) { Main.map.mapView.getEditLayer().data.addSelected(selection); } else { Main.map.mapView.getEditLayer().data.setSelected(selection); } } protected void updateEnabledState() { setEnabled(displaylist.getSelectedIndices() != null && displaylist.getSelectedIndices().length > 0); } public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * Sets the current selection to the list of relations selected in this dialog * */ class SelectMembersAction extends AbstractAction implements ListSelectionListener{ boolean add; public SelectMembersAction(boolean add) { putValue(SHORT_DESCRIPTION,add ? tr("Add the members of all selected relations to current selection") : tr("Select the members of all selected relations")); putValue(SMALL_ICON, ImageProvider.get("selectall")); putValue(NAME, add ? tr("Select members (add)") : tr("Select members")); this.add = add; updateEnabledState(); } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; List relations = model.getSelectedRelations(); HashSet members = new HashSet(); for(Relation r: relations) { members.addAll(r.getMemberPrimitives()); } if(add) { Main.map.mapView.getEditLayer().data.addSelected(members); } else { Main.map.mapView.getEditLayer().data.setSelected(members); } } protected void updateEnabledState() { setEnabled(displaylist.getSelectedIndices() != null && displaylist.getSelectedIndices().length > 0); } public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * The action for downloading members of all selected relations * */ class DownloadMembersAction extends AbstractAction implements ListSelectionListener{ public DownloadMembersAction() { putValue(SHORT_DESCRIPTION,tr("Download all members of the selected relations")); putValue(NAME, tr("Download members")); putValue(SMALL_ICON, ImageProvider.get("dialogs", "downloadincomplete")); putValue("help", ht("/Dialog/RelationList#DownloadMembers")); updateEnabledState(); } protected void updateEnabledState() { setEnabled(! model.getSelectedNonNewRelations().isEmpty()); } public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } public void actionPerformed(ActionEvent e) { List relations = model.getSelectedNonNewRelations(); if (relations.isEmpty()) return; Main.worker.submit(new DownloadRelationTask( model.getSelectedNonNewRelations(), Main.map.mapView.getEditLayer()) ); } } /** * Action for downloading incomplete members of selected relations * */ class DownloadSelectedIncompleteMembersAction extends AbstractAction implements ListSelectionListener{ public DownloadSelectedIncompleteMembersAction() { putValue(SHORT_DESCRIPTION, tr("Download incomplete members of selected relations")); putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincompleteselected")); putValue(NAME, tr("Download incomplete members")); updateEnabledState(); } public Set buildSetOfIncompleteMembers(List rels) { Set ret = new HashSet(); for(Relation r: rels) { ret.addAll(r.getIncompleteMembers()); } return ret; } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; List rels = model.getSelectedRelationsWithIncompleteMembers(); if (rels.isEmpty()) return; Main.worker.submit(new DownloadRelationMemberTask( rels, buildSetOfIncompleteMembers(rels), Main.map.mapView.getEditLayer() )); } protected void updateEnabledState() { setEnabled(!model.getSelectedRelationsWithIncompleteMembers().isEmpty()); } public void valueChanged(ListSelectionEvent e) { updateEnabledState(); } } /** * The list model for the list of relations displayed in the relation list * dialog. * */ private class RelationListModel extends AbstractListModel { private final ArrayList relations = new ArrayList(); private DefaultListSelectionModel selectionModel; public RelationListModel(DefaultListSelectionModel selectionModel) { this.selectionModel = selectionModel; } public Relation getRelation(int idx) { return relations.get(idx); } public void sort() { Collections.sort( relations, DefaultNameFormatter.getInstance().getRelationComparator() ); } private boolean isValid(Relation r) { return !r.isDeleted() && r.isVisible() && !r.isIncomplete(); } public void setRelations(Collection relations) { List sel = getSelectedRelations(); this.relations.clear(); if (relations == null) { selectionModel.clearSelection(); fireContentsChanged(this,0,getSize()); return; } for (Relation r: relations) { if (isValid(r)) { this.relations.add(r); } } sort(); fireIntervalAdded(this, 0, getSize()); setSelectedRelations(sel); } /** * Add all relations in addedPrimitives to the model for the * relation list dialog * * @param addedPrimitives the collection of added primitives. May include nodes, * ways, and relations. */ public void addRelations(Collection addedPrimitives) { boolean added = false; for (OsmPrimitive p: addedPrimitives) { if (! (p instanceof Relation)) { continue; } Relation r = (Relation)p; if (relations.contains(r)) { continue; } if (isValid(r)) { relations.add(r); added = true; } } if (added) { List sel = getSelectedRelations(); sort(); fireIntervalAdded(this, 0, getSize()); setSelectedRelations(sel); } } /** * Removes all relations in removedPrimitives from the model * * @param removedPrimitives the removed primitives. May include nodes, ways, * and relations */ public void removeRelations(Collection removedPrimitives) { if (removedPrimitives == null) return; // extract the removed relations // Set removedRelations = new HashSet(); for (OsmPrimitive p: removedPrimitives) { if (! (p instanceof Relation)) { continue; } removedRelations.add((Relation)p); } if (removedRelations.isEmpty()) return; int size = relations.size(); relations.removeAll(removedRelations); if (size != relations.size()) { List sel = getSelectedRelations(); sort(); fireContentsChanged(this, 0, getSize()); setSelectedRelations(sel); } } /** * Replies the list of selected relations with incomplete members * * @return the list of selected relations with incomplete members */ public List getSelectedRelationsWithIncompleteMembers() { List ret = getSelectedNonNewRelations(); Iterator it = ret.iterator(); while(it.hasNext()) { Relation r = it.next(); if (!r.hasIncompleteMembers()) { it.remove(); } } return ret; } public Object getElementAt(int index) { return relations.get(index); } public int getSize() { return relations.size(); } /** * Replies the list of selected, non-new relations. Empty list, * if there are no selected, non-new relations. * * @return the list of selected, non-new relations. */ public List getSelectedNonNewRelations() { ArrayList ret = new ArrayList(); for (int i=0; i getSelectedRelations() { ArrayList ret = new ArrayList(); for (int i=0; i sel) { selectionModel.clearSelection(); if (sel == null || sel.isEmpty()) return; for (Relation r: sel) { int i = relations.indexOf(r); if (i<0) { continue; } selectionModel.addSelectionInterval(i,i); } } /** * Returns the index of the relation * * @return index of relation (null if it cannot be found) */ public Integer getRelationIndex(Relation rel) { int i = relations.indexOf(rel); if (i<0) return null; return i; } public void updateTitle() { if (getSize() > 0) { RelationListDialog.this.setTitle(tr("Relations: {0}", getSize())); } else { RelationListDialog.this.setTitle(tr("Relations")); } } } class RelationDialogPopupMenu extends ListPopupMenu { public RelationDialogPopupMenu(JList list) { super(list); // -- download members action // add(new DownloadMembersAction()); // -- download incomplete members action // add(new DownloadSelectedIncompleteMembersAction()); addSeparator(); // -- select members action // add(new SelectMembersAction(false)); add(new SelectMembersAction(true)); // -- select action // add(new SelectAction(false)); add(new SelectAction(true)); } } public void addPopupMenuSeparator() { popupMenu.addSeparator(); } public JMenuItem addPopupMenuAction(Action a) { return popupMenu.add(a); } public void addPopupMenuListener(PopupMenuListener l) { popupMenu.addPopupMenuListener(l); } public void removePopupMenuListener(PopupMenuListener l) { popupMenu.addPopupMenuListener(l); } public Collection getSelectedRelations() { return model.getSelectedRelations(); } /* ---------------------------------------------------------------------------------- */ /* DataSetListener */ /* ---------------------------------------------------------------------------------- */ public void nodeMoved(NodeMovedEvent event) {/* irrelevant in this context */} public void wayNodesChanged(WayNodesChangedEvent event) {/* irrelevant in this context */} public void primitivesAdded(final PrimitivesAddedEvent event) { model.addRelations(event.getPrimitives()); model.updateTitle(); } public void primitivesRemoved(final PrimitivesRemovedEvent event) { model.removeRelations(event.getPrimitives()); model.updateTitle(); } public void relationMembersChanged(final RelationMembersChangedEvent event) { List sel = model.getSelectedRelations(); model.sort(); model.setSelectedRelations(sel); displaylist.repaint(); } public void tagsChanged(TagsChangedEvent event) { OsmPrimitive prim = event.getPrimitive(); if (prim == null || ! (prim instanceof Relation)) return; // trigger a sort of the relation list because the display name may // have changed // List sel = model.getSelectedRelations(); model.sort(); model.setSelectedRelations(sel); displaylist.repaint(); } public void dataChanged(DataChangedEvent event) { initFromLayer(Main.main.getEditLayer()); } public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* ignore */} }