// 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.BorderLayout;
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.Collection;
import java.util.Collections;
import java.util.Comparator;
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.DefaultListSelectionModel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
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.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 {
//private static final Logger logger = Logger.getLogger(RelationListDialog.class.getName());
/** 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());
add(new JScrollPane(displaylist), BorderLayout.CENTER);
// create the panel with buttons
//
JPanel tp = getButtonPanel(5);
// the new action
//
newAction = new NewAction();
tp.add(new SideButton(newAction, false));
// the edit action
//
editAction = new EditAction();
displaylist.addListSelectionListener(editAction);
tp.add(new SideButton(editAction, false));
// the duplicate action
//
DuplicateAction duplicateAction = new DuplicateAction();
displaylist.addListSelectionListener(duplicateAction);
tp.add(new SideButton(duplicateAction, false));
// the delete action
//
deleteAction = new DeleteAction();
displaylist.addListSelectionListener(deleteAction);
tp.add(new SideButton(deleteAction, false));
// the select action
//
SelectAction selectAction = new SelectAction();
displaylist.addListSelectionListener(selectAction);
tp.add(new SideButton(selectAction, false));
add(tp, BorderLayout.SOUTH);
// 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();
}
@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;
int [] idx = displaylist.getSelectedIndices();
ArrayList toDelete = new ArrayList(idx.length);
for (int i: idx) {
toDelete.add(model.getRelation(i));
}
for (Relation r: toDelete) {
deleteRelation(r);
}
}
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{
public SelectAction() {
putValue(SHORT_DESCRIPTION,tr("Set the current selection to the list of selected relations"));
putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
setEnabled(false);
}
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));
}
Main.map.mapView.getEditLayer().data.setSelected(selection);
}
public void valueChanged(ListSelectionEvent e) {
setEnabled(displaylist.getSelectedIndices() != null && displaylist.getSelectedIndices().length > 0);
}
}
/**
* Sets the current selection to the list of relations selected in this dialog
*
*/
class SelectMembersAction extends AbstractAction implements ListSelectionListener{
public SelectMembersAction() {
putValue(SHORT_DESCRIPTION,tr("Select the members of all selected relations"));
putValue(SMALL_ICON, ImageProvider.get("selectall"));
putValue(NAME, tr("Select members"));
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());
}
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 extends OsmPrimitive> 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 extends OsmPrimitive> 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 JPopupMenu {
protected void build() {
// -- download members action
//
DownloadMembersAction downloadMembersAction = new DownloadMembersAction();
displaylist.addListSelectionListener(downloadMembersAction);
add(downloadMembersAction);
// -- download incomplete members action
//
DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembers = new DownloadSelectedIncompleteMembersAction();
displaylist.addListSelectionListener(downloadSelectedIncompleteMembers);
add(downloadSelectedIncompleteMembers);
addSeparator();
// -- select members action
//
SelectMembersAction selectMembersAction = new SelectMembersAction();
displaylist.addListSelectionListener(selectMembersAction);
add(selectMembersAction);
}
public RelationDialogPopupMenu() {
build();
}
}
/* ---------------------------------------------------------------------------------- */
/* DataSetListener */
/* ---------------------------------------------------------------------------------- */
public void nodeMoved(NodeMovedEvent event) {/* irrelevant in this context */}
public void wayNodesChanged(WayNodesChangedEvent event) {/* irrelevant in this context */}
public void primtivesAdded(final PrimitivesAddedEvent event) {
model.addRelations(event.getPrimitives());
model.updateTitle();
}
public void primtivesRemoved(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 */}
}