// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs;
import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.AbstractInfoAction;
import org.openstreetmap.josm.data.SelectionChangedListener;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.User;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.OpenBrowser;
import org.openstreetmap.josm.tools.Shortcut;
import org.openstreetmap.josm.tools.Utils;
/**
* Displays a dialog with all users who have last edited something in the
* selection area, along with the number of objects.
*
*/
public class UserListDialog extends ToggleDialog implements SelectionChangedListener, ActiveLayerChangeListener {
/**
* The display list.
*/
private JTable userTable;
private UserTableModel model;
private SelectUsersPrimitivesAction selectionUsersPrimitivesAction;
/**
* Constructs a new {@code UserListDialog}.
*/
public UserListDialog() {
super(tr("Authors"), "userlist", tr("Open a list of people working on the selected objects."),
Shortcut.registerShortcut("subwindow:authors", tr("Toggle: {0}", tr("Authors")), KeyEvent.VK_A, Shortcut.ALT_SHIFT), 150);
build();
}
@Override
public void showNotify() {
DataSet.addSelectionListener(this);
Main.getLayerManager().addActiveLayerChangeListener(this);
}
@Override
public void hideNotify() {
Main.getLayerManager().removeActiveLayerChangeListener(this);
DataSet.removeSelectionListener(this);
}
protected void build() {
model = new UserTableModel();
userTable = new JTable(model);
userTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
userTable.addMouseListener(new DoubleClickAdapter());
// -- select users primitives action
//
selectionUsersPrimitivesAction = new SelectUsersPrimitivesAction();
userTable.getSelectionModel().addListSelectionListener(selectionUsersPrimitivesAction);
// -- info action
//
ShowUserInfoAction showUserInfoAction = new ShowUserInfoAction();
userTable.getSelectionModel().addListSelectionListener(showUserInfoAction);
createLayout(userTable, true, Arrays.asList(new SideButton[] {
new SideButton(selectionUsersPrimitivesAction),
new SideButton(showUserInfoAction)
}));
}
/**
* Called when the selection in the dataset changed.
* @param newSelection The new selection array.
*/
@Override
public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
refresh(newSelection);
}
@Override
public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
Layer activeLayer = e.getSource().getActiveLayer();
if (activeLayer instanceof OsmDataLayer) {
refresh(((OsmDataLayer) activeLayer).data.getAllSelected());
} else {
refresh(null);
}
}
/**
* Refreshes user list from given collection of OSM primitives.
* @param fromPrimitives OSM primitives to fetch users from
*/
public void refresh(Collection<? extends OsmPrimitive> fromPrimitives) {
model.populate(fromPrimitives);
GuiHelper.runInEDT(() -> {
if (model.getRowCount() != 0) {
setTitle(trn("{0} Author", "{0} Authors", model.getRowCount(), model.getRowCount()));
} else {
setTitle(tr("Authors"));
}
});
}
@Override
public void showDialog() {
super.showDialog();
Layer layer = Main.getLayerManager().getActiveLayer();
if (layer instanceof OsmDataLayer) {
refresh(((OsmDataLayer) layer).data.getAllSelected());
}
}
class SelectUsersPrimitivesAction extends AbstractAction implements ListSelectionListener {
/**
* Constructs a new {@code SelectUsersPrimitivesAction}.
*/
SelectUsersPrimitivesAction() {
putValue(NAME, tr("Select"));
putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user"));
new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true);
updateEnabledState();
}
public void select() {
int[] indexes = userTable.getSelectedRows();
if (indexes.length == 0)
return;
model.selectPrimitivesOwnedBy(userTable.getSelectedRows());
}
@Override
public void actionPerformed(ActionEvent e) {
select();
}
protected void updateEnabledState() {
setEnabled(userTable != null && userTable.getSelectedRowCount() > 0);
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
/**
* Action for launching the info page of a user.
*/
class ShowUserInfoAction extends AbstractInfoAction implements ListSelectionListener {
ShowUserInfoAction() {
super(false);
putValue(NAME, tr("Show info"));
putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user"));
new ImageProvider("help/internet").getResource().attachImageIcon(this, true);
updateEnabledState();
}
@Override
public void actionPerformed(ActionEvent e) {
int[] rows = userTable.getSelectedRows();
if (rows.length == 0)
return;
List<User> users = model.getSelectedUsers(rows);
if (users.isEmpty())
return;
if (users.size() > 10) {
Main.warn(tr("Only launching info browsers for the first {0} of {1} selected users", 10, users.size()));
}
int num = Math.min(10, users.size());
Iterator<User> it = users.iterator();
while (it.hasNext() && num > 0) {
String url = createInfoUrl(it.next());
if (url == null) {
break;
}
OpenBrowser.displayUrl(url);
num--;
}
}
@Override
protected String createInfoUrl(Object infoObject) {
if (infoObject instanceof User) {
User user = (User) infoObject;
return Main.getBaseUserUrl() + '/' + Utils.encodeUrl(user.getName()).replaceAll("\\+", "%20");
} else {
return null;
}
}
@Override
protected void updateEnabledState() {
setEnabled(userTable != null && userTable.getSelectedRowCount() > 0);
}
@Override
public void valueChanged(ListSelectionEvent e) {
updateEnabledState();
}
}
class DoubleClickAdapter extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
selectionUsersPrimitivesAction.select();
}
}
}
/**
* Action for selecting the primitives contributed by the currently selected users.
*
*/
private static class UserInfo implements Comparable<UserInfo> {
public final User user;
public final int count;
public final double percent;
UserInfo(User user, int count, double percent) {
this.user = user;
this.count = count;
this.percent = percent;
}
@Override
public int compareTo(UserInfo o) {
if (count < o.count)
return 1;
if (count > o.count)
return -1;
if (user == null || user.getName() == null)
return 1;
if (o.user == null || o.user.getName() == null)
return -1;
return user.getName().compareTo(o.user.getName());
}
public String getName() {
if (user == null)
return tr("<new object>");
return user.getName();
}
}
/**
* The table model for the users
*
*/
static class UserTableModel extends DefaultTableModel {
private final transient List<UserInfo> data;
UserTableModel() {
setColumnIdentifiers(new String[]{tr("Author"), tr("# Objects"), "%"});
data = new ArrayList<>();
}
protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) {
Map<User, Integer> ret = new HashMap<>();
if (primitives == null || primitives.isEmpty())
return ret;
for (OsmPrimitive primitive: primitives) {
if (ret.containsKey(primitive.getUser())) {
ret.put(primitive.getUser(), ret.get(primitive.getUser()) + 1);
} else {
ret.put(primitive.getUser(), 1);
}
}
return ret;
}
public void populate(Collection<? extends OsmPrimitive> primitives) {
Map<User, Integer> statistics = computeStatistics(primitives);
data.clear();
if (primitives != null) {
for (Map.Entry<User, Integer> entry: statistics.entrySet()) {
data.add(new UserInfo(entry.getKey(), entry.getValue(), (double) entry.getValue() / (double) primitives.size()));
}
}
Collections.sort(data);
GuiHelper.runInEDTAndWait(this::fireTableDataChanged);
}
@Override
public int getRowCount() {
if (data == null)
return 0;
return data.size();
}
@Override
public Object getValueAt(int row, int column) {
UserInfo info = data.get(row);
switch(column) {
case 0: /* author */ return info.getName() == null ? "" : info.getName();
case 1: /* count */ return info.count;
case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent);
default: return null;
}
}
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
public void selectPrimitivesOwnedBy(int... rows) {
Set<User> users = new HashSet<>();
for (int index: rows) {
users.add(data.get(index).user);
}
Collection<OsmPrimitive> selected = Main.getLayerManager().getEditDataSet().getAllSelected();
Collection<OsmPrimitive> byUser = new LinkedList<>();
for (OsmPrimitive p : selected) {
if (users.contains(p.getUser())) {
byUser.add(p);
}
}
Main.getLayerManager().getEditDataSet().setSelected(byUser);
}
public List<User> getSelectedUsers(int... rows) {
List<User> ret = new LinkedList<>();
if (rows == null || rows.length == 0)
return ret;
for (int row: rows) {
if (data.get(row).user == null) {
continue;
}
ret.add(data.get(row).user);
}
return ret;
}
}
}