001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.event.ActionEvent; 008import java.awt.event.KeyEvent; 009import java.awt.event.MouseAdapter; 010import java.awt.event.MouseEvent; 011import java.text.NumberFormat; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.Iterator; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023 024import javax.swing.AbstractAction; 025import javax.swing.JTable; 026import javax.swing.ListSelectionModel; 027import javax.swing.event.ListSelectionEvent; 028import javax.swing.event.ListSelectionListener; 029import javax.swing.table.DefaultTableModel; 030 031import org.openstreetmap.josm.Main; 032import org.openstreetmap.josm.actions.AbstractInfoAction; 033import org.openstreetmap.josm.data.SelectionChangedListener; 034import org.openstreetmap.josm.data.osm.DataSet; 035import org.openstreetmap.josm.data.osm.OsmPrimitive; 036import org.openstreetmap.josm.data.osm.User; 037import org.openstreetmap.josm.gui.SideButton; 038import org.openstreetmap.josm.gui.layer.Layer; 039import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 040import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 041import org.openstreetmap.josm.gui.layer.OsmDataLayer; 042import org.openstreetmap.josm.gui.util.GuiHelper; 043import org.openstreetmap.josm.tools.ImageProvider; 044import org.openstreetmap.josm.tools.OpenBrowser; 045import org.openstreetmap.josm.tools.Shortcut; 046import org.openstreetmap.josm.tools.Utils; 047 048/** 049 * Displays a dialog with all users who have last edited something in the 050 * selection area, along with the number of objects. 051 * 052 */ 053public class UserListDialog extends ToggleDialog implements SelectionChangedListener, ActiveLayerChangeListener { 054 055 /** 056 * The display list. 057 */ 058 private JTable userTable; 059 private UserTableModel model; 060 private SelectUsersPrimitivesAction selectionUsersPrimitivesAction; 061 062 /** 063 * Constructs a new {@code UserListDialog}. 064 */ 065 public UserListDialog() { 066 super(tr("Authors"), "userlist", tr("Open a list of people working on the selected objects."), 067 Shortcut.registerShortcut("subwindow:authors", tr("Toggle: {0}", tr("Authors")), KeyEvent.VK_A, Shortcut.ALT_SHIFT), 150); 068 build(); 069 } 070 071 @Override 072 public void showNotify() { 073 DataSet.addSelectionListener(this); 074 Main.getLayerManager().addActiveLayerChangeListener(this); 075 } 076 077 @Override 078 public void hideNotify() { 079 Main.getLayerManager().removeActiveLayerChangeListener(this); 080 DataSet.removeSelectionListener(this); 081 } 082 083 protected void build() { 084 model = new UserTableModel(); 085 userTable = new JTable(model); 086 userTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 087 userTable.addMouseListener(new DoubleClickAdapter()); 088 089 // -- select users primitives action 090 // 091 selectionUsersPrimitivesAction = new SelectUsersPrimitivesAction(); 092 userTable.getSelectionModel().addListSelectionListener(selectionUsersPrimitivesAction); 093 094 // -- info action 095 // 096 ShowUserInfoAction showUserInfoAction = new ShowUserInfoAction(); 097 userTable.getSelectionModel().addListSelectionListener(showUserInfoAction); 098 099 createLayout(userTable, true, Arrays.asList(new SideButton[] { 100 new SideButton(selectionUsersPrimitivesAction), 101 new SideButton(showUserInfoAction) 102 })); 103 } 104 105 /** 106 * Called when the selection in the dataset changed. 107 * @param newSelection The new selection array. 108 */ 109 @Override 110 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 111 refresh(newSelection); 112 } 113 114 @Override 115 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) { 116 Layer activeLayer = e.getSource().getActiveLayer(); 117 if (activeLayer instanceof OsmDataLayer) { 118 refresh(((OsmDataLayer) activeLayer).data.getAllSelected()); 119 } else { 120 refresh(null); 121 } 122 } 123 124 /** 125 * Refreshes user list from given collection of OSM primitives. 126 * @param fromPrimitives OSM primitives to fetch users from 127 */ 128 public void refresh(Collection<? extends OsmPrimitive> fromPrimitives) { 129 model.populate(fromPrimitives); 130 GuiHelper.runInEDT(() -> { 131 if (model.getRowCount() != 0) { 132 setTitle(trn("{0} Author", "{0} Authors", model.getRowCount(), model.getRowCount())); 133 } else { 134 setTitle(tr("Authors")); 135 } 136 }); 137 } 138 139 @Override 140 public void showDialog() { 141 super.showDialog(); 142 Layer layer = Main.getLayerManager().getActiveLayer(); 143 if (layer instanceof OsmDataLayer) { 144 refresh(((OsmDataLayer) layer).data.getAllSelected()); 145 } 146 } 147 148 class SelectUsersPrimitivesAction extends AbstractAction implements ListSelectionListener { 149 150 /** 151 * Constructs a new {@code SelectUsersPrimitivesAction}. 152 */ 153 SelectUsersPrimitivesAction() { 154 putValue(NAME, tr("Select")); 155 putValue(SHORT_DESCRIPTION, tr("Select objects submitted by this user")); 156 new ImageProvider("dialogs", "select").getResource().attachImageIcon(this, true); 157 updateEnabledState(); 158 } 159 160 public void select() { 161 int[] indexes = userTable.getSelectedRows(); 162 if (indexes.length == 0) 163 return; 164 model.selectPrimitivesOwnedBy(userTable.getSelectedRows()); 165 } 166 167 @Override 168 public void actionPerformed(ActionEvent e) { 169 select(); 170 } 171 172 protected void updateEnabledState() { 173 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 174 } 175 176 @Override 177 public void valueChanged(ListSelectionEvent e) { 178 updateEnabledState(); 179 } 180 } 181 182 /** 183 * Action for launching the info page of a user. 184 */ 185 class ShowUserInfoAction extends AbstractInfoAction implements ListSelectionListener { 186 187 ShowUserInfoAction() { 188 super(false); 189 putValue(NAME, tr("Show info")); 190 putValue(SHORT_DESCRIPTION, tr("Launches a browser with information about the user")); 191 new ImageProvider("help/internet").getResource().attachImageIcon(this, true); 192 updateEnabledState(); 193 } 194 195 @Override 196 public void actionPerformed(ActionEvent e) { 197 int[] rows = userTable.getSelectedRows(); 198 if (rows.length == 0) 199 return; 200 List<User> users = model.getSelectedUsers(rows); 201 if (users.isEmpty()) 202 return; 203 if (users.size() > 10) { 204 Main.warn(tr("Only launching info browsers for the first {0} of {1} selected users", 10, users.size())); 205 } 206 int num = Math.min(10, users.size()); 207 Iterator<User> it = users.iterator(); 208 while (it.hasNext() && num > 0) { 209 String url = createInfoUrl(it.next()); 210 if (url == null) { 211 break; 212 } 213 OpenBrowser.displayUrl(url); 214 num--; 215 } 216 } 217 218 @Override 219 protected String createInfoUrl(Object infoObject) { 220 if (infoObject instanceof User) { 221 User user = (User) infoObject; 222 return Main.getBaseUserUrl() + '/' + Utils.encodeUrl(user.getName()).replaceAll("\\+", "%20"); 223 } else { 224 return null; 225 } 226 } 227 228 @Override 229 protected void updateEnabledState() { 230 setEnabled(userTable != null && userTable.getSelectedRowCount() > 0); 231 } 232 233 @Override 234 public void valueChanged(ListSelectionEvent e) { 235 updateEnabledState(); 236 } 237 } 238 239 class DoubleClickAdapter extends MouseAdapter { 240 @Override 241 public void mouseClicked(MouseEvent e) { 242 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { 243 selectionUsersPrimitivesAction.select(); 244 } 245 } 246 } 247 248 /** 249 * Action for selecting the primitives contributed by the currently selected users. 250 * 251 */ 252 private static class UserInfo implements Comparable<UserInfo> { 253 public final User user; 254 public final int count; 255 public final double percent; 256 257 UserInfo(User user, int count, double percent) { 258 this.user = user; 259 this.count = count; 260 this.percent = percent; 261 } 262 263 @Override 264 public int compareTo(UserInfo o) { 265 if (count < o.count) 266 return 1; 267 if (count > o.count) 268 return -1; 269 if (user == null || user.getName() == null) 270 return 1; 271 if (o.user == null || o.user.getName() == null) 272 return -1; 273 return user.getName().compareTo(o.user.getName()); 274 } 275 276 public String getName() { 277 if (user == null) 278 return tr("<new object>"); 279 return user.getName(); 280 } 281 } 282 283 /** 284 * The table model for the users 285 * 286 */ 287 static class UserTableModel extends DefaultTableModel { 288 private final transient List<UserInfo> data; 289 290 UserTableModel() { 291 setColumnIdentifiers(new String[]{tr("Author"), tr("# Objects"), "%"}); 292 data = new ArrayList<>(); 293 } 294 295 protected Map<User, Integer> computeStatistics(Collection<? extends OsmPrimitive> primitives) { 296 Map<User, Integer> ret = new HashMap<>(); 297 if (primitives == null || primitives.isEmpty()) 298 return ret; 299 for (OsmPrimitive primitive: primitives) { 300 if (ret.containsKey(primitive.getUser())) { 301 ret.put(primitive.getUser(), ret.get(primitive.getUser()) + 1); 302 } else { 303 ret.put(primitive.getUser(), 1); 304 } 305 } 306 return ret; 307 } 308 309 public void populate(Collection<? extends OsmPrimitive> primitives) { 310 Map<User, Integer> statistics = computeStatistics(primitives); 311 data.clear(); 312 if (primitives != null) { 313 for (Map.Entry<User, Integer> entry: statistics.entrySet()) { 314 data.add(new UserInfo(entry.getKey(), entry.getValue(), (double) entry.getValue() / (double) primitives.size())); 315 } 316 } 317 Collections.sort(data); 318 GuiHelper.runInEDTAndWait(this::fireTableDataChanged); 319 } 320 321 @Override 322 public int getRowCount() { 323 if (data == null) 324 return 0; 325 return data.size(); 326 } 327 328 @Override 329 public Object getValueAt(int row, int column) { 330 UserInfo info = data.get(row); 331 switch(column) { 332 case 0: /* author */ return info.getName() == null ? "" : info.getName(); 333 case 1: /* count */ return info.count; 334 case 2: /* percent */ return NumberFormat.getPercentInstance().format(info.percent); 335 default: return null; 336 } 337 } 338 339 @Override 340 public boolean isCellEditable(int row, int column) { 341 return false; 342 } 343 344 public void selectPrimitivesOwnedBy(int ... rows) { 345 Set<User> users = new HashSet<>(); 346 for (int index: rows) { 347 users.add(data.get(index).user); 348 } 349 Collection<OsmPrimitive> selected = Main.getLayerManager().getEditDataSet().getAllSelected(); 350 Collection<OsmPrimitive> byUser = new LinkedList<>(); 351 for (OsmPrimitive p : selected) { 352 if (users.contains(p.getUser())) { 353 byUser.add(p); 354 } 355 } 356 Main.getLayerManager().getEditDataSet().setSelected(byUser); 357 } 358 359 public List<User> getSelectedUsers(int ... rows) { 360 List<User> ret = new LinkedList<>(); 361 if (rows == null || rows.length == 0) 362 return ret; 363 for (int row: rows) { 364 if (data.get(row).user == null) { 365 continue; 366 } 367 ret.add(data.get(row).user); 368 } 369 return ret; 370 } 371 } 372}